用 Rust 写了一个极简撮合引擎核心:Lighting Match Engine Core

[开源] 用 Rust 写了一个极简撮合引擎核心:Lighting Match Engine Core

大家好,我最近整理并开源了一个用 Rust 写的撮合引擎核心项目:

GitHub: https://github.com/philipgreat/lighting-match-engine-core

项目名字叫 lighting-match-engine-core。它不是一个完整交易所系统,而是一个尽量窄的 matching core:只关注订单簿、连续竞价、集合竞价、基础交易阶段和固定长度二进制消息编码。OMS、风控、账户、行情网关、交易时段排程、产品管理这些外围系统都不放在 core 里。

我把它发出来,一方面是想和 Rust 社区交流一下低延迟撮合系统的设计取舍,另一方面也希望有人帮忙 review:哪些抽象合理,哪些地方还有明显的性能或正确性问题。

目前已经有的能力

连续竞价订单簿目前有两种实现:

  • dense:适合价格区间和 tick 比较明确的产品,用数组价格档位换取更直接的索引访问。

  • sparse:用 BTreeMap 管价格档位,适合价格范围更稀疏或不想预分配大数组的场景。

订单匹配按价格优先、时间优先处理。买单吃卖一档,卖单吃买一档;未完全成交的限价单会进入簿内。

集合竞价单独做成了 CallAuctionPool,目前支持:

  • 开盘集合竞价

  • 可选收盘集合竞价

  • 预留波动中断集合竞价类型

  • 最大成交量优先

  • 剩余不平衡量最小

  • 平衡时的价格 tie-break

  • 只接受限价单,市价单会被拒绝

交易阶段通过 MarketPhaseSessionRunner 驱动,核心阶段包括:

  • PreOpen

  • AuctionOrderEntry

  • AuctionFrozen

  • AuctionMatching

  • ContinuousTrading

  • TradingHalt

  • Closed

协议层目前实现的是固定 64 字节 packet 编码,包含:

  • 下单

  • 撤单

  • 成交广播

  • 状态广播

  • 错误回报

这部分现在主要是 core 边界和 wire format 的雏形,还没有实现真实 socket I/O。

为什么 core 要保持窄

我目前的判断是:撮合核心越窄越好。

比如下面这些能力我暂时都放在外围,而不是塞进 core:

  • 午间休市、夜盘、节假日

  • 市场特有交易制度

  • 冰山单、止损单、TWAP 等策略型订单

  • 风控、保证金、持仓、强平

  • 产品上市、退市和 session 编排

core 只保留“没有它就无法完成基础撮合”的东西:

  • 订单簿

  • phase routing

  • tick / dense range 校验

  • 集合竞价池

  • 基础成交结果

  • 基础二进制消息格式

外围系统可以把复杂订单拆成普通订单,把复杂交易日历转换成 phase transition,把风控事件转换成普通平仓单。这样 core 的行为更容易测试,也更容易替换外围策略。

一个简化的运行流程

当前 demo 大概是这样跑的:

  1. 读取 CLI 参数,生成 AppConfig

  2. 根据参数创建 densesparse 订单簿

  3. 初始化 EngineState

  4. 通过 SessionRunner 执行开盘集合竞价,进入连续竞价

  5. 加载测试订单簿

  6. 循环提交买卖单,统计内部撮合耗时

  7. 打印最后一次撮合结果和延迟分位数

可以直接运行:

cargo run --features match-timing --release -- --prodid 7 --name AAPL --test-order-book-size 50k

也可以切换订单簿实现:

cargo run --features match-timing --release -- \
  --prodid 7 \
  --name AAPL \
  --test-order-book-size 50k \
  --order-book sparse

集合竞价 benchmark:

cargo run --release -- --prodid 7 --name AAPL --bench-call-auction-only

关于性能数字

README 里有一个本地 demo 截图:在 Apple M1 Max MacBook Pro 上,50K bids + 50K asks 的测试簿里,内部 core matching latency 曾测到过个位数 ns 级别。

这里我想明确说明一下:这个数字只代表当前 demo/benchmark 路径下的内部匹配耗时,不包含网络、序列化、风控、持久化、行情广播、真实多线程调度等成本,也不等价于生产环境端到端延迟。

我更关心的是:

  • 订单簿数据结构是否还能优化

  • dense/sparse 两种实现的边界是否清晰

  • benchmark 是否应该补更真实的分布和回放数据

  • Rust 下有哪些更好的 cache/locality/branch 优化方式

如果大家有相关经验,很欢迎拍砖。

当前测试状态

当前仓库里有单元测试覆盖:

  • 配置解析和参数校验

  • dense 订单簿价格区间和 tick 校验

  • basic buy/sell matching

  • 集合竞价价格计算和撮合

  • 集合竞价撤单

  • phase routing

  • demo session runner

  • 64 字节协议编码中的错误回报和成交批量编码

本地执行:

cargo test

当前结果是 34 个测试通过。

不过项目仍然很早期,还有不少 warning,包括一些未使用代码,以及 Cargo.toml 里 target-specific rustflags 的写法目前会被 Cargo 当成 unused manifest key。这个后面会整理。

我想请教社区的几个问题

  1. 对于撮合核心,大家会倾向保持现在这种“窄 core + 外围编排”的边界吗?还是某些市场制度应该下沉到 core?

  2. dense 订单簿用数组档位换速度,sparse 用树结构换通用性,这个拆分是否合理?还有没有更推荐的数据结构?

  3. 集合竞价的价格计算目前基于候选价格扫描和累计量计算,有没有更好的实现方式或边界 case 需要特别注意?

  4. 固定 64 字节 packet 这种 wire format,在 Rust 里大家会更推荐手写 codec,还是用 zerocopy/bytemuck 这类方式?

  5. 对这种低延迟 core,大家通常怎么设计 benchmark,才能避免自嗨式的 ns 数字?

后续计划

短期想补的东西:

  • 整理 warning 和 manifest 配置

  • 更完整的撤单路径

  • 更严谨的 benchmark 输入

  • 补充行情广播/成交输出边界

  • 把协议 codec 和 core matching 的边界再理清楚

  • 增加更多集合竞价边界用例

中长期可能会做:

  • 外围 session scheduler 示例

  • 简单 TCP/UDP gateway 示例

  • 市场数据回放 benchmark

  • 多产品多实例部署示例

  • 更完整的文档和架构图

项目地址:

https://github.com/philipgreat/lighting-match-engine-core

欢迎 review、提 issue、提 PR,也欢迎直接在帖子里指出设计问题。这个项目还处在早期阶段,我更希望先把 core 的边界和正确性打牢,再逐步补外围组件。

聊天