Tutoriel bot Polymarket · Chapitre 24 sur 32
Polymarket Perps dispose d'une API publique complète, avec des SDK officiels en TypeScript et en Python. Ce chapitre construit un bot sur la véritable interface : authentification avec une clé de trading proxy, données de marché via REST et WebSocket, ordres avec take-profit et stop-loss, et contrôles de risque qui empêchent un bot à effet de levier de vider le compte.
Contenu de ce chapitre
- Chiffres clés pour les développeurs de bots
- Comment l'API Perps est construite
- Étape 1 : installer le SDK et ouvrir une session
- Étape 2 : lire les données de marché via REST et WebSocket
- Étape 3 : passer des ordres avec take-profit et stop-loss
- Étape 4 : un squelette de bot avec contrôles de risque dans le code
- Erreurs, dead man's switch, règles applicables aux bots et hébergement
Chaque méthode du SDK et chaque endpoint cités ici proviennent de la documentation Perps de Polymarket (docs.polymarket.com/perps, consultée le 6 novembre 2026). Notre propre logique est signalée dans les commentaires, et la fonction de stratégie est un espace réservé clairement identifié.
Chiffres clés pour les développeurs de bots (en novembre 2026)
| Élément | Valeur |
|---|---|
| URL de base REST | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| SDK TypeScript | @polymarket/client (avec viem pour la signature) |
| SDK Python | polymarket-client (asynchrone) |
| Données publiques | /v1/info/*, sans authentification |
| Lectures privées | /v1/account/* avec les en-têtes polymarket-proxy et polymarket-secret |
| Écritures signées | /v1/trade/*, signature EIP-712 par la clé proxy, valable 5 minutes |
| Identifiants de session | Expirent par défaut au bout d'une semaine |
| Ordre minimum | 10 $ de notionnel ; dépôt minimum de 10 pUSD |
| Ordre au marché maximal | 1 000 000 $ sur les marchés 50x et 20x, 500 000 $ sur la plupart des autres, 50 000 $ sur les cryptos à 3x et 5x |
| Format du prix et de la taille | Chaînes décimales, sans dépasser le nombre de décimales du marché ni 5 chiffres significatifs |
| Effet de levier maximal | 50x (BTC, ETH, USA500, US100), sinon 20x, 10x, 5x ou 3x selon le marché, moins pour les grosses positions |
| Frais (niveau de base) | Taker 0,040 %, maker 0,0125 % ; ANT à moitié prix |
| Financement | Toutes les heures, plafonné à +/-4 % par heure |
| Déclencheurs TP/SL | Prix de marque, pas le dernier prix échangé |
| Keepalive WebSocket | Ping environ toutes les 25 secondes ; considérer la connexion comme périmée après environ 65 secondes de silence |
| Fraîcheur du ticker | Jusqu'à 10 secondes de retard |
Comment l'API Perps est construite
Perps est une plateforme d'échange hybride : l'appariement des ordres se fait offchain, la conservation des fonds et le règlement sur Polygon. Seuls les dépôts et les retraits sont onchain, il n'y a donc pas de frais de gas par ordre. L'API comporte trois couches :
- Données publiques (
/v1/info/) : instruments, tickers, carnet d'ordres, bougies, transactions, financement, frais. Aucune clé requise et aucun blocage géographique. - Lectures privées (
/v1/account/) : portefeuille, soldes, ordres, exécutions, configuration. - Écritures signées (
/v1/trade/) : ordres, annulations, effet de levier, marge isolée, annulation automatique. Chaque écriture est encodée en MessagePack, hachée avec keccak256 et signée sous forme de données typées EIP-712 par la clé proxy.
Les SDK s'occupent de la signature à votre place. Il existe aussi une spécification OpenAPI (/api-spec/perps-openapi.json sur le site de documentation) et une spécification AsyncAPI pour le WebSocket.
BTC-PERP. L'API en production renvoie BTC-USD, SP500-USD (USA500), NAS100-USD (US100), ANT-USD et ainsi de suite. Récupérez la liste des instruments, faites la correspondance sur le symbol réel et enregistrez l'id numérique. Ne copiez pas les ID des exemples : en production, BTC vaut 6 et SP500 vaut 1.Étape 1 : installer le SDK et ouvrir une session
Installez avec pip install polymarket-client ou npm install @polymarket/client@latest viem. Le bot a besoin de l'adresse de votre portefeuille Polymarket et de la clé privée du signataire qui détient le compte Perps (le compte est rattaché à l'EOA signataire, pas au Safe). L'ouverture d'une session demande une seule signature pour autoriser une nouvelle clé de trading proxy, qui signe ensuite chaque ordre : votre clé de financement ne trade donc jamais. Conservez session.credentials sous forme chiffrée pour reprendre la session plus tard.
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);
Déposer depuis le code (deposit_to_perps / depositToPerps) nécessite en plus une clé API Relayer, à créer dans polymarket.com, Paramètres, Clés API. Plus simple et plus sûr : approvisionnez le compte depuis l'application web et tenez le bot à l'écart des mouvements de fonds. Depuis le 5 novembre 2026, les comptes qui utilisent le carnet d'adresses de retrait ne peuvent retirer que vers des adresses enregistrées. Pour les tableaux de bord, utilisez une clé en lecture seule (un proxy dont le libellé commence par read-only:, qui refuse les écritures signées), jamais la clé de trading. Le stockage des clés est traité dans notre chapitre sur les portefeuilles et l'authentification.
Étape 2 : lire les données de marché via REST et WebSocket
Le ticker comporte trois prix : l'index (juste valeur calculée à partir de flux comme Pyth, Chainlink Data Streams et Hyperliquid), le prix de marque (utilisé pour le PnL, la marge, la liquidation et les TP/SL) et le dernier prix (exécution uniquement), ainsi que le taux de financement et l'heure du prochain financement. Basez vos signaux sur le prix de marque ou l'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);
}
Sans le SDK, abonnez-vous sur le WebSocket brut avec {"id": 1, "req": "sub", "chs": ["bbo::6"]}. Les canaux incluent bbo::<id>, book::<id>, trades::<id>, tickers::<id> et klines::<id>::1m. Chaque mise à jour porte un numéro de séquence sq : après un trou dans la séquence ou une reconnexion, relisez l'état via REST. Pour les backtests, list_perps_candles / listPerpsCandles pagine l'historique (l'endpoint REST renvoie jusqu'à 1 000 bougies par appel).
Étape 3 : passer des ordres avec take-profit et stop-loss
Un ordre comporte un side (BUY ou SELL), une quantity en unités de l'actif de base, un price facultatif en pUSD et une durée de validité (gtc, ioc ou fok). Le post-only exige GTC ; un ordre reduce-only ne peut que réduire une position. Il n'existe pas d'indicateur long ou short : sans position ouverte, BUY ouvre un long et SELL ouvre un short.
Un ordre bracket rattache un take-profit et un stop-loss à l'ordre d'entrée. Ils restent inactifs jusqu'à l'exécution complète de l'entrée, et sont annulés si l'entrée est annulée ou seulement partiellement exécutée. Les prix ci-dessous sont hypothétiques.
# 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);
Pour une position ouverte, place_position_tp_sl / placePositionTpSl ajoute des déclencheurs qui ferment l'intégralité de la position. Règles :
- Les déclencheurs surveillent le prix de marque et envoient un ordre reduce-only. Le TP/SL de position sort au marché : attendez-vous à du slippage dans les carnets peu profonds.
- Un seul take-profit et un seul stop-loss de position par marché. Déplacer un stop implique d'annuler puis de recréer, ce qui n'est pas atomique.
- Un déclencheur que le prix de marque a déjà franchi est rejeté, et les prix limites trop éloignés du prix de marque sont refusés.
Pour tester le flux d'ordres à la main avant de l'automatiser, ouvrez le marché BTC sur Polymarket Perps et passez le plus petit ordre autorisé. Voir les exécutions, les frais et le panneau de position rend les réponses de l'API plus faciles à interpréter.
Étape 4 : un squelette de bot avec contrôles de risque dans le code
Le squelette Python ci-dessous n'utilise que des appels documentés du SDK ; la logique de risque est la nôtre. Trois niveaux de protection :
- Plafond d'effet de levier. L'effet de levier est fixé à 3x ou au maximum du marché, selon la valeur la plus basse, en marge isolée, et les tailles qui tomberaient dans un palier de risque à plafond inférieur sont refusées.
- Coupe-circuits (kill switches). Un fichier HALT, une limite de perte journalière de 5 % et une sortie en cas de liquidation. Le coupe-circuit ferme les positions avec des ordres IOC reduce-only, annule les ordres restants, puis vérifie que les deux ont bien disparu.
- Un stop sur chaque position. Des entrées fill-or-kill garantissent qu'aucune exécution partielle ne reste sans protection. Un stop et un take-profit de position suivent chaque exécution ; s'ils sont rejetés, la position est fermée immédiatement.
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())
Le calcul de la taille. La taille découle de la distance du stop, pas de l'effet de levier. Avec une offre de vente BTC hypothétique à 100 000 pUSD et un stop à 2 %, le stop se situe à 98 000. Le risque par unité est de 2 000, la quantité est donc de 20 / 2 000 = 0,01 BTC, soit un notionnel de 1 000 pUSD. À 3x, cela immobilise environ 333 pUSD de marge. Le taux de marge de maintenance de BTC est de 1 % (0,5 / 50x), donc le prix de liquidation du long est de 100 000 x (1 - 1/3) / (1 - 0,01) = environ 67 340, soit une baisse de 32,7 %. Le stop à -2 % se déclenche bien avant. Si le stop est exécuté à son prix de déclenchement, la perte est de 20 pUSD plus les frais : 0,40 à l'ouverture et environ 0,39 à la clôture au taux taker de 0,040 %, soit environ 20,79 pUSD avant slippage. L'effet de levier détermine seulement le montant de marge immobilisé. Il ne change pas la perte au stop.
Avant de passer en réel, ajoutez une limite de trades journalière, des alertes sur les exécutions et les coupe-circuits, et une réconciliation au démarrage (fetch_portfolio, fetch_open_orders, fetch_orders, list_fills). Consultez nos chapitres sur le code de gestion du risque et le paper trading, le guide de l'effet de levier et de la liquidation sur Perps et le guide des taux de financement sur Perps.
Erreurs, nouvelles tentatives et dead man's switch
503 service_unavailablene prouve pas que la requête a échoué. Relancez les lectures et les changements d'effet de levier avec un backoff et respectezRetry-After. Pour les modifications de solde signées (marge, retraits), ne renvoyez que la requête signée d'origine, à l'identique ; une nouvelle signature constitue un second ajustement.signature expired: signature de plus de 5 minutes. Gardez l'horloge synchronisée.insufficient_margin/invalid_leverage: les contrôles tiennent compte de votre position et de tous les ordres en attente dans le carnet.message_rate_limited: quota WebSocket épuisé. Les clés en lecture seule ont droit à 1 000 messages par minute et par IP (pic de 50) et à quatre connexions par compte et par passerelle ; pour le reste, voir la page Rate Limits de la documentation.- Erreurs de format : trop de décimales ou plus de 5 chiffres significatifs. D'où la fonction
fit().
L'annulation automatique (auto-cancel) annule tous les ordres ouverts si votre bot cesse de la réarmer. L'échéance doit être fixée au moins 5 secondes à l'avance, et le nombre de déclenchements par jour UTC est limité : désarmez-la donc lors d'un arrêt propre.
// 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();
Compétitions, builder codes et règles applicables aux bots
La documentation cite les « bots de trading qui réagissent aux signaux du marché » et les teneurs de marché parmi les usages de l'API. Deux ensembles de règles s'appliquent.
- Compétitions. Le wash trading, le self-trading, les comptes multiples, le trading principalement destiné à générer du volume et « les bots, scripts ou moyens automatisés destinés à manipuler le Programme » sont interdits. Un bot qui trade réellement est accepté ; un bot qui gonfle artificiellement le volume peut être disqualifié.
- Builder codes. Une application qui trade pour d'autres utilisateurs peut ajouter des frais allant jusqu'à 10 points de base, avec l'accord de chaque trader. Elle doit vérifier la localisation de chaque utilisateur et bloquer les ordres provenant de juridictions restreintes, pas simplement afficher un avertissement.
Les récompenses de liquidité pour les makers et les compétitions sont traitées dans le guide Polymarket Perps.
Héberger le bot
Un bot à effet de levier ne doit pas tourner sur un ordinateur portable. Il doit maintenir son WebSocket actif, réagir aux exécutions en quelques secondes et survivre à une coupure de la connexion internet domestique. Faites-le tourner sur un VPS sous systemd avec redémarrage automatique, conservez la clé dans un fichier d'environnement lisible uniquement par l'utilisateur du bot, et synchronisez l'horloge. Le passage d'ordres n'est pas autorisé depuis les zones restreintes : n'hébergez donc pas le bot aux États-Unis ou au Canada, et n'utilisez jamais un VPS pour contourner un blocage qui vous concerne. Nous faisons tourner nos propres bots sur TradingVPS (lien affilié : nous touchons une petite commission et le prix que vous payez reste le même). Notre chapitre sur l'hébergement VPS le compare à d'autres fournisseurs en termes de latence, de prix et de régions.
Après le paper trading, approvisionnez le compte avec un petit montant depuis la page Polymarket Perps et tournez à la taille minimale pendant une semaine avant d'augmenter.
À retenir
- Utilisez les SDK officiels,
@polymarket/clientoupolymarket-client. Votre portefeuille signe une seule fois ; une clé proxy signe les ordres. - Identifiez les marchés par leur symbole réel (
BTC-USD, pasBTC-PERP) et enregistrez l'ID numérique. - Calculez la taille à partir de la distance du stop, plafonnez l'effet de levier dans le code, utilisez la marge isolée et placez un stop sur chaque position juste après l'exécution.
- Le TP/SL se déclenche sur le prix de marque et sort au marché. L'annulation automatique peut supprimer les stops.
- Les bots sont autorisés, la manipulation des compétitions ne l'est pas. Ne tradez jamais depuis un pays restreint.





