Polymarket Bot Tutorial · Chapitre 32 sur 32
Vraies erreurs de Polymarket bot et postmortems: phantom fills, sticky-fail dedup, lol-ctg-ccg whipsaw, bug du flag NegRisk, go-live prématuré - avec les commits et les dates qui ont corrigé chacun.
Ce que couvre ce chapitre
Notre propre journal de production des bugs qui ont coûté de l'argent réel. Le schéma compte plus que les détails - les mêmes catégories de bugs reviennent d’un bot à l’autre, et la correction est généralement un watchdog manquant, pas une meilleure strategy. Ce chapitre vise à vous éviter ces frais de scolarité.
- Phantom fills (commits e68a087, 8bb7761)
- Bug du flag NegRisk (commit 06deaef)
- Sticky-fail dedup (commit 4c0bef1)
- Incident de whipsaw: lol-ctg-ccg
- Go-live prématuré: wipe 2025
- Sleep-through-bug: le kill switch a fonctionné
- Leçons généralisables
Phantom fills (commits e68a087, 8bb7761)
Le premier incident majeur de phantom-fill sur notre trader, en mai 2025. Le bot a passé 22 achats FOK, tous matched sur le CLOB. Le bot a immédiatement tenté de poster 22 ventes GTC. 8 ont été rejetées avec "balance: 0 / sum of active orders: 0 / order amount: 10000000."
Cause racine: settlement lag (chapitre 12). Le CLOB a matched en 100 ms, le bot a posté la vente en 200 ms, mais le transfert ERC-1155 sur Polygon a pris environ 2 secondes. Le CLOB a rejeté la vente parce que la chaîne montrait encore un solde nul.
Correction: insérer une attente bloquante de 5 secondes entre tout achat réussi et tout suivi GTC sur le même token. Commits e68a087 et 8bb7761. Zéro incident de phantom-fill depuis.
Leçon: le temps API et le temps de la chaîne sont deux lignes temporelles différentes. Le code qui suppose qu’elles sont synchrones tombera exactement dans ce mode de défaillance.
Bug du flag NegRisk (commit 06deaef)
Un événement multi-résultats NegRisk avec 8 candidats avait un arb momentané de 1,8 c (somme des YES asks = 0,982). Notre arber a déclenché les 8 achats FOK. 6 ont été fillés ; 2 se sont réglés dans le mauvais exchange contract.
Cause racine: le bot appelait createAndPostOrder sans définir negRisk: true dans l’objet flags. Deux marchés avaient une date de création historique différente et exigeaient le flag ; six n’en avaient pas besoin parce que leur contrat sous-jacent était déjà routé via NegRisk par défaut.
Correction: lire market.negRisk depuis Gamma pour chaque marché, puis le transmettre à chaque appel d’ordre. Commit 06deaef. Nous avons relancé l’arb avec le flag défini ; les 2 restants se sont réglés correctement.
Leçon: ne mettez jamais de valeur par défaut pour une propriété de marché. Lisez-la explicitement depuis la source de vérité à chaque fois.
Sticky-fail dedup (commit 4c0bef1)
Le bot a retenté un achat échoué 5 fois en 12 secondes. La première tentative a en réalité réussi (un timeout réseau a empêché le bot de voir la réponse) ; les 4 tentatives suivantes ont créé 4 positions supplémentaires. Total: 5 positions sur le même marché alors que nous en voulions 1.
Cause racine: absence de client-order-id idempotent. La logique de retry du bot était: "si ça échoue, réessaie avec un nouveau salt." Le CLOB n’avait aucun moyen de reconnaître les retries comme des doublons.
Correction: générer un UUID déterministe par ordre prévu avant la première tentative. Tous les retries utilisent le même client-order-id, ce qui permet au CLOB de faire le dedup. Commit 4c0bef1.
Leçon: des retries sans idempotence créent des doublons. Chaque ordre a besoin d’un identifiant stable côté client.
Incident de whipsaw: lol-ctg-ccg
Un match esports (CTG vs CCG) a vu le bot entrer en achat à 0,45 lorsque l’imbalance est devenue positive. En moins de 30 secondes, l’imbalance est redevenue négative et notre vente GTC à 0,50 a été touchée par l’ordre de quelqu’un d’autre. PnL: +5 c × 10 shares = +$0,50.
10 minutes plus tard, l’imbalance du même marché est redevenue positive. Le bot est revenu à l’achat à 0,42. Cette fois, l’imbalance ne s’est jamais rétablie ; le mid a dérivé jusqu’à 0,18 et la position est allée à la résolution à 0.
Cause racine: la strategy traitait l’imbalance comme un signal directionnel mais ne suivait pas le fait que l’imbalance oscillait - les deux signaux étaient du bruit, pas de l’information. Le bot a été whipsawed sur deux faux signaux sur le même marché en moins de 20 minutes.
Correction: cooldown par marché - après un fill, aucune nouvelle entrée sur le même marché pendant 30 minutes. Cela autorisait plusieurs entrées sur différents marchés, mais pas des allers-retours sur le même.
Leçon: un signal qui rebondit n’est pas un signal. Filtrez la persistance avant d’agir.
Go-live prématuré: wipe 2025
Une nouvelle strategy de market-making a validé 12 paper trades. Le builder n’a pas attendu 30, s’est dit "ça a l’air bon", et a déployé en live avec 500 $ de capital. En moins de 18 heures, le wallet était à 200 $.
Cause racine: 12 trades ne suffisent pas comme échantillon pour distinguer un WR de 60 % d’un WR de 35 %. La strategy était en réalité à 35 % de WR ; la fenêtre de 12 paper trades avait simplement une séquence non représentative.
Le gate à 30 trades existe pour une raison. La variance sur un échantillon de 12 trades le rend indiscernable de "la strategy ne fonctionne pas".
Leçon: la discipline bat la conviction. Le gate à 30 trades n’est pas négociable.
Sleep-through-bug: le kill switch a fonctionné
Le bot avait un off-by-one dans son filtre d’heure - censé s’arrêter à 02:00 UTC, il s’arrêtait en réalité à 03:00 UTC. Pendant l’heure 02:00-03:00 où il n’était pas arrêté, le RPC Polygon limitait fortement nos requêtes ; le read path du bot renvoyait des données obsolètes.
Le bot a continué à trader sur des prix stale. PnL sur l’heure: -3,20 $ sur 22 trades. Le kill switch de perte journalière s’est déclenché à -5 %, a arrêté le bot, et a envoyé une alerte Telegram à 03:08 UTC. Le builder s’est réveillé devant un bot stoppé à 09:00, les dégâts totaux étant limités au seuil du kill.
Leçon: le bug était réel mais le kill switch a fonctionné. -3,20 $ au lieu de -50,00 $. Les risk controls n’empêchent pas les bugs ; ils plafonnent le coût des bugs que vous n’aviez pas vus venir.
Leçons généralisables
À travers tous les postmortems, quatre schémas reviennent.
- API time ≠ chain time. Le settlement lag, le RPC lag, le WebSocket lag - tout cela crée des écarts que le code du bot doit gérer explicitement.
- Les retries ont besoin d’idempotence. Un retry sans client-order-id est un risque d’ordre dupliqué. Toujours.
- Lisez explicitement chaque propriété de marché. Flag NegRisk, tick size, expiration. Ne mettez jamais de valeur par défaut ; lisez toujours depuis la source de vérité.
- Le kill switch est le plancher, pas une feature. Les risk controls plafonnent les pertes dues aux bugs. Les strategies ne préviennent pas les bugs ; elles supposent que le bot fonctionne correctement. Le bot ne fonctionnera pas toujours correctement.
Chaque chapitre de cette série contient quelque part l’un de ces schémas. Ce sont les principes porteurs d’un bot de production. Ignorez-les et vous les retrouverez dans vos propres postmortems.





