Polymarket Bot 教程 · 第 32 章,共 32 章
真实的 Polymarket bot 失误与事后分析:phantom fills、sticky-fail dedup、lol-ctg-ccg whipsaw、NegRisk flag bug、premature go-live-以及修复每个问题的 commits 和日期。
本章内容
这是我们自己在生产环境中记录的、真正造成金钱损失的 bug 日记。模式比细节更重要-同类 bug 会在不同 bots 中反复出现,而修复通常是缺少一个 watchdog,而不是更好的 strategy。本章旨在帮你省下这笔学费。
- Phantom fills(commits e68a087, 8bb7761)
- NegRisk flag bug(commit 06deaef)
- Sticky-fail dedup(commit 4c0bef1)
- Whipsaw incident: lol-ctg-ccg
- Premature go-live: 2025 wipe
- Sleep-through-bug: kill switch 生效
- 可泛化的经验教训
Phantom fills(commits e68a087, 8bb7761)
这是我们 trader 上第一次重大的 phantom-fill 事件,发生在 2025 年 5 月。Bot 下了 22 个 FOK 买单,全部在 CLOB 上成交。Bot 随即尝试挂出 22 个 GTC 卖单。其中 8 个被拒绝,错误信息为 "balance: 0 / sum of active orders: 0 / order amount: 10000000."
根本原因:settlement lag(第 12 章)。CLOB 在 100ms 内完成撮合,bot 在 200ms 内挂出卖单,但 Polygon ERC-1155 transfer 需要约 2 秒。由于链上仍显示余额为零,CLOB 拒绝了该卖单。
修复:在任何成功买入与同一 token 上后续的 GTC 操作之间,插入 5 秒的阻塞等待。commits e68a087 和 8bb7761。此后再无 phantom-fill 事件。
经验:API time 和 chain time 是两条不同的时间线。假设它们同步的代码,会撞上这个完全相同的失败模式。
NegRisk flag bug(commit 06deaef)
一个有 8 个候选项的 NegRisk 多结果事件,出现了短暂的 arb,价差为 1.8c(YES asks 之和 = 0.982)。我们的 arber 立刻触发了全部 8 个 FOK 买单。其中 6 个成交;另外 2 个结算到了错误的 exchange contract。
根本原因:bot 调用 createAndPostOrder 时,没有在 flags 对象里设置 negRisk: true。其中两个 markets 有不同的历史创建日期,因此需要这个 flag;另外六个不需要,因为它们的底层 contract 默认已经通过 NegRisk 路由。
修复:从 Gamma 读取每个 market 的 market.negRisk,并传递到每一次 order 调用中。commit 06deaef。我们在设置好该 flag 后重新跑了 arb;剩余 2 笔也正确结算了。
经验:永远不要为 market property 设默认值。每次都要从 truth source 明确读取。
Sticky-fail dedup(commit 4c0bef1)
Bot 在 12 秒内重试了一笔失败的买单 5 次。第一次其实已经成功(network timeout 导致 bot 没看到响应);接下来的 4 次重试又创建了 4 个额外仓位。总计:在我们只想要 1 个仓位的情况下,同一个 market 上出现了 5 个仓位。
根本原因:缺少幂等的 client-order-id。Bot 的重试逻辑是“如果失败了,就换个新的 salt 再试一次”。CLOB 没法识别这些重试其实是重复订单。
修复:在第一次尝试之前,为预期下出的每一笔订单生成一个确定性的 UUID。所有重试都使用同一个 client-order-id,从而允许 CLOB 去重。commit 4c0bef1。
经验:没有幂等性的重试,就是重复下单。每一笔订单都需要一个稳定的客户端标识符。
Whipsaw incident: lol-ctg-ccg
一场电竞比赛(CTG 对 CCG)中,bot 在 imbalance 转为正值时以 0.45 买入。30 秒内,imbalance 又转为负值,我们挂出的 0.50 GTC 卖单被别人的订单成交。PnL:+5c × 10 shares = +$0.50。
10 分钟后,同一个 market 的 imbalance 再次转为正值。Bot 再次以 0.42 进场。这一次 imbalance 再也没有恢复;mid 一路下滑到 0.18,这个仓位最终以 0 结束。
根本原因:strategy 把 imbalance 当成了方向信号,但没有追踪它实际上是在反复跳动-这两个信号都只是 noise,不是 information。Bot 在同一个 market 上的 20 分钟内,被两个失败信号来回“whipsaw”了。
修复:按 market 设置 cooldown-一次成交后,同一 market 30 分钟内不允许再次入场。不同 markets 之间仍可多次入场,但同一个 market 不能背靠背连续交易。
经验:一个会来回跳动的信号,不是信号。先筛选持续性,再采取行动。
Premature go-live: 2025 wipe
一个新的 market-making strategy 通过了 12 笔 paper trades。构建者没有等到 30 笔,就觉得“看起来不错”,于是带着 $500 资金上线实盘。18 小时内,钱包余额降到了 $200。
根本原因:12 笔交易的样本量不足以区分 60% WR 和 35% WR。这个 strategy 实际上只有 35% WR;那 12 笔 paper window 只是碰巧出现了一段不具代表性的连胜/连败。
30-trade gate 之所以存在,是有原因的。12 笔样本的方差太大,和“strategy 不工作”几乎无法区分。
经验:纪律胜过信念。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 limiting;bot 的读取路径返回了过期数据。
Bot 继续基于过期价格交易。该小时的 PnL:22 笔交易合计 -$3.20。daily-loss kill switch 触发在 -5%,停止了 bot,并在 03:08 UTC 发送了 Telegram 提醒。构建者在 09:00 醒来时看到 bot 已被停止,总损失被限制在 kill threshold 内。
经验:bug 确实存在,但 kill switch 起作用了。损失是 -$3.20,而不是 -$50.00。风险控制不会阻止 bug;它们只会限制你没预见到的 bug 的成本。
可泛化的经验教训
在所有 postmortems 中,有四种模式反复出现。
- API time ≠ chain time。Settlement lag、RPC lag、WebSocket lag-这些都会引入 bot 代码必须显式处理的间隙。
- 重试需要幂等性。没有 client-order-id 的重试就是重复订单风险。永远如此。
- 明确读取每一个 market property。NegRisk flag、tick size、expiration。永远不要默认;始终从 truth source 读取。
- Kill switch 是底线,不是功能点。风险控制限制 bug 带来的损失。Strategy 不会阻止 bug;它假设 bot 正常工作。而 bot 不可能永远正常工作。
本系列的每一章,都在某处嵌入了这些模式中的一种。它们是生产级 bot 的承重原则。跳过它们,你会在自己的 postmortems 里再次遇见它们。





