Kaspar:C++20 高频交易系统与订单簿模拟器
Kaspar – C++20 HFT trading system and order book simulator
Kaspar 是一个基于 C++20 actor 框架的高频交易系统兼订单簿模拟器,同一套策略代码可在 PCAP 回放、实时模拟盘和 iLink 3 实盘执行间通过配置切换。其内置 CME MDP3 行情与 iLink 3 报单会话已通过 CME 自动认证,fast_send 同步调用相比直接调用仅增加约 7.5 ns 开销。
生产级交易系统与仓位感知订单簿模拟器
与交易场所无关 • 包含 CME MDP3 和 iLink 3 • PCAP 回放 • POV 执行 • C++20
面向 HFT 的 Actor 框架论文(arXiv) • Shadow-POV 论文(arXiv) • 一页简介
Kaspar-hft 让一个策略在同一条代码路径上经历三个阶段:从数据包捕获进行模拟、基于实时市场数据进行模拟交易,以及实盘执行。它既是一个生产级交易系统,也是一个队列位置精确的订单簿模拟器,构建在 C++20 actor 框架之上。它不绑定于单一市场:行情处理器和订单录入会话均可插拔,并预置了 CME 的 MDP3 行情数据和 iLink 3 订单录入,且已通过 CME 认证。股票或其他交易场所需要为这两部分提供各自的处理器;而订单簿、模拟器、actor 框架和策略代码则完全相同。
Kaspar-hft 亮点
- 低延迟 actor 框架:无数据竞争,你的代码中无锁,且框架开销几乎为零。
- 回测 == 生产:同一个策略、执行算法和订单簿在 PCAP 回放、模拟交易和实盘 iLink 3 中运行;切换只需改配置,因此回测所走的正是将来实盘交易的同一代码路径。
- 与交易场所无关,包含 CME:行情处理器和订单录入会话均可插拔。随附的 MDP3 行情处理器和 iLink 3 订单录入会话已通过 CME 自动认证,并实现了完整的会话生命周期。
- Shadow 执行算法:一种无模型的成交量百分比执行算法,支持被动和主动,基于一年的 ES 数据进行了测量。
- 用 C++ 或 Rust 编写策略:以 C++ 进程内 actor 形式编写策略(延迟最低),或通过进程内 C++/Rust FFI 互操作以 Rust 编写。
- 两篇论文可深入阅读,更多正在撰写中:C++ actor 框架设计 arXiv:2609.21173 和执行算法结果 arXiv:2609.18019。
- fast_send 的形式化语义:
fast_send作为第二个原语加入 actor 模型,并给出操作语义,以及它保留了什么的证明(隔离性、相对于回复的确定性)和它代价的证明(除非 actor 之间循环等待,否则活性成立)—— 论文草稿(PDF)。 - 异步和同步(
fast_send)投递可互换:无论哪种方式,处理器都是相同的代码,因此 actor 到线程的映射可以在部署时决定,并针对尾延迟进行调优(arXiv:2609.32848)。
Kaspar 是共享同一代码库的两样东西。
它是一个生产级交易系统:行情数据输入,逐笔重建完整订单簿,在其上运行执行算法,再通过订单录入会话输出。行情处理器和订单录入会话是交易场所相关的部分;随附的 CME 组合(MDP3 组播输入,iLink 3 输出——SBE 编码、HMAC 认证、序列管理和主/备故障切换)已通过认证。其他交易场所可接入自己的处理器和会话。
它也是一个仓位感知订单簿模拟器:从数据包捕获文件重建订单簿,将你的订单放入价格-时间队列中,只有当市场实际成交穿过它们时才会成交。成交通过精确的队列核算推断得出。
同一份策略代码、同一套执行算法和同一本订单簿,在 PCAP 回放、实时模拟交易以及对接真实交易所中运行;在它们之间切换只是改一下配置。回测所走的代码路径,就是将来实际交易时走的路径。它构建在一个自定义的 C++ actor 框架之上,消息传递达到纳秒级。
为什么用 actor? 每个 actor 拥有自己的私有状态,只通过消息通信,因此 actor 之间不共享可变状态:没有内存级的数据竞争,没有撕裂读或撕裂写,你自己的代码里也没有锁。你每次只需针对一致的状态推理一条消息。死锁仍然可能发生——同步的 fast_send 调用形成环就可能造成死锁——但远比之前难以触发。actor 代码对 AI 编码代理来说也异常容易编写:模型简单,框架文档就是为它们写的,而且一个 actor 天然自带独立的单元测试——发一条消息进去,对回复做断言。对 actor 模型常见的反对意见是消息传递开销;Kaspar 用 fast_send 来回应,它在调用者线程上内联执行接收方的处理函数,并把回复作为值返回(相比直接调用约 7.5 ns 的开销)。
以 Kasprowy Wierch 命名——“西塔特拉山脉一条长山脊上的山峰,波兰主要的冬季滑雪区之一。”
作者: Vincent Mayeski(发表时署名 Vincent Maciejewski)—— mayeski@gmail.com
生产级会话处理——通过 CME 认证
Kaspar 的行情数据和订单录入栈是完整的会话实现,两者都通过了 CME 自动认证。它们实现了完整的协议生命周期及其边界情况——商业 SDK 靠售卖来覆盖的恢复、重连和故障转移逻辑——所以你无需自己手写这些。
构建
快速开始,使用 ./build.sh:
./build.sh schema # generate the CME SBE codecs (pinned versions) ./build.sh # libraries (== make all) ./build.sh -C kaspr/src # the kaspr binary ./build.sh debug # debug build ./build.sh -C actors/cpp # build just one component
运行模式
| 模式 | 数据源 | 执行 | 订单簿 | 使用场景 |
|---|---|---|---|---|
| PCAP 回放 | .pcap 文件 |
SOM(模拟) | OB(MBP) | 回测、策略开发 |
| 模拟交易 | 实时 CME 组播 | SOM(模拟) | OB(MBP) | 用真实数据进行前向测试 |
| 实盘交易 | 实时 CME 组播 | iLink 3 对接 CME | TachBook(MBO L3) | 生产执行 |
Actor 框架
actor 框架为整个系统提供统一的并发模型。编码代理会阅读文档,并为你编写 actor 组件。你只需填写自己的消息处理函数。
- 消息传递——每个 actor 一个邮箱(默认
BQueue;见下文) - 组——一个
Group在单线程上以单个消息队列运行多个 actor。可实现确定性模拟。 - 零拷贝快速路径——
fast_send()在调用者线程中执行处理函数以处理同步查询——无队列、无线程切换,消息在栈上传递。 - C++/Rust 互操作——策略可以用 C++ 或 Rust 编写。
该框架背后的设计写在 Deep Dives 下列出的文章中。
class MyStrategy : public Actor { public: MyStrategy(actor_ptr ob, actor_ptr som) : Actor("strategy") { MESSAGE_HANDLER(frame::ob::msg::EndOfBurst, on_book_update); MESSAGE_HANDLER(frame::som::msg::Fill, on_fill); this->ob = ob; this->som = som; } private: void on_book_update(const frame::ob::msg::EndOfBurst* eob) { // React to order book changes auto* order = new frame::som::msg::Order( "ESM6", en::BuySell::BUY, 1, eob->best_bid, en::x::CMEMDFUT); som->send(order, this); } void on_fill(const frame::som::msg::Fill* fill) { // Handle execution } };
Rust 移植版(actors/rust)中的同一个策略 actor——处理函数就是普通方法,由 handle_messages! 宏连接起来;消息 id 是编译期常量:
struct MyStrategy { ob: ActorRef, som: ActorRef, } impl MyStrategy { fn on_book_update(&mut self, eob: &EndOfBurst, ctx: &mut ActorContext) { // React to order book changes self.som.send( Box::new(Order::new("ESM6", Side::Buy, 1, eob.best_bid, Venue::CmeMdFut)), ctx.self_ref(), ); } fn on_fill(&mut self, _fill: &Fill, _ctx: &mut ActorContext) { // Handle execution } } handle_messages!(MyStrategy, EndOfBurst => on_book_update, Fill => on_fill, );
为你的 Actor 选择合适的队列
每个 actor 都有一个邮箱:一个多生产者/单消费者(MPSC)队列,其他线程向其中推送消息,actor 自己的线程从中取出。
为你的使用场景选择最合适的队列:
- BQueue — 围绕环形缓冲区的互斥锁 + 条件变量。简单、FIFO、空闲时休眠。这是默认选项(无需调用
set_mailbox)。 - BQueueBatched — 相同,但消费者在一次加锁下排空整个邮箱,而不是每条消息加锁一次。
- ShardedBQueue — 邮箱拆分为 N 条独立通道,每条有自己的锁;生产者跨通道轮询以避免争用同一把锁。它不保证跨通道的 FIFO,因此仅用于处理程序与顺序无关的 actor(聚合器 / 订单簿),绝不可用于假定 Start-before-Data 或序列号顺序的场景。
- LockFreeMPSC — 一个有界无锁环;生产者通过单次原子操作认领一个槽位。有空间时无需挂起;若环满,生产者短暂自旋后阻塞(因此请按峰值积压量设置环大小)。
- LockFreeMPSCSpin — 相同的环,但消费者从不休眠:actor 的线程忙轮询其邮箱。线程间交接延迟最低,代价是每个 actor 占用一整个核心;仅在隔离核心上才值得。
关于选择的更多内容:并非所有队列都适用于低延迟系统。
监控
运行中的 kaspr 进程会暴露一个 ZMQ 请求/应答控制台(即 mq0 服务器),用于实时监控和手动干预——检查订单簿和持仓、手动下单/撤单,以及暂停/恢复订单撮合器,全程无需重启。
连接
import zmq ctx = zmq.Context() sock = ctx.socket(zmq.REQ) sock.setsockopt(zmq.RCVTIMEO, 10000) sock.setsockopt(zmq.SNDTIMEO, 10000) sock.setsockopt(zmq.LINGER, 0) sock.connect("tcp://localhost:7777") sock.send_string("bbbo") print(sock.recv_string()) # prints an ASCII table
配置
kaspr {
general {
universe config/universe.csv
mqport 7777
tachbook false # true + USE_TACHBOOK=1 for live
}
channels {
chan_310 true # ES futures
chan_318 true # NQ futures
}
}文档
| 文档 | 描述 |
|---|---|
| STRATEGY_SIMULATOR_GUIDE.md | 三种运行模式的完整指南 |
| SHADOW_ALGORITHM.md | 影子执行算法规范 |
| ACTORS_INVENTORY.md | 系统中的每一个 actor |
| FILE_STRUCTURE.md | 完整的文件和目录清单 |
| CLAUDE_AGENT_GUIDE.md | Actor 框架技术参考 |
| actors/rust/README.md | Rust actor 框架移植版——概览与快速入门 |
| actors/rust/DEVELOPER_GUIDE.md | 在 Rust 移植版中编写 actor |
| tech_reports/fast_send.pdf · arXiv:2609.21173 | 技术报告:fast_send 同步消息投递 |
| tech_reports/shadow_pov.pdf · arXiv:2609.18019 | 技术报告:Shadow-POV 被动执行 |
性能特征
- Actor 异步发送:快速入队(互斥锁 + 条件变量,热路径上无分配)
- Actor 同步发送:在栈上、在调用者线程上,绕过队列(它从不中断正在运行的处理程序)
- 内存:针对同尺寸消息使用池分配器
- 分组:每个 actor 组一个线程
实测:actor 消息往返延迟
actor 机制比裸函数调用多花多少开销? 在同一块 AMD EPYC 9374F 上干净地计时(围绕紧凑循环的一对时钟读取,栈输入上相同的琐碎工作):直接的非内联调用耗时 1.6 ns,而 fast_send 分发耗时 9.1 ns,因此 actor 机制每跳增加约 7.5 ns。带回复的完整 fast_send 往返约 30 ns(见上图)。详情:perf README。
市场数据延迟(实时 CME MDP3)
在 2026-10-02 于实盘 CME 期货(ES、NQ 和 ZN)上测得的 socket 到订单簿延迟:两次 5 分钟运行,每次丢弃前 60 秒,Onload,单路行情(关闭 feed B),线程未绑定。socket 读取线程自行解码每个数据包并更新订单簿,没有线程交接。按每条消息测量,从 socket 读取(t0)到订单簿发布(t1);NIC 和 socket 缓冲区中的时间不计入。所有数值单位为 µs。
| stream | messages | p1 | p10 | p50 | p90 | p99 | p99.9 |
|---|---|---|---|---|---|---|---|
| ES 订单簿 | 401,462 | 0.5 | 0.7 | 1.1 | 2.7 | 9.9 | 24.1 |
| NQ 订单簿 | 581,010 | 0.4 | 0.5 | 0.8 | 1.9 | 4.7 | 8.8 |
| ZN 订单簿 | 141,972 | 0.4 | 0.6 | 1.0 | 2.5 | 20.6 | 69.3 |
| ES 成交 | 45,733 | 0.5 | 0.7 | 2.5 | 8.8 | 36.2 | 70.1 |
| NQ 成交 | 21,093 | 0.4 | 0.6 | 0.9 | 3.3 | 17.8 | 31.5 |
| ZN 成交 | 13,282 | 0.4 | 0.7 | 6.6 | 41.5 | 90.5 | 124.0 |
影子执行算法
大多数执行算法要么跨越价差(成本高),要么持续报价(噪声大、逆向选择)。Kaspar 走第三条路:影子执行——一种按成交量百分比的算法,通过跟随其他参与者下的订单来参与自然市场流。
更多细节——Model-Free Passive Execution via Order-Level Shadowing——见 arXiv:arXiv:2609.18019。
添加订单簿模型
Kaspar 与模型无关:任何订单簿模型(订单流失衡、队列或 Hawkes 模型、深度网络)都可以作为订单簿与策略之间的又一个 actor 接入。同一个 actor 随后在回放、模拟盘和实盘交易中不加改动地运行。
- 定义消息。从
actors::MessageT<Prediction>派生一个Prediction消息(id 自动分配)。携带标的、订单簿更新的市场时间和预测值。 - 编写模型 actor。从
actors::Actor派生,并向MESSAGE_HANDLER注册两个处理函数:Start,在其中向订单簿发送Subscribe(Subscribe::HI);以及EndOfBurst,订单簿在每批更新后发送它。 - 计算并发布。在每个
EndOfBurst上,读取最多 16 个价位,计算预测,并向每个策略 actorsend一个新的Prediction。 - 在策略中使用它。向策略 actor 添加一个
Prediction处理函数,并在其下单决策中使用最新值,例如作为是否跟随某笔新增订单的门控。 - 接入它。在回放模拟器中,将该 actor 添加到模拟器的组中,位于行情数据读取器之前;整个回放保持在同一个队列和同一个线程上,因此保持确定性。在交易二进制文件中,将其添加到策略 actor 旁边。
要评估一个模型,请在同一回放中针对无模型影子基准运行它:相同的日期、相同的目标数量、相同的延迟。
关于模型本身(每个模型如何构建、预测什么、如何在队列精确回放中测试它,以及如何在加速硬件上部署它),请参阅 A Survey of Limit-Order-Book Models, Simulators and Hardware Acceleration(PDF)。
深度解析
关于 Kaspar 背后设计的深度解析(作者 Substack——vincentmayeski.substack.com):
- Low-Latency Actor Systems in C++ and Rust——用两种语言构建同一个 actor 框架:本仓库的 C++ 核心及其 Rust 移植版(
actors/rust),以及哪些可以沿用、哪些因借用检查器而改变。 - Actors in C++ and Rust: The Benchmarks, and the Bridge Between Them——两个移植版的正面对比基准测试(
fast_send、send、内存分配),以及进程内 C++/Rust 互操作桥,附有数据。 - Lock-Free Isn't Free: Cache Pollution, Busy Cores, and Why Kaspar Blocks——为什么在高负载下无锁可能是更慢的选择(自旋消费者、缓存一致性、超额订阅),以及为什么
BQueue会阻塞、fast_send反而最小化线程跳转。 - 如果机器要写代码,那就让它写 Rust —— 为什么 Rust 是 AI 生成代码的最佳语言(编译型,而且是在前期捕获 bug 方面最严格的主流编译器),以及 Kaspar 为什么加入 C++/Rust 互操作。
- 面向低延迟软件的 Actor 模型 —— 一个为单 CPU 机器发明的并发模型,结果证明它正是多核场景下的正确选择。
- Kaspar C++ Actor 系统中的高性能邮箱 —— 环形缓冲区很好,直到它溢出(
BQueue设计)。 - 为 Kaspar Actor 系统定制的内存分配器带来 10 倍提升 —— 当你在编译期就知道大小时,分配器所做的几乎所有事情都变得不必要(对象池)。
- 消息批处理如何让 Actor 模型吞吐量提升一倍以上 —— 在一把锁下清空整个邮箱,再加上消息池,带来 2.46 倍吞吐量提升(以及为什么批处理发送方会适得其反)。代码位于
sharded-mailbox分支(PR #20),该分支已关闭且未合并;批量清空可作为BQueueBatched邮箱使用。 - 中位数会骗人,尾部会致命:Kaspar 为何分片邮箱锁 —— 每个 actor 独立的邮箱将锁竞争分片,从而拉平尾部延迟抖动(p99.9 处约 180 倍)。分片邮箱可作为
ShardedBQueue使用。 - 无锁的幻觉:为什么 CAS 风暴会在竞争下摧毁 Actor 队列 —— 无锁邮箱何时胜出,何时尾部表现比互斥锁更差;队列选择矩阵。无锁邮箱可作为
LockFreeMPSC使用。 - 影子 POV 执行:在市场将要交易的地方交易 —— 一种跟随被动流的成交量百分比算法。
许可证
MIT 许可证。版权所有 (c) 2026 Vincent Mayeski / M2 Tech (16425640 Canada Inc.)。参见 LICENSE。
来源:Hacker News · github.com