Polymarket Bot 教程 · 第 29 章,共 32 章
在正式上线前,先构建一个 Polymarket paper trading 引擎:基于真实价格模拟下单,跟踪 P&L,在任何真实资金投入前执行 30 笔交易门槛(胜率 >=55%,且 PnL 为正),并提供代码骨架。
本章内容
Paper trading 是从策略想法到正式部署之间不可妥协的一步。本章展示的是一个简单的 paper engine-我们发布过的每个 live bot 都必须先通过它的门槛-代码不到 200 行 Python,能把每一笔交易记录到 JSONL 日志里,并应用与 live 路径相同的费用/滑点。
- 为什么要先 paper,后 live(永远如此)
- 30 笔交易门槛(验证过的 +55% 胜率 + 正 PnL)
- 构建一个简单的 paper engine
- 将 paper 日志与 live 日志并行跟踪
- paper 与 live 何时出现偏差(以及原因)
- 升级到 live:先小额首存
- 代码:最小 paper engine
为什么要先 paper,后 live(永远如此)
30 笔交易的 paper 门槛,是区分 15.9% 盈利的 Polymarket trader 与 84.1% 亏损者的唯一纪律。大多数 builder 会跳过这一步,然后交学费。它之所以有效,原因很朴素:paper trading 会在足够多的样本下,暴露策略真实胜率,从而区分信号与运气。
跳过 paper 其实比它省下的成本更多。一种看起来在 backtest 中盈利、但实际上只是抛硬币的策略,往往会先烧掉 $200-500 的 live 资金,才积累到 30 个 live 样本。相同的 30 笔交易如果先做 paper,成本是 $0。
paper engine 不需要复杂。它需要诚实-同样的 fees、同样的 slippage、同样的 fill latency,和 live 路径保持一致。越简单越好,因为任何可有可无的东西都会被删掉,bot 就会比该上线的时间更早地进入 live。
30 笔交易门槛(验证过的 +55% 胜率 + 正 PnL)
这个门槛是二元的:30 笔已平仓的 paper 交易、事先写明的成功标准(通常是对于正 EV 策略,胜率 WR ≥ 55%),否则就不允许 live 部署。
30 是最小样本量,此时真实胜率的 95% 置信区间已经足够窄,可以区分信号和噪声。少于 30 时,观测到的 60% 胜率可能对应真实胜率 45-75%。到 30 及以上时,区间会收窄到大约 50-70%-仍然不算窄,但足以排除“这个策略只是抛硬币”这种情况。
成功标准必须在 paper 运行开始之前就设定好。之后再设定,只会导致事后合理化(你会想办法把任何 30 笔交易解释成“差不多够好”)。
构建一个简单的 paper engine
paper engine 本质上就是 live trading 代码,只是把下单函数替换成了模拟成交。这个模拟会:
- 读取 live order book:与 live bot 会调用的接口相同。
- 模拟成交:如果以 FOK 买入且价格 >= best ask,则按吃掉的 asks 的 volume-weighted average 成交;把成交写入 paper 日志。
- 应用 fees:扣除 live 路径本来会支付的同样费用。
- 跟踪 inventory:维护一个并行的 paper-balance 和 paper-positions 字典。
整个引擎只需 100-200 行 Python。关键纪律是:live 路径的每一个假设(成交率、延迟、费用)都必须在 paper 中复现,哪怕略微比现实更差-paper 应该是下限,而不是上限。
将 paper 日志与 live 日志并行跟踪
paper trading 运行会生成一个 JSONL 日志,其结构与 bot 以后会写出的 live 日志完全一致。字段相同:timestamp、action、market_slug、side、size、price、expected_fill_price、simulated_pnl_at_exit。
使用相同格式有两个原因。第一,读取 live 交易的分析工具(PnL 报告、胜率计算器)无需修改就能用于 paper。第二,后续将 paper 与 live 对比时,可以发现那些指向 bug 的偏差。
生产建议:让 paper engine 将数据写入与 live per_trade.jsonl 同一目录下的 per_trade_paper.jsonl。一条命令即可比较二者:diff -y <(jq -r .market_slug per_trade.jsonl) <(jq -r .market_slug per_trade_paper.jsonl).
paper 与 live 何时出现偏差(以及原因)
paper 和 live 之间出现偏差是不可避免的。常见的有三种。
- Slippage:paper 以快照中的 ask 成交;live 会逐级吃单,在流动性薄的市场里可能差 1-2c。解决办法:在 paper 中模拟 slippage,给每笔交易增加一个等于 spread 一半的惩罚。
- Fill latency:paper 立即成交;live 则需要 200-500ms,在这段时间价格可能已经变动。解决办法:在 paper 中模拟等待,并在“成交”前重新读取 order book。
- Adverse selection:paper 假设你拿到的是最佳 ask;live 中你要与其他 bots 竞争,它们可能已经先一步吃掉了那个 ask。解决办法:这更难模拟;至少要诚实地告诉自己,paper 往往会高估结果。
当 paper 显示 +5%/月,而 live 跑出来是 -2%/月时,差距通常就来自这些因素之一。应当逐项审计,而不是直接假设策略本身错了。
升级到 live:先小额首存
paper 通过 30 笔交易后,live 部署计划如下:
- 先存入 $25-50 作为 smoke-test 资金。把它当作学费;如果亏掉了,这个教训也值这个价。
- 让 bot 以 live 模式运行 5-10 笔交易,仓位保持最小尺寸(5 shares)。
- 验证每一笔成交与 paper 预期的误差是否在 2c 以内。若偏差更大,先调查再继续。
- 如果这 5-10 笔 live 交易与 paper 一致,则追加 $200-500,并按正常尺寸运行。
- 如果不一致,立即停止,调试,修复,然后从步骤 1 重新开始。
首次部署时,live 与 paper 最常见的差距是遗漏 fee 或对 slippage 的估计错误。修复这些通常并不复杂;难点在于,在扩大资金规模之前先发现偏差。
代码:最小 paper engine
参考:读取 live book + 模拟 FOK 成交的简单 paper engine。
import json, time
PAPER_BAL = 10_000.0 # USD starting
positions = {} # token_id -> shares
def paper_fok_buy(token_id, max_price, size):
book = fetch_book(token_id)
# Walk asks, fill what we can within max_price
filled = 0; cost = 0
for level in book.asks:
px = float(level["price"])
if px > max_price: break
avail = float(level["size"])
take = min(avail, size - filled)
filled += take
cost += take * px
if filled >= size: break
if filled < size:
return {"status":"rejected","filled":0} # FOK semantics
global PAPER_BAL
PAPER_BAL -= cost
positions[token_id] = positions.get(token_id, 0) + filled
log_paper({"ts": int(time.time()), "action":"buy",
"token": token_id, "size": filled, "price": cost/filled})
return {"status":"matched","filled":filled,"cost":cost}
生产补充:paper 卖出函数(与 buy 对称)、paper GTC 模拟(在 book 上按价格挂单,当 mid 到达该价格时模拟成交)、paper 日志与“本可以发生的” live 日志之间的对账。





