用 Java PriorityQueue 手写核心“撮合引擎” (从零打造交易所 Day 01)
前言
很多 Java 开发者提到“量化交易”或“交易所开发”,第一反应往往是:“那是 Python 的事儿吧?”作为一个 Java 老兵,今天我要告诉你一个行业的真相:Python 是用来“开车”的,而 Java 和 C++ 才是用来“造车”的。
如果你想做策略回测、画K线图,Python 确实是王者;但如果你想构建一个毫秒级响应、高并发、强一致性的交易所核心——撮合引擎 (Matching Engine),Java 才是那个能扛事的“工业级”选择。今天 我们就用 Java 最基础的数据结构 PriorityQueue,带你通过几十行代码,看透华尔街和币圈交易所最核心的秘密。
一、 为什么用 Java 讲这个?(鄙视链的顶端)
在教学开始前,先解决大家的疑惑:“为什么不用 Python?”
1.速度与激情
撮合引擎追求的是微秒级 (Microseconds) 的延迟。Python 的解释器机制和 GIL 锁注定它无法处理这种高频计算。头部交易所的核心引擎,清一色是 C++ 或 Java (LMAX 架构)。
2.严谨的逻辑
Python 的字典 (Dict) 满天飞,容易掩盖底层逻辑。而 Java 的强类型 (Type Safety) 和显式比较器 (Comparator),能强制我们理解“对象”和“排序规则”——这正是撮合逻辑的精髓。
3.生态位不同
- Python (用户视角):我是 Taker,我调用 API 去交易。
- Java (上帝视角):我是 Maker,我制定规则,我维护秩序。
二、 核心概念映射:Java 程序员的降维打击
交易所里那些听起来高大上的术语,在 Java 开发者眼里,无非就是 JDK 里的标准库。
1. 什么是“订单簿” (Order Book)?
想象一下,所有想买和想卖的人都在排队。
-
买方:希望买得越便宜越好,但为了成交,谁出的价格高,谁就排前面(价高者得)。
-
卖方:希望卖得越贵越好,但为了成交,谁要的价格低,谁就排前面(价低者先)。
在 Java 里,这完美的对应了 两个优先队列 (PriorityQueue):
| 交易所概念 | Java 数据结构 | 排序逻辑 (Comparator) |
|---|---|---|
| 买单队列 (Bids) | 大顶堆 (Max-Heap) | 价格从高到低 desc,时间先进先出 |
| 卖单队列 (Asks) | 小顶堆 (Min-Heap) | 价格从低到高 asc,时间先进先出 |

2. 什么是“撮合” (Matching)?
本质上就是一个 while 循环! 每来一个新订单,就去看看能不能和对手方 (Counter Party) 队列的堆顶元素 (peek) 成交。能成交就扣减数量,不能成交就自己入队 (offer)。
三、 代码实战:手撸核心引擎
Step 1: 定义订单 (The POJO)
首先,我们需要一个类来描述“订单”。注意 timestamp,它是解决“价格相同时谁先成交”的关键:
public class Order {
private long id;
private long timestamp; // 时间戳,用于时间优先原则
private double price; // 价格
private double amount; // 数量
private String side; // "BUY" 或 "SELL"
public Order(long id, double price, double amount, String side) {
this.id = id;
this.timestamp = System.nanoTime(); // 模拟纳秒级时间戳
this.price = price;
this.amount = amount;
this.side = side;
}
// 省略 Getter/Setter/ToString
}
Step 2: 构建订单簿 (The Order Book)
用 Comparator 定义市场规则:价格优先 > 时间优先:
import java.util.PriorityQueue;
import java.util.Comparator;
public class OrderBook {
// 卖单队列 (Min-Heap):价格越低越靠前,价格相同时间早的靠前
private PriorityQueue<Order> sellQueue = new PriorityQueue<>(
Comparator.comparingDouble(Order::getPrice)
.thenComparingLong(Order::getTimestamp)
);
// 买单队列 (Max-Heap):价格越高越靠前 (注意 reversed),价格相同时间早的靠前
private PriorityQueue<Order> buyQueue = new PriorityQueue<>(
Comparator.comparingDouble(Order::getPrice).reversed()
.thenComparingLong(Order::getTimestamp)
);
// ...
}

