订单簿数据结构:价格优先与时间优先原则、订单队列、Level 1/2/3 数据
聊到订单簿,很多人第一反应就是「买一卖一」那两行数据。说实话,我刚入行时也这么想,觉得订单簿不就是个价格列表嘛。直到我在实盘系统里踩过一次坑——一个高频策略因为没搞懂订单队列的排队逻辑,连续吃了好几次滑点,回撤直接拉爆。嗯,从那以后,我对订单簿的数据结构就再也不敢马虎了。
今天咱们就把它彻底拆开看看。说白了,订单簿就是交易所用来记录所有挂单的账本。但怎么记、怎么排、怎么更新,这里头门道不少。
价格优先与时间优先原则
这是订单簿最核心的规则,没有之一。你想想看,如果两个人同时挂买单,谁先成交?
价格优先:买单出价高的排前面,卖单出价低的排前面。这个好理解,谁出的价更「激进」,谁就先成交。
时间优先:价格一样时,先挂单的先成交。这个也合理,先来后到嘛。
我习惯把这两条规则记成一句话:「价格是第一优先级,时间是第二优先级。」在实盘系统里,这个逻辑直接决定了订单队列的排序方式。
核心要点:
- 买单队列:价格从高到低排序,同价按时间先后
- 卖单队列:价格从低到高排序,同价按时间先后
- 最优价格(买一/卖一)永远是队列头部
订单队列:排队背后的细节
每个价格水平上,都有一堆订单在排队。这个队列怎么维护?我给大家画个图就清楚了。
这个图展示的是卖单侧的队列结构。每个价格水平上,订单按到达时间排成一条队。新来的订单只能排在队尾,只有前面的订单全部成交或撤单后,才能轮到它。
我曾经在开发一个做市策略时,发现策略频繁挂单又撤单,结果每次重新挂单都排到队尾去了。后来我改成「挂单后尽量不撤,除非价格明显偏离」,成交率反而上来了。这就是队列位置的价值。
实战技巧:
如果你做高频交易,队列位置就是你的生命线。排在买一队列的第1位和第100位,成交概率天差地别。我个人习惯在挂单前先估算一下当前队列深度,太深的话宁愿稍微让点价格,也不去排队。
Level 1/2/3 数据:不同深度的订单簿
交易所提供的数据,按详细程度分为三个层级。我刚开始接触时也搞混过,这里给大家理清楚。
| 层级 | 包含内容 | 典型用途 | 数据量 |
|---|---|---|---|
| Level 1 | 最优买卖价(买一/卖一)、最新成交价、成交量 | 散户看盘、简单策略 | 极低(每秒几次) |
| Level 2 | 多档买卖盘口(通常5-10档)、每档挂单量 | 量化策略、盘口分析 | 中等(每秒几十次) |
| Level 3 | 全量订单簿、每笔订单详情(含订单ID、时间戳) | 高频交易、做市商 | 极高(每秒上千次) |
说白了,Level 1 就是给你看个大概,知道当前最优价格在哪。Level 2 能看到深度,比如买一到买五各有多少单子挂着。Level 3 则是把整个订单簿的底裤都扒了——每个订单的ID、挂单时间、数量,全都能看到。
我记得有一次帮客户排查一个策略的成交延迟问题。客户用的是Level 2数据,但策略逻辑里需要知道某个大单是否被撤了。结果Level 2只告诉你总量变化,根本不知道是哪个订单撤了。后来换成Level 3数据,问题才解决。所以选哪个层级,完全取决于你的策略需要多细的粒度。
注意:
Level 3数据虽然信息最全,但处理起来也最麻烦。我见过不少团队,数据量太大导致系统处理不过来,反而比用Level 2的团队反应还慢。别盲目追求全量数据,够用就好。
订单簿的更新机制
订单簿不是静态的,它每时每刻都在变。常见的更新事件有四种:
- 新订单到达:插入到对应价格队列的尾部
- 订单成交:从队列头部移除(部分成交则减少数量)
- 订单撤单:从队列中任意位置移除
- 订单修改:先撤单,再以新参数重新挂单
这里有个坑,我当年就栽过。订单修改在大多数交易所里,实际上是「先撤后挂」。也就是说,如果你修改了价格,你的订单会先被撤掉,然后重新排到新价格队列的队尾。有些新手以为修改订单能保留原来的排队位置,结果发现成交率骤降。
来看一段伪代码,模拟订单簿的更新逻辑:
// 订单簿更新伪代码
function processOrder(order, orderBook) {
if (order.type == 'NEW') {
// 找到对应价格队列,插入队尾
queue = orderBook.getQueue(order.price, order.side)
queue.append(order)
} else if (order.type == 'CANCEL') {
// 从队列中移除指定订单
queue = orderBook.getQueue(order.price, order.side)
queue.remove(order.id)
} else if (order.type == 'TRADE') {
// 从队列头部移除成交数量
queue = orderBook.getQueue(order.price, order.side)
queue.head.quantity -= order.tradeQty
if (queue.head.quantity == 0) {
queue.removeHead()
}
}
}
这段代码虽然简单,但实际生产环境里要考虑的东西多得多。比如并发处理、数据一致性、性能优化等等。我建议初学者先把这个逻辑跑通,再考虑优化的事。
实战中的数据结构选择
在实现订单簿时,选什么数据结构很关键。我个人的经验是:
- 价格层级用红黑树或跳表:需要快速找到最优价格,同时支持按价格遍历
- 订单队列用双向链表:需要频繁在头部和尾部操作,同时支持中间删除(撤单)
- 订单ID用哈希表:需要根据订单ID快速定位到具体订单
说白了,订单簿就是一个「价格到队列」的映射。价格维度需要有序,队列维度需要支持快速插入和删除。选对数据结构,性能能差出好几个数量级。
总结一下:
订单簿的核心就是「价格优先、时间优先」这八个字。Level 1/2/3 数据对应不同的信息粒度,选哪个取决于你的策略需求。订单队列的维护看似简单,但实际开发中坑不少——尤其是撤单和修改的排队逻辑。搞懂了这些,你就能看懂订单簿里每一笔数据背后的故事了。