4. 限价订单的插入与档位更新

限价订单进入撮合引擎后,并不是简单地“挂到队尾”就结束了。一笔新订单要真正成为订单簿的一部分,需要依次完成价格档位查找、队列插入、档位数量更新三个动作,并在过程中处理“档位不存在需新建”“档位已存在需追加”“订单可立即成交需先撮合”等分支。本节围绕这一插入过程展开,重点讲清楚每一步在数据结构上究竟发生了什么。

一、插入前先判断:这笔订单会不会先成交

限价订单到达时,引擎首先做的是可成交性判断,而不是直接插入。以买方向为例,若新买单价格大于等于当前卖一价,则它与卖侧存在交叉,必须先进入撮合流程,成交剩余部分才可能挂入买侧订单簿;若新买单价格低于卖一价,则不存在交叉,直接进入挂单插入流程。卖方向对称:卖价小于等于买一价则先撮合,否则直接挂单。这个判断决定了后续是“先撮合再插入”还是“纯插入”,是插入流程的第一个分叉点。

需要强调的是,只有“不可立即成交”的订单才走完整的档位查找与插入路径。因此本节讨论的插入,指的是订单在对手侧无交叉、需要停留在本方订单簿中的情形。

二、价格档位查找:定位目标档位

订单簿通常按价格档位组织:每个价格对应一个档位对象,档位内部维护一个订单队列。查找的目标是判断“该价格档位是否已存在”。实现上常见两种结构:

  • 有序数组/链表:按价格排序存放档位,查找可用二分或顺序扫描,适合档位数量较少的场景;
  • 哈希表 + 有序索引:用哈希表以价格为键做 O(1) 定位,同时用有序结构(如平衡树、跳表)维护价格顺序以便取最优价,适合档位多、更新频繁的场景。

查找结果只有两种:命中已有档位,或未命中需要新建档位。以示例参数说明:假设当前买侧已有档位 100.00(队列含 3 笔)、99.98(队列含 1 笔),新买单价格为 99.99,则查找未命中,需要新建 99.99 档位,并把它插入到 100.00 与 99.98 之间,保持买侧价格从高到低排列。

三、队列插入:档位内部的时间优先

同一价格档位内,订单按到达时间先后排队,先到先成交,这就是价格优先、时间优先原则中的时间维度。新订单插入时一律追加到该档位队列的尾部,不允许插队。若档位是新建的,则创建空队列后把该订单作为队首放入。

队列本身可用双向链表实现,尾部追加为 O(1);也可用动态数组,但要注意删除中间订单(撤单)时的搬移成本。实际工程中常配合“订单号 → 队列节点”的映射,使得撤单时能直接定位并摘除节点,而不必扫描整个队列。

四、档位数量更新:维护聚合字段

每个档位通常维护两个聚合量:订单笔数和累计数量。新订单插入后,需要把该档位的订单笔数加 1,累计数量加上本单剩余数量。这两个字段是行情推送和最优价计算的重要输入,必须与队列内容严格一致。

以示例说明:新买单剩余数量 500 股,插入 99.99 新建档位后,该档位订单笔数由 0 变为 1,累计数量由 0 变为 500。若随后又有一笔 300 股的 99.99 买单到达,则笔数变为 2,累计数量变为 800。反过来,撤单或成交导致订单离队时,必须对称地减 1 并扣减相应数量;当笔数与累计数量同时归零时,该档位应从订单簿中移除,避免留下空档位影响最优价判断。

档位数量更新最容易出错的地方是“只加不减”或“减错数量”。建议在实现中把插入与移除封装成一对对称操作,并在每次变更后校验:档位累计数量应等于队列中所有订单剩余数量之和。

五、图解:一笔新买单的插入路径

下图以一笔价格 99.99、数量 500 股的买单为例,展示从到达、可成交性判断、档位查找、队列插入到档位数量更新的完整路径,并标出“命中已有档位”与“新建档位”两个分支。

新买单插入订单簿的处理路径(示例:99.99 / 500 股) ① 新限价买单到达 价格 99.99 · 数量 500 ② 可成交性判断 99.99 ≥ 卖一价? 是:先撮合 剩余量再挂单 ③ 查找 99.99 档位 哈希定位 / 有序扫描 未命中:新建档位 插入 100.00 与 99.98 之间 命中:复用已有档位 直接追加到队尾 ④ 队列尾部插入 时间优先,不插队 ⑤ 档位数量更新 笔数 +1 · 累计量 +500 否:无交叉 结果:买侧新增 99.99 档位,队列 [500],笔数 1,累计量 500;买一价仍为 100.00

六、实现要点与常见问题

把上述过程落到代码,有几个细节值得注意。第一,档位查找与新建必须保证价格顺序正确:新建档位要插入到有序索引的合适位置,买侧按价格降序、卖侧按价格升序,否则最优价会取错。第二,队列插入与档位数量更新应当原子完成,避免出现“订单已入队但笔数未加”的中间状态被行情线程读到。第三,订单剩余数量在部分成交后会变化,档位累计数量应基于“剩余数量”而非“原始数量”维护。

一个可参考的插入伪代码如下,重点看查找、插入、更新三步的衔接:

insertLimitOrder(order):
    if canCross(order):            # 与对手侧存在交叉
        order = match(order)       # 先撮合,返回剩余部分
        if order.remaining == 0:
            return                 # 全部成交,无需挂单

    level = book.findLevel(order.side, order.price)
    if level is None:              # 未命中,新建档位
        level = book.createLevel(order.side, order.price)
        book.insertLevelInOrder(level)   # 维持价格有序

    level.queue.append(order)      # 队尾追加,时间优先
    level.orderCount += 1
    level.totalQty += order.remaining
    order.levelRef = level         # 便于后续撤单/成交时回查

这段伪代码体现了本节的核心链条:可成交性判断决定是否先撮合,档位查找决定新建还是复用,队列插入保证时间优先,数量更新维护聚合字段。四者缺一不可,任何一步遗漏都会导致订单簿状态与真实委托不一致。

七、如何验证插入结果正确

插入完成后,可以从三个层面检查。其一,看档位是否存在且价格顺序正确,例如买侧应呈现 100.00、99.99、99.98 的降序排列。其二,看目标档位队列的尾部是否为刚插入的订单,且笔数与累计数量与队列内容吻合。其三,看最优价是否被意外改变:本例中 99.99 低于买一价 100.00,因此买一价不变;若新单价格高于原买一价且无交叉,则买一价应更新为新单价格。把这三个检查点固化为测试用例,能有效覆盖插入逻辑的主要分支。

若发现档位累计数量与队列实际之和不一致,优先排查两处:撤单时是否漏减数量,以及部分成交后是否仍按原始数量累计。这类不一致会直接污染行情快照,且往往在压测或长时间运行后才暴露。

至此,一笔限价订单从到达、判断、查找、插入到数量更新的完整路径已经清晰。理解这条路径,是后续分析撤单、成交扣减以及订单簿快照生成的基础。