3、订单簿动态更新:订单的进入、撮合、撤销与修改机制
订单簿,说白了就是交易所的「记账本」。每一笔买单、卖单,都在这里排队。但记账本不是死的,它每时每刻都在变。新订单进来、老订单成交、有人撤单、有人改价……这些动作,都会触发订单簿的更新。
我做了这么多年做市商系统,最深的体会就是:订单簿的动态更新,是整个交易系统的「心脏」。心脏跳得快不快、稳不稳,直接决定了你的策略能不能赚钱。
3.1 订单进入:排队入场
一个新订单进来,系统要干三件事:
- 校验:检查账户余额、风控限制、订单参数是否合法
- 定价:确定这个订单该排在哪个价位
- 入队:插入到对应价位的队列中
这里有个细节,很多人会忽略——时间优先。同一价位,先来的订单先成交。所以订单进入时,系统必须记录精确的时间戳。我见过一个项目,时间戳精度只到毫秒,结果高频交易时,同一毫秒进来的订单顺序全乱了。嗯,后来他们改成了纳秒级。
关键点:订单进入时,系统要同时更新「价格队列」和「总量统计」。别小看这个统计,做市商策略全靠它来判断市场深度。
3.2 订单撮合:谁跟谁配对
撮合,就是找对手盘。一个买单进来,系统会从卖单队列里找价格最低的卖单。如果买单价格 ≥ 卖单价格,就成交。
撮合的逻辑其实不复杂,但性能要求极高。我参与过一个交易所的撮合引擎优化,核心就一句话:尽量减少锁的粒度。
举个例子,一个买单进来,它可能跟多个卖单成交。如果每次成交都锁整个订单簿,那性能肯定上不去。我们当时的做法是:只锁当前价位的队列,其他价位不受影响。
// 伪代码:撮合逻辑
function matchOrder(buyOrder) {
while (buyOrder.quantity > 0) {
sellOrder = getLowestSellOrder();
if (sellOrder == null || buyOrder.price < sellOrder.price) {
break; // 没有可撮合的卖单
}
tradeQuantity = min(buyOrder.quantity, sellOrder.quantity);
executeTrade(buyOrder, sellOrder, tradeQuantity);
// 更新双方订单的剩余数量
buyOrder.quantity -= tradeQuantity;
sellOrder.quantity -= tradeQuantity;
if (sellOrder.quantity == 0) {
removeOrder(sellOrder); // 卖单完全成交,移除
}
}
if (buyOrder.quantity > 0) {
addToOrderBook(buyOrder); // 未完全成交,放入订单簿
}
}
个人经验:撮合时,一定要处理「部分成交」的情况。我曾经遇到过一个bug,部分成交后,订单的剩余数量没更新,结果同一个订单被撮合了两次。那天的对账,简直是一场噩梦。
3.3 订单撤销:说走就走
撤单,听起来简单,但坑不少。一个撤单请求进来,系统要:
- 找到这个订单在队列中的位置
- 从队列中移除
- 更新总量统计
- 释放冻结的资金或资产
你想想看,如果订单正在被撮合,这时候撤单怎么办?这就是经典的「撤单与撮合竞态」问题。
我记得有一次,我们的系统在高并发下,撤单和撮合同时操作同一个订单。结果订单被撮合了,但撤单也成功了,资金被释放了两次。嗯,那天的损失,够我吃一年泡面。
避坑指南:我曾经在撤单逻辑里加了一个「状态锁」。订单一旦进入撮合流程,就标记为「撮合中」,撤单请求必须等待撮合完成。虽然牺牲了一点性能,但保证了数据一致性。
3.4 订单修改:改价还是改量
订单修改,其实是个「伪操作」。大部分交易所的实现方式是:先撤单,再下新单。为什么?因为修改价格,相当于改变了订单在队列中的位置。如果直接改,时间优先的规则就乱了。
但这里有个优化点:如果只修改数量,且是减少数量,可以不撤单。因为减少数量不影响时间优先,直接更新队列里的数量就行。
| 修改类型 | 实现方式 | 性能影响 |
|---|---|---|
| 改价 | 先撤单,再下新单 | 高(两次队列操作) |
| 增量(增加数量) | 先撤单,再下新单 | 高(两次队列操作) |
| 减量(减少数量) | 直接更新队列 | 低(一次队列操作) |
我个人习惯,在做市商策略中,尽量用「减量修改」来调整订单。因为撤单再下单,中间有短暂的时间窗口,订单不在订单簿里,可能会错过行情。
3.5 订单簿的动态更新流程
说了这么多,我们来看一张整体流程图。这张图,是我当年做系统设计时画的,后来成了团队内部培训的经典教材。
这张图,说白了就是订单从生到死的完整生命周期。每个环节,都有坑。我当年做系统时,每个环节都踩过雷。
3.6 做市商视角:如何利用订单簿更新
作为做市商,我们不只是被动地接收订单簿更新。我们更关心的是:如何利用这些更新,来优化自己的报价策略。
举个例子,当一个大买单进入订单簿时,说明买方力量强。这时候,我会适当提高卖单价格,赚取更多价差。反之,如果出现大卖单,我会降低买单价格。
但这里有个陷阱:不要只看单笔订单。我曾经被一个「虚假大单」骗过——有人下了个大买单,然后马上撤单,制造买盘强劲的假象。等我提高卖价后,他再偷偷卖出。
我的做法:我会监控「订单存活时间」。如果一个订单进来后,几秒钟就撤了,那大概率是虚假订单。我会忽略它的影响。
3.7 性能优化:订单簿更新的关键指标
订单簿更新的性能,直接决定了你的交易延迟。我总结几个关键指标:
- 吞吐量:每秒能处理多少笔订单操作(进入/撮合/撤销/修改)
- 延迟:从订单进入系统到完成更新的时间
- 一致性:订单簿的状态是否与撮合结果一致
我参与过一个项目,订单簿更新延迟从 10ms 优化到了 100μs。怎么做到的?说白了就两招:无锁数据结构 和 内存池。
嗯,这里不展开讲,但你可以记住一个原则:订单簿的操作,尽量在内存中完成,避免磁盘IO。磁盘IO是性能杀手,没有之一。
警告:我曾经见过一个团队,把订单簿数据存在数据库里,每次更新都写一次数据库。结果系统一上线,延迟直接飙到秒级。嗯,那项目后来重写了。
好了,订单簿的动态更新,核心就是这些。记住:进入、撮合、撤销、修改,这四个动作,每一个都值得你花时间去优化。做市商赚的钱,说白了就是从这些毫秒级的优化里抠出来的。