3. 订单簿数据结构:Level1/Level2/Level3数据、订单簿重建、增量更新、快照同步

做市系统里,订单簿就是我们的「战场地图」。你想想看,没有这张地图,你连对手的兵力部署都看不清,还怎么打仗?

我个人习惯把订单簿数据分成三个层级来理解。这就像看照片——Level1是缩略图,Level2是高清图,Level3是原始RAW文件。每个层级都有它的用途,也有它的代价。

3.1 Level1、Level2、Level3,到底差在哪?

先说说最常见的Level1数据。说白了,就是最优买卖价和成交量。交易所通常会推送这样的数据:

{
  "bid": 100.50,    // 最高买价
  "ask": 100.55,    // 最低卖价
  "bid_vol": 1000,  // 买一量
  "ask_vol": 800,   // 卖一量
  "last": 100.52,   // 最新成交价
  "volume": 50000   // 累计成交量
}

我在项目中遇到过不少新手,觉得Level1就够了。嗯,如果你只是看看行情,确实够用。但做市不一样——我们需要知道更深层的流动性分布。

Level2数据就丰富多了。它通常包含多个档位的买卖盘口,比如前10档或前50档。我记得有一次调试一个高频策略,发现Level1显示买盘很强,但Level2一看,买一到买五全是小单,真正的大单在买十挂着。这种「虚假深度」在Level1里根本看不出来。

Level3数据呢?这是最完整的。它包含订单簿上每一笔挂单的详细信息——订单ID、价格、数量、时间戳,甚至是谁挂的。为什么需要这个?举个例子:

  • 订单簿重建:系统崩溃后,你需要用Level3数据完整恢复订单簿状态
  • 大单追踪:通过订单ID可以识别同一笔大单是否被部分成交
  • 冰山订单识别:同一ID在不同价格出现,大概率是冰山单

核心区别总结

层级 数据量 延迟 典型用途
Level1 极小 最低 行情展示、简单策略
Level2 中等 较低 做市策略、深度分析
Level3 极大 较高 订单簿重建、风控审计

3.2 订单簿重建——从零开始拼地图

假设你的系统刚启动,或者断线重连了。这时候交易所会给你一个「快照」,也就是当前时刻订单簿的完整状态。你需要用这个快照重建出订单簿。

我曾经踩过一个坑:快照数据里,价格和数量都是对的,但顺序是乱的。如果你直接用Python的dict去存,遍历的时候顺序不对,盘口显示就全乱了。

正确的做法是用有序字典:

from collections import OrderedDict

class OrderBook:
    def __init__(self):
        self.bids = OrderedDict()  # 买盘,价格降序
        self.asks = OrderedDict()  # 卖盘,价格升序
    
    def rebuild_from_snapshot(self, snapshot):
        """从快照重建订单簿"""
        self.bids.clear()
        self.asks.clear()
        
        # 买盘按价格降序排列
        for bid in sorted(snapshot['bids'], key=lambda x: -x[0]):
            self.bids[bid[0]] = bid[1]
        
        # 卖盘按价格升序排列
        for ask in sorted(snapshot['asks'], key=lambda x: x[0]):
            self.asks[ask[0]] = ask[1]
    
    def get_top_n(self, n=5):
        """获取前n档盘口"""
        top_bids = list(self.bids.items())[:n]
        top_asks = list(self.asks.items())[:n]
        return top_bids, top_asks

小技巧:重建时别忘了校验快照的时间戳。如果快照太旧(比如超过1秒),最好等下一个快照。我见过有人用5秒前的快照重建,结果盘口价格差了0.3%,直接导致策略误判。

3.3 增量更新——别每次都重建

快照重建很费资源。你想想看,如果每秒重建一次,CPU和内存都扛不住。所以交易所会同时推送「增量更新」——只告诉你哪些订单变了。

增量更新的消息通常有三种类型:

  • 新增:新挂了一笔订单
  • 删除:一笔订单被撤单或成交
  • 修改:订单数量变了(比如部分成交)

处理逻辑其实不复杂:

def apply_update(self, update):
    """应用增量更新"""
    action = update['action']  # 'add', 'delete', 'modify'
    side = update['side']      # 'bid' or 'ask'
    price = update['price']
    volume = update['volume']
    
    book = self.bids if side == 'bid' else self.asks
    
    if action == 'add':
        book[price] = volume
    elif action == 'delete':
        if price in book:
            del book[price]
    elif action == 'modify':
        if price in book:
            book[price] = volume
        else:
            # 这种情况很少见,但发生过
            print(f"警告:修改不存在的价格 {price}")

注意:增量更新和快照之间可能存在「间隙」。比如你收到快照后,又收到了几条增量,但中间漏了一条。这时候你的订单簿就会「漂移」——和交易所的真实状态不一致。

我曾经因为这个问题亏过钱。解决方案是:定期(比如每10秒)请求一次新快照,和当前状态做对比。如果差异超过阈值,就强制重建。

3.4 快照同步——保证数据一致性

快照同步是订单簿管理的核心难点。为什么这么说?因为快照和增量是两条并行的数据流,它们之间需要精确对齐。

标准的做法是这样的:

  1. 请求快照时,记录下快照的序列号(seq_no)
  2. 从该序列号开始,缓存所有增量更新
  3. 快照到达后,按顺序应用缓存的增量
  4. 之后每来一条增量,直接应用

用代码表示就是:

class OrderBookSync:
    def __init__(self):
        self.book = OrderBook()
        self.pending_updates = []
        self.snapshot_seq = None
        self.last_applied_seq = None
    
    def on_snapshot(self, snapshot):
        self.snapshot_seq = snapshot['seq_no']
        self.book.rebuild_from_snapshot(snapshot)
        
        # 应用缓存的增量
        for update in self.pending_updates:
            if update['seq_no'] > self.snapshot_seq:
                self.book.apply_update(update)
                self.last_applied_seq = update['seq_no']
        
        self.pending_updates.clear()
    
    def on_update(self, update):
        if self.last_applied_seq is None:
            # 还没收到快照,先缓存
            self.pending_updates.append(update)
        else:
            if update['seq_no'] == self.last_applied_seq + 1:
                self.book.apply_update(update)
                self.last_applied_seq = update['seq_no']
            else:
                # 序列号不连续,需要重新同步
                print("序列号跳跃,请求新快照...")
                self.request_snapshot()

3.5 核心逻辑流程图

下面这张图展示了订单簿数据流的完整链路。我建议你仔细看看,尤其是「快照同步」那部分——这是最容易出问题的地方。

订单簿数据流核心逻辑 交易所数据流 快照 (Snapshot) 增量更新 (Update) 缓存增量 (Pending) 订单簿重建 快照同步 (序列号对齐) 可用订单簿

这张图里,最关键的就是「缓存增量」和「快照同步」这两个环节。我见过太多人忽略了缓存这一步,结果快照到了之后,中间漏掉的增量就永远丢失了。

实战建议

  • 缓存增量时,建议用队列(queue)而不是列表。队列可以控制最大长度,防止内存溢出
  • 序列号最好用uint64,别用int。我遇到过序列号溢出导致同步失败的情况
  • 每处理1000条增量,做一次自检——计算当前订单簿的总量和快照时的总量对比

好了,订单簿的数据结构就讲到这里。说白了,核心就三件事:快照重建、增量更新、序列号同步。把这三点吃透了,大部分交易所的数据你都能轻松驾驭。

专注资料整理