订单簿数据结构:价格队列与时间优先
做量化交易这些年,我见过不少新手一上来就盯着最新成交价看。其实真正有价值的信息,藏在订单簿里。说白了,订单簿就是交易所的「挂单台账」——谁想买、谁想卖、什么价格、挂了多少,全记在上面。
我个人习惯把订单簿想象成一个双层的书架。左边放买单,右边放卖单。每一层就是一个价格,价格越好的越靠外。嗯,这里要注意:买单是价格越高越靠前,卖单是价格越低越靠前。
价格队列的核心逻辑
每个价格点上都排着一队订单。这队人怎么排?看时间。先来的站前面,后来的排后面。这就是所谓的「价格优先、时间优先」原则。
举个例子:
- 张三挂100元买10手,时间是09:30:01
- 李四挂100元买10手,时间是09:30:02
- 王五挂101元买10手,时间是09:30:03
谁先成交?王五。因为101元比100元高,价格优先。如果王五也挂100元,那张三先成交,因为时间优先。
核心要点:价格是第一优先级,时间是第二优先级。价格相同看时间,价格不同看价格。
我在项目中遇到过一个问题:某个策略在开盘瞬间频繁撤单重挂,导致订单总是排到队尾。后来才发现,交易所的时间戳精度是微秒级的,你以为同时提交的订单,其实有先后顺序。
Level 2 数据解析
Level 2 数据,也叫深度数据。它比 Level 1 多了什么?Level 1 只告诉你最优买卖价和成交量,Level 2 把整个订单簿的每一层都给你看。
一个典型的 Level 2 快照长这样:
{
"asks": [
[100.50, 200],
[100.55, 150],
[100.60, 300]
],
"bids": [
[100.45, 180],
[100.40, 250],
[100.35, 100]
]
}
每个数组里,第一个数是价格,第二个数是该价格上的总挂单量。注意,这个总挂单量是合并后的——所有挂在这个价格上的订单,不管是谁挂的,量都加在一起。
小技巧:我习惯把卖单数据叫「卖方压力」,买单数据叫「买方支撑」。如果卖方压力突然增大,价格大概率要跌。反之亦然。
快照与增量更新
这里有个关键问题:订单簿变化极快,每秒可能有上千次变动。如果每次变动都传整个订单簿,网络带宽扛不住。所以交易所用了「快照+增量」的模式。
快照就是某一时刻的完整订单簿。增量就是后续的变化记录。你拿到一个快照后,后续只接收增量数据,自己维护订单簿的更新。
增量数据长这样:
{
"type": "update",
"changes": [
{"side": "ask", "price": 100.50, "size": 0},
{"side": "bid", "price": 100.45, "size": 300}
]
}
size=0 表示撤单,size>0 表示新增或修改。我刚开始做的时候踩过一个坑:增量数据可能乱序到达。比如先收到一个撤单消息,后收到这个订单的挂单消息,那订单簿就乱了。
避坑指南:我曾经因为没处理增量乱序,导致策略在回测和实盘表现完全不一样。解决方案是给每条增量加一个序列号,本地维护一个缓冲区,按序列号顺序处理。
订单簿的本地维护
在代码里,我通常用红黑树或跳表来维护订单簿。为什么?因为需要快速找到最优价格,同时支持按价格范围查询。
一个简单的 Python 实现思路:
class OrderBook:
def __init__(self):
self.bids = {} # 价格 -> 总量
self.asks = {} # 价格 -> 总量
self.bid_prices = [] # 排序后的价格列表
self.ask_prices = []
def apply_snapshot(self, snapshot):
self.bids = {p: s for p, s in snapshot['bids']}
self.asks = {p: s for p, s in snapshot['asks']}
self.bid_prices = sorted(self.bids.keys(), reverse=True)
self.ask_prices = sorted(self.asks.keys())
def apply_update(self, update):
side = update['side']
price = update['price']
size = update['size']
if side == 'bid':
if size == 0:
self.bids.pop(price, None)
else:
self.bids[price] = size
else:
if size == 0:
self.asks.pop(price, None)
else:
self.asks[price] = size
你想想看,这个结构虽然简单,但实际生产环境里要考虑的东西多得多。比如内存占用、GC 停顿、多线程安全……嗯,这些后面会细讲。
数据结构的可视化
为了让你更直观地理解订单簿的结构,我画了一张图:
这张图里,卖单从高到低排列,买单从低到高排列。中间那条虚线就是当前最优买卖价之间的差距,也就是「价差」。价差越小,市场流动性越好。
实际应用中的注意事项
做 Level 2 数据解析时,有几个坑我帮你提前踩了:
- 快照和增量的对齐:订阅数据流后,先等一个完整快照,再开始处理增量。如果增量序号比快照序号还小,直接丢弃。
- 价格精度:不同交易所的价格精度不一样。有的支持小数点后8位,有的只支持2位。处理时统一用整数表示,避免浮点误差。
- 内存管理:订单簿数据量很大,尤其是高频场景。我建议用对象池复用数据结构,减少 GC 压力。
个人经验:我习惯在本地维护两份订单簿。一份用于策略决策,另一份用于校验。每隔一段时间对比两份数据是否一致,发现不一致就重新拉取快照。
说白了,订单簿数据结构是量化交易的基石。你把它搞明白了,后面那些复杂的策略逻辑才能站得住脚。不然的话,数据都是错的,策略再漂亮也是白搭。
公众号:蓝海资料掘金营,微信deep3321