3、订单簿事件驱动模型:订单到达、取消、成交事件的处理逻辑,事件循环与状态更新。

好,我们直接切入正题。

订单簿,说白了就是一个不断被事件冲击的“状态机”。

你想想看,每一秒都有成千上万的订单在涌进来,有的要买,有的要卖,有的刚挂上就撤了,有的直接吃掉对手盘。如果我们的程序不能高效地处理这些事件,那订单簿的数据就是错的,交易逻辑也就无从谈起。

我个人习惯把订单簿的事件驱动模型拆成三块:事件本身、事件循环、状态更新。这三块缺一不可。

3.1 事件的三种基本类型

在真实的交易所接口里,比如币安的WebSocket流,或者CTP的行情接口,推送过来的无非就是这三类事件:

  • 订单到达(Add / Limit Order):一个新的限价单挂进来了。
  • 订单取消(Cancel / Delete):某个挂单被撤掉了。
  • 订单成交(Trade / Match):一个买单和一个卖单撞上了,产生了交易。

嗯,这里要注意。有些新手会把“成交”和“订单到达”混在一起处理,这是个大坑。成交事件会同时影响买卖两侧的深度,而订单到达只影响单侧。

核心原则: 事件是驱动状态变化的唯一原因。没有事件,订单簿就不该有任何变化。

3.2 事件循环:心脏的跳动

事件循环,就是整个系统的发动机。

我曾经在一个低延迟项目里,因为事件循环里加了一个日志打印,导致整个系统的吞吐量直接腰斩。从那以后,我对事件循环的性能就特别敏感。

一个标准的事件循环,长这样:

// 伪代码,展示核心逻辑
while (running) {
    // 1. 从队列里取出一个事件
    Event event = eventQueue.dequeue();

    // 2. 根据事件类型,调用不同的处理函数
    switch (event.type) {
        case ADD:
            handleAdd(event);
            break;
        case CANCEL:
            handleCancel(event);
            break;
        case TRADE:
            handleTrade(event);
            break;
    }

    // 3. 更新订单簿的快照(如果需要)
    updateSnapshot();
}

这里有个细节:事件队列必须是线程安全的。因为行情推送线程和策略计算线程往往是分开的。我个人习惯用无锁队列(Lock-Free Queue)来搞,能省掉不少锁竞争的开销。

避坑指南: 我曾经用过一个带锁的队列,在高频场景下,锁的争抢导致事件处理延迟从微秒级飙升到毫秒级。后来换成无锁队列,问题才解决。记住,事件循环里不要做任何阻塞操作。

3.3 状态更新:订单簿的“增删改查”

事件来了,怎么更新订单簿?说白了就是维护一个价格-数量的映射关系。

我建议用红黑树(TreeMap)或者跳表(SkipList)来存储订单簿的深度。因为我们需要按价格排序,而且需要快速找到最优的买一卖一。

3.3.1 订单到达的处理

当一个买单到达时,我们只需要在买单侧的红黑树里,找到对应的价格档位,把数量加上去就行。

void handleAdd(Event event) {
    // 假设 event 包含了 price, quantity, side
    if (event.side == BUY) {
        bidBook[event.price] += event.quantity;
    } else {
        askBook[event.price] += event.quantity;
    }
}

很简单,对吧?但要注意,如果这个价格档位之前不存在,需要新建一个节点。

3.3.2 订单取消的处理

取消事件稍微麻烦一点。因为我们需要验证这个取消请求是否合法。

你想想看,如果一个订单已经被成交了,然后又来了一个取消事件,怎么办?

嗯,这里就要引入订单ID(OrderID)的概念了。每个订单都有一个唯一的ID,我们需要维护一个订单状态表,记录每个订单当前是“存活”还是“已成交”或“已取消”。

void handleCancel(Event event) {
    // 先查订单状态表
    OrderStatus status = orderStatusMap.get(event.orderId);
    if (status == null || status == CANCELLED || status == FILLED) {
        // 订单不存在或已处理,忽略这个事件
        return;
    }

    // 从订单簿中减去数量
    if (event.side == BUY) {
        bidBook[event.price] -= event.quantity;
        // 如果数量归零,删除这个价格档位
        if (bidBook[event.price] <= 0) {
            bidBook.remove(event.price);
        }
    } else {
        // 同理处理卖单侧
    }

    // 更新订单状态
    orderStatusMap.put(event.orderId, CANCELLED);
}

警告: 取消事件的处理,一定要做幂等性检查。同一个取消事件可能会被推送两次,如果没做检查,会导致订单簿数量被多减一次,造成数据错误。我曾经因为这个bug,在回测里亏了不少虚拟资金。

3.3.3 订单成交的处理

成交事件是最复杂的。因为它会同时影响买单侧和卖单侧。

举个例子:一个买单以100元的价格成交了10股。这意味着买单侧100元档位的数量减少了10股,同时卖单侧100元档位的数量也减少了10股。

void handleTrade(Event event) {
    // 成交事件通常包含:价格、数量、买单ID、卖单ID
    // 更新买单侧
    bidBook[event.price] -= event.quantity;
    if (bidBook[event.price] <= 0) {
        bidBook.remove(event.price);
    }

    // 更新卖单侧
    askBook[event.price] -= event.quantity;
    if (askBook[event.price] <= 0) {
        askBook.remove(event.price);
    }

    // 更新两个订单的状态为已成交
    orderStatusMap.put(event.bidOrderId, FILLED);
    orderStatusMap.put(event.askOrderId, FILLED);
}

3.4 知识体系与核心逻辑图

为了让你更直观地理解整个流程,我画了一张图。这张图展示了事件如何驱动订单簿状态的变化。

订单簿事件驱动模型流程图 事件源 交易所行情推送 事件队列 FIFO / 无锁队列 事件循环 while(running) 事件分发 (switch) handleAdd() 增加深度数量 handleCancel() 减少深度 + 状态检查 handleTrade() 双边更新 + 状态更新 订单簿状态更新 事件源 队列 循环 处理函数

3.5 总结一下

事件驱动模型,说白了就是“兵来将挡,水来土掩”。

  • 来一个订单到达事件,我就往订单簿里加一笔。
  • 来一个取消事件,我就从订单簿里减一笔,同时检查订单状态。
  • 来一个成交事件,我就两边都减,同时更新订单状态。

事件循环就是那个不断从队列里取事件、分发的“调度员”。

我个人觉得,理解了这个模型,你就掌握了订单簿微观结构的核心。剩下的,就是优化性能,处理边界情况了。

一句话总结: 事件驱动模型,是连接“市场数据”和“交易策略”的桥梁。桥稳了,策略才能跑得稳。


专注资料整理