撮合引擎原理:价格优先、时间优先原则,逐笔撮合与批量撮合

做高频交易的朋友都知道,订单簿重建是整个系统的地基。但地基下面还有地基——撮合引擎。说白了,交易所到底是怎么把两个订单凑到一起的?这个机制决定了你所有的策略逻辑该怎么写。

我刚开始做这块的时候,觉得撮合不就是比价格、比时间嘛,简单。结果第一次对接实盘环境,发现事情远没那么简单。嗯,今天咱们就把这个底层逻辑彻底讲透。

一、价格优先与时间优先:两条铁律

撮合引擎的核心原则就两条,我称之为「铁律」:

  • 价格优先:买单出价高的优先成交,卖单出价低的优先成交
  • 时间优先:同等价格下,先到单子的优先成交

你想想看,这其实很符合直觉。谁出的价更激进,谁就先成交。价格一样?那就看谁手快。

关键点:价格优先的优先级高于时间优先。只有价格相同时,时间才起作用。

我在项目中遇到过一个问题:某个策略同时挂了多个限价单,价格相同,但成交顺序总是不对。后来排查发现,是订单到达交易所的时间戳精度不够,导致时间优先判断失效。这个坑,后面会细说。

二、订单簿的数据结构设计

要实现撮合,首先得把订单簿组织好。我个人习惯用「价格链表 + 订单队列」的组合:

// 简化版订单簿结构
struct Order {
    uint64_t order_id;
    double price;
    uint32_t volume;
    uint64_t timestamp;  // 纳秒级
    Order* next;         // 同价格队列中的下一个
};

struct PriceLevel {
    double price;
    uint32_t total_volume;
    Order* head;         // 时间优先队列头
    Order* tail;         // 时间优先队列尾
    PriceLevel* prev;    // 价格链表前驱
    PriceLevel* next;    // 价格链表后继
};

struct OrderBook {
    PriceLevel* buy_head;   // 买单价格链表(从高到低)
    PriceLevel* sell_head;  // 卖单价格链表(从低到高)
};

为什么用链表?因为高频场景下,订单的增删改极其频繁。数组的插入删除是 O(n),链表是 O(1)。你想想看,每秒几万笔订单,O(n) 的代价谁也扛不住。

三、逐笔撮合:实时匹配

逐笔撮合,就是来一笔订单,立刻尝试匹配。这是最主流的撮合方式,也是高频交易最关心的。

流程其实很简单:

  1. 新订单到达,先判断是买单还是卖单
  2. 如果是买单,从卖单价格链表的最低价格开始,逐层匹配
  3. 如果卖单价格 ≤ 买单价格,成交
  4. 成交后,更新双方订单的剩余数量
  5. 如果买单还有剩余,插入到买单价格链表中

我的经验:逐笔撮合的关键在于「价格交叉检查」。我曾经漏掉了跨价格层的匹配,导致订单簿出现「价格空洞」,回测数据全废了。一定要确保从最优价格开始,逐层向下/向上扫描。

来看一个实际例子:

卖单队列(从低到高):
  100.00 → 100.01 → 100.02 → 100.03

新买单:价格 100.02,数量 500

匹配过程:
  1. 检查 100.00:100.00 ≤ 100.02,成交
  2. 检查 100.01:100.01 ≤ 100.02,成交
  3. 检查 100.02:100.02 ≤ 100.02,成交
  4. 买单剩余数量为 0,匹配结束

四、批量撮合:集合竞价

批量撮合,说白了就是攒一批订单,统一处理。常见于开盘、收盘的集合竞价阶段。

为什么需要批量撮合?因为逐笔撮合在极端行情下会产生「闪电崩盘」——价格瞬间被打穿。批量撮合可以平滑价格波动。

批量撮合的核心逻辑:

  • 收集一段时间内的所有订单
  • 计算一个「均衡价格」,使得成交量最大
  • 所有订单按这个价格统一成交

注意:批量撮合的时间窗口选择很关键。窗口太短,起不到平滑作用;窗口太长,订单延迟太大。我见过一个交易所把窗口设成 500ms,结果高频策略全废了——订单延迟比逐笔撮合大了两个数量级。

五、两种撮合方式的对比

特性 逐笔撮合 批量撮合
延迟 微秒级 毫秒级
公平性 时间优先严格 按窗口内随机/比例分配
价格波动 可能剧烈 相对平滑
适用场景 连续交易时段 开盘/收盘/临时停牌

六、核心流程图:撮合引擎的完整逻辑

下面这张图,是我自己画的一个撮合引擎核心流程。你看一眼就能明白整个匹配过程:

撮合引擎核心流程 新订单到达 买单还是卖单? 扫描卖单队列 扫描买单队列 卖价 ≤ 买价? (买单处理) 买价 ≥ 卖价? (卖单处理) 执行成交 更新订单簿 执行成交 更新订单簿 订单有剩余? 插入订单队列 撮合完成

七、避坑指南:我踩过的几个坑

坑一:浮点数精度

我曾经用 double 存储价格,结果在价格比较时出现了 0.0000001 的误差,导致撮合逻辑偶尔失效。后来全部改用整数存储(比如用 10000 表示 100.00),问题解决。

坑二:时间戳精度不够

有些交易所的时间戳只到毫秒级。高频场景下,同一毫秒内可能有几十笔订单。如果只靠毫秒时间戳,时间优先原则就形同虚设。我建议用纳秒级时间戳 + 自增序列号来保证唯一性。

坑三:订单簿的并发访问

撮合引擎通常是单线程处理的,但如果你自己实现订单簿重建,要注意多线程读写锁的问题。我见过一个团队用读写锁,结果读操作被写操作阻塞,延迟飙升。后来改用无锁队列 + 内存屏障,性能提升了一个数量级。

八、总结

撮合引擎的原理,说白了就是「价格优先、时间优先」这八个字。但实现起来,细节决定成败。

逐笔撮合适合连续交易,延迟低但价格波动大。批量撮合适合集合竞价,价格平滑但延迟高。两者各有适用场景,没有绝对的好坏。

我个人建议,做高频交易的朋友一定要把逐笔撮合的逻辑吃透。因为你的策略最终要跟交易所的撮合引擎打交道,理解它的行为模式,才能写出真正有效的订单簿重建代码。

嗯,今天就先聊到这里。记住,撮合引擎是订单簿重建的基石,这块搞不明白,后面所有的优化都是空中楼阁。

专注资料整理