Polymarket Bot 教程 · 第 12 章,共 32 章
如何检测 Polymarket phantom fills(看起来已成交但实际上没有)的情况,实现幂等重试,区分 status=matched 与 rested-on-book,以及在临时故障中保持稳定运行。
本章涵盖内容
phantom fill 是 Polymarket 特有的一种故障模式:CLOB 已确认撮合成功,但链上尚未确认 ERC-1155 转账。随后在约 5 秒内发起的订单会被一个误导性的 "balance: 0" 错误拒绝。解决办法是幂等性和结算等待。本章是我们用真金白银换来的生产级操作手册。
- 什么是 phantom fill
- status=matched vs status=delayed vs status=posted
- 轮询模式:先轮询状态,再庆祝
- 用 FOK 抵御 phantom-fill
- 结合 client_order_id 的幂等重试
- 真实生产事故:我们是如何修复的
- 代码:detect-then-act 的下单后处理模式
什么是 phantom fill
phantom fill 指的是:CLOB API 对你的订单返回 status: "matched",但链上 ERC-1155 转账尚未完成结算。CLOB matcher 的速度比 Polygon 出块更快(每个区块约 2 秒)。在 API match 之后约 2-5 秒内,你的钱包在链上还没有真正持有 matcher 声称你拥有的代币。
当 bot 在这个窗口内执行后续动作时,就会暴露出 bug-通常是为了挂止盈而发起的 GTC 卖单。CLOB 会检查链上余额,发现是零,并以 not enough balance / allowance: balance: 0, order amount: N 拒绝。报错把原因归咎于 allowance;真正原因是结算延迟。
第一次遇到这种情况时,你会以为是 allowance bug,然后白白浪费一个小时。解决方法很简单:等待、验证,然后再挂单。
status=matched vs status=delayed vs status=posted
下单响应中包含一个 status 字段,其中有三个关键值。
matched:订单已立即与订单簿撮合成功。资产会在 2-5 秒内结算。FOK/FAK 在成功时返回的就是这个状态。delayed:matcher 无法同步结算,已将撮合排队。较少见;通常表示拥堵。就等待 + 验证模式而言,可以把它当成matched处理。posted(也叫live):订单挂在订单簿上,尚未成交。GTC 订单若没有立即撮合,就会返回该状态。资产不受影响;此时还不需要后续动作。
决策规则:如果 status 是 matched 或 delayed,在确认链上转账之前,不要执行任何需要新库存的后续操作。
轮询模式:先轮询状态,再庆祝
验证模式是:在成功撮合后,轮询 CTF 余额,直到它反映出新代币,然后再继续执行。
def wait_for_settlement(token_id, expected_size, timeout=15):
"""Block until on-chain balance reaches expected_size or timeout."""
start = time.time()
while time.time() - start < timeout:
bal = ctf_contract.functions.balanceOf(PROXY, token_id).call()
if bal >= expected_size:
return True
time.sleep(0.5)
return False
典型结算时间:网络状况良好时为 2-5 秒,Polygon 拥堵时最长可达 15 秒。等待 5 秒可覆盖 95% 的情况;生产环境建议将超时设为 15 秒,并在超时后告警。
对于无法承受阻塞的高频 bot,另一种方法是事件订阅:监听 CTF 的 TransferSingle 事件,针对你的 proxy 地址,在收到事件后触发下游动作。这样可以把等待交给队列,而不是阻塞策略主循环。
用 FOK 抵御 phantom-fill
相较于 FAK,选择 FOK 可以部分防御 phantom-fill 乱象。FOK 要么整单成交,要么返回 cancelled;FAK 可能返回部分 filled_size。当部分成交后又按原始订单大小发起 GTC 卖单时,卖单会因结算延迟加上数量不匹配而失败-这是两个叠加的 bug。
使用 FOK 时,数量是二元的:要么全部成交,要么完全没有成交。后续挂单逻辑始终知道应该预期什么。
这并不能消除等待的必要性-即使是完美的 FOK 撮合,也仍然受 2-5 秒结算窗口影响。但它移除了一个账务偏差来源。
结合 client_order_id 的幂等重试
下单过程中的网络故障会制造最糟糕的场景:bot 的 HTTP 调用超时,但订单可能已经被接收,也可能没有被接收。盲目重试会导致重复下单;不重试则可能丢失仓位。
解决办法是下单时使用 client_order_id 字段。为每一笔预期订单生成一个确定性的 UUID;如果服务器之前见过这个 ID,它会返回已有订单的状态,而不是创建重复订单。
import uuid
oid = str(uuid.uuid4()) # generate once, retry with same value
for attempt in range(3):
try:
resp = c.create_and_post_order(args, OrderType.FOK, client_order_id=oid)
return resp
except (TimeoutError, ConnectionError):
time.sleep(0.5 * (2 ** attempt))
raise RuntimeError("post failed after 3 attempts")
模式是:先生成 ID,遇到传输失败就重试,遇到逻辑拒绝则不要重试。服务端去重按 API key 维度生效,持续约 5 分钟。
真实生产事故:我们是如何修复的
来自我们自己的生产日志,2025 年 5 月。一个 60 分钟窗口内,trader bot 发出了 22 个买单,全部成交,但只有 14 个 GTC 卖单被接受。8 个仓位没有挂出退出单。
根因:bot 在买单成交后 800ms 内就挂出了 GTC 卖单,远早于链上确认 ERC-1155 转账。CLOB 以 "balance: 0" 消息拒绝了请求;bot 记录了错误,但没有重试。8 个仓位在没有止盈保护的情况下静静地持有到结算。有 3 个最终以亏损结束;有 1 个靠运气以 0.99 结束。
修复方案是在任何同一 token 的买入成交与 GTC 挂单之间加入 5 秒阻塞等待。通过 30 笔纸面交易加 30 笔真实交易验证;此后再未出现 balance 为零的错误。
教训是:一个悄无声息的错误路径,比一个显眼的错误更昂贵。此后我们让所有 phantom-fill 错误都触发 Telegram 告警,这样未来的漂移模式会在几秒内被看见。
代码:detect-then-act 的下单后处理模式
生产环境中的先买后挂模式。
def buy_then_post_tp(token_id, size, buy_price, tp_price):
# 1. Place FOK buy
buy_args = OrderArgs(token_id=token_id, price=buy_price, size=size, side="BUY")
buy_resp = c.create_and_post_order(buy_args, OrderType.FOK)
if buy_resp.status != "matched":
return {"ok": False, "stage": "buy", "reason": buy_resp.status}
# 2. Wait for on-chain settlement (5s sane default; bump for congestion)
if not wait_for_settlement(token_id, expected_size=size, timeout=15):
return {"ok": False, "stage": "settle", "reason": "timeout"}
# 3. Confirm minimum size for GTC
if size < 5:
# GTC won't accept; fall back to ride-to-resolve or FOK sell at TP later
return {"ok": True, "stage": "buy_only", "note": "size<5, no GTC posted"}
# 4. Post GTC sell
sell_args = OrderArgs(token_id=token_id, price=tp_price, size=size, side="SELL")
sell_resp = c.create_and_post_order(sell_args, OrderType.GTC)
return {"ok": sell_resp.status in ("posted","live"), "buy": buy_resp, "sell": sell_resp}
这种模式能够抵御常见故障:phantom-fill、临时网络中断、低于最小 GTC 数量限制。它返回足够的信息,让策略层决定哪些要重试、哪些要记录、哪些要告警。










