3、订单簿事件驱动:订单到达、成交、撤单、修改事件的处理逻辑
好,咱们进入正题。
订单簿不是静态的。它每时每刻都在变。谁来了、谁走了、谁改了主意——这些变化,就是事件。我个人习惯把订单簿看作一个「事件响应系统」。你想想看,交易所每秒要处理几万笔订单,每一笔都是一个事件。处理不好,轻则成交延迟,重则数据错乱。
3.1 事件驱动的核心思想
说白了,就是「来了什么,就处理什么」。订单簿不需要主动去轮询,而是被动响应。每个事件都带着自己的数据,系统根据事件类型,调用对应的处理函数。
我在项目中遇到过一个问题:一开始把所有事件混在一起处理,结果撤单和成交的逻辑互相干扰,查了半天才发现是事件队列没做隔离。嗯,这里要注意——事件驱动,第一步就是分类型。
- 订单到达(New Order):新订单进入系统
- 订单成交(Trade):买卖双方匹配成功
- 订单撤单(Cancel):主动撤销未成交订单
- 订单修改(Modify):修改价格或数量
3.2 订单到达事件
这是最常见的事件。一个新订单来了,系统要做什么?
第一步,判断买卖方向。买单进买队列,卖单进卖队列。第二步,尝试撮合。新订单进来后,先看看对面队列有没有能匹配的。如果有,就触发成交事件。如果没有,就挂单等待。
我建议把撮合逻辑单独抽出来。别在到达事件里写死。为什么呢?因为撮合规则可能会变——比如有的交易所支持冰山订单,有的支持止损单。抽出来,好维护。
// 伪代码:订单到达处理
function onNewOrder(order):
if order.side == 'buy':
matchWithSellQueue(order)
if order.remainingQty > 0:
buyQueue.insert(order)
else:
matchWithBuyQueue(order)
if order.remainingQty > 0:
sellQueue.insert(order)
3.3 订单成交事件
成交事件,是订单簿里最核心的环节。它意味着价格发现了。
成交的逻辑其实不复杂:买单价格 ≥ 卖单价格,就能成交。但细节很多。比如,成交价格怎么定?是按买单价格,还是卖单价格,还是中间价?
我记得在某个实盘项目中,就因为成交价格取错了,导致回测和实盘对不上。后来发现,交易所用的是「吃单方价格」——也就是主动下单那一方的价格。这个细节,很多人会忽略。
| 场景 | 成交价格 | 说明 |
|---|---|---|
| 买单主动吃卖单 | 卖单价格 | 买方接受卖方报价 |
| 卖单主动吃买单 | 买单价格 | 卖方接受买方报价 |
| 双方同时到达 | 先到者价格 | 按时间优先原则 |
成交后,要更新订单簿。被吃掉的订单要从队列里移除,剩余数量要更新。如果一笔大单吃了多个对手单,就要循环处理,直到完全成交或无法继续。
3.4 订单撤单事件
撤单,就是取消一个还没成交的订单。听起来简单,但坑不少。
我曾经踩过一个坑:撤单时只删了队列里的记录,忘了更新订单状态。结果后续查询时,订单状态还是「已挂单」,导致重复撤单。嗯,这个bug查了我一下午。
撤单的正确流程:
- 根据订单ID找到对应的订单
- 检查订单是否还在队列中(可能已经成交了)
- 如果还在,从队列中移除
- 更新订单状态为「已撤销」
- 释放该订单占用的资金或仓位
你想想看,如果撤单时没释放资金,用户就会觉得「我明明撤单了,怎么钱还被锁着?」。这在实盘里是大忌。
3.5 订单修改事件
修改事件,其实是「撤单+新订单」的组合操作。但要注意,修改不是简单的删除再插入。
为什么?因为修改时,订单的时间优先级可能会变。比如,一个订单本来排在前面,修改价格后,它应该重新排队。但有些交易所允许「修改数量不修改价格」,这种情况下,时间优先级可以保留。
我个人习惯把修改事件分成两种:
- 价格修改:先撤单,再下新单。时间戳更新。
- 数量修改:只改数量,时间戳不变。但要注意,减少数量没问题,增加数量可能需要重新检查资金。
// 伪代码:订单修改处理
function onModifyOrder(orderId, newPrice, newQty):
oldOrder = findOrder(orderId)
if oldOrder == null:
return error("订单不存在")
if newPrice != oldOrder.price:
// 价格变了,重新排队
cancelOrder(orderId)
placeNewOrder(oldOrder.side, newPrice, newQty)
else:
// 只改数量
oldOrder.qty = newQty
// 检查资金是否足够
checkFunds(oldOrder)
3.6 事件处理的整体流程
好了,四种事件都讲完了。咱们把它们串起来看看。
一个完整的订单簿事件处理流程,大概是这样的:
- 事件到达,进入队列
- 按事件类型分发(到达/成交/撤单/修改)
- 执行对应的处理逻辑
- 更新订单簿状态
- 触发后续事件(比如成交后可能触发止损)
- 记录日志,方便复盘
下面这张图,是我自己画的事件处理流程图。你看一眼,应该就能明白整体架构了。
这张图里,事件队列在最上面,事件分发器在中间,四个处理模块在下面。处理完后,统一更新订单簿状态。注意,成交事件可能会触发新的订单到达(比如止损单),所以有一个反馈回路。
好了,这就是订单簿事件驱动的核心逻辑。四种事件,各有各的坑。但只要你把流程理清楚,把边界条件想明白,写出来的代码就不会太差。
我曾经在一个高频交易系统里,把事件处理延迟从50微秒降到了5微秒。怎么做到的?其实就是把事件分发从「if-else」改成了「函数指针表」。嗯,细节决定成败。