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" }
}
4.3 撤单事件
撤单就是订单被取消。注意:撤单事件必须包含原始订单ID,不能只传价格和数量。为什么?因为同一个价格可能有多个订单,只传价格你根本不知道撤哪个。
#[derive(Debug, Clone)]
pub struct CancelOrderEvent {
pub order_id: u64, // 要撤销的订单ID
pub timestamp: u64,
pub sequence: u64,
}
你想想看,如果交易所只告诉你「有人撤了一笔100块的买单」,你根本不知道撤的是哪一笔。所以订单ID是必须的。
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个事件生成一次快照。太频繁浪费存储,太稀疏重建时间太长。你可以根据自己系统的吞吐量调整这个阈值。
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 错误。这时候不能盲目处理,得停下来检查。
4.7 事件类型关系图
下面这张图展示了五类事件在订单簿重建中的流转关系。你可以看到,快照事件是起点,新增和撤单修改订单簿状态,成交事件同时影响买卖双方,错误事件则可能触发回滚。
4.8 事件处理优先级
最后说一个实战中容易忽略的点:事件处理优先级。当多个事件同时到达时,处理顺序很重要。
- 快照事件优先级最高——一旦收到快照,必须立即清空当前订单簿,用快照重建。
- 错误事件次之——错误事件意味着当前状态可能不可信,需要暂停处理。
- 新增、撤单、成交按序列号顺序处理——这三个事件之间没有优先级差异,但必须严格按序列号递增处理。
嗯,到这里事件类型定义就讲完了。这五类事件就像五块积木,搭好了它们,订单簿重建的骨架才算真正立起来。下一章我们开始写具体的处理逻辑——怎么把这些事件喂给订单簿,让它自己动起来。