3、订单簿快照与增量更新:快照数据的生成,增量更新的机制(如逐笔委托),以及如何合并两者
做量化交易的朋友都知道,订单簿是市场的「心跳」。但心跳怎么记录、怎么传输、怎么还原,这里面门道不少。
我刚开始接触高频数据时,也踩过不少坑。最典型的一个——以为拿到快照就万事大吉了。结果回测时发现,订单簿状态根本对不上。后来才明白,快照和增量更新,缺一不可。
3.1 快照数据是怎么生成的?
快照,说白了就是某一时刻订单簿的「全家福」。它记录了当前所有挂单的价格、数量、方向。
交易所生成快照的时机,一般有两种:
- 定时生成:比如每100毫秒拍一张。我见过有些平台甚至每秒拍10张。
- 事件触发:比如每发生N笔成交,或者订单簿变化超过阈值,就生成一次。
快照的数据结构,通常长这样:
{
"timestamp": 1628000000123,
"bids": [
[100.50, 2000], // 价格, 数量
[100.49, 1500],
...
],
"asks": [
[100.51, 1800],
[100.52, 2200],
...
]
}
这里有个细节——快照里的价格档位,通常只保留前N档。比如只保留买一至买十、卖一至卖十。为什么?因为全量数据太大了,传输和存储都扛不住。
核心要点:快照是「静态的切片」,它告诉你「现在长什么样」。但光有快照,你无法知道中间发生了什么。
3.2 增量更新机制:逐笔委托
增量更新,就是记录「变化」。最常见的增量形式,就是逐笔委托。
每一笔委托进来,交易所会广播一条消息。消息里包含:
- 委托ID(唯一标识)
- 方向(买/卖)
- 价格
- 数量
- 操作类型(新增、撤销、成交)
举个例子:
// 新增买单
{
"type": "add",
"side": "bid",
"price": 100.51,
"size": 500,
"order_id": "abc123"
}
// 撤销卖单
{
"type": "cancel",
"side": "ask",
"price": 100.52,
"size": 300,
"order_id": "def456"
}
// 部分成交
{
"type": "trade",
"side": "bid",
"price": 100.50,
"size": 100,
"order_id": "ghi789"
}
嗯,这里要注意——增量更新的顺序,必须严格按时间排列。一旦顺序乱了,订单簿就重建不回去了。我在项目中遇到过,因为网络延迟导致增量乱序,结果回测数据全废了。后来加了序列号校验,才解决这个问题。
个人经验:建议在接收增量时,给每条消息打上本地时间戳和交易所序列号。这样即使乱序,也能通过排序恢复。
3.3 快照与增量如何合并?
这才是核心问题。你想想看,快照是静态的,增量是动态的。怎么把它们拼起来?
标准做法是三步走:
- 拿到一个基准快照:作为初始状态。
- 记录快照之后的增量:从快照时间点开始,缓存所有增量消息。
- 逐条应用增量:把增量按顺序「回放」到快照上。
用代码表示,大概是这样:
class OrderBook:
def __init__(self, snapshot):
self.bids = snapshot['bids'] # 买盘
self.asks = snapshot['asks'] # 卖盘
self.seq = snapshot['seq'] # 快照序列号
def apply_update(self, update):
# 检查序列号是否连续
if update['seq'] != self.seq + 1:
# 这里要处理丢包或乱序
self.resync()
return
if update['type'] == 'add':
self._add_order(update)
elif update['type'] == 'cancel':
self._cancel_order(update)
elif update['type'] == 'trade':
self._execute_trade(update)
self.seq = update['seq']
def _add_order(self, update):
side = self.bids if update['side'] == 'bid' else self.asks
side.append([update['price'], update['size']])
# 按价格排序
side.sort(key=lambda x: x[0], reverse=(update['side'] == 'bid'))
你可能会问:如果增量丢了怎么办?
嗯,这是个好问题。实际中,增量丢失是常态。解决方案是:
- 定期重新获取快照:比如每5分钟拉一次新快照,然后丢弃之前的增量。
- 序列号校验:发现跳号时,主动请求缺失的增量或重新同步。
避坑指南:我曾经因为没做序列号校验,导致订单簿价格偏离了0.5个tick。回测结果看起来很美,实盘一跑就亏钱。后来花了整整两天排查,才发现是增量丢了一条。
3.4 知识体系总览
下面这张图,是我自己总结的订单簿数据流。你看一眼,应该就能明白整个流程:
从图上你能看到,整个流程是闭环的。交易所产生快照和增量,我们在本地合并重建订单簿,然后喂给策略。策略做出决策后,再发单回交易所。
3.5 实际工程中的几个坑
最后,分享几个我踩过的坑,希望能帮你省点时间:
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 增量乱序 | 订单簿价格异常 | 加序列号校验,缓存乱序消息 |
| 快照与增量时间戳不匹配 | 合并后数据有跳变 | 以快照序列号为基准,丢弃之前的增量 |
| 增量丢失 | 订单簿逐渐偏离真实状态 | 定期重新拉取快照,做全量同步 |
| 内存占用过高 | 缓存了太多增量 | 设置增量缓存上限,超限强制同步 |
说白了,快照和增量更新就是一对「搭档」。快照给你一个起点,增量告诉你每一步怎么走。两者配合好了,你才能还原出真实的订单簿状态。
我个人习惯是,每5分钟强制拉一次新快照。这样即使中间有丢包,最多也就5分钟的数据偏差。对于大多数策略来说,这个精度完全够用了。
一句话总结:快照是「基准」,增量是「变化」。合并时,先拿基准,再按顺序回放变化。别忘了做序列号校验和定期同步。