Polymarket Bot Tutorial · Розділ 32 із 32
Реальні помилки Polymarket bot і postmortems: phantom fills, sticky-fail dedup, lol-ctg-ccg whipsaw, NegRisk flag bug, premature go-live - з commits і датами, які виправили кожну з них.
Що охоплює цей розділ
Наш власний production diary багів, які коштували реальних грошей. Важливішим за деталі є сам патерн - ті самі класи багів повторюються в bot-ах, а ліки зазвичай не в кращій strategy, а у відсутньому watchdog. Цей розділ має заощадити вам навчання на власних помилках.
- Phantom fills (commits e68a087, 8bb7761)
- NegRisk flag bug (commit 06deaef)
- Sticky-fail dedup (commit 4c0bef1)
- Whipsaw incident: lol-ctg-ccg
- Premature go-live: wipe 2025 року
- Sleep-through-bug: kill switch спрацював
- Уроки, що узагальнюються
Phantom fills (commits e68a087, 8bb7761)
Перший великий phantom-fill incident у нашому trader-і, травень 2025 року. Bot виставив 22 FOK buys, усі matched на CLOB. Bot одразу спробував виставити 22 GTC sells. 8 із них були rejected з повідомленням "balance: 0 / sum of active orders: 0 / order amount: 10000000."
Root cause: settlement lag (розділ 12). CLOB matched за 100ms, bot виставив sell за 200ms, але Polygon ERC-1155 transfer тривав приблизно 2 seconds. CLOB відхилив sell, бо chain усе ще показував нульовий balance.
Fix: вставити 5-second blocking wait між будь-яким успішним buy і будь-яким GTC follow-up на тому самому token. Commits e68a087 і 8bb7761. Відтоді - нуль phantom-fill incidents.
Lesson: API time і chain time - це різні timelines. Code, який припускає, що вони synchronous, зіткнеться саме з цим failure mode.
NegRisk flag bug (commit 06deaef)
NegRisk multi-outcome event із 8 candidates мав миттєвий arb на 1.8c (sum of YES asks = 0.982). Наш arber відправив усі 8 FOK buys. 6 із них filled; 2 settled у неправильний exchange contract.
Root cause: bot викликав createAndPostOrder, не встановивши negRisk: true в flags object. Два markets мали іншу historical creation date і потребували цього flag; шість - ні, бо їхній underlying contract уже маршрутизувався через NegRisk за замовчуванням.
Fix: читати market.negRisk із Gamma для кожного market, передавати це в кожен order call. Commit 06deaef. Ми повторно запустили arb із встановленим flag; решта 2 settled correctly.
Lesson: ніколи не задавайте market property за замовчуванням. Кожного разу читайте його явно з source of truth.
Sticky-fail dedup (commit 4c0bef1)
Bot повторив failed buy 5 разів за 12 seconds. Перша спроба насправді succeeded (network timeout спричинив те, що bot не побачив response); наступні 4 retries створили 4 додаткові positions. Разом: 5 positions на тому самому market, хоча ми хотіли 1.
Root cause: відсутній idempotent client-order-id. Retry logic у bot-а була такою: "if it failed, try again with a new salt." CLOB не мав способу розпізнати retries як duplicates.
Fix: генерувати deterministic UUID для кожного запланованого order перед першою спробою. Усі retries використовують той самий client-order-id, що дозволяє CLOB виконати dedup. Commit 4c0bef1.
Lesson: retries without idempotence are duplicates. Кожен order потребує стабільного client-side identifier.
Whipsaw incident: lol-ctg-ccg
Esports match (CTG vs CCG) спричинив buy на 0.45, коли imbalance став позитивним. Протягом 30 seconds imbalance став негативним, і наш GTC sell на 0.50 був виконаний чиєюсь іншою order. PnL: +5c × 10 shares = +$0.50.
10 minutes потому, imbalance у тому самому market знову став позитивним. Bot увійшов знову на 0.42. Цього разу imbalance так і не відновився; mid drifted до 0.18, і position дожила до resolution на 0.
Root cause: strategy сприймала imbalance як directional signal, але не відстежувала, що imbalance "скаче" - обидва signals були noise, а не information. Bot був whipsawed на двох хибних signals в одному й тому самому market протягом 20 minutes.
Fix: cooldown per market - після fill жодних нових entries у той самий market протягом 30 minutes. Дозволялося робити multiple entries у різних markets, але не back-to-back в одному й тому самому.
Lesson: signal, який "скаче", - це не signal. Перед дією перевіряйте persistence.
Premature go-live: wipe 2025 року
Нова market-making strategy пройшла 12 paper trades. Builder не дочекався 30, вирішив, що "виглядає добре", і запустив live з $500 capital. За 18 hours wallet упав до $200.
Root cause: 12 trades - це замала sample, щоб відрізнити 60% WR від 35% WR. Насправді strategy мала 35% WR; 12-trade paper window просто випадково мала нерепрезентативну streak.
30-trade gate існує не просто так. Variance на 12-trade sample робить його невідмінним від "strategy не працює".
Lesson: discipline beats conviction. 30-trade gate не підлягає обговоренню.
Sleep-through-bug: kill switch спрацював
Bot мав off-by-one у фільтрі часу доби - мав ставити паузу о 02:00 UTC, але насправді паузився о 03:00 UTC. Під час незупиненого вікна 02:00-03:00 Polygon RPC сильно rate-limited наші requests; read path bot-а повертав застарілі дані.
Bot продовжував торгувати на застарілих prices. PnL за годину: -$3.20 на 22 trades. Daily-loss kill switch спрацював на рівні -5%, зупинив bot і надіслав Telegram alert о 03:08 UTC. Builder прокинувся до зупиненого bot-а о 09:00; загальну шкоду було обмежено kill threshold.
Lesson: баг був реальним, але kill switch спрацював. -$3.20 замість -$50.00. Risk controls не запобігають багам; вони обмежують вартість багів, яких ви не передбачили.
Уроки, що узагальнюються
У всіх postmortems повторюються чотири патерни.
- API time ≠ chain time. Settlement lag, RPC lag, WebSocket lag - усе це створює розриви, які code bot-а має обробляти явно.
- Retries потребують idempotence. Retry без client-order-id - це ризик duplicate-order. Завжди.
- Читайте кожну market property явно. NegRisk flag, tick size, expiration. Ніколи не задавайте default; завжди читайте з source of truth.
- Kill switch - це підлога, а не feature. Risk controls обмежують збитки від багів. Strategies не запобігають багам; вони припускають, що bot працює correctly. Bot не завжди працюватиме correctly.
У кожному розділі цієї серії десь захований один із цих patterns. Це несучі принципи production bot-а. Пропустіть їх - і знайдете їх знову у власних postmortems.





