4、订单簿重建与维护:Level 2/3数据解析、增量更新、快照同步
做高频交易的人都知道,订单簿就是我们的战场地图。没有它,你根本不知道敌人在哪、火力有多猛。今天咱们聊聊怎么把这张地图从零开始画出来,并且实时更新。
说实话,我刚开始接触Level 2数据时,被那密密麻麻的逐笔委托给整懵了。后来才明白,这东西说白了就是交易所把「谁在什么价位挂了多大的单子」一股脑全倒给你。怎么接住、怎么整理,这就是本章要解决的问题。
4.1 订单簿的数据结构设计
先想清楚一个问题:订单簿本质上是个啥?
它就是个价格-数量的映射表。买盘按价格从高到低排,卖盘从低到高排。就这么简单。
我个人习惯用红黑树或者跳表来实现。为什么?因为我们需要频繁地插入、删除、修改,还要能快速拿到最优买卖价。Python里我一般用sortedcontainers库,或者自己手撸一个跳表。
核心数据结构:
- 买盘(Bid):价格降序排列,每个价格对应一个委托队列
- 卖盘(Ask):价格升序排列,每个价格对应一个委托队列
- 委托队列:同一价格下的所有委托,按时间顺序排列
class OrderBook:
def __init__(self):
self.bids = SortedDict(lambda k: -k) # 买盘,价格降序
self.asks = SortedDict() # 卖盘,价格升序
self.orders = {} # 委托ID -> 委托对象
self.last_update_id = 0 # 用于快照同步
嗯,这里要注意:orders字典是必须的。我在项目中遇到过一个问题——增量更新时只给了委托ID,没有价格信息。如果没有这个映射表,你根本不知道要去哪个价位上删单子。
4.2 Level 2/3数据解析
Level 2和Level 3的区别,说白了就是「深度」和「广度」的区别。
| 数据级别 | 内容 | 更新频率 | 典型用途 |
|---|---|---|---|
| Level 1 | 最优买卖价、成交量 | 秒级 | 散户看盘 |
| Level 2 | 前N档买卖盘口 | 毫秒级 | 量化策略 |
| Level 3 | 全部委托单 | 微秒级 | 高频做市 |
你想想看,Level 3数据里每笔委托都有唯一的ID。这意味着你可以追踪到某个大单是不是被拆成了小单,或者某个机构在偷偷吸筹。
解析的时候,我一般这么处理:
def parse_level2_snapshot(self, data):
"""解析Level 2快照数据"""
book = OrderBook()
for bid in data['bids']:
price = bid[0]
volume = bid[1]
book.bids[price] = volume
for ask in data['asks']:
price = ask[0]
volume = ask[1]
book.asks[price] = volume
book.last_update_id = data['update_id']
return book
def parse_level3_incremental(self, data):
"""解析Level 3增量数据"""
for order in data['orders']:
if order['type'] == 'NEW':
self._add_order(order)
elif order['type'] == 'CANCEL':
self._cancel_order(order['order_id'])
elif order['type'] == 'TRADE':
self._execute_order(order['order_id'], order['volume'])
避坑指南:我曾经在解析Level 3数据时,没处理「部分成交」的情况。结果订单簿里的数量越算越不对。后来加了个executed_volume字段,每次成交都更新剩余量,问题才解决。
4.3 增量更新机制
为什么要有增量更新?你想想看,如果每次行情变化都发一整个快照,那网络带宽得炸。所以交易所只发「变化的部分」,我们自己在本地维护订单簿。
增量更新的核心逻辑就三个操作:
- 新增委托:在对应价格上插入一条新委托
- 撤销委托:从对应价格上移除一条委托
- 成交委托:减少对应委托的数量,如果数量归零则移除
def apply_incremental(self, update):
"""应用增量更新"""
for action in update['actions']:
if action['action'] == 'add':
self._add(action['price'], action['volume'], action['order_id'])
elif action['action'] == 'delete':
self._delete(action['order_id'])
elif action['action'] == 'modify':
self._modify(action['order_id'], action['new_volume'])
self.last_update_id = update['update_id']
这里有个细节:修改操作。有些交易所允许修改委托数量(比如从100手改成50手),但不允许改价格。如果允许改价格,那就相当于先撤单再重新挂单。
注意:增量更新的顺序极其重要!我曾经因为网络延迟导致更新乱序,订单簿直接崩了。解决方案是给每个更新加一个序列号,本地维护一个「期望的序列号」,如果收到的序列号不连续,就触发快照重同步。
4.4 快照同步策略
说白了,快照同步就是「万一订单簿乱了,怎么重新拉一份正确的」。这在高频交易里是保命技能。
我一般用两种策略:
- 定时同步:每隔N秒主动请求一次快照,和本地订单簿做对比
- 异常触发同步:当检测到增量更新序列号不连续时,立即请求快照
同步的流程大概是这样的:
def sync_orderbook(self):
"""快照同步"""
# 1. 记录当前时间
sync_time = time.time()
# 2. 请求快照
snapshot = self.request_snapshot()
# 3. 清空本地订单簿
self.clear()
# 4. 重建订单簿
self.rebuild_from_snapshot(snapshot)
# 5. 回放快照之后的增量更新
for update in self.pending_updates:
if update['timestamp'] > sync_time:
self.apply_incremental(update)
# 6. 验证一致性
assert self.last_update_id == snapshot['update_id']
关键点:步骤5的回放非常重要。因为在你请求快照和收到快照之间,可能已经产生了新的增量更新。如果不回放,订单簿就会「少」一段数据。
4.5 实战中的性能优化
做高频交易,性能就是生命。我分享几个实战经验:
- 内存池:预分配委托对象,避免频繁GC。Python里可以用
__slots__减少内存开销 - 无锁队列:多线程环境下,用
queue.Queue或者collections.deque做生产者-消费者模型 - 批量处理:不要来一条更新就处理一条,攒够一批再处理,减少上下文切换
- 预计算:把常用的指标(如买卖价差、订单簿不平衡度)在更新时一并算好,避免查询时再算
class OptimizedOrderBook:
__slots__ = ('bids', 'asks', 'orders', 'spread', 'imbalance')
def __init__(self):
self.bids = SortedDict(lambda k: -k)
self.asks = SortedDict()
self.orders = {}
self.spread = 0.0 # 买卖价差,预计算
self.imbalance = 0.0 # 订单簿不平衡度,预计算
def _update_metrics(self):
"""更新预计算指标"""
if self.bids and self.asks:
best_bid = self.bids.peekitem(0)[0]
best_ask = self.asks.peekitem(0)[0]
self.spread = best_ask - best_bid
bid_vol = sum(self.bids.values())
ask_vol = sum(self.asks.values())
total = bid_vol + ask_vol
self.imbalance = (bid_vol - ask_vol) / total if total > 0 else 0
我的习惯:在开发阶段,我会给订单簿加一个「校验模式」。每次更新后,都检查买卖盘是否交叉(即最优买价 >= 最优卖价)。如果交叉了,说明数据有问题,立刻报警。这个习惯帮我抓到了好几次数据源的问题。
4.6 知识体系总览
下面这张图是我自己总结的订单簿重建与维护的核心流程,你可以对照着理解:
从这张图可以看出来,整个流程是环环相扣的。数据源进来,经过解析、处理、存储,最后输出给策略引擎。任何一个环节出问题,订单簿就废了。
好了,关于订单簿重建与维护,核心内容就这些。记住一句话:数据是命,性能是魂。把这两点抓牢了,你的高频交易系统就稳了一半。
公众号:蓝海资料掘金营,微信deep3321