2. 订单簿数据结构与消息协议
要做快照与增量消息的一致性校验,第一步不是写比对代码,而是把“订单簿长什么样”和“消息怎么描述它”这两件事彻底弄清楚。校验的本质是:用增量消息在本地重建出的订单簿,必须与交易所下发的快照逐档、逐笔完全一致。如果对数据结构或协议字段的理解有偏差,后续所有比对都会建立在错误的前提上。
限价订单簿的买卖盘结构
限价订单簿(Limit Order Book,LOB)由买盘(Bid)和卖盘(Ask)两侧组成。买盘是愿意买入的挂单集合,卖盘是愿意卖出的挂单集合。两侧各自按价格排序:买盘价格从高到低,卖盘价格从低到高,最优买价(Best Bid)和最优卖价(Best Ask)分别位于两侧的顶端,二者之差即买卖价差(Spread)。
订单簿在逻辑上是“价格档位 → 订单队列”的两级结构。同一价格上的所有订单构成一个价格档位(Price Level),档位内按时间优先原则排队,先到者排在队首,撮合时优先成交。因此一个档位至少需要记录两个量:该价位的聚合数量(所有订单剩余数量之和)和订单笔数。很多协议只下发聚合数量,不暴露单笔订单,这时本地重建也只能维护到档位粒度;若协议携带订单级明细,则需额外维护订单 ID 到档位的映射。
这里有一个容易出错的约定:价格通常以整数最小变动单位(tick)表示,数量也以整数最小交易单位表示。校验时若一方用浮点价格、另一方用整数 tick,必须统一换算,否则会出现“看起来差一点”的伪不一致。下文所有示例均约定价格为整数 tick、数量为整数单位。
快照消息与增量消息的字段定义
快照(Snapshot)是某一时刻订单簿的完整切片,通常包含:快照序号(lastUpdateId 或 sequence)、买盘档位数组、卖盘档位数组,每个档位含价格与数量。增量消息(Delta / Update)只描述自上一序号以来发生的变化,典型字段包括:起始序号、结束序号、买卖两侧的变更档位。变更档位的语义有两种常见约定,必须区分清楚:
- 绝对量覆盖:消息给出某价位的最新总量,本地直接替换该档位数量;数量为 0 表示删除该档位。
- 相对量增减:消息给出数量的增减值,本地需在原有数量上累加;累加后为 0 则删除档位。
把这两种语义搞混,是增量重建出错的高频原因。校验程序在读入协议时,应把“覆盖/增减”作为显式配置项,而不是靠猜测。
常见协议格式
同一份订单簿数据可以用不同协议承载,字段名和编码方式差异很大,但语义可以对齐。JSON 可读性最好,常用于 REST 快照与 WebSocket 增量;FIX 是金融行业标准报文,用标签号(如 35=W 表示快照、269=0/1 表示买/卖)表达字段,适合机构对接;SBE(Simple Binary Encoding)是定长二进制编码,字段按 schema 顺序紧凑排列,延迟最低,常见于高频场景。下表给出三种格式在“买盘一档”上的字段映射示例(示例值仅用于说明结构):
| 语义 | JSON 字段 | FIX 标签 | SBE 字段 | 示例值 |
|---|---|---|---|---|
| 快照序号 | lastUpdateId | Tag 52 / 自定义 | sequence | 1024 |
| 买盘价格 | bids[0][0] | Tag 132 | bidPrice | 10050 |
| 买盘数量 | bids[0][1] | Tag 133 | bidSize | 320 |
| 增量起止序号 | U / u | 自定义 Tag | firstSeq / lastSeq | 1025 / 1030 |
无论哪种格式,校验前都应先做一次“归一化”:把消息解析成统一的内部结构(序号、方向、价格、数量、语义类型),再进入重建与比对环节。这样协议差异被隔离在解析层,比对逻辑只需面对一种数据模型。
从消息到本地订单簿的重建
下面这段伪代码展示了增量应用的核心逻辑,重点在于序号衔接判断与覆盖/增减语义的分支处理:
def apply_delta(book, msg, mode="overwrite"):
# 序号连续性检查:上一条已应用序号 + 1 应等于 msg.first_seq
if msg.first_seq != book.last_seq + 1:
raise SequenceGap(book.last_seq, msg.first_seq)
for side, levels in (("bid", msg.bids), ("ask", msg.asks)):
for price, qty in levels:
if mode == "overwrite":
new_qty = qty
else: # 相对增减
new_qty = book.get(side, price, 0) + qty
if new_qty == 0:
book.remove(side, price)
else:
book.set(side, price, new_qty)
book.last_seq = msg.last_seq
return book
重建过程中有三个观察点值得记录:序号是否连续、档位数量是否出现负值、买卖盘是否出现交叉(最优买价 ≥ 最优卖价)。前两者是数据错误信号,第三者通常意味着重建逻辑或消息顺序出了问题。把这些检查嵌入重建流程,比事后逐档比对更容易定位根因。
上图把两级结构、价差关系与一次增量应用放在同一视图中:左侧买盘、右侧卖盘,中间虚线标出价差;紫色消息框给出序号区间与两条变更,箭头指向受影响的档位。应用后买盘 10050 档由 320 变为 200,卖盘 10090 档因数量归零被删除,最优卖价从 10070 上移到 10080。这个例子说明,一次增量可能同时改变档位数量与最优价位置,校验时不能只比数量,还要比档位集合本身。
把上述结构、字段与协议差异梳理清楚后,本地订单簿就成为一个可预测、可检查的数据对象:每一次增量应用都有明确的输入、可观察的中间状态和可判断的结果。下一阶段的一致性校验,正是围绕“快照重建结果”与“增量重建结果”在同一序号点上的逐档比对展开的。