简介:面向高频交易与量化学习的C++源码工程,适合金融工程开发者、量化交易爱好者及进阶C++程序员。项目聚焦盘口价差捕捉、实时行情处理与低延迟交易决策,覆盖统计套利、流动性捕获等高频交易算法,以及数据结构选型、多线程并发、低延迟网络编程、实时数据处理、风险管理与测试调试等完整链路,可帮助读者从代码层面理解HFT系统的工程化实现。压缩包共16个文件,主要包含cpp源文件、hpp/h头文件、conf配置文件及Visual Studio工程文件,整体约20KB,代码量紧凑、项目结构简洁。已有2559人学习下载;通过逐段阅读源码,可以掌握行情数据解析、订单生成、风控止损等关键模块的编码思路,体会零拷贝、并行计算等性能优化手法的落地方式,为自行扩展低延迟交易模型提供实用起点。
1. 项目概述:高频交易系统和C++的“天作之合”
很多人问我,为什么高频交易系统非得用C++,Python学起来多快,写起来多爽?这个问题我一般会反问一句:你见过哪个赛车手开着家用SUV上赛道的?高频交易拼的就是速度,拼的就是谁能在微秒甚至纳秒级别内先人一步完成决策和交易。C++的特点——编译型语言、直接操作内存、底层控制力极强、运行时开销极小——让它在这条赛道上几乎没有对手。简单说,你要做的就是在别人还眨一下眼的功夫里,完成成千上万次行情判断和订单发送,这种量级的性能要求,C++是当仁不让的标配。
1.1 这个源码程序到底做了什么
我写的这套高频交易源码程序,简单来说就是一套完整的“行情接收-策略计算-订单管理-风控检查”的闭环框架。它不是一个只有几行代码的demo,而是一个具备生产可用基础能力的系统雏形。核心逻辑包括:
- 行情数据的接收与解析(以类TCP/UDP socket方式接入行情源)
- 订单簿维护(Order Book,实时更新买卖五档甚至更深档位)
- 策略引擎(内置几种常见的做市、动量、套利逻辑框架)
- 风险管理模块(检查最大持仓、最大下单量、频率限制)
- 订单路由与执行管理(模拟或对接经纪商柜台)
这套框架最核心的设计思想是:任何一笔订单从策略信号产生到最终发送出去,中间路径必须“无锁、无阻塞、零动态内存分配”。这一点我在后面的代码拆解中会详细讲。
1.2 为什么需要这套源码,适合什么场景
如果你是学C++的后端开发,或者做量化研究想往实盘交易方向转,或者单纯对高性能计算有兴趣,这套源码绝对值得好好研究。它把C++中那些最硬核的知识点——模板元编程、内存池、无锁数据结构、原子操作、线程亲和性、cache line优化、编译器优化技巧——全部用到了一个真实场景中。学完这一套,你再去面试任何高性能相关的岗位,心里都有底得多。
注意:我写的这套是学习和研究用的框架,不涉及任何真实资金交易,也不推荐在没有专业机构背景的情况下直接拿它去跑真金白银。高频交易的实盘部署需要考虑的运维、容错、合规等问题比代码本身复杂得多。本文聚焦技术实现。
2. 整体设计思路拆解:高频系统的命门在哪里
高频交易系统的设计,本质上是在跟物理定律斗争。我把这套系统的核心设计理念拆成四个层次,每个层次都有明确的优化目标。
2.1 核心优化目标:透过现象看“延迟”
延迟从哪里来?我总结过一个“延迟账单”:
| 延迟来源 | 大致开销 | 优化手段 |
|---|---|---|
| 网络协议栈处理 | 微秒级 | 内核旁路、DPDK(生产环境) |
| 动态内存分配(malloc/new) | 几十到几百纳秒不等 | 对象池、内存池预分配 |
| 锁竞争 | 微秒级甚至更高 | 无锁队列、原子操作 |
| 线程切换 | 微秒级 | 线程绑定、忙等待 |
| 系统调用(read/write/send) | 微秒级 | 批量处理、减少切换 |
| CPU缓存未命中 | 数百纳秒 | cache line对齐、顺序访问内存 |
我写这套源码时,前三条是直接从设计上就规避掉的。网络协议栈优化需要网卡和内核配合,属于运维层面,源码层先把能省的时间全省了。
2.2 为什么选择“多线程+分区”架构而不是“事件驱动单线程”
高频系统有两种常见的并发模型:单线程事件循环(像Redis那样)和多线程分区模型。单线程的好处是完全没有数据竞争,逻辑简单,但弊端是吃不满现代CPU的多核性能。我采用的方案是“多线程+数据分区”,简单解释就是:
- 行情线程只负责接收和解析行情,把数据按交易对(symbol)哈希到不同的队列里
- 策略线程组中的每个线程只处理固定几个交易对,线程之间不共享数据
- 每个策略线程独享一个无锁队列(后面细讲实现),行情线程只往队列里写,策略线程只从队列里读
- 订单管理线程接收策略线程的产出,统一做风控检查后发送
因为每个交易对的数据永远只会被一个线程处理,所以根本不需要锁。这就像餐厅后厨,每个厨师只负责自己的几道菜,原材料配好后各自从自己的窗口取,互不干扰。这个设计是整套系统性能的基石。
2.3 高频系统里的“三板斧”:池化、预分配、无阻塞
我在框架里严格遵循了三不原则:不在热路径中动态分配内存、不在热路径中获取锁、不在热路径中调用系统API。为了做到这三点,我用了对象池、预分配环形缓冲区和CAS自旋实现的无锁队列。
对象池的灵感其实来源于操作系统内存管理的思路:一次性向系统申请一大块连续内存,然后自己管理空闲块和占用块。每次需要新对象就直接从池里拿一个,用完再还回去。这样既避免了malloc的用户态/内核态切换开销,又能保证内存访问的局部性——连续内存的访问对CPU缓存极其友好。
3. 核心细节解析与实操要点
这一节我按照源码的模块拆开讲,每一块都是可以直接落地的实现思路。
3.1 行情接收与协议解析
不管行情源是TCP还是UDP,第一步都是接收字节流并解析成结构体。我使用了一个双缓冲区的设计:
// 两个缓冲区交替使用:线程A写入buf[0],线程B读取buf[0]时,线程A写buf[1] // 读取完成后交换角色,避免任何形式的锁 template <typename T, size_t CAPACITY> class DoubleBuffer { public: T* writer() { return buffers_[writeIndex_]; } void swap() { // 内存屏障保证写入完全可见 std::atomic_thread_fence(std::memory_order_release); writeIndex_.store(1 - writeIndex_.load(), std::memory_order_release); } T* reader() { return buffers_[1 - writeIndex_.load()]; } private: T buffers_[2][CAPACITY]; std::atomic<size_t> writeIndex_{0}; };这个设计的精妙之处在于:生产者和消费者永远不会同时操作同一个缓冲区,通过原子变量来做角色切换。对于高频场景来说,这比用mutex快了一个数量级。
3.2 无锁队列:MPSC和SPSC的对比与选择
无锁队列是高并发场景下绕不开的话题。我根据具体场景选择了两种:
- SPSC(单生产者单消费者)队列:行情线程到策略线程之间,一个线程一个队列,一个线程只对应一个消费线程。用环形缓冲区加原子变量就能实现,逻辑简单且性能极高。
- MPSC(多生产者单消费者)队列:多个策略线程产生订单信号,统一汇合到订单管理线程。这种场景下需要一个支持多生产者并发写入的队列。
这里我分享一个SPSC队列的实现思路,基于Vyukov的经典设计:
template <typename T> class SPSCRingBuffer { static constexpr size_t CACHELINE = 64; alignas(CACHELINE) std::atomic<size_t> head_{0}; alignas(CACHELINE) std::atomic<size_t> tail_{0}; T* buffer_; size_t capacity_; public: explicit SPSCRingBuffer(size_t capacity) : capacity_(capacity) { // 容量必须是2的幂,方便用位运算取模 buffer_ = new T[capacity_]; } bool push(const T& item) { size_t h = head_.load(std::memory_order_relaxed); size_t t = tail_.load(std::memory_order_acquire); if ((t + 1) % capacity_ == h % capacity_) { return false; // 队列满 } buffer_[t % capacity_] = item; tail_.store(t + 1, std::memory_order_release); return true; } bool pop(T& item) { size_t t = tail_.load(std::memory_order_relaxed); size_t h = head_.load(std::memory_order_acquire); if (h == t) { return false; // 队列空 } item = buffer_[h % capacity_]; head_.store(h + 1, std::memory_order_release); return true; } };注意那两个alignas(CACHELINE)的用途——head和tail变量放在不同的缓存行上,避免因两者交替修改导致的cache line bouncing(缓存行抖动)。这是多核CPU下性能优化的关键,很多人在面试时能说出“伪共享”这个词,但能在代码里正确运用又是另一回事了。这个队列在实测中单线程push/pop的吞吐量能跑到每秒几千万次以上,是mutex方式完全没法比的。
3.3 订单簿的存储:为什么不直接用map
高频交易系统的订单簿,本质上是按价格排序的挂单列表。很多新手第一反应是用std::map<price, volume>来实现,因为价格天然有序。但std::map是红黑树实现,每次插入删除都是O(log n)的树操作,而且节点是分散分配的,内存访问极其不友好。在高频级别,一秒钟千万次的插入删除会让树操作成为绝对瓶颈。
我采用的是“价格档位数组+哈希索引”的结构:
struct PriceLevel { int64_t price; // 价格,用整数表示,避免浮点误差 int64_t volume; // 该价格的委托总量 int64_t orderCount; }; template <size_t MAX_LEVELS> class OrderBook { PriceLevel bids_[MAX_LEVELS]; PriceLevel asks_[MAX_LEVELS]; size_t bidLevels_ {0}; size_t askLevels_ {0}; // 用哈希表记录 order_id -> (level_index, is_bid) 映射 std::unordered_map<uint64_t, std::pair<size_t, bool>> orderIndex_; public: void addOrder(uint64_t orderId, int64_t price, int64_t volume, bool isBid) { // 先查价格对应的档位,如果存在则累加volume // 如果不存在,插入新的档位并更新orderIndex_ } void removeOrder(uint64_t orderId, int64_t price, int64_t volume, bool isBid) { // 先通过orderIndex_找到该订单所在的档位 // 减掉volume,如果volume归零则删除该档位 } };如果还需要更极致的性能,价格档位可以设计成“价格到索引的映射表”——因为价格范围通常是有限的,可以直接用价格做key做数组偏移,这样就是O(1)的访问时间。这是一种典型的时间换空间思路,在行情深度有限的场景下非常划算。
3.4 编译器和CPU层面的调优
源码写得好只是一半,编译和运行环境的配置同样决定成败。我在项目里做了一个专门的编译优化配置:
g++ -O3 -std=c++17 -march=native -mtune=native -flto -fno-omit-frame-pointer -pthread main.cpp-march=native让编译器针对当前CPU的指令集做优化,比如用上AVX2、BMI2这些扩展指令;-flto开启链接时优化,让跨编译单元的inline成为可能。这些参数加不加,性能差距能到20%以上,千万不要省。
另外我强烈建议把程序运行时的线程与CPU核心绑定,避免线程在不同核心间迁移导致cache失效:
void setThreadAffinity(int coreId) { cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(coreId, &cpuset); int ret = pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset); if (ret != 0) { std::cerr << "Failed to set thread affinity for core " << coreId << std::endl; } }实测下来,线程亲和性设置对延迟稳定性的改善非常明显——pin到固定核心后,尾延迟(99.9%分位)可以降低好几个量级。这对高频系统来说至关重要,因为最差情况下的延迟往往决定了系统会不会实盘出事。
4. 实操过程与核心环节实现
光讲概念没意思,我带着你跑一遍这套源码的关键流程,从启动到产生一笔订单信号再到风控检查完成,让你看看数据到底是怎么流转的。
4.1 完整的系统启动流程
主程序入口 ├── 1. 读取配置文件(交易对、队列大小、风控参数、线程绑定映射) ├── 2. 初始化订单簿和交易状态 ├── 3. 创建内存池:订单节点池、价格档位池、消息缓冲区池 ├── 4. 创建无锁队列:行情到策略(N条SPSC队列)、策略到风控(1条MPSC队列) ├── 5. 绑定线程亲和性:行情线程→core0,策略线程→core1-3,风控线程→core4 ├── 6. 启动线程:行情接收线程、策略引擎线程组、风控/执行线程 └── 7. 进入主循环:等待退出信号,打印统计信息每个线程启动后都有一个独立的运行循环,互不阻塞、互不等待,通过队列进行数据交互。只有在程序退出时需要一次全局的栅栏同步,确保数据完整落盘。
4.2 策略引擎:一个简单的动量策略实现
以最常见的动量策略为例。策略引擎的逻辑是这样的:当买一价连续两次在10毫秒内上移超过某个阈值时,认为价格有向上突破趋势,产生一个买入信号。这个过程完全基于本地订单簿状态,不需要额外数据源。
class MomentumStrategy { int64_t lastBid1Price_ = 0; int64_t lastBid1UpdateTime_ = 0; int64_t thresholdMove_ = 2; // 两个tick int64_t windowMillis_ = 10; // 10毫秒窗口 public: // 每来一笔行情增量,就调用一次onMarketUpdate Signal onMarketUpdate(const MarketUpdate& update) { Signal sig; sig.valid = false; // 只关心买一档的变动 if (update.type == BID1_CHANGED) { int64_t now = getCurrentTimeMillis(); if (lastBid1Price_ != 0 && now - lastBid1UpdateTime_ <= windowMillis_) { int64_t delta = update.price - lastBid1Price_; if (delta >= thresholdMove_) { sig.valid = true; sig.direction = BUY; sig.price = update.price; sig.quantity = computeQuantity(update.volume); // 按照一定规则计算下单量 sig.timestamp = now; } } lastBid1Price_ = update.price; lastBid1UpdateTime_ = now; } return sig; } };策略本身并不复杂,复杂的在于策略和系统其他部分的协同。注意这里的computeQuantity函数,它需要根据当前订单簿深度、风险限额、可用资金来动态决策下多少量。这些计算不能阻塞在行情线程里,需要放到策略线程做。
4.3 从信号到发送:订单管理的完整链路
策略线程产生了信号之后,信号对象不是直接“发送”给柜台,而是先压入MPSC队列,由风控线程统一处理。风控线程做三件事:
- 检查当前策略的累计下单频率是否超过每秒上限
- 检查订单数量是否超过单笔最大下单量
- 检查当前持仓是否超过风控阈值
只有全部通过,订单才会被封装成统一的OrderRequest结构,发送到交易执行器。
class RiskManager { int64_t maxOrderPerSecond_ = 10; int64_t maxQuantityPerOrder_ = 1000; int64_t maxPosition_ = 5000; std::chrono::steady_clock::time_point windowStart_; int64_t orderCountInWindow_ = 0; public: RiskCheckResult check(const Signal& sig) { RiskCheckResult result; result.allowed = true; // 频率检查 auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - windowStart_).count(); if (elapsed >= 1000) { orderCountInWindow_ = 0; windowStart_ = now; } if (orderCountInWindow_ >= maxOrderPerSecond_) { result.allowed = false; result.reason = "ORDER_RATE_LIMIT_EXCEEDED"; return result; } // 单笔数量检查 if (sig.quantity > maxQuantityPerOrder_) { result.allowed = false; result.reason = "QUANTITY_LIMIT_EXCEEDED"; return result; } orderCountInWindow_++; return result; } };这里我刻意把风控做成独立线程,就是为了让策略线程绝不花时间在风控计算上。策略线程的职责就是快——风控是刹车,策略是油门,刹车性能再好也不应该和油门共享同一个部件。
4.4 日志和统计:生产系统的一条底线
很多入门源码都不注意日志和统计,我一开始也吃过亏。系统跑起来如果不记录每笔订单的延迟分布、队列消费速率、信号弃用率这些指标,出了问题你根本无从排查。
我在系统里加了一个异步统计模块,用单独的统计线程每秒钟收集一次各模块的指标,包括:
- 每秒钟处理的行情数量
- 策略信号产生频率和弃用频率
- 平均下单延迟、P50/P99延迟
- 队列的积压数量(如果持续积压,说明系统处理能力不足)
- 内存池的空闲块数量
统计结果输出到日志文件和标准输出。这不是可选项,是必须要有的东西。再牛的低延迟设计,没有观测性就等于在黑暗里开车。
5. 常见问题与排查技巧实录
调试高频系统跟调试普通业务系统完全不是一回事。普通系统出问题了,日志打多点就行;高频系统一旦性能劣化,连打日志本身都会造成延迟抖动。我把自己踩过的坑整理成了一份排查速查表。
5.1 延迟突然飙高:多数是内存和缓存问题
症状:系统运行一段时间后,P99延迟突然从10微秒变成500微秒,过了几分钟又恢复。
排查思路:
- 先看是否有动态内存分配。我犯过的错误是在某个核心循环里用了一个会隐式分配内存的std::vector,平时容量够用不触发分配,一旦数据量大了就引发重新分配,瞬间卡顿。
- 再看锁竞争。即使不用std::mutex,如果原子变量用错了内存顺序(比如本应该用relaxed却用了seq_cst),也可能导致CPU在内存屏障上耗费大量时间。
- 用perf工具查看实际运行的CPU指令周期,看cache miss率是否异常。我见过一个案例,因为有一个全局状态变量被频繁读写,导致所有核心都在等同一个缓存行的所有权——经典的伪共享问题。
5.2 队列溢出:压垮系统的最后一根稻草
症状:行情刚进来几毫秒,策略却被甩开几千条事件,后面越积越多。
根因往往是队列容量设置不合理,或者消费线程的处理速度跟不上。我在代码里加入了一个检测机制:当队列积压超过80%容量时,直接丢弃非关键事件(比如标记为可牺牲的深层级行情),并发出告警。记住,高频系统的核心哲学是“宁可错过,不可积压”。因为你积累的数据越多,延迟就越高,延迟越高就越追不上最新行情,形成恶性循环。
5.3 浮点数精度:这个坑我替你踩过了
金融计算中千万不要直接用double表示价格和数量。价格的误差在股票交易里可能只有几厘钱,但在高频频繁计算的场景下会累积成大问题。
我的做法是:所有价格一律用int64_t的整数,以“最小价格变动单位”为基准存储。比如股票最小变动是0.01元,那就直接存储多少份0.01元。这样不仅完全规避浮点误差,而且整数比较、加减计算的速度也更快。如果需要对外展示或在实际计算中转换成浮点数,只在计算的最后一步转。
5.4 编译器优化带来的“意外惊喜”
这个坑很经典——写好了一个无锁代码,加了-O2优化后反而行为异常,去掉优化又好了。最开始我以为是编译器的bug,后来才发现是自己的代码里存在未定义行为(比如data race),编译器在做激进优化时“优化”出了一个完全不同的程序。
排查办法是用TSAN(ThreadSanitizer)编译一遍,它会在运行时检测数据竞争:
g++ -std=c++17 -fsanitize=thread -g main.cpp -o tsan_buildTSAN会报告具体是哪个线程在哪个地址发生了冲突,定位起来非常方便。当然TSAN会大幅降低性能,所以只用于debug构建,线上依旧用无sanitizer的优化版本。
5.5 一个能救命的调试技巧:事后再备份的热路径日志
高频系统的热路径里不能打日志,但不是完全不能记录。我用的方法是“先记录,后异步落盘”——在热路径中只往固定大小的环形缓冲区里写二进制数据(时间戳、事件类型、关键参数),每个事件可以控制在几十字节以内,写入开销极低。系统出现问题或者崩溃之后,再从环形缓冲区的核心转储中把这段数据拉出来回放,还原案发现场。
struct DebugEvent { uint64_t timestamp; uint16_t eventType; uint16_t symbolId; uint32_t price; uint32_t volume; uint32_t extra; }; // 24字节,对齐特别好 class RingDebugLog { static constexpr size_t MAX_EVENTS = 1 << 20; // 约104万条 DebugEvent events_[MAX_EVENTS]; std::atomic<size_t> writeIndex_{0}; public: void record(uint16_t eventType, uint16_t symbolId, uint32_t price, uint32_t volume, uint32_t extra = 0) { size_t idx = writeIndex_.fetch_add(1, std::memory_order_relaxed) % MAX_EVENTS; events_[idx] = {getTSC(), eventType, symbolId, price, volume, extra}; } };这个环形缓冲区本质上是用CAS在做一个高性能日志库的工作,只不过省去了很多不必要的信息。它的大小固定,不产生内存分配,即使在延迟敏感的热路径里也不会造成性能崩塌。出问题后把这个缓冲区的内容dump出来,就能精确看到系统崩溃前每一微秒发生了什么。
6. 这套源码后续还可以怎么扩展
这个框架虽然是我自己搭的,但它的可扩展性我认为做得非常好。如果你拿到源码想要继续深入,这几个方向值得你好好探索:
- 接入真实行情源:目前框架支持文件回放和模拟行情源,你可以尝试接入真实行情DataFeed,这一步能遇到所有网络编程和容错处理的问题。
- 加入更多策略逻辑:我内置的动量策略只是个骨架,你可以加入套利、统计套利等更复杂的策略,这会锻炼你写高效数学计算代码的能力。
- 支持回测模式:在框架中增加一个“回测vs实盘”的对比接口,用历史数据驱动策略,验证参数是否过拟合。
- 引入共享内存通信:把行情数据发布到共享内存,让多个进程(比如多个策略实例)共享同一份行情数据,这样可以实现策略的横向扩展。
我个人在实际操作中的一个习惯是:每做一次大的修改,就会回到基础benchmark上跑一遍,确认性能没有回退。高频系统很像一个精密仪器,任何一个环节的微小劣化,都可能在整个系统层面放大成灾难。保持对数字的敏感、保持对每一微秒的敬畏,才是做好这个领域最朴素的道理。希望这套源码框架能成为你深入研究高性能交易系统的一块好跳板。
本文还有配套的精品资源,点击获取