Step 3: 撮合引擎逻辑 (The Engine)
假设现在来了一个买单,核心逻辑如下:
public void processBuyOrder(Order newBuyOrder) {
System.out.println("收到买单: 价格 " + newBuyOrder.getPrice() + ", 数量 " + newBuyOrder.getAmount());
// 开启撮合循环:
// 1. 卖单池子里得有货 (!isEmpty)
// 2. 我的出价 >= 卖一价 (peek),买卖双方心里价位达成一致
while (!sellQueue.isEmpty() && newBuyOrder.getPrice() >= sellQueue.peek().getPrice()) {
Order bestSell = sellQueue.peek(); // 取出卖一(对手方最优价格)
// 计算成交量:你要买10个,他只卖5个,那就成交5个 (取最小值)
double tradeVol = Math.min(newBuyOrder.getAmount(), bestSell.getAmount());
// --- 真正的成交发生在这里 ---
System.out.println(">>> 撮合成功! 成交价: " + bestSell.getPrice() + ", 数量: " + tradeVol);
// 扣减双方数量
newBuyOrder.setAmount(newBuyOrder.getAmount() - tradeVol);
bestSell.setAmount(bestSell.getAmount() - tradeVol);
// 如果卖一被吃光了,把它踢出队列 (poll)
if (bestSell.getAmount() <= 0) {
sellQueue.poll();
}
// 如果我的买单全满足了,任务结束
if (newBuyOrder.getAmount() <= 0) {
return;
}
}
// 循环结束,如果我的买单还有剩 (说明市面上没人卖这么便宜)
// 我就得乖乖去排队,变成“挂单 (Maker)”
if (newBuyOrder.getAmount() > 0) {
System.out.println("剩余 " + newBuyOrder.getAmount() + " 个进入买单队列等待...");
buyQueue.offer(newBuyOrder);
}
}
四、 场景模拟 (Debug 模式)
为了彻底理解,我们跑一个 Test Case:
================================================================================
🛒 订单簿快照 (Order Book Snapshot)
================================================================================
【状态 A:交易前 (Before Match)】
-------------------------------------------------------
[卖单队列 ASKS] (Min-Heap 小顶堆)
| 105.00 | 10 qty |
👉 | 102.00 | 5 qty | <-- 卖一 (Best Ask)
+----------+----------+
⚡️ 撮合区 (Spread) ⚡️
+----------+----------+
👉 | 99.00 | 2 qty | <-- 买一 (Best Bid)
[买单队列 BIDS] (Max-Heap 大顶堆)
-------------------------------------------------------
🚀 新订单进场: BUY (买入), 价格 $103.00, 数量 6
⬇️ 开始撮合 (Matching...) ⬇️
1. 对比价格: 买($103) >= 卖一($102) -> ✅ 成交!
2. 扣减数量: 卖单5个全被吃掉, 买单还剩 1 个
3. 继续对比: 买($103) < 卖一($105) -> ❌ 停止
⬇️ 撮合结束 (Finished) ⬇️
【状态 B:交易后 (After Match)】
-------------------------------------------------------
[卖单队列 ASKS]
| 105.00 | 10 qty | <-- 新卖一 (原卖二上位)
+----------+----------+
(102.00 已成交移除)
⚡️ 撮合区 (Spread) ⚡️
+----------+----------+
🆕 | 103.00 | 1 qty | <-- 新买一 (刚才没吃完的)
| 99.00 | 2 qty | <-- 退居买二
[买单队列 BIDS]
-------------------------------------------------------
1.初始状态:
-
卖单队列:
[102元(5个), 105元(10个)](想卖的人最低要102) -
买单队列:
[99元(2个)]
2.场景:我来了一个“暴力买单”,出价 103 元,要买 6 个。
(1)Round 1:
检查:我出 103 >= 卖一 102? -> YES。
动作:吃掉卖一。成交价 102 (按对手价成交),成交数量 5 个。
结果:卖一 (102元) 归零出队。我的单子还剩 1 个。
(2)Round 2:
检查新的卖一 (105元)。我出 103 >= 卖一 105? -> NO。
动作:买不起了,停止循环。
(3)收尾:
-
我剩下的 1 个订单,带着 103 元的价格,进入买单队列。
-
新买一诞生了,现在的市场买一价变成了 103 元。
五、 总结与展望
通过 Java 的 PriorityQueue,我们用不到 50 行代码就复现了交易所的核心逻辑。对 Java 开发者来说,这只是冰山一角。真实的生产环境(如 LMAX 架构)还会面临更多挑战,这正是 Java 大显身手的地方:
GC 问题:频繁创建 Order 对象会导致 GC 卡顿,怎么解决?
(答案:对象池 Object Pooling)
并发锁:多线程同时操作队列怎么保证安全?
(答案:无锁并发框架 Disruptor)
Day 01 的课程就到这里。 别再觉得 Java 笨重了,在这个追求极致确定性的金融世界里,它是当之无愧的基石。
如果你对“用 Java 手写高频交易系统”感兴趣,请在评论区告诉我,我们 Day 02 继续深入!
更多推荐




所有评论(0)