3、订单簿动态更新:订单的进入、撮合、撤销与修改机制

订单簿,说白了就是交易所的「记账本」。每一笔买单、卖单,都在这里排队。但记账本不是死的,它每时每刻都在变。新订单进来、老订单成交、有人撤单、有人改价……这些动作,都会触发订单簿的更新。

我做了这么多年做市商系统,最深的体会就是:订单簿的动态更新,是整个交易系统的「心脏」。心脏跳得快不快、稳不稳,直接决定了你的策略能不能赚钱。

3.1 订单进入:排队入场

一个新订单进来,系统要干三件事:

  1. 校验:检查账户余额、风控限制、订单参数是否合法
  2. 定价:确定这个订单该排在哪个价位
  3. 入队:插入到对应价位的队列中

这里有个细节,很多人会忽略——时间优先。同一价位,先来的订单先成交。所以订单进入时,系统必须记录精确的时间戳。我见过一个项目,时间戳精度只到毫秒,结果高频交易时,同一毫秒进来的订单顺序全乱了。嗯,后来他们改成了纳秒级。

关键点:订单进入时,系统要同时更新「价格队列」和「总量统计」。别小看这个统计,做市商策略全靠它来判断市场深度。

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 订单撤销:说走就走

撤单,听起来简单,但坑不少。一个撤单请求进来,系统要:

  1. 找到这个订单在队列中的位置
  2. 从队列中移除
  3. 更新总量统计
  4. 释放冻结的资金或资产

你想想看,如果订单正在被撮合,这时候撤单怎么办?这就是经典的「撤单与撮合竞态」问题。

我记得有一次,我们的系统在高并发下,撤单和撮合同时操作同一个订单。结果订单被撮合了,但撤单也成功了,资金被释放了两次。嗯,那天的损失,够我吃一年泡面。

避坑指南:我曾经在撤单逻辑里加了一个「状态锁」。订单一旦进入撮合流程,就标记为「撮合中」,撤单请求必须等待撮合完成。虽然牺牲了一点性能,但保证了数据一致性。

3.4 订单修改:改价还是改量

订单修改,其实是个「伪操作」。大部分交易所的实现方式是:先撤单,再下新单。为什么?因为修改价格,相当于改变了订单在队列中的位置。如果直接改,时间优先的规则就乱了。

但这里有个优化点:如果只修改数量,且是减少数量,可以不撤单。因为减少数量不影响时间优先,直接更新队列里的数量就行。

修改类型 实现方式 性能影响
改价 先撤单,再下新单 高(两次队列操作)
增量(增加数量) 先撤单,再下新单 高(两次队列操作)
减量(减少数量) 直接更新队列 低(一次队列操作)

我个人习惯,在做市商策略中,尽量用「减量修改」来调整订单。因为撤单再下单,中间有短暂的时间窗口,订单不在订单簿里,可能会错过行情。

3.5 订单簿的动态更新流程

说了这么多,我们来看一张整体流程图。这张图,是我当年做系统设计时画的,后来成了团队内部培训的经典教材。

订单簿动态更新流程 订单进入 可撮合? 撮合处理 订单入队 更新订单簿(价格/数量/队列) 事件推送(行情/成交/撤单)

这张图,说白了就是订单从生到死的完整生命周期。每个环节,都有坑。我当年做系统时,每个环节都踩过雷。

3.6 做市商视角:如何利用订单簿更新

作为做市商,我们不只是被动地接收订单簿更新。我们更关心的是:如何利用这些更新,来优化自己的报价策略

举个例子,当一个大买单进入订单簿时,说明买方力量强。这时候,我会适当提高卖单价格,赚取更多价差。反之,如果出现大卖单,我会降低买单价格。

但这里有个陷阱:不要只看单笔订单。我曾经被一个「虚假大单」骗过——有人下了个大买单,然后马上撤单,制造买盘强劲的假象。等我提高卖价后,他再偷偷卖出。

我的做法:我会监控「订单存活时间」。如果一个订单进来后,几秒钟就撤了,那大概率是虚假订单。我会忽略它的影响。

3.7 性能优化:订单簿更新的关键指标

订单簿更新的性能,直接决定了你的交易延迟。我总结几个关键指标:

  • 吞吐量:每秒能处理多少笔订单操作(进入/撮合/撤销/修改)
  • 延迟:从订单进入系统到完成更新的时间
  • 一致性:订单簿的状态是否与撮合结果一致

我参与过一个项目,订单簿更新延迟从 10ms 优化到了 100μs。怎么做到的?说白了就两招:无锁数据结构内存池

嗯,这里不展开讲,但你可以记住一个原则:订单簿的操作,尽量在内存中完成,避免磁盘IO。磁盘IO是性能杀手,没有之一。

警告:我曾经见过一个团队,把订单簿数据存在数据库里,每次更新都写一次数据库。结果系统一上线,延迟直接飙到秒级。嗯,那项目后来重写了。

好了,订单簿的动态更新,核心就是这些。记住:进入、撮合、撤销、修改,这四个动作,每一个都值得你花时间去优化。做市商赚的钱,说白了就是从这些毫秒级的优化里抠出来的。