Tutorial sui bot di Polymarket · Capitolo 24 di 32
Polymarket Perps ha un'API pubblica completa, con SDK ufficiali per TypeScript e Python. In questo capitolo costruiamo un bot sull'interfaccia reale: autenticazione con una chiave di trading proxy, dati di mercato via REST e WebSocket, ordini con take-profit e stop-loss, e i controlli del rischio che impediscono a un bot con leva di svuotare il conto.
Cosa trovi in questo capitolo
- I numeri chiave per chi sviluppa bot
- Com'è strutturata l'API di Perps
- Passo 1: installa l'SDK e apri una sessione
- Passo 2: leggi i dati di mercato via REST e WebSocket
- Passo 3: invia ordini con take-profit e stop-loss
- Passo 4: lo scheletro di un bot con i controlli del rischio nel codice
- Errori, dead man's switch, regole per i bot e hosting
Ogni metodo dell'SDK ed ogni endpoint citati qui provengono dalla documentazione di Polymarket su Perps (docs.polymarket.com/perps, consultata il 6 novembre 2026). La logica scritta da noi è segnalata nei commenti, e la funzione della strategia è un segnaposto dichiarato.
I numeri chiave per chi sviluppa bot (a novembre 2026)
| Voce | Valore |
|---|---|
| URL di base REST | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| SDK TypeScript | @polymarket/client (con viem per la firma) |
| SDK Python | polymarket-client (asincrono) |
| Dati pubblici | /v1/info/*, senza autenticazione |
| Letture private | /v1/account/* con gli header polymarket-proxy e polymarket-secret |
| Scritture firmate | /v1/trade/*, firma EIP-712 della chiave proxy, valida 5 minuti |
| Credenziali di sessione | Scadono dopo una settimana, salvo impostazioni diverse |
| Ordine minimo | 10 $ di valore nozionale; deposito minimo 10 pUSD |
| Ordine a mercato massimo | 1 milione di $ sui mercati a 50x e 20x, 500.000 $ sulla maggior parte degli altri, 50.000 $ sulle monete a 3x e 5x |
| Formato di prezzo e quantità | Stringhe decimali, entro i decimali del mercato e con al massimo 5 cifre significative |
| Leva massima | 50x (BTC, ETH, USA500, US100), altrimenti 20x, 10x, 5x o 3x a seconda del mercato, più bassa per le posizioni grandi |
| Commissioni (livello base) | Taker 0,040%, maker 0,0125%; ANT a metà prezzo |
| Finanziamento | Orario, con un tetto di +/-4% all'ora |
| Attivazione TP/SL | Prezzo di riferimento (mark), non l'ultimo scambio |
| Keepalive WebSocket | Ping circa ogni 25 secondi; considera la connessione inattiva dopo circa 65 secondi di silenzio |
| Aggiornamento del ticker | Fino a 10 secondi di ritardo |
Com'è strutturata l'API di Perps
Perps è un exchange ibrido: l'abbinamento degli ordini avviene offchain, mentre custodia e regolamento sono su Polygon. Solo depositi e prelievi sono onchain, quindi non si paga gas per ogni ordine. L'API ha tre livelli:
- Dati pubblici (
/v1/info/): strumenti, ticker, libro ordini, candele, scambi, finanziamento, commissioni. Non serve una chiave e non c'è blocco geografico. - Letture private (
/v1/account/): portafoglio, saldi, ordini, esecuzioni, configurazione. - Scritture firmate (
/v1/trade/): ordini, cancellazioni, leva, margine isolato, cancellazione automatica. Ogni scrittura viene codificata in MessagePack, sottoposta a hash con keccak256 e firmata come dati tipizzati EIP-712 dalla chiave proxy.
Gli SDK gestiscono la firma al posto tuo. Esistono anche una specifica OpenAPI (/api-spec/perps-openapi.json sul sito della documentazione) e una specifica AsyncAPI per il WebSocket.
BTC-PERP. L'API reale restituisce BTC-USD, SP500-USD (USA500), NAS100-USD (US100), ANT-USD e così via. Scarica l'elenco degli strumenti, cerca il symbol reale e salva l'id numerico. Non copiare gli ID dagli esempi: nell'API reale BTC è 6 e SP500 è 1.Passo 1: installa l'SDK e apri una sessione
Installa con pip install polymarket-client oppure npm install @polymarket/client@latest viem. Il bot ha bisogno dell'indirizzo del tuo wallet Polymarket e della chiave privata del firmatario che possiede il conto Perps (il conto è legato all'EOA firmatario, non al Safe). Aprire una sessione richiede una sola firma per autorizzare una nuova chiave di trading proxy, che poi firma ogni ordine: così la chiave che custodisce i fondi non fa mai trading. Salva session.credentials in forma cifrata per riprendere la sessione in seguito.
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);
Per depositare dal codice (deposit_to_perps / depositToPerps) serve anche una chiave API del Relayer, da polymarket.com, Impostazioni, Chiavi API. La soluzione più semplice e sicura: finanzia il conto dall'app web e tieni il bot lontano dai movimenti di denaro. Dal 5 novembre 2026, i conti che usano la rubrica degli indirizzi di prelievo possono prelevare solo verso indirizzi salvati. Per le dashboard usa una chiave di sola lettura (un proxy con etichetta read-only:, che rifiuta le scritture firmate), mai la chiave di trading. La custodia delle chiavi è trattata nel nostro capitolo su wallet e autenticazione.
Passo 2: leggi i dati di mercato via REST e WebSocket
Il ticker riporta tre prezzi: index (il valore equo ricavato da fonti come Pyth, Chainlink Data Streams e Hyperliquid), mark (il prezzo di riferimento usato per PnL, margine, liquidazione e TP/SL) e last (solo per l'esecuzione), oltre al tasso di finanziamento e all'orario del prossimo pagamento. Basa i segnali sul mark o sull'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);
}
Senza SDK, iscriviti sul WebSocket grezzo con {"id": 1, "req": "sub", "chs": ["bbo::6"]}. Tra i canali ci sono bbo::<id>, book::<id>, trades::<id>, tickers::<id> e klines::<id>::1m. Ogni aggiornamento ha un numero di sequenza sq: dopo un buco nella sequenza o una riconnessione, rileggi lo stato via REST. Per i backtest, list_perps_candles / listPerpsCandles scorre lo storico a pagine (l'endpoint REST restituisce fino a 1.000 candele per chiamata).
Passo 3: invia ordini con take-profit e stop-loss
Un ordine ha un side (BUY o SELL), una quantity in unità dell'asset sottostante, un price facoltativo in pUSD e una validità temporale (gtc, ioc o fok). Il post-only richiede GTC; il reduce-only può solo ridurre una posizione. Non esiste un flag long o short: senza posizioni aperte, BUY apre un long e SELL apre uno short.
Un ordine bracket collega all'ingresso un take-profit e uno stop-loss. Restano inattivi finché l'ingresso non viene eseguito per intero, e vengono cancellati se l'ingresso è cancellato o eseguito solo in parte. I prezzi qui sotto sono ipotetici.
# 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);
Per una posizione già aperta, place_position_tp_sl / placePositionTpSl aggiunge trigger che chiudono l'intera posizione. Le regole:
- I trigger osservano il prezzo di riferimento (mark) e inviano un ordine reduce-only. Il TP/SL di posizione esce a mercato, quindi aspettati slippage sui book poco profondi.
- Un solo take-profit e un solo stop-loss di posizione per mercato. Spostare uno stop significa cancellarlo e poi crearne uno nuovo, un'operazione non atomica.
- Un trigger già superato dal prezzo di riferimento viene rifiutato, e anche i prezzi limite troppo lontani dal prezzo di riferimento vengono respinti.
Per provare il flusso degli ordini a mano prima di automatizzarlo, apri il mercato BTC su Polymarket Perps e invia l'ordine più piccolo consentito. Vedere esecuzioni, commissioni e pannello delle posizioni rende più facile interpretare le risposte dell'API.
Passo 4: lo scheletro di un bot con i controlli del rischio nel codice
Lo scheletro Python qui sotto usa solo chiamate documentate dell'SDK; la logica di rischio è nostra. Tre livelli di protezione:
- Tetto alla leva. La leva è impostata a 3x o al massimo del mercato, se più basso, con margine isolato, e le dimensioni che ricadono in una fascia di rischio con tetto più basso vengono rifiutate.
- Interruttori di emergenza. Un file HALT, un limite di perdita giornaliera del 5% e l'uscita in caso di liquidazione. L'interruttore chiude le posizioni con ordini IOC reduce-only, cancella gli ordini rimasti e poi verifica che non ci sia più nulla di aperto.
- Uno stop su ogni posizione. Gli ingressi fill-or-kill evitano esecuzioni parziali non protette. Dopo ogni esecuzione vengono impostati stop e take-profit di posizione; se vengono rifiutati, la posizione viene chiusa subito.
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())
Il calcolo della dimensione. La dimensione dipende dalla distanza dello stop, non dalla leva. Con un ask ipotetico di BTC a 100.000 pUSD e uno stop al 2%, lo stop si trova a 98.000. Il rischio per unità è 2.000, quindi la quantità è 20 / 2.000 = 0,01 BTC, per un valore nozionale di 1.000 pUSD. A 3x servono circa 333 pUSD di margine. Il tasso di margine di mantenimento di BTC è 1% (0,5 / 50x), quindi il prezzo di liquidazione del long è 100.000 x (1 - 1/3) / (1 - 0,01) = circa 67.340, cioè un calo del 32,7%. Lo stop a -2% scatta molto prima. Se lo stop viene eseguito al prezzo di attivazione, la perdita è di 20 pUSD più le commissioni: 0,40 in apertura e circa 0,39 in chiusura con la tariffa taker dello 0,040%, quindi circa 20,79 pUSD prima dello slippage. La leva decide solo quanto margine resta impegnato. Non cambia la perdita allo stop.
Prima di andare live, aggiungi un limite giornaliero di operazioni, avvisi su esecuzioni e interruttori di emergenza, e una riconciliazione all'avvio (fetch_portfolio, fetch_open_orders, fetch_orders, list_fills). Consulta i nostri capitoli sul codice per la gestione del rischio e sul paper trading, la guida a leva e liquidazione su Perps e la guida ai tassi di finanziamento su Perps.
Errori, nuovi tentativi e dead man's switch
503 service_unavailablenon dimostra che la richiesta sia fallita. Ripeti letture e modifiche della leva con un backoff e rispettaRetry-After. Per le variazioni di saldo firmate (margine, prelievi), invia di nuovo solo l'esatta richiesta firmata originale: una nuova firma equivale a una seconda modifica.signature expired: firma più vecchia di 5 minuti. Tieni l'orologio sincronizzato.insufficient_margin/invalid_leverage: i controlli considerano la tua posizione più tutti gli ordini in attesa.message_rate_limited: hai esaurito il budget del WebSocket. Le chiavi di sola lettura hanno 1.000 messaggi al minuto per IP (burst di 50) e quattro connessioni per conto per gateway; per il resto consulta la pagina Rate Limits della documentazione.- Errori di formato: troppi decimali o più di 5 cifre significative. Da qui la funzione
fit().
La cancellazione automatica cancella tutti gli ordini aperti se il bot smette di riattivarla. La scadenza deve essere almeno 5 secondi nel futuro e le attivazioni per giorno UTC sono limitate, quindi disattivala quando arresti il bot in modo ordinato.
// 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();
Competizioni, builder code e regole per i bot
La documentazione elenca "trading bots that react to market signals" (bot di trading che reagiscono ai segnali di mercato) e i market maker tra gli usi previsti dell'API. Valgono due insiemi di regole.
- Competizioni. Sono vietati wash trading, self-trading, account multipli, trading fatto principalmente per generare volume e "bots, scripts, or automated means intended to manipulate the Program" (bot, script o mezzi automatici volti a manipolare il programma). Un bot che fa trading reale va bene; uno che gonfia il volume può essere squalificato.
- Builder code. Un'app che fa trading per conto di altri utenti può aggiungere una commissione fino a 10 punti base, con l'approvazione di ciascun trader. Deve verificare la posizione geografica di ogni utente e bloccare gli ordini provenienti da giurisdizioni soggette a restrizioni, non limitarsi a un avviso.
Le ricompense per la liquidità dei maker e le competizioni sono trattate nella guida a Polymarket Perps.
Hosting del bot
Un bot con leva non dovrebbe girare su un portatile. Deve mantenere attivo il WebSocket, reagire alle esecuzioni in pochi secondi e resistere a un'interruzione della connessione di casa. Eseguilo su un VPS sotto systemd con riavvio automatico, conserva la chiave in un file di ambiente leggibile solo dall'utente del bot e sincronizza l'orologio. Non è consentito inviare ordini da località soggette a restrizioni, quindi non ospitare il bot negli Stati Uniti o in Canada, e non usare mai un VPS per aggirare un blocco che si applica a te. Facciamo girare i nostri bot su TradingVPS (link di affiliazione: riceviamo una piccola commissione e il prezzo per te resta lo stesso). Il nostro capitolo sull'hosting VPS lo confronta con altri fornitori per latenza, prezzo e regioni.
Dopo il paper trading, finanzia il conto con un piccolo importo dalla pagina di Polymarket Perps e opera con la dimensione minima per una settimana prima di aumentare.
Punti chiave
- Usa gli SDK ufficiali,
@polymarket/clientopolymarket-client. Il tuo wallet firma una sola volta; poi è una chiave proxy a firmare gli ordini. - Identifica i mercati tramite il simbolo reale (
BTC-USD, nonBTC-PERP) e salva l'ID numerico. - Calcola la dimensione dalla distanza dello stop, limita la leva nel codice, usa il margine isolato e metti uno stop su ogni posizione subito dopo l'esecuzione.
- Il TP/SL si attiva sul prezzo di riferimento (mark) ed esce a mercato. La cancellazione automatica può eliminare gli stop.
- I bot sono ammessi, manipolare le competizioni no. Non fare mai trading da un paese soggetto a restrizioni.





