4、订单簿重建与维护:Level 2/3数据解析、增量更新、快照同步

做高频交易的人都知道,订单簿就是我们的战场地图。没有它,你根本不知道敌人在哪、火力有多猛。今天咱们聊聊怎么把这张地图从零开始画出来,并且实时更新。

说实话,我刚开始接触Level 2数据时,被那密密麻麻的逐笔委托给整懵了。后来才明白,这东西说白了就是交易所把「谁在什么价位挂了多大的单子」一股脑全倒给你。怎么接住、怎么整理,这就是本章要解决的问题。

4.1 订单簿的数据结构设计

先想清楚一个问题:订单簿本质上是个啥?

它就是个价格-数量的映射表。买盘按价格从高到低排,卖盘从低到高排。就这么简单。

我个人习惯用红黑树或者跳表来实现。为什么?因为我们需要频繁地插入、删除、修改,还要能快速拿到最优买卖价。Python里我一般用sortedcontainers库,或者自己手撸一个跳表。

核心数据结构:

  • 买盘(Bid):价格降序排列,每个价格对应一个委托队列
  • 卖盘(Ask):价格升序排列,每个价格对应一个委托队列
  • 委托队列:同一价格下的所有委托,按时间顺序排列
class OrderBook:
    def __init__(self):
        self.bids = SortedDict(lambda k: -k)  # 买盘,价格降序
        self.asks = SortedDict()              # 卖盘,价格升序
        self.orders = {}                      # 委托ID -> 委托对象
        self.last_update_id = 0               # 用于快照同步

嗯,这里要注意:orders字典是必须的。我在项目中遇到过一个问题——增量更新时只给了委托ID,没有价格信息。如果没有这个映射表,你根本不知道要去哪个价位上删单子。

4.2 Level 2/3数据解析

Level 2和Level 3的区别,说白了就是「深度」和「广度」的区别。

数据级别 内容 更新频率 典型用途
Level 1 最优买卖价、成交量 秒级 散户看盘
Level 2 前N档买卖盘口 毫秒级 量化策略
Level 3 全部委托单 微秒级 高频做市

你想想看,Level 3数据里每笔委托都有唯一的ID。这意味着你可以追踪到某个大单是不是被拆成了小单,或者某个机构在偷偷吸筹。

解析的时候,我一般这么处理:

def parse_level2_snapshot(self, data):
    """解析Level 2快照数据"""
    book = OrderBook()
    for bid in data['bids']:
        price = bid[0]
        volume = bid[1]
        book.bids[price] = volume
    for ask in data['asks']:
        price = ask[0]
        volume = ask[1]
        book.asks[price] = volume
    book.last_update_id = data['update_id']
    return book

def parse_level3_incremental(self, data):
    """解析Level 3增量数据"""
    for order in data['orders']:
        if order['type'] == 'NEW':
            self._add_order(order)
        elif order['type'] == 'CANCEL':
            self._cancel_order(order['order_id'])
        elif order['type'] == 'TRADE':
            self._execute_order(order['order_id'], order['volume'])

避坑指南:我曾经在解析Level 3数据时,没处理「部分成交」的情况。结果订单簿里的数量越算越不对。后来加了个executed_volume字段,每次成交都更新剩余量,问题才解决。

4.3 增量更新机制

为什么要有增量更新?你想想看,如果每次行情变化都发一整个快照,那网络带宽得炸。所以交易所只发「变化的部分」,我们自己在本地维护订单簿。

增量更新的核心逻辑就三个操作:

  1. 新增委托:在对应价格上插入一条新委托
  2. 撤销委托:从对应价格上移除一条委托
  3. 成交委托:减少对应委托的数量,如果数量归零则移除
def apply_incremental(self, update):
    """应用增量更新"""
    for action in update['actions']:
        if action['action'] == 'add':
            self._add(action['price'], action['volume'], action['order_id'])
        elif action['action'] == 'delete':
            self._delete(action['order_id'])
        elif action['action'] == 'modify':
            self._modify(action['order_id'], action['new_volume'])
    
    self.last_update_id = update['update_id']

