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 如何维护本地订单簿?
拿到快照和增量后,我们需要在本地维护一个「影子订单簿」。流程是这样的:
- 连接建立后,先请求一次深度快照,初始化本地订单簿
- 之后持续接收增量更新,逐条应用到本地订单簿
- 定期检查本地订单簿与交易所的一致性(比如每5分钟请求一次快照做对比)
- 发现不一致时,重新请求快照,重置本地订单簿
我建议用哈希表(或字典)来存储订单簿,以价格为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 知识体系总览
下面这张图,是我对本章核心逻辑的总结:
2.7 实战中的几个坑
最后分享几个我踩过的坑,希望能帮你省点时间:
- 快照和增量混在一起:有些交易所会在增量流中偶尔插入快照,别傻傻地当增量处理了
- 价格精度问题:浮点数比较时,用整数(如最小价格单位)来存储,避免精度误差
- 内存泄漏:如果增量更新中出现了异常价格(比如价格极低或极高),要及时过滤,否则订单簿会无限膨胀
我曾经因为没处理好价格过滤,导致本地订单簿积累了上万个无效档位,内存直接爆了。嗯,那次教训挺深刻的。
好了,关于订单簿数据结构就聊这么多。记住一句话:快照是基础,增量是灵魂,一致性是生命线。