订单簿构建原理:Level 2/Level 3数据模型、增量更新与快照更新机制、内存中的红黑树与跳表实现
大家好,我是老张。今天咱们聊聊订单簿的构建原理。说实话,这是整个量化交易系统里最核心、也最容易踩坑的部分。我做了这么多年,见过太多团队在订单簿这块翻车——要么速度跟不上,要么数据对不上。
订单簿是什么?说白了,就是交易所里所有挂单的实时汇总。买单排一边,卖单排一边,价格优先、时间优先。嗯,就这么简单。但真要把它在内存里建好、维护好、更新快,学问就大了。
Level 2 vs Level 3:你到底需要多深的数据?
先说说数据模型。交易所给的数据分好几个层级,咱们最常用的是 Level 2 和 Level 3。
Level 2 数据模型,也叫市场深度数据。它告诉你每个价位上有多少订单,但不告诉你每个订单是谁的。比如买一价 100.50 有 1000 股,买二价 100.49 有 2000 股——但你不知道这 1000 股是张三的 500 股加李四的 500 股,还是王五一个人挂的。
我个人习惯把 Level 2 叫做「聚合视图」。它够用吗?大部分策略其实够用。高频做市商看 Level 2 就能做很多事了。
Level 3 数据模型就不一样了。它给你的是每个订单的完整信息——订单ID、交易员ID、时间戳、价格、数量、买卖方向。你能看到每一笔挂单的完整生命周期:什么时候进来的,什么时候被部分成交了,什么时候撤单了。
我记得有一次帮一家自营团队优化系统,他们坚持用 Level 2 做订单流分析。结果发现策略老是亏钱。后来我建议他们换成 Level 3,一查才发现,有个大机构在频繁挂单撤单,Level 2 根本看不出来。换了 Level 3 之后,策略立马转正。
核心区别一句话:Level 2 告诉你「有什么」,Level 3 告诉你「谁在干什么」。
| 特性 | Level 2 | Level 3 |
|---|---|---|
| 数据粒度 | 价位聚合 | 逐笔订单 |
| 带宽消耗 | 低 | 高(约10-50倍) |
| 内存占用 | 约 1-5 MB | 约 50-200 MB |
| 适用策略 | 趋势跟踪、均值回归 | 订单流分析、冰山订单检测 |
增量更新 vs 快照更新:两种更新机制的博弈
好,数据模型定下来了。接下来就是怎么更新订单簿。这里有两个流派:增量更新和快照更新。
快照更新最简单粗暴。每隔一段时间(比如 100 毫秒),交易所给你发一份完整的订单簿快照。你直接把旧的扔掉,换上新的。优点是实现简单,不容易出错。缺点是——你想想看,100 毫秒内可能发生了上千笔交易,你只能看到最终结果,中间的过程全丢了。
增量更新就精细多了。交易所只告诉你「发生了什么变化」——新增了一个订单、某个订单被撤了、某个订单被部分成交了。你需要在本地维护一份订单簿,然后一条一条地应用这些增量消息。
我曾经在项目中踩过一个坑。当时用增量更新,但网络偶尔会丢包。丢了一个增量消息之后,本地订单簿就跟交易所对不上了。那叫一个惨,策略疯狂报错。后来我加了一个机制:每 10 秒请求一次快照做校准,平时用增量更新。这样既保证了实时性,又不会因为丢包而崩盘。
我的建议:用增量更新做主流程,快照更新做备份校验。别只依赖一种。
增量更新的消息格式大概长这样:
// 增量消息示例
{
"type": "add", // add / cancel / execute
"order_id": "12345",
"side": "buy",
"price": 100.50,
"quantity": 1000,
"timestamp": 1623456789
}
你收到这条消息,就在本地订单簿里插入一条记录。如果收到 cancel 消息,就把对应的订单删掉。execute 消息表示部分成交,需要减少对应订单的数量。
内存中的红黑树与跳表:谁才是订单簿的王者?
现在问题来了。订单簿在内存里用什么数据结构存?
需求其实很明确:
- 按价格排序(价格优先)
- 快速插入、删除(订单来了要加,成交了要删)
- 快速查询最优买卖价(我要知道当前 best bid / best ask)
两个主流选择:红黑树和跳表。
红黑树是平衡二叉搜索树的一种。插入、删除、查找的时间复杂度都是 O(log n)。C++ 里的 std::map 就是红黑树实现的。Java 里的 TreeMap 也是。
我刚开始做订单簿的时候,直接用 C++ 的 std::map。代码写起来很爽,几行就搞定了。但跑起来之后发现一个问题——订单量大的时候,内存碎片化严重。因为每个订单都是一个节点,频繁插入删除会导致内存分配器忙不过来。
跳表就不一样了。它用多层链表实现,也是 O(log n) 的复杂度。但它的内存分配更友好——可以预分配一大块内存,用对象池来管理节点。而且跳表在范围查询上比红黑树快,因为链表结构天然支持顺序遍历。
我个人现在更倾向于跳表。为什么?
- 实现起来更直观,调试方便
- 内存管理可控,不容易碎片化
- 并发性能更好,可以分段加锁
注意:跳表虽然好,但如果你用 Python 做原型验证,红黑树更省事。Python 的 bisect 模块配合列表也能模拟,但别在生产环境这么干——列表插入是 O(n) 的,订单一多就卡死。
下面我用 SVG 画了一张图,展示订单簿在内存中的核心结构:
这张图展示了订单簿的核心结构。左边是买盘,价格从高到低排列(买一最高)。右边是卖盘,价格从低到高排列(卖一最低)。中间那个价差,就是买一和卖一之间的差距。价差越小,市场流动性越好。
每个价格节点下面,挂着一串订单详情。这就是 Level 3 数据。如果你只看 Level 2,你只知道 100.0 这个价位上有 1000 股,但不知道这 1000 股是几个订单组成的。
实战中的选择建议
说了这么多,到底怎么选?我给大家一个参考:
- 做高频做市:用 Level 3 + 增量更新 + 跳表。延迟敏感,需要看到每一笔订单的细节。
- 做中低频策略:用 Level 2 + 快照更新 + 红黑树。够用,而且实现简单。
- 做订单流分析:必须上 Level 3。没有逐笔数据,你分析个啥?
一个小技巧:如果你用 Python 做原型,可以用 sortedcontainers 库里的 SortedDict,底层就是跳表。性能比纯 Python 实现好很多,而且 API 友好。
嗯,今天就聊到这儿。订单簿这东西,看着简单,做起来全是细节。我当年第一次写订单簿,以为两三天就能搞定,结果调了整整两周的 bug。尤其是增量更新丢包那个问题,差点让我怀疑人生。
不过话说回来,一旦你把订单簿搞明白了,整个量化交易系统的地基就算打牢了。后面那些策略、风控、回测,都是在这个地基上盖房子。