3. 内存撮合引擎基础:撮合逻辑
做市商报价引擎的核心,说白了就是撮合。你报了个价,市场上有对手盘,系统怎么把你们俩凑到一起?这就是撮合引擎要干的事。我当年刚接触这块时,觉得不就是个匹配嘛,后来踩了不少坑才明白——撮合逻辑的每一行代码,都直接关系到你的盈亏。
3.1 限价单与市价单:两种最基本的订单类型
先聊聊订单类型。做市商系统里,最常见的就是限价单和市价单。
限价单,就是你指定一个价格,比如“我只在100元买,高于这个价我不买”。系统会把这个单子挂到订单簿上,等着有人来成交。我在项目中遇到过,有些做市商喜欢用限价单来提供流动性,赚取买卖价差。
市价单,就是“不管什么价格,我现在就要成交”。系统会立即以当前最优的价格帮你吃掉对手盘。市价单不挂单,直接成交。嗯,这里要注意——市价单在流动性不足时,可能会滑点很大。
核心区别:
- 限价单:指定价格,挂单等待,提供流动性
- 市价单:不指定价格,立即成交,消耗流动性
你想想看,做市商报价引擎里,我们通常用限价单来挂双边报价,用市价单来应对突发的大额订单。两者配合,才能把市场做活。
3.2 价格优先-时间优先原则
撮合引擎的黄金法则是什么?价格优先,时间优先。这个原则,所有交易所都在用。
价格优先:买单,出价高的优先成交;卖单,出价低的优先成交。说白了,谁给的价格更激进,谁就先成交。
时间优先:价格相同的情况下,谁先挂单,谁先成交。这个很好理解,先来后到嘛。
我曾经在一个项目中,因为时间戳精度不够,导致两个同时挂的单子顺序错乱,结果被客户投诉。后来我强制要求所有订单的时间戳精确到纳秒级别,才彻底解决这个问题。
避坑指南:我曾经在实现时间优先时,直接用系统时间戳。后来发现,如果两个订单在同一微秒内到达,系统时间戳可能相同。我的建议是:使用全局递增的序列号,配合时间戳,确保唯一性和顺序性。
3.3 部分成交与完全成交处理
一个订单进来,可能一次性全部成交,也可能只成交一部分。这两种情况,处理方式完全不同。
完全成交:订单的剩余数量为0,直接移除。比如你挂了10手买单,对手盘正好有10手卖单,一碰就全没了。这时候订单从订单簿里消失,成交记录生成。
部分成交:订单只成交了一部分,剩余部分继续挂在订单簿上。比如你挂了10手买单,对手盘只有3手卖单,那这3手成交,剩下的7手继续等着。
我建议你在设计数据结构时,把订单的filled_quantity和remaining_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