4、订单簿重建:从增量更新重建完整订单簿
做量化交易的朋友都知道,行情数据有两种形态:快照和增量。快照就像你拍了一张照片,记录的是某个瞬间的完整订单簿。增量呢,就像一段视频,告诉你从上一帧到这一帧发生了什么变化。
我刚开始做高频交易系统时,就踩过一个坑。当时只接了快照数据,觉得够用了。结果发现,快照更新频率太低,等拿到新快照时,价格早就变了。后来我才明白——真正的实时订单簿,必须靠增量更新来维持。
快照与增量的逻辑关系
说白了,订单簿重建就是一道数学题:
当前订单簿 = 上一帧订单簿 + 增量更新
听起来简单吧?但实际做起来,细节多得让人头疼。
交易所通常这样设计:
- 快照:每隔一段时间(比如1秒)推送一次完整订单簿
- 增量:每次订单变动(挂单、撤单、成交)都推送一条
你想想看,如果只依赖快照,那两次快照之间的价格波动你就全错过了。如果只依赖增量,万一漏了一条数据,整个订单簿就全乱了。
所以正确的做法是:以快照为基准,用增量来持续更新。
核心原则:快照保证一致性,增量保证实时性。两者缺一不可。
增量更新的三种操作
增量更新通常包含三种操作类型。我整理了一个表格,方便你对照:
| 操作类型 | 含义 | 示例 |
|---|---|---|
| 新增 | 新的买单或卖单进入队列 | 价格100.5,数量200 |
| 删除 | 某个订单被撤销或成交 | 价格100.5,数量0 |
| 修改 | 订单数量发生变化(部分成交) | 价格100.5,数量从200变为150 |
嗯,这里要注意:不同交易所对增量的定义略有不同。比如币安的增量更新里,数量为0就表示删除。而有些交易所会单独用delete字段标识。
Python实现订单簿类
接下来,我们动手写一个订单簿类。我个人习惯用OrderBook这个命名,清晰明了。
class OrderBook:
def __init__(self):
# 买单:价格降序排列(最高价在前)
self.bids = {} # 价格 -> 数量
# 卖单:价格升序排列(最低价在前)
self.asks = {} # 价格 -> 数量
def apply_snapshot(self, snapshot):
"""应用快照,重置订单簿"""
self.bids = {item[0]: item[1] for item in snapshot['bids']}
self.asks = {item[0]: item[1] for item in snapshot['asks']}
def apply_update(self, update):
"""应用增量更新"""
side = update['side'] # 'buy' 或 'sell'
price = update['price']
quantity = update['quantity']
target = self.bids if side == 'buy' else self.asks
if quantity == 0:
# 删除该价格档位
target.pop(price, None)
else:
# 新增或修改
target[price] = quantity
def get_top_n(self, n=5):
"""获取前N档买卖盘口"""
sorted_bids = sorted(self.bids.items(), reverse=True)[:n]
sorted_asks = sorted(self.asks.items())[:n]
return {
'bids': sorted_bids,
'asks': sorted_asks
}
这段代码看起来简单,但我在项目中遇到过一个问题:增量更新的顺序很重要。如果先收到一个删除操作,再收到一个新增操作,顺序错了,订单簿就会乱掉。
避坑指南:我曾经因为没处理增量更新的时间戳,导致订单簿数据错乱。建议每条增量都带上时间戳或序列号,按顺序处理。
订单簿重建的核心流程
下面这张SVG图,展示了订单簿重建的完整流程。我画得比较直观,你一看就懂。
进阶:处理增量丢失的情况
实际生产中,网络抖动、系统延迟都可能导致增量丢失。我建议你这样做:
- 维护一个序列号:每条增量都带一个递增的序列号
- 检测序列号跳变:如果发现序列号不连续,说明有丢失
- 请求重新快照:一旦发现丢失,立即请求最新快照,重置订单簿
class RobustOrderBook(OrderBook):
def __init__(self):
super().__init__()
self.last_seq = 0
def apply_update(self, update):
seq = update.get('seq', 0)
if seq != self.last_seq + 1:
# 序列号不连续,需要重新请求快照
print(f"警告:序列号跳变,期望{self.last_seq+1},实际{seq}")
return False
self.last_seq = seq
super().apply_update(update)
return True
重要提醒:不要试图用算法去「猜测」丢失的增量数据。我曾经试过用插值法补数据,结果回测时看起来很美,实盘一跑就亏钱。丢失了就重新请求快照,这是最稳妥的做法。
性能优化小技巧
订单簿重建对性能要求很高,尤其是高频场景。我分享几个实战经验:
- 用字典代替列表:价格作为key,查询速度是O(1)
- 预分配内存:如果知道订单簿大概的深度,可以预先分配字典大小
- 延迟排序:只在需要展示盘口时才排序,平时保持字典结构
说白了,订单簿重建的核心就两点:快照保证正确,增量保证速度。两者配合好了,你的交易系统才能跑得稳、跑得快。
我见过不少新手,一上来就追求极致的性能,结果忽略了数据一致性。最后订单簿乱了,策略也跟着乱。记住:正确的数据,比快的数据更重要。