4. 增量更新机制:逐笔成交与订单簿变化事件、序列号与数据对齐
好,咱们今天聊点实在的。
订单簿重建,说白了就是要把市场里那一堆乱七八糟的买卖单,拼回一张完整的“价格-数量”表。但问题来了——市场是活的,每秒都有几百上千笔成交和撤单。你总不能每次都把整个订单簿推倒重来吧?
嗯,这就是增量更新的用武之地。
4.1 为什么需要增量更新?
我刚开始做高频交易系统时,犯过一个低级错误:每次收到行情快照,就全量替换本地订单簿。结果呢?网络延迟稍微一高,订单簿就“跳帧”,策略直接亏钱。
你想想看,如果每秒有5000笔订单变动,你每次都全量拉取,带宽和CPU都扛不住。更关键的是——全量快照之间,你根本不知道中间发生了什么。
所以,增量更新是必须的。它只告诉你“变了什么”,而不是“现在长什么样”。
4.2 逐笔成交与订单簿变化事件
增量更新里,最核心的就是两类事件:
- 逐笔成交(Trade):谁和谁成交了,价格多少,数量多少。
- 订单簿变化(OrderBook Change):新增订单、撤销订单、修改订单。
我个人习惯把这两类事件分开处理。为什么呢?因为成交事件会影响最新价、成交量这些指标,而订单簿变化影响的是盘口深度。混在一起处理,逻辑容易乱。
举个例子,某交易所的增量事件格式可能是这样的:
{
"type": "trade",
"price": 100.5,
"quantity": 200,
"timestamp": 1699000000123
}
{
"type": "orderbook_change",
"side": "bid",
"price": 100.0,
"quantity": 150,
"action": "add" // 或 "remove", "update"
}
注意,有些交易所会把“修改订单”拆成“删除旧单 + 新增新单”。我在对接某家二线交易所时就踩过这个坑——他们文档里写的是“update”,实际发的是两条事件。嗯,这里一定要看文档,更要看实际数据。
4.3 序列号:数据对齐的命根子
增量事件最大的问题是什么?乱序。
网络传输不是完美的。你可能会先收到第100号事件,再收到第99号。如果你直接按收到顺序处理,订单簿就乱了。
所以,每个增量事件都必须带一个单调递增的序列号。
序列号对齐的逻辑其实很简单:
- 本地维护一个
last_seq,记录最后处理的事件序列号。 - 收到新事件时,检查其序列号是否等于
last_seq + 1。 - 如果相等,正常处理;如果大于,说明有事件丢失,请求全量快照。
- 如果小于,说明是重复事件,直接丢弃。
代码实现大概长这样:
def process_incremental(event):
global last_seq
if event.seq == last_seq + 1:
apply_to_orderbook(event)
last_seq = event.seq
elif event.seq > last_seq + 1:
request_snapshot() # 丢了事件,重新同步
else:
pass # 重复事件,忽略
你可能会问:为什么不直接缓存乱序事件,等前面的到了再处理?
理论上可以,但实际中我很少这么做。因为缓存逻辑会增加复杂度,而且一旦网络抖动严重,缓存会越积越多。还不如直接拉快照来得干脆。
4.4 数据对齐的完整流程
好了,我们把上面这些串起来,看看一个完整的增量更新流程长什么样。
下面这张图是我自己画的,帮你理清思路:
这个流程我用了好多年,基本没出过问题。核心就一句话:信任序列号,别信任顺序。
4.5 实战中的几个细节
最后,分享几个我在实战中积累的小经验:
- 序列号最好是全局单调递增的,而不是每个价格档位单独递增。否则对齐逻辑会复杂很多。
- 收到全量快照后,要清空本地订单簿再重建,而不是在原有基础上叠加。我见过有人直接叠加,结果重复数据越积越多。
- 增量事件里如果包含“修改”操作,一定要确认修改前后的数量。有些交易所的“修改”事件只给新数量,不给旧数量,你得自己算差值。
- 定期请求全量快照做“健康检查”。即使序列号一直连续,也可能因为程序bug导致数据偏差。我习惯每5分钟拉一次快照做对比。
好了,增量更新这块就聊到这儿。说白了,就是“用序列号管住乱序,用快照兜底”。你只要把这两点做到位,订单簿的实时性基本就有保障了。