3. 订单簿数据结构:价格队列、时间优先原则、价格优先原则、订单簿的构建与维护

好,咱们今天聊聊订单簿的底层数据结构。说实话,这东西看着简单,但坑特别多。我刚开始做量化的时候,以为订单簿就是个价格和数量的表格,后来被实盘数据狠狠教育了一顿。

订单簿的核心,说白了就是一本「账本」。它记录着市场上所有未成交的买单和卖单。你想想看,交易所每天处理几百万笔订单,如果没有一套高效的数据结构,系统早就崩了。

3.1 价格优先原则

这是订单簿的第一条铁律。什么意思?买单出价高的优先成交,卖单出价低的优先成交

举个例子:

  • 买单A:出价100.5元,买10手
  • 买单B:出价100.3元,买20手

这时候来了一个卖单,价格100.4元。谁会先成交?买单A。因为A出价更高,他愿意多付钱,市场自然优先满足他。

核心逻辑:价格优先原则保证了市场效率。愿意出更高价的买家,和愿意出更低价的卖家,理应优先成交。这是市场经济的底层逻辑。

反过来看卖单:

  • 卖单C:出价99.8元,卖5手
  • 卖单D:出价100.0元,卖10手

来了一个买单,价格99.9元。谁先成交?卖单C。因为他卖得更便宜。

嗯,这里要注意:价格优先是第一优先级。也就是说,不管你的订单时间多早,只要价格不占优,就得排在后面。我在项目中遇到过有人以为「我先挂单就能先成交」,结果被价格更高的订单插队了,气得直跺脚。

3.2 时间优先原则

价格相同的时候怎么办?那就看谁先来。

时间优先原则很简单:同一价格,先挂单的先成交

举个例子:

  • 订单E:10:00:00挂单,价格100.0元,买10手
  • 订单F:10:00:01挂单,价格100.0元,买10手

这时候来了一个卖单,价格100.0元,卖10手。谁会成交?订单E。因为他早挂了1秒钟。

实战技巧:做高频交易的时候,时间优先原则特别重要。我曾经为了抢1毫秒的时间优势,把服务器搬到了交易所机房里。你想想看,同一价格下,早1毫秒可能就意味着多赚几万块。

但这里有个细节:时间优先只在同一价格层级内有效。不同价格之间,还是价格优先说了算。说白了,价格是「硬门槛」,时间是「软排序」。

3.3 价格队列

价格队列,就是订单簿的基本组织方式。每个价格对应一个队列,队列里按时间顺序排列着该价格的所有订单。

结构大概是这样的:

买单队列(买盘):
  价格100.5元 → [订单A(10手, 10:00:00), 订单B(5手, 10:00:02)]
  价格100.3元 → [订单C(20手, 10:00:01)]
  价格100.0元 → [订单D(8手, 10:00:03)]

卖单队列(卖盘):
  价格100.6元 → [订单E(15手, 10:00:00)]
  价格100.8元 → [订单F(10手, 10:00:02)]
  价格101.0元 → [订单G(12手, 10:00:01)]

你看,买单从高到低排列,卖单从低到高排列。这就是订单簿的标准结构。

我个人习惯用跳表(Skip List)来实现价格队列。为什么?因为跳表的插入和删除都是O(log n)复杂度,比红黑树好写,比数组快得多。实盘数据每秒几千次变动,用数组的话CPU会哭的。

避坑指南:我曾经用Python的list来实现价格队列,结果回测跑得飞快,一上实盘就卡死。后来才发现,list的插入是O(n)复杂度,价格层级一多(比如上千个价位),性能直接崩了。所以,千万别用数组或list做订单簿的核心数据结构

3.4 订单簿的构建与维护

订单簿不是静态的,它每时每刻都在变化。构建和维护订单簿,本质上就是处理三类事件:

  1. 新增订单:新订单进入队列
  2. 撤销订单:已有订单被取消
  3. 成交订单:订单部分或全部成交

咱们一步步来看。

3.4.1 新增订单

新订单来了,先判断是买单还是卖单。然后:

  • 如果是买单,从卖单队列的最低价开始匹配
  • 如果是卖单,从买单队列的最高价开始匹配

匹配规则:

  • 买单价格 ≥ 卖单价格 → 成交
  • 买单价格 < 卖单价格 → 进入买单队列等待

