简介:这份资源是一套用C++编写的高频交易系统源码,面向已有一定编程基础、希望了解量化交易底层实现的开发者。程序围绕低延迟行情处理、算法下单与自动化执行展开,可帮助理解高频交易中的多线程协作、内存管理、配置加载和风险控制等核心环节。压缩包共十六个文件,大小约二十KB,构成包括五个cpp源文件、四个hpp头文件、两个conf配置文档、一个解决方案工程及若干辅助文件,目录结构清晰,方便直接编译和在此基础上扩展功能。已有一千八百九十四人学习浏览,适合作为个人学习高频交易系统设计的入门参考。通过阅读和调试这份源码,可以掌握交易系统的基本骨架,包括主程序入口、配置文件解析、日志模块和接口封装,并结合描述中的算法交易、低延迟技术和合规要求,形成从理论到实践的整体认识。 干高频交易系统这一行,真正拉开差距的往往不是策略本身,而是那套在微秒级、纳秒级尺度上稳定运转的C++程序。我见过太多团队把策略写得花团锦簇,但一上实盘就被延迟打脸,问题几乎都出在源码工程化上:线程模型乱、内存分配失控、日志把主链路卡死、锁竞争让延迟抖动上千倍。这篇文章我把这些年做C++高频交易系统的实战经验完整拆一遍,从整体架构、核心组件、代码骨架到性能优化和回测思路,按可落地的步骤讲。
不管你是刚开始学C++、想往量化方向走的新手,还是已经在做行情接入、订单撮合这类基础服务的开发者,这套从源码角度切入的高频交易程序体系都能帮你建立正确的技术框架。我不是在教你调某个API,而是在拆一个真正能跑的、延迟可控的高频交易系统到底由哪些代码模块组成,以及这些模块之间怎么配合。
1. 高频交易系统的本质:延迟、吞吐和确定性
做高频交易最怕的不是行情慢,而是系统行为不可预测。一个正常行情下200纳秒返回的接口,在突发流量下突然变成200微秒,这比一开始就慢更致命。所以C++在这个领域长期无法被替代,核心原因不在于所谓“C++最快”这种笼统说法,而在于它能提供最直接的内存布局控制、零成本的抽象、以及不受垃圾回收影响的确定性执行流。
1.1 高频交易系统的经典分层
一个完整可用的高频交易系统,源码层面通常分成这几层:
- 行情接入层:接收交易所或行情源的多播数据,解码、校验、组装成内部统一格式。
- 策略决策层:基于订单簿、成交明细、自身持仓计算买卖信号,这块是研究员最常写的代码。
- 订单管理层:负责订单状态跟踪、撤单重发、超时处理,是防止“裸奔”的安全阀。
- 风控层:在订单上下网络前做价格校验、持仓校验、频率校验,所有规则必须在纳秒级完成。
- 交易网关层:连接柜台或交易所的协议接口,处理登录、心跳、报文重传等底层网络细节。
这五层不是简单的函数调用链,而是通过事件驱动串联起来的数据流。每一层只消费上一层的产出事件,再发布自己的新事件。这样做的核心价值是解耦——任何时候策略层需要换模型,都不用去动底层网络代码。
1.2 为什么这个领域对延迟敏感度极高
高频交易领域的竞争单位已经从微秒缩小到百纳秒。当时钟周期在3GHz左右时,一个CPU指令大约0.33纳秒,一个L1 cache命中约1纳秒,一次内存随机访问约100纳秒。这意味着你每次不必要地分配一块堆内存,可能就等于浪费了几百次计算机会。这种环境下,靠JVM的JIT预热或者Go的GC调优来博延迟,远不如C++直接把对象放在栈上、放在预分配内存池里来得可控。源码里的每一行都要扪心自问:这里是否会触发系统调用?是否会分配内存?是否会引起cache miss?
2. 核心组件设计:从事件循环到无锁队列
我在设计任何一个C++高并发服务时,第一件事永远是画线程模型。高频交易系统不是线程越多越好,恰恰相反,真正讲究的行情链路通常只有两三个线程,其他杂活全部丢到旁路线程。
2.1 单线程事件循环是低延迟的基石
为什么不建议在高频交易主链路上使用多线程并行处理?核心原因是锁和同步带来的不确定性远大于并行带来的收益。高频交易的数据本质上是串行的——每一笔行情都有一个时间戳,策略必须按时间顺序处理。既然数据天然是序列化的,那并行化就失去了意义,反而引入了竞争和乱序复杂度。
单线程事件循环的设计模式大家都不陌生,类似muduo或者Redis的Reactor模型。核心结构是:一个epoll或io_uring事件循环,监听行情socket、用户指令socket、定时器fd,收到事件后在单线程内完成全部解码、策略计算、下单。这里的关键在于,事件循环内部绝对不能出现阻塞调用,比如同步日志、网络IO重试、锁等待,否则整个行情链路就跟着停摆。
2.2 SPSC无锁队列与原子操作
虽然主链路是单线程,但系统终究需要与其他模块通信,比如把风控结果回传、把行情快照发给监控页面。这种场景我倾向于用无锁队列,其中最经典的便是SPSC(Single Producer Single Consumer)队列。它的核心不用锁,只依赖原子变量和内存序。
写SPSC队列时最容易踩的坑是内存序用错。对于生产者和消费者各操作一个独立的索引,其实可以用memory_order_relaxed,因为索引之间天然形成了Release-Acquire关系。但如果图省事全部用memory_order_seq_cst,在部分CPU上会引入不必要的同步屏障,延迟翻倍。源码里不要滥用最强内存序,按需选择即可。
template <typename T, size_t N> class SpscQueue { static_assert((N & (N - 1)) == 0, "N must be power of 2"); std::vector<T> data_; alignas(64) std::atomic<size_t> head_{0}; alignas(64) std::atomic<size_t> tail_{0}; public: explicit SpscQueue() : data_(N) {} bool push(const T& item) { size_t h = head_.load(std::memory_order_relaxed); size_t t = tail_.load(std::memory_order_relaxed); if (t - h == N) return false; // full data_[t & (N - 1)] = item; tail_.store(t + 1, std::memory_order_release); return true; } bool pop(T& item) { size_t h = head_.load(std::memory_order_relaxed); size_t t = tail_.load(std::memory_order_acquire); if (h == t) return false; // empty item = data_[h & (N - 1)]; head_.store(h + 1, std::memory_order_relaxed); return true; } };这个队列的容量固定为2的幂,通过位与运算替代取模。head_和tail_各占用一个缓存行,避免两个核心修改同一个缓存行时互相失效。第二行代码里head_的load操作放在tail_的load之后,是为了确保队列满时的判断不会读到过期的tail值,从而避免ABA问题的出现。
2.3 内存池:别让malloc成为延迟地雷
高频交易源码中最容易被忽略的性能杀手就是malloc。默认分配器在多线程环境下存在锁竞争,即使在高性能tcmalloc下,分配和释放的路径依然存在原子操作和缓存行伪共享。我在核心链路里从不直接使用new或malloc,而是根据消息类型建立独立的对象池。
对象池的核心逻辑是预分配固定数量对象,用一个自由链表维护可用对象。释放时把对象重新挂回链表。整个过程中没有任何系统调用,彻底绕开了内核。比如行情解码器里每收到一个行情包需要生成一个Event对象,如果这个Event每次都在堆上new,那么几百万包每秒的压力下分配器必然成为瓶颈。
template <typename T, size_t PoolSize> class ObjectPool { std::array<T, PoolSize> storage_; std::array<T*, PoolSize> free_list_; size_t free_count_; public: ObjectPool() : free_count_(PoolSize) { for (size_t i = 0; i < PoolSize; ++i) free_list_[i] = &storage_[i]; } T* acquire() { if (free_count_ == 0) return nullptr; return free_list_[--free_count_]; } void release(T* obj) { free_list_[free_count_++] = obj; } };这个基本内存池没有处理对象构造析构的细节,生产环境还需要配合placement new和显式析构调用。但核心思路是一致的:固定内存、零系统调用、常数时间分配。它的代价是PoolSize需要提前估算,池子大了浪费内存、小了在极端行情下会返回nullptr,因此监控池的使用率是非常必要的可视化手段。
3. 实战:写一个最小可用的高频行情处理框架
这一节我用可编译的C++代码,把一个最小的高频行情处理框架搭出来。实际工程至少比这复杂一个量级,但骨架逻辑保持不变,理解了这一段,后面扩展就有方向。
3.1 数据结构设计:行情快照与增量更新
行情数据分为快照和增量两类。快照是全量的五档甚至十档报价,增量是每次变化的一条记录。为了节省带宽,高频源通常减少快照推送频率,重点推送增量。策略端需要自己在内存中维护一个订单簿,把增量合并进去。
订单簿的设计有以下几种方式:用红黑树实现价格层的有序集合,用unordered_map做价格到层级的映射,再加一个链表维护聚合的量。在C++实现里,std::map天然就是红黑树,但它的节点分配开销较大。高频领域更常见的做法是使用侵入式容器,把节点结构内嵌到对象里,减少分配与缓存缺失。
3.2 事件总线与指令流
事件总线用来承载各模块的事件传递。定义统一的EventHeader,包含事件类型、时间戳、序列号、长度。不同的事件类型可以已知布局,也可以带变长数据。这里最核心的约束是“单一写者”——即事件总线的数据写入只允许一个线程,读取可以多线程,但通过SPSC队列隔离。
enum class EventType : uint8_t { MARKET_DATA_UPDATE, ORDER_ACK, ORDER_FILL, RISK_REJECT, USER_COMMAND, TIMER_TICK }; struct EventHeader { EventType type; uint16_t length; uint64_t timestamp_ns; uint64_t seq; }; struct MarketDataEvent { EventHeader header; uint32_t symbol_id; int64_t price; int64_t volume; uint8_t side; };事件总线接到一个事件后,按类型分发到对应的处理函数。策略端接收MarketDataEvent后更新自己的订单簿,然后调用策略逻辑。订单管理层接收OrderAck后更新订单状态。整个过程都在单线程内完成,没有排队、没有调度器、没有锁。
3.3 编译选项与配置
源码写得好,编译参数跟不上也是白搭。我习惯用这样一套编译配置做高频交易核心服务:
g++ -std=c++17 -O3 -march=native -mtune=native -flto \ -fno-exceptions -fno-rtti -falign-functions=64 \ -fno-stack-protector -fno-asynchronous-unwind-tables \ -pthread -o hft_app main.cpp gateway.cpp strategy.cpp逐条解释一下核心选项:-O3 启用所有安全优化,-march=native 针对当前CPU生成指令集,-flto 做跨编译单元优化,-fno-exceptions 去掉运行时异常处理开销,-fno-rtti 关闭运行时类型识别。这些选项都是为了减少生成的机器码中的隐含分支和元数据操作。代价是代码里不能依赖异常机制,错误处理全部改用错误码。
4. 性能优化常见问题实录
很多人对高频交易源码的误解是“只要用了无锁就快了”。实际上无锁只是消灭了锁等待,真正拖慢系统的往往是缓存伪共享、错误的分支预测、频繁的系统调用和内存分配。下面这些是我实际项目中遇到率最高的问题。
4.1 缓存伪共享(False Sharing)
两个线程各自操作独立变量,但这两个变量恰好落在同一个64字节缓存行里,那么任何一个线程修改变量都会导致另一个线程的缓存行失效,不得不重新从内存加载。解决办法是人为给每个变量添加填充。
我在SPSC队列中已经展示了head_和tail_分别用alignas(64)隔离。实际项目中,线程间的统计计数器、状态标志位都要做同样处理。排查伪共享最有效的方法是先用perf查看cache miss事件,然后对比修改前后的命中率变化。
4.2 锁与原子操作的选择
高频主链路上不应该出现std::mutex。如果实在需要同步,优先使用原子变量加自旋锁,而不是进入内核睡眠。自旋锁适合临界区极短的场景,但要注意在高竞争下会空转CPU。
涉及ABA问题的场景通常发生在无锁数据结构的指针操作中。比如无锁栈里一个线程把节点A弹出,另一个线程又把A压回,第一个线程读到的栈顶仍然是A,但此时的A已经是新数据。解决ABA问题的经典方法是给每个写入的指针附加一个版本号,比较时同时比较版本号,也就是带标记的原子指针。
4.3 日志系统绝不能拖垮主链路
很多人刚写高频系统时会习惯性在行情处理函数里加LOG_INFO,一上行情流量立刻发现延迟飙升。原因很简单:同步日志要加锁、要写磁盘、要刷缓冲,一旦磁盘抖动,主线程直接卡住。
我的方案是主线程只把日志内容写进一个预分配的内存缓冲,然后通过SPSC队列交给日志线程异步写盘。如果缓冲满了,直接丢弃新日志并计数,绝不阻塞主链路。日志内容里应该带毫秒级时间戳和线程号,方便复盘定位。
5. 回测与模拟撮合:不能只在生产环境验证
高频系统比其他软件更复杂的一点在于:它面对的是不可重放的实时市场。因此必须有可靠的回测和模拟撮合环境,把策略在历史数据和模拟盘上验证充分,才能最后接入实盘。
5.1 撮合引擎的公平性
回测中的撮合引擎必须严格按时间优先、价格优先的原则处理订单。如果用价格优先但时间排序不精确,回测结果和实盘表现会偏差巨大。最简单实现是维护一个买卖方向的order set,按价格、时间戳排序,每次行情变动时扫描可能成交的价格档位。
5.2 模拟延时注入
真正的交易系统不可避免存在网络延迟、撮合延迟、交易所排队延迟。回测时如果完全不模拟延迟,很容易得到过度乐观的结果。我通常会根据历史统计建立延迟分布模型,给每个订单、每个行情更新注入一个符合分布的随机延迟。数学期望和方差用实测值填充,这样回测结果才有参考意义。
5.3 从回测到实盘的验证清单
我在切换实盘前有一套固定检查项:
- 行情时间戳和本地时间是否对齐,偏差超过1毫秒必须修正。
- 订单状态机是否完备,能否覆盖全部异常路径,比如超时、拒单、重复回报。
- 内存池容量是否足够支撑历史最高瞬时流量,并至少留出30%余量。
- 日志缓冲是否足够长,行情骤增时不能阻塞主链路。
- 风控规则是否在行情模块之前执行,确保风险订单来不及发往交易所。
6. 高频交易系统的稳定性经验清单
代码性能只是前半场,真正决定一个系统能否在真实环境生存的是稳定性。这一节我整理一些踩坑后沉淀下来的规则,每一条都有真实的教训在里面。
- 任何调用链都不允许无界增长,必须设置队列容量和超时时间。某个模块一旦积压,宁可丢弃数据也不能无限堆积导致内存爆炸。
- 核心链路禁用虚函数。虚函数虽然单次调用损耗不大,但在高频调用下会影响分支预测和inline效果。
- 网络连接的心跳检测必须独立于行情线程。行情停了可能是行情源问题,但不代表交易链路本身断了,两者要分开处理。
- 系统启动时要先预热所有内存池和线程栈,避免运行前几分钟因缺页中断导致延迟突变。
- 每次发布前编译选项、代码变更、配置项都要纳入版本管理。高频系统最怕“昨天还能跑,今天突然延迟变高”这种不知原因的回归。
- 监控项要覆盖延迟均值、P99延迟、订单丢失率、内存池水位、队列积压数。延迟平均值低没有意义,P99才是真实体验,因为那部分尾巴会直接表现为失败订单。
另外还有一件容易被忽视的事:源码保护。高频交易程序往往承载公司核心策略,部署在客户环境或云端服务器上时,需要防止被逆向。编译器开启 -O2/-O3 后代码优化程度很高,但符号表仍然可以被提取。生产环境发布前一般要strip掉符号,还需要给关键算法做混淆处理。源码本身的托管权限要收紧,不是所有开发人员都能接触完整策略代码,模块化编译、分权限编译是常用做法。
7. 快速定位问题的三个排查工具
实战中最常用的三个工具,能覆盖大部分高频交易系统的问题排查。
第一个是perf,用来统计CPU采样热点和缓存命中率。对可疑函数跑一下perf record,然后看perf report里的占比分布,通常热函数一目了然。
第二个是strace,用来检测系统调用频率和阻塞点。如果某个线程频繁出现futex、write、read等系统调用,大概率发生了不必要的锁竞争或日志写入。线上抓strace会大幅影响性能,一般只在测试环境复现问题时候用。
第三个是gdb,用来调试崩溃和死锁,配合core dump分析。重点看各个线程的堆栈,确认阻塞在线程同步还是IO上。
这三个工具可以组合使用:先用监控数据定位时间区间,再用perf抓热点,最后用gdb看线程现场。我踩过的很多莫名其妙的问题,比如偶发的超时、偶发的爆量丢单,最后都是靠perf抓出热函数,再结合源码逐行看出来的。
我在实际做高频交易系统的过程中,最大的体会是:延迟优化不是靠某个单一技巧,而是靠一整套系统化的源码设计,从事件循环、内存管理、队列设计、编译优化到日志旁路,每一环都必须收敛。一个小毛病没处理好,在高频的放大效应下都会变成灾难。如果你准备自己动手写一套C++高频交易程序,先别急着堆功能,从行情接入、事件循环、内存池这三块起步,跑通后再逐步加入策略、回测和风控。等你能稳定处理百万级每秒行情,同时对订单处理延迟保持微秒级,那时候你才算真正入门了。
本文还有配套的精品资源,点击获取