4. 订单簿快照与增量更新:如何高效同步市场数据
做量化交易的朋友都知道,订单簿数据是高频策略的命脉。但这里有个很现实的问题——交易所的订单簿每秒都在变,你总不能每次都把整个订单簿拉下来吧?带宽扛不住,延迟也受不了。
我个人习惯把这个问题拆成两个部分来看:快照和增量。说白了,快照就是给你一张完整的地图,增量就是告诉你哪里修路了、哪里盖楼了。两者配合,才能做到既省流量又实时。
4.1 为什么不能只靠全量快照?
你想想看,一个深度还不错的币对,订单簿里可能有几千甚至上万个价位。每次更新都全量推送,数据量有多大?我算过一笔账:
| 更新方式 | 单次数据量 | 每秒更新次数 | 每秒总流量 |
|---|---|---|---|
| 全量快照 | ~200KB | 10次 | ~2MB |
| 增量更新 | ~2KB | 100次 | ~200KB |
看到了吧?增量更新的流量只有全量的十分之一。而且这还只是单个币对,如果你同时跑几十个币对呢?带宽直接爆炸。
核心原则:全量快照用于初始化,增量更新用于持续同步。两者缺一不可。
4.2 增量更新的数据结构
增量更新到底长什么样?我拿一个真实的交易所协议来举例。每条增量消息通常包含这几个字段:
- side:买盘还是卖盘(bid/ask)
- price:价格
- size:数量(如果为0,表示删除这个价位)
- sequence:序列号(用于校验顺序)
举个例子,一条增量消息可能是这样的:
{
"side": "bid",
"price": 50000.0,
"size": 1.5,
"sequence": 123456
}
这条消息的意思是:在50000这个价位上,买盘的数量变成了1.5个BTC。如果之前有这个价位,就覆盖;如果没有,就新增。如果size是0,那就删掉这个价位。
小技巧:我个人习惯在本地维护一个字典结构,key是价格,value是数量。收到增量消息后,直接更新字典。这样查询速度极快,O(1)复杂度。
4.3 快照与增量的衔接
这里有个坑,我踩过不止一次。你想想看:你请求了一个全量快照,但在快照返回的过程中,交易所又产生了新的增量消息。如果你先处理快照再处理增量,那中间这段时间的增量就丢了。
正确的做法是这样的:
- 先订阅增量消息流,但先不处理,缓存起来
- 请求全量快照
- 收到快照后,丢弃所有序列号小于快照序列号的增量
- 按顺序处理剩余的增量消息
嗯,这里要注意:序列号是关键。每个增量消息都带一个递增的序列号,快照也带一个序列号。只有序列号大于快照序列号的增量,才是有效的。
避坑指南:我曾经在某个项目中忽略了序列号校验,结果订单簿数据一直对不上。排查了两天才发现,原来是增量消息的顺序乱了。从那以后,我每次都会在本地维护一个lastSequence变量,严格校验。
4.4 本地订单簿的维护
有了快照和增量,怎么在本地维护一个完整的订单簿?我一般用两个红黑树(或者跳表)来存,一个存买盘,一个存卖盘。为什么用红黑树?因为我们需要按价格排序,而且插入删除操作频繁。
伪代码大概是这样的:
class OrderBook:
def __init__(self):
self.bids = SortedDict() # 买盘,价格降序
self.asks = SortedDict() # 卖盘,价格升序
self.last_sequence = 0
def apply_snapshot(self, snapshot):
self.bids.clear()
self.asks.clear()
for level in snapshot['bids']:
self.bids[level['price']] = level['size']
for level in snapshot['asks']:
self.asks[level['price']] = level['size']
self.last_sequence = snapshot['sequence']
def apply_update(self, update):
if update['sequence'] <= self.last_sequence:
return # 丢弃过期消息
side = self.bids if update['side'] == 'bid' else self.asks
if update['size'] == 0:
side.pop(update['price'], None) # 删除价位
else:
side[update['price']] = update['size'] # 新增或覆盖
self.last_sequence = update['sequence']
这段代码看起来简单,但实际生产环境里要考虑的东西多得多。比如网络抖动导致的消息重传、交易所偶尔推送的乱序消息、还有快照和增量之间的时间窗口问题。
4.5 核心逻辑流程图
下面这张图是我自己画的一个流程,帮你理清快照和增量的关系:
这张图把整个流程串起来了。你从「开始同步」出发,先订阅增量但不处理,然后请求快照,应用快照后再处理增量。顺序不能乱,否则数据就歪了。
4.6 实际项目中的经验
最后聊点实战经验。我在做高频交易系统的时候,遇到过几个比较头疼的问题:
- 快照太大:有些交易所的快照有几百KB,网络不好的时候传输时间很长。我的做法是压缩传输,用gzip或者snappy,能省60%以上的带宽。
- 增量丢失:偶尔会有几条增量消息没收到。这时候不能硬撑,得重新请求一次快照。我一般设置一个超时机制,如果连续5秒没收到增量,就触发重新同步。
- 内存占用:订单簿的价位越多,内存占用越大。对于深度很大的币对,我建议只保留前100档价位,后面的直接忽略。反正交易量也集中在头部。
总结一下:快照负责初始化,增量负责持续更新。两者通过序列号衔接,缺一不可。本地用红黑树或跳表维护,注意处理乱序和丢失的情况。做到这几点,你的订单簿同步基本就稳了。
额外提示:如果你用的是WebSocket,记得开启心跳机制。我见过不少因为连接断开导致数据中断的情况,心跳能帮你及时发现并重连。
公众号:蓝海数据掘金营,微信deep3321