4、订单簿构建:从原始行情数据到实时订单簿的构建流程
说实话,很多人觉得订单簿构建就是个简单的「排序+汇总」过程。但我在实际项目中踩过不少坑,才明白这里面门道有多深。今天我就把完整的构建流程拆开揉碎了讲给你听。
4.1 原始行情数据长什么样?
先看看交易所到底给我们发了什么。以最常见的沪深交易所Level-2行情为例,原始数据大概长这样:
// 逐笔成交数据示例
{
"time": "09:30:05.123",
"price": 10.25,
"volume": 1000,
"direction": "buy", // 主动买入
"type": "F" // F表示成交,A表示撤单
}
// 委托快照数据示例
{
"time": "09:30:05.000",
"bid_prices": [10.24, 10.23, 10.22, ...],
"bid_volumes": [500, 800, 1200, ...],
"ask_prices": [10.25, 10.26, 10.27, ...],
"ask_volumes": [600, 900, 1100, ...]
}
嗯,这里要注意:不同交易所的数据格式差异很大。我当年做美股的时候,纳斯达克的ITCH协议和A股的Level-2完全是两套逻辑。但核心思想是一样的——我们都需要把这些碎片化的数据拼成一张完整的「挂单地图」。
4.2 构建流程的核心三步
我个人习惯把订单簿构建拆成三个步骤。你想想看,这就像搭积木:
- 数据清洗:去掉脏数据、重复数据
- 增量更新:用逐笔数据更新快照
- 实时排序:维护价格-数量的映射关系
核心原则:订单簿不是每次都从头构建的,而是基于快照做增量更新。否则每秒几千笔成交,你的CPU会直接冒烟。
4.3 数据清洗——别小看这一步
我曾经在项目中遇到过一个问题:某天开盘后订单簿突然乱掉了,买一价居然比卖一价还高。查了半天,发现是交易所发了一条时间戳错乱的数据。
所以清洗阶段至少要做三件事:
- 时间戳校验:丢弃明显异常的时间点(比如比当前时间晚5秒以上的数据)
- 价格合理性检查:价格不能为负,不能超过涨跌停板
- 数量非零检查:数量为0的委托其实代表撤单,要特殊处理
避坑指南:我曾经因为没处理「重复数据」导致订单簿多了一层。后来加了个简单的序列号去重,问题就解决了。记住:交易所偶尔会重发数据。
4.4 增量更新——这才是核心
说白了,订单簿维护的就是两个有序列表:买单列表(从高到低)和卖单列表(从低到高)。每次收到新数据,我们只需要做三件事:
// 伪代码示例
function updateOrderBook(snapshot, delta) {
// 1. 处理撤单:从对应价格档位减去数量
if (delta.type === 'cancel') {
removeVolume(snapshot, delta.price, delta.volume)
}
// 2. 处理成交:从对应价格档位减去数量
if (delta.type === 'trade') {
removeVolume(snapshot, delta.price, delta.volume)
}
// 3. 处理新委托:在对应价格档位增加数量
if (delta.type === 'new') {
addVolume(snapshot, delta.price, delta.volume)
}
// 4. 如果某个价格档位数量归零,移除该档位
cleanupEmptyLevels(snapshot)
}
你可能会问:为什么成交和撤单都是减数量?因为成交的本质就是「买单和卖单互相抵消」,体现在订单簿上就是两边同时减少。
4.5 实时排序——用什么数据结构?
这是很多新手会纠结的地方。我直接说结论:
| 数据结构 | 适用场景 | 我的评价 |
|---|---|---|
| 红黑树(std::map) | 通用场景,价格档位多 | 稳定可靠,C++首选 |
| 跳表(Skip List) | 并发读写频繁 | 适合高频交易场景 |
| 数组+二分查找 | 价格档位固定(如A股) | 速度最快,但灵活性差 |
| 哈希表 | 仅查询,不排序 | 别用,订单簿必须有序 |
我个人习惯用红黑树。原因很简单:它保证了O(log n)的插入和删除,而且天然有序。你想想看,如果每次更新都要手动排序,那性能就完蛋了。
小技巧:对于A股这种价格档位固定的市场(最小变动单位0.01元),可以用数组+偏移量的方式。比如价格10.25元,直接映射到数组下标1025。这样查询就是O(1)的,爽得很。
4.6 完整的构建流程图
下面这张图是我自己总结的,基本上覆盖了所有场景:
4.7 实战中的几个坑
嗯,最后分享几个我亲身踩过的坑:
- 数据延迟问题:交易所的快照和增量数据可能不是同时到达的。我建议先缓存增量数据,等快照到了再一起处理。
- 精度问题:价格用浮点数?千万别。我见过因为浮点精度导致订单簿对不齐的惨案。用整数(比如乘以10000)存储。
- 内存管理:如果每个价格档位都存一个对象,一天下来内存就爆了。用对象池复用,别频繁new/delete。
一句话总结:订单簿构建的本质就是「用快照做基准,用增量做修正」。把这个逻辑理清了,剩下的就是数据结构和性能优化的问题了。
好了,这一章的内容就到这里。记住我上面说的那些坑,你在实际开发中能少走不少弯路。