Tutorial boți Polymarket · Capitolul 24 din 32
Polymarket Perps are un API public complet, cu SDK-uri oficiale pentru TypeScript și Python. În acest capitol construim un bot pe interfața reală: autentificare cu o cheie proxy de tranzacționare, date de piață prin REST și WebSocket, ordine cu take-profit și stop-loss, plus controalele de risc care împiedică un bot cu efect de levier să golească contul.
Ce acoperă acest capitol
- Cifrele cheie pentru cei care construiesc boți
- Cum este construit API-ul Perps
- Pasul 1: instalează SDK-ul și deschide o sesiune
- Pasul 2: citește datele de piață prin REST și WebSocket
- Pasul 3: plasează ordine cu take-profit și stop-loss
- Pasul 4: un schelet de bot cu controale de risc în cod
- Erori, mecanismul dead man's switch, reguli pentru boți și găzduire
Fiecare metodă SDK și fiecare endpoint de aici provine din documentația Perps a Polymarket (docs.polymarket.com/perps, consultată pe 6 noiembrie 2026). Logica noastră proprie este marcată în comentarii, iar funcția de strategie este un placeholder etichetat ca atare.
Cifrele cheie pentru cei care construiesc boți (noiembrie 2026)
| Element | Valoare |
|---|---|
| URL de bază REST | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| SDK TypeScript | @polymarket/client (cu viem pentru semnare) |
| SDK Python | polymarket-client (async) |
| Date publice | /v1/info/*, fără autentificare |
| Citiri private | /v1/account/* cu antetele polymarket-proxy și polymarket-secret |
| Scrieri semnate | /v1/trade/*, semnătură EIP-712 cu cheia proxy, valabilă 5 minute |
| Credențialele sesiunii | Expiră implicit după o săptămână |
| Ordin minim | $10 valoare nominală; depunere minimă 10 pUSD |
| Cel mai mare ordin la piață | $1M pe piețele cu 50x și 20x, $500K pe majoritatea celorlalte, $50K pe monedele cu 3x și 5x |
| Formatul prețului și al cantității | Șiruri zecimale, cel mult numărul de zecimale al pieței și 5 cifre semnificative |
| Efect de levier maxim | 50x (BTC, ETH, USA500, US100), altfel 20x, 10x, 5x sau 3x în funcție de piață, mai mic pentru pozițiile mari |
| Comisioane (nivelul de bază) | Taker 0,040%, maker 0,0125%; ANT la jumătate de preț |
| Finanțare | Orară, plafonată la +/-4% pe oră |
| Declanșatoare TP/SL | Prețul de marcare, nu ultima tranzacție |
| Keepalive WebSocket | Ping cam la fiecare 25 de secunde; consideră conexiunea moartă după aproximativ 65 de secunde de tăcere |
| Prospețimea ticker-ului | Până la 10 secunde vechime |
Cum este construit API-ul Perps
Perps este un exchange hibrid: potrivirea ordinelor are loc offchain, iar custodia și decontarea pe Polygon. Doar depunerile și retragerile sunt onchain, deci nu plătești gas pentru fiecare ordin. API-ul are trei niveluri:
- Date publice (
/v1/info/): instrumente, tickere, registrul de ordine, lumânări, tranzacții, finanțare, comisioane. Nu ai nevoie de cheie și nu există blocare geografică. - Citiri private (
/v1/account/): portofoliu, solduri, ordine, execuții, configurație. - Scrieri semnate (
/v1/trade/): ordine, anulări, efect de levier, marjă izolată, anulare automată. Fiecare scriere este codificată MessagePack, hash-uită cu keccak256 și semnată ca date tipizate EIP-712 de cheia proxy.
SDK-urile se ocupă de semnare în locul tău. Există și o specificație OpenAPI (/api-spec/perps-openapi.json pe site-ul documentației) și o specificație AsyncAPI pentru WebSocket.
BTC-PERP. API-ul live returnează BTC-USD, SP500-USD (USA500), NAS100-USD (US100), ANT-USD și așa mai departe. Preia lista de instrumente, potrivește symbol live și salvează id numeric. Nu copia ID-urile din exemple: live, BTC are 6, iar SP500 are 1.Pasul 1: instalează SDK-ul și deschide o sesiune
Instalează cu pip install polymarket-client sau npm install @polymarket/client@latest viem. Botul are nevoie de adresa portofelului tău Polymarket și de cheia privată a semnatarului care deține contul Perps (contul este legat de EOA-ul semnatar, nu de Safe). Deschiderea unei sesiuni semnează o singură dată pentru a autoriza o cheie proxy de tranzacționare nouă, care semnează apoi fiecare ordin, așa că cheia ta de finanțare nu tranzacționează niciodată. Salvează session.credentials criptat, ca să poți relua sesiunea mai târziu.
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);
Depunerea din cod (deposit_to_perps / depositToPerps) necesită și o cheie Relayer API din polymarket.com, Settings, API Keys. Mai simplu și mai sigur: alimentează contul din aplicația web și ține botul departe de mișcările de bani. Din 5 noiembrie 2026, conturile care folosesc agenda de adrese pentru retrageri pot retrage doar către adresele salvate. Pentru dashboard-uri folosește o cheie read-only (un proxy etichetat read-only:, care refuză scrierile semnate), niciodată cheia de tranzacționare. Păstrarea cheilor este explicată în capitolul despre portofele și autentificare.
Pasul 2: citește datele de piață prin REST și WebSocket
Ticker-ul are trei prețuri: index (valoarea justă din surse precum Pyth, Chainlink Data Streams și Hyperliquid), mark (prețul de marcare, folosit pentru PnL, marjă, lichidare și TP/SL) și last (doar pentru execuție), plus rata de finanțare și momentul următoarei finanțări. Bazează-ți semnalele pe prețul de marcare sau pe 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);
}
Fără SDK, abonează-te direct pe WebSocket cu {"id": 1, "req": "sub", "chs": ["bbo::6"]}. Canalele includ bbo::<id>, book::<id>, trades::<id>, tickers::<id> și klines::<id>::1m. Fiecare actualizare are un număr de secvență sq: după un gol sau o reconectare, recitește starea prin REST. Pentru backtesting, list_perps_candles / listPerpsCandles paginează istoricul (endpoint-ul REST returnează până la 1.000 de lumânări per apel).
Pasul 3: plasează ordine cu take-profit și stop-loss
Un ordin are un side (BUY sau SELL), o quantity în unități ale activului de bază, un price opțional în pUSD și o durată de valabilitate (gtc, ioc sau fok). Post-only necesită GTC; reduce-only poate doar să micșoreze o poziție. Nu există un indicator long sau short: fără poziție deschisă, BUY deschide un long, iar SELL deschide un short.
Un ordin bracket atașează intrării un take-profit și un stop-loss. Acestea rămân inactive până când intrarea este executată integral și sunt anulate dacă intrarea este anulată sau executată doar parțial. Prețurile de mai jos sunt ipotetice.
# 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);
Pentru o poziție deschisă, place_position_tp_sl / placePositionTpSl adaugă declanșatoare care închid întreaga poziție. Reguli:
- Declanșatoarele urmăresc prețul de marcare și trimit un ordin reduce-only. TP/SL pe poziție iese la piață, așa că te poți aștepta la slippage în registrele de ordine cu lichiditate redusă.
- Un singur take-profit și un singur stop-loss pe poziție pentru fiecare piață. Mutarea unui stop înseamnă anulare și apoi creare, operație care nu este atomică.
- Un declanșator pe care prețul de marcare l-a depășit deja este respins, iar prețurile limită prea îndepărtate de prețul de marcare sunt refuzate.
Ca să încerci manual fluxul ordinelor înainte să-l automatizezi, deschide piața BTC pe Polymarket Perps și plasează cel mai mic ordin permis. Când vezi execuțiile, comisioanele și panoul poziției, răspunsurile API devin mai ușor de citit.
Pasul 4: un schelet de bot cu controale de risc în cod
Scheletul Python de mai jos folosește doar apeluri SDK documentate; logica de risc este a noastră. Trei niveluri de protecție:
- Plafon pentru efectul de levier. Efectul de levier este setat la 3x sau la maximul pieței, oricare este mai mic, cu marjă izolată, iar dimensiunile care intră într-un nivel de risc cu plafon mai mic sunt refuzate.
- Kill switch-uri. Un fișier HALT, o limită de pierdere zilnică de 5% și ieșire în caz de lichidare. Kill switch-ul închide pozițiile cu ordine IOC reduce-only, anulează ordinele rămase, apoi verifică dacă au dispărut ambele.
- Un stop pe fiecare poziție. Intrările fill-or-kill înseamnă că nu există execuții parțiale neprotejate. După fiecare execuție urmează un stop și un take-profit pe poziție; dacă sunt respinse, poziția este închisă imediat.
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())
Calculul dimensiunii. Dimensiunea rezultă din distanța până la stop, nu din efectul de levier. Cu un preț ask ipotetic pentru BTC de 100.000 pUSD și un stop de 2%, stopul este la 98.000. Riscul pe unitate este 2.000, deci cantitatea este 20 / 2.000 = 0,01 BTC, adică o valoare nominală de 1.000 pUSD. La 3x, asta necesită aproximativ 333 pUSD marjă. Rata marjei de menținere pentru BTC este 1% (0,5 / 50x), deci prețul de lichidare al poziției long este 100.000 x (1 - 1/3) / (1 - 0,01) = aproximativ 67.340, o scădere de 32,7%. Stopul de la -2% se declanșează cu mult înainte. Dacă stopul este executat la prețul de declanșare, pierderea este 20 pUSD plus comisioane: 0,40 la deschidere și aproximativ 0,39 la închidere, la rata taker de 0,040%, deci cam 20,79 pUSD înainte de slippage. Efectul de levier decide doar câtă marjă este blocată. Nu schimbă pierderea la stop.
Înainte să treci live, adaugă o limită zilnică de tranzacții, alerte pentru execuții și kill switch-uri, plus o reconciliere la pornire (fetch_portfolio, fetch_open_orders, fetch_orders, list_fills). Vezi capitolele noastre despre codul de risc și tranzacționarea demo, ghidul Perps despre efectul de levier și lichidare și ghidul Perps despre ratele de finanțare.
Erori, reîncercări și dead man's switch
503 service_unavailablenu dovedește că cererea a eșuat. Reîncearcă citirile și modificările efectului de levier cu backoff și respectăRetry-After. Pentru modificările de sold semnate (marjă, retrageri), retrimite doar cererea semnată originală, exact așa cum era; o semnătură nouă înseamnă o a doua ajustare.signature expired: semnătura e mai veche de 5 minute. Ține ceasul sincronizat.insufficient_margin/invalid_leverage: verificările iau în calcul poziția ta plus toate ordinele în așteptare.message_rate_limited: bugetul WebSocket s-a epuizat. Cheile read-only primesc 1.000 de mesaje pe minut per IP (rafală de 50) și patru conexiuni per cont per gateway; pentru restul, vezi pagina Rate Limits din documentație.- Erori de format: prea multe zecimale sau mai mult de 5 cifre semnificative. De aici
fit().
Anularea automată anulează toate ordinele deschise dacă botul tău nu o mai reactivează. Termenul limită trebuie să fie cu cel puțin 5 secunde în viitor, iar numărul de declanșări pe zi UTC este limitat, așa că dezactiveaz-o la o oprire curată.
// 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();
Competiții, coduri de builder și regulile pentru boți
Documentația enumeră „boții de tranzacționare care reacționează la semnalele pieței” și market makerii printre utilizările API-ului. Se aplică două seturi de reguli.
- Competiții. Sunt interzise tranzacțiile fictive (wash trading), tranzacționarea cu tine însuți (self-trading), conturile multiple, tranzacționarea în principal pentru volum și „boții, scripturile sau mijloacele automate destinate manipulării Programului”. Un bot care tranzacționează cu adevărat este în regulă; unul care generează volum artificial poate fi descalificat.
- Coduri de builder. O aplicație care tranzacționează pentru alți utilizatori poate adăuga un comision de până la 10 puncte de bază, cu aprobarea fiecărui trader. Ea trebuie să verifice locația fiecărui utilizator și să blocheze ordinele din jurisdicțiile restricționate, nu doar să avertizeze.
Recompensele pentru lichiditatea maker și competițiile sunt explicate în ghidul Polymarket Perps.
Găzduirea botului
Un bot cu efect de levier nu ar trebui să ruleze pe un laptop. Trebuie să-și țină WebSocket-ul activ, să reacționeze la execuții în câteva secunde și să supraviețuiască unei căderi a internetului de acasă. Rulează-l pe un VPS sub systemd cu repornire automată, păstrează cheia într-un fișier de mediu pe care doar utilizatorul botului îl poate citi și sincronizează ceasul. Plasarea ordinelor nu este permisă din locațiile restricționate, deci nu găzdui botul în SUA sau Canada și nu folosi niciodată un VPS ca să ocolești o blocare care ți se aplică. Noi ne rulăm boții pe TradingVPS (link de afiliere: primim un mic comision, iar prețul pe care îl plătești rămâne același). Capitolul nostru despre găzduirea pe VPS îl compară cu alți furnizori după latență, preț și regiuni.
După tranzacționarea demo, alimentează contul cu o sumă mică de pe pagina Polymarket Perps și rulează la dimensiunea minimă timp de o săptămână înainte să crești.
Concluzii cheie
- Folosește SDK-urile oficiale,
@polymarket/clientsaupolymarket-client. Portofelul tău semnează o singură dată; o cheie proxy semnează ordinele. - Identifică piețele după simbolul live (
BTC-USD, nuBTC-PERP) și salvează ID-ul numeric. - Calculează dimensiunea din distanța până la stop, plafonează efectul de levier în cod, folosește marjă izolată și pune un stop pe fiecare poziție imediat după execuție.
- TP/SL se declanșează pe prețul de marcare și iese la piață. Anularea automată poate elimina stopurile.
- Boții sunt permiși, manipularea competițiilor nu. Nu tranzacționa niciodată dintr-o țară restricționată.





