4、订单簿重建:从增量日志重建完整订单簿、数据对齐与校验

做高频交易的人都知道,交易所发过来的行情数据,说白了就是一堆「增量日志」。

每一笔成交、每一次撤单、每一个新限价单,都被记录成一条增量消息。你拿到手的,不是完整的订单簿快照,而是这些零散的「碎片」。

那问题来了——怎么把这些碎片拼回完整的订单簿?

嗯,这就是我今天要聊的「订单簿重建」。我做了这么多年量化,踩过的坑有一半都跟数据重建有关。你想想看,如果重建出来的订单簿都是错的,那后面的策略逻辑再漂亮也是白搭。

4.1 为什么需要重建?

交易所为了节省带宽,不会每毫秒都给你发一张完整的订单簿照片。它们只发「变化」——比如:

  • 「价格 100.50 的买一,数量从 200 变成 180」
  • 「价格 101.00 的卖二,被吃掉了,整层消失」
  • 「新的限价单进来,挂在 100.60,数量 50」

这些增量消息,一条接一条地推过来。你的任务就是:拿上一刻的完整订单簿,应用这些增量,得到下一刻的完整订单簿。

核心公式:

OrderBookt+1 = Apply(OrderBookt, Δt→t+1)

其中 Δ 就是增量日志中的每一条操作。

我在项目中遇到过最头疼的情况:某次网络抖动,漏掉了 3 条增量消息。结果后面的订单簿全部「跑偏」,策略在那段时间疯狂报错。后来我才意识到——重建之前,必须先做数据对齐与校验。

4.2 增量日志的常见格式

不同交易所的增量格式略有不同,但核心字段大同小异。我整理了一个通用结构:

字段 含义 示例
seq_no 消息序列号(全局递增) 1002345
side 买卖方向 bid / ask
price 价格(精确到 tick) 100.50
size 数量(变化后的值) 180
type 操作类型 new / cancel / trade / clear
timestamp 交易所时间戳 2025-01-15 09:30:00.123456

我个人习惯把 seq_no 当作「唯一凭证」。只要 seq_no 是连续的,我就敢放心重建。一旦发现跳号,立刻触发快照补全流程。

4.3 重建流程:三步走

重建订单簿,我一般分三步。每一步都有对应的校验逻辑,防止数据「带病运行」。

4.3.1 第一步:获取基准快照

你需要一个「干净的起点」。交易所通常每隔一段时间(比如 1 秒或 10 毫秒)会发一次完整快照。

  • 快照包含当前所有价格档位的完整买卖盘口
  • 快照本身也带一个 seq_no,表示「这个快照对应到哪个时刻」

拿到快照后,我建议先做一次自检:

  • 检查买卖盘口是否平衡(买一价 ≤ 卖一价?)
  • 检查价格档位是否按价格排序
  • 检查数量是否为正数

注意: 有些交易所的快照可能包含「脏数据」。我曾经遇到过某次快照里,买一和卖一价格重叠了 0.01 元——这明显是交易所内部状态不一致导致的。遇到这种情况,直接丢弃该快照,等下一个。

4.3.2 第二步:按序应用增量

有了基准快照,接下来就是把增量一条一条「喂」进去。

伪代码大概是这样的:

def rebuild_orderbook(snapshot, deltas):
    # 1. 从快照初始化订单簿
    ob = OrderBook()
    ob.load_snapshot(snapshot)
    
    # 2. 按 seq_no 排序增量
    sorted_deltas = sorted(deltas, key=lambda d: d.seq_no)
    
    # 3. 逐条应用
    for delta in sorted_deltas:
        # 校验:当前增量 seq_no 必须等于上一增量 seq_no + 1
        if delta.seq_no != expected_seq_no:
            raise DataGapError(f"丢失增量: 期望 {expected_seq_no}, 实际 {delta.seq_no}")
        
        # 应用增量
        if delta.type == 'new':
            ob.add_level(delta.side, delta.price, delta.size)
        elif delta.type == 'cancel':
            ob.update_level(delta.side, delta.price, delta.size)
        elif delta.type == 'trade':
            ob.reduce_level(delta.side, delta.price, delta.size)
        elif delta.type == 'clear':
            ob.remove_level(delta.side, delta.price)
        
        expected_seq_no += 1
    
    return ob

这里有个细节:增量消息中的 size 是「变化后的值」,不是变化量。比如 cancel 操作,size=0 表示这一层被完全撤掉;size=50 表示这一层还剩 50 手。

