撮合引擎原理:价格优先、时间优先原则,逐笔撮合与批量撮合
做高频交易的朋友都知道,订单簿重建是整个系统的地基。但地基下面还有地基——撮合引擎。说白了,交易所到底是怎么把两个订单凑到一起的?这个机制决定了你所有的策略逻辑该怎么写。
我刚开始做这块的时候,觉得撮合不就是比价格、比时间嘛,简单。结果第一次对接实盘环境,发现事情远没那么简单。嗯,今天咱们就把这个底层逻辑彻底讲透。
一、价格优先与时间优先:两条铁律
撮合引擎的核心原则就两条,我称之为「铁律」:
- 价格优先:买单出价高的优先成交,卖单出价低的优先成交
- 时间优先:同等价格下,先到单子的优先成交
你想想看,这其实很符合直觉。谁出的价更激进,谁就先成交。价格一样?那就看谁手快。
关键点:价格优先的优先级高于时间优先。只有价格相同时,时间才起作用。
我在项目中遇到过一个问题:某个策略同时挂了多个限价单,价格相同,但成交顺序总是不对。后来排查发现,是订单到达交易所的时间戳精度不够,导致时间优先判断失效。这个坑,后面会细说。
二、订单簿的数据结构设计
要实现撮合,首先得把订单簿组织好。我个人习惯用「价格链表 + 订单队列」的组合:
// 简化版订单簿结构
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) 的代价谁也扛不住。
三、逐笔撮合:实时匹配
逐笔撮合,就是来一笔订单,立刻尝试匹配。这是最主流的撮合方式,也是高频交易最关心的。
流程其实很简单:
- 新订单到达,先判断是买单还是卖单
- 如果是买单,从卖单价格链表的最低价格开始,逐层匹配
- 如果卖单价格 ≤ 买单价格,成交
- 成交后,更新双方订单的剩余数量
- 如果买单还有剩余,插入到买单价格链表中
我的经验:逐笔撮合的关键在于「价格交叉检查」。我曾经漏掉了跨价格层的匹配,导致订单簿出现「价格空洞」,回测数据全废了。一定要确保从最优价格开始,逐层向下/向上扫描。
来看一个实际例子:
卖单队列(从低到高):
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),问题解决。
坑二:时间戳精度不够
有些交易所的时间戳只到毫秒级。高频场景下,同一毫秒内可能有几十笔订单。如果只靠毫秒时间戳,时间优先原则就形同虚设。我建议用纳秒级时间戳 + 自增序列号来保证唯一性。
坑三:订单簿的并发访问
撮合引擎通常是单线程处理的,但如果你自己实现订单簿重建,要注意多线程读写锁的问题。我见过一个团队用读写锁,结果读操作被写操作阻塞,延迟飙升。后来改用无锁队列 + 内存屏障,性能提升了一个数量级。
八、总结
撮合引擎的原理,说白了就是「价格优先、时间优先」这八个字。但实现起来,细节决定成败。
逐笔撮合适合连续交易,延迟低但价格波动大。批量撮合适合集合竞价,价格平滑但延迟高。两者各有适用场景,没有绝对的好坏。
我个人建议,做高频交易的朋友一定要把逐笔撮合的逻辑吃透。因为你的策略最终要跟交易所的撮合引擎打交道,理解它的行为模式,才能写出真正有效的订单簿重建代码。
嗯,今天就先聊到这里。记住,撮合引擎是订单簿重建的基石,这块搞不明白,后面所有的优化都是空中楼阁。