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.

  1. 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.
  2. Les retries ont besoin d’idempotence. Un retry sans client-order-id est un risque d’ordre dupliqué. Toujours.
  3. 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é.
  4. 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.

Foire aux questions

Quelle est l’erreur de Polymarket bot la plus coûteuse ?
Passer en live avant que le paper-trading n’atteigne le gate des 30 trades. Nous l’avons fait. L’erreur n’est pas seulement de perdre de l’argent - c’est de perdre l’occasion d’apprendre de la strategy dans un environnement contrôlé. Les bots qui passent en live trop tôt sont soit pulvérisés puis abandonnés, soit passent des mois à se remettre avant de revenir au paper-trading.
Qu’est-ce qu’un bug de phantom fill ?
Quand le bot pense qu’un ordre a fillé alors que l’exchange l’a enregistré comme non encore fillé. Symptômes: la position apparaît dans l’état de vos bots mais pas on-chain, ce qui entraîne des ordres en double lors du retry. Corrigé dans notre trader via trois commits (e68a087, 8bb7761, 06deaef): utiliser FOK pour les achats, poller le statut jusqu’au matched, ne jamais faire confiance à status=delayed comme s’il signifiait filled.
Qu’est-ce que l’incident de whipsaw lol-ctg-ccg ?
Un marché esports sur un order book peu profond où notre trader a déclenché un stop-loss à -2,55 $ à 0,14, puis a vu le prix remonter à 0,325 en moins de 2 minutes. Nous avions configuré le stop-loss à -4 points de pourcentage, ce qui est trop serré pour les order books esports peu profonds. Correction: élargir le SL à -8 pp pour les marchés à faible liquidité, et garder un SL plus serré seulement pour les books profonds (NBA, football à forte liquidité). Voir memory/trader-sl-wider.md.
Comment le bug du flag NegRisk s’est-il manifesté ?
Le bot passait des ordres sans définir neg_risk=true sur les marchés multi-résultats. Les ordres étaient rejetés avec des messages d’erreur confus, ce qui entraînait des délais de plusieurs secondes avant retry, puis des fills manqués. Correction dans le commit 06deaef: toujours définir neg_risk selon les métadonnées du marché, ne jamais supposer.
Qu’était l’incident sleep-through-bug ?
Le wallet s’est retrouvé bloqué avec un ordre coincé à 4h du matin. Le propriétaire a demandé au bot de s’arrêter ; il a touché le fichier data/halt_autobuy. Le bot a détecté le fichier avant la prochaine tentative d’ordre et a refusé de passer des ordres. Le propriétaire s’est réveillé avec un état propre au lieu d’un état pire. Cela a validé le pattern du halt-sentinel ; nous l’expédions maintenant par défaut dans chaque bot.
Quelle est la leçon la plus généralisable de ces postmortems ?
Ne faites jamais confiance au happy path. Chaque bug que nous avons livré est venu du fait de supposer qu’une requête avait réussi, qu’un fill était réel, ou qu’un prix n’allait pas bouger. Codez défensivement: supposez que les ordres échouent, supposez que les reconciliations divergent, supposez qu’un marché est sur le point de faire quelque chose d’étrange. La taxe de paranoia est faible ; le coût de l’ignorer, c’est le postmortem que vous écrirez plus tard.