Botguide för Polymarket · Kapitel 24 av 32
Polymarket Perps har ett fullständigt öppet API med officiella SDK:er för TypeScript och Python. I det här kapitlet bygger vi en bot mot det riktiga gränssnittet: autentisering med en proxy-handelsnyckel, marknadsdata via REST och WebSocket, order med take-profit och stop-loss samt de riskkontroller som hindrar en bot med hävstång från att tömma kontot.
Det här tar kapitlet upp
- Viktiga siffror för dig som bygger botar
- Hur Perps-API:t är uppbyggt
- Steg 1: installera SDK:n och öppna en session
- Steg 2: läs marknadsdata via REST och WebSocket
- Steg 3: lägg order med take-profit och stop-loss
- Steg 4: ett botskelett med riskkontroller i koden
- Fel, dödmansgreppet, regler för botar och hosting
Varje SDK-metod och endpoint här kommer från Perps-dokumentationen hos Polymarket (docs.polymarket.com/perps, läst 6 november 2026). Vår egen logik är markerad i kommentarerna, och strategifunktionen är en tydligt märkt platshållare.
Viktiga siffror för dig som bygger botar (november 2026)
| Punkt | Värde |
|---|---|
| REST-bas-URL | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| TypeScript SDK | @polymarket/client (med viem för signering) |
| Python SDK | polymarket-client (asynkron) |
| Öppen data | /v1/info/*, ingen autentisering |
| Privata läsningar | /v1/account/* med headrarna polymarket-proxy och polymarket-secret |
| Signerade skrivningar | /v1/trade/*, EIP-712-signatur från proxynyckeln, giltig i 5 minuter |
| Sessionsuppgifter | Löper ut efter en vecka som standard |
| Minsta order | $10 i nominellt värde; minsta insättning 10 pUSD |
| Största marknadsorder | $1 miljon på marknaderna med 50x och 20x, $500 000 på de flesta andra, $50 000 på mynt med 3x och 5x |
| Format för pris och storlek | Decimalsträngar, högst marknadens antal decimaler och 5 värdesiffror |
| Högsta hävstång | 50x (BTC, ETH, USA500, US100), annars 20x, 10x, 5x eller 3x beroende på marknad, lägre för stora positioner |
| Avgifter (grundnivå) | Taker 0,040 %, maker 0,0125 %; ANT till halva priset |
| Finansieringsavgift | Varje timme, högst +/-4 % per timme |
| TP/SL-triggers | Markpris, inte senaste affär |
| WebSocket keepalive | Ping ungefär var 25:e sekund; betrakta anslutningen som död efter cirka 65 sekunders tystnad |
| Tickerns aktualitet | Upp till 10 sekunder gammal |
Hur Perps-API:t är uppbyggt
Perps är en hybridbörs: matchningen sker offchain, medan förvaring och avräkning sker på Polygon. Bara insättningar och uttag är onchain, så det kostar ingen gas per order. API:t har tre lager:
- Öppen data (
/v1/info/): instrument, tickers, orderbok, candles, affärer, finansieringsavgift, avgifter. Ingen nyckel krävs och ingen geografisk blockering. - Privata läsningar (
/v1/account/): portfölj, saldon, order, avslut, konfiguration. - Signerade skrivningar (
/v1/trade/): order, makuleringar, hävstång, isolerad marginal, automatisk makulering. Varje skrivning kodas med MessagePack, hashas med keccak256 och signeras som EIP-712 typed data av proxynyckeln.
SDK:erna sköter signeringen åt dig. Det finns också en OpenAPI-specifikation (/api-spec/perps-openapi.json på dokumentationssajten) och en AsyncAPI-specifikation för WebSocket.
BTC-PERP. Det skarpa API:t returnerar BTC-USD, SP500-USD (USA500), NAS100-USD (US100), ANT-USD och så vidare. Hämta instrumentlistan, matcha mot det skarpa fältet symbol och spara det numeriska id. Kopiera inte ID:n från exemplen: i skarp drift är BTC 6 och SP500 1.Steg 1: installera SDK:n och öppna en session
Installera med pip install polymarket-client eller npm install @polymarket/client@latest viem. Boten behöver din plånboksadress på Polymarket och den privata nyckeln för den signerare som äger Perps-kontot (kontot är knutet till signerarens EOA, inte till Safe). När du öppnar en session signerar du en gång för att godkänna en ny proxy-handelsnyckel, som sedan signerar varje order, så att nyckeln som håller dina pengar aldrig handlar. Spara session.credentials krypterat så att du kan återuppta senare.
import asyncio
import os
from datetime import timedelta
from polymarket import AsyncSecureClient
async def main():
client = await AsyncSecureClient.create(
private_key=os.environ["PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
)
# One signature creates a proxy trading key. Orders are signed by the proxy,
# not by your wallet key. Credentials expire after one week by default.
session = await client.open_perps_session(
expires_in=timedelta(days=1),
label="perps-bot",
)
stored = session.credentials.model_dump(mode="json") # keep it encrypted
instruments = await client.fetch_perps_instruments()
btc = next(i for i in instruments if i.symbol == "BTC-USD")
print(btc.id, btc.max_leverage, btc.min_notional, btc.price_decimals)
asyncio.run(main())
import { createSecureClient } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";
const client = await createSecureClient({
wallet: process.env.POLYMARKET_WALLET_ADDRESS!,
signer: privateKey(process.env.PRIVATE_KEY!),
});
// One signature creates a proxy trading key. Orders are signed by the proxy,
// not by your wallet key. Credentials expire after one week by default.
const session = await client.openPerpsSession({
expiresIn: 24 * 60 * 60 * 1000,
label: "perps-bot",
});
const stored = session.credentials; // keep it encrypted
const instruments = await client.fetchPerpsInstruments();
const btc = instruments.find((i) => i.symbol === "BTC-USD");
if (!btc) throw new Error("BTC-USD not listed");
console.log(btc.id, btc.maxLeverage, btc.minNotional, btc.priceDecimals);
Insättning från kod (deposit_to_perps / depositToPerps) kräver dessutom en Relayer-API-nyckel från polymarket.com under Settings, API Keys. Enklare och säkrare: finansiera kontot i webbappen och håll boten borta från penningförflyttningar. Sedan 5 november 2026 kan konton med en adressbok för uttag bara ta ut till sparade adresser. Använd en skrivskyddad nyckel för dashboards (en proxy märkt read-only:, som vägrar signerade skrivningar), aldrig handelsnyckeln. Hur du förvarar nycklar går vi igenom i vårt kapitel om plånböcker och autentisering.
Steg 2: läs marknadsdata via REST och WebSocket
Tickern har tre priser: index (verkligt värde från källor som Pyth, Chainlink Data Streams och Hyperliquid), mark (används för PnL, marginal, likvidation och TP/SL) och last (endast utförande), plus finansieringsavgiften och tidpunkten för nästa finansieringsavgift. Basera signaler på markpris eller index.
# Inside your async main(), after Step 1 (btc is the BTC-USD instrument)
from polymarket import AsyncPublicClient
from polymarket.streams import PerpsBboSpec
client = AsyncPublicClient()
ticker = await client.fetch_perps_ticker(instrument_id=btc.id)
print(ticker.mark_price, ticker.index_price, ticker.funding_rate)
stream = await client.subscribe(PerpsBboSpec(instrument_id=btc.id))
async for event in stream:
print(event.payload.bid_price, event.payload.ask_price)
// After Step 1 (btc is the BTC-USD instrument)
import { createPublicClient } from "@polymarket/client";
const client = createPublicClient();
const ticker = await client.fetchPerpsTicker({ instrumentId: btc.id });
console.log(ticker.markPrice, ticker.indexPrice, ticker.fundingRate);
const stream = await client.subscribe([{ topic: "perps.bbo", instrumentId: btc.id }]);
for await (const event of stream) {
console.log(event.payload.bidPrice, event.payload.askPrice);
}
Utan SDK:n prenumererar du direkt på WebSocket med {"id": 1, "req": "sub", "chs": ["bbo::6"]}. Kanalerna omfattar bland annat bbo::<id>, book::<id>, trades::<id>, tickers::<id> och klines::<id>::1m. Varje uppdatering har ett sekvensnummer sq: efter ett glapp eller en återanslutning läser du in tillståndet igen via REST. För backtester bläddrar list_perps_candles / listPerpsCandles igenom historiken (REST-endpointen returnerar upp till 1 000 candles per anrop).
Steg 3: lägg order med take-profit och stop-loss
En order har en side (BUY eller SELL), en quantity i enheter av basvalutan, ett valfritt price i pUSD och en giltighetstid (gtc, ioc eller fok). Post-only kräver GTC; reduce-only kan bara minska en position. Det finns ingen flagga för lång eller kort: utan position öppnar BUY en lång position och SELL en kort.
En bracket-order kopplar en take-profit och en stop-loss till ingångsordern. De ligger vilande tills ingångsordern är helt fylld och makuleras om ingångsordern makuleras eller bara fylls delvis. Priserna nedan är hypotetiska.
# Inside your async main(), after Step 1 (btc is the BTC-USD instrument)
from polymarket import PerpsTpSlTrigger
result = await session.place_order(
instrument_id=btc.id,
side="BUY",
price="100000",
quantity="0.01",
time_in_force="gtc",
take_profit=PerpsTpSlTrigger(trigger_price="104000"),
stop_loss=PerpsTpSlTrigger(trigger_price="98000"),
)
print(result.order.id, result.order.status)
// After Step 1 (btc is the BTC-USD instrument)
import { OrderSide, PerpsTimeInForce } from "@polymarket/client";
const result = await session.placeOrder({
instrumentId: btc.id,
side: OrderSide.BUY,
price: "100000",
quantity: "0.01",
timeInForce: PerpsTimeInForce.GTC,
takeProfit: { triggerPrice: "104000" },
stopLoss: { triggerPrice: "98000" },
});
console.log(result.order.id, result.tpSl.stopLoss?.orderId);
För en öppen position lägger place_position_tp_sl / placePositionTpSl till triggers som stänger hela positionen. Regler:
- Triggers bevakar markpriset och skickar en reduce-only-order. Positionens TP/SL stänger till marknadspris, så räkna med slippage i tunna orderböcker.
- En take-profit och en stop-loss per position och marknad. Att flytta en stop innebär att makulera och sedan skapa en ny, vilket inte är atomärt.
- En trigger som markpriset redan har passerat avvisas, och limitpriser som ligger för långt från markpriset nekas.
Vill du testa orderflödet för hand innan du automatiserar det, öppna BTC-marknaden på Polymarket Perps och lägg minsta tillåtna order. När du har sett avslut, avgifter och positionspanelen blir API-svaren lättare att läsa.
Steg 4: ett botskelett med riskkontroller i koden
Python-skelettet nedan använder bara dokumenterade SDK-anrop; risklogiken är vår egen. Tre skyddslager:
- Tak för hävstång. Hävstången sätts till 3x eller marknadens maximum, beroende på vilket som är lägst, med isolerad marginal, och storlekar som hamnar i en risknivå med lägre tak nekas.
- Nödstopp. En HALT-fil, en daglig förlustgräns på 5 % och avslut vid likvidation. Nödstoppet stänger positioner med reduce-only IOC-order, makulerar återstående order och kontrollerar sedan att båda är borta.
- En stop på varje position. Fill-or-kill-ingångar innebär inga oskyddade delfyllnader. En stop och en take-profit för positionen läggs efter varje avslut; om de avvisas stängs positionen direkt.
import asyncio
import logging
import os
from datetime import timedelta
from decimal import Decimal, ROUND_DOWN
from polymarket import AsyncSecureClient, PerpsPositionTpSlTrigger
SYMBOL = "BTC-USD" # live API symbol; docs examples say "BTC-PERP"
LEVERAGE_CAP = 3 # our cap, far below the 50x BTC allows
RISK_PER_TRADE = Decimal("20") # pUSD we accept losing if the stop fills
STOP_PCT = Decimal("0.02") # stop 2% from entry
TAKE_PCT = Decimal("0.04") # take profit 4% from entry
MAX_NOTIONAL = Decimal("2000") # hard cap per position, in pUSD
MAX_SLIPPAGE = Decimal("0.002") # worst entry price: ask + 0.2%
MAX_FUNDING = Decimal("0.0005") # skip longs if funding > 0.05% per hour
DAILY_LOSS_LIMIT = Decimal("0.05") # kill switch at -5% of start equity
HALT_FILE = "/opt/perps-bot/HALT" # touch this file to stop the bot
def fit(value: Decimal, decimals: int, sig: int = 5) -> str:
"""Round down to the market's decimals and to 5 significant figures."""
v = value.quantize(Decimal(1).scaleb(-decimals), rounding=ROUND_DOWN)
if v != 0:
step = Decimal(1).scaleb(v.adjusted() - sig + 1)
if step > Decimal(1).scaleb(-decimals):
v = v.quantize(step, rounding=ROUND_DOWN)
return format(v, "f")
def tier_cap(inst, notional: Decimal) -> int:
"""Max leverage allowed for this notional by the market's risk tiers."""
cap = inst.max_leverage
for tier in inst.risk_tiers:
if notional >= Decimal(tier.lower_bound):
cap = tier.max_leverage
return cap
async def close_all(session):
"""Close every position with reduce-only IOC orders, then cancel the rest."""
portfolio = await session.fetch_portfolio()
for pos in portfolio.positions:
if pos.size != 0:
await session.place_order(
instrument_id=pos.instrument_id,
side="SELL" if pos.size > 0 else "BUY",
quantity=abs(pos.size),
time_in_force="ioc",
reduce_only=True,
)
await session.cancel_all_orders()
async def kill_switch(session, reason: str):
logging.error("KILL SWITCH: %s", reason)
for attempt in range(3):
await close_all(session)
portfolio = await session.fetch_portfolio()
open_orders = await session.fetch_open_orders()
if not open_orders and all(p.size == 0 for p in portfolio.positions):
return
await asyncio.sleep(2)
logging.critical("Positions still open after kill switch: act manually")
async def open_long(client, session, inst):
ticker = await client.fetch_perps_ticker(instrument_id=inst.id)
if Decimal(ticker.funding_rate) > MAX_FUNDING:
return # longs would pay too much
book = await client.fetch_perps_book(instrument_id=inst.id, depth=10)
if not book.asks:
return
ask = Decimal(book.asks[0].price)
stop = ask * (1 - STOP_PCT)
qty = RISK_PER_TRADE / (ask - stop) # size from the stop, not leverage
qty = min(qty, MAX_NOTIONAL / ask)
notional = qty * ask
if notional < Decimal(inst.min_notional):
return
if tier_cap(inst, notional) < LEVERAGE_CAP:
return # size would break our cap
entry = await session.place_order(
instrument_id=inst.id,
side="BUY",
price=fit(ask * (1 + MAX_SLIPPAGE), inst.price_decimals),
quantity=fit(qty, inst.quantity_decimals),
time_in_force="fok", # all or nothing
)
if entry.order.status != "filled":
return
try: # stop on the whole position
await session.place_position_tp_sl(
instrument_id=inst.id,
take_profit=PerpsPositionTpSlTrigger(
trigger_price=fit(ask * (1 + TAKE_PCT), inst.price_decimals)),
stop_loss=PerpsPositionTpSlTrigger(
trigger_price=fit(stop, inst.price_decimals)),
)
except Exception:
logging.exception("TP/SL rejected: closing the unprotected position")
await close_all(session)
async def my_signal(client, inst) -> bool:
"""PLACEHOLDER: your strategy goes here. Not part of the SDK."""
return False
async def main():
logging.basicConfig(level=logging.INFO)
client = await AsyncSecureClient.create(
private_key=os.environ["PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
)
session = await client.open_perps_session(
expires_in=timedelta(days=1), label="perps-bot")
inst = next(i for i in await client.fetch_perps_instruments()
if i.symbol == SYMBOL)
# Control 1: leverage cap and isolated margin, set before any order
await session.update_leverage(
instrument_id=inst.id,
leverage=min(LEVERAGE_CAP, inst.max_leverage),
cross_margin=False,
)
# Field names below follow the documented portfolio JSON
# (margin.total_account_value, in_liquidation). Check them in your SDK.
start_equity = Decimal((await session.fetch_portfolio()).margin.total_account_value)
while True:
portfolio = await session.fetch_portfolio()
equity = Decimal(portfolio.margin.total_account_value)
# Control 2: kill switches
if os.path.exists(HALT_FILE):
await kill_switch(session, "HALT file found")
break
if equity < start_equity * (1 - DAILY_LOSS_LIMIT):
await kill_switch(session, f"daily loss limit hit: {equity}")
break
if portfolio.in_liquidation:
await kill_switch(session, "account in liquidation")
break
in_position = any(p.instrument_id == inst.id and p.size != 0
for p in portfolio.positions)
if not in_position and await my_signal(client, inst):
await open_long(client, session, inst) # Control 3 lives here
await asyncio.sleep(5)
asyncio.run(main())
Matematiken bakom storleken. Storleken bestäms av avståndet till stoppen, inte av hävstången. Med ett hypotetiskt säljpris för BTC på 100 000 pUSD och en stop på 2 % ligger stoppen på 98 000. Risken per enhet är 2 000, så kvantiteten blir 20 / 2 000 = 0,01 BTC, ett nominellt värde på 1 000 pUSD. Vid 3x kräver det cirka 333 pUSD i marginal. Underhållsmarginalen för BTC är 1 % (0,5 / 50x), så likvidationspriset för den långa positionen blir 100 000 x (1 - 1/3) / (1 - 0,01) = cirka 67 340, ett fall på 32,7 %. Stoppen på -2 % utlöses långt innan dess. Om stoppen fylls på sitt triggerpris blir förlusten 20 pUSD plus avgifter: 0,40 för att öppna och cirka 0,39 för att stänga med takeravgiften 0,040 %, alltså cirka 20,79 pUSD före slippage. Hävstången avgör bara hur mycket marginal som binds. Den ändrar inte förlusten vid stoppen.
Innan du går skarpt bör du lägga till en daglig gräns för antal affärer, larm vid avslut och nödstopp samt en avstämning vid uppstart (fetch_portfolio, fetch_open_orders, fetch_orders, list_fills). Se våra kapitel om riskkod och övningshandel, guiden om hävstång och likvidation på Perps och guiden om finansieringsavgifter på Perps.
Fel, nya försök och dödmansgreppet
503 service_unavailablebevisar inte att anropet misslyckades. Gör om läsningar och ändringar av hävstång med backoff och respekteraRetry-After. För signerade saldoändringar (marginal, uttag) skickar du bara exakt samma ursprungliga signerade anrop igen; en ny signatur blir en andra justering.signature expired: äldre än 5 minuter. Håll klockan synkad.insufficient_margin/invalid_leverage: kontrollerna räknar med din position plus alla vilande order.message_rate_limited: WebSocket-kvoten är förbrukad. Skrivskyddade nycklar får 1 000 meddelanden per minut per IP (burst 50) och fyra anslutningar per konto och gateway; se sidan Rate Limits i dokumentationen för övriga gränser.- Formatfel: för många decimaler eller fler än 5 värdesiffror. Därav
fit().
Automatisk makulering makulerar alla öppna order om din bot slutar förnya den. Tidsgränsen måste ligga minst 5 sekunder fram i tiden, och antalet utlösningar per UTC-dygn är begränsat, så avaktivera den vid en kontrollerad avstängning.
// For quoting bots: cancel all resting orders if this process dies
await session.armAutoCancel({ cancelAt: Date.now() + 60_000 });
const rearm = setInterval(() => {
// A missed re-arm is fail-safe: the armed switch still fires.
session.armAutoCancel({ cancelAt: Date.now() + 60_000 }).catch(console.error);
}, 20_000);
// On a clean shutdown
clearInterval(rearm);
await session.disarmAutoCancel();
Tävlingar, builder codes och reglerna för botar
Dokumentationen anger ”trading bots that react to market signals” (handelsbotar som reagerar på marknadssignaler) och market makers som användningsområden för API:t. Två uppsättningar regler gäller.
- Tävlingar. Wash trading, handel med sig själv, flera konton, handel främst för volymens skull och ”bots, scripts, or automated means intended to manipulate the Program” är förbjudna. En bot som handlar på riktigt är okej; en som pumpar upp volym kan diskvalificeras.
- Builder codes. En app som handlar åt andra användare kan lägga till en avgift på upp till 10 baspunkter med varje handlares godkännande. Den måste kontrollera varje användares plats och blockera order från begränsade jurisdiktioner, inte bara varna.
Likviditetsbelöningar för makers och tävlingar går vi igenom i guiden till Polymarket Perps.
Hosting av boten
En bot med hävstång ska inte köras på en laptop. Den måste hålla sin WebSocket vid liv, reagera på avslut inom sekunder och klara ett avbrott i hemmets internet. Kör den på en VPS under systemd med automatisk omstart, förvara nyckeln i en miljöfil som bara botens användare kan läsa och synka klockan. Det är inte tillåtet att lägga order från begränsade platser, så hosta inte i USA eller Kanada, och använd aldrig en VPS för att kringgå en blockering som gäller dig. Vi kör våra egna botar på TradingVPS (affiliatelänk: vi får en liten provision och du betalar samma pris). Vårt kapitel om VPS-hosting jämför den med andra leverantörer när det gäller latens, pris och regioner.
Efter övningshandeln finansierar du kontot med ett litet belopp från sidan för Polymarket Perps och kör med minsta storlek i en vecka innan du skalar upp.
Viktigaste slutsatserna
- Använd de officiella SDK:erna,
@polymarket/clientellerpolymarket-client. Din plånbok signerar en gång; en proxynyckel signerar order. - Matcha marknader mot den skarpa symbolen (
BTC-USD, inteBTC-PERP) och spara det numeriska ID:t. - Bestäm storleken utifrån avståndet till stoppen, sätt tak för hävstången i koden, använd isolerad marginal och lägg en stop på varje position direkt efter avslutet.
- TP/SL utlöses på markpriset och stänger till marknadspris. Automatisk makulering kan ta bort stoppar.
- Botar är tillåtna, att manipulera tävlingar är det inte. Handla aldrig från ett begränsat land.