举个例子:

当前卖单队列最低价:100.2元(卖10手)
新来买单:价格100.5元,买20手

匹配过程:
1. 买单价格100.5 ≥ 卖单价格100.2 → 成交10手
2. 买单还剩10手,继续匹配下一个卖单
3. 下一个卖单价格100.4元(卖5手)→ 成交5手
4. 买单还剩5手,继续匹配
5. 下一个卖单价格100.6元(卖8手)
6. 买单价格100.5 < 卖单价格100.6 → 无法成交
7. 剩余5手进入买单队列,价格100.5元

嗯,这就是订单簿的「撮合引擎」在干的事。说白了,就是不停地「碰」,能碰上的就成交,碰不上的就排队。

3.4.2 撤销订单

撤销订单相对简单。找到对应的价格队列,然后从队列里删除该订单。

但要注意:部分成交的订单。如果一个订单已经成交了5手,还剩5手没成交,这时候撤销只撤销剩下的5手。已成交的部分已经无法撤销了。

实战经验:我在做回测系统时,发现撤销订单的处理特别容易出错。尤其是当订单被部分成交后,剩余数量要精确计算。我曾经因为浮点数精度问题,导致剩余数量算成了0.0000001手,结果系统一直报错。后来统一改用整数(比如用「分」代替「元」),问题就解决了。

3.4.3 成交订单

成交是订单簿最核心的操作。成交后,需要:

  1. 从双方队列中扣除成交数量
  2. 如果订单全部成交,从队列中删除
  3. 更新订单簿的「最优买卖价」(即买一、卖一)

这里有个关键点:部分成交。一个订单可能分多次成交,每次成交一部分。比如你挂了100手的买单,可能先成交30手,再成交50手,最后20手。每次成交都要更新订单的剩余数量。

3.5 订单簿的完整结构图

下面我用SVG画一张图,把整个订单簿的结构和流程展示出来。这张图我画了好几次才满意,你仔细看看:

订单簿数据结构与处理流程 买单队列(买盘) 价格 100.5元 → [订单A(10手, 10:00:00)] 价格 100.3元 → [订单B(20手, 10:00:01)] 价格 100.0元 → [订单C(8手, 10:00:03)] 撮合引擎 价格优先 → 时间优先 匹配 → 成交 → 更新 卖单队列(卖盘) 价格 100.6元 [订单D(15手)] 价格 100.8元 [订单E(10手)] 价格 101.0元 [订单F(12手)] 订单处理流程 1. 接收订单 判断买卖方向 2. 价格匹配 检查能否成交 3. 执行成交 扣除数量/删除 4. 更新队列 维护价格层级 注:买单队列按价格从高到低排列,卖单队列按价格从低到高排列 同一价格层级内,按时间先后顺序排列 每次成交后,需重新计算最优买卖价(买一、卖一)

这张图把订单簿的核心逻辑都画出来了。你注意看:买单队列从高到低,卖单队列从低到高,中间是撮合引擎。每次新订单进来,都要走一遍这个流程。

3.6 实战中的注意事项

最后,我分享几个实战中踩过的坑:

  • 数据同步问题:订单簿的数据必须和交易所保持同步。我见过有人用WebSocket接收数据,结果网络延迟导致订单簿状态不一致,策略直接亏钱。
  • 内存管理:订单簿的数据量很大,尤其是高频场景。我建议用对象池来复用订单对象,避免频繁GC。
  • 快照与增量:实盘中,交易所会定期发送订单簿快照,平时只发增量更新。你需要维护一个「本地订单簿」,用增量更新来修正快照。
  • 边界情况:比如价格相同但数量为0的订单、订单被瞬间撤销又重挂、部分成交后剩余数量为0...这些边界情况都要处理。

重要提醒:千万别以为订单簿只是「存数据」那么简单。它是整个交易系统的核心。订单簿一旦出错,你的策略就是「盲人摸象」。我见过有人因为订单簿数据错误,导致策略在错误的价格上反复交易,一天亏了30%。

好了,订单簿的数据结构就讲到这里。记住三个关键词:价格优先、时间优先、队列维护。把这三点吃透了,订单簿这块你就入门了。


公众号:蓝海资料掘金营,微信deep3321