撮合引擎原理
撮合引擎是交易所的心脏,决定了交易的公平性和效率
📖 章节概述
本章深入讲解撮合引擎的核心原理,包括订单簿结构、撮合算法、价格优先时间优先规则等,为后续实现打下坚实基础。
学习目标
- 理解撮合引擎的基本概念和作用
- 掌握订单簿的数据结构设计
- 理解价格优先、时间优先的撮合规则
- 掌握限价单和市价单的撮合流程
- 了解撮合引擎的性能要求
一、撮合引擎基础
1.1 什么是撮合引擎
撮合引擎(Matching Engine)是交易所的核心组件,负责:
| 功能 | 说明 |
|---|---|
| 接收订单 | 处理用户的买卖订单 |
| 维护订单簿 | 管理所有未成交订单 |
| 执行撮合 | 匹配买卖订单并成交 |
| 生成成交 | 记录成交价格、数量、时间 |
| 更新账户 | 扣减买方资金、增加卖方资金 |
1.2 撮合引擎的要求
| 要求 | 说明 | 目标值 |
|---|---|---|
| 公平性 | 严格按价格优先、时间优先规则 | 100% |
| 高吞吐 | 支持高并发订单处理 | > 100万QPS |
| 低延迟 | 订单到成交的时间 | < 1ms |
| 高可用 | 系统稳定性 | 99.99% |
| 准确性 | 撮合结果零错误 | 100% |
1.3 撮合模式
模式1:连续撮合(Continuous Matching)
特点:
- 订单实时撮合
- 适用于:股票、数字货币现货交易
- 优点:即时成交
- 缺点:可能产生价格波动
模式2:集合竞价(Call Auction)
特点:
- 收集一段时间的订单,统一撮合
- 适用于:开盘、收盘
- 优点:减少价格波动,发现合理价格
- 缺点:成交延迟高
本手册重点介绍连续撮合模式
二、订单簿(Order Book)
2.1 订单簿的概念
订单簿是所有未成交订单的集合,分为买单簿(Bids)和卖单簿(Asks)。
卖单簿(Asks) - 按价格从低到高排列
┌────────────────────────────────┐
│ 价格 数量 时间 │
├────────────────────────────────┤
│ 101.5 10 14:00:03 │ ← 卖1(最低卖价)
│ 101.6 20 14:00:05 │ ← 卖2
│ 101.8 15 14:00:01 │ ← 卖3
└────────────────────────────────┘
现价:100.0
┌────────────────────────────────┐
│ 价格 数量 时间 │
├────────────────────────────────┤
│ 100.0 30 14:00:00 │ ← 买1(最高买价)
│ 99.9 25 14:00:02 │ ← 买2
│ 99.8 40 14:00:04 │ ← 买3
└────────────────────────────────┘
买单簿(Bids) - 按价格从高到低排列
2.2 关键概念
| 概念 | 说明 |
|---|---|
| 买1(Best Bid) | 订单簿中最高的买入价格 |
| 卖1(Best Ask) | 订单簿中最低的卖出价格 |
| 价差(Spread) | 卖1 - 买1,反映市场流动性 |
| 深度(Depth) | 每个价格档位的订单数量 |
| 盘口(Book) | 买卖双方的前N档报价 |
示例:
卖1: 101.5 (10张)
买1: 100.0 (30张)
价差: 101.5 - 100.0 = 1.5
2.3 订单簿的数据结构
需求分析
| 操作 | 频率 | 复杂度要求 |
|---|---|---|
| 插入订单 | 极高 | O(log n) |
| 删除订单(撤单) | 高 | O(log n) |
| 查找最优价格 | 极高 | O(1) |
| 遍历同价格订单 | 高 | O(k),k是该价格订单数 |
数据结构选择
type OrderBook struct {
Symbol string
Bids *PriceLevel // 买单簿(红黑树/跳表)
Asks *PriceLevel // 卖单簿(红黑树/跳表)
}
// 价格档位
type PriceLevel struct {
Prices map[decimal.Decimal]*PriceQueue // 价格 -> 订单队列
}
// 同价格订单队列(按时间排序)
type PriceQueue struct {
Price decimal.Decimal
Orders []*Order // 时间优先队列
Volume decimal.Decimal // 总量
}
// 订单
type Order struct {
ID string
UserID string
Symbol string
Side Side // BUY/SELL
Type OrderType // LIMIT/MARKET
Price decimal.Decimal
Quantity decimal.Decimal
Remaining decimal.Decimal // 剩余数量
Timestamp time.Time
Status OrderStatus // PENDING/PARTIAL/FILLED/CANCELED
}
为什么用红黑树/跳表?
| 数据结构 | 插入 | 删除 | 查找最值 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 数组 | O(n) | O(n) | O(1) | 简单 | 插入删除慢 |
| 哈希表 | O(1) | O(1) | O(n) | 快 | 无序,找最值慢 |
| 红黑树 | O(log n) | O(log n) | O(log n) | 平衡 | 实现复杂 |
| 跳表 | O(log n) | O(log n) | O(log n) | 简单 | 空间消耗大 |
实践选择:
- Go语言:自己实现跳表(标准库无红黑树)
- Java:TreeMap(红黑树)
- C++:std::map(红黑树)
⚖️ 三、撮合规则
3.1 价格优先原则
买单:价格高的优先成交 卖单:价格低的优先成交
买单簿:
100.0 (最优) ← 优先成交
99.9
99.8
卖单簿:
101.5 (最优) ← 优先成交
101.6
101.8
3.2 时间优先原则
同价格:先到的订单优先成交
买单簿(价格100.0):
订单A: 14:00:00 ← 优先成交
订单B: 14:00:05
订单C: 14:00:10
3.3 撮合条件
成交条件:买价 >= 卖价
买单: 价格100.0, 数量10
卖单: 价格99.5, 数量5
成交价格: 99.5 (卖单价格,因为卖单在前)
成交数量: 5
买单剩余: 5 (继续留在订单簿)
🔄 四、撮合流程
4.1 限价单撮合
场景1:买单 vs 卖单簿
订单簿状态:
卖单:
101.5 -> [订单1: 10张]
101.6 -> [订单2: 20张]
买1: 100.0
新买单:
价格: 102.0, 数量: 15张
撮合流程:
1. 检查买价(102.0) >= 卖1(101.5) 可成交
2. 取出卖单簿最优价格(101.5)的订单1(10张)
3. 成交10张 @ 101.5 (价格取卖单价格)
4. 买单剩余5张,继续撮合
5. 检查买价(102.0) >= 卖1(101.6) 可成交
6. 取出卖单簿次优价格(101.6)的订单2(20张)
7. 成交5张 @ 101.6
8. 订单2剩余15张,留在卖单簿
9. 买单完全成交,结束
结果:
- 成交1: 10张 @ 101.5
- 成交2: 5张 @ 101.6
- 卖单簿更新:
101.6 -> [订单2: 15张]
场景2:买单部分成交后挂单
订单簿状态:
卖单:
101.5 -> [订单1: 5张]
新买单:
价格: 102.0, 数量: 10张
撮合流程:
1. 成交5张 @ 101.5
2. 买单剩余5张
3. 检查买价(102.0) >= 卖1(无) 无法继续成交
4. 买单剩余5张挂到买单簿
100.0 -> [新买单: 5张]
结果:
- 成交: 5张 @ 101.5
- 挂单: 5张 @ 102.0
4.2 市价单撮合
市价单以最优价格立即成交,不考虑价格,直到成交完成或订单簿耗尽。
订单簿状态:
卖单:
101.5 -> [订单1: 10张]
101.6 -> [订单2: 20张]
101.8 -> [订单3: 15张]
新市价买单:
数量: 35张
撮合流程:
1. 取卖1(101.5), 成交10张 @ 101.5
2. 取卖1(101.6), 成交20张 @ 101.6
3. 取卖1(101.8), 成交5张 @ 101.8
4. 市价单完全成交
结果:
- 成交1: 10张 @ 101.5
- 成交2: 20张 @ 101.6
- 成交3: 5张 @ 101.8
- 平均成交价: (10×101.5 + 20×101.6 + 5×101.8) / 35 = 101.59
市价单风险:
- 如果订单簿深度不足,可能以很差的价格成交
- 因此实盘通常限制市价单数量或设置价格保护
五、撮合算法伪代码
5.1 限价买单撮合
def matchLimitBuyOrder(buyOrder):
"""
撮合限价买单
"""
while buyOrder.remaining > 0:
# 1. 获取卖单簿最优价格
bestAsk = askBook.getBestPrice()
if bestAsk is None or buyOrder.price < bestAsk.price:
# 无法成交,挂到买单簿
bidBook.addOrder(buyOrder)
break
# 2. 获取该价格的订单队列(时间优先)
askQueue = askBook.getQueue(bestAsk.price)
# 3. 逐个撮合
while buyOrder.remaining > 0 and len(askQueue) > 0:
sellOrder = askQueue[0] # 时间最早的卖单
# 计算成交数量
matchQty = min(buyOrder.remaining, sellOrder.remaining)
# 生成成交
trade = Trade(
price=sellOrder.price, # 价格取卖单价格
quantity=matchQty,
buyOrderID=buyOrder.id,
sellOrderID=sellOrder.id,
timestamp=now()
)
trades.append(trade)
# 更新订单剩余数量
buyOrder.remaining -= matchQty
sellOrder.remaining -= matchQty
# 如果卖单完全成交,从队列移除
if sellOrder.remaining == 0:
askQueue.pop(0)
sellOrder.status = FILLED
# 如果该价格的卖单耗尽,移除该价格档位
if len(askQueue) == 0:
askBook.removePrice(bestAsk.price)
# 更新买单状态
if buyOrder.remaining == 0:
buyOrder.status = FILLED
elif buyOrder.remaining < buyOrder.quantity:
buyOrder.status = PARTIAL
5.2 市价买单撮合
def matchMarketBuyOrder(buyOrder):
"""
撮合市价买单(以任意价格成交)
"""
while buyOrder.remaining > 0:
# 1. 获取卖单簿最优价格
bestAsk = askBook.getBestPrice()
if bestAsk is None:
# 订单簿耗尽,市价单无法完全成交
buyOrder.status = PARTIAL
break
# 2. 获取该价格的订单队列
askQueue = askBook.getQueue(bestAsk.price)
# 3. 逐个撮合(同限价单)
while buyOrder.remaining > 0 and len(askQueue) > 0:
sellOrder = askQueue[0]
matchQty = min(buyOrder.remaining, sellOrder.remaining)
trade = Trade(
price=sellOrder.price,
quantity=matchQty,
buyOrderID=buyOrder.id,
sellOrderID=sellOrder.id,
timestamp=now()
)
trades.append(trade)
buyOrder.remaining -= matchQty
sellOrder.remaining -= matchQty
if sellOrder.remaining == 0:
askQueue.pop(0)
sellOrder.status = FILLED
if len(askQueue) == 0:
askBook.removePrice(bestAsk.price)
# 市价单必须完全成交或部分成交,不挂单
if buyOrder.remaining == 0:
buyOrder.status = FILLED
六、撮合示例
6.1 完整撮合流程示例
初始状态:
订单簿为空
步骤1:用户A挂限价卖单
卖单1: 价格101.5, 数量10
订单簿:
卖单: 101.5 -> [卖单1: 10]
步骤2:用户B挂限价卖单
卖单2: 价格101.6, 数量20
订单簿:
卖单: 101.5 -> [卖单1: 10]
101.6 -> [卖单2: 20]
步骤3:用户C挂限价买单
买单1: 价格100.0, 数量30
检查: 100.0 < 101.5,无法成交,挂单
订单簿:
卖单: 101.5 -> [卖单1: 10]
101.6 -> [卖单2: 20]
买单: 100.0 -> [买单1: 30]
步骤4:用户D下限价买单
买单2: 价格102.0, 数量15
检查: 102.0 >= 101.5,可成交
成交1: 10 @ 101.5 (买单2 vs 卖单1)
买单2剩余5
检查: 102.0 >= 101.6,可成交
成交2: 5 @ 101.6 (买单2 vs 卖单2)
买单2完全成交
卖单2剩余15
订单簿:
卖单: 101.6 -> [卖单2: 15]
买单: 100.0 -> [买单1: 30]
步骤5:用户E下市价卖单
卖单3: 数量20(市价)
检查: 买1 = 100.0,取买单簿
成交3: 20 @ 100.0 (卖单3 vs 买单1)
买单1剩余10
卖单3完全成交
订单簿:
卖单: 101.6 -> [卖单2: 15]
买单: 100.0 -> [买单1: 10]
七、性能优化
7.1 内存优化
1. 对象池:复用Order、Trade对象,减少GC
2. 预分配:订单簿预分配容量
3. 批量处理:批量生成成交记录
7.2 并发优化
1. 单线程撮合:避免锁竞争
2. 无锁队列:订单输入使用无锁队列
3. LMAX Disruptor:环形缓冲区
7.3 算法优化
1. 跳表:O(log n)查找,简单高效
2. 价格缓存:缓存最优买卖价
3. 增量更新:只推送变化的深度数据
📖 八、总结
核心要点
- 订单簿:用红黑树/跳表维护价格优先队列
- 撮合规则:价格优先 > 时间优先
- 限价单:价格不满足则挂单
- 市价单:立即成交,不挂单
- 性能:单线程 + 无锁 + 高效数据结构
关键数据结构
OrderBook
│
├── Bids (跳表/红黑树)
│ ├── 价格100.0 -> [订单1, 订单2, ...]
│ ├── 价格99.9 -> [订单3, ...]
│ └── ...
│
└── Asks (跳表/红黑树)
├── 价格101.5 -> [订单4, 订单5, ...]
├── 价格101.6 -> [订单6, ...]
└── ...