4、事件类型定义:新增订单事件、撤单事件、成交事件、行情快照事件、错误事件

好,咱们接着聊。上一章我们把事件驱动的整体骨架搭起来了,现在该往里面填「肉」了——也就是具体的事件类型。

我个人习惯,在设计事件系统时,先把事件类型定死。为什么?因为事件类型就是整个系统的「通信协议」。协议没定好,后面写代码就是互相打架。我在项目中遇到过好几次,因为事件类型定义模糊,导致行情模块和策略模块互相甩锅的情况。

说白了,订单簿重建的核心就靠这五类事件:新增订单撤单成交行情快照错误。一个都不能少。

4.1 事件基类设计

先定个基类。所有事件都继承它,这样后面做序列化、日志记录都方便。

// 事件基类
#[derive(Debug, Clone)]
pub enum OrderBookEvent {
    Add(AddOrderEvent),
    Cancel(CancelOrderEvent),
    Trade(TradeEvent),
    Snapshot(SnapshotEvent),
    Error(ErrorEvent),
}

// 每个事件都带时间戳和序列号
pub trait EventBase {
    fn timestamp(&self) -> u64;
    fn sequence(&self) -> u64;
    fn event_type(&self) -> &'static str;
}

嗯,这里要注意:sequence 字段很重要。它用来保证事件顺序。我在生产环境吃过亏——网络延迟导致事件乱序,订单簿直接重建出负数价格。后来强制要求交易所返回的每个事件都带递增序列号,才解决。

4.2 新增订单事件

这是最基础的事件。一条新订单进入市场,我们就得把它插入订单簿。

字段 类型 说明
order_id u64 订单唯一ID
price f64 价格(注意精度问题)
quantity f64 数量
side Side 买/卖
timestamp u64 事件发生时间
#[derive(Debug, Clone)]
pub struct AddOrderEvent {
    pub order_id: u64,
    pub price: f64,
    pub quantity: f64,
    pub side: Side,
    pub timestamp: u64,
    pub sequence: u64,
}

impl EventBase for AddOrderEvent {
    fn timestamp(&self) -> u64 { self.timestamp }
    fn sequence(&self) -> u64 { self.sequence }
    fn event_type(&self) -> &'static str { "Add" }
}
我的经验:价格字段别用f64,精度会坑死人。我建议用i64表示最小价格单位(比如1分钱),或者用decimal库。f64在累加时误差会累积,订单簿重建到后面价格对不上,排查起来想哭。

4.3 撤单事件

撤单就是订单被取消。注意:撤单事件必须包含原始订单ID,不能只传价格和数量。为什么?因为同一个价格可能有多个订单,只传价格你根本不知道撤哪个。

#[derive(Debug, Clone)]
pub struct CancelOrderEvent {
    pub order_id: u64,      // 要撤销的订单ID
    pub timestamp: u64,
    pub sequence: u64,
}

你想想看,如果交易所只告诉你「有人撤了一笔100块的买单」,你根本不知道撤的是哪一笔。所以订单ID是必须的。

我曾经踩过的坑:某个交易所的撤单事件不返回订单ID,只返回价格和数量。我不得不维护一个「价格→订单列表」的映射表,然后按FIFO顺序猜测撤的是哪一笔。结果就是订单簿经常和交易所对不上。后来我直接找交易所要了原始数据流,才彻底解决。

4.4 成交事件

成交事件最复杂。一笔成交会同时影响买单和卖单两边的订单簿。

字段 类型 说明
trade_id u64 成交唯一ID
buy_order_id u64 买单ID
sell_order_id u64 卖单ID
price f64 成交价格
quantity f64 成交数量
timestamp u64 成交时间
#[derive(Debug, Clone)]
pub struct TradeEvent {
    pub trade_id: u64,
    pub buy_order_id: u64,
    pub sell_order_id: u64,
    pub price: f64,
    pub quantity: f64,
    pub timestamp: u64,
    pub sequence: u64,
}

处理成交事件时,要同时更新买单和卖单的剩余数量。如果某个订单被完全成交,还要把它从订单簿里移除。这个逻辑说起来简单,但并发场景下很容易出bug。

4.5 行情快照事件

快照事件是订单簿重建的「锚点」。当系统启动或者检测到数据不一致时,就需要加载一个快照,然后在此基础上增量更新。

#[derive(Debug, Clone)]
pub struct SnapshotEvent {
    pub bids: Vec<Level>,   // 买单队列
    pub asks: Vec<Level>,   // 卖单队列
    pub timestamp: u64,
    pub sequence: u64,
}

#[derive(Debug, Clone)]
pub struct Level {
    pub price: f64,
    pub quantity: f64,
    pub order_count: u32,   // 该价格上的订单数量
}
核心原则:快照事件必须带序列号。这样我们才能知道快照对应的是哪个时间点的状态。如果快照的序列号是1000,那么之后的事件序列号必须大于1000,否则就丢弃。

我个人习惯每1000个事件生成一次快照。太频繁浪费存储,太稀疏重建时间太长。你可以根据自己系统的吞吐量调整这个阈值。

4.6 错误事件

错误事件不是用来处理业务逻辑的,而是用来做「熔断」的。当系统检测到无法恢复的错误时,就抛出错误事件,让上层决定是重连、回滚还是报警。

#[derive(Debug, Clone)]
pub enum ErrorEvent {
    SequenceGap {
        expected: u64,
        received: u64,
    },
    DataCorruption {
        description: String,
    },
    Timeout {
        last_event_time: u64,
    },
}

举个例子:如果你收到的事件序列号是100,下一个事件序列号突然变成了102,中间跳过了101。这就是 SequenceGap 错误。这时候不能盲目处理,得停下来检查。

注意:错误事件不要吞掉。很多新手喜欢在catch块里打个日志就继续跑。但订单簿这种状态机系统,一旦状态错了,后面的所有计算都是错的。我建议:遇到不可恢复的错误,直接停掉订单簿重建,等人工介入。

4.7 事件类型关系图

下面这张图展示了五类事件在订单簿重建中的流转关系。你可以看到,快照事件是起点,新增和撤单修改订单簿状态,成交事件同时影响买卖双方,错误事件则可能触发回滚。

事件类型关系图 行情快照事件 新增订单事件 撤单事件 成交事件 错误事件 快照(起点) 新增 撤单 成交 错误(熔断) 异常路径

4.8 事件处理优先级

最后说一个实战中容易忽略的点:事件处理优先级。当多个事件同时到达时,处理顺序很重要。

  1. 快照事件优先级最高——一旦收到快照,必须立即清空当前订单簿,用快照重建。
  2. 错误事件次之——错误事件意味着当前状态可能不可信,需要暂停处理。
  3. 新增、撤单、成交按序列号顺序处理——这三个事件之间没有优先级差异,但必须严格按序列号递增处理。

嗯,到这里事件类型定义就讲完了。这五类事件就像五块积木,搭好了它们,订单簿重建的骨架才算真正立起来。下一章我们开始写具体的处理逻辑——怎么把这些事件喂给订单簿,让它自己动起来。


专注资料整理