3. 内存撮合引擎基础:撮合逻辑

做市商报价引擎的核心,说白了就是撮合。你报了个价,市场上有对手盘,系统怎么把你们俩凑到一起?这就是撮合引擎要干的事。我当年刚接触这块时,觉得不就是个匹配嘛,后来踩了不少坑才明白——撮合逻辑的每一行代码,都直接关系到你的盈亏。

3.1 限价单与市价单:两种最基本的订单类型

先聊聊订单类型。做市商系统里,最常见的就是限价单和市价单。

限价单,就是你指定一个价格,比如“我只在100元买,高于这个价我不买”。系统会把这个单子挂到订单簿上,等着有人来成交。我在项目中遇到过,有些做市商喜欢用限价单来提供流动性,赚取买卖价差。

市价单,就是“不管什么价格,我现在就要成交”。系统会立即以当前最优的价格帮你吃掉对手盘。市价单不挂单,直接成交。嗯,这里要注意——市价单在流动性不足时,可能会滑点很大。

核心区别:

  • 限价单:指定价格,挂单等待,提供流动性
  • 市价单:不指定价格,立即成交,消耗流动性

你想想看,做市商报价引擎里,我们通常用限价单来挂双边报价,用市价单来应对突发的大额订单。两者配合,才能把市场做活。

3.2 价格优先-时间优先原则

撮合引擎的黄金法则是什么?价格优先,时间优先。这个原则,所有交易所都在用。

价格优先:买单,出价高的优先成交;卖单,出价低的优先成交。说白了,谁给的价格更激进,谁就先成交。

时间优先:价格相同的情况下,谁先挂单,谁先成交。这个很好理解,先来后到嘛。

我曾经在一个项目中,因为时间戳精度不够,导致两个同时挂的单子顺序错乱,结果被客户投诉。后来我强制要求所有订单的时间戳精确到纳秒级别,才彻底解决这个问题。

避坑指南:我曾经在实现时间优先时,直接用系统时间戳。后来发现,如果两个订单在同一微秒内到达,系统时间戳可能相同。我的建议是:使用全局递增的序列号,配合时间戳,确保唯一性和顺序性。

3.3 部分成交与完全成交处理

一个订单进来,可能一次性全部成交,也可能只成交一部分。这两种情况,处理方式完全不同。

完全成交:订单的剩余数量为0,直接移除。比如你挂了10手买单,对手盘正好有10手卖单,一碰就全没了。这时候订单从订单簿里消失,成交记录生成。

部分成交:订单只成交了一部分,剩余部分继续挂在订单簿上。比如你挂了10手买单,对手盘只有3手卖单,那这3手成交,剩下的7手继续等着。

我建议你在设计数据结构时,把订单的filled_quantityremaining_quantity分开存储。这样处理部分成交时,只需要更新剩余数量,不需要重建订单对象。

注意:部分成交后,订单的时间优先级不变。也就是说,你原来挂单的时间戳保持不变,后续如果有新的对手盘进来,你仍然排在前面。这一点很多新手会搞错,以为部分成交后要重新排队。

3.4 撮合结果事件推送

撮合完成后,系统需要把结果推送给各个模块。比如风控模块、资金模块、行情模块,还有做市商自己的策略引擎。

我习惯把撮合结果封装成一个事件对象,包含以下字段:

字段名 类型 说明
event_type string 事件类型,如"trade"、"cancel"
order_id string 订单ID
trade_price double 成交价格
trade_quantity int 成交数量
remaining_quantity int 剩余未成交数量
timestamp long 撮合时间戳

事件推送的方式,我推荐用发布-订阅模式。撮合引擎只管发布事件,其他模块自己订阅感兴趣的事件。这样耦合度低,扩展性好。

举个例子,行情模块订阅了trade事件,每次撮合完成,行情模块就能实时更新最新成交价和成交量。做市商策略引擎订阅了order_update事件,就能知道自己的订单有没有成交,从而决定是否调整报价。

我的经验:事件推送一定要做到至少一次的语义。也就是说,事件不能丢。我曾经因为事件队列满了,丢了几笔成交记录,结果对账时发现资金对不上。后来我改用有界队列+背压机制,才彻底解决这个问题。

3.5 核心撮合流程示意图

下面我用一张SVG图,把整个撮合流程串起来。你看完应该就清楚了。

内存撮合引擎核心流程 订单到达 判断订单类型 市价单 限价单 立即匹配最优价 挂单到订单簿 价格优先-时间优先匹配 推送撮合结果事件

这张图把整个流程串起来了。订单进来,先判断类型,市价单直接匹配,限价单挂单等待。然后按照价格优先-时间优先的原则进行匹配,最后推送结果事件。每一步都很清晰。

3.6 代码示例:简单的撮合逻辑

最后,我写一段简单的Python代码,演示一下核心的撮合逻辑。你感受一下。

class Order:
    def __init__(self, order_id, side, price, quantity, timestamp):
        self.order_id = order_id
        self.side = side  # 'buy' or 'sell'
        self.price = price
        self.quantity = quantity
        self.remaining = quantity
        self.timestamp = timestamp

class MatchEngine:
    def __init__(self):
        self.buy_orders = []   # 买单列表,按价格降序、时间升序
        self.sell_orders = []  # 卖单列表,按价格升序、时间升序
    
    def add_order(self, order):
        if order.side == 'buy':
            self._match(order, self.sell_orders)
            if order.remaining > 0:
                self.buy_orders.append(order)
                self.buy_orders.sort(key=lambda x: (-x.price, x.timestamp))
        else:
            self._match(order, self.buy_orders)
            if order.remaining > 0:
                self.sell_orders.append(order)
                self.sell_orders.sort(key=lambda x: (x.price, x.timestamp))
    
    def _match(self, taker_order, maker_orders):
        for maker_order in maker_orders[:]:
            if taker_order.remaining == 0:
                break
            # 价格优先检查
            if taker_order.side == 'buy' and taker_order.price < maker_order.price:
                break
            if taker_order.side == 'sell' and taker_order.price > maker_order.price:
                break
            # 计算成交数量
            trade_qty = min(taker_order.remaining, maker_order.remaining)
            taker_order.remaining -= trade_qty
            maker_order.remaining -= trade_qty
            # 推送成交事件
            self._emit_trade(taker_order, maker_order, trade_qty)
            # 移除完全成交的订单
            if maker_order.remaining == 0:
                maker_orders.remove(maker_order)
    
    def _emit_trade(self, taker, maker, qty):
        print(f"成交: {taker.order_id} <-> {maker.order_id}, 数量: {qty}")

这段代码虽然简单,但核心逻辑都在了。你想想看,实际生产环境里,我们还要考虑并发、性能优化、内存管理等问题。但基础框架就是这个样子。

我的建议:刚开始做撮合引擎时,不要一上来就追求高性能。先把逻辑写对,再考虑优化。我见过太多人一开始就搞无锁队列、内存池,结果逻辑错了,性能再高也没用。

好了,这一章的内容就到这里。撮合逻辑是报价引擎的心脏,你把它搞透了,后面的章节学起来就轻松多了。


公众号:蓝海资料掘金营,微信deep3321