限价订单簿与撮合引擎 · C++17 · 单线程 · 零依赖
一个为「语义正确、可验证、可复现」而写的撮合内核:限价 / 市价 / IOC / FOK / AON、改单、止损单、事件通知,
配套独立参考实现随机对拍、CSV 回放三层对账,以及有剖析依据的性能数据。
| 语义完整 | 限价、市价、IOC、FOK、AON、Modify、Stop / Stop-Limit、撤单、可选监听器 |
| 可验证 | 一份不共享任何代码的朴素参考实现,与引擎跑同一套用例并做数万事件的逐步对拍 |
| 可复现 | CSV 回放逐字节一致,三层对账(事件 / 订单 / 数量)任何不一致都以非零码退出 |
| 可解释 | VS2022 CPU 采样的热点报告、每一步优化的前后对比、结构体尺寸纳入验收 |
| 性能 | classic 负载 14.25 M events/sec(GCC 15 -O2,单线程),同流对照约为 liquibook 的 21 倍 |
| 零依赖 | 核心库只依赖 C++17 标准库;测试用 GoogleTest,基准与回放器各自独立 |
cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failureLinux / macOS 把
-G "MinGW Makefiles"去掉即可。基准必须用 Release 与-O2:
cmake -S . -B build-rel -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O2 -DNDEBUG"
cmake --build build-rel -j
build-rel/benchmarks/lob_bench --events 1000000 --warmup 100000 --seed 42 --repeat 20 \
--no-latency --reserve-trades --reserve-orders 100000#include "lob/matching_engine.hpp"
lob::MatchingEngine engine;
lob::Order sell;
sell.order_id = 1; // 仅在活跃订单内唯一,终结后可复用
sell.participant_id = 7; // 自成交按参与者判定
sell.side = lob::Side::Sell;
sell.price = 100'000; // int64 tick,必须 > 0
sell.quantity = 10;
engine.add_limit(sell); // 未交叉 → 挂入订单簿,分配 seq
lob::Order buy;
buy.order_id = 2;
buy.participant_id = 8;
buy.side = lob::Side::Buy;
buy.price = 100'050;
buy.quantity = 4;
const lob::AddResult result = engine.add_limit(buy);
// result.status == lob::OrderStatus::Filled
// result.filled_quantity == 4,成交价取 maker 价 100'000
const auto best_ask = engine.book().best_ask(); // std::optional<int64_t>
const auto trades = engine.trades(); // 逐笔 Trade改单、市价、止损与监听器:
engine.modify(1, 100'100, 12); // 数量按「新总量」解释;加量/改价会失去时间优先级
engine.add_market(market_order); // 剩余量自动作废
engine.add_stop(stop_order); // 触发后按市价执行
class Printer : public lob::OrderListener {
public:
void on_trade(const lob::Trade& t) override { /* t.price 是 maker 价 */ }
void on_level_changed(lob::Side, int64_t price, uint64_t total) override {}
};
Printer printer;
engine.set_listener(&printer); // 不设置时热路径只有一次空指针判断 ┌──────────────────────────────┐
订单事件 ───▶ │ MatchingEngine │
│ 校验 → 撮合 → 收尾 → 触发止损 │
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ OrderBook │
│ ├─ 买/卖 价位 std::map │
│ ├─ 档位内侵入式双向链表 (FIFO)│
│ ├─ 订单池 std::vector<Node> │
│ └─ order_id → 下标 开放寻址表 │
└──────────────────────────────┘
- 订单池 + 侵入式链表:撤单 O(1),无逐单
malloc/free;Node缓存所属档位指针,扣减档位总量不再查map。 - 开放寻址索引:自研
detail::IndexMap(线性探测 + 向后移位删除),替代std::unordered_map的每单分配。 - 价格与数量:
int64_ttick 与uint64_t数量,热路径无浮点。 - FIFO:只由引擎分配的
seq决定,timestamp不参与任何比较。
环境:Windows 11 / Intel i7-14650HX / GCC 15.2.0 -O2 / 100 万事件 / 预热 10 万 / seed=42 / 20 次冷启动重放。
| 场景 | 吞吐 |
|---|---|
| classic(限价 + 撤单) | 14.25 M events/sec |
| features(混入改单/市价/IOC/FOK/AON/止损) | 7.64 M events/sec |
| 逐事件计时模式(可出 P50/P99) | 11.0 M events/sec,P50 = 100 ns,P99 = 500 ns |
与 liquibook 的同流对照(同编译器、同负载、各自独立进程跑 3 次取中位数):
| 引擎 | 吞吐 | 簿子终态 |
|---|---|---|
| 本引擎 | 14.25 M events/sec | 87194 活跃 / 4392827 挂单量 |
| liquibook | 0.67 M events/sec | 87194 活跃 / 4392827 挂单量 |
两侧结果完全一致,说明对比跑的是同一份负载。注意 liquibook 官方数字是纯 insert 口径,不能与这里 35% 撤单的混合流直接比较。复现方式见 docs/benchmark.md §9。
lob::detail::SpscQueue<T> 是一个单生产者/单消费者有界环队列:容量向上取整到 2 的幂、构造后零分配、
无锁、失败只返回 false(不抛异常),元素可以是只能移动的类型。它位于引擎之外,不影响撮合的确定性。
三种喂入方式各跑 3 轮(100 万事件、预热 10 万、同负载):
| 方式 | 吞吐 | 说明 |
|---|---|---|
| 单线程直连 | 14.32 M events/sec | 参照组 |
| SPSC 队列 | 9.66 M events/sec | 比直连慢 33%,比加锁快 24% |
| mutex + deque | 7.79 M events/sec | 比直连慢 46% |
纯队列(不撮合)对比是 76.4 M 对 19.8 M msg/sec,约 3.9 倍。结论:队列解决的是解耦与削峰,
不是吞吐——每事件一次跨核缓存行传递的代价会直接叠加在撮合成本上(详见 docs/benchmark.md §10)。
只有一个线程喂入时,直接调引擎更快。
- 参考实现对拍——
tests/reference/reference_engine.hpp是一份全量扫描的朴素实现,禁止 include 引擎任何头文件;先独立通过同一套用例,再与引擎跑数万事件的随机序列,逐步比较盘口、每个订单的remaining/seq/状态、每笔成交与全部计数器。 - 三层对账——回放器校验事件层(每类事件计数)、订单层(
accepted_add == resting + pending + filled + cancelled + taker_cancelled)、数量层(逐单filled + resting + cancelled == 下单量,并用成交记录独立重算volume)。 - 逐字节可复现——同一 CSV 多次回放输出必须完全一致,
ctest里直接比对两次输出与examples/*.expected。 - 不变式——每个事件后断言
best_bid < best_ask、档位总量与链表一致、seq严格递增;Debug 构建自动执行。
137 个用例(133 个 GoogleTest + 4 个回放集成)
├─ test_order_book 基础订单簿与不变式
├─ test_matching 撮合语义(FIFO、maker 价、跨档、自成交、ID 复用…)
├─ test_order_types Modify / Market / IOC / FOK / AON
├─ test_stop_orders 止损、级联与待触发集合
├─ test_listener 事件通知与档位/BBO 增量
├─ test_spsc_queue 无锁队列(FIFO、环绕、满队列、并发 20 万条)
├─ test_reference 参考实现的同套边界用例
├─ test_differential 随机对拍(8 种形态 + 附加种子)
├─ test_index_map 开放寻址索引(含 4 万步随机对拍)
└─ examples/ mini / parse_errors / features / stops 四组回放
ctest --test-dir build --output-on-failure
build/tests/lob_tests --gtest_filter=Matching.*在 WSL2 的 Ubuntu 26.04 上(g++ 15.2.0)实跑过一遍,137 个用例全部通过,没有 AddressSanitizer / LeakSanitizer / UndefinedBehaviorSanitizer 报告(含 20 万条并发队列用例):
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DLOB_SANITIZE=ON
cmake --build build-asan -j
ASAN_OPTIONS=detect_leaks=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
ctest --test-dir build-asan --output-on-failure记录日期 2026-11-04。Windows/MinGW 上的 sanitizer 结果不作结论。
CSV 固定 9 列、无表头:
event_seq,event_type,order_id,side,price,quantity,timestamp,participant_id,stop_price
事件类型:ADD_LIMIT、ADD_IOC、ADD_FOK、ADD_LIMIT_AON、ADD_MARKET、ADD_STOP、ADD_STOP_LIMIT、MODIFY、CANCEL。
build/tools/replay/lob_replay --in examples/features.csv输出先逐笔打印成交,再给出全部计数器与四个 reconcile_* 结论;任一不一致退出码为 1。
include/lob/ 公共头文件(Order / OrderBook / MatchingEngine / OrderListener)
include/lob/detail/ 内部数据结构(开放寻址索引、SPSC 队列)
src/ 实现
tests/ 单元、边界、不变式、随机对拍
tests/reference/ 朴素参考实现(不参与发布产物)
benchmarks/ 基准、liquibook 对照与队列基准
tools/replay/ CSV 回放器
examples/ 示例 CSV 与期望输出
docs/ 需求、设计、基准、剖析报告
| 文档 | 内容 |
|---|---|
docs/需求文档.md |
唯一需求来源:撮合规则、回放格式、验收标准 |
docs/design.md |
所有权模型、容器选型、终结路径、复杂度与演进 |
docs/benchmark.md |
测量条件、历代对比、liquibook 对照 |
docs/profiling.md |
VS2022 CPU 采样热点与逐项优化分析 |
AGENTS.md |
工程约束与易错点清单(R1–R17) |
- 单品种、单线程;不含行情接入、订单网关、风控、持仓或持久化。
- 回放器的 CSV 解析耗时独立于撮合基准,两者不可直接相加比较。
nodes_扩容会使外部持有的Order*失效,接口约定为「仅在下一次修改前有效」。- 延迟桶粒度为 100 ns 起,尾部延迟未做隔离;跨会话绝对值会有约 8% 漂移,比较请在会话内交替测量。
- M0 工程骨架与 CI 无关的本地验收
- M1 核心撮合 + 回放对账
- M2 订单池与侵入式链表
- M3 无锁输入队列(可选扩展,结论:解耦有用、不提升单引擎吞吐)
- M4 订单类型:市价 / IOC / FOK / AON / Modify
- M5 止损单与级联触发
- M6 事件通知与档位增量
- 与 liquibook 的逐接口功能对照(AON+IOC 组合、待触发单改单)