高频交易这个名字本身就是一个误导。它真正难的不是「频率高」,而是「单笔决策的端到端延迟必须可预测、可压缩、可监控」。一支日内成交一万笔的中频策略,如果平均决策延迟 200 微秒,方差正常,是「中频高吞吐」;一支一天只发 50 笔单的策略,如果每一笔都必须在交易所撮合引擎打出行情后 5 微秒内挂出,那才是「高频」。区分二者的不是次数,而是有没有为「每一纳秒」付工程代价。
这一篇要把这件事拆开讲:从一个 tick 离开撮合引擎到自家订单回到撮合引擎的这段「tick-to-trade」时间,时间究竟花在哪里、每一段能砍多少、需要付出什么样的硬件与软件代价;在系统软件层,内核旁路(kernel-bypass)解决了什么、解决不了什么;在硬件层,FPGA 与 ASIC 把哪一段路径直接踢出了 CPU;以及作为一个用 Python 做研究的量化工程师,应当在这条延迟链里站在哪一段、不该幻想自己能站在哪一段。
上一篇《做市策略》讨论了报价的逻辑层,假定订单可以瞬时挂出与撤出;本篇要把这个假定还原回真实硬件。下一篇《交易系统架构》会从 HFT 的纳秒级单机设计退一步,讨论一般量化机构的多机分布式交易系统应该怎么搭。三篇连起来覆盖「策略意图 → 单机执行极限 → 多机生产部署」三个层级。
代码示例使用 Python 3.11、numpy 1.26、numba 0.59、timeit 标准库。本系列约定所有实操代码用 Python,HFT 一章不例外,但会反复明确:真实生产 HFT 系统不是用 Python 写的,本文的 numba 代码只用于演示数据布局与算法骨架,绝不能照搬到生产路径。
风险提示:本文出现的所有架构、延迟数字、硬件型号、软件库名仅用于阐释 HFT 工程方法论本身,不构成任何投资建议,也不构成任何硬件采购或服务商选型建议。文内引用的延迟数量级是公开资料中可查证的典型值范围,不代表任何具体机构、具体合约、具体撮合引擎的真实测量结果。把示例代码搬到生产前,请重新核对操作系统版本、内核参数、NIC 驱动、CPU 型号、PCIe 拓扑、交易所协议规范。HFT 对资金、人员、合规均有极高门槛,零售投资者和小型机构不应将本文视为「自建低延迟系统」的可行性论证。
一、HFT 的定义与边界
「高频」是一个需要工程化定义的词。从合规、学术、工程三个角度看,得到的定义并不一致,写策略的人和搭系统的人也常常各说各话。本节先把边界画清楚,避免后面「优化错了对象」。
一点一、按延迟分级
工程上最实用的分级是按 tick-to-trade 的量级划分:
- 超低延迟(ultra low latency, ULL):tick-to-trade 一般在 1 微秒以下,关键路径几乎完全跑在 FPGA 上,CPU 只做参数下发和事后分析。代表场景是做市商的报价更新(quote update)和最简单的延迟套利(latency arbitrage)。这一档的工程预算非常高,全栈定制硬件。
- 低延迟(low latency, LL):tick-to-trade 在 1 至 10 微秒,主要逻辑跑在 CPU 上,依赖内核旁路、busy-poll、cache 友好的数据结构。多数 HFT 自营商的主力策略落在这一档。
- 中频(medium frequency):tick-to-trade 在 100 微秒至几毫秒,操作系统正常调度、网络栈走内核也无所谓,可以用 C++ 或 Java 写,单元测试比延迟更重要。本系列前面 19 至 24 章涉及的回测引擎、执行算法、SOR 都属于这一档。
- 低频(low frequency):tick-to-trade 不重要,决策周期分钟级以上,Python 完全够用。
把目标先定到具体哪一档,决定了后面要做什么、不做什么。一个「分钟频率因子策略」声称「也要低延迟」是没意义的:信号每分钟刷新一次,订单从挂出到成交有秒级 fill rate,整个 tick-to-trade 就是几百微秒还是几毫秒,对最终 PnL 完全没有可观察的影响。
一点二、速度优势的来源
HFT 的盈利来源不是「比别人聪明」,而是比别人早。在一个有限的时间窗口里——通常是一个公共信息事件发生到这个信息被广泛定价——谁先到、谁就拿走超额收益。这条「速度优势」的来源可以分成三种:
第一种是信息速度优势:某个市场的价格变化先被另一个市场吸收。最经典的例子是芝商所(CME)的标普期货与 NYSE 的 SPY ETF 之间的指数套利(index arb),两个市场之间的信息传播速度由芝加哥到纽约的物理距离决定。在 2010 年微波链路被铺设之前,信号通过光纤传播需要 13.1 ms 左右;铺设微波后压缩到约 8.5 ms;后续叠加激光与中继塔,压到 8 ms 以内。
第二种是队列优势(queue priority):在限价单簿(limit order book)的同价位上,先挂的单先成交。能够在一个新价位出现的瞬间就挂上,意味着抢到了这个价位的整段队列;后到的所有人,要么排在你后面,要么必须更激进的价格才能成交。做市商策略的核心生命线就是这个。
第三种是取消速度优势:当行情显示「我已经被人挑了」(adversely picked off)时,第一时间撤掉剩余报价比第一时间挂出新报价更值钱。一个做市商挂买卖两档,被人吃掉买档之后,必须立即上调卖档报价,否则两档都会被吃。如果吃单方比你快 200 纳秒,你就吃不到这一笔的双边利润。
这三种速度优势对硬件的要求不一样:信息速度优势主要靠地理与物理介质(colocation、微波);队列优势主要靠接收侧延迟(NIC、行情解码、本地簿更新);取消速度优势主要靠发送侧延迟(信号、风控、订单序列化、NIC TX)。系统设计时把目标策略对应到哪一种,决定了哪一段路径要重点优化。
一点三、什么不是 HFT
把这几个边界说清楚,避免误把别的事当成 HFT 来做:
- 高频日内策略不等于 HFT。一支日内换手 20 次的均值回归策略,每天发 200 笔单,根本不需要纳秒级。它需要的是稳定的执行算法(VWAP、Implementation Shortfall)和良好的成交跟踪。
- 「快速量化」不等于 HFT。研究侧用 GPU 做特征工程、用 Rust 做回测引擎、用低延迟 RPC 做信号分发,这些都很「快」,但只要决策周期是秒级以上,就不需要内核旁路与 FPGA。
- 市价单不等于 HFT。零售客户挂市价单也是「立即成交」,但延迟是百毫秒级,与 HFT 无关。
- 「最快的 Python」不等于 HFT。numba、Cython、PyPy 能把热路径压到几百纳秒以内,但 Python 解释器的内存分配、GIL、引用计数让它永远无法达到 ULL 档次。这点本文第八节会回头详细说。
把 HFT 的边界定在「tick-to-trade < 10µs,且为了这个目标愿意付出 colocation / kernel-bypass / FPGA 任意一项的工程代价」,本文后续讨论都围绕这个定义展开。
一点四、谁在这条赛道上
把 HFT 行业的玩家分成几类,能帮助理解后面工程决策的市场背景:
- 自营做市商(proprietary market makers):Jane Street、Citadel Securities、Virtu、Jump Trading、Flow Traders、Optiver、Tower Research、IMC、SIG。这一类是 HFT 流量的主体,单纯靠双边报价赚价差与 rebate,对延迟最敏感。
- 统计套利与跨市场套利:DRW、HRT(Hudson River Trading)、Two Sigma 部分策略、Quantlab。延迟敏感但往往低于纯做市档次,常用 LL 而非 ULL 系统。
- 大型投行的电子化做市部门:高盛、摩根士丹利、JPMorgan 的内部做市台。系统比纯自营商重,更看重风控与合规,延迟略让步。
- 交易所与做市鼓励项目:交易所发放的「指定做市商」资格附带 rebate 与速度优势,门槛是必须保持一定报价覆盖率。
了解这些玩家有助于理解延迟竞赛的真实分布:金字塔顶端十几家自营商在拼 100 至 300 纳秒级;下面一圈几十家在 1 至 5 微秒级;再下一圈几百家在 10 微秒级。每往下一档,硬件成本掉一半,但 PnL 池也按市场结构重新切分。
一点五、HFT 的盈利上限
HFT 这条赛道不是无穷蛋糕。关键事实:
- 市场总价差是有限的。一只股票每天的总买卖价差乘以成交量,就是所有做市商分掉的最大盘子。这个数随波动率上升、随股票成交量上升,但与 HFT 团队数量无关。新进玩家只能挤压老玩家的份额。
- 速度优势的边际收益在递减。从 50 微秒压到 5 微秒,PnL 大约翻倍;从 5 微秒压到 500 纳秒,可能再翻一倍;从 500 纳秒压到 100 纳秒,工程成本爆炸增长,PnL 增量却只有几个百分点。这条曲线决定了大多数玩家停在哪一档。
- 行业 PnL 长期下行。NYSE / Nasdaq 流动性最高的几百只股票的有效价差从 2010 年的几个 bp 压到 2020 年前后的不到 1 bp,HFT 行业整体收入随之下降。这是为什么 HFT 团队近年在向加密、衍生品、新兴市场扩展——找还没被压榨干净的流动性池。
把这两条放进决策框架,结论就清楚:「我们也要做 HFT」之前,先评估目标市场的总价差池有多大、目前有几个量级的玩家、自己能站到哪一档、按当前规模能切到多少 PnL。多数情况下,得到的答案是「不值得」。
二、延迟预算分解
低延迟工程的第一步永远是画延迟预算瀑布图:把一个事件从开始到结束的每一段可观察时间都列出来,按贡献排序,确定哪一段值得花成本去优化。这一节给出一个典型 colo 部署下的 tick-to-trade 分解,作为后面所有工程决策的参照系。
图中的环节按照事件发生的物理顺序排列。下面把每一段的可优化空间和代价讲清楚。
二点一、撮合引擎打包到光纤离开
这一段在交易所内部,外部用户无法干预。从撮合引擎完成事件到行情数据完成多播打包并进入光纤,典型耗时几百纳秒。不同交易所差异显著:纳斯达克的 ITCH 5.0 协议设计紧凑,打包延迟可控制在 250 纳秒级;某些老旧交易所协议冗余、字段繁多,打包延迟可达微秒级。
这一段的「优化」对外部用户而言不是优化代码,而是选交易所、选数据源、选直连协议而不是聚合行情。例如选择直连交易所原生 multicast 而不是某个聚合商的整合 feed,能直接砍掉聚合商内部的转发延迟(往往是几百微秒到几毫秒)。
二点二、光纤跨连
物理光纤传输延迟由光速与光纤折射率共同决定,标准单模光纤(SMF-28)的群速度约为
c/1.4682 ≈ 2.04 × 10⁸ m/s,对应每米 4.9
纳秒。colocation 机房内部的 cross-connect 长度通常在 5 至 50
米之间,对应 25 至 250 纳秒延迟。
跨城传输的差异更显著:芝加哥到新泽西的直线距离约 1180 公里,光纤路径因绕行约 1300 公里,对应单程光纤延迟约 6.4 ms;同一段路径用微波链路(通过中继塔,几乎走直线)单程约 4.05 ms;空气中的光速比光纤中快 47%,所以微波 + 激光路径比纯光纤少约 35%。这就是芝加哥与纽约之间「微波套利」的技术基础。
二点三、NIC 接收到用户态
数据包到达 NIC 后,需要经过 PHY → MAC → DMA → 内存 → 用户态读取的路径。在传统 Linux 网络栈下,这一段会经历内核中断、协议栈处理、socket 缓冲区拷贝,端到端延迟约 5 至 20 微秒。对 HFT 而言这是不可接受的。
内核旁路技术把数据包直接 DMA 到用户态预先 pin 住的内存,由用户态轮询读取,这条路径可以压到 500 至 800 纳秒。具体技术第四节展开。
二点四、行情解码与本地簿维护
行情解码(feed decoding)把二进制 wire format(如 ITCH、SBE、OUCH)解析成内部事件结构,本地簿(local order book)维护把这些事件应用到本地维护的限价单簿上,得到「策略可以查询的状态」。这两步加起来在 1 至 2 微秒之间。
优化空间在于:避免任何形式的内存分配、字符串处理、虚函数调用。事件结构必须是 plain old data,本地簿用扁平数组按价格索引(price level array),而不是平衡树或哈希表。具体数据结构第七节展开。
二点五、信号决策与前置风控
这是策略真正动脑的一段。简单的延迟套利、统计套利信号几十条机器指令就能算完,对应几十到几百纳秒;复杂的因子组合可能需要矩阵乘法或者小型神经网络推理(neural network inference),延迟急剧上升,超过 10 微秒就基本退出 ULL 档次。
前置风控(pre-trade risk check)必须在订单出门前完成,包括:单笔最大数量、价格带(fat-finger 检查)、当前持仓限额、单位时间报单速率(rate limit)、自成交防止(self-trade prevention)。每一项都可以做成 SIMD 并行的几纳秒检查,但累积起来仍可能贡献几百纳秒。
二点六、报单序列化与 NIC TX
把决策结构序列化为交易所协议的报单消息,再通过内核旁路写入 NIC TX 队列。序列化部分典型 200 至 500 纳秒,TX 延迟约 500 至 800 纳秒。FPGA 可以把这一整段卸载到硬件,将 send 路径压到 100 纳秒级。
二点七、抖动与尾延迟
平均延迟只是一个起点。HFT 系统真正关心的是尾延迟(tail latency):一千次决策里最慢的那 10 次有多慢、一万次里最慢的那 1 次有多慢。原因是市场行为在统计意义上是「事件驱动」的——绝大多数行情你不需要响应,少数关键事件(公告、订单簿失衡、对手大单)需要在第一时间响应;而尾延迟正好就是这些事件最容易踩中的区间。
工程上要追踪的几条尾延迟曲线:
- p50:中位数延迟,反映「日常」性能。
- p99:百分位 99,反映偶发抖动。
- p99.9:千分位,开始受 GC、上下文切换、cache flush 影响。
- p99.99:万分位,受系统级事件(kernel housekeeping、PCIe 错误重传、网络重路由)影响。
- max:最坏情况,往往由不可避免的硬件级事件决定。
一支健康的 HFT 系统的延迟分布应该是「尖头窄尾」:p50 低、p99 / p99.99 与 p50 的比值不超过 3 至 5 倍。如果尾巴拉到几十倍以上,说明热路径上还有未被根除的抖动源——堆分配、cache miss、IRQ、调度。
二点八、不可控段与可控段
承接上一节,把每段环节按「能不能动」分类:完全不可控(撮合引擎内部、对方光纤、撮合引擎入队),部分可控(光纤长度,依赖机房选址;NIC 收发,依赖硬件选型),完全可控(用户态软件路径)。HFT 优化的实际工作量 80% 落在「完全可控」一段,因为只有这一段能持续投入工程师改进;硬件改造的回报陡峭但有上限。
延迟预算分解最重要的产出不是数字,而是优先级:哪一段贡献最多、改动一纳秒的成本是多少、当前还有多少改进空间。把这张表更新到每周的工程例会上,比任何「我们要做 HFT」的口号都管用。
二点九、几条参考延迟
为了给读者一个数量级的锚点,下面列出几条公开资料中常见的典型值(不代表任何具体机构的真实测量):
| 环节 | 顶级 ULL(FPGA) | 高端软件(EF_VI) | 普通软件(DPDK) | 内核栈(普通 Linux) |
|---|---|---|---|---|
| NIC RX 到用户态 | 100–200 ns | 500–800 ns | 1–2 µs | 5–20 µs |
| 行情解码 + 本地簿 | 50–150 ns(硬件) | 800–1500 ns | 1.5–3 µs | 5–10 µs |
| 信号 + 风控 | 50–100 ns(硬件) | 500–1500 ns | 1–3 µs | 不适用 |
| 订单序列化 + TX | 100–200 ns | 500–800 ns | 1–2 µs | 5–20 µs |
| 端到端 tick-to-trade | 300–700 ns | 3–6 µs | 6–12 µs | 30+ µs |
这张表的用法是反向选档:先看自家策略对延迟的真实需求(一笔下单延迟从 6 µs 降到 3 µs,PnL 增量是多少),再看自家工程能力能站到哪一档,最后核对这一档对应的硬件与人力成本。任何不做这个核算就喊「我们要做最快」的项目,最后大概率会停在「钱花了一半、延迟没下来」的烂尾状态。
三、网络与硬件
物理与链路层是 HFT 的下界:再优秀的软件也跑不过光速。这一节按「机房 → 跨城 → 时钟」三层讨论网络硬件的选择。
三点一、Colocation 与 Cross-connect
Colocation(简称 colo)指的是把自己的服务器放在交易所的撮合引擎所在的同一栋数据中心里,机柜与撮合引擎之间用预先布线的光纤直连(cross-connect)。主流交易所对应的 colo 设施大致是这样:纳斯达克在新泽西卡特里特(NY11,运营商 Equinix);纽交所在新泽西马瓦(NJ2,运营商 ICE);芝商所在伊利诺伊州奥罗拉(CH1);伦交所在 LD4(运营商 Equinix);东证在共立 KDC1。
colo 的核心承诺是「等延迟(equidistant cabling)」:每一个客户机柜到撮合引擎的光纤长度严格相等,避免不同客户因为机柜物理位置不同而产生几十纳秒的优势。这是法规与交易所规则的硬约束,违反会被处罚。
但这条规则只覆盖 cross-connect 长度。客户能在 colo 内部做的优化包括:选择支持 PCIe Gen4 的服务器、选择最短的内部跳线、选择延迟更低的 ToR 交换机(top-of-rack switch)甚至直接消除中间交换机走光纤直插。
colo 的费用结构是显著的:每月每机柜数千至数万美元,每条 cross-connect 数百美元/月,10G/40G 端口费另算。一个三十台服务器的 colo 部署,年度运营成本通常在数百万美元量级,这是 HFT 与一般量化机构的第一道资金门槛。
三点二、跨城网络:微波、激光、卫星
跨地域的链路差异决定了跨市场套利的可行性。下面给出几条典型链路的可实现单程延迟(来自公开论文与新闻报道,仅作量级参考):
- 芝加哥(CME)↔︎ 新泽西(Nasdaq、NYSE):光纤路径约 1300 km,单程约 6.4 ms;微波约 4.05 ms;微波 + 激光(McKay Brothers、Quincy Data 等运营商)约 4.0 ms。
- 伦敦(LD4)↔︎ 法兰克福(FR2):光纤约 4.5 ms;微波约 2.55 ms。
- 纽约 ↔︎ 伦敦:海底光缆 Hibernia Express 约 30 ms;目前没有商用化的跨大西洋微波链路(因海洋曲率与中继塔限制),有人提议用低轨卫星(如 Starlink 类网络),但工程化到 HFT 级别尚未实现。
微波链路的工程缺点是带宽极低、可用性受天气影响。一条微波链路典型带宽 10 Mbps(与光纤的 100 Gbps 相差四个数量级),只能传精简后的关键字段(如某几个标的的最优买卖价、CME E-mini 期货合约价)。雨天与雾天信号衰减显著,必须有光纤作为备用。
激光链路(free-space optical)作为微波的补充,带宽更高、抗干扰更好,但只能短距离视距传输(几公里以内),所以一般用于「最后一英里」连接微波塔与机房。
三点三、硬件时间戳与 PTP 时钟
低延迟系统的一个隐性门槛是时间戳精度:你必须能区分两个事件谁先谁后,且偏差远小于事件间隔。微秒级系统需要纳秒级时钟同步。
PTP(Precision Time Protocol,IEEE 1588) 是事实标准。它的工作方式是:网络中有一个 grandmaster 时钟(通常由 GPS 接收机驱动),通过专门的时间同步报文将时间分发给所有 slave;每一跳交换机都需要支持 PTP transparent clock 或 boundary clock,对报文驻留时间做硬件级补偿;NIC 必须支持硬件时间戳(hardware timestamping),在数据包真正进入 / 离开 PHY 时由硬件打上时间戳,而不是由 CPU 在用户态打。
实现良好的 PTP 网络可以把全网时钟偏差控制在 100 纳秒以内。HFT 系统会把每一个行情包到达时间、每一笔订单发送时间、每一笔成交回报时间都用硬件时间戳记录,事后用这些时间戳重建因果链:哪一笔行情触发了哪一笔决策、决策耗时多少、订单到撮合引擎前后耗时多少、是否被对手抢先。
不做硬件时间戳的代价是所有延迟分析都不可信。CPU
上调用 clock_gettime(CLOCK_REALTIME)
的精度受系统调度抖动影响,可能产生几微秒的噪声,这个噪声会淹没你想测量的纳秒级差异。
三点四、NIC 选型
主流低延迟 NIC 阵营有四家:
- Solarflare(被 Xilinx 收购,现在属于 AMD):旗舰型号 X2 系列、X4 系列。配合 OpenOnload 或 EF_VI(更底层)API。ef_vi 路径下端到端延迟可压到 600 纳秒以下,是软件 HFT 的事实标准之一。
- Mellanox(被 NVIDIA 收购):ConnectX-6/7 系列,配合 VMA 或 DPDK。
- Exablaze(被思科收购,停产,存量仍有):FastPath 系列,集成 FPGA 的智能 NIC。
- Napatech:以可编程智能 NIC 与 FPGA SmartNIC 见长。
选型的考量除了延迟,还有:是否支持硬件时间戳、是否支持多队列与 RSS、是否支持 inline FPGA 卸载、驱动稳定性、原厂支持响应速度。HFT 团队通常会在生产中并存两家供应商以降低单点故障。
三点五、PCIe 拓扑与 NIC 摆放
NIC 的选型只是一半,怎么把 NIC 插到主板上同样决定延迟。现代服务器有多颗 CPU、每颗 CPU 提供数十条 PCIe lane,分到不同的 PCIe 插槽。错误的 NIC 摆放会让数据从 NIC 进入后绕道:先穿过 PCIe,再走 UPI 到另一颗 CPU 的内存控制器,再回到本应处理它的核。这条「跨 socket」路径会多花几百纳秒,而且抖动巨大。
正确做法:
- NIC 与处理它的 CPU 同 NUMA 节点:用
lspci -vv | grep -A2 NUMA或cat /sys/bus/pci/devices/<addr>/numa_node确认。 - 优先用直连 CPU 的 PCIe Gen4 / Gen5 插槽,避开经过 PCIe 交换机(PCH)转发的插槽。
- PCIe lane 数足够:100G NIC 至少 PCIe Gen3 ×16 或 Gen4 ×8,否则带宽是瓶颈。
- 关闭 PCIe ASPM(Active State Power Management):低功耗模式会让 PCIe 进入 L1 状态,唤醒延迟以毫秒计。
三点六、机房选址的商业现实
colocation 设施名义上对所有客户「等延迟」,但实际可获得的资源仍有差异。例如:
- 机柜电力配额:高密度机柜(每柜 10 kW 以上)需要排队,新客户可能拿不到。
- cross-connect 数量:每条 cross-connect 都要付月费,并占用「光纤入口面板」的端口,端口紧张时大客户优先。
- 可视范围:colo 机房不允许客户随意进出,所有上架、维护必须通过 remote hands;响应时间从几小时到几天不等。
- 跨地域同步部署:CME 的 CH1、Nasdaq 的 NY11、ICE 的 NJ2 三个机房同步上架对一个跨市场套利团队是基础要求,单点上架基本没意义。
把这些纳入决策表,HFT 的「硬件成本」远不止服务器和 NIC 的单价。一个完整的多机房部署,年化运营成本(机柜 + cross-connect + 跨城带宽 + 微波租赁 + 机房 remote hands)能轻松到千万美元量级。
三点七、灾备与多活
低延迟系统的灾备设计与一般业务系统不一样。一般业务追求「跨机房热备 + 自动切换」,但跨机房切换本身需要几百毫秒到几秒,这对 HFT 是致命的。HFT 的灾备模式更接近「就地降级 + 同机房多副本」:
- 同一交易所的同一策略,部署到同机房的两台或三台服务器上,各自独立连接交易所、独立计算、独立报单,但只有「主」机器的订单生效,「备」机器只跑影子流。
- 切换由人工触发或由独立的健康检查器触发,目标是 1 秒内完成。期间策略停止报新单,已挂单由 hard kill 撤回。
- 跨机房灾备只用于「灾难性事件」(机房断电、网络中断),不是日常切换。
- 监控网络与交易网络物理分离:交易网络只跑撮合相关流量;监控、配置、日志走另一个 NIC、另一个 VLAN,避免监控流量污染热路径。
这种设计的隐含假设是「停机比下错单便宜」。HFT 的小时级停机损失也许是几万美元,但一次错单可能直接毁掉整个公司(如 2012 年 Knight Capital 的 4.4 亿美元事件)。「停机优先」是底层共识。
四、内核旁路
普通 Linux 网络栈对一个 64 字节 UDP 包的端到端延迟通常在 5 至 20 微秒区间,且抖动(jitter)可达数十微秒,原因是数据包要穿过:硬件中断 → 软中断(softirq)→ 协议栈(IP / UDP)→ socket 缓冲区 → 用户态 read 的多次拷贝与上下文切换。HFT 必须把这条路径换掉,统称为「内核旁路(kernel-bypass)」。
四点一、DPDK
DPDK(Data Plane Development Kit) 由 Intel 主导发起,目前是开源社区中最广泛使用的内核旁路方案。它的核心思想是:
- 用户态驱动:把 NIC 从内核驱动解绑,挂到 DPDK 的 PMD(poll-mode driver),所有收发由用户态轮询完成,没有中断。
- 预分配内存池:所有网络缓冲(mbuf)从大页(huge page)预分配的内存池里取,避免运行时分配。
- CPU 亲和性(affinity)与隔离(isolcpus):网络处理线程绑死在特定 CPU 核上,与内核调度隔离。
- 批处理 API:一次系统调用处理多包,摊薄单包开销。
DPDK 的优势是开源、生态广、对所有主流 NIC 都有 PMD。劣势是 API 偏向通用网络功能(虚拟交换、负载均衡、DPI 等),HFT 场景下大部分功能不需要,且它的最优延迟(约 1 微秒级)仍高于专用方案。
四点二、Solarflare Onload 与 EF_VI
OpenOnload 是 Solarflare(现 AMD)提供的一个特殊 LD_PRELOAD 库,它的卖点是应用程序不需要改一行代码:原本调用 socket / send / recv 的应用启动时加载 onload,会把这些系统调用拦截到用户态实现,自动完成内核旁路。这一层抽象让既有的 C++ socket 代码无缝从 50 微秒降到 2 微秒级。
但 onload 仍然是一个通用 socket 兼容层,自带一定开销。对最敏感的路径,HFT 会绕过 onload,直接调用 EF_VI 这一更底层的 API。EF_VI 把 NIC 的 RX / TX 描述符环(descriptor ring)直接暴露给用户态,应用自己管理缓冲、自己 poll、自己 doorbell。这条路径可以做到端到端 600 纳秒以下,代价是代码侵入性高,必须按 EF_VI 的模型重构。
EF_VI 的另一个独特能力是 TCPDirect 和 CTPIO(Cut-Through PIO):发包时不通过 DMA,而是 CPU 直接写 PCIe MMIO 区域,省掉 DMA descriptor 的来回;硬件支持的话,还能在写到一半时就开始向网络发送(cut-through)。这种把发送侧再压一两百纳秒的极端优化,是 ULL 档系统的常见做法。
四点三、AF_XDP
AF_XDP 是 Linux 内核 4.18 引入的官方内核旁路方案。与 DPDK 不同,它仍然走内核,但只走极短的一段:数据包从 NIC 进到内核后,立刻在驱动层被 XDP(eXpress Data Path)程序拦截,redirect 到 AF_XDP socket 暴露给用户态的 UMEM(user memory)。
AF_XDP 的优势是:
- 官方维护,不依赖第三方驱动:DPDK 需要自己解绑 NIC,AF_XDP 复用内核驱动。
- 可以与内核网络栈共存:同一块 NIC 可以一部分流量走旁路、一部分走内核,便于和管理面、监控共享接口。
- eBPF 可编程:XDP 程序可以做包过滤、负载均衡,逻辑灵活。
劣势是延迟比 DPDK 略高(仍然有内核驱动的处理),且对 NIC 驱动有 zero-copy 模式的依赖。HFT 内部更多用 AF_XDP 做监控旁路、灰度环境,主路径仍是 DPDK 或 EF_VI。
四点四、内核旁路解决不了什么
内核旁路把数据包从硬件搬到用户态的延迟压低了,但没有改变 CPU 在用户态做事的延迟。如果策略代码里有:
- 内存分配(
malloc、new) - 锁(mutex、spinlock 在竞争时)
- 系统调用(printf、log、文件 I/O、futex)
- 缓存未命中(cache miss)
- 分支预测失败
任何一项都会瞬间贡献几百纳秒到几微秒的不可控延迟。所以内核旁路只是必要条件,不是充分条件。剩下的工作全部落在用户态代码与 CPU 微架构层面。
四点五、典型的 busy-poll RX 循环
把 EF_VI 的接收循环骨架展示一下,便于读者理解「内核旁路 + busy-poll + 零拷贝」三件事在代码里长什么样:
void rx_loop(ef_vi* vi, ef_event* evs, FeedHandler& handler) {
constexpr int BATCH = 16;
while (running) {
int n = ef_eventq_poll(vi, evs, BATCH);
if (n == 0) continue; // 没事件,立刻再轮询
for (int i = 0; i < n; ++i) {
ef_event& e = evs[i];
if (EF_EVENT_TYPE(e) == EF_EVENT_TYPE_RX) {
int rx_id = EF_EVENT_RX_RQ_ID(e);
int len = EF_EVENT_RX_BYTES(e);
const char* pkt = vi->rx_buf(rx_id);
handler.on_packet(pkt, len); // 直接处理用户态指针
ef_vi_receive_init(vi, vi->rx_buf_addr(rx_id), rx_id);
}
}
}
}值得注意的几点:
- 整个循环里没有
read、recvfrom、epoll_wait之类的系统调用。 - 包数据存放在 NIC DMA
直接写入的用户态缓冲,
pkt是这块缓冲的指针,不需要拷贝。 - 处理完后调用
ef_vi_receive_init把缓冲归还,复用。 BATCH = 16是吞吐与延迟的折中:批越大、单包平摊开销越小,但首包等待越长。HFT 一般取小批(4 至 16),优先延迟。
DPDK 的接收循环结构类似,调用
rte_eth_rx_burst、rte_pktmbuf_free,逻辑上一一对应。
四点六、TX 路径的两种模式
发送侧有两种典型实现:
- DMA 发送(descriptor mode):CPU 写一个描述符到 ring,NIC DMA 引擎读取描述符,再 DMA 拉取数据。两次 DMA 操作,延迟典型 500 至 800 纳秒。
- PIO(Programmed I/O,又称 CTPIO):CPU 直接把数据通过 MMIO 写到 NIC 的发送 FIFO。一次 PIO 写完,NIC 不需要再 DMA 拉数据。延迟可压到 200 纳秒以下。代价是 CPU 占用更高,且只适合小包(几十到几百字节)——HFT 订单包正好属于这一档。
ULL 档系统的发送路径几乎都用 PIO;中频系统用 DMA
即可。EF_VI 的 ef_vi_transmit_pio 与 DPDK 的
inline send 都对应这一模式。
五、内存与 CPU 优化
把网络数据包搬进用户态后,下一段时间花在 CPU 上。这一段的优化要回到 CPU 微架构层面:缓存、NUMA、流水线、分支、调度。
五点一、NUMA 拓扑
现代服务器是 NUMA(Non-Uniform Memory Access)架构:每颗 CPU 插槽都有自己的本地内存控制器,跨插槽访问内存(remote NUMA access)需要走 UPI / Infinity Fabric 互联,延迟与带宽都明显劣于本地访问。典型 Intel Xeon 上,本地内存访问约 80 纳秒,跨 socket 访问约 130 纳秒。
HFT 系统的标准做法是:把整个热路径(feed
handler、strategy、order encoder)线程都绑在同一个
NUMA 节点的核上,对应的网络 NIC 也插在同一个 NUMA
节点的 PCIe 插槽(用 lspci -vv 与
numactl --hardware 核对),所有内存(mbuf
池、ring buffer、本地簿)都从这个 NUMA 节点分配。Linux 下用
numactl --cpunodebind=0 --membind=0
启动进程,或在程序内调用 mbind /
numa_alloc_onnode 显式控制。
跨 NUMA 共享数据是延迟杀手。即使所有数据本地,一旦多个核之间有共享缓存行被另一个 NUMA 节点访问,就会触发缓存一致性协议(MESI)的跨 socket 流量,单次抖动几百纳秒。
五点二、缓存友好的数据结构
L1 数据缓存通常 32 至 48 KB,访问 4 至 5 个周期;L2 256 KB 至 1 MB,约 12 周期;L3 共享,几十周期;DRAM 几百周期。把热数据塞进 L1 是 HFT 数据结构设计的第一原则。
具体做法包括:
- 结构体按 cache line(64
字节)对齐:
alignas(64) struct Quote { ... };,避免 false sharing。多线程会写的字段之间用 64 字节 padding 隔离。 - AoS 与 SoA 选择:访问模式是「按对象遍历」用 array of struct,「按字段遍历」用 struct of array。HFT 行情解码典型用 SoA,因为往往一次只关心一两个字段。
- 预取(prefetch):在使用数据前若干周期插入
__builtin_prefetch提示 CPU 拉取,掩盖访存延迟。 - 本地簿用扁平数组:每个价位分配一个
slot,
book[price_idx]O(1) 访问;价位变化用循环数组 + 起始偏移管理,避免移位。 - 无堆分配:所有事件、订单结构走对象池(object pool),生命周期可控。
五点三、Busy-Poll 与抖动
热路径线程不睡觉。它在一个无限循环里轮询输入队列,没事件就立刻再轮询一遍,CPU 占用永远 100%。这与一般服务器编程的「事件驱动 + 阻塞 epoll」哲学相反,但在 HFT 是必须的:任何一次 wakeup(哪怕只是 futex 上的几百纳秒)都意味着一笔订单可能慢半拍。
为了让 busy-poll 线程不被打扰,需要做:
isolcpus内核启动参数:把目标核从默认调度域排除。nohz_full:让该核不接受时钟中断(tickless)。rcu_nocbs:把 RCU 回调推到其他核。- IRQ affinity:所有硬件中断绑到非热路径核(NIC 中断除外,且 NIC 在 busy-poll 模式下也不发中断)。
- 关闭 turbo / C-states:CPU
频率必须固定,避免进入低功耗状态后唤醒延迟。BIOS 与
cpupower frequency-set -g performance。 - 关闭 transparent huge pages 的
khugepaged:
echo never > /sys/kernel/mm/transparent_hugepage/defrag。 - 使用静态分配的 huge pages(1 GB):减少 TLB miss。
抖动(jitter)是 HFT 系统的核心 KPI,通常报告为 p99 / p99.9 / p99.99 延迟,而不是平均值。一个平均 5 微秒、p99.99 50 微秒的系统比一个平均 8 微秒、p99.99 12 微秒的系统差得多——后者更可预测。
五点四、分支与流水线
CPU 流水线深度十几级,分支预测失败会清空整条流水线,代价 15 至 20 个周期。在热路径上:
- 避免难以预测的条件:用 branch-free
写法替换条件赋值(
x = cond ? a : b在编译器优化下可能生成 cmov 指令,比 if-else 更稳)。 - 把热路径放在 if 的 likely 分支:用
__builtin_expect或[[likely]](C++20)提示。 - 避免虚函数与函数指针:每次间接调用都是分支预测的考验,热路径上的多态尽量用模板特化(CRTP)替换。
五点五、SIMD 与位级技巧
行情解码与本地簿更新里有大量并行可做:一次解码 16
个字节、一次比较 8
个价位、一次更新多档买卖盘。AVX-512、AVX2、SSE4
都能用上。HFT 工程师通常手写
intrinsics(_mm256_*),不依赖编译器自动向量化(auto-vectorization),因为后者在数据排布稍有不齐时就失效。
五点六、Linux 调优 checklist
把上面这些操作整理成一份开机脚本/启动 checklist,HFT 服务器上线前需要逐项核对:
- 内核启动参数:
isolcpus=2-15 nohz_full=2-15 rcu_nocbs=2-15 nosoftlockup intel_pstate=disable mitigations=off audit=0 transparent_hugepage=never default_hugepagesz=1G hugepagesz=1G hugepages=64 - BIOS 设置:关闭 Turbo Boost、关闭 C-states、关闭 Hyper-Threading(HT 共享 L1/L2,会污染热路径核的缓存)、关闭 P-states、关闭节能模式(Performance / Maximum Performance)。
- IRQ
affinity:
/proc/irq/<n>/smp_affinity_list设置为非热路径核。 - CPU
频率治理:
cpupower frequency-set -g performance,并核对/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor。 - 进程调度:热路径线程用
SCHED_FIFO实时调度,priority 99;用chrt -f 99启动或在程序内sched_setscheduler。 - 内存锁定:
mlockall(MCL_CURRENT | MCL_FUTURE)防止换页。 - 关闭交换:
swapoff -a。 - 关闭 NMI
watchdog:
echo 0 > /proc/sys/kernel/nmi_watchdog。 - 关闭 RT
throttling:
echo -1 > /proc/sys/kernel/sched_rt_runtime_us。 - 关闭 timer
migration:
echo 0 > /proc/sys/kernel/timer_migration。 - 网络:
ethtool -K eth0 gro off lro off tso off gso off,禁用所有卸载(HFT 自己做)。 - 时钟源:
echo tsc > /sys/devices/system/clocksource/clocksource0/current_clocksource。
每一项漏掉都可能在 p99.99 上产生几微秒到几十微秒的尾巴。运维上最好用 Ansible / Salt 把这套配置固化、版本化,每次硬件更换或内核升级都重跑。
五点七、性能分析工具链
低延迟系统的性能问题都不是「平均慢」,而是「偶尔慢」。对应的分析工具:
perf stat:跑一段时间统计 cycles、instructions、cache misses、branch misses,给出整体微架构画像。perf record+perf report:函数级 CPU 采样,找热点。perf c2c:识别 cache line 共享冲突(false sharing)。perf trace:跟踪系统调用,热路径上应当为零。bpftrace/ eBPF:在内核侧打动态探针,跟踪调度抖动、IRQ、page fault。- Intel VTune / AMD uProf:商业级微架构分析,能看到流水线 stall 的原因(front-end vs back-end vs bad speculation vs retire)。
- 自带时间戳直方图:每条热路径都把每段耗时(行情接收、解码、决策、发送)记录到原子直方图,定时 dump,能看到尾巴的形状。
把这些工具固化到日常工程流程,比单看「平均延迟」有用得多。HFT 团队往往有专门的 performance engineer 角色,常年跟这些工具打交道。
五点八、大页与 TLB
虚拟内存翻译走 TLB(Translation Lookaside Buffer)。x86-64 的 L1 TLB 通常 64 至 128 项,每项映射一页(默认 4 KB)。如果热路径访问的内存超过这个范围,就会发生 TLB miss,每次几十纳秒。
解决方法是大页(huge pages):把页大小从 4 KB 扩大到 2 MB 或 1 GB,单条 TLB 项映射更大的内存范围,覆盖整个工作集。HFT 系统通常这么配:
- 启动参数预留 1 GB
大页:
default_hugepagesz=1G hugepagesz=1G hugepages=64,预留 64 GB。 - 进程通过
mmap+MAP_HUGETLB | MAP_HUGE_1GB申请大页。 - 关键数据结构(环形缓冲、对象池、本地簿)全部分配在大页上。
- 关闭 transparent huge pages,避免后台 khugepaged 在运行时碎片整理。
TLB 命中的影响在尾延迟上尤其显著:一次 TLB miss 触发的多级页表遍历可能踩到 cache cold 区域,最坏情况叠加上百纳秒。把整个热路径工作集塞进 TLB 覆盖范围,能直接砍掉 p99.9 上的一根尾巴。
五点九、CPU 隔离的常见错误
调优文档里常见的几个坑:
isolcpus写了,但用户态进程仍然跑在所有核上:必须配合taskset/numactl/sched_setaffinity显式绑核。- 关了 HT 但 BIOS 又重启时恢复:升级 BIOS 后默认值可能恢复,每次升级要重新核对。
- 关了 turbo 但 CPU 频率仍跳变:还要关 P-states 与 C-states,把 governor 设为 performance。
- 绑了核但 kthread
仍跑在该核:
kthread_cpus配合irq_affinity才能彻底清空。 - 绑核绑到了 HT sibling:HT 关闭时
lscpu看到的核编号会跳变,按物理核而不是逻辑编号绑。
每条都是真实环境里被踩过的坑,每一条留下的都是几微秒尾延迟。系统调优的工作很大一部分就是把这些「以为做了其实没做」的配置项一一核对到位。
六、FPGA 与 ASIC
当 CPU + 内核旁路压到 1 微秒以下后,再想往下走,硬件层面只剩 FPGA 与 ASIC 两条路。
六点一、为什么是 FPGA
FPGA(Field-Programmable Gate Array)的优势对 HFT 而言是可预测延迟 + 可重构:
- 延迟可预测到时钟周期:FPGA 的逻辑设计在硬件级跑,每一级流水线一个时钟周期(几纳秒),没有缓存、没有调度、没有中断。从 NIC PHY 到 PHY 的「tick-in-tick-out」延迟可以做到 100 至 300 纳秒。
- 可重构:策略参数变了、协议升级了,重新综合一次比特流(bitstream)即可,不用换硬件。这是 FPGA 相对 ASIC 的核心优势。
六点二、Tick-to-Trade FPGA 的典型架构
一条完整的 tick-to-trade FPGA 数据通路通常长这样:
- MAC + PHY:以太网底层,由 FPGA 内核 IP 提供,几十纳秒。
- 行情解码模块:流水线式解析交易所协议,识别目标 symbol、目标事件类型,几十纳秒。
- 本地簿更新模块:维护几个目标 symbol 的最优档(top of book)或前几档,几十纳秒。
- 触发判断模块:硬连线的简单规则——价格触发、价差触发、不平衡触发——几纳秒到几十纳秒。
- 订单模板模块:预先打好的订单二进制模板(pre-canned order),命中触发条件后填入数量与价格,立即送 TX。
- TX MAC + PHY:发出。
整条路径不经过软件,CPU 完全旁观。CPU 通过 PCIe 下发参数(触发阈值、数量、目标价位),通过共享内存读取统计(命中次数、PnL)。这种架构的延迟天花板由 FPGA 本身的时钟频率(300 MHz 至 500 MHz)和流水线深度决定。
六点三、FPGA 的编程模型
主流的 FPGA 编程语言是 Verilog 与 VHDL,工程上写起来更接近「画电路图」而不是「写代码」。Xilinx Vivado 与 Intel Quartus 是两大综合工具链。一个完整的 tick-to-trade FPGA 设计的开发周期:
- 协议解析模块:1 至 3 个月
- 本地簿模块:1 至 2 个月
- 风控与下单模块:2 至 4 个月
- 验证与时序收敛(timing closure):贯穿整个项目,通常占 30% 工时
- 上线后的部署与回归:每次比特流改动需要重新走全套测试
近年来 HLS(High-Level Synthesis) 工具(Vivado HLS、Intel HLS)允许用 C/C++ 写综合到 FPGA,门槛降低,但生成的电路效率与人工 RTL 仍有差距,HFT 关键路径仍以手写 RTL 为主。
商用框架(如 Algo-Logic、Enyx、Hyannis Port,以及 Cisco/Exablaze 的 FastPath)能提供已经做好的协议解码、本地簿、风控模块,团队只需要写策略部分。这类框架把开发周期从「年」压到「月」,代价是授权费与定制灵活性。
六点四、ASIC 与维护成本
ASIC(Application-Specific Integrated Circuit)相对 FPGA 进一步去掉了可重构性,把电路固化到芯片上。延迟可以再压低 30% 至 50%,功耗显著降低,但流片成本几百万美元起,一旦协议或策略变更就只能重新流片。HFT 圈用 ASIC 的极少数案例集中在「高度稳定的链路层硬件」(如以太网 MAC、PCIe 接口、PTP 时间戳),策略层基本没人用 ASIC——风险与回报不成比例。
FPGA 路线的真正成本不是开发,是维护。交易所协议每年都会升级(新字段、新订单类型、新限价规则);监管要求会强制接入新风控模块;机房硬件迭代会换 PCIe 版本、换 NIC 型号。每一次升级都意味着重新综合、重新验证、重新跑回归。一个稳定运行的 FPGA 团队,工程师与维护比一般软件团队高出一档。
六点五、FPGA 的「不要」
FPGA 不是万能锤。下面几件事不要用 FPGA 做:
- 复杂策略逻辑:浮点运算、矩阵乘、查表都不是 FPGA 的强项。FPGA 适合「定长结构、固定流水线、判断触发」,不适合「带反馈环的优化问题」。后者放在 CPU。
- 状态高度依赖历史的逻辑:长周期统计、回归、PCA 之类应该跑在 CPU,FPGA 上跑这种需要大量片上 RAM 与复杂状态机,得不偿失。
- 频繁变化的逻辑:策略每天都要调整阈值的,把阈值做成 CPU 下发参数;但策略骨架本身每周就要重写的,FPGA 的开发周期跟不上。
合理的边界是:信号触发、风控、报单序列化 走 FPGA;信号生成、参数学习、组合管理 走 CPU;中间用共享内存或 PCIe MMIO 通信。
六点六、HLS 风格的代码示意
读者如果没接触过 HLS,可以通过下面的 C++ 风格示意感受 FPGA 设计的「流水线」思维。这段代码不是 Python 也不能跑,仅作示意:
#pragma HLS PIPELINE II=1
void on_quote(quote_msg_t q, order_t& out, bool& fire) {
// 一个时钟周期内能并行做的所有判断
bool px_ok = (q.bid_px <= cfg.fair - cfg.edge);
bool sz_ok = (q.bid_sz >= cfg.min_sz);
bool risk_ok = (pos.long_qty + cfg.lot <= cfg.pos_limit);
bool rate_ok = (rate.tokens > 0);
fire = px_ok & sz_ok & risk_ok & rate_ok;
if (fire) {
out.symbol = q.symbol;
out.side = SIDE_BUY;
out.qty = cfg.lot;
out.px = q.bid_px;
out.tag = next_tag++;
}
}#pragma HLS PIPELINE II=1
告诉综合工具:每个时钟周期接受一个新事件、流水线吞吐为
1。所有判断在这一个时钟周期内并行做,不存在「一条一条算」的串行延迟。HLS
的代价是:所有循环必须可静态展开,所有数组必须有定界,所有变量必须有定宽位。这种约束让
HLS 适合「数据通路型」逻辑,不适合「控制流复杂」的逻辑。
六点七、FPGA 调试的现实
FPGA 调试与软件 debug 完全不同。常用手段是:
- 波形仿真(waveform simulation):综合前用 ModelSim / Verilator 模拟,画出每条信号的时序图,逐周期对照预期。
- 片上逻辑分析仪(ILA / ChipScope):在 FPGA 内部插入采样模块,运行时把信号采样到片上 RAM,事后通过 JTAG 读出。
- 环回测试(loopback):把 TX 接回 RX,验证整条数据通路。
任何一次 FPGA 改动都要走完仿真 → 综合 → 实现 → 时序收敛 → 环回 → 在线灰度的全套流程,单次迭代周期通常半天到几天。这与软件「改一行、跑一次」的节奏完全不同,是 FPGA 团队人月成本高的根本原因。
七、软件架构
CPU 上的软件路径要做到极致延迟,背后有一整套「在所有人都共识下不需要这么写」的写法。这一节把核心模式列出来。
七点一、Lock-Free 与 SPSC
热路径上不能有锁。锁的成本不只是 lock contention,而是调度依赖:一旦 owner 线程被调度走(哪怕只有几微秒),所有 waiter 都阻塞。HFT 上线的第一条工程纪律就是「热路径上没有任何 mutex 或 condition variable」。
替代方案是无锁队列。HFT 的最常见模式是 SPSC(Single Producer Single Consumer) 环形缓冲:一个生产者线程(feed handler)写入,一个消费者线程(strategy)读取。SPSC 比 MPMC(Multi Producer Multi Consumer)简单得多,因为只有一个写者和一个读者,不需要 CAS 操作,仅靠 acquire/release 内存序就能正确同步。
C++ 实现的标准骨架(用伪代码描述):
template <typename T, size_t N>
class SpscRing {
static_assert((N & (N - 1)) == 0, "N must be power of 2");
alignas(64) std::atomic<size_t> head_{0}; // producer writes
alignas(64) std::atomic<size_t> tail_{0}; // consumer writes
alignas(64) T buf_[N];
public:
bool try_push(const T& x) {
size_t h = head_.load(std::memory_order_relaxed);
size_t t = tail_.load(std::memory_order_acquire);
if (h - t >= N) return false;
buf_[h & (N - 1)] = x;
head_.store(h + 1, std::memory_order_release);
return true;
}
bool try_pop(T& out) {
size_t t = tail_.load(std::memory_order_relaxed);
size_t h = head_.load(std::memory_order_acquire);
if (t == h) return false;
out = buf_[t & (N - 1)];
tail_.store(t + 1, std::memory_order_release);
return true;
}
};要点:
head_与tail_各占一个 cache line,避免 false sharing。- 容量是 2 的幂,下标取模用位与,避免除法。
- 内存序选择保证:消费者读到
head_后能看到对应位置的数据;生产者读到tail_后能看到对应位置已经被消费。 - 没有任何系统调用、堆分配、虚函数。
七点二、其他无锁模式
- MPSC(多生产者单消费者):常用 Vyukov bounded MPSC 或 segmented queue。
- MPMC:不是不能做(如 Disruptor 模型),但 contention 高时性能不可预测,HFT 一般避免;多生产者就分多个 SPSC,由消费者轮询。
- Hazard pointer / RCU:用于读多写少的全局结构(如配置、symbol 元数据),生产侧少更新,消费侧无锁读。
七点三、零拷贝与对象池
热路径上每一次 memcpy 都要算账。设计上用
flat buffer + 偏移量
描述消息,避免序列化与反序列化中间结构。SBE(Simple Binary
Encoding)就是金融行业为这种 zero-copy
风格设计的二进制协议家族。
对象池(object pool)取代
new/delete:在初始化时一次性分配 N
个对象,运行时从池里取、用完归还。对象池本身可以用 SPSC
队列实现「空闲索引」管理。
七点四、Cache-friendly 数据结构
本地订单簿(local order book)的实现是一个经典考题:
- 价位用扁平数组:以「最小价位变动单位(tick size)」为粒度,开 N 个 slot 的数组,每个 slot 存该价位的累计数量。价格 → 数组索引是 O(1) 的算术。
- 当前最优买卖位用两个游标:买一价、卖一价的索引直接维护,不需要扫描数组找最小最大。
- 价位变化用环形位移:当行情打穿当前数组覆盖范围时,要么扩展数组(罕见),要么把数组解释成环形,旋转起始点。
- 关注顶部档:策略实际只关心 top of book 与前几档,把 top 5 / top 10 单独一个紧凑结构(一个 cache line 内)频繁访问,深档用稀疏结构。
这套设计能让一次行情更新 + 一次 top of book 查询稳定在 50 纳秒以内。
七点五、日志、监控与降级
热路径上不能直接写日志——任何
fprintf 都意味着 stdio
锁、内存分配、系统调用。正确做法是把每条日志写入一个 SPSC
环形缓冲(专门用于日志),由另一个低优先级线程异步消费,落盘或转发到日志服务。监控指标同样走旁路。
降级(degradation)也要事先设计:当 SPSC 满了,热路径不能阻塞、不能 spin 等待。HFT 通常采取「丢弃旧消息 + 计数告警」的策略——丢一条日志比让策略卡住小一万倍。
七点六、用 numba 演示 SPSC 思路
下面给出一个简化版的 SPSC 环形缓冲演示。这个演示只用于说明数据布局与算法骨架,不能用于真实的多线程并发:
- numba 没有 C++ 的内存序语义。下面的实现假设「单线程或者足够弱的并发」,仅证明性能数量级。
- numba 的
prange可以让生产消费跑在不同线程,但 GIL 与 numba 的同步原语都不足以保证正确的发布语义。 - 真实生产路径用 C++ 或 Rust 写,本演示只是把 SPSC 的数据流量与单线程吞吐展示一遍,便于读者直观感受。
import numpy as np
from numba import njit
from numba.types import int64
# 容量必须是 2 的幂
CAP = 1 << 14
MASK = CAP - 1
@njit(cache=True, boundscheck=False)
def spsc_init():
head = np.zeros(1, dtype=np.int64) # 生产者写入位置
tail = np.zeros(1, dtype=np.int64) # 消费者读取位置
buf = np.empty(CAP, dtype=np.int64)
return head, tail, buf
@njit(cache=True, boundscheck=False, fastmath=False)
def spsc_push(head, tail, buf, value):
h = head[0]
t = tail[0]
if (h - t) >= CAP:
return False # 队列满
buf[h & MASK] = value
head[0] = h + 1
return True
@njit(cache=True, boundscheck=False, fastmath=False)
def spsc_pop(head, tail, buf):
h = head[0]
t = tail[0]
if t == h:
return -1 # 队列空(约定 -1,演示用)
v = buf[t & MASK]
tail[0] = t + 1
return v
@njit(cache=True, boundscheck=False)
def spsc_roundtrip(n):
head, tail, buf = spsc_init()
s = 0
for i in range(n):
spsc_push(head, tail, buf, i)
s += spsc_pop(head, tail, buf)
return s这个版本在单线程下做 push 立刻 pop 的「乒乓」基准,可以反映出 numba 的最优单次操作时间。
七点七、用 timeit 做基准对比
把上面的代码与 Python 内置
collections.deque、queue.Queue
做一个对比:
import timeit
import numpy as np
from collections import deque
from queue import Queue
# 触发 numba JIT 编译
spsc_roundtrip(1)
N = 1_000_000
t_numba = timeit.timeit(lambda: spsc_roundtrip(N), number=3) / 3
print(f"numba SPSC 单次 push+pop: {t_numba * 1e9 / N:.1f} ns")
def deque_bench():
q = deque()
for i in range(N):
q.append(i)
q.popleft()
t_deque = timeit.timeit(deque_bench, number=3) / 3
print(f"deque 单次 append+popleft: {t_deque * 1e9 / N:.1f} ns")
def queue_bench():
q = Queue()
for i in range(N):
q.put_nowait(i)
q.get_nowait()
t_queue = timeit.timeit(queue_bench, number=3) / 3
print(f"queue.Queue 单次 put+get: {t_queue * 1e9 / N:.1f} ns")在 x86-64 桌面级 CPU 上,典型量级是这样:numba SPSC 单次
push + pop 约 30 至 80 纳秒;collections.deque
约 80 至 150 纳秒;queue.Queue
因为带锁,至少几百纳秒。numba 的版本接近 C 编写的 SPSC
单线程性能。
但这只是一个演示:
- 真实 SPSC 是跨线程的,要靠内存序保证可见性。numba 这一套没法做。
- 真实 HFT 单次 SPSC 操作目标是 10 纳秒级,会用
alignas(64)隔离 head/tail,会做严格的 cache line padding,会用编译器内建函数控制内存序。 - 本演示也没有处理「跨线程虚假共享」「TLB miss」「cache line bouncing」这些真实瓶颈。
把这个对比的目的限定在「让你看到 Python 标准库与底层结构之间存在数量级差距」,不要把它当成「HFT 可以用 numba 写」的证据。
七点八、确定性与可观测性
低延迟系统不只是「快」,而是「可预测的快」。每一次决策、每一笔订单、每一次状态变化都要打上硬件时间戳,落盘到事后分析。监控仪表盘上必须显示:
- p50 / p99 / p99.9 / p99.99 tick-to-trade 延迟
- 消息处理速率与丢包率
- SPSC 队列高水位
- 每个核 CPU 占用与 cache miss 计数
- NIC 硬件时间戳与本地时钟偏差
- 风控触发次数(按规则分类)
这些指标既是性能调优的数据源,也是「策略今天有没有出问题」的第一手证据。HFT 团队会把这套观测体系当成一等公民来建设,与策略开发同等重要。事后分析(post-trade analytics)的能力直接决定策略迭代速度——能看到「为什么这一笔比预期慢 800 纳秒」,团队才能把问题定位到 cache miss 还是 IRQ 干扰;只能看到「平均延迟」的团队,问题永远是模糊的。
七点九、生产级 SPSC 的真实样貌
为了让读者理解上面的演示与生产代码的距离,下面把生产级 SPSC 的关键工程细节列一遍:
- 缓存行隔离:head 与 tail 各自独占一个 64 字节缓存行,缓冲数组首尾各加一行 padding,避免与相邻数据共享缓存行。
- 缓存的本地副本:消费者 pop 时把生产者的 head 缓存到本地,多次 pop 不重复触发跨核同步;生产者 push 时同理缓存 tail。这能把多次 push / pop 平均到一次跨核读。
- 批量推送 / 弹出:单次 push
一个对象代价高于批量 push 多个;提供
push_n(buf, n)接口供 producer 批量入队,同样消费侧批量出队。 - 环绕标记的 generation:64 位的 head / tail 永远不溢出(按当代 CPU 频率算需要数千年),不必处理回卷。
- 冷启动 prefault:初始化时把整个缓冲数组写一遍 0,确保所有页面被 OS 实际分配(避免热路径上首次访问触发 page fault)。
- runtime 验证:运行时统计「队列满次数」「队列空次数」,如果生产端经常满或消费端经常空,说明速率不匹配,要调整批大小或优先级。
下面给出一个稍微更接近生产形态的 numba 单线程基准,演示「批量推 / 批量弹」在吞吐上的提升:
@njit(cache=True, boundscheck=False)
def spsc_push_batch(head, tail, buf, values):
h = head[0]
t = tail[0]
n = values.shape[0]
if (h - t) + n > CAP:
return 0
for i in range(n):
buf[(h + i) & MASK] = values[i]
head[0] = h + n
return n
@njit(cache=True, boundscheck=False)
def spsc_pop_batch(head, tail, buf, out):
h = head[0]
t = tail[0]
avail = h - t
n = out.shape[0] if out.shape[0] < avail else avail
for i in range(n):
out[i] = buf[(t + i) & MASK]
tail[0] = t + n
return n
@njit(cache=True, boundscheck=False)
def spsc_batch_bench(n_iter, batch):
head, tail, buf = spsc_init()
src = np.arange(batch, dtype=np.int64)
dst = np.empty(batch, dtype=np.int64)
s = 0
for _ in range(n_iter):
spsc_push_batch(head, tail, buf, src)
spsc_pop_batch(head, tail, buf, dst)
s += dst[batch - 1]
return s把 batch 设为 1、4、16、64 跑一遍,能看到每个元素的平摊时间随 batch 上升明显下降——这是生产 HFT 系统普遍采用 batch 接口的原因。但在低速率场景(例如清晨开盘前),batch 反而会因等不齐而恶化首包延迟,所以工程上常用「自适应 batch」:来包多时合并、来包少时立即发。
七点十、从基准到生产的鸿沟
把上面这套基准跑出漂亮数字,距离一个能上线的 HFT 路径还有几道坎:
- 多线程正确性:跨线程的 SPSC 必须用
std::atomic与正确的 acquire / release 内存序;numba 表达不出来。 - 多队列接收:现代 NIC 支持 RSS(receive side scaling),同一个 stream 的包会按 hash 分到不同队列,需要保证策略关心的所有 symbol 落在同一队列(用 ntuple flow steering 强制)。
- 失序与重传:UDP 多播行情没有重传机制,丢包靠 A/B feed 互补恢复;TCP 发单链路的重传则会引入毫秒级抖动,必须有 backup 链路。
- 风控的 hard kill:所有热路径外部必须有一条独立链路,能在 1 毫秒内强制断开报单链路(kill switch),由独立机器或 FPGA 实现。
- 撤单优先级:交易所的 cancel-replace 协议在不同实现下延迟差异巨大,有些交易所允许 mass cancel(一次撤所有),有些必须逐笔,影响策略撤单成本模型。
- 协议变更应对:交易所每年会发布协议升级文档,所有解码器、序列化器都要回归测试,FPGA 比特流要重新综合。
这些坎一个比一个吃工程量,每过一道都要付出人月级别的代价。
低延迟系统不只是「快」,而是「可预测的快」。每一次决策、每一笔订单、每一次状态变化都要打上硬件时间戳,落盘到事后分析。监控仪表盘上必须显示:
- p50 / p99 / p99.9 / p99.99 tick-to-trade 延迟
- 消息处理速率与丢包率
- SPSC 队列高水位
- 每个核 CPU 占用与 cache miss 计数
- NIC 硬件时间戳与本地时钟偏差
- 风控触发次数(按规则分类)
这些指标既是性能调优的数据源,也是「策略今天有没有出问题」的第一手证据。HFT 团队会把这套观测体系当成一等公民来建设,与策略开发同等重要。
八、Python 在 HFT 的边界与配合
本系列所有代码都用 Python,HFT 这一章也不破例。但必须把边界画清楚:Python 不能写 HFT 的热路径,永远不能。这一节回答两个问题:Python 在 HFT 工程师的工具箱里能干什么,干不了什么;本系列演示性质的 numba 代码到底能映射到生产中的什么部分。
八点一、研究侧:Python 是默认语言
HFT 团队的研究侧(research)几乎全用 Python。这一侧的工作内容包括:
- 数据回放与 PCAP 分析:把交易所多播 PCAP 解码后用 pandas / polars 分析,找到延迟热点、行情异常、对手单模式。Python 在这里是事实标准,因为分析代码每天都在改。
- 特征工程与因子研究:信号设计、统计套利对、做市偏移参数,都是 Python + numpy + scipy + statsmodels。
- 回测与仿真:撮合仿真、订单生命周期建模、对手单建模都用 Python;本系列前面 19、20 章的回测引擎模式直接适用。
- 可视化与报告:matplotlib / plotly 画延迟分布、PnL 归因、做市价差。
- 机器学习:尽管 HFT 热路径不跑神经网络,研究侧用神经网络发掘模式、再蒸馏成简单规则放到 FPGA / C++,是常见模式。
研究侧的代码不上生产,所以延迟无关紧要,写得清楚、跑得对、改得快才是硬要求。
八点二、运维侧:Python 也是默认语言
HFT 系统的非热路径——监控、告警、配置下发、合规报告、市后批处理——大量用 Python。一个典型生产环境会有:
- 配置管理:YAML 配置 + Python 脚本生成 C++ 头文件,用于编译期常量。
- 每日批处理:盘后 PnL 归因、风控指标计算、合规报表生成。
- 告警与值守:Prometheus / Grafana / 自研监控系统的告警规则、紧急处置脚本。
- 协议测试客户端:与交易所联调时的 mock 客户端,用 Python 写起来快。
这些都不是热路径,毫秒级的 Python 性能完全够用。
八点三、热路径:Python 的硬上限
Python 解释器有几个先天属性,决定了它无法用于 HFT 热路径:
- GIL(Global Interpreter Lock):CPython 的全局锁让多线程的 Python 字节码无法并行执行,限制了 busy-poll 风格的多线程模型。Python 3.13+ 的 free-threaded build 取消 GIL,但生态成熟需要时间,且性能仍不如 C++。
- 引用计数:每个对象的访问都要增减引用计数,单次操作几纳秒到几十纳秒,热路径上汇总起来不可接受。
- 内存分配:Python 对象创建在堆上,分配延迟不可预测,且会触发 GC。
- 解释开销:CPython 字节码解释每条指令几十纳秒。
- 缺乏精确内存控制:无法 alignas、无法 prefetch、无法明确内存序。
即使用 numba JIT 编译核心循环,能把单次操作压到几十纳秒,依然有两个问题:一是 JIT 编译和 Python runtime 共存,runtime 任何一次 GC 或锁都会污染热路径;二是 numba 无法表达跨线程的内存序,正确的多线程 SPSC 写不出来。
八点四、Cython、Mypyc、Pythran 的边界
「把 Python 编译成原生代码」的工具链能进一步压低单线程延迟,但不解决根本问题:
- Cython:把 Python 翻译成 C,需要手动加类型注解。生成的 C 代码可以做到接近手写 C 的性能,但仍依赖 Python runtime(GIL、对象、引用计数)。
- mypyc:把带类型的 Python 编译成 C 扩展,性能与 Cython 类似,限制相同。
- Pythran:科学计算专用,把带注解的 Python 翻译成 C++,对纯数值代码效果好。
- PyPy:JIT 解释器,有 GC 暂停,HFT 不可接受。
在「中频量化」场景(决策周期毫秒级以上、不需要 colocation)里,Cython / numba 是 Python 团队提升关键路径性能的合理选择,单核吞吐能上一两个数量级。但「中频可用」不等于「HFT 可用」。
八点五、混合架构:Python + C++ / Rust
实际生产中常见的混合架构是:
- 热路径:C++(绝大多数)或 Rust(增长中)。直接调用 EF_VI / DPDK / 内核旁路 API,与 FPGA 通过共享内存或 PCIe 通信。
- 配置与控制面:Python。通过 ZeroMQ / gRPC / 共享内存与 C++ 进程通信,下发参数、读取状态。
- 研究与回测:Python。共享与生产相同的数据结构定义(用 protobuf / Cap’n Proto / SBE 描述),保证回测逻辑与生产逻辑等价。
这种架构下 Python 工程师与 C++ 工程师分工明确:Python 工程师产出策略原型与参数,C++ 工程师把原型固化到热路径并保证延迟达标。两边通过协议契约(schema)解耦,任何一方的改动都要走变更评审。
八点六、从 Python 原型到 C++ 生产的迁移路径
如果一个研究侧的 Python 原型最终需要进入 HFT 热路径,标准的迁移路径是这样:
- Python 原型确认逻辑:用 pandas / numpy 把策略逻辑跑通,所有边界条件、异常分支都覆盖。这一步的产出物是一份算法规约(algorithm specification)——文字 + 公式 + 测试用例,不依赖具体实现。
- 抽出确定性接口:把策略写成纯函数:输入是事件流(行情更新、订单回报),输出是订单意图。所有副作用(日志、监控、I/O)剥离。
- C++ 重写 + 等价性测试:C++ 重新实现,与 Python 版本跑同一份录制行情,逐 tick 比对输出。允许的差异只能是数值精度(浮点末位),任何逻辑差异都是 bug。
- 集成到热路径框架:把 C++ 实现挂到 SPSC 环形缓冲、订单编码器、风控前置检查后面,跑 dry-run 模式(不真正发单)确认延迟达标。
- 影子模式(shadow mode)上线:与现有生产策略并行跑,记录所有「假如真的发了单」的订单意图,与 fill 数据对比。一般跑两到四周。
- 小额放量:从 1% 仓位起,逐步放开。
- 持续回归:策略后续任何参数调整,都要重跑等价性测试。
这条路径的关键是步骤 3 的等价性测试。Python 与 C++ 的浮点行为、整数溢出语义、对齐假设、空 series 处理都不一样,没有自动化的等价性测试,研究与生产的逻辑会逐渐漂移,几个月后谁也说不清「为什么生产 PnL 跟回测对不上」。
八点七、HFT 招聘与人才结构
观察一下 HFT 团队的招聘画像,能反向印证语言与架构的边界:
- Quant Researcher:Python 为主,要求统计与机器学习背景,关心市场结构与因子。
- Quant Developer / Strategy Developer:C++ 为主,要求懂策略也懂系统,连接研究与生产。
- Low-Latency Engineer / Performance Engineer:C++ + Linux 内核 + 微架构知识,处理调优与抖动。
- FPGA Engineer:Verilog / VHDL,独立的硬件工程岗位,与软件团队相对解耦。
- Network Engineer:交换机、PTP、微波链路、机房运维,独立岗位。
- SRE / DevOps:监控、配置管理、CI / CD、容灾切换,Python / Go 为主。
一个完整的 HFT 团队大致 30 至 80 人,研究 + 工程比一般在 1 比 2 到 1 比 3。Python 工程师的位置主要在第一档(研究)和最后一档(运维),中间的低延迟核心几乎不沾 Python。把这点放在职业规划上:想做 HFT 系统工程师,C++ 是必备;想做 HFT 策略研究,Python 是默认;二者都不是的话,HFT 不是合适的赛道。
八点八、本系列的定位
回到本系列的边界:
- 我们用 Python 演示数据流、算法骨架、性能数量级,让读者理解 HFT 系统的「形」。
- 我们不演示真实的 HFT 热路径,因为那需要 C++ / Rust + 操作系统调优 + 真实硬件,超出了一个 Python 系列的范围。
- 关键的设计原则——延迟预算、内核旁路、缓存友好、无锁队列、硬件时间戳——是语言无关的。读者把这些原则掌握,未来用任何语言去实现都不会偏。
- 当读者真的进入 HFT 行业、需要写热路径时,应当切换到 C++ / Rust 的专门系列,而不是继续用 Python 的 numba 或 Cython 硬撑。
把 Python 的位置摆正,HFT 这一章对学习者的价值才落得下来。Python 不是「不行」,是「适合的位置不在这」;C++ / Rust 也不是「更高级」,是「不得不用」。承认每种语言的物理边界,是工程成熟度的标志。
上图把第二节到第七节的全部组件放在同一张图里。读者可以把这张图当作一份 checklist:每搭一段、回头核对一次延迟预算瀑布图(第二节图),看实际延迟与目标的差距出现在哪一段。HFT 系统调优的工作循环就是这两张图之间的反复对照:调一处、量一次、回到瀑布图上看新瓶颈在哪、再调下一处。
九、写在最后
HFT 这件事的核心矛盾是:策略上限来自市场结构,工程下限来自物理学。市场结构决定了某一类速度优势能赚多少(队列优势的 PnL、跨市场套利的价差大小),工程能力决定了你站在哪一档的延迟分级里能不能拿到这份钱。两端不匹配,任何一端再强也变不成业绩——再快的系统抓不到没机会的市场,再聪明的策略架不到 200 微秒的延迟。
理性的工程路径是按需分级:不是所有量化策略都要 HFT 化,绝大多数策略在毫秒级就足够,把 colocation、FPGA、内核旁路这些工具留给真正吃延迟的那一档。盲目追求「我们也要做 HFT」会把工程团队拖进无尽的硬件维护与协议跟进,最后发现策略本身的 PnL 撑不起这套基础设施。
对于本系列的读者——以 Python 为主力工具的量化工程师——HFT 一章的价值不在于「让你能写 HFT」,而在于:
- 理解延迟预算的分解方法,把同样的方法用到中频系统的优化上。
- 理解内核旁路、cache、NUMA、无锁队列的工作原理,写中频系统时不会犯同样的错。
- 理解硬件时间戳、PTP、p99 延迟监控,把这套观测体系搬到任何延迟敏感系统里。
- 理解 Python 与 C++ 的边界,能与 C++ 团队对接,能合理预估什么场景需要切换语言。
下一篇《交易系统架构》会从 HFT 的单机极限退一步,回到一般机构的多机分布式交易系统:策略服务、订单服务、风控服务、回报服务、市场数据服务、跨机房部署、容灾切换。延迟不再是纳秒级,但正确性、可观测性、可恢复性成为新的工程主线。两个层级的对比能让读者更清楚地看到「为速度付的代价」与「为可靠付的代价」是两种完全不同的工程取舍。
参考文献
- Aldridge, I. (2013). High-Frequency Trading: A Practical Guide to Algorithmic Strategies and Trading Systems (2nd ed.). Wiley.
- Durbin, M. (2010). All About High-Frequency Trading. McGraw-Hill.
- Lewis, M. (2014). Flash Boys: A Wall Street Revolt. W. W. Norton.
- Budish, E., Cramton, P., & Shim, J. (2015). The High-Frequency Trading Arms Race: Frequent Batch Auctions as a Market Design Response. The Quarterly Journal of Economics, 130(4), 1547–1621.
- Menkveld, A. J. (2013). High Frequency Trading and the New Market Makers. Journal of Financial Markets, 16(4), 712–740.
- Hasbrouck, J., & Saar, G. (2013). Low-Latency Trading. Journal of Financial Markets, 16(4), 646–679.
- Laughlin, G., Aguirre, A., & Grundfest, J. (2014). Information Transmission Between Financial Markets in Chicago and New York. Financial Review, 49(2), 283–312.
- DPDK Project. Data Plane Development Kit Documentation. https://doc.dpdk.org/
- AMD / Solarflare. OpenOnload User Guide. https://github.com/Xilinx-CNS/onload
- AMD / Solarflare. EF_VI User Guide. https://github.com/Xilinx-CNS/onload
- Linux Kernel Documentation. AF_XDP. https://www.kernel.org/doc/html/latest/networking/af_xdp.html
- IEEE. (2019). IEEE 1588-2019: Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems.
- Intel Corporation. Intel 64 and IA-32 Architectures Optimization Reference Manual.
- Drepper, U. (2007). What Every Programmer Should Know About Memory. https://akkadia.org/drepper/cpumemory.pdf
- Vyukov, D. 1024cores: Lock-Free Algorithms. https://www.1024cores.net/
- LMAX. (2011). Disruptor: High Performance Alternative to Bounded Queues for Exchanging Data Between Concurrent Threads. https://lmax-exchange.github.io/disruptor/
- Real Logic. Simple Binary Encoding (SBE). https://github.com/real-logic/simple-binary-encoding
- Lockwood, J. W., et al. (2012). A Low-Latency Library in FPGA Hardware for High-Frequency Trading. IEEE Hot Interconnects.
- Leber, C., Geib, B., & Litz, H. (2011). High Frequency Trading Acceleration Using FPGAs. International Conference on Field Programmable Logic and Applications.
- Numba Project. Numba Documentation. https://numba.readthedocs.io/
十、延迟监控与基准测试
在真实的 HFT 部署中,延迟监测不能只看均值,必须看分布,特别是 p99、p99.9、p99.99 百分位。
十点一、无损延迟采样
为了不让测量工具本身吃掉宝贵的纳秒,通常用硬件时间戳:
# 伪代码:硬件时间戳比对
import numpy as np
from timeit import timeit
# 用 numpy 录制 10000 个事件的端到端延迟(纳秒)
latencies = np.array([...]) # 从硬件时间戳日志读入
p50 = np.percentile(latencies, 50)
p95 = np.percentile(latencies, 95)
p99 = np.percentile(latencies, 99)
p999 = np.percentile(latencies, 99.9)
# 检测尾部离群
outlier_threshold = p99 + 5 * (p99 - p95)
outliers = latencies[latencies > outlier_threshold]
print(f"Median: {p50:.0f}ns, P99: {p99:.0f}ns, P99.9: {p999:.0f}ns")
print(f"Outliers (>{outlier_threshold:.0f}ns): {len(outliers)}")关键约束:采样不能用 Python 的
time.perf_counter()(精度不足),必须用系统调用拿到纳秒时钟,或者在内核模块里直接读
TSC(Time Stamp Counter)。Numba 可以用
np.uint64 的无符号整数表示
TSC,但交叉对齐成本高。最实用的做法是在 C 层采集、用 mmap
暴露给 Python 的环形缓冲区。
十点二、压力测试下的延迟特征
高频系统的延迟在正常流量和压力流量下会有显著差异:
- 正常流量:策略能定稳在 p99 < 100 微秒。
- 行情风暴(相同期货合约从 1000 手/秒涌到 100000 手/秒):p99 可能跳到 500 微秒以上,因为行情线程队列开始积压。
- 多资产并行:同时跑 100 只股票的行情订阅 + 5 个期货合约,p99 延迟可能再增加 20-30%,因为 L3 缓存竞争加剧。
这意味着绝不能只在低流量环境下做基准测试。必须在生产级的流量冲击下采集 p99.9 的真实分布,这是压力测试的核心目标。
十点三、基准测试的正确姿势
一个严谨的基准过程是这样的:
# 伪代码:多轮基准,记录每轮的分布
def benchmark_latency(num_iterations=100000, num_runs=10):
results = []
for run_idx in range(num_runs):
print(f"Run {run_idx + 1}/{num_runs}")
# 每轮预热 1000 次消除缓存冷启
for _ in range(1000):
_ = measure_one_tick_to_trade()
# 正式测 100k 次
latencies = np.array([
measure_one_tick_to_trade() for _ in range(num_iterations)
])
p50, p95, p99, p999 = [
np.percentile(latencies, q) for q in [50, 95, 99, 99.9]
]
results.append({
'run': run_idx,
'p50': p50,
'p95': p95,
'p99': p99,
'p99.9': p999,
'mean': latencies.mean(),
'std': latencies.std(),
})
print(f" P99: {p99:.1f}ns, P99.9: {p999:.1f}ns")
# 10 轮之间的 P99 方差是什么?如果超过 10%,说明系统不稳定
p99_values = [r['p99'] for r in results]
p99_cv = np.std(p99_values) / np.mean(p99_values)
if p99_cv > 0.1:
print(f"WARNING: P99 稳定性差(CV={p99_cv:.1%}),可能有 GC / CPU 频率抖动 / cache line conflict")
return results系列导航
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
量化交易
从因子研究到生产执行的量化交易全栈工程。覆盖市场微结构、数据管线、因子构造、组合优化、回测方法论、执行算法、做市策略、高频架构到生产运维。面向策略研究员与工程师。
【量化交易】量化交易全景:从信号到订单的工程链路
量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。
【量化交易】市场结构:交易所、做市商、暗池、ECN
系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。
【量化交易】市场微结构:订单簿、价差、流动性、冲击
系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。