3、订单簿事件驱动:订单到达、成交、撤单、修改事件的处理逻辑
做量化交易的朋友都知道,订单簿不是静态的。它每时每刻都在变化。说白了,订单簿就是一个事件流处理器。你想想看,每一笔订单的到达、成交、撤单、修改,都会引发一连串的连锁反应。
我个人习惯把订单簿比作一个繁忙的十字路口。订单就像车辆,到达、离开、变道,都需要一套清晰的交通规则来管理。今天我们就来拆解这四个核心事件的处理逻辑。
3.1 订单到达事件
订单到达是最基础的事件。新订单进入系统后,第一件事就是判断它是买单还是卖单,然后插入到对应的队列中。
这里有个关键点:价格优先、时间优先。这是订单簿的铁律。
- 价格优先:买单价格高的排在前面,卖单价格低的排在前面。
- 时间优先:同价格的订单,先到的排在前面。
我在项目中遇到过一个问题:某个高频策略每秒发送上千笔订单,如果每次都用线性插入,性能会急剧下降。后来我改用跳表(Skip List)来维护价格层级,插入效率从 O(n) 降到了 O(log n)。
核心逻辑:订单到达后,先检查是否能与对手盘立即成交。如果不能,则进入订单簿排队。
// 伪代码:订单到达处理
function onOrderArrival(order):
// 检查是否能立即成交
if canMatch(order):
matchOrder(order)
else:
// 插入到订单簿
insertIntoBook(order)
// 更新最佳买卖价
updateBBO()
3.2 订单成交事件
成交事件是订单簿最核心的驱动力。当买单和卖单的价格匹配时,就会触发成交。
成交的处理逻辑其实不复杂,但细节很多。我总结了一个三步法:
- 价格匹配检查:买单价格 ≥ 卖单价格
- 数量匹配:取 min(买单量, 卖单量) 作为成交量
- 队列更新:减少或移除已成交的订单
嗯,这里要注意:部分成交的情况很常见。比如一个买单有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 订单修改事件
订单修改是最容易被忽视的事件。很多人觉得修改不就是改个价格或数量吗?其实不然。
修改订单的本质是:先撤单,再重新下单。为什么?因为修改价格后,订单在队列中的位置会发生变化。如果直接修改价格而不重新排队,就违反了时间优先原则。
我建议的修改处理流程:
- 记录原订单的信息
- 执行撤单操作
- 用新参数创建新订单
- 执行订单到达逻辑
注意:修改数量时,如果只是减少数量,有些交易所允许不改变队列位置。但如果是增加数量,通常需要重新排队。这个细节因交易所而异,一定要仔细看文档。
// 伪代码:订单修改处理
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 事件驱动的整体流程
最后,我用一张流程图来展示这四个事件是如何协同工作的:
这张图展示了事件驱动的完整流程。所有事件先进入队列,然后由核心引擎按顺序处理,最终输出成交记录、订单簿快照和行情更新。注意那个反馈循环——成交事件可能会触发新的订单到达,形成连锁反应。
总结一下:订单簿事件驱动的核心就是四个字——有序处理。无论是到达、成交、撤单还是修改,都要保证事件处理的顺序性和一致性。这是订单簿正确性的基石。