4. 订单簿快照重建
逐笔数据本身是“事件流”,而绝大多数因子计算、回测撮合和微观结构分析都需要“状态量”——某一时刻买一卖一的价格与数量、前五档深度、盘口不平衡度等。把事件流还原为状态序列的过程,就是订单簿重建。本节围绕三件事展开:如何用逐笔委托与逐笔成交维护限价订单簿(LOB)、快照频率如何选择、以及如何对重建结果做状态校验。
一、事件语义:先分清“谁改变了订单簿”
重建的第一步不是写代码,而是明确每类逐笔事件对订单簿的作用。以常见的逐笔委托(order)与逐笔成交(trade)双通道数据为例,需要区分以下语义:
- 限价委托挂单:在指定价位新增一个订单,增加该价位队列尾部,订单簿深度增加。
- 市价委托/即时成交剩余撤销:不进入订单簿,直接与对手方最优价撮合,只通过成交事件体现。
- 撤单:从订单簿中移除指定订单,减少对应价位数量;若该价位清零则删除该档。
- 成交:同时减少买、卖两侧对应订单的剩余量,成交量从盘口扣除。
关键陷阱在于:若成交事件已经隐含了“订单被消耗”,而委托通道又重复推送了撤单,就会造成重复扣减。工程上通常采用“以订单号为唯一键”的订单表来消歧:每个订单记录 order_id、side、price、qty、remaining、status,任何事件先查订单表再决定是否改动盘口。若数据源只提供成交而不提供委托明细,则只能用“成交价+成交量”做近似扣减,此时必须接受盘口数量存在误差,并在校验环节标注。
二、LOB 的数据结构与更新逻辑
一个可用的 LOB 至少需要两侧的价格档位映射。买侧用大顶堆或降序有序映射维护“价格→该价位订单队列”,卖侧用升序结构。每个价位节点保存该价位的总剩余量与该价位订单的 FIFO 队列,用于后续计算队列位置、撤单率和挂单存活时间等因子。
# 伪代码:单事件更新
def on_order(ev):
if ev.type == "ADD":
book[ev.side][ev.price].append(ev) # 入队尾
book[ev.side][ev.price].total += ev.qty
elif ev.type == "CANCEL":
o = orders[ev.order_id]
book[o.side][o.price].remove(o)
book[o.side][o.price].total -= o.remaining
if book[o.side][o.price].total == 0:
del book[o.side][o.price]
def on_trade(ev):
for side in ("buy", "sell"):
o = orders[ev.resting_order_id_on(side)]
o.remaining -= ev.qty
book[side][o.price].total -= ev.qty
if o.remaining == 0:
book[side][o.price].remove(o)
注意成交事件通常只标明“主动方”,被动方(resting order)需要靠订单表反查。若同一价位有多个订单,必须按 FIFO 顺序扣减,否则队列位置类因子会失真。更新完成后,盘口的最优价即买侧最大价、卖侧最小价,任一时刻应满足 best_bid < best_ask,这是最基本的合法性约束。
三、快照频率的选择
快照频率不是越密越好。它取决于下游因子的时间尺度与数据成本:
- 高频微观因子(盘口不平衡、队列位置、订单流不平衡 OFI):需要事件级或至少 100ms~500ms 快照,否则会丢失盘口变化细节。
- 中频因子(价差、深度斜率、成交量加权价差):1~3 秒快照通常足够,能显著降低存储与计算量。
- 低频因子(日内波动、流动性水平):分钟级快照即可,但要注意快照时点应避开整分钟边界,减少与其它参与者同步采样带来的偏差。
实践中推荐“双轨制”:以事件驱动方式维护 LOB 状态,在需要的时间点(定时或事件触发)落盘快照。定时快照便于对齐多标的;事件快照(如每次盘口变化后)便于研究冲击响应。若存储受限,可只保留前 N 档(如 5 档或 10 档),但需注意深度类因子在档位截断处的偏差。
四、状态维护与校验
重建过程中最容易出问题的是“状态漂移”:某一笔撤单未匹配到订单、成交重复扣减、或跨日未重置。建议在每个快照点执行以下校验,并记录异常计数:
| 校验项 | 判断条件 | 异常含义 |
|---|---|---|
| 盘口交叉 | best_bid ≥ best_ask | 更新顺序错误或漏处理撤单 |
| 数量非负 | 任一档 total < 0 | 重复扣减或订单表缺失 |
| 订单表一致性 | 订单表剩余量之和 ≠ 盘口总量 | 事件丢失或乱序 |
| 与官方快照比对 | 前 5 档价格/数量偏差超阈值 | 语义理解错误或数据源差异 |
校验发现异常时,不要简单跳过。可先检查事件时间戳是否单调、同毫秒事件是否按交易所序号排序;若数据源本身存在丢包,则需在快照中标记 quality_flag,下游因子计算时对低质量区间降权或剔除。对于跨日重建,务必在开盘前清空订单表并重新初始化,避免昨日未成交订单污染今日盘口。
最后,重建结果应能回答一个基本问题:给定任意时刻,我能准确说出买一卖一的价格、数量以及前 N 档的分布。只有这个状态序列可信,后续的订单流不平衡、盘口压力、队列位置等因子才有意义。建议先用一小段真实逐笔数据(例如某只股票连续竞价 30 分钟)跑通全流程,与交易所官方快照逐点比对,确认偏差在可接受范围内,再扩展到全市场与全交易日。