3. 订单簿重建原理与回放基准

逐笔盘口回放的核心,是把交易所推送的逐笔委托与逐笔成交两类事件,按时间顺序“重放”到一张限价订单簿上,使任意时刻的盘口状态都能被精确复现。要做到可复现,必须同时解决三个问题:订单簿如何增量更新、回放从哪个快照起步、逐笔事件与快照如何对齐时序。本节围绕这三点展开,并给出可落地的回放基准定义。

一、限价订单簿的增量更新逻辑

限价订单簿本质上是按价格分档、档内按时间优先排列的双向队列结构。买侧(Bid)价格从高到低,卖侧(Ask)价格从低到高,最优买价与最优卖价构成盘口。重建时并不需要每来一笔事件就重算全簿,而是维护“价格档 → 订单队列”的映射,并对每类事件做局部增量修改。

逐笔委托事件通常包含委托编号、方向、价格、数量、委托类型等字段。新增限价委托时,在对应价格档的队尾插入该订单,并累加该档总量;若该价格优于当前最优价,则更新最优价指针。撤单事件按委托编号定位订单,从档内队列删除并扣减档量,若该档清空则移除价格档。逐笔成交事件则同时影响两侧:对买方委托,扣减其剩余数量;对卖方委托同理;若某笔委托全部成交,则从队列中移除。这里有一个关键约定必须固定下来——成交事件只扣减数量,不重复插入订单,因为订单在委托事件中已经入簿,成交是消耗而非新增。若把成交也当作入簿处理,会导致订单簿数量翻倍,这是重建中最常见的错误。

为便于理解,下面用一段伪代码表达单事件处理的核心分支:

for event in events_sorted_by_time:
    if event.type == "ORDER":
        book.add(order_id, side, price, qty)
    elif event.type == "CANCEL":
        book.remove(order_id)
    elif event.type == "TRADE":
        book.reduce(buy_order_id, trade_qty)
        book.reduce(sell_order_id, trade_qty)
    snapshot_at(event.ts)  # 按需记录盘口

需要强调的是,价格档的排序与最优价维护应采用有序结构(如平衡树或维护好的价格数组),避免每次事件都全量排序。对于高频数据,单日事件量可达数百万条,增量更新的复杂度直接决定回放能否在可接受时间内完成。

二、回放起始快照的确定

回放不能从“空簿”开始,因为真实市场在开盘前已有大量挂单。正确做法是选取一个基准快照作为起点,再叠加该快照之后的所有逐笔事件。快照的来源通常是交易所或行情商提供的定时盘口快照(如每 3 秒一档的十档行情),或日初的静态订单簿文件。

选择起始快照需满足两个条件:其一,快照时间必须早于或等于回放区间的第一个逐笔事件;其二,快照必须包含完整的订单明细或至少完整的档位聚合信息。若只有聚合档位而无订单编号,则无法处理后续撤单与成交对具体订单的定位,此时只能做“档位级”近似回放,精度会下降。因此工程上优先选择带订单明细的快照,或采用“聚合档位 + 逐笔事件重放”的混合模式,并明确标注精度等级。

建议把回放基准定义为四元组:起始快照标识、快照时间戳、事件区间 [t_start, t_end]、以及事件排序规则。四者固定后,同一数据集任意次回放结果应完全一致,这就是“可复现”的判定标准。

三、逐笔事件与快照的时序对齐规则

时序对齐是回放基准中最容易出错的环节。逐笔委托、逐笔成交、盘口快照往往来自不同通道,时间戳精度和含义可能不同:有的精确到毫秒,有的到微秒;有的是交易所撮合时间,有的是行情落地时间。对齐时必须统一到同一时间基准,并明确“同一时间戳内事件的先后顺序”。

常用规则如下:第一,所有事件按时间戳升序排列;第二,时间戳相同时,按“委托先于成交、成交先于快照”的固定优先级排序,因为成交由委托触发,快照反映的是事件处理后的状态;第三,快照时间戳若与事件相同,规定快照在该时刻所有事件处理完毕后再采集,保证快照与重放状态一致。若数据源无法保证同一毫秒内顺序,应引入序列号字段作为次级排序键,序列号缺失时则需在基准中声明“同刻事件顺序不确定”,并评估其对盘口的影响。

下图展示了从起始快照出发,逐笔事件按对齐规则驱动订单簿增量更新,并在指定时点输出回放盘口的关系:

起始快照 时间戳 t0 含订单明细/档位 逐笔事件流(按对齐规则排序) ① 逐笔委托 ORDER ② 逐笔成交 TRADE ③ 撤单 CANCEL 限价订单簿(增量更新) 买侧:价格降序 / 时间优先 卖侧:价格升序 / 时间优先 最优买价 / 最优卖价 叠加 驱动 时序对齐规则 1. 统一时间基准,按时间戳升序;2. 同刻优先级:委托 → 成交 → 快照; 3. 同刻无法区分时以序列号为次级键;4. 快照在同时刻事件处理完毕后采集。 回放盘口输出 任意时刻 t 的买卖档位与最优价 与快照逐档比对,偏差即校验信号 可复现基准校验 同一基准多次回放结果一致 与真实快照档位偏差在阈值内

四、形成可复现的回放基准

把上述要素固化下来,就得到一份可交付的回放基准。它至少应记录:数据集版本与校验和、起始快照文件及其时间戳、事件区间、排序规则(含同刻优先级与次级键)、订单簿数据结构版本、以及校验方法。校验方法建议采用“快照对拍”:在回放过程中,每当遇到原始盘口快照的时间点,就把当前重建盘口与真实快照逐档比较,统计档位价格与数量的偏差。若偏差持续为 0 或低于预设阈值(例如档位数量偏差不超过 1 手),说明重建逻辑与对齐规则正确;若偏差随时间累积增大,通常指向成交重复入簿、撤单未定位到订单或同刻顺序错误。

以一个教学示例说明:假设某标的起始快照在 09:30:00.000 给出买一 10.00 元 500 手、卖一 10.01 元 300 手。随后事件依次为:09:30:00.120 新增买委托 10.00 元 200 手;09:30:00.120 成交 10.00 元 100 手(买方为该新委托);09:30:00.350 撤销卖一剩余 300 手中的 100 手。按对齐规则,同一毫秒内委托先于成交,则买一档量变为 500+200−100=600 手;撤单后卖一档量变为 200 手。若回放输出与 09:30:01.000 的真实快照一致,则该段基准通过校验。上述数值为示例假设,实际应以数据集真实字段为准。

常见陷阱:把逐笔成交当作新增订单插入订单簿,或在快照时间点先采集再处理同刻事件,都会造成盘口数量虚高或档位错位。前者使订单簿“只增不减”,后者使快照与重放状态相差一个事件,二者都会让回放不可复现。

最后,回放基准应随数据集一同版本化管理。任何一次对排序规则、快照选择或订单簿实现的修改,都必须生成新的基准版本并重新跑对拍校验,避免“同一数据集、不同结果”的隐性不一致。做到这一步,逐笔盘口回放才真正具备可复现、可审计、可交付的工程属性。