简介:C++高频交易源码包面向中高级C++开发者和量化交易爱好者,旨在展示一套接近实战的高频交易系统骨架,覆盖算法交易、低延迟通信、实时行情处理、订单生成与风控等核心主题。压缩包内共16个文件,包含5个cpp源文件用于实现主流程与业务逻辑,4个hpp头文件用于声明接口与数据结构,2个conf配置文件用于参数和日志设置,还附带了解决方案文件与工程过滤器,整体约20KB,体量精简但功能模块清晰,适合快速通读。该资源迄今已有1894人学习,热度验证了其实用价值。通过研读源码,可以掌握配置加载、日志记录、行情快照解析、交易指令构造等高频交易中绕不开的编码细节,也能体会C++在多线程、低延迟场景下的性能优化手法。无论是为自建交易系统寻找参照,还是想理解HFT系统在毫秒级决策背后的技术支撑,这份代码都提供了很好的起点。
C++编写的高频交易源码程序:从架构到低延迟落地的完整拆解
在高频交易(HFT)这个圈子里,C++基本是绕不开的语言。无论是看muduo这类网络库的源码,还是自己从零搭一套行情转发引擎,最终玩的就是“低延迟”三个字。最近在整理一套用C++编写的高频交易源码程序,我把它从架构、线程模型、无锁队列到内存池逐层拆开来讲,尽量把“为什么这么写”说透,而不是只贴代码。这套系统适合三类人看:一是想入行量化交易开发的后端C++工程师,二是已经在做交易系统但想优化延迟的同行,三是对高并发低延迟编程感兴趣的纯粹技术爱好者。
先说这套源码程序到底做了什么。它的核心链路是一套非常典型的窄链路:行情接入 → 策略计算 → 订单路由 → 风控校验 → 交易所回报处理。源码覆盖了这些模块的完整实现,包括网络通信层、内存管理、多线程调度、无锁队列和日志系统。我在实际跑测中,单跳行情穿透延迟可以稳定在微秒级别,去掉网络开销后,纯策略计算加路由发送基本在几百纳秒内完成。这背后靠的不是什么黑魔法,而是对CPU、内存和线程模型的理解。
1. 高频交易系统的核心诉求:为什么偏偏是C++
1.1 低频与高频的本质差异
低频交易系统跑一遍策略可能花几百毫秒,多50毫秒没人关心。但高频交易系统的时间单位是微秒,甚至纳秒级别。一个订单从行情触发到发送到交易所,链路每多一个微秒,就意味着在同样的行情波动下,你可能比别人晚进场或晚出场,结果就是成交价格变差,甚至直接错过成交机会。这是高频交易和低频交易最本质的差异:延迟敏感性和确定性。低频系统可以容忍偶发延迟,高频系统则对每一次操作的耗时都有严格上限。
也正因为如此,高频系统的设计目标不是“平均延迟低”,而是“尾延迟低且稳定”。也就是说,P99、P99.9的延迟必须和平均延迟接近,不能出现偶发的几百微秒的尖刺。这就需要排除掉所有可能导致不确定性的因素:锁竞争、内存分配、系统调用、日志写入、缓存未命中,这些都是尾延迟的来源。
1.2 语言选型对比:GC是硬伤
为什么在这个场景下C++是主流,而不是Java或Python?最核心的原因有三个。第一,C++没有垃圾回收(GC),对象生命周期完全由开发者控制,不会出现GC暂停导致的毫秒级延迟尖刺。JVM虽然有ZGC、C4这类低延迟GC,但长时间运行下仍可能出现阶段性的STW,这在高频场景是不可接受的。第二,C++能直接操作内存和硬件,可以做缓存行对齐、NUMA感知、CPU亲和性绑定,甚至通过DPDK和内核旁路技术绕开协议栈,这些能力是Java和Python很难提供的。第三,C++标准库和模板机制能支撑零成本抽象,写出来的无锁队列、内存池在优化后可以和手写汇编媲美。
Python就不用多说了,做策略研究和回测很方便,但真要上生产做实时交易,解释器开销和GIL注定它只能做辅助角色。Java在券商核心系统里确实有应用,因为它生态成熟、开发效率高,但高频交易这种极致场景,C++的确定性优势是别的语言难以替代的。
1.3 核心设计指标:延迟、吞吐与确定性
一套高频交易系统在设计阶段就要明确三个指标。
第一个是端到端延迟,即从行情事件发生到订单进入交易所前置机的时间,这个值通常用微秒衡量,好的系统可以控制在10微秒以内。第二个是吞吐量,即每秒能处理的行情消息数和订单数,通常用每秒百万条(M msg/s)级别来衡量。第三个是确定性,即延迟的抖动范围,核心指标是P99和最大延迟。
这三个指标其实是相互制约的。追求极致延迟,就要减少处理环节,而减少处理环节意味着风控和校验逻辑要精简,这又可能牺牲安全性。实际工程中要做的是平衡:在满足合规和风控要求的前提下,尽可能压缩每一步的耗时。我在源码里看到的设计思路也是这样,各模块之间不共享锁,而是通过无锁队列传递数据,每个模块只做自己那一件事,尽可能让CPU流水线保持忙碌。
2. 源码整体架构与链路设计
2.1 分层架构与模块职责
这套源码的分层非常典型,一个完整的低延迟交易系统通常包括这几层:行情接入层,负责对接交易所或行情源,解析并广播行情消息;策略决策层,根据行情数据执行交易策略,产生买卖信号;订单管理层,负责维护订单状态、管理订单生命周期;风控层,在下单前进行资金核对、持仓核对、频率限制等校验;路由与网关层,负责与交易所前置机通信,发送订单并接收回报。
各层之间通过无锁队列解耦,每一层都运行在独立的线程上,线程之间不共享可变状态,只通过队列传递消息。这种设计的最大好处是避免了锁竞争,每个线程的工作集较小,CPU缓存命中率高,同时逻辑清晰,方便定位问题。我在代码里特别注意了队列的生产和消费速度匹配,如果某一层处理不过来,系统应该有背压机制通知上游降速,而不是无限堆积。
2.2 数据流:从行情Tick到订单回报
一条典型的交易数据流是这样走的。行情源推送一个Tick,行情接入线程收到后做解析和校验,把行情数据放入策略队列。策略线程从队列取出行情数据,运行策略模型,如果产生交易信号,就组装成订单消息,交给风控队列。风控线程校验通过后,把订单交给路由线程。路由线程将订单发送给交易所网关,同时把订单状态记录到本地。交易所回报回来后,回报处理线程更新订单状态,并触发相关回调逻辑。
整个过程涉及多个线程协作,但没有任何一个线程在等待另一个线程的锁。所有线程都在自己的队列上忙等或短暂休眠。这套设计本质上是把一个大任务拆成小流水线,每个阶段只做少量工作,最大程度减少上下文切换和缓存污染。这也是我为什么强调,高频交易的“快”不是靠某一段代码的奇技淫巧,而是靠整体架构的合理分工。
2.3 线程模型与CPU亲和性绑定
线程模型的设计尤为关键。我在这套源码中看到的是每个核心模块一个线程,线程数量固定不动态增减。这样做的原因很简单:动态创建线程涉及系统调用和内存分配,会带来延迟波动;线程多了会导致大量上下文切换,反而降低吞吐。每个线程绑定到一个物理核心上,通过pthread_setaffinity_np设置CPU亲和性,避免线程在不同核心间迁移,保证CPU缓存热度。
有一个细节值得特别说明:绑核不能只看CPU编号,还要看物理核心和逻辑核心的关系。现代CPU大多有超线程,同一个物理核心上的两个逻辑核心共享L1/L2缓存。如果两个繁忙的线程被绑定到同一个物理核心的两个逻辑核心上,反而会因为争抢缓存资源导致性能下降。正确的做法是让每个线程独占一个物理核心。我自己实测过,线程绑核与否,在同样负载下P99延迟能相差30%以上。
2.4 关键数据结构:环形缓冲与对象复用
在频繁收发消息的场景下,动态内存分配是性能杀手。系统运行中如果没有对象池,每个Tick都new一个对象再delete,不仅耗时,还会产生内存碎片,长时间运行后性能断崖式下跌。这套源码的解法很直接:启动时一次性分配好所有需要的内存,运行时只做复用。
具体实现上,行情队列和订单队列用的是无锁环形缓冲区(RingBuffer),容量在初始化时固定,不扩容不收缩。每个槽位预分配一块固定大小的内存,消息以拷贝或移动的方式放入槽位,消费者取出后清空槽位复用。队列的读写指针用原子变量维护,一个线程只做生产者操作,另一个线程只做消费者操作,通过内存序(memory order)保证可见性,避免使用锁带来的内核态切换。这个设计简单且高效,值得反复推敲。
3. 源码中的核心C++技术点拆解
3.1 无锁队列:内存序才是灵魂
无锁队列是这个系统的核心数据结构。很多人写过无锁队列,但很容易忽略内存序的问题。如果只使用默认的顺序一致性(seq_cst),性能会大打折扣。正确的做法是区分场景:生产者只需要保证写入的数据对其他线程可见,消费者只需要保证读取到的数据是完整的,所以生产者侧使用release,消费者侧使用acquire即可。
一个基于数组的SPSC(单生产者单消费者)队列的核心逻辑大致是这样:
template<typename T, size_t CAPACITY> class SPSCRingBuffer { public: bool push(const T& item) { size_t pos = write_pos.load(std::memory_order_relaxed); size_t next = (pos + 1) % CAPACITY; if (next == read_pos.load(std::memory_order_acquire)) { return false; // 队列已满 } buffer_[pos] = item; write_pos.store(next, std::memory_order_release); return true; } bool pop(T& item) { size_t pos = read_pos.load(std::memory_order_relaxed); if (pos == write_pos.load(std::memory_order_acquire)) { return false; // 队列为空 } item = buffer_[pos]; read_pos.store((pos + 1) % CAPACITY, std::memory_order_release); return true; } private: // 关键:读写指针需要放在不同的缓存行上 alignas(64) std::atomic<size_t> read_pos{0}; alignas(64) std::atomic<size_t> write_pos{0}; alignas(64) T buffer_[CAPACITY]; };这段代码里有三个关键点。第一,读写指针都加了alignas(64),避免伪共享,这一点后面细说。第二,push只用了release,pop只用了acquire,而不是默认的seq_cst,这能显著降低内存屏障的开销。第三,队列满了就返回失败,由上层决定是丢弃还是重试,而不是用锁让生产者阻塞,因为阻塞会带来上下文切换和唤醒的延迟。实际测试中,同样的队列用seq_cst和用release/acquire相比,在8核机器上吞吐能差5%到10%,延迟抖动也更小。
3.2 缓存行对齐与伪共享问题
伪共享是高并发编程里最容易踩的坑。简单说,CPU缓存的最小单位是缓存行,通常是64字节。如果两个不同线程的变量恰好在同一个缓存行上,即使它们逻辑上毫无关联,一个线程修改其中一个变量,会导致另一个变量所在的缓存行失效,另一个线程读取时就必须去内存重新加载。在循环高频读写的场景下,这个开销是巨大的。
这套源码在涉及线程间通信的结构体上,都做了缓存行对齐处理。最典型的就是环形缓冲区的读写指针,通过alignas(64)把读指针和写指针分到不同的缓存行。我一开始没有做这个优化,压测时发现两个线程的吞吐始终上不去,后来用perf stat观察,cache-miss率极高,加了alignas(64)之后性能直接翻倍。这个优化代码改动不大,但效果立竿见影。
不只是队列指针,订单类、行情消息类这种会被不同线程访问的结构体,也建议做同样的处理。不过要注意,盲目增大结构体对齐会浪费内存空间,常规做法是只对高频读写的热字段做对齐,冷字段不要混入热缓存行。
3.3 内存池:用空间换确定性
内存池的作用前面提到了,但具体实现还有一些讲究。这套源码中,内存池被设计为固定大小的对象池,每个池只分配一种类型的对象。比如行情池、订单池、回报池各自独立。池的基本操作是allocate和deallocate,内部维护一个空闲链表,分配时从链表头部取一个对象,释放时把对象挂回链表头部。
为了避免内存池自身的锁竞争,每个线程绑定一个独立的内存池实例,线程之间不互相借用。这样一来,分配和释放完全是无锁的。线程本地内存池的大小需要根据业务峰值来估算,比如行情峰值每秒100万条消息,每条消息对象占用256字节,线程本地池就至少要预留256MB的空间,再留一些余量防止极端行情下池耗尽。
这块有一个实用的建议:内存池一定要做监控,统计池内空闲对象数量和水位线。如果内存池频繁耗尽,系统就需要扩容;如果空闲对象长期很多,说明池子开大了,造成浪费。我习惯在压测时把水位线打印到指标日志中,既能观察内存使用是否合理,也能提前发现内存泄漏或使用异常。
3.4 原子操作与ABA问题
无锁数据结构经常要处理ABA问题。简单解释一下,一个指针A被线程1读到了,此时线程2把A的内存释放掉,再重新分配一块相同地址的内存并写入了新值,线程1再次检查发现指针还是A,以为没有变化,但实际指向的内容已经变了。在无锁队列中,如果使用引用计数和内存回收机制,就有可能出现这个问题。
这套系统里用的是SPSC模型,生产者和消费者各自持有独立的指针,不存在多线程同时修改同一个指针的场景,所以ABA问题在这个场景下基本不会出现。但如果你的系统用的是MPMC(多生产者多消费者)模型,那就要小心了。解法一般是使用带标签的原子指针,即一个atomic整数同时编码指针地址和版本号,每次修改时版本号加1,比较时同时比较地址和版本号。64位系统下可以把版本号压缩到高位,或使用指针加计数器组成的16字节CAS,不过16字节对齐在x86上要依赖cmpxchg16b指令,在ARM上则是LSE扩展,平台兼容性需要注意。
3.5 锁与阻塞的代价
锁为什么在高频系统里是禁忌?因为锁的代价不只是获取和释放的时间,还有阻塞带来的上下文切换、线程唤醒、CPU调度等一连串开销。一个锁竞争导致的线程休眠,可能轻松给系统增加10到100微秒的延迟,对于高频系统来说是毁灭性的。就算是不阻塞的自旋锁,在竞争激烈时也会消耗大量CPU空转,浪费计算资源。
这套源码几乎看不到锁,代码里大量的互斥操作都用原子变量代替。比如订单状态机的状态切换,用exchange原子操作实现CAS循环,比用加锁保护状态字段快很多。有一处地方是订单序列号的自增,用fetch_add实现,保证每个订单号全局唯一且线程安全。这些都是高频开发中非常典型的锁替代方案。当然,无锁不等于没有复杂度,无锁代码的调试和排查比加锁代码难得多,所以国内很多量化团队在非核心路径上还是会用锁,只在核心链路上做无锁化。
4. 实操:从源码到可运行系统
4.1 环境准备与依赖安装
这套源码依赖的东西不多,核心依赖是CMake(版本3.14以上)、gcc或clang(支持C++17)、以及libpthread和librt这两个系统库。如果你要启用DPDK做内核旁路,还需要额外安装DPDK开发库,不过普通测试环境不需要。操作系统建议是Linux,最好是Ubuntu 20.04以上或CentOS 8以上,内核版本5.x。
我在Ubuntu 22.04上测试时,一条命令就能装好所有依赖:
sudo apt-get update sudo apt-get install -y build-essential cmake libpthread-stubs0-dev如果要用perf做性能分析,顺手装上linux-tools-$(uname -r)。注意,perf在某些云主机上可能因为权限限制无法使用,这时候可以用gdb的采样功能或者bpftrace替代。
4.2 编译与运行全流程
编译过程不复杂,但要确保采用Release模式并开启编译优化,否则延迟数据没有参考意义。
git clone <你的代码仓库地址> hft-system cd hft-system mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=17 make -j$(nproc)编译完成后,在build目录下会生成hft_main和hft_client等可执行文件。启动系统前,先看一下配置文件,通常叫hft.conf,里面配置了行情源地址、交易账户信息、风控参数、日志级别等。测试时可以把行情源配置为一个模拟行情程序(源码里自带的simulator),用回放数据驱动系统运行,这样既安全又可以反复测试。
运行入口一般是这样:
./hft_main --config=hft.conf ./simulator --host=127.0.0.1 --port=7001启动日志里会打印各模块的初始化信息和测试行情连接状态。启动顺序建议先启动行情源模拟器,再启动hft_main,避免重连逻辑触发不必要的日志告警。
4.3 关键配置参数说明
配置文件里的参数直接决定系统行为,这里挑几个关键项做解释。
| 参数名 | 默认值 | 说明 |
|---|---|---|
| ticker_thread_cpu | 2 | 行情线程绑定的CPU编号 |
| strategy_thread_cpu | 3 | 策略线程绑定的CPU编号 |
| queue_size | 1048576 | 行情队列的容量(槽位数) |
| enable_gc_log | false | 是否输出GC日志(C++无GC,这里主要指内存池水位) |
| risk_max_order_rate | 1000 | 每秒最大下单数,超过即触发熔断 |
| log_level | info | 日志级别,生产建议warning |
配置CPU编号时不要随便填,要先看机器拓扑。用lscpu查看物理核心分布,再用taskset验证线程是否绑到了预期核心。我遇到过一种情况,机器的0号CPU被系统中断占用严重,把行情线程绑到0号核心后延迟飙升,换到4号核心就恢复正常。如果实测发现某个核心中断负载很高,优先换绑。
4.4 压测方法与延迟观测
验证系统性能最好的方式是压测。源码自带一个latency_probe工具,可以模拟行情推送并测量从Tick进入系统到订单路由到模拟交易所的耗时。命令如下:
./latency_probe --ticks=1000000 --config=hft.conf运行结束后会输出延迟分布报告,包含平均延迟、P50、P99、P99.9和最大延迟。我建议关注P99.9而不是平均延迟。平均延迟容易被极端快的样本拉低,P99.9更代表真实用户感知的尾部表现。我第一次跑压测时平均延迟只有3微秒,看着很高兴,但P99.9到了40微秒,排查后发现是某条日志在队列拥塞时同步刷盘导致的,把日志改成异步写盘,P99.9立刻降到了8微秒以内。
压测时还要注意CPU频率波动问题。笔记本上的CPU频率会因温度变化而波动,导致延迟数据忽高忽低,跑基准测试前要先把CPU调成performance模式,并且固定在某个频率,才能测出稳定可复现的数字。
5. 常见问题与排查技巧实录
5.1 延迟突然抖动:先从环境查起
压测中最让人头疼的问题就是延迟突然跳一下,然后又恢复正常。我在排查这类问题的顺序是:先看CPU频率是否被降频,再看是否有其他进程抢占核心,再看线程是否被迁移,最后才怀疑代码逻辑。之前遇到一次延迟尖峰,排查到最后发现是隔壁机器上的备份任务把网络带宽占满了,导致行情数据包排队。这个经验就是:延迟抖动的问题,先查环境,再查代码,别一上来就埋头看源码。
如果是服务器上的多租户共享机器,还需要检查是否被其他进程抢占了CPU和内存带宽。用perf top可以快速判断哪个进程正在大量占用CPU资源,也可以通过检查/proc/loadavg和pidstat观察全局负载。高负载时期不建议做性能基准测试,数据不具参考价值。
5.2 伪共享导致性能骤降:一个真实的排查案例
前面提过伪共享,这里说一个真实案例。有一次我压测订单路由模块,发现吞吐从每秒80万笔掉到40万笔,但内存和CPU占用都不高。用perf record采集cache-miss事件,发现某个结构体上的缓存未命中率极高。定位后发现,订单状态字段和更新时间字段被定义在同一个结构体里,路由线程频繁写状态,而监控线程频繁读更新时间,两个字段恰好挤在一个缓存行上,互相拖累。把这两个字段分别用alignas(64)隔开后,吞吐立刻恢复正常。
排查伪共享有一个简单实用的小技巧:在代码里给结构体字段标注偏移量,通过打印addressof确认字段之间的内存布局。如果两个热点变量距离小于64字节,就要考虑对齐到不同缓存行。另外,gcc编译时加-fno-optimize-sibling-calls之类的优化选项可能会改变结构体重排,但C++标准没有规定字段布局顺序一定要跟声明顺序一致,所以不要依赖编译器的默认行为,主动对齐最保险。
5.3 日志IO阻塞:高频系统的隐形杀手
日志在高频系统里是个矛盾的存在。完全不打印日志,排查问题如同大海捞针;每条消息都打印,IO开销又可能延误主链路的处理。这套源码的解决方式是双层日志:核心链路的关键事件只记录到内存环形缓冲中,不写入磁盘;只有发生异常或达到某个阈值时才把内存中的日志批量刷到磁盘。这种设计既保证了诊断信息不会丢失,又不会阻塞交易链路的前进。
我自己的习惯是:核心链路里只做计数,不做日志格式化。比如订单发送成功,就某个原子计数加1;订单发送失败,才走慢路径打日志。配合监控系统定时拉取计数器数据,既能实时观察系统状态,也不会产生过多的日志IO开销。如果一定要在核心链路打印日志,务必使用异步日志,并且设置背压阈值,防止日志队列无限堆积拖垮整个进程。
5.4 无锁队列的偶发丢消息问题
无锁队列最怕的不是性能差,而是丢消息。在生产消费模型中,如果队列满时生产者选择了丢弃,就会造成行情或订单数据缺失。排查这类问题时,第一步确认生产者的写入是否在队列满时被拒,第二步确认消费者的读取频率是否跟得上。我遇到过一次行情数据偶发缺失的情况,队列大小配置为1万,平时毫秒级就能消费完,但遇到极端行情时行情消息突增,消费者处理不过来,队列满后就丢消息了。
解决方案有两个方向:一是增大队列容量,给消费者留出足够的缓冲空间;二是引入优先级机制,重要的消息类型可以插队,低优先级的消息可以丢弃。实际操作中,我更推荐双管齐下:核心消息不允许丢弃,必须阻塞生产或使用旁路缓冲;非核心消息可以丢弃并记录计数。这个配置要做到系统里可调,不能写死。
5.5 调试工具与实战建议
高频系统的调试和普通后端调试有很大区别。普通代码可以打断点、可以慢慢看日志,但交易系统在真实行情下断点一停,所有行情就错过了,甚至可能因为交易中断产生风险。所以我在调试时优先使用gdb的批量命令和非侵入式采样,避免长时间停住进程。
推荐的调试工具组合有两个。第一是gdb + core dump,适用于进程崩溃后的现场还原,核心命令是bt和info threads,重点看崩溃时各线程的调用栈和栈变量。第二是perf + flamegraph,适用于性能瓶颈定位,采集call-graph后生成火焰图,能非常直观地看到哪个函数占用了最多CPU周期。另外提醒一下,编译时要保留调试符号,即cmake配置为RelWithDebInfo模式,不然perf火焰图显示的全是乱码地址,没法分析。
用perf采集call-graph的基本命令:
perf record -F 99 -g -- ./hft_main --config=hft.conf perf script -i perf.data > out.perf # 再用火焰图脚本生成 svg stackcollapse-perf.pl out.perf > out.folded flamegraph.pl out.folded > flamegraph.svg-F 99表示每秒采样99次,这个频率在CPU开销和采样精度之间比较平衡。采样时间建议至少跑1分钟,数据才足够稳定。
5.6 实战中的三条核心经验
最后分享三条我在实践里踩了无数次坑才真正理解的经验。第一条内存布局决定性能这条之前反复强调过。很多人优化代码时只关心算法复杂度,很少关注内存布局和cache友好性,但高频系统恰恰是缓存敏感型系统,可能只是一个字段位置的调整就是几倍的性能差距。第二条是把“锁”去掉不只要改代码,还要改思路。无锁编程要求开发者在设计阶段就思考数据归属和所有权,而不是等代码写完再做性能优化。什么时候可以用锁、什么时候必须用无锁、什么时候用原子变量就够,这些需要清晰的判断。第三条是性能测试一定要在仿真环境下反复跑,并且把结果记录下来。没有基线数据,后续优化跟进无从谈起;有基线数据,每次改动后跑一次对比,性能退化一眼就能看到。
这套C++高频交易源码程序算不上行业顶尖,但作为一套完整可运行的参考系统,它涵盖了低延迟交易系统的所有关键环节。代码本身是起点,真正值钱的是代码背后对延迟、确定性、内存布局这些概念的掌控力。如果你也想往这个方向发展,我的建议很简单:先把源码里的无锁队列和内存池吃透,然后自己动手重写一遍,再去做压测和优化。这个过程本身,比任何教程都有用。
本文还有配套的精品资源,点击获取