这里有个细节:修改操作。有些交易所允许修改委托数量(比如从100手改成50手),但不允许改价格。如果允许改价格,那就相当于先撤单再重新挂单。

注意:增量更新的顺序极其重要!我曾经因为网络延迟导致更新乱序,订单簿直接崩了。解决方案是给每个更新加一个序列号,本地维护一个「期望的序列号」,如果收到的序列号不连续,就触发快照重同步。

4.4 快照同步策略

说白了,快照同步就是「万一订单簿乱了,怎么重新拉一份正确的」。这在高频交易里是保命技能。

我一般用两种策略:

  • 定时同步:每隔N秒主动请求一次快照,和本地订单簿做对比
  • 异常触发同步:当检测到增量更新序列号不连续时,立即请求快照

同步的流程大概是这样的:

def sync_orderbook(self):
    """快照同步"""
    # 1. 记录当前时间
    sync_time = time.time()
    
    # 2. 请求快照
    snapshot = self.request_snapshot()
    
    # 3. 清空本地订单簿
    self.clear()
    
    # 4. 重建订单簿
    self.rebuild_from_snapshot(snapshot)
    
    # 5. 回放快照之后的增量更新
    for update in self.pending_updates:
        if update['timestamp'] > sync_time:
            self.apply_incremental(update)
    
    # 6. 验证一致性
    assert self.last_update_id == snapshot['update_id']

关键点:步骤5的回放非常重要。因为在你请求快照和收到快照之间,可能已经产生了新的增量更新。如果不回放,订单簿就会「少」一段数据。

4.5 实战中的性能优化

做高频交易,性能就是生命。我分享几个实战经验:

  • 内存池:预分配委托对象,避免频繁GC。Python里可以用__slots__减少内存开销
  • 无锁队列:多线程环境下,用queue.Queue或者collections.deque做生产者-消费者模型
  • 批量处理:不要来一条更新就处理一条,攒够一批再处理,减少上下文切换
  • 预计算:把常用的指标(如买卖价差、订单簿不平衡度)在更新时一并算好,避免查询时再算
class OptimizedOrderBook:
    __slots__ = ('bids', 'asks', 'orders', 'spread', 'imbalance')
    
    def __init__(self):
        self.bids = SortedDict(lambda k: -k)
        self.asks = SortedDict()
        self.orders = {}
        self.spread = 0.0      # 买卖价差,预计算
        self.imbalance = 0.0   # 订单簿不平衡度,预计算
    
    def _update_metrics(self):
        """更新预计算指标"""
        if self.bids and self.asks:
            best_bid = self.bids.peekitem(0)[0]
            best_ask = self.asks.peekitem(0)[0]
            self.spread = best_ask - best_bid
            
            bid_vol = sum(self.bids.values())
            ask_vol = sum(self.asks.values())
            total = bid_vol + ask_vol
            self.imbalance = (bid_vol - ask_vol) / total if total > 0 else 0

我的习惯:在开发阶段,我会给订单簿加一个「校验模式」。每次更新后,都检查买卖盘是否交叉(即最优买价 >= 最优卖价)。如果交叉了,说明数据有问题,立刻报警。这个习惯帮我抓到了好几次数据源的问题。

4.6 知识体系总览

下面这张图是我自己总结的订单簿重建与维护的核心流程,你可以对照着理解:

订单簿重建与维护核心流程 交易所数据流 数据解析层 Level 2快照 / Level 3增量 / 逐笔成交 核心处理层 增量更新(新增/撤销/成交) | 快照同步 | 序列号校验 内存池管理 | 批量处理 | 预计算指标 订单簿存储 (红黑树/跳表) 策略引擎 / 风控系统 关键指标 • 买卖价差 • 订单簿不平衡度 • 委托到达率 • 撤销率 • 成交率 • 序列号连续性 图:订单簿重建与维护的完整数据流

从这张图可以看出来,整个流程是环环相扣的。数据源进来,经过解析、处理、存储,最后输出给策略引擎。任何一个环节出问题,订单簿就废了。

好了,关于订单簿重建与维护,核心内容就这些。记住一句话:数据是命,性能是魂。把这两点抓牢了,你的高频交易系统就稳了一半。


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