交易所撮合系统测试指南
前言
撮合引擎是交易所的核心系统,负责匹配买卖订单,完成交易。撮合系统的正确性和性能直接影响交易所的稳定性和用户体验。本文档将详细讲解撮合系统的原理、测试方法和测试场景。
一、撮合系统基础
1.1 什么是撮合?
撮合定义:
撮合(Matching)是指交易所根据价格优先、时间优先的原则,将买单和卖单进行匹配,完成交易的过程。
简单理解:
- 就像菜市场的买卖双方讨价还价
- 买方出价,卖方要价
- 当价格匹配时,交易达成
- 交易所就是中间人,负责匹配
撮合的核心:
- 价格优先:价格更优的订单优先成交
- 时间优先:相同价格下,先提交的订单优先成交
- 数量匹配:订单数量要能匹配
1.2 撮合系统的组成
订单簿(Order Book):
- 买单(Bid):买方愿意买入的价格和数量
- 卖单(Ask):卖方愿意卖出的价格和数量
- 按价格排序,形成订单簿
撮合引擎(Matching Engine):
- 接收订单
- 匹配买卖订单
- 生成成交记录
- 更新订单簿
交易记录(Trade):
- 成交价格
- 成交数量
- 买卖双方
- 成交时间
1.3 撮合流程
1. 用户提交订单
↓
2. 订单进入撮合引擎
↓
3. 检查订单有效性
↓
4. 在订单簿中查找匹配订单
↓
5. 价格匹配?
├─ 是 → 执行撮合
└─ 否 → 加入订单簿等待
↓
6. 生成成交记录
↓
7. 更新订单簿
↓
8. 通知用户
二、撮合规则详解
2.1 价格优先原则
规则:
- 买单:价格高的优先成交
- 卖单:价格低的优先成交
示例:
买单价格优先:
订单簿(买单):
价格 数量
100.5 100 ← 最高价,优先成交
100.3 200
100.0 300
卖单价格优先:
订单簿(卖单):
价格 数量
100.0 100 ← 最低价,优先成交
100.3 200
100.5 300
2.2 时间优先原则
规则:
- 相同价格下,先提交的订单优先成交
示例:
价格 100.0 的买单:
时间 数量
10:00:01 100 ← 最早,优先成交
10:00:02 200
10:00:03 300
2.3 撮合价格确定
规则:
- 使用被动方(订单簿中的订单)的价格作为成交价
示例:
场景1:新买单匹配订单簿中的卖单
- 新买单:100.5,数量100
- 订单簿卖单:100.0,数量100
- 成交价:100.0(使用卖单价格)
- 成交数量:100
场景2:新卖单匹配订单簿中的买单
- 新卖单:100.0,数量100
- 订单簿买单:100.5,数量100
- 成交价:100.5(使用买单价格)
- 成交数量:100
2.4 订单类型
限价单(Limit Order):
- 指定价格和数量
- 只有价格匹配时才成交
- 不匹配时加入订单簿
市价单(Market Order):
- 不指定价格,按当前最优价格成交
- 立即成交,不加入订单簿
- 可能全部成交或部分成交
示例:
限价买单:100.0,数量100
- 如果订单簿有≤100.0的卖单,立即成交
- 如果没有,加入订单簿等待
市价买单:数量100
- 立即与订单簿中最低价的卖单成交
- 按卖单价格成交
三、撮合测试场景
3.1 基础撮合场景
场景1:完全匹配(Full Match)
描述:新订单与订单簿中的订单完全匹配
测试步骤:
- 订单簿已有卖单:价格100.0,数量100
- 提交买单:价格100.0,数量100
- 验证撮合结果
预期结果:
- ✅ 订单完全成交
- ✅ 成交价格:100.0
- ✅ 成交数量:100
- ✅ 订单簿中卖单被移除
- ✅ 生成1条成交记录
测试数据:
{
"orderBook": {
"asks": [
{"price": 100.0, "quantity": 100, "orderId": "ask-001"}
],
"bids": []
},
"newOrder": {
"side": "buy",
"price": 100.0,
"quantity": 100,
"orderId": "bid-001"
},
"expected": {
"trades": [
{
"price": 100.0,
"quantity": 100,
"buyOrderId": "bid-001",
"sellOrderId": "ask-001"
}
],
"orderBook": {
"asks": [],
"bids": []
}
}
}
场景2:部分匹配(Partial Match)
描述:新订单部分成交,剩余部分加入订单簿
测试步骤:
- 订单簿已有卖单:价格100.0,数量50
- 提交买单:价格100.0,数量100
- 验证撮合结果
预期结果:
- ✅ 部分成交:50
- ✅ 剩余50加入订单簿(买单)
- ✅ 成交价格:100.0
- ✅ 订单簿中卖单被移除
- ✅ 生成1条成交记录
测试数据:
{
"orderBook": {
"asks": [
{"price": 100.0, "quantity": 50, "orderId": "ask-001"}
],
"bids": []
},
"newOrder": {
"side": "buy",
"price": 100.0,
"quantity": 100,
"orderId": "bid-001"
},
"expected": {
"trades": [
{
"price": 100.0,
"quantity": 50,
"buyOrderId": "bid-001",
"sellOrderId": "ask-001"
}
],
"orderBook": {
"asks": [],
"bids": [
{"price": 100.0, "quantity": 50, "orderId": "bid-001"}
]
}
}
}
场景3:多订单匹配(Multiple Match)
描述:新订单与多个订单簿订单匹配
测试步骤:
- 订单簿已有卖单:
- 价格100.0,数量50
- 价格100.1,数量30
- 价格100.2,数量20
- 提交市价买单:数量100
- 验证撮合结果
预期结果:
- ✅ 与3个卖单依次成交
- ✅ 成交价格:100.0(50)、100.1(30)、100.2(20)
- ✅ 总成交数量:100
- ✅ 生成3条成交记录
- ✅ 订单簿中3个卖单被移除
测试数据:
{
"orderBook": {
"asks": [
{"price": 100.0, "quantity": 50, "orderId": "ask-001"},
{"price": 100.1, "quantity": 30, "orderId": "ask-002"},
{"price": 100.2, "quantity": 20, "orderId": "ask-003"}
],
"bids": []
},
"newOrder": {
"side": "buy",
"type": "market",
"quantity": 100,
"orderId": "bid-001"
},
"expected": {
"trades": [
{"price": 100.0, "quantity": 50, "buyOrderId": "bid-001", "sellOrderId": "ask-001"},
{"price": 100.1, "quantity": 30, "buyOrderId": "bid-001", "sellOrderId": "ask-002"},
{"price": 100.2, "quantity": 20, "buyOrderId": "bid-001", "sellOrderId": "ask-003"}
],
"orderBook": {
"asks": [],
"bids": []
}
}
}
3.2 价格优先测试场景
场景4:买单价格优先
描述:多个买单,价格高的优先成交
测试步骤:
- 订单簿已有买单:
- 价格100.5,数量100(时间10:00:01)
- 价格100.3,数量200(时间10:00:00)
- 提交卖单:价格100.0,数量50
- 验证撮合结果
预期结果:
- ✅ 与价格100.5的买单成交(价格优先)
- ✅ 成交价格:100.5
- ✅ 成交数量:50
- ✅ 价格100.5的买单剩余50
测试数据:
{
"orderBook": {
"asks": [],
"bids": [
{"price": 100.5, "quantity": 100, "orderId": "bid-001", "timestamp": "10:00:01"},
{"price": 100.3, "quantity": 200, "orderId": "bid-002", "timestamp": "10:00:00"}
]
},
"newOrder": {
"side": "sell",
"price": 100.0,
"quantity": 50,
"orderId": "ask-001"
},
"expected": {
"trades": [
{
"price": 100.5,
"quantity": 50,
"buyOrderId": "bid-001",
"sellOrderId": "ask-001"
}
],
"orderBook": {
"asks": [],
"bids": [
{"price": 100.5, "quantity": 50, "orderId": "bid-001"},
{"price": 100.3, "quantity": 200, "orderId": "bid-002"}
]
}
}
}
场景5:卖单价格优先
描述:多个卖单,价格低的优先成交
测试步骤:
- 订单簿已有卖单:
- 价格100.0,数量100
- 价格100.3,数量200
- 提交买单:价格100.5,数量50
- 验证撮合结果
预期结果:
- ✅ 与价格100.0的卖单成交(价格优先)
- ✅ 成交价格:100.0
- ✅ 成交数量:50
3.3 时间优先测试场景
场景6:相同价格时间优先
描述:相同价格下,先提交的订单优先成交
测试步骤:
- 订单簿已有买单(价格都是100.0):
- 时间10:00:00,数量100
- 时间10:00:01,数量200
- 时间10:00:02,数量300
- 提交卖单:价格100.0,数量50
- 验证撮合结果
预期结果:
- ✅ 与时间10:00:00的买单成交(时间优先)
- ✅ 成交价格:100.0
- ✅ 成交数量:50
3.4 边界条件测试场景
场景7:最小数量撮合
描述:测试最小可交易数量
测试步骤:
- 订单簿已有卖单:价格100.0,数量0.0001(最小单位)
- 提交买单:价格100.0,数量0.0001
- 验证撮合结果
预期结果:
- ✅ 最小数量可以正常撮合
- ✅ 精度计算正确
场景8:最大数量撮合
描述:测试大额订单撮合
测试步骤:
- 订单簿已有卖单:价格100.0,数量1000000
- 提交买单:价格100.0,数量1000000
- 验证撮合结果
预期结果:
- ✅ 大额订单可以正常撮合
- ✅ 数量计算准确
- ✅ 性能正常
场景9:价格边界撮合
描述:测试价格边界情况
测试步骤:
- 订单簿已有卖单:价格0.0001(最小价格)
- 提交买单:价格0.0001
- 验证撮合结果
预期结果:
- ✅ 最小价格可以正常撮合
- ✅ 价格精度正确
3.5 异常场景测试
场景10:余额不足
描述:用户余额不足以完成交易
测试步骤:
- 用户余额:50 USDT
- 提交买单:价格100.0,数量100(需要10000 USDT)
- 验证结果
预期结果:
- ✅ 订单被拒绝
- ✅ 返回余额不足错误
- ✅ 订单簿不变
场景11:价格不匹配
描述:限价单价格不匹配,加入订单簿
测试步骤:
- 订单簿已有卖单:价格100.0,数量100
- 提交买单:价格99.0,数量100(价格低于卖单)
- 验证结果
预期结果:
- ✅ 订单不成交
- ✅ 订单加入订单簿(买单)
- ✅ 订单簿状态正确
场景12:订单取消
描述:订单在撮合前被取消
测试步骤:
- 提交买单:价格100.0,数量100
- 订单加入订单簿
- 立即取消订单
- 验证结果
预期结果:
- ✅ 订单从订单簿移除
- ✅ 订单状态变为已取消
- ✅ 不生成成交记录
3.6 并发测试场景
场景13:并发订单撮合
描述:多个订单同时提交,测试并发撮合
测试步骤:
- 订单簿已有卖单:价格100.0,数量1000
- 同时提交10个买单,每个数量100
- 验证撮合结果
预期结果:
- ✅ 所有订单正确撮合
- ✅ 成交顺序正确(时间优先)
- ✅ 订单簿状态正确
- ✅ 无数据竞争
场景14:高并发压力测试
描述:大量订单同时提交,测试系统性能
测试步骤:
- 准备1000个订单
- 同时提交所有订单
- 监控撮合性能
预期结果:
- ✅ 所有订单正确处理
- ✅ 撮合延迟在可接受范围
- ✅ 系统稳定运行
- ✅ 无数据丢失
3.7 市价单测试场景
场景15:市价买单撮合
描述:市价买单与订单簿卖单撮合
测试步骤:
- 订单簿已有卖单:
- 价格100.0,数量50
- 价格100.1,数量30
- 提交市价买单:数量80
- 验证撮合结果
预期结果:
- ✅ 与价格100.0的卖单成交50
- ✅ 与价格100.1的卖单成交30
- ✅ 成交价格:100.0(50)、100.1(30)
- ✅ 总成交数量:80
场景16:市价单深度不足
描述:市价单数量超过订单簿深度
测试步骤:
- 订单簿已有卖单:价格100.0,数量50
- 提交市价买单:数量100
- 验证撮合结果
预期结果:
- ✅ 部分成交:50
- ✅ 剩余50无法成交
- ✅ 订单被拒绝或部分成交
- ✅ 返回深度不足提示
3.8 订单簿更新测试场景
场景17:订单簿正确更新
描述:撮合后订单簿状态正确
测试步骤:
- 初始订单簿状态记录
- 执行撮合
- 验证订单簿更新
预期结果:
- ✅ 已成交订单从订单簿移除
- ✅ 部分成交订单数量更新
- ✅ 新订单正确加入订单簿
- ✅ 价格排序正确
- ✅ 时间排序正确
四、撮合测试方法
4.1 单元测试
测试对象:撮合引擎核心算法
测试框架:JUnit、pytest等
测试示例(Java):
public class MatchingEngineTest {
private MatchingEngine engine;
private OrderBook orderBook;
@BeforeEach
void setUp() {
orderBook = new OrderBook();
engine = new MatchingEngine(orderBook);
}
@Test
void testFullMatch() {
// 准备数据
Order sellOrder = new Order("ask-001", "sell", 100.0, 100);
orderBook.addOrder(sellOrder);
Order buyOrder = new Order("bid-001", "buy", 100.0, 100);
// 执行撮合
MatchResult result = engine.match(buyOrder);
// 验证结果
assertEquals(1, result.getTrades().size());
assertEquals(100.0, result.getTrades().get(0).getPrice());
assertEquals(100, result.getTrades().get(0).getQuantity());
assertTrue(orderBook.getAsks().isEmpty());
}
@Test
void testPartialMatch() {
Order sellOrder = new Order("ask-001", "sell", 100.0, 50);
orderBook.addOrder(sellOrder);
Order buyOrder = new Order("bid-001", "buy", 100.0, 100);
MatchResult result = engine.match(buyOrder);
// 验证部分成交
assertEquals(1, result.getTrades().size());
assertEquals(50, result.getTrades().get(0).getQuantity());
// 验证剩余订单
assertEquals(1, orderBook.getBids().size());
assertEquals(50, orderBook.getBids().get(0).getQuantity());
}
@Test
void testPricePriority() {
// 添加多个买单
orderBook.addOrder(new Order("bid-001", "buy", 100.5, 100));
orderBook.addOrder(new Order("bid-002", "buy", 100.3, 200));
Order sellOrder = new Order("ask-001", "sell", 100.0, 50);
MatchResult result = engine.match(sellOrder);
// 验证价格高的优先成交
assertEquals("bid-001", result.getTrades().get(0).getBuyOrderId());
assertEquals(100.5, result.getTrades().get(0).getPrice());
}
}
Python示例:
import pytest
from matching_engine import MatchingEngine, OrderBook, Order
class TestMatchingEngine:
def setup_method(self):
self.order_book = OrderBook()
self.engine = MatchingEngine(self.order_book)
def test_full_match(self):
# 准备数据
sell_order = Order("ask-001", "sell", 100.0, 100)
self.order_book.add_order(sell_order)
buy_order = Order("bid-001", "buy", 100.0, 100)
# 执行撮合
result = self.engine.match(buy_order)
# 验证结果
assert len(result.trades) == 1
assert result.trades[0].price == 100.0
assert result.trades[0].quantity == 100
assert len(self.order_book.asks) == 0
4.2 集成测试
测试对象:撮合系统与订单系统、账户系统的集成
测试示例:
@SpringBootTest
public class MatchingIntegrationTest {
@Autowired
private MatchingService matchingService;
@Autowired
private AccountService accountService;
@Test
void testMatchingWithAccount() {
// 准备账户
accountService.deposit("user1", 10000, "USDT");
accountService.deposit("user2", 100, "BTC");
// 提交订单
Order buyOrder = createBuyOrder("user1", 100.0, 100);
Order sellOrder = createSellOrder("user2", 100.0, 100);
// 执行撮合
matchingService.submitOrder(buyOrder);
matchingService.submitOrder(sellOrder);
// 验证账户更新
assertEquals(0, accountService.getBalance("user1", "USDT"));
assertEquals(10000, accountService.getBalance("user1", "BTC"));
assertEquals(10000, accountService.getBalance("user2", "USDT"));
assertEquals(0, accountService.getBalance("user2", "BTC"));
}
}
4.3 性能测试
测试指标:
- 撮合延迟(Latency)
- 吞吐量(Throughput)
- 并发处理能力
测试工具:JMeter、Gatling、Locust
测试示例(使用JMeter):
<!-- 配置大量订单提交 -->
<ThreadGroup>
<num_threads>100</num_threads>
<ramp_time>10</ramp_time>
<loop_count>100</loop_count>
</ThreadGroup>
<HTTPSampler>
<path>/api/orders</path>
<method>POST</method>
<body>{"side":"buy","price":100.0,"quantity":100}</body>
</HTTPSampler>
性能基准:
- 单笔撮合延迟:< 1ms
- 吞吐量:> 100,000 TPS
- 并发订单:> 10,000
4.4 压力测试
测试场景:
- 大量订单同时提交
- 订单簿深度很大
- 高频小额订单
- 大额订单冲击
测试示例:
@Test
void testHighConcurrency() {
int threadCount = 100;
int ordersPerThread = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
for (int j = 0; j < ordersPerThread; j++) {
Order order = createRandomOrder();
matchingService.submitOrder(order);
}
} finally {
latch.countDown();
}
});
}
latch.await();
// 验证所有订单正确处理
}
4.5 正确性测试
测试方法:对比测试
步骤:
- 准备相同的测试数据
- 使用标准撮合算法(参考实现)处理
- 使用待测试系统处理
- 对比结果
测试数据生成:
public class TestDataGenerator {
public List<Order> generateOrders(int count) {
List<Order> orders = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < count; i++) {
String side = random.nextBoolean() ? "buy" : "sell";
double price = 100.0 + random.nextDouble() * 10;
int quantity = random.nextInt(1000) + 1;
orders.add(new Order("order-" + i, side, price, quantity));
}
return orders;
}
}
五、撮合测试检查清单
5.1 功能正确性
基础撮合:
- 完全匹配正确
- 部分匹配正确
- 多订单匹配正确
- 价格优先正确
- 时间优先正确
订单类型:
- 限价单撮合正确
- 市价单撮合正确
- 订单加入订单簿正确
边界条件:
- 最小数量处理正确
- 最大数量处理正确
- 价格边界处理正确
- 精度计算正确
5.2 数据一致性
订单簿一致性:
- 撮合后订单簿状态正确
- 价格排序正确
- 时间排序正确
- 数量计算正确
账户一致性:
- 余额扣减正确
- 资产增加正确
- 冻结资金处理正确
- 账户状态一致
交易记录:
- 成交记录完整
- 成交价格正确
- 成交数量正确
- 时间戳正确
5.3 异常处理
订单异常:
- 余额不足处理
- 价格不匹配处理
- 数量无效处理
- 订单取消处理
系统异常:
- 系统故障恢复
- 数据丢失恢复
- 并发冲突处理
- 死锁避免
5.4 性能指标
延迟:
- 单笔撮合延迟 < 1ms
- 批量撮合延迟可接受
- 订单簿查询延迟低
吞吐量:
- 支持高TPS
- 并发处理能力强
- 系统资源使用合理
5.5 安全性
数据安全:
- 订单数据不丢失
- 交易记录不可篡改
- 账户数据安全
业务安全:
- 防止重复撮合
- 防止订单篡改
- 防止恶意刷单
六、撮合测试工具
6.1 测试数据生成工具
public class OrderGenerator {
public Order generateLimitOrder(String side, double basePrice, int quantityRange) {
Random random = new Random();
double price = basePrice + (random.nextDouble() - 0.5) * 10;
int quantity = random.nextInt(quantityRange) + 1;
return new Order(
UUID.randomUUID().toString(),
side,
price,
quantity,
"limit"
);
}
public List<Order> generateOrderBook(int askCount, int bidCount, double basePrice) {
List<Order> orders = new ArrayList<>();
// 生成卖单
for (int i = 0; i < askCount; i++) {
double price = basePrice + i * 0.1;
orders.add(generateLimitOrder("sell", price, 100));
}
// 生成买单
for (int i = 0; i < bidCount; i++) {
double price = basePrice - i * 0.1;
orders.add(generateLimitOrder("buy", price, 100));
}
return orders;
}
}
6.2 撮合结果验证工具
public class MatchResultValidator {
public ValidationResult validate(MatchResult result, OrderBook expectedOrderBook) {
ValidationResult validation = new ValidationResult();
// 验证成交记录
if (result.getTrades().isEmpty() && !expectedOrderBook.isEmpty()) {
validation.addError("Expected trades but got none");
}
// 验证订单簿
if (!orderBookEquals(result.getOrderBook(), expectedOrderBook)) {
validation.addError("Order book mismatch");
}
// 验证价格
for (Trade trade : result.getTrades()) {
if (trade.getPrice() <= 0) {
validation.addError("Invalid trade price: " + trade.getPrice());
}
}
return validation;
}
private boolean orderBookEquals(OrderBook actual, OrderBook expected) {
// 比较订单簿状态
return actual.getAsks().size() == expected.getAsks().size() &&
actual.getBids().size() == expected.getBids().size();
}
}
6.3 性能监控工具
@Component
public class MatchingPerformanceMonitor {
private final MeterRegistry meterRegistry;
public void recordMatchLatency(long latency) {
meterRegistry.timer("matching.latency").record(latency, TimeUnit.MILLISECONDS);
}
public void recordMatchThroughput(int count) {
meterRegistry.counter("matching.throughput").increment(count);
}
public void recordOrderBookSize(int size) {
meterRegistry.gauge("orderbook.size", size);
}
}
七、撮合测试最佳实践
7.1 测试策略
-
分层测试
- 单元测试:测试核心算法
- 集成测试:测试系统集成
- 端到端测试:测试完整流程
-
测试覆盖
- 正常场景:100%覆盖
- 异常场景:关键异常覆盖
- 边界条件:所有边界覆盖
-
测试数据
- 使用真实数据模式
- 覆盖各种价格和数量
- 包含极端情况
7.2 测试环境
-
独立测试环境
- 与生产环境隔离
- 可重复执行
- 数据可清理
-
测试数据管理
- 测试前准备数据
- 测试后清理数据
- 数据可复用
7.3 持续测试
-
自动化测试
- CI/CD集成
- 每次代码变更自动测试
- 快速反馈
-
回归测试
- 关键场景回归
- 防止功能退化
- 保证稳定性
八、总结
8.1 关键要点
-
撮合规则:
- 价格优先
- 时间优先
- 数量匹配
-
测试重点:
- 功能正确性
- 数据一致性
- 性能指标
- 异常处理
-
测试方法:
- 单元测试
- 集成测试
- 性能测试
- 压力测试
8.2 测试场景总结
基础场景(必须测试):
- 完全匹配
- 部分匹配
- 多订单匹配
- 价格优先
- 时间优先
边界场景(重要测试):
- 最小/最大数量
- 价格边界
- 精度计算
异常场景(关键测试):
- 余额不足
- 价格不匹配
- 订单取消
- 系统异常
性能场景(定期测试):
- 并发撮合
- 高吞吐量
- 低延迟
8.3 持续改进
- 不断完善测试用例
- 优化测试流程
- 提升测试效率
- 提高测试质量
文档版本:v1.0
最后更新:2026年
适用对象:交易所开发人员、测试工程师、QA团队
附录:测试用例模板
A. 撮合测试用例模板
**测试用例ID**:TC-MATCH-001
**测试用例名称**:完全匹配撮合测试
**优先级**:P0
**前置条件**:
- 订单簿已有卖单:价格100.0,数量100
- 用户账户有足够余额
**测试步骤**:
1. 提交买单:价格100.0,数量100
2. 等待撮合完成
3. 验证撮合结果
**预期结果**:
- 订单完全成交
- 成交价格:100.0
- 成交数量:100
- 订单簿中卖单被移除
- 生成1条成交记录
**实际结果**:[填写]
**测试状态**:[通过/失败]
**备注**:[填写]
B. 性能测试报告模板
**测试时间**:2026-01-01
**测试环境**:测试环境
**测试工具**:JMeter
**测试结果**:
- 撮合延迟:平均0.5ms,P99 1.2ms
- 吞吐量:120,000 TPS
- 并发处理:10,000并发订单
- CPU使用率:60%
- 内存使用率:70%
**结论**:性能指标满足要求
评论区