3、订单簿事件驱动:订单到达、成交、撤单、修改事件的处理逻辑

做量化交易的朋友都知道,订单簿不是静态的。它每时每刻都在变化。说白了,订单簿就是一个事件流处理器。你想想看,每一笔订单的到达、成交、撤单、修改,都会引发一连串的连锁反应。

我个人习惯把订单簿比作一个繁忙的十字路口。订单就像车辆,到达、离开、变道,都需要一套清晰的交通规则来管理。今天我们就来拆解这四个核心事件的处理逻辑。

3.1 订单到达事件

订单到达是最基础的事件。新订单进入系统后,第一件事就是判断它是买单还是卖单,然后插入到对应的队列中。

这里有个关键点:价格优先、时间优先。这是订单簿的铁律。

  • 价格优先:买单价格高的排在前面,卖单价格低的排在前面。
  • 时间优先:同价格的订单,先到的排在前面。

我在项目中遇到过一个问题:某个高频策略每秒发送上千笔订单,如果每次都用线性插入,性能会急剧下降。后来我改用跳表(Skip List)来维护价格层级,插入效率从 O(n) 降到了 O(log n)。

核心逻辑:订单到达后,先检查是否能与对手盘立即成交。如果不能,则进入订单簿排队。

// 伪代码:订单到达处理
function onOrderArrival(order):
    // 检查是否能立即成交
    if canMatch(order):
        matchOrder(order)
    else:
        // 插入到订单簿
        insertIntoBook(order)
        // 更新最佳买卖价
        updateBBO()

3.2 订单成交事件

成交事件是订单簿最核心的驱动力。当买单和卖单的价格匹配时,就会触发成交。

成交的处理逻辑其实不复杂,但细节很多。我总结了一个三步法:

  1. 价格匹配检查:买单价格 ≥ 卖单价格
  2. 数量匹配:取 min(买单量, 卖单量) 作为成交量
  3. 队列更新:减少或移除已成交的订单

嗯,这里要注意:部分成交的情况很常见。比如一个买单有100手,但卖单只有30手,那么买单会成交30手,剩余70手继续在队列中等待。

个人经验:我曾经在处理部分成交时,忘记更新订单的剩余数量,导致后续的成交计算出现负数。这个bug排查了我整整一个下午。所以,每次成交后一定要检查订单的剩余数量是否准确。

// 伪代码:订单成交处理
function matchOrder(order):
    while order.remainingQty > 0:
        // 获取对手盘的最佳订单
        counterOrder = getBestCounterOrder(order)
        if not counterOrder:
            break
        
        // 计算成交量
        tradeQty = min(order.remainingQty, counterOrder.remainingQty)
        tradePrice = counterOrder.price
        
        // 执行成交
        executeTrade(order, counterOrder, tradeQty, tradePrice)
        
        // 更新订单剩余数量
        order.remainingQty -= tradeQty
        counterOrder.remainingQty -= tradeQty
        
        // 如果对手盘订单完全成交,移除它
        if counterOrder.remainingQty == 0:
            removeFromBook(counterOrder)
    
    // 如果当前订单还有剩余,插入订单簿
    if order.remainingQty > 0:
        insertIntoBook(order)

3.3 订单撤单事件

撤单事件看似简单,但处理不当会引发大问题。撤单的核心逻辑就是:从订单簿中移除指定的订单

但这里有个坑:撤单请求可能发生在订单成交的瞬间。也就是说,订单可能已经被成交了,但撤单请求才到达。这时候怎么办?

避坑指南:我曾经在处理撤单时,没有检查订单的成交状态,结果把已经成交的订单从订单簿中移除了,导致成交记录丢失。正确的做法是:撤单前先检查订单状态,如果已经成交,则忽略撤单请求。

撤单处理的几个关键步骤:

  • 验证订单是否存在
  • 检查订单是否已成交
  • 从队列中移除订单
  • 更新订单簿的统计信息(如总订单数、总数量等)
// 伪代码:撤单处理
function onCancelOrder(orderId):
    order = findOrder(orderId)
    if not order:
        return "订单不存在"
    
    if order.status == FILLED:
        return "订单已成交,无法撤单"
    
    // 从订单簿中移除
    removeFromBook(order)
    order.status = CANCELLED
    
    // 更新统计信息
    updateStatistics(order)
    
    return "撤单成功"

3.4 订单修改事件

订单修改是最容易被忽视的事件。很多人觉得修改不就是改个价格或数量吗?其实不然。

修改订单的本质是:先撤单,再重新下单。为什么?因为修改价格后,订单在队列中的位置会发生变化。如果直接修改价格而不重新排队,就违反了时间优先原则。

我建议的修改处理流程:

  1. 记录原订单的信息
  2. 执行撤单操作
  3. 用新参数创建新订单
  4. 执行订单到达逻辑

注意:修改数量时,如果只是减少数量,有些交易所允许不改变队列位置。但如果是增加数量,通常需要重新排队。这个细节因交易所而异,一定要仔细看文档。

// 伪代码:订单修改处理
function onModifyOrder(orderId, newPrice, newQty):
    oldOrder = findOrder(orderId)
    if not oldOrder:
        return "订单不存在"
    
    // 先撤单
    cancelResult = onCancelOrder(orderId)
    if cancelResult != "撤单成功":
        return cancelResult
    
    // 创建新订单
    newOrder = createOrder(
        symbol = oldOrder.symbol,
        side = oldOrder.side,
        price = newPrice,
        qty = newQty
    )
    
    // 执行订单到达逻辑
    onOrderArrival(newOrder)
    
    return "修改成功"

3.5 事件处理的时序问题

这四个事件不是独立发生的。它们可能同时到达,甚至相互冲突。比如:一个订单刚到达,紧接着就来了撤单请求。或者两个订单同时到达,需要决定谁先处理。

我个人习惯使用事件队列来处理这个问题。所有事件先进入队列,然后按顺序逐个处理。这样可以保证事件的有序性和一致性。

实战建议:在分布式系统中,事件队列还可以帮助实现订单簿的副本同步。我在做多活架构时,就是通过事件队列来同步不同节点的订单簿状态。

3.6 事件处理的核心数据结构

为了高效处理这些事件,我们需要合适的数据结构。下面是我常用的几种:

数据结构 用途 时间复杂度
跳表 (Skip List) 维护价格层级 O(log n) 插入/查找
双向链表 维护同一价格下的订单队列 O(1) 插入/删除
哈希表 按订单ID快速查找 O(1) 查找
优先级队列 快速获取最佳买卖价 O(1) 获取最小值/最大值

你想想看,如果没有这些数据结构,每次查找订单都要遍历整个订单簿,那性能得多差?

3.7 事件驱动的整体流程

最后,我用一张流程图来展示这四个事件是如何协同工作的:

订单簿事件驱动流程图 事件队列 订单到达 订单成交 订单撤单 订单修改 订单簿核心引擎 价格优先 · 时间优先 · 队列管理 输出:成交记录 · 订单簿快照 · 行情更新 反馈循环

这张图展示了事件驱动的完整流程。所有事件先进入队列,然后由核心引擎按顺序处理,最终输出成交记录、订单簿快照和行情更新。注意那个反馈循环——成交事件可能会触发新的订单到达,形成连锁反应。

总结一下:订单簿事件驱动的核心就是四个字——有序处理。无论是到达、成交、撤单还是修改,都要保证事件处理的顺序性和一致性。这是订单簿正确性的基石。


专注资料整理