4.3.3 第三步:重建后校验

增量全部应用完后,别急着用。先做一轮校验:

  • 价格档位连续性: 买盘从高到低,卖盘从低到高,中间不能有「空洞」
  • 数量合理性: 单档数量不能超过交易所规定的最大限制
  • 价差检查: 买一和卖一的价差应该在合理范围内(比如 1 个 tick)
  • 交叉检查: 买一价必须小于卖一价,否则就是「交叉盘」,说明数据有问题

我的小技巧: 重建完成后,我会把当前订单簿的哈希值(比如所有价格档位的拼接字符串的 MD5)存下来。下次重建时,如果哈希值对不上,说明中间有数据被篡改或丢失了。这在回测中特别有用。

4.4 数据对齐:多源数据的时间同步

实际生产中,你往往不止一个数据源。比如:

  • 行情数据来自交易所的 WebSocket
  • 成交数据来自交易所的 REST API
  • 自成交数据来自你自己的交易系统

这些数据源的时间戳可能不一致。怎么对齐?

我个人的做法是:以交易所的 seq_no 为唯一对齐基准。时间戳只作为辅助参考,不用于对齐。

为什么?因为网络延迟会导致时间戳漂移。你本地收到消息的时间,跟交易所发出消息的时间,可能差了几十毫秒。但 seq_no 是交易所内部生成的,严格递增,不会受网络影响。

4.5 常见坑与避坑指南

做订单簿重建这些年,我踩过的坑可以写一本小册子了。挑几个典型的说说:

  • 坑一:增量消息乱序到达
    交易所的 WebSocket 可能因为网络重传,导致后发的消息先到。解决方案:不要立即应用,先缓存起来,按 seq_no 排序后再处理。
  • 坑二:快照与增量之间的「缝隙」
    快照的 seq_no 是 1000,但第一条增量的 seq_no 是 1002。中间丢了 1001?这时候不能直接跳过,必须请求补发 1001,或者重新获取快照。
  • 坑三:增量消息中的 size 为 0
    有些交易所用 size=0 表示「删除这一层」,有些交易所则用单独的 delete 操作。一定要仔细看文档,别搞混了。

我曾经踩过的坑: 有一次,交易所的增量日志里出现了一条「size 为负数」的消息。我当时没做校验,直接应用了,结果订单簿的某一层数量变成了 -50。后面的所有计算全部崩溃。从那以后,我在应用增量之前,一定会加一层「数据合理性校验」——价格不能为负,数量不能为负,价格不能为 0。

4.6 核心逻辑流程图

下面这张图,是我自己画的一个「订单簿重建核心流程」。每次做新项目,我都会把它贴在墙上:

订单簿重建核心流程 获取基准快照 校验快照完整性 缓存增量消息 按 seq_no 排序 逐条应用增量 重建后校验 从交易所获取 完整快照 检查价格、数量 买卖盘平衡 等待所有增量 到达本地 保证顺序正确 new/cancel/trade 价差、交叉检查

4.7 性能优化建议

订单簿重建如果做得不好,会成为整个系统的瓶颈。尤其是当你有几十个交易对同时重建时,性能问题会非常突出。

我分享几个优化思路:

  • 使用红黑树或跳表: 订单簿的底层数据结构,建议用红黑树(C++)或 TreeMap(Java)。插入、删除、查找都是 O(log n),比线性数组快得多。
  • 预分配内存: 每个价格档位的大小是固定的,可以预先分配好内存池,避免频繁的 new/delete。
  • 批量处理: 如果增量消息密集到达(比如每秒 10 万条),可以攒一批再处理,减少上下文切换。
  • 增量压缩: 有些交易所会发送「冗余增量」——比如同一价格档位连续更新多次。你可以合并这些更新,只保留最后一次。

我的经验: 在 C++ 实现中,我用的是 std::map 作为价格档位的容器,配合自定义的内存池分配器。实测下来,单线程每秒可以处理 200 万条增量消息。对于大部分场景,这个性能已经足够了。

4.8 总结

订单簿重建,说白了就是「快照 + 增量」的拼图游戏。核心要点就三个:

  • 基准要稳: 快照必须干净、完整
  • 顺序要对: 增量必须按 seq_no 严格排序
  • 校验要严: 重建前后都要做数据校验

做到这三点,你的订单簿重建就不会出大问题。剩下的,就是根据具体交易所的协议细节,做一些微调了。


公众号:蓝海资料掘金营,微信deep3321