高频交易中的 C++:从市场数据到确定性延迟
简介
高频交易有一种简化技术论证的方法。在许多软件领域,系统可以保持受人尊敬,同时将低效率隐藏在规模、硬件预算或慷慨的响应时间预期背后。在HFT中,缓慢的代价是高昂的。不稳定会损害策略质量、模糊诊断并削弱对整个堆栈的信任。该领域迫使理论对时间做出回应。
这就是为什么 C++ 在交易系统中仍然如此重要。该语言之所以能够生存下来,是因为 HFT 反复要求 C++ 仍然提供异常良好的属性组合:对内存布局的控制、精确的性能工作、成熟的本机工具、深度操作系统和网络集成,以及通过数十年在压力下构建的真实系统积累的大量实践知识。
人们很容易将其简化为一个口号,例如C++很快。但这个口号太小了。 HFT 并不奖励抽象的原始速度。它奖励从市场数据到决策到订单传输的整个路径上的确定性行为。平均延迟有所帮助,但可预测的延迟更有帮助。一个偶尔表现出色但经常紧张的系统通常比一个稍慢但始终易于理解的系统更糟糕。更深层次的故事是,C++ 仍然是构建低延迟系统的最强大的语言之一,这些系统的行为可以在细节上进行塑造、测量和纠正。
为什么 HFT 不断返回 C++
按时竞争的交易堆栈关心大多数其他领域可以模糊的细节。热路径上发生了多少次分配?哪些数据一起存在于缓存中?哪个线程运行在哪里?有多少队列跳跃将数据包到达与策略逻辑分开?解析器是否占用了过多的内存?网关是否跨核心迁移?所谓无害的日志记录或标准化步骤是否会扩大延迟分布的尾部?这些不是装饰性的问题。他们就是工作。
C++ 仍然是这项工作的自然家园,因为它让工程师直接面对这些细节。该语言不会在整个系统上强制采用一种分配模型、一种排队故事、一种所有权故事或一种运行时调度程序。这种自由对于粗心的团队来说是危险的,但 HFT 是有纪律地使用这种自由创造真正优势的地方之一。成熟的贸易组织并不想对机器提出善意的要求。他们想确切地知道机器被告知要做什么以及成本到底隐藏在哪里。
还有一个生态系统的争论比人们承认的更重要。 HFT 是一个语言、工具和体验问题。 C++ 配备了成熟的编译器、分析器、火焰图、硬件计数器工作流程、消毒剂支持、操作系统级集成模式,以及来自邻近性能关键行业的长期继承。人工智能助手越来越多地受益于同样的公共遗产。当工程师寻求帮助改进解析器、收紧队列或解释本机热路径中的分析输出时,C++ 周围的历史密度仍然是一个重要的优势。
市场数据事件的真实体验
它有助于将一个市场数据事件想象为通过机器移动的物理负担。包裹到达。它必须从网络堆栈或提要处理程序接收、解析、映射为某种内部表示、应用于一个或多个账簿结构、通过策略逻辑观察、通过风险检查进行过滤,并且可能转换为出库订单或取消订单。如果一切顺利,这条链条感觉是瞬间的。如果架构不小心,数据包会在每一步都会获得权重。
这里有一个额外的分配,那里有一个共享队列,复制的规范化过程超出了应有的范围,一本在教科书意义上很优雅但在内存中很冷的书籍结构,一个仅出于安全考虑的日志路径,一个在错误时刻迁移的线程:这些成本单独来看都不是神话。它们的危险在于积累和重复。 HFT工程师学会以这种累积的方式思考,因为系统会惩罚乐观情绪。当乘以市场活动、策略频率和可预测反应时间的业务重要性时,每个事件的微小效率低下就会变得很大。
这也是为什么交易中的热门路径很少只是一种功能。这是一种生态。市场数据、状态管理、调度、序列化、风险和传输都是相互作用的。工程师只优化最迷人的循环,同时忽视协调和布局,通常会生产出在片段中进行良好基准测试的系统,但在唯一重要的地方(完整路径)令人失望。
确定性延迟是一门架构学科
短语“低延迟”经常被用作描述函数的属性。在严肃的HFT中,低延迟是架构的一个属性。它是从整个系统的形成方式中产生的。热点数据应该保持热点。内存所有权应该是显而易见的。线程应该有意识地放置,而不是随意放置。共享的可变状态应该受到怀疑。队列只应在必要时才存在。可观察性应该足够便宜,以便系统可以保持可检查性,而不会陷入自身的诊断中。
数据布局很重要,因为机器仍然通过内存移动,而不是通过意图。连续的布局、紧凑的书籍表示以及反映访问模式而不是程序员情绪的结构比看起来可重用但将热状态分散到各处的巧妙抽象更有价值。分配规则很重要,因为热路径上的动态内存可能会产生抖动、争用以及与运行时其余部分的令人惊讶的交互。在HFT中,抖动往往是更令人羞辱的问题。线程值得同样严肃对待。更多线程并不自动意味着更高的性能。有时它们意味着更多的协调、更多的缓存移动、更多的亲和性错误以及更多的操作系统成为非自愿共同作者的地方。成熟的交易系统会刻意固定线程,尊重相关的NUMA边界,并将共享决策的数量保持在架构允许的范围内尽可能低。这并不会让代码感觉很时尚。它使行为更加稳定,这通常更有价值。
网络、解析和书籍维护
交易中的网络路径值得尊重,因为它是最容易出现抽象的地方。二进制提要是状态变化的流,必须忠实且快速地解释。解析器越快,下游混乱的空间就越小。它执行的分配和分支越少,就越容易理解机器的付费目的。正是由于这个原因,提要处理代码通常看起来很简朴。它通过痛苦了解到哪些形式的优雅是市场不奖励的。
订单簿维护具有类似的特征。当一本书可以在负载下更新、查询、重播和推理时,它就具有价值。可重玩性在这里比外人有时想象的更重要。 HFT 团队通过重放真实流量、比较各个版本的策略行为以及诊断系统在何处变慢或不稳定来了解大量信息。难以重放或检查的书籍表现形式可能在狭窄的测试中仍然显得很快,但操作上却很弱。在交易中,快速和可诊断击败了快速和神秘。
这就是 C++ 特别适合的地方。它允许相同的代码库流畅地为解析器、内存感知数据结构、分析工具和低级操作系统行为提供支持。其他语言可以参与交易系统,而且许多语言都可以参与,但是当所讨论的子系统本身就是热路径时,C++ 仍然提供控制和生态系统支持的最佳组合之一。
风险、重放和运营成熟度
将 HFT 想象为剥夺治理的纯粹速度是错误的。如果世界上最快的路径可能发送错误的指令、无法恢复状态或在市场波动后变得无法解释,那么它也是毫无用处的。因此,良好的交易系统会保持明确的风险检查、排练故障处理以及重放基础设施以接近日常工程生活。这些不是官僚的附属品。它们是竞争力的一部分。
健康的 HFT 代码库通常反映了这种成熟度。它包含廉价的可观察性而不是没有可观察性。它包含重播工具,因为团队知道无法重播的内容就无法充满信心地改进。它包含查看整个路径的基准测试和分析器,包括精心挑选的有用的微内核。它将部署一致性、编译器设置、关联策略和机器配置视为一流的工程问题。换句话说,最好的交易系统是严格的技术环境。
这就是稳定性常常胜过原始聪明的原因之一。实验室基准的微小改进不如一个可重复的系统有价值,该系统的尾部是可以理解的,其馈送处理是可以解释的,其策略行为可以在事后重建。进入HFT的工程师有时会期待英雄事迹。成熟的团队往往修炼的反而是一种冷静严谨。他们消除了惊喜。市场已经提供了足够的这些。
常见的神话应该被淘汰
有几个神话之所以流传下来,是因为它们奉承工程师。有人说 HFT 性能主要与手写汇编或深奥的微优化有关。事实上,最有意义的胜利来自于建筑、测量和重复清除普通废物。另一种说法是,无锁结构自然是优越的。有时他们是完全正确的。有时,他们将复杂性和内存排序成本引入到更简单的设计会表现更好的地方。第三个人说更多的线程总是有帮助的。在低延迟系统中,额外的并发性降低可预测性的速度比提高吞吐量的速度还要快。
还有一个现代神话认为,HFT 中继续使用 C++ 肯定主要是历史惯性。历史当然很重要,但在一个不断用金钱和时间来衡量系统的领域中,仅靠惯性是无法生存的。 C++ 仍然存在,因为团队不断发现该语言、其工具及其周围的工程文化仍然与确定性低延迟设计的现实保持良好契合。如果另一种语言在最热门的交易路径上持续创造更好的结果,HFT 公司会注意到。他们有足够强烈的动机去关注。
为什么这个领域仍然值得研究
即使对于从未在贸易公司工作过的工程师来说,HFT 仍然是一位有价值的老师,因为它使系统真相难以避免。它迫使代码和结果之间建立密切的关系。它告诉我们,数据布局不是装饰,队列不是空闲的,平均延迟可能是谎言,重播是一种理解形式,架构通常是最重要的优化。这些经验教训远远超出了交易范围。
C++ 仍然是该课程的中心,因为它使工程师能够保持困难的平衡。它具有足够的表现力来构建实质性的系统,足够低的水平来诚实地暴露成本,并且足够古老,可以继承大量的工具和生活实践。这种组合在我们拥有的最苛刻的性能领域之一仍然很重要。
HFT 的激励部分是提醒我们软件可以在压力下变得精确、可测量和有尊严。 C++ 仍然是该学科使用最流利的语言之一。
动手实验室:构建一个小型的 feed-to-book 重放最后让我们制作一个微型 HFT 风格的玩具。它不会赚钱。那太好了。大多数承诺赚钱的代码示例都是以最糟糕的方式具有教育意义的。
它将做的事情更有用:将一系列市场更新重播到内存中的一个微小的书籍表示中,并报告最佳的出价和要价。
HFT```cpp
include <algorithm>
include <chrono>
include <cstdint>
include <iostream>
include <limits>
include <string>
include <vector>
enum class Side { Bid, Ask };
struct Update { Side side; int price; int qty; };
struct Book { std::vector<Update> bids; std::vector<Update> asks;
void apply(const Update& u) {
auto& side = (u.side == Side::Bid) ? bids : asks;
auto it = std::find_if(side.begin(), side.end(), [&](const Update& x) {
return x.price == u.price;
});
if (u.qty == 0) {
if (it != side.end()) side.erase(it);
return;
}
if (it == side.end()) {
side.push_back(u);
} else {
it->qty = u.qty;
}
}
int best_bid() const {
int best = 0;
for (const auto& b : bids) best = std::max(best, b.price);
return best;
}
int best_ask() const {
int best = std::numeric_limits<int>::max();
for (const auto& a : asks) best = std::min(best, a.price);
return best;
}
};
int main() { std::vector<Update> replay{ {Side::Bid, 10010, 5}, {Side::Bid, 10020, 3}, {Side::Ask, 10040, 4}, {Side::Ask, 10035, 8}, {Side::Bid, 10020, 0}, {Side::Ask, 10035, 6}, {Side::Bid, 10025, 7} };
Book book;
const auto t0 = std::chrono::steady_clock::now();
for (const auto& u : replay) {
book.apply(u);
}
const auto t1 = std::chrono::steady_clock::now();
const auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count();
std::cout << "best_bid=" << book.best_bid() << "\n";
std::cout << "best_ask=" << book.best_ask() << "\n";
std::cout << "replay_ns=" << ns << "\n";
}
在 Linux 或 macOS 上:```bash
g++ -O2 -std=c++20 -o tiny_book main.cpp
./tiny_book
```在 Windows 上:```powershell
cl /O2 /std:c++20 main.cpp
.\main.exe
```### 这教给你什么
即使是这个微小的重播程序也会很快引发真正的HFT问题:
* 价格水平应该存在于向量、地图、数组还是自定义阶梯中?
* 当重播从 7 次更新增加到 700 万次时会发生什么?
* 状态更新与报告花费了多少时间?
* 如果结构动态扩展,分配会出现在哪里?
例子虽小,但问题一点也不小。
## 爱好者测试任务
1. 将 HFT 中的线性搜索替换为可扩展性更好并比较重放时间的结构。
2. 生成一百万个综合更新并测量原始结构如何退化。
3. 在 feed 重放和书籍更新之间添加一个生产者线程和一个带有 SPSC 队列的消费者线程,然后比较稳定性和复杂性。
4. 将重放线程固定到 Linux 上的核心并比较运行之间的差异。
5. 故意添加嘈杂的日志记录路径,并观察“无害”的调试决策对延迟测量的影响有多快。
这些练习很简单,但这正是它们的好处。真正的低延迟工程是由许多简陋的结构构建而成的,这些结构要么是精心选择的,要么是后来后悔的。
## 总结
C++ 仍然是高频交易的核心,因为 HFT 的目的是在从市场数据到订单传输的整个路径上构建确定性低延迟系统,然后使这些系统足够易于理解,以便在压力下进行诊断。这项工作取决于严格的数据布局、有限的分配、仔细的线程、诚实的分析、可重播的验证以及重视稳定性和速度的文化。
这就是 C++ 继续坚守阵地的原因。它为工程师提供了该领域仍然值得奖励的控制水平、工具深度和历史实践。其他语言可以而且确实对交易堆栈做出了贡献,但是当问题是热路径本身时,C++ 仍然是我们所知的将性能从口号转变为可重复的工程属性的最强大方法之一。
## 参考文献
1. 纳斯达克 TotalView-ITCH 规范:C++
2.DPDK文档:ITCH
3. Linux 套接字 API 手册页:API
4. Linux 时间戳文档:[https://docs.kernel.org/networking/timestamping.html](https://docs.kernel.org/networking/timestamping.html)
5. Linux PTP 硬件时钟基础设施:[https://docs.kernel.org/driver-api/ptp.html](https://docs.kernel.org/driver-api/ptp.html)
6. Linux Linux 手册页:[https://man7.org/linux/man-pages/man1/perf.1.html](https://man7.org/linux/man-pages/man1/perf.1.html)
7. Brendan Gregg 的火焰图:[https://www.brendangregg.com/flamegraphs.html](https://www.brendangregg.com/flamegraphs.html)
8.英特尔 VTune Profiler 文档:[https://www.intel.com/content/www/us/en/docs/vtune-profiler/overview.html](https://www.intel.com/content/www/us/en/docs/vtune-profiler/overview.html)
## 当系统已经处于压力下时会是什么样子
C++ 高频交易往往在团队希望季度安静的时刻变得紧迫。一项功能已经呈现在客户面前,或者一个平台已经具有内部依赖性,并且系统选择了特定的一周来揭示其优雅的理论和运行时行为已经礼貌地分开生活。这就是为什么这么多严肃的工程工作都是从协调开始的。团队需要将其认为系统所做的事情与系统在负载、变化和截止日期下实际所做的事情相协调,这会让每个人都稍微更有创造力,但稍微不那么明智。
在低延迟系统中,最重要的情况通常是市场数据摄取、确定性预算下的订单路由以及用于回归控制的重放和分析工作流程。这些情况会带来技术、预算、信任、路线图,有时甚至是声誉后果。当多个团队依赖技术问题并且没有人能够完全解释为什么它不断产生噪音、延迟和成本时,技术问题在政治上就会变得更大。
这就是为什么我们建议从运营压力和交付现实的角度来看待这个问题。设计可能在理论上很漂亮,但在操作上却是毁灭性的。另一种设计可能几乎是无聊的,但却可以使产品继续前进多年,因为它是可测量的、可修复的,并且诚实地进行权衡。认真的工程师学会更喜欢第二类。它减少了史诗般的演讲,也减少了紧急回顾,每个人都用被动语态说话,没有人记得谁批准了这条捷径。
## 始终如一的实践
第一个持久的做法是不断衡量一条有代表性的路径。团队通常会在定义将改变决策的信号之前收集广泛的遥测数据。选择真正重要的道路,反复衡量,拒绝让讨论变成装饰性的故事讲述。在高频交易中 C++ 的工作中,有用的衡量标准通常是尾部延迟、队列规则、缓存局部性和操作可重复性。一旦这些决定变得可见,其余的决定就会变得更加人性化,不再那么神秘。第二个持久的做法是将证据与承诺分开。在系统得出结论之前,工程师常常被迫说方向是正确的。抵制这种压力。首先建立一个狭义的证明,尤其是当主题与客户或金钱密切相关时。经过验证的小改进比未经验证的大目标具有更大的商业价值。这听起来很明显,直到季度末审查将假设变成最后期限,并且整个组织开始将乐观情绪视为日程安排工件。
第三个持久的做法是用所有权语言撰写建议。一段说“提高绩效”或“加强界限”的段落在情感上令人愉快,但在操作上毫无用处。一个段落说明了谁更改了什么、按什么顺序、在什么回滚条件下更改,这些内容实际上在周一早上仍然存在。这是很多技术写作失败的地方。它希望听起来先进,而不是可调度。
## 节省时间的反例
当地的成功并不能证明已经准备好迎接更艰难的环境。在扩展这个想法之前,团队必须升级测量规则并证明相同的行为在更大的压力下仍然存在。
另一个反例是工具通货膨胀。一个新的分析器、一个新的运行时、一个新的仪表板、一个新的代理、一个新的自动化层、一个承诺协调旧包装器的新包装器。这些事情本质上都不是坏事。问题是当他们被要求补偿一个没有人明确命名的边界时会发生什么。然后,系统就会变得更加仪器化、更加令人印象深刻,并且只是偶尔更容易理解。团队很快就能感受到这一点。即使没有这样的措辞,他们也能察觉到堆栈何时成为决策的昂贵替代品。
第三个反例是将人工审查视为自动化的失败。在实际系统中,人工审查通常是保持自动化在商业上可接受的控制措施。成熟的团队知道在哪里积极自动化以及在哪里保持批准或解释可见。不成熟的团队希望机器做所有事情,因为“一切”在幻灯片中听起来都很高效。然后第一个严重事件到来,突然之间,带着转换体验的诚意重新发现了手动审核。
## 我们推荐的交付模式
好的工作首先是通过足够强大的技术阅读来减轻压力,以阻止循环辩论。下一个有界实施改进了一条重要路径,并且复测使工程和领导层能够清楚地了解方向。这个顺序比确切的工具选择更重要,因为它将技术技能转化为前进的动力。
实际上,我们建议缩小第一个周期:收集工件,产生一个硬诊断,发布一个有限的更改,复测真实路径,并用简单的语言编写下一个决策。简单的语言很重要。团队很少会后悔清晰度。团队常常会在收据到达之前后悔给自己留下了深刻的印象。
这也是语气很重要的地方。强大的技术工作听起来应该像以前遇到过生产一样。冷静、精确、精确地进行权衡和证据。该音调携带操作信号。它表明该团队了解系统工程的古老真理:机器速度很快,路线图很脆弱,迟早要为每一个允许保持诗意的假设付出代价。
## 在准备就绪之前我们将使用的清单
在低延迟系统中,准备状态不是一种情绪。这是一个带有后果的清单。在我们称高频交易中的C++工作准备好进行更广泛的推广之前,我们希望以尽可能最好的方式让一些事情变得无聊。我们想要一条在代表性负载下表现可预测的路径。我们想要一组不矛盾的测量结果。我们希望团队知道边界在哪里以及打破它意味着什么。我们希望工作的输出足够清晰,以便实施室之外的人仍然可以从中做出正确的决定。
该清单通常涉及尾部延迟、队列规则、缓存局部性和操作可重复性。在昂贵的意外事件投入生产之前,使用该清单来测试解释质量、现场弹性和回滚清晰度。
这也是团队发现他们是否正在解决真正的问题或只是在其周围排练能力的地方。许多技术努力都感觉很成功,直到有人要求提供可重复性、生产证据或会影响预算的决定。在那一刻,弱的工作变得模糊,而强的工作变得异常简单。平淡就好。平淡通常意味着系统已经不再依赖魅力。
## 我们建议如何谈论结果
最终的解释应该足够简短,以便在领导会议上通过,并且足够具体,以便在工程审查中通过。这比听起来更难。过于技术性的语言隐藏了顺序。过于简化的语言隐藏着风险。正确的中间立场是以一种听起来平静而不是胜利的方式描述路径、证据、有限的变化以及下一步建议的步骤。
我们推荐这样的结构。首先,说明评估了什么路径以及它为何重要。其次,说出该路径的错误或不确定之处。第三,说明更改、测量或验证的内容。第四,说出尚未解决的问题以及下一步投资将购买什么。这种结构之所以有效,是因为它尊重工程和购买行为。工程师想要细节。团队需要排序。当下一步明确时,每个人都会受益。以这种方式说话的隐藏好处是文化。清楚地解释技术工作的团队通常也会更清楚地执行它。他们不再将模糊性视为复杂性。他们变得更难用行话打动人心,而更容易通过困难的系统获得信任。这是工程成熟度中最被低估的形式之一。
## 我们仍然拒绝伪造的内容
即使系统改进后,成熟的团队也会在低延迟系统中保持不确定性。弱测量需要更清晰的证据,硬边界需要简单的语言,更平静的演示需要真正的操作准备。必须减少一些不确定性;有些必须诚实地命名。令人困惑的是这两项工作是如何令人尊敬的项目变成了昂贵的寓言。
同样的规则也适用于高频交易中围绕 C++ 的决策。如果团队仍然缺乏可重复的基准、值得信赖的回滚路径或关键接口的明确所有者,那么最有用的输出可能是更明确的“否”或更窄的下一步,而不是更大的承诺。这种纪律使技术工作与其要改进的现实保持一致。
以这种方式工作会有一种奇怪的轻松感。一旦系统不再依赖乐观的故事讲述,工程对话就会变得更简单,即使工作仍然很困难。在生产中,这通常被视为一种小形式的恩典。
## 关于高频交易系统的附加说明
在HFT中,当设计在压力下的表现与在检查下的表现相同时,它就会赢得尊重。这比营销部门希望的要少见,也比幻灯片中优雅的伪数学更有价值。确定性延迟不是口号。这是当硬件、编译器、内核或工作负载以某种微小且侮辱性的方式发生变化时,正确做出并重新检查上千个无聊选择的结果。
我们还建议将重播和交易后调查视为堆栈的一等公民。无法解释自身的快速系统会成为代价高昂的文化问题。交易者想要速度。工程师想要真相。合规部门需要记录。最好的 C++ HFT 系统尊重所有三个,而不假装它们是同一个对话。这种平衡是 C++ 在这里仍然如此重要的原因之一:它使团队能够精确控制行为,同时为周围的纪律留出足够的空间,使速度变得可信。