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查了我一下午。

撤单的正确流程:

  1. 根据订单ID找到对应的订单
  2. 检查订单是否还在队列中(可能已经成交了)
  3. 如果还在,从队列中移除
  4. 更新订单状态为「已撤销」
  5. 释放该订单占用的资金或仓位

你想想看,如果撤单时没释放资金,用户就会觉得「我明明撤单了,怎么钱还被锁着?」。这在实盘里是大忌。

关键点:撤单事件要保证「幂等性」。也就是说,同一个订单撤两次,结果应该和撤一次一样。不会报错,也不会重复释放资金。

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)
建议:修改事件最好加一个「版本号」。每次修改,版本号+1。这样在并发场景下,可以防止「旧修改覆盖新修改」的问题。

3.6 事件处理的整体流程

好了,四种事件都讲完了。咱们把它们串起来看看。

一个完整的订单簿事件处理流程,大概是这样的:

  1. 事件到达,进入队列
  2. 按事件类型分发(到达/成交/撤单/修改)
  3. 执行对应的处理逻辑
  4. 更新订单簿状态
  5. 触发后续事件(比如成交后可能触发止损)
  6. 记录日志,方便复盘

下面这张图,是我自己画的事件处理流程图。你看一眼,应该就能明白整体架构了。

订单簿事件处理流程图 事件队列 事件分发器 订单到达 处理 订单成交 处理 订单撤单 处理 订单修改 处理 订单簿状态更新 触发后续事件

这张图里,事件队列在最上面,事件分发器在中间,四个处理模块在下面。处理完后,统一更新订单簿状态。注意,成交事件可能会触发新的订单到达(比如止损单),所以有一个反馈回路。

性能提醒:事件处理要快。每个事件的处理时间,最好控制在微秒级。如果处理逻辑太复杂,可以考虑用「事件预处理器」做初步过滤。比如,无效订单直接丢弃,不进入主流程。

好了,这就是订单簿事件驱动的核心逻辑。四种事件,各有各的坑。但只要你把流程理清楚,把边界条件想明白,写出来的代码就不会太差。

我曾经在一个高频交易系统里,把事件处理延迟从50微秒降到了5微秒。怎么做到的?其实就是把事件分发从「if-else」改成了「函数指针表」。嗯,细节决定成败。

专注资料整理