在交易市场中,撮合交易算法是每一笔订单成交的核心机制。每当买卖双方提交订单时,系统会根据价格优先、时间优先的规则自动匹配成交。对于老股民来说,这些规则很熟悉,但从开发者角度来看,设计一个高效、可复用的撮合引擎仍然是一项挑战。本文从技术实现的角度出发,讲解如何用 Java 构建一个简化的撮合交易算法。

买卖盘的设计思路

撮合引擎的核心是 订单簿(OrderBook),它维护两条队列:买盘和卖盘。买盘按价格从高到低排序,确保报价最高的买单优先成交;卖盘按价格从低到高排序,确保报价最低的卖单优先成交。

在交易市场中,买1价格永远不会高于等于卖1价格,否则说明系统漏掉了应成交的订单。对于多个相同价格的订单,则按时间顺序成交——在我们的实现中,用全局唯一的 sequenceId 来保证排序和处理顺序,而不是直接用创建时间。

很多人会想到用 List<Order> 来存储订单,但在高频交易环境中,插入和删除订单的成本太高(O(N)),效率难以保证。更好的方案是用平衡树结构,比如 Java 的 TreeMap,可以让插入、删除和查找操作的时间复杂度保持在 O(logN)。

public record OrderKey(long sequenceId, BigDecimal price) { }

由于买盘和卖盘的排序规则不同,需要为 TreeMap 提供两个不同的比较器:

private static final Comparator<OrderKey> SORT_SELL = (o1, o2) -> {
    int cmp = o1.price().compareTo(o2.price()); // 价格低在前
    return cmp == 0 ? Long.compare(o1.sequenceId(), o2.sequenceId()) : cmp;
};

private static final Comparator<OrderKey> SORT_BUY = (o1, o2) -> {
    int cmp = o2.price().compareTo(o1.price()); // 价格高在前
    return cmp == 0 ? Long.compare(o1.sequenceId(), o2.sequenceId()) : cmp;
};

有了比较器后,OrderBook 的实现就非常直接:

public class OrderBook {
    public final Direction direction;
    public final TreeMap<OrderKey, OrderEntity> book;

    public OrderBook(Direction direction) {
        this.direction = direction;
        this.book = new TreeMap<>(direction == Direction.BUY ? SORT_BUY : SORT_SELL);
    }

    public OrderEntity getFirst() {
        return this.book.isEmpty() ? null : this.book.firstEntry().getValue();
    }

    public boolean remove(OrderEntity order) {
        return this.book.remove(new OrderKey(order.sequenceId, order.price)) != null;
    }

    public boolean add(OrderEntity order) {
        return this.book.put(new OrderKey(order.sequenceId, order.price), order) == null;
    }
}

小提示:Java 中比较 BigDecimal 的值一定要用 compareTo(),不要用 equals(),否则 1.2 和 1.20 会被判定不相等。

撮合引擎的核心逻辑

有了买卖盘,就可以实现撮合引擎。核心数据结构如下:

public class MatchEngine {
    public final OrderBook buyBook = new OrderBook(Direction.BUY);
    public final OrderBook sellBook = new OrderBook(Direction.SELL);
    public BigDecimal marketPrice = BigDecimal.ZERO;
    private long sequenceId;

    public MatchResult processOrder(long sequenceId, OrderEntity order) {
        switch (order.direction) {
            case BUY:
                return processOrder(order, this.sellBook, this.buyBook);
            case SELL:
                return processOrder(order, this.buyBook, this.sellBook);
            default:
                throw new IllegalArgumentException(“Invalid direction.”);
        }
    }
}

处理订单时,如果是买单,就尝试与卖盘匹配;如果是卖单,则尝试与买盘匹配。新订单称为 Taker,已经挂在盘上的订单称为 Maker。Taker 如果未完全成交,则转为 Maker 挂在订单簿上。

MatchResult processOrder(OrderEntity takerOrder, OrderBook makerBook, OrderBook anotherBook) {
    long ts = takerOrder.createdAt;
    MatchResult matchResult = new MatchResult(takerOrder);
    BigDecimal takerUnfilledQuantity = takerOrder.quantity;

    for (;;) {
        OrderEntity makerOrder = makerBook.getFirst();
        if (makerOrder == null) break;

        if ((takerOrder.direction == Direction.BUY && takerOrder.price.compareTo(makerOrder.price) < 0) ||
            (takerOrder.direction == Direction.SELL && takerOrder.price.compareTo(makerOrder.price) > 0)) {
            break;
        }

        this.marketPrice = makerOrder.price;
        BigDecimal matchedQuantity = takerUnfilledQuantity.min(makerOrder.unfilledQuantity);
        matchResult.add(makerOrder.price, matchedQuantity, makerOrder);

        takerUnfilledQuantity = takerUnfilledQuantity.subtract(matchedQuantity);
        BigDecimal makerUnfilledQuantity = makerOrder.unfilledQuantity.subtract(matchedQuantity);

        if (makerUnfilledQuantity.signum() == 0) {
            makerOrder.updateOrder(makerUnfilledQuantity, OrderStatus.FULLY_FILLED, ts);
            makerBook.remove(makerOrder);
        } else {
            makerOrder.updateOrder(makerUnfilledQuantity, OrderStatus.PARTIAL_FILLED, ts);
        }

        if (takerUnfilledQuantity.signum() == 0) {
            takerOrder.updateOrder(takerUnfilledQuantity, OrderStatus.FULLY_FILLED, ts);
            break;
        }
    }

    if (takerUnfilledQuantity.signum() > 0) {
        takerOrder.updateOrder(takerUnfilledQuantity,
            takerUnfilledQuantity.compareTo(takerOrder.quantity) == 0 ? OrderStatus.PENDING : OrderStatus.PARTIAL_FILLED,
            ts);
        anotherBook.add(takerOrder);
    }

    return matchResult;
}

MatchResult 会记录本次 Taker 订单及所有撮合记录,方便清算系统进行结算。

支持多交易对

一个引擎实例只能处理一个交易对。如果想在同一个系统中支持多个交易对,可以用一个引擎组来管理:

class MatchEngineGroup {
    Map<Long, MatchEngine> engines = new HashMap<>();

    public MatchResult processOrder(long sequenceId, OrderEntity order) {
        Long symbolId = order.symbolId;
        MatchEngine engine = engines.get(symbolId);
        if (engine == null) {
            engine = new MatchEngine();
            engines.put(symbolId, engine);
        }
        return engine.processOrder(sequenceId, order);
    }
}

每个订单加上 symbolId 属性,就能被路由到对应的交易对引擎实例,系统内部保持隔离。

实例演示

假设有如下订单输入撮合引擎:

方向价格数量
buy2082.341
sell2087.62
buy2087.81
buy2085.015
sell2088.023
sell2087.606
buy2081.117
buy2086.03
buy2088.331
sell2086.542
sell2086.555
buy2086.553

撮合完成后,买卖盘会自动更新,市场最新成交价也会随之改变,系统的内部状态完全可复现。每一笔成交都严格遵循价格优先、时间优先原则,整个撮合流程清晰高效。