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 知识体系与核心逻辑图
为了让你更直观地理解整个流程,我画了一张图。这张图展示了事件如何驱动订单簿状态的变化。
3.5 总结一下
事件驱动模型,说白了就是“兵来将挡,水来土掩”。
- 来一个订单到达事件,我就往订单簿里加一笔。
- 来一个取消事件,我就从订单簿里减一笔,同时检查订单状态。
- 来一个成交事件,我就两边都减,同时更新订单状态。
事件循环就是那个不断从队列里取事件、分发的“调度员”。
我个人觉得,理解了这个模型,你就掌握了订单簿微观结构的核心。剩下的,就是优化性能,处理边界情况了。
一句话总结: 事件驱动模型,是连接“市场数据”和“交易策略”的桥梁。桥稳了,策略才能跑得稳。