订单簿数据结构:OrderBook设计、Level2数据模型、价格-数量映射、深度合并算法
做市商报价引擎的核心,说白了就是一本「电子账本」。
这本账本记录着市场上所有未成交的买卖意愿。谁想买、谁想卖、出什么价、要多少量,全得记清楚。我当年刚接触高频交易系统时,第一件事就是被老大扔去研究订单簿——他说,不懂OrderBook,就别谈做市。
嗯,这话一点不夸张。
一、OrderBook的核心职责
一个订单簿,至少得干三件事:
- 存储:把买卖双方的挂单按价格层级存好
- 更新:收到新订单、撤单、成交时,实时修改数据
- 查询:快速给出最优买卖价、深度分布、累计量
我习惯把OrderBook比作一个「双端优先队列」。买盘是最大堆,卖盘是最小堆。但实际工程里,我们很少直接用堆结构——因为堆不支持随机删除,而撤单是家常便饭。
核心原则:订单簿的更新速度,决定了你的报价延迟。每笔行情进来,你必须在微秒级完成数据重组。
二、Level2数据模型
Level2,也叫市场深度数据。它不像Level1只给最优价,而是把每个价位的挂单量都告诉你。
一个典型的Level2快照长这样:
{
"asks": [
[100.50, 2000], // 价格100.50,卖单总量2000股
[100.55, 1500],
[100.60, 3000]
],
"bids": [
[100.45, 2500], // 价格100.45,买单总量2500股
[100.40, 1800],
[100.35, 2200]
],
"timestamp": 1699000000123,
"symbol": "BTC/USDT"
}
这里有个细节:每个价格层级上的量,是所有挂在这个价位的订单的总和。你想想看,如果100.50上有10个人挂单,每人200股,那这个价位的量就是2000。
我在项目中遇到过一个问题:有些交易所的Level2数据是「增量更新」的,只告诉你哪个价位变了。这时候你就得维护一个本地快照,每次收到增量就patch上去。嗯,这里要注意——增量更新的顺序不能乱,否则快照就废了。
三、价格-数量映射:用什么数据结构?
说白了,我们需要一个从价格到数量的映射。而且这个映射要支持:
- 按价格排序(从小到大或从大到小)
- 快速定位某个价格
- 快速删除某个价格
- 快速获取最优价
我推荐用红黑树或跳表。C++里直接用std::map,Java里用TreeMap,Python里用sortedcontainers库。
但如果你追求极致性能,我建议自己实现一个价格桶数组。为什么?
因为价格精度是有限的。比如股票价格最小变动单位是0.01元,那你可以把价格乘以100转成整数,然后用数组下标直接索引。这样查询就是O(1),比红黑树快一个数量级。
我的经验:在币圈做市时,价格精度到小数点后8位,没法用桶数组。这时候我用了跳表——实现简单,而且并发性能比红黑树好。
四、深度合并算法
深度合并,就是把多个订单簿合并成一个。比如你在三个交易所都挂了单,想看看总的深度分布。
算法其实很简单:
- 遍历所有订单簿的买盘,按价格分组,相同价格的量相加
- 遍历所有订单簿的卖盘,同样处理
- 按价格排序输出
伪代码长这样:
function mergeDepth(books):
merged_asks = {}
merged_bids = {}
for book in books:
for price, qty in book.asks:
merged_asks[price] = merged_asks.get(price, 0) + qty
for price, qty in book.bids:
merged_bids[price] = merged_bids.get(price, 0) + qty
// 按价格排序
sorted_asks = sorted(merged_asks.items())
sorted_bids = sorted(merged_bids.items(), reverse=True)
return (sorted_asks, sorted_bids)
但实际工程里,有个坑:不同交易所的价格精度可能不一样。比如A交易所价格是100.50,B交易所可能是100.50000001。这时候直接按价格合并就会出问题。
我曾经踩过的坑:有一次合并三个交易所的深度,发现总深度比实际少了一大截。查了半天,原来是某个交易所的价格精度是小数点后4位,另一个是8位。同样的价格,一个存成100.5000,一个存成100.50000000,在哈希表里成了两个key。后来我统一做了价格截断,才解决。
五、知识体系总览
下面这张图,是我自己总结的订单簿数据结构核心逻辑。你看一眼,基本就明白整个章节在讲什么了。
六、实战中的选择建议
说了这么多理论,到底怎么选?我给出几条实际建议:
| 场景 | 推荐数据结构 | 理由 |
|---|---|---|
| 股票(价格精度0.01) | 价格桶数组 | O(1)查询,内存连续,缓存友好 |
| 加密货币(高精度) | 跳表 | 支持高精度,并发插入删除性能好 |
| 多交易所合并 | 红黑树 + 哈希表 | 需要快速查找和范围遍历 |
| 低延迟做市 | 自定义内存池 + 跳表 | 避免GC,控制内存分配 |
一个小技巧:不管用什么数据结构,记得把「最优买卖价」单独缓存起来。每次更新订单簿时,只检查最优价有没有变。这样查询最优价就是O(1),不用每次都去树里找最小/最大值。
嗯,订单簿这块内容其实挺深的。今天讲的这些,是我在实际做市系统里反复验证过的。你如果自己动手写一个OrderBook,建议先从最简单的std::map版本开始,跑通逻辑,再考虑优化性能。
毕竟,能跑起来的代码,才是有价值的代码。
公众号:蓝海资料掘金营,微信deep3321