Polymarket 的盘口增量 price_change 从采集第一周起就采用 keep-first 节流。到这次探针运行时,窗口按币种分档:BTC 20ms、ETH 100ms、其余 500ms。节流键是市场级,每次放行后,窗口内其余帧丢弃。
代价是盘口重建不完整。被丢掉的删除更新会让重建盘口留下旧档位。10 月 10 日到 12 日写探针并测存储量、重建误差,10-13 的笔记又记录了吃单价格误差。这篇把几轮结果放在一起。
三种策略同时跑
探针 scripts/probe-price-change-coalesce.mjs 对生产完全只读:自己开 socket,不连数据库,除了输出目录什么都不写。为了让结果能代表生产,它的订阅方式和采集器一样(同样的币种 × 5m/15m、当前槽位和下一个槽位),每个市场直接用采集器编译出来的那个 socket 类,keepalive、slow consumer 快速重连、断连窗口记账都相同,节流的 key 也和生产逐字一致。某个市场的 socket 断着时,它的 token 不进准确度采样,断开时长单独报告。
每一帧 price_change 同时喂给三种策略:
- raw:每一帧都留,就是上游实际发的;
- drop:采集器现在的做法,窗口内留第一帧、丢其余;
- coalesce:快照与 drop 相同;每帧
price_change的变化进入待发集合,同一 asset、方向、价位后写覆盖先写。它维护自己的发送时刻:第一帧立即发,之后累计到窗口到期再发,定时检查也会发出到期集合。发送时刻不必与 drop 放行的帧相同;放行的快照会清掉对应 token 的旧待发变化。
每种策略各维护一份重建盘口:快照替换该 token 的整本,price_change 把价位设成绝对量,0 表示删除。每秒将 drop、coalesce 与 raw 比较,统计量不一致的档位、有幽灵档位的 token 样本,以及最优买价或卖价是否不一致。raw 是这条连接实际收到的流,不是交易所内部盘口;断连期间不参与准确度采样。coalesce 仍有窗口内的延迟。
每种策略的存储按归档的 JSONL 行算字节,并且真的过一遍 gzip。gzip 积压的未压缩字节超过 128MB 就中止、标记 invalid,不让探针在采集器和中心库旁边无限吃内存。统计只认 final: true 且 invalid: null 的那份汇总。
第一轮:存储和准确度
10-11 到 10-12 跑满 24 小时,按天算的 price_change 量:
| 策略 | 行数 | 原始字节 | gzip |
|---|---|---|---|
| drop(现状) | 9.33M | 6.9GB | 0.96GB |
| coalesce | 11.37M | 35.1GB | 5.07GB |
| raw | 138M | 102GB | 11.5GB |
合并之后行数只多了两成,字节却是 5 倍,因为每行带上了窗口里所有被合并的档位变化。
在 token × 采样时刻的样本中,最优买价或卖价与 raw 不一致的比例,drop 是 50.6%,coalesce 是 8.3%;至少含一个幽灵档位的样本比例分别是 62.0% 和 12.4%。后两个数不表示幽灵档位占全部档位的比例。
第二轮:回测这一侧能补回多少
改采集器要多花存储,所以第二个问题是:不改采集器,只在回测里做修正,能修好多少。
9 月初开始,采集器多订阅了一个 best_bid_ask 流。它不节流,是上游自己给出的完整分辨率的最优买卖价,但只有价格没有量。回测引擎用它做一件事:在对应 token 上,删除严格优于最新最优价、而且最后一次被设置早于这个最优价的档位。只删不加,因为一个不知道量的档位就是凭空编出来的流动性。
要量这个修正的效果,不能按帧到达的顺序比。回测并不按到达顺序回放:runner/events.mjs 把一个市场的事件按上游时间戳排序,同一毫秒里快照排在增量前面,没有时间戳的行直接丢弃。所以第二轮加了三个按回测顺序重放的模型:
- rawT:全部帧;
- dropT:drop 存下来的帧;
- dropbboT:drop 存下来的帧,再按回测的
Ladder.prune规则应用每一帧best_bid_ask。
三者经过同一个 5 秒的重排窗口,按回测的排序规则用水位释放,滞后相同,互相比较是公平的。dropbboT 比 dropT 多修好的,就是不改采集器、只靠回测能拿回来的部分。没有时间戳的帧和迟于窗口到达的帧单独计数。
这一轮笔记记录,dropbboT 的最优价不一致样本比例为 36.5%,含幽灵档位的样本比例为 48.3%。50.6% 是前一轮按到达顺序的 drop 结果,不能直接当作这一轮 dropT 的基线。
剩下的误差主要来自另一个方向:被丢掉的增量里有一次「在更优的价位新挂了单」。best_bid_ask 能告诉你最优价变了,但它不带量,引擎不能凭空加一个档位。所以这部分只能删、不能补。
第三轮:误差折成价格是多少
「36.5% 的时刻最优价不对」对用户来说不好理解,第三轮把它折成价格:最优买卖价差了几个 0.001 的价位,以及 10、100、1000 股的吃单买入和卖出,在模型盘口和真实盘口上的成交均价差多少(偏向吃单方算乐观)。只有一边吃得满的样本单独计数,比例以全部样本为分母,并输出样本数。
10-13 的项目笔记概括:dropbboT 吃单均价偏悲观,约 0.4 到 0.9 美分;dropT 偏乐观,约 1 美分。原始汇总没有留在这几份素材中,不能把这个范围说成全部订单的平均偏差。探针代码只在模型和 raw 都能吃满时计算价格差,吃不满的情况另计。
旧档位未删除,可能让 dropT 高估可成交的流动性;bbo 剪枝能删除越过最优价的旧档位,却补不回漏掉的新挂单。这能解释偏差的方向,但不保证每个样本都朝同一方向偏。
决定:采集器不改
10-11 决定采集器不改。24 小时结果中,coalesce 比 drop 多约 4GB gzip 数据,未压缩 JSONL 字节约为 5 倍;这不是 PostgreSQL 实际占用的测量值。回测侧继续用 bbo 剪枝,并在文档里说明漏掉的新档位和改量仍无法恢复。
回测这边跟着做了几件事(10-12,镜像 1.27.0):
- 清单声明
book时自动带上bbo(IMPLIED_DATASETS,JS 的normalizeDatasets和 Python 的with_implied_datasets同步),原来要客户自己声明。顺带的好处是预热只剩一种形态; - 报告新增
book_age:吃单成交量按所吃档位的「年龄」分档,年龄是成交时刻减去这个档位最后一次被快照或增量设置的时刻,只对归档市场计算,两个引擎逐场景全等。用来看一个 run 的吃单量里有多少来自成交前一秒以上没被更新过的档位; - 方法说明里写上这次实测的数字,以及「这个规则恢复不了什么」:两次快照之间,被抽稀丢掉的增量里新增或改量的档位,在下一次快照之前是缺失或过时的,顶部也一样。
SDK 数据客户端的 OrderBook 更早在 10-10 加入(22b2cab,JS 和 Python 同构):快照替换、增量设绝对量、按最新最优价删掉越过的档位。README 说明顶部仍可能缺档或量旧,best() 不保证等于市场当时的最优价。两端有逐条镜像的单测,以及固定种子畸形流的跨语言一致性测试。
outcometick 工程笔记(19/21)