4、Level 2 数据:深度盘口(Order Book Depth)的聚合与快照

好,咱们接着聊。前面几章我们把订单簿的底层逻辑和逐笔成交都捋了一遍。今天要聊的,是真正让量化交易员「上瘾」的东西——深度盘口,也就是 Level 2 数据的核心。

说白了,Level 1 数据只告诉你「现在买一价是多少,卖一价是多少」。但 Level 2 数据,它把整个订单簿的「肚子」都翻给你看。你能看到买一到买十,甚至买五十档的所有挂单。这玩意儿,才是真正能用来做策略的原料。

4.1 什么是深度盘口?

深度盘口,英文叫 Order Book Depth。它本质上就是订单簿在不同价格水平上的挂单量分布。

我举个例子。假设某只股票当前价格是 100 元。Level 1 数据告诉你:买一 99.99 元,1000 股;卖一 100.01 元,800 股。但 Level 2 数据会告诉你:

  • 买一到买十,每个价位各挂了多少钱
  • 卖一到卖十,每个价位各挂了多少钱
  • 甚至能看到每个价位的挂单笔数

你想想看,这信息量完全不是一个量级。

核心概念:深度盘口 = 价格 + 挂单量 + 挂单笔数(部分交易所提供)

4.2 聚合:把散单变成「墙」

这里有个关键问题。交易所推送的 Level 2 数据,通常是逐笔的。什么意思?就是每来一个订单,它就推送一条。如果你直接拿这个数据去算盘口,那你的 CPU 会哭的。

所以我们需要做聚合

聚合的逻辑其实很简单:

  1. 把所有买单按价格从高到低排序
  2. 把所有卖单按价格从低到高排序
  3. 相同价格的订单,把数量加起来
  4. 生成一个「价格-总量」的映射表

嗯,这里要注意。我在项目中遇到过一个问题:有些交易所的订单是支持「冰山订单」的。就是大单只显示一部分,剩下的藏起来。如果你只聚合可见部分,那你的盘口深度其实是「假」的。这个坑,我后面会专门讲。

4.3 快照:给盘口拍一张「照片」

聚合完了,接下来就是快照

快照的意思,就是在某个时间点,把整个订单簿的状态完整地「拍」下来。这个快照,就是你策略的输入。

快照通常包含以下字段:

字段 说明 示例
timestamp 快照时间戳 2024-01-15 09:30:00.123
bids 买单列表(价格从高到低) [[99.99, 1000], [99.98, 2000], ...]
asks 卖单列表(价格从低到高) [[100.01, 800], [100.02, 1500], ...]
depth_levels 深度档位数 10

我个人习惯,快照的生成频率不要太高。一般 100ms 到 500ms 一次就够了。太频繁了,数据量太大,反而容易把策略跑崩。

4.4 核心逻辑:从增量到全量

这里有个重要的设计模式,叫增量更新 + 全量快照

交易所推送的 Level 2 数据,通常是增量数据。比如:

  • 「买一价 99.99 增加了 500 股」
  • 「卖三价 100.05 被撤单了 200 股」
  • 「卖五价 100.10 全部成交了」

你的程序需要维护一个本地的订单簿副本。每来一条增量,就更新这个副本。然后每隔一段时间,把这个副本「拍」成快照。

为什么会这样设计?

因为全量快照太贵了。如果每次更新都重新排序、重新聚合,那计算量是 O(n log n) 的。而增量更新,只需要 O(1) 或 O(log n) 的操作。

我的经验:我曾经在一个高频策略里,把快照频率设成了 10ms。结果程序跑了 5 分钟,内存就爆了。后来改成 200ms 一次,配合增量更新,稳如老狗。

4.5 代码示例:一个简单的盘口聚合器

光说不练假把式。我写个简单的 Python 示例,帮你理解这个过程。

class OrderBookDepth:
    def __init__(self, depth_levels=10):
        self.bids = {}  # 价格 -> 总量
        self.asks = {}
        self.depth_levels = depth_levels
    
    def update(self, side, price, size):
        """增量更新"""
        if side == 'buy':
            if size == 0:
                # 撤单或全部成交
                self.bids.pop(price, None)
            else:
                self.bids[price] = size
        else:
            if size == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = size
    
    def snapshot(self):
        """生成快照"""
        # 买单按价格降序排列
        sorted_bids = sorted(self.bids.items(), reverse=True)[:self.depth_levels]
        # 卖单按价格升序排列
        sorted_asks = sorted(self.asks.items())[:self.depth_levels]
        
        return {
            'timestamp': time.time(),
            'bids': [[p, s] for p, s in sorted_bids],
            'asks': [[p, s] for p, s in sorted_asks]
        }

这段代码很简单,但核心逻辑都在里面了。你想想看,实际生产环境里,你还要处理并发、数据校验、异常恢复等问题。但原理就是这个原理。

4.6 避坑指南:我曾经踩过的坑

聊几个我实际踩过的坑,希望能帮你省点时间。

坑一:价格精度问题

我曾经在聚合时,直接用浮点数做字典的 key。结果因为浮点数精度问题,99.99 和 99.9900001 被当成了两个不同的价格。后来我改用整数(比如把价格乘以 10000)来存储,问题就解决了。

坑二:快照与增量的时序

如果你同时接收增量和全量快照,一定要保证时序正确。我见过有人先处理了全量快照,然后又处理了之前的增量,结果盘口数据全乱了。正确的做法是:收到全量快照后,先清空本地订单簿,再应用快照,之后再处理增量。

坑三:冰山订单的隐藏量

有些大资金会用冰山订单来隐藏真实意图。如果你只看可见量,你的盘口深度分析就是错的。我建议在策略层面,对盘口深度做一个「置信度」的调整。比如,如果某个价位连续出现多次小单,那背后很可能有冰山订单。

4.7 知识体系图

下面这张图,帮你理清本章的核心逻辑。

深度盘口聚合与快照核心流程 交易所 Level 2 数据 增量更新(逐笔) 聚合器 按价格合并 + 排序 本地订单簿副本 bids + asks 字典 快照生成器 定时拍快照(100-500ms) 深度盘口快照 关键要点: • 增量更新:O(1) 或 O(log n) 复杂度 • 全量快照:O(n log n) 复杂度,但频率低 • 快照频率:100ms-500ms 为宜 • 注意:浮点数精度、冰山订单、时序问题 • 输出格式:[[price, size], ...]

这张图把整个流程串起来了。从交易所的增量数据,到聚合器,到本地订单簿,再到快照生成。每一步都有它的意义。

4.8 小结

深度盘口的聚合与快照,说白了就是两件事:

  • 聚合:把散乱的订单按价格合并,形成有序的买卖盘
  • 快照:在特定时间点,把订单簿的状态「拍」下来,供策略使用

我个人觉得,这部分是量化交易中最「实在」的知识。你理解了它,就理解了市场微观结构的核心。后面讲盘口数据计算、订单流分析,都是在这个基础上展开的。

嗯,今天就到这里。记住,代码可以抄,但坑得自己踩一遍才能真正记住。希望我踩过的那些坑,能帮你少走点弯路。


专注资料整理