3. 订单簿重建:从增量消息重建完整订单簿
做量化交易的朋友都知道,交易所发过来的行情数据,很少会给你完整的订单簿快照。大多数时候,你收到的是一串增量消息——「有人撤单了」、「有人挂了一笔新单」、「有一笔成交了」。
说白了,你手里拿到的不是一张完整的照片,而是一堆拼图碎片。你的任务,就是把这些碎片拼回完整的订单簿。
我刚开始做这个的时候,觉得挺简单的——不就是增删改查嘛。结果第一次实盘测试,订单簿就崩了。嗯,这里面的坑,比想象中多得多。
3.1 增量消息的本质
先搞清楚一件事:增量消息到底是什么?
以最常见的L2行情为例,交易所通常会推送两类数据:
- 快照消息:某个时刻的完整订单簿,一般每几秒发一次
- 增量消息:每次订单簿变化时推送,频率极高
增量消息的格式,各家交易所大同小异。我拿一个典型的例子来说:
{
"type": "update",
"side": "bid",
"price": 100.50,
"size": 200,
"action": "add" // add / modify / delete
}
每条消息告诉你:有人在哪个价位,做了什么操作。你想想看,如果每秒有几千条这样的消息涌进来,你的系统能不能扛住?
3.2 事件驱动更新:核心逻辑
重建订单簿的核心,其实就是三个操作:
- 新增:某个价位出现新订单
- 修改:某个价位的订单数量变了
- 删除:某个价位的订单消失了
我习惯用一个有序字典来维护订单簿。Python里可以用SortedDict,C++里用std::map。为什么要有序?因为买卖双方的价格需要按顺序排列,方便计算最优报价。
来看一段核心代码:
class OrderBook:
def __init__(self):
self.bids = SortedDict(reverse=True) # 买盘,价格从高到低
self.asks = SortedDict() # 卖盘,价格从低到高
def apply_update(self, msg):
side = self.bids if msg['side'] == 'bid' else self.asks
price = msg['price']
size = msg['size']
action = msg['action']
if action == 'delete':
if price in side:
del side[price]
elif action == 'modify':
side[price] = size
elif action == 'add':
side[price] = size
# 注意:有些交易所的add也可能是覆盖操作
这段代码看起来简单,对吧?但我在项目中遇到过一个问题:有些交易所的增量消息不是按顺序发的。比如先发了一条delete,后发了一条add,但实际顺序应该是先add再delete。这就麻烦了。
3.3 并发处理:别让锁拖死你
做高频交易的人都知道,行情处理线程和策略交易线程往往是并行的。一个线程在更新订单簿,另一个线程在读取订单簿做决策。这时候,并发问题就来了。
我见过最糟糕的做法:整个订单簿加一把大锁。读和写都抢同一把锁,性能直接掉到地板。
更好的做法是什么?
- 读写锁:多个线程可以同时读,但写的时候独占。Python里可以用
threading.RLock或者asyncio.Lock - 无锁数据结构:比如用
atomic操作更新价格和数量。C++里可以用std::atomic - 双缓冲:一个订单簿在后台更新,更新完直接切换指针。读线程永远读的是完整快照
我个人最推荐双缓冲方案。为什么?因为它的延迟最稳定,没有锁竞争导致的抖动。
class DoubleBufferOrderBook:
def __init__(self):
self.active = OrderBook()
self.pending = OrderBook()
self.lock = threading.Lock()
def update(self, msg):
# 在pending上更新
self.pending.apply_update(msg)
def switch(self):
# 原子切换
with self.lock:
self.active, self.pending = self.pending, self.active
self.pending.clear() # 或者复用
3.4 性能优化:从毫秒到微秒
说到性能,我得坦白一件事。早期我做订单簿重建,用的是Python的dict,每秒处理几千条消息就卡得不行。后来我做了几件事,把吞吐量提了10倍以上。
核心优化点:
| 优化项 | 优化前 | 优化后 | 效果 |
|---|---|---|---|
| 数据结构 | Python dict | array + 索引映射 | 减少哈希开销 |
| 内存分配 | 频繁创建对象 | 对象池复用 | 减少GC压力 |
| 消息解析 | JSON解析 | 二进制协议 | 解析速度提升5x |
| 锁粒度 | 全局锁 | 分段锁/无锁 | 并发能力提升 |
你想想看,如果每秒有10万条增量消息,每条消息都要解析、更新、排序,任何一个环节慢了,都会导致行情延迟。而延迟,在量化交易里就是真金白银的损失。
3.5 知识体系总览
下面这张图,是我自己总结的订单簿重建核心逻辑。你看一遍,应该就能把握住整体脉络。
这张图里,最关键的其实是「事件驱动引擎」那一层。说白了,你写的代码就是在做三件事:增、删、改。但难就难在,这三件事要在微秒级别内完成,还要保证数据一致性。
嗯,说到这里,我想起一个细节。有些交易所的增量消息里,modify操作不是直接覆盖,而是增量修改。比如原先是100手,消息说「增加50手」,那结果应该是150手。这种操作,如果你直接赋值,订单簿就错了。
好了,这一章的内容就到这里。记住一句话:订单簿重建做不好,后面的策略全是空中楼阁。我见过太多人在这上面栽跟头了。
公众号:蓝海资料掘金营,微信deep3321