撮合引擎原理:限价单、市价单的撮合逻辑与价格优先、时间优先原则

说到撮合引擎,这其实是整个交易系统的核心心脏。我当年刚入行时,总觉得撮合不就是把买单和卖单匹配一下嘛,有什么难的?直到自己动手写了一个简陋版本,上线第一天就被行情冲击得七零八落……嗯,从那以后我再也不敢小看这个模块了。

今天咱们就来聊聊,撮合引擎到底是怎么工作的。说白了,就是两件事:谁先成交以什么价格成交

一、订单类型:限价单 vs 市价单

先搞清楚两个基本概念。你想想看,你去菜市场买菜,有两种方式:

  • 限价单:你说「我只出5块钱一斤,多了不要」。这就是限价单——你指定了价格,只有价格到了你的心理价位才成交。
  • 市价单:你说「给我来两斤,多少钱都行,赶紧的」。这就是市价单——你不关心价格,只关心能不能立刻成交。

在代码层面,这两种订单的处理逻辑完全不同。我习惯把限价单叫做「有耐心的订单」,把市价单叫做「急性子订单」。

核心区别一句话:

限价单是「等价格」,市价单是「抢时间」。

二、价格优先原则:谁出价好谁先上

价格优先,这个很好理解。但具体怎么「优先」?我见过不少新手搞反了方向。

对于买单:出价高的优先成交。比如A出100块买,B出99块买,那A的订单永远排在B前面。

对于卖单:出价低的优先成交。比如C挂100块卖,D挂101块卖,那C的订单先被吃掉。

为什么会这样?因为市场永远倾向于「对对手方更有利」的价格。你想想看,如果你是买家,你当然希望买到更便宜的货;如果你是卖家,你当然希望卖得更贵。但撮合引擎是站在中间做匹配的,它要保证整个市场的流动性最优。

我个人的一个小习惯:

在写撮合逻辑时,我会把买单按价格从高到低排序,卖单按价格从低到高排序。这样每次取队首元素,就是当前最优价格的订单。

三、时间优先原则:先来后到

当价格相同时,谁先挂单谁先成交。这个原则看似简单,但实现起来有个坑——时间戳的精度问题

我曾经在一个项目中遇到过这样的情况:两个订单在同一毫秒内到达,时间戳完全一样。结果撮合引擎随机选了一个先成交,客户投诉说「凭什么他的单子先成交?」

从那以后,我引入了序列号机制。每个订单除了时间戳,还有一个全局递增的序列号。即使时间戳相同,序列号小的先处理。这样就能保证绝对的公平。

注意:

不要依赖系统时间来做唯一排序依据。分布式环境下,不同服务器的时钟可能有偏差。建议使用「时间戳 + 自增ID」的组合方案。

四、限价单的撮合逻辑

限价单的撮合,其实就是一个「价格匹配」的过程。我画个图帮你理解:

限价单撮合流程 买单队列(价格从高到低) 买1: 100.5元 10手 买2: 100.3元 5手 买3: 100.0元 8手 卖单队列(价格从低到高) 卖1: 100.2元 3手 卖2: 100.4元 7手 卖3: 100.6元 4手 价格匹配 成交:100.2元 3手(买1 vs 卖1) 买1出价100.5元,卖1出价100.2元 买价 ≥ 卖价,可以成交 成交价取卖1的价格(100.2元)

具体流程是这样的:

  1. 新来一个买单,价格是P_buy,数量是Q_buy
  2. 从卖单队列的队首(最低价)开始检查
  3. 如果P_buy ≥ 卖单价格,则成交。成交价取卖单价格(这是限价单的规则)
  4. 成交数量取 min(Q_buy, 卖单数量)
  5. 如果买单还有剩余,继续匹配下一个卖单
  6. 直到P_buy < 下一个卖单价格,或者买单全部成交
  7. 如果还有剩余,把买单插入到买单队列中

关键点:

限价单的成交价,永远取「对手方」的价格。也就是说,买单吃卖单,成交价是卖单价;卖单吃买单,成交价是买单价。

五、市价单的撮合逻辑

市价单就简单粗暴多了。它不关心价格,只关心能不能成交。所以市价单进来后,直接去对手方队列里「扫货」。

我举个例子:

假设当前卖单队列是这样的:

  • 卖1: 100.2元 3手
  • 卖2: 100.4元 7手
  • 卖3: 100.6元 4手

现在来了一个市价买单,数量是10手。那它会怎么做?

  1. 先吃掉卖1的3手,成交价100.2元
  2. 再吃掉卖2的7手,成交价100.4元
  3. 10手全部成交,结束

注意,这里产生了两个不同的成交价。所以市价单可能会造成「滑点」——你实际成交的平均价格,可能比你预期的要差一些。

避坑指南:

我曾经见过一个系统,市价单进来后直接按对手方最优价成交全部数量。结果一个大单直接把价格打穿了,市场剧烈波动。正确的做法是:市价单要逐层扫货,每层按该层的价格成交。

六、价格优先 + 时间优先的完整实现

把两个原则结合起来,就是完整的撮合逻辑。我习惯用两个优先队列来实现:

  • 买单队列:按价格降序排列,价格相同按时间升序
  • 卖单队列:按价格升序排列,价格相同按时间升序

每次新订单进来,先检查能否与对手方队列匹配。如果能,就按上述逻辑成交;如果不能,就插入到自己的队列中等待。

这里有个细节:部分成交。如果一个订单只成交了一部分,剩下的部分要重新插入队列,并且保持原来的时间戳。这样才能保证时间优先的公平性。

我个人的实现技巧:

用红黑树或者跳表来实现订单队列,插入和删除都是O(log n)的复杂度。如果订单量不大,直接用数组+二分查找也行。但千万不要用链表,否则在高并发下性能会崩。

七、实战中的几个坑

最后分享几个我踩过的坑:

现象 解决方案
浮点数精度 价格比较时出现0.0000001的误差 全部用整数表示,比如把价格乘以10000
并发安全 两个线程同时修改队列,数据错乱 使用无锁队列或细粒度锁
大单拆分 一个大单吃掉多个对手单,产生多个成交记录 每个成交记录独立存储,不要合并
自成交 同一个用户的买单和卖单互相匹配 检查订单的user_id,避免自成交

嗯,以上就是撮合引擎的核心原理。说白了就是两句话:好价格优先,同价格先来后到。但真正实现起来,细节决定成败。希望今天的分享对你有帮助。