2、订单簿数据结构:Level2行情数据解析,深度快照与增量更新。

做市商这行,说白了就是跟订单簿打交道。你每天盯着的那个不断跳动的数字矩阵,就是我们的战场。今天我来聊聊Level2行情数据,这东西到底长什么样,怎么用。

2.1 什么是Level2行情?

很多人以为行情就是买一卖一的价格和成交量。其实那只是Level1,太粗糙了。Level2行情给你的是完整的订单簿深度——你能看到买一到买N档,卖一到卖N档,每一档的挂单量和价格。

我记得刚入行那会儿,有个老交易员跟我说:「小伙子,只看买一卖一,你就像蒙着眼睛过马路。」后来我才明白,真正的博弈都在订单簿的深处。

Level2数据通常包含:

  • 深度快照:某一时刻订单簿的完整状态
  • 增量更新:订单簿发生的变化(新增、撤销、成交)
  • 逐笔成交:每一笔成交的明细

嗯,这里要注意,不同交易所的Level2定义略有不同。比如上交所的Level2和深交所的就不完全一样,但核心逻辑是相通的。

2.2 深度快照:订单簿的「全家福」

深度快照,就是给订单簿拍一张照片。你想想看,在某个时间点,市场上所有挂单的分布情况,都被记录下来了。

一个典型的深度快照数据结构长这样:

{
  "timestamp": 1628000000000,  // 快照时间戳(毫秒)
  "bids": [                     // 买单,从高到低排列
    [100.50, 1000],             // [价格, 数量]
    [100.49, 2000],
    [100.48, 1500],
    ...
  ],
  "asks": [                     // 卖单,从低到高排列
    [100.51, 800],
    [100.52, 1200],
    [100.53, 900],
    ...
  ]
}

我在项目中遇到过一个问题:快照数据量很大,尤其是深度达到50档甚至100档的时候。一次快照可能就有几百KB,如果频繁请求,带宽和CPU都扛不住。

核心要点:深度快照是「全量数据」,用于初始化订单簿。一般只在连接建立时或数据异常时请求一次。

2.3 增量更新:订单簿的「变化日记」

如果每次都传全量快照,那效率太低了。所以交易所提供了增量更新机制——只告诉你订单簿发生了什么变化。

增量更新的格式通常是:

{
  "type": "snapshot" | "update",  // 类型:快照或更新
  "timestamp": 1628000000100,
  "changes": [
    {
      "side": "bid" | "ask",      // 买卖方向
      "price": 100.50,            // 价格
      "quantity": 500             // 新数量(0表示撤销)
    },
    ...
  ]
}

这里有个关键点:quantity为0表示该价格档位的订单全部撤销。我曾经踩过这个坑——以为0是忽略,结果订单簿越积越多,最后完全失真。

避坑指南:处理增量更新时,一定要正确处理quantity=0的情况。这是撤销信号,不是忽略信号。

2.4 如何维护本地订单簿?

拿到快照和增量后,我们需要在本地维护一个「影子订单簿」。流程是这样的:

  1. 连接建立后,先请求一次深度快照,初始化本地订单簿
  2. 之后持续接收增量更新,逐条应用到本地订单簿
  3. 定期检查本地订单簿与交易所的一致性(比如每5分钟请求一次快照做对比)
  4. 发现不一致时,重新请求快照,重置本地订单簿

我建议用哈希表(或字典)来存储订单簿,以价格为key,数量为value。这样增删改查都是O(1)复杂度,性能杠杠的。

下面是一个简单的Python实现思路:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 买单:价格 -> 数量
        self.asks = {}  # 卖单:价格 -> 数量
    
    def apply_snapshot(self, snapshot):
        """应用快照"""
        self.bids.clear()
        self.asks.clear()
        for price, qty in snapshot['bids']:
            self.bids[price] = qty
        for price, qty in snapshot['asks']:
            self.asks[price] = qty
    
    def apply_update(self, update):
        """应用增量更新"""
        for change in update['changes']:
            side = change['side']
            price = change['price']
            qty = change['quantity']
            
            if side == 'bid':
                if qty == 0:
                    self.bids.pop(price, None)  # 撤销
                else:
                    self.bids[price] = qty      # 新增或修改
            else:
                if qty == 0:
                    self.asks.pop(price, None)
                else:
                    self.asks[price] = qty

小技巧:实际生产环境中,建议用有序字典(如Python的sortedcontainers)来维护价格排序,方便快速获取最优买卖价。

2.5 增量更新的时序问题

增量更新是按顺序到达的,但网络延迟可能导致乱序。你想想看,如果先收到后面的更新,再收到前面的,订单簿就乱了。

解决办法有两个:

  • 序列号机制:每个增量更新带一个递增的序列号,本地按序列号顺序处理
  • 时间戳校验:如果发现更新的时间戳比本地最后更新的时间戳还早,直接丢弃

我个人习惯用序列号,因为时间戳可能因为时钟同步问题而不准确。序列号是交易所生成的,绝对有序。

2.6 知识体系总览

下面这张图,是我对本章核心逻辑的总结:

订单簿数据结构核心逻辑 交易所Level2行情 深度快照(全量) 增量更新(变化) 本地订单簿 bids: {price: qty} asks: {price: qty} 做市策略信号提取 关键流程:快照初始化 → 增量持续更新 → 本地维护 → 策略使用 ⚠ 注意:增量更新需处理乱序、quantity=0撤销、定期快照校验一致性 序列号机制是保证增量有序性的最佳实践

2.7 实战中的几个坑

最后分享几个我踩过的坑,希望能帮你省点时间:

  • 快照和增量混在一起:有些交易所会在增量流中偶尔插入快照,别傻傻地当增量处理了
  • 价格精度问题:浮点数比较时,用整数(如最小价格单位)来存储,避免精度误差
  • 内存泄漏:如果增量更新中出现了异常价格(比如价格极低或极高),要及时过滤,否则订单簿会无限膨胀

我曾经因为没处理好价格过滤,导致本地订单簿积累了上万个无效档位,内存直接爆了。嗯,那次教训挺深刻的。

好了,关于订单簿数据结构就聊这么多。记住一句话:快照是基础,增量是灵魂,一致性是生命线