web3现货交易撮合逻辑怎么做?

8 阅读5分钟

现货交易撮合逻辑怎么做?一篇讲透核心流程

原创声明:本文为原创首发内容。
适合人群:后端工程师、交易系统开发者、微服务架构实践者
关键词:现货交易、撮合引擎、订单簿、价格优先、时间优先、成交回报
阅读收益:看完可独立设计一版可落地的现货撮合主流程


前言

“撮合”本质上就是一句话:

把买单和卖单,按照既定规则配对成交。

但工程实现里,真正难的是:

  • 高并发下的顺序一致性
  • 成交与资产变更的一致性
  • 部分成交、撤单、重复请求等边界处理

这篇文章从第一性原理拆解:规则 -> 数据结构 -> 流程 -> 一致性 -> 工程落地


1. 先定交易规则(不先定规则,代码一定乱)

现货交易最常见规则是:

  1. 价格优先
  2. 时间优先(同价位按先来后到)
  3. 买卖方向约束

解释:

  • 买单想“买便宜”,优先与最低卖价撮合
  • 卖单想“卖贵点”,优先与最高买价撮合

1.1 可成交条件

  • 买单价格 >= 最优卖价,可成交
  • 卖单价格 <= 最优买价,可成交

1.2 常见订单类型

  • 限价单:指定价格上限/下限
  • 市价单:按当前盘口最优价格立即成交(需风控保护)

2. 核心数据模型(最小闭环)

建议至少包含这 5 类对象:

  1. Order(订单)
  2. OrderBook(订单簿)
  3. Trade(成交)
  4. Position/Balance(资产与冻结)
  5. Event(事件流:下单、成交、撤单)

2.1 订单关键字段

  • orderId
  • userId
  • symbol(如 BTC/USDT)
  • side(BUY/SELL)
  • type(LIMIT/MARKET)
  • price
  • quantity
  • filledQuantity
  • status(NEW/PARTIAL/FILLED/CANCELED)
  • createTime

2.2 订单簿结构建议

  • 买盘:按价格从高到低
  • 卖盘:按价格从低到高
  • 每个价位下是 FIFO 队列(时间优先)

Java 常见实现:

  • TreeMap<Price, Deque<Order>>(买盘可用逆序比较器)

3. 撮合主流程(下单到成交)

3.1 限价买单示例

假设用户下限价买单:买 2 BTC @ 100

当前卖盘:

  • 卖1:99,数量 0.8
  • 卖2:100,数量 0.7
  • 卖3:101,数量 1.5

撮合过程:

  1. 先吃卖1(99)0.8,剩余 1.2
  2. 再吃卖2(100)0.7,剩余 0.5
  3. 卖3价格 101 > 买价 100,不可成交
  4. 剩余 0.5 挂入买盘(价格 100)

结果:

  • 两笔成交回报
  • 买单状态 PARTIAL
  • 剩余数量入簿等待后续撮合

4. 撮合伪代码(可直接映射 Java 实现)

public MatchResult submitOrder(Order taker) {
    // 1. 基础校验:交易对、最小下单量、价格精度、风控阈值
    validateOrder(taker);

    // 2. 资产预检查与冻结(买冻结计价币,卖冻结基础币)
    reserveBalance(taker);

    // 3. 进入撮合循环
    while (taker.hasRemaining()) {
        Order maker = orderBook.peekBestOpposite(taker.getSide());
        if (maker == null) {
            break;
        }

        // 4. 价格不可成交则退出
        if (!priceCrossed(taker, maker)) {
            break;
        }

        // 5. 计算本次成交量
        BigDecimal fillQty = taker.remaining().min(maker.remaining());

        // 6. 成交价:一般取挂单方(maker)价格
        BigDecimal fillPrice = maker.getPrice();

        // 7. 生成成交记录并更新双方订单
        Trade trade = createTrade(taker, maker, fillQty, fillPrice);
        applyFill(taker, maker, trade);

        // 8. 若 maker 完成,则从订单簿移除
        if (maker.isFilled()) {
            orderBook.removeTopOpposite(taker.getSide());
        }

        // 9. 推送成交事件(异步)
        publishTradeEvent(trade);
    }

    // 10. 若 taker 仍有剩余且是限价单,挂单入簿
    if (taker.hasRemaining() && taker.isLimit()) {
        orderBook.add(taker);
    }

    // 11. 结算与解冻差额
    settleAndRelease(taker);

    // 12. 推送订单状态更新
    publishOrderEvent(taker);

    return buildResult(taker);
}

5. 资金与一致性(交易系统最容易出事故的地方)

5.1 资产处理建议

  • 下单前先冻结
  • 每次成交即时扣减冻结并增加对应资产
  • 订单结束后解冻未成交部分

5.2 成交与资产更新要么都成功,要么都失败

至少保证以下原子性:

  1. 成交记录写入
  2. 订单状态更新
  3. 账户余额更新

常见做法:

  • 撮合核心内存化串行执行
  • 通过事件日志/消息队列做异步落库
  • 采用幂等键防重复消费

6. 高并发架构建议(实战版)

6.1 核心原则:一个交易对一个串行撮合线程

为什么?

  • 同一交易对需要强顺序
  • 串行可天然避免锁竞争与乱序成交

可扩展方式:

  • symbol 分片(BTC/USDT 一条队列,ETH/USDT 一条队列)
  • 每个分片单线程事件循环
  • 多交易对并行扩容

6.2 典型链路

  1. 网关接单
  2. 风控预校验
  3. 投递到撮合分片队列
  4. 撮合引擎串行撮合
  5. 生成成交事件
  6. 账户、订单、K线等消费者异步处理

7. 必须处理的边界场景

  1. 部分成交:订单状态应为 PARTIAL
  2. 撤单并发:撤单与成交竞争时,需按序判定最终状态
  3. 重复下单:客户端请求幂等(clientOrderId
  4. 市价单滑点:设置保护价或最大成交金额
  5. 精度问题:数量和价格统一精度与舍入策略
  6. 宕机恢复:从快照 + 增量日志恢复订单簿

8. 手把手拆分模块(微服务视角)

可以按下面拆:

  1. trade-api:接单、查单、撤单接口
  2. risk-service:风控校验(余额、限额、黑白名单)
  3. match-engine:撮合核心(内存簿 + 串行事件循环)
  4. account-service:冻结、解冻、资产变更
  5. market-service:行情、深度、K线
  6. audit-service:审计与对账

9. 测试清单(上线前至少覆盖)

  1. 正常撮合(全成交、部分成交)
  2. 并发下单顺序一致性
  3. 撤单竞态
  4. 宕机恢复一致性
  5. 重复消息幂等
  6. 极端行情(瞬时大量订单)

10. 总结

现货撮合并不神秘,核心就三步:

  1. 定规则(价格优先、时间优先)
  2. 建结构(双边订单簿 + FIFO)
  3. 保一致(成交、订单、资产原子更新)

真正决定系统稳定性的,不是“算法多复杂”,而是:

  • 是否控制了顺序
  • 是否处理了边界
  • 是否保证了资金一致性

后续

如果想要了解更多现货交易相关内容,可以关注后续文章内容更新