跳到正文
Hacker News· vincent212·· 9 天前AI 评分34

Kaspar:C++20 高频交易系统与订单簿模拟器

Kaspar – C++20 HFT trading system and order book simulator

AI 导读

Kaspar 是一个基于 C++20 actor 框架的高频交易系统兼订单簿模拟器,同一套策略代码可在 PCAP 回放、实时模拟盘和 iLink 3 实盘执行间通过配置切换。其内置 CME MDP3 行情与 iLink 3 报单会话已通过 CME 自动认证,fast_send 同步调用相比直接调用仅增加约 7.5 ns 开销。

正文 · AI 翻译

生产级交易系统与仓位感知订单簿模拟器

与交易场所无关 • 包含 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 消息往返延迟

fast_send round-trip latency (EPYC): 3370 ns cross-thread async, 90 ns grouped, 30 ns fast_send

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 随后在回放、模拟盘和实盘交易中不加改动地运行。

  1. 定义消息。从 actors::MessageT<Prediction> 派生一个 Prediction 消息(id 自动分配)。携带标的、订单簿更新的市场时间和预测值。
  2. 编写模型 actor。从 actors::Actor 派生,并向 MESSAGE_HANDLER 注册两个处理函数:Start,在其中向订单簿发送 Subscribe(Subscribe::HI);以及 EndOfBurst,订单簿在每批更新后发送它。
  3. 计算并发布。在每个 EndOfBurst 上,读取最多 16 个价位,计算预测,并向每个策略 actor send 一个新的 Prediction。
  4. 在策略中使用它。向策略 actor 添加一个 Prediction 处理函数,并在其下单决策中使用最新值,例如作为是否跟随某笔新增订单的门控。
  5. 接入它。在回放模拟器中,将该 actor 添加到模拟器的组中,位于行情数据读取器之前;整个回放保持在同一个队列和同一个线程上,因此保持确定性。在交易二进制文件中,将其添加到策略 actor 旁边。

要评估一个模型,请在同一回放中针对无模型影子基准运行它:相同的日期、相同的目标数量、相同的延迟。

关于模型本身(每个模型如何构建、预测什么、如何在队列精确回放中测试它,以及如何在加速硬件上部署它),请参阅 A Survey of Limit-Order-Book Models, Simulators and Hardware Acceleration(PDF)。

深度解析

关于 Kaspar 背后设计的深度解析(作者 Substack——vincentmayeski.substack.com):

许可证

MIT 许可证。版权所有 (c) 2026 Vincent Mayeski / M2 Tech (16425640 Canada Inc.)。参见 LICENSE。

来源:Hacker News · github.com