2. 订单簿数据结构与市场微结构基础
要重建订单簿历史并做一致性回测,第一步不是写撮合引擎,而是把“订单簿到底是什么、由哪些数据构成、按什么规则变化”这件事定义清楚。订单簿重建的所有误差,最终都会追溯到数据模型与撮合规则的理解偏差:把撤单当成成交、把市价单当成限价单、忽略价格时间优先,都会让重建出的盘口与真实市场系统性偏离。本节围绕限价订单簿的层次结构、订单类型、撮合规则与价格时间优先原则展开,目标是建立一套后续重建与回测共用的统一数据模型。
一、限价订单簿的层次结构
限价订单簿(Limit Order Book,LOB)本质上是按价格分档、档内按时间排序的双边队列集合。买侧(Bid)与卖侧(Ask)各自维护一组价格档位,每个档位挂着一个订单队列。对任一侧而言,价格越优的档位越靠近盘口:买侧价格越高越优,卖侧价格越低越优。最优买价(Best Bid)与最优卖价(Best Ask)之间的差额即买卖价差(Spread),两者构成盘口(Top of Book)。
从数据建模角度,一个档位至少需要两类信息:聚合层(该价位的总量、订单数)与明细层(该价位上每一笔订单的编号、数量、到达时间)。行情快照通常只给聚合层,逐笔委托(如 Add/Modify/Cancel/Trade 消息)才能还原明细层。重建订单簿时,明细层是“真相”,聚合层是“投影”;只有明细层正确,聚合层才可能一致。因此统一数据模型应以订单为最小实体,档位与盘口均由订单集合派生,而不是反过来。
二、订单类型与生命周期
重建订单簿时最常遇到的订单类型包括:限价单(指定价格与数量,可立即成交部分、剩余挂单)、市价单(只指定数量,按对手方最优价逐档吃单,不进入订单簿)、IOC(立即成交剩余撤销)、FOK(全部立即成交否则整单撤销)、Post-Only(只做挂单,若会立即成交则拒单)。不同交易所对同一名称的语义可能有细微差异,重建前必须核对交易所规则文档,不能凭经验假设。
每笔订单在模型中应具备明确的生命周期状态:新建 → 部分成交 → 全部成交,或 新建 → 撤销,或 新建 → 部分成交 → 撤销。状态迁移必须由逐笔消息驱动,且每次迁移都要记录触发消息的序号与时间戳。这是后续“一致性”校验的锚点:任何时刻,订单簿中所有活动订单的数量之和,应等于对应档位聚合量;任何一笔成交,都应能对应到买卖双方订单数量的同步减少。
三、撮合规则与价格时间优先
连续竞价的核心规则是价格优先、时间优先:新到订单先与对手方最优价格档撮合;同一价格档内,先到达的订单先成交。以图中示例说明:买一档 10.00 有 #A01(80)、#A02(60)、#A03(40) 三笔订单,按到达顺序排队。此时新到卖单 S1 价格 10.00、数量 100,因卖价 10.00 ≤ 买一价 10.00,可立即成交。撮合按队列顺序进行:#A01 的 80 全部成交并移出队列,#A02 成交 20、剩余 40 仍居队首,买一档总量由 180 降为 80。若 S1 数量为 200,则 #A01、#A02 全部成交,#A03 成交 60 剩 0,买一价档位被清空,最优买价下移至 9.98。
这里有两个容易出错的细节。其一,成交价取被动方价格:S1 以 10.00 成交,是因为它吃的是挂在 10.00 的买单,而非以自身报价成交。其二,部分成交不改变时间优先级:剩余 40 的 #A02 仍排在 #A03 之前,重建时若把部分成交订单重新入队到队尾,就会破坏时间优先,导致后续回测的成交概率失真。
四、统一数据模型的关键字段
为了让重建与回测共用一套结构,建议以订单事件流为核心,定义如下最小字段集(示例为合理假设的字段设计,非某交易所实测格式):
| 字段 | 含义 | 示例 |
|---|---|---|
| seq | 交易所消息序号,单调递增 | 100237 |
| ts | 事件时间戳(纳秒或毫秒,全文统一) | 1710000000123 |
| type | 事件类型:ADD / CANCEL / TRADE / MODIFY | ADD |
| order_id | 订单唯一标识 | A02 |
| side | 买卖方向 | BID |
| price | 限价(市价单为空) | 10.00 |
| qty | 本次事件数量 | 60 |
| remaining | 事件后剩余量 | 40 |
其中 seq 是重建顺序的唯一权威依据,ts 仅用于回测中的时间对齐与延迟建模。二者不可混用:同一毫秒内可能有多条消息,只有 seq 能确定真实先后。重建时应严格按 seq 回放,遇到序号缺口立即标记为数据不完整,而不是用时间戳猜测补全。