订单簿数据结构:买单队列与卖单队列的存储逻辑
做量化交易这些年,我见过不少新手一上来就盯着K线看,却忽略了订单簿这个真正的「战场」。说白了,订单簿就是交易所里所有挂单的实时快照。你看到的每一笔成交,背后都是订单簿里买单和卖单的激烈博弈。
今天我们就来拆解一下,订单簿最核心的数据结构——买单队列和卖单队列,到底是怎么存储的。
一、订单簿的基本构成
一个标准的订单簿,其实就两个队列:
- 买单队列(Bid Queue):按价格从高到低排列。谁出价高,谁排前面。
- 卖单队列(Ask Queue):按价格从低到高排列。谁卖得便宜,谁排前面。
为什么这么排?因为交易所要优先撮合「最高买入价」和「最低卖出价」。这是最朴素的供需逻辑。
核心概念:买单队列的最高价叫「买一」,卖单队列的最低价叫「卖一」。买一和卖一的价差,就是「点差」(Spread)。点差越小,市场流动性越好。
二、存储逻辑:价格优先 + 时间优先
嗯,这里要注意。订单簿的排序规则是「价格优先,时间优先」。什么意思?
- 价格优先:买单价格高的排在前面,卖单价格低的排在前面。
- 时间优先:相同价格下,先挂单的排在前面。
我刚开始做回测系统时,就踩过这个坑。当时我简单用了一个数组来存订单,每次插入新订单都遍历一遍找位置。结果回测跑起来慢得离谱。后来才意识到,订单簿的插入和删除操作极其频繁,必须用高效的数据结构。
三、数据结构选型:我为什么偏爱跳表
常见的实现方式有这么几种:
| 数据结构 | 插入复杂度 | 删除复杂度 | 查询复杂度 | 实际应用 |
|---|---|---|---|---|
| 有序数组 | O(n) | O(n) | O(log n) | 不推荐,太慢 |
| 红黑树 | O(log n) | O(log n) | O(log n) | C++常用,实现复杂 |
| 跳表(Skip List) | O(log n) | O(log n) | O(log n) | 我个人的首选 |
| 哈希表 + 双向链表 | O(1) 平均 | O(1) | O(1) | 内存占用大 |
我个人习惯用跳表。为什么?因为它实现简单,而且支持范围查询。你想想看,做量化交易时经常需要查「买一到买十」或者「卖一到卖十」,跳表天然支持这种操作。
我的经验:在低延迟场景下,跳表的性能非常稳定。我曾经把一个基于红黑树的订单簿改成跳表实现,延迟抖动降低了40%左右。当然,如果你用C++,也可以考虑用std::map(底层是红黑树),但Java和Go的话,跳表更友好。
四、代码示例:一个极简的订单簿
下面我用Python写一个简化版的订单簿,帮你理解核心逻辑。实际生产环境比这复杂得多,但原理是一样的。
import bisect
from collections import defaultdict
class OrderBook:
def __init__(self):
# 买单队列:价格从高到低
self.bids = [] # 存储价格列表
self.bid_orders = defaultdict(list) # 价格 -> 订单列表
# 卖单队列:价格从低到高
self.asks = []
self.ask_orders = defaultdict(list)
def add_order(self, order):
"""添加订单"""
if order.side == 'buy':
# 买单:价格高的优先
if order.price not in self.bid_orders:
bisect.insort(self.bids, -order.price) # 取负值实现降序
self.bid_orders[order.price].append(order)
else:
# 卖单:价格低的优先
if order.price not in self.ask_orders:
bisect.insort(self.asks, order.price)
self.ask_orders[order.price].append(order)
def get_top_n(self, n=5):
"""获取前N档行情"""
top_bids = []
for neg_price in self.bids[:n]:
price = -neg_price
total_vol = sum(o.volume for o in self.bid_orders[price])
top_bids.append((price, total_vol))
top_asks = []
for price in self.asks[:n]:
total_vol = sum(o.volume for o in self.ask_orders[price])
top_asks.append((price, total_vol))
return top_bids, top_asks
这段代码虽然简单,但已经包含了订单簿的核心逻辑:
- 用两个列表维护价格顺序
- 用字典按价格分组存储订单
- 插入时自动排序
避坑指南:我曾经在实盘环境中犯过一个低级错误——直接用浮点数作为价格key。结果因为浮点数精度问题,两个明明相同的价格被当成了不同的key,导致订单簿错乱。后来我统一改用整数(比如价格乘以10000)来存储,再也没出过问题。
五、订单簿的实时更新流程
订单簿不是静态的,它每时每刻都在变化。主要操作就三种:
- 新增订单:插入到对应队列的正确位置
- 撤销订单:从队列中删除指定订单
- 成交订单:从队列头部移除已成交的订单
你想想看,一个高频交易系统每秒可能要处理几万笔订单操作。如果数据结构选得不好,光排序就能把CPU吃满。
我记得有一次帮一家做市商优化系统,他们用的就是数组+遍历的方式。在行情剧烈波动时,订单簿更新延迟高达几十毫秒。换成跳表后,延迟降到了微秒级。这就是数据结构的威力。
六、知识体系结构图
下面我用一张SVG图来总结本章的核心逻辑:
七、总结
订单簿的数据结构,说白了就是两个有序队列的管理问题。选对数据结构,你的交易系统就成功了一半。
我个人建议:
- 如果追求极致性能,用C++的红黑树
- 如果追求开发效率,用跳表
- 如果只是做研究,Python的有序字典也能凑合
但不管选什么,一定要理解背后的排序逻辑和操作复杂度。否则,等你的策略跑起来才发现性能瓶颈,那就晚了。