订单簿数据结构:核心概念与实战解析
做市商报价引擎,说白了就是跟订单簿打交道。我做了这么多年量化系统,订单簿数据结构这块踩过的坑,够写一本小册子了。今天咱们就把它掰开揉碎了讲清楚。
订单簿核心概念
订单簿是什么?简单说,就是市场上所有未成交订单的集合。它记录了谁想买、谁想卖,出什么价、要多少量。
我习惯把订单簿想象成一个双层的书架:
- 买单(Bids):买家出的价格,从高到低排列
- 卖单(Asks):卖家要的价格,从低到高排列
中间那个空隙,就是买卖双方的博弈地带。价差越小,流动性越好。
关键指标:
- 买一价(Best Bid):最高的买入价格
- 卖一价(Best Ask):最低的卖出价格
- 价差(Spread):卖一价 - 买一价
- 市场深度:各价位上的挂单总量
限价订单簿:L2 vs L3
这里有个容易混淆的概念——L2和L3。我在面试候选人的时候,发现很多人分不清。
L2(Level 2):聚合了每个价格档位的总挂单量。比如买一价100.00有1000股,买二价99.99有2000股。它不关心这些订单是谁挂的,只看总量。
L3(Level 3):能看到每个订单的详细信息。谁挂的、挂了多少、什么时候挂的、订单ID是什么。说白了,L3是原始数据,L2是加工后的聚合数据。
| 特性 | L2 | L3 |
|---|---|---|
| 数据粒度 | 按价格聚合 | 逐笔订单 |
| 数据量 | 较小(几十到几百档) | 巨大(成千上万笔) |
| 更新频率 | 中等 | 极高 |
| 典型用途 | 做市策略、盘口分析 | 订单流分析、高频策略 |
嗯,这里要注意:L3数据量非常大。我做过一个项目,处理全市场L3数据,单日数据量就超过100GB。如果你不是做极高频交易,L2基本够用了。
价格-时间优先级
这是订单簿的核心规则。说白了就是:
- 价格优先:买单出价高的优先成交,卖单要价低的优先成交
- 时间优先:价格相同时,先挂单的优先成交
为什么这么设计?你想想看,如果只看价格不看时间,那大家都等着最后时刻出价,市场就乱了。价格-时间优先级保证了公平性。
实战经验:我在做回测系统时,发现很多人忽略了时间戳的精度问题。交易所的时间戳通常是纳秒级的,但你的系统如果只记录到毫秒,那时间优先级的判断就会出错。曾经有个策略因为这个原因,回测和实盘表现差了20%。
订单簿快照与增量
这是订单簿数据分发的两种方式:
快照(Snapshot):某一时刻订单簿的完整状态。包含所有价格档位的挂单情况。
增量(Incremental/Diff):订单簿的变化部分。比如新增了一个订单、撤销了一个订单、成交了一部分。
为什么需要两种?我举个例子:
- 如果你刚连上交易所,需要先拿一个快照,知道当前市场状态
- 之后只需要接收增量更新,保持本地订单簿同步
- 每隔一段时间(比如1分钟),再拿一次快照做校验
避坑指南:我曾经遇到过增量数据丢失的情况。交易所的网络偶尔会断,增量序列中间缺了几条,结果本地订单簿跟交易所对不上了。从那以后,我强制要求系统每30秒做一次快照校验。发现不一致就重新拉取全量快照。
订单簿数据结构设计
用Python实现一个简单的订单簿,我习惯这样设计:
class OrderBook:
def __init__(self):
self.bids = {} # 价格 -> 总量
self.asks = {} # 价格 -> 总量
self.orders = {} # order_id -> Order对象
self.last_update_id = 0
def apply_snapshot(self, snapshot):
self.bids.clear()
self.asks.clear()
for price, qty in snapshot['bids']:
self.bids[price] = qty
for price, qty in snapshot['asks']:
self.asks[price] = qty
self.last_update_id = snapshot['update_id']
def apply_update(self, update):
side = self.bids if update['side'] == 'buy' else self.asks
if update['type'] == 'new':
side[update['price']] = side.get(update['price'], 0) + update['qty']
elif update['type'] == 'cancel':
side[update['price']] = side.get(update['price'], 0) - update['qty']
if side[update['price']] <= 0:
del side[update['price']]
self.last_update_id = update['update_id']
def get_best_bid(self):
return max(self.bids.keys()) if self.bids else None
def get_best_ask(self):
return min(self.asks.keys()) if self.asks else None
def get_spread(self):
bid = self.get_best_bid()
ask = self.get_best_ask()
if bid and ask:
return ask - bid
return None
这个实现虽然简单,但核心逻辑都在了。实际生产环境中,我会用红黑树或者跳表来维护价格排序,因为Python的dict是无序的,每次取最优价都要遍历,性能不行。
订单簿核心逻辑流程图
实战中的几个坑
讲几个我实际踩过的坑:
- 浮点数精度:价格用浮点数存,比较的时候出问题。我后来统一用整数,价格乘以10000存。
- 内存泄漏:L3订单簿如果不清除已成交订单,内存会暴涨。我设置了一个定时任务,每5分钟清理一次。
- 并发访问:多线程同时读写订单簿,不加锁会出大问题。我用的是读写锁,读多写少的场景性能很好。
小技巧:做市商策略对订单簿的实时性要求很高。我建议把订单簿放在独立线程里,用无锁队列跟策略线程通信。这样即使订单簿更新再频繁,也不会阻塞策略逻辑。
订单簿数据结构,说白了就是这些内容。但真正做好,需要大量的实战经验。我建议你从模拟交易所开始,自己实现一个订单簿,然后跑一些历史数据看看效果。纸上得来终觉浅,绝知此事要躬行。