土法炼钢兴趣小组的算法知识备份

【量化交易】高频交易架构:低延迟、内核旁路、FPGA 概览

文章导航

分类入口
quant
标签入口
#hft#low-latency#kernel-bypass#fpga#dpdk

目录

高频交易这个名字本身就是一个误导。它真正难的不是「频率高」,而是「单笔决策的端到端延迟必须可预测、可压缩、可监控」。一支日内成交一万笔的中频策略,如果平均决策延迟 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 的量级划分:

把目标先定到具体哪一档,决定了后面要做什么、不做什么。一个「分钟频率因子策略」声称「也要低延迟」是没意义的:信号每分钟刷新一次,订单从挂出到成交有秒级 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 的边界定在「tick-to-trade < 10µs,且为了这个目标愿意付出 colocation / kernel-bypass / FPGA 任意一项的工程代价」,本文后续讨论都围绕这个定义展开。

一点四、谁在这条赛道上

把 HFT 行业的玩家分成几类,能帮助理解后面工程决策的市场背景:

了解这些玩家有助于理解延迟竞赛的真实分布:金字塔顶端十几家自营商在拼 100 至 300 纳秒级;下面一圈几十家在 1 至 5 微秒级;再下一圈几百家在 10 微秒级。每往下一档,硬件成本掉一半,但 PnL 池也按市场结构重新切分。

一点五、HFT 的盈利上限

HFT 这条赛道不是无穷蛋糕。关键事实:

把这两条放进决策框架,结论就清楚:「我们也要做 HFT」之前,先评估目标市场的总价差池有多大、目前有几个量级的玩家、自己能站到哪一档、按当前规模能切到多少 PnL。多数情况下,得到的答案是「不值得」。


二、延迟预算分解

低延迟工程的第一步永远是画延迟预算瀑布图:把一个事件从开始到结束的每一段可观察时间都列出来,按贡献排序,确定哪一段值得花成本去优化。这一节给出一个典型 colo 部署下的 tick-to-trade 分解,作为后面所有工程决策的参照系。

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 次有多慢。原因是市场行为在统计意义上是「事件驱动」的——绝大多数行情你不需要响应,少数关键事件(公告、订单簿失衡、对手大单)需要在第一时间响应;而尾延迟正好就是这些事件最容易踩中的区间。

工程上要追踪的几条尾延迟曲线:

一支健康的 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 与一般量化机构的第一道资金门槛。

三点二、跨城网络:微波、激光、卫星

跨地域的链路差异决定了跨市场套利的可行性。下面给出几条典型链路的可实现单程延迟(来自公开论文与新闻报道,仅作量级参考):

微波链路的工程缺点是带宽极低、可用性受天气影响。一条微波链路典型带宽 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 阵营有四家:

选型的考量除了延迟,还有:是否支持硬件时间戳、是否支持多队列与 RSS、是否支持 inline FPGA 卸载、驱动稳定性、原厂支持响应速度。HFT 团队通常会在生产中并存两家供应商以降低单点故障。

三点五、PCIe 拓扑与 NIC 摆放

NIC 的选型只是一半,怎么把 NIC 插到主板上同样决定延迟。现代服务器有多颗 CPU、每颗 CPU 提供数十条 PCIe lane,分到不同的 PCIe 插槽。错误的 NIC 摆放会让数据从 NIC 进入后绕道:先穿过 PCIe,再走 UPI 到另一颗 CPU 的内存控制器,再回到本应处理它的核。这条「跨 socket」路径会多花几百纳秒,而且抖动巨大。

正确做法:

三点六、机房选址的商业现实

colocation 设施名义上对所有客户「等延迟」,但实际可获得的资源仍有差异。例如:

把这些纳入决策表,HFT 的「硬件成本」远不止服务器和 NIC 的单价。一个完整的多机房部署,年化运营成本(机柜 + cross-connect + 跨城带宽 + 微波租赁 + 机房 remote hands)能轻松到千万美元量级。

三点七、灾备与多活

低延迟系统的灾备设计与一般业务系统不一样。一般业务追求「跨机房热备 + 自动切换」,但跨机房切换本身需要几百毫秒到几秒,这对 HFT 是致命的。HFT 的灾备模式更接近「就地降级 + 同机房多副本」:

这种设计的隐含假设是「停机比下错单便宜」。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 主导发起,目前是开源社区中最广泛使用的内核旁路方案。它的核心思想是:

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 的另一个独特能力是 TCPDirectCTPIO(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 驱动有 zero-copy 模式的依赖。HFT 内部更多用 AF_XDP 做监控旁路、灰度环境,主路径仍是 DPDK 或 EF_VI。

四点四、内核旁路解决不了什么

内核旁路把数据包从硬件搬到用户态的延迟压低了,但没有改变 CPU 在用户态做事的延迟。如果策略代码里有:

任何一项都会瞬间贡献几百纳秒到几微秒的不可控延迟。所以内核旁路只是必要条件,不是充分条件。剩下的工作全部落在用户态代码与 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);
            }
        }
    }
}

值得注意的几点:

DPDK 的接收循环结构类似,调用 rte_eth_rx_burstrte_pktmbuf_free,逻辑上一一对应。

四点六、TX 路径的两种模式

发送侧有两种典型实现:

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 -vvnumactl --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 数据结构设计的第一原则。

具体做法包括:

五点三、Busy-Poll 与抖动

热路径线程不睡觉。它在一个无限循环里轮询输入队列,没事件就立刻再轮询一遍,CPU 占用永远 100%。这与一般服务器编程的「事件驱动 + 阻塞 epoll」哲学相反,但在 HFT 是必须的:任何一次 wakeup(哪怕只是 futex 上的几百纳秒)都意味着一笔订单可能慢半拍。

为了让 busy-poll 线程不被打扰,需要做:

抖动(jitter)是 HFT 系统的核心 KPI,通常报告为 p99 / p99.9 / p99.99 延迟,而不是平均值。一个平均 5 微秒、p99.99 50 微秒的系统比一个平均 8 微秒、p99.99 12 微秒的系统差得多——后者更可预测。

五点四、分支与流水线

CPU 流水线深度十几级,分支预测失败会清空整条流水线,代价 15 至 20 个周期。在热路径上:

五点五、SIMD 与位级技巧

行情解码与本地簿更新里有大量并行可做:一次解码 16 个字节、一次比较 8 个价位、一次更新多档买卖盘。AVX-512、AVX2、SSE4 都能用上。HFT 工程师通常手写 intrinsics(_mm256_*),不依赖编译器自动向量化(auto-vectorization),因为后者在数据排布稍有不齐时就失效。

五点六、Linux 调优 checklist

把上面这些操作整理成一份开机脚本/启动 checklist,HFT 服务器上线前需要逐项核对:

每一项漏掉都可能在 p99.99 上产生几微秒到几十微秒的尾巴。运维上最好用 Ansible / Salt 把这套配置固化、版本化,每次硬件更换或内核升级都重跑。

五点七、性能分析工具链

低延迟系统的性能问题都不是「平均慢」,而是「偶尔慢」。对应的分析工具:

把这些工具固化到日常工程流程,比单看「平均延迟」有用得多。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 系统通常这么配:

TLB 命中的影响在尾延迟上尤其显著:一次 TLB miss 触发的多级页表遍历可能踩到 cache cold 区域,最坏情况叠加上百纳秒。把整个热路径工作集塞进 TLB 覆盖范围,能直接砍掉 p99.9 上的一根尾巴。

五点九、CPU 隔离的常见错误

调优文档里常见的几个坑:

每条都是真实环境里被踩过的坑,每一条留下的都是几微秒尾延迟。系统调优的工作很大一部分就是把这些「以为做了其实没做」的配置项一一核对到位。


六、FPGA 与 ASIC

当 CPU + 内核旁路压到 1 微秒以下后,再想往下走,硬件层面只剩 FPGA 与 ASIC 两条路。

六点一、为什么是 FPGA

FPGA(Field-Programmable Gate Array)的优势对 HFT 而言是可预测延迟 + 可重构

六点二、Tick-to-Trade FPGA 的典型架构

一条完整的 tick-to-trade FPGA 数据通路通常长这样:

  1. MAC + PHY:以太网底层,由 FPGA 内核 IP 提供,几十纳秒。
  2. 行情解码模块:流水线式解析交易所协议,识别目标 symbol、目标事件类型,几十纳秒。
  3. 本地簿更新模块:维护几个目标 symbol 的最优档(top of book)或前几档,几十纳秒。
  4. 触发判断模块:硬连线的简单规则——价格触发、价差触发、不平衡触发——几纳秒到几十纳秒。
  5. 订单模板模块:预先打好的订单二进制模板(pre-canned order),命中触发条件后填入数量与价格,立即送 TX。
  6. TX MAC + PHY:发出。

整条路径不经过软件,CPU 完全旁观。CPU 通过 PCIe 下发参数(触发阈值、数量、目标价位),通过共享内存读取统计(命中次数、PnL)。这种架构的延迟天花板由 FPGA 本身的时钟频率(300 MHz 至 500 MHz)和流水线深度决定。

六点三、FPGA 的编程模型

主流的 FPGA 编程语言是 Verilog 与 VHDL,工程上写起来更接近「画电路图」而不是「写代码」。Xilinx Vivado 与 Intel Quartus 是两大综合工具链。一个完整的 tick-to-trade FPGA 设计的开发周期:

近年来 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;信号生成、参数学习、组合管理 走 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 完全不同。常用手段是:

任何一次 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;
    }
};

要点:

七点二、其他无锁模式

七点三、零拷贝与对象池

热路径上每一次 memcpy 都要算账。设计上用 flat buffer + 偏移量 描述消息,避免序列化与反序列化中间结构。SBE(Simple Binary Encoding)就是金融行业为这种 zero-copy 风格设计的二进制协议家族。

对象池(object pool)取代 new/delete:在初始化时一次性分配 N 个对象,运行时从池里取、用完归还。对象池本身可以用 SPSC 队列实现「空闲索引」管理。

七点四、Cache-friendly 数据结构

本地订单簿(local order book)的实现是一个经典考题:

这套设计能让一次行情更新 + 一次 top of book 查询稳定在 50 纳秒以内。

七点五、日志、监控与降级

热路径上不能直接写日志——任何 fprintf 都意味着 stdio 锁、内存分配、系统调用。正确做法是把每条日志写入一个 SPSC 环形缓冲(专门用于日志),由另一个低优先级线程异步消费,落盘或转发到日志服务。监控指标同样走旁路。

降级(degradation)也要事先设计:当 SPSC 满了,热路径不能阻塞、不能 spin 等待。HFT 通常采取「丢弃旧消息 + 计数告警」的策略——丢一条日志比让策略卡住小一万倍。

七点六、用 numba 演示 SPSC 思路

下面给出一个简化版的 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.dequequeue.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 单线程性能。

这只是一个演示

把这个对比的目的限定在「让你看到 Python 标准库与底层结构之间存在数量级差距」,不要把它当成「HFT 可以用 numba 写」的证据。

七点八、确定性与可观测性

低延迟系统不只是「快」,而是「可预测的快」。每一次决策、每一笔订单、每一次状态变化都要打上硬件时间戳,落盘到事后分析。监控仪表盘上必须显示:

这些指标既是性能调优的数据源,也是「策略今天有没有出问题」的第一手证据。HFT 团队会把这套观测体系当成一等公民来建设,与策略开发同等重要。事后分析(post-trade analytics)的能力直接决定策略迭代速度——能看到「为什么这一笔比预期慢 800 纳秒」,团队才能把问题定位到 cache miss 还是 IRQ 干扰;只能看到「平均延迟」的团队,问题永远是模糊的。

七点九、生产级 SPSC 的真实样貌

为了让读者理解上面的演示与生产代码的距离,下面把生产级 SPSC 的关键工程细节列一遍:

下面给出一个稍微更接近生产形态的 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 路径还有几道坎:

这些坎一个比一个吃工程量,每过一道都要付出人月级别的代价。

低延迟系统不只是「快」,而是「可预测的快」。每一次决策、每一笔订单、每一次状态变化都要打上硬件时间戳,落盘到事后分析。监控仪表盘上必须显示:

这些指标既是性能调优的数据源,也是「策略今天有没有出问题」的第一手证据。HFT 团队会把这套观测体系当成一等公民来建设,与策略开发同等重要。


八、Python 在 HFT 的边界与配合

本系列所有代码都用 Python,HFT 这一章也不破例。但必须把边界画清楚:Python 不能写 HFT 的热路径,永远不能。这一节回答两个问题:Python 在 HFT 工程师的工具箱里能干什么,干不了什么;本系列演示性质的 numba 代码到底能映射到生产中的什么部分。

八点一、研究侧:Python 是默认语言

HFT 团队的研究侧(research)几乎全用 Python。这一侧的工作内容包括:

研究侧的代码不上生产,所以延迟无关紧要,写得清楚、跑得对、改得快才是硬要求。

八点二、运维侧:Python 也是默认语言

HFT 系统的非热路径——监控、告警、配置下发、合规报告、市后批处理——大量用 Python。一个典型生产环境会有:

这些都不是热路径,毫秒级的 Python 性能完全够用。

八点三、热路径:Python 的硬上限

Python 解释器有几个先天属性,决定了它无法用于 HFT 热路径:

即使用 numba JIT 编译核心循环,能把单次操作压到几十纳秒,依然有两个问题:一是 JIT 编译和 Python runtime 共存,runtime 任何一次 GC 或锁都会污染热路径;二是 numba 无法表达跨线程的内存序,正确的多线程 SPSC 写不出来。

八点四、Cython、Mypyc、Pythran 的边界

「把 Python 编译成原生代码」的工具链能进一步压低单线程延迟,但不解决根本问题:

在「中频量化」场景(决策周期毫秒级以上、不需要 colocation)里,Cython / numba 是 Python 团队提升关键路径性能的合理选择,单核吞吐能上一两个数量级。但「中频可用」不等于「HFT 可用」。

八点五、混合架构:Python + C++ / Rust

实际生产中常见的混合架构是:

这种架构下 Python 工程师与 C++ 工程师分工明确:Python 工程师产出策略原型与参数,C++ 工程师把原型固化到热路径并保证延迟达标。两边通过协议契约(schema)解耦,任何一方的改动都要走变更评审。

八点六、从 Python 原型到 C++ 生产的迁移路径

如果一个研究侧的 Python 原型最终需要进入 HFT 热路径,标准的迁移路径是这样:

  1. Python 原型确认逻辑:用 pandas / numpy 把策略逻辑跑通,所有边界条件、异常分支都覆盖。这一步的产出物是一份算法规约(algorithm specification)——文字 + 公式 + 测试用例,不依赖具体实现。
  2. 抽出确定性接口:把策略写成纯函数:输入是事件流(行情更新、订单回报),输出是订单意图。所有副作用(日志、监控、I/O)剥离。
  3. C++ 重写 + 等价性测试:C++ 重新实现,与 Python 版本跑同一份录制行情,逐 tick 比对输出。允许的差异只能是数值精度(浮点末位),任何逻辑差异都是 bug。
  4. 集成到热路径框架:把 C++ 实现挂到 SPSC 环形缓冲、订单编码器、风控前置检查后面,跑 dry-run 模式(不真正发单)确认延迟达标。
  5. 影子模式(shadow mode)上线:与现有生产策略并行跑,记录所有「假如真的发了单」的订单意图,与 fill 数据对比。一般跑两到四周。
  6. 小额放量:从 1% 仓位起,逐步放开。
  7. 持续回归:策略后续任何参数调整,都要重跑等价性测试。

这条路径的关键是步骤 3 的等价性测试。Python 与 C++ 的浮点行为、整数溢出语义、对齐假设、空 series 处理都不一样,没有自动化的等价性测试,研究与生产的逻辑会逐渐漂移,几个月后谁也说不清「为什么生产 PnL 跟回测对不上」。

八点七、HFT 招聘与人才结构

观察一下 HFT 团队的招聘画像,能反向印证语言与架构的边界:

一个完整的 HFT 团队大致 30 至 80 人,研究 + 工程比一般在 1 比 2 到 1 比 3。Python 工程师的位置主要在第一档(研究)和最后一档(运维),中间的低延迟核心几乎不沾 Python。把这点放在职业规划上:想做 HFT 系统工程师,C++ 是必备;想做 HFT 策略研究,Python 是默认;二者都不是的话,HFT 不是合适的赛道。

八点八、本系列的定位

回到本系列的边界:

把 Python 的位置摆正,HFT 这一章对学习者的价值才落得下来。Python 不是「不行」,是「适合的位置不在这」;C++ / Rust 也不是「更高级」,是「不得不用」。承认每种语言的物理边界,是工程成熟度的标志。


HFT 系统架构图

上图把第二节到第七节的全部组件放在同一张图里。读者可以把这张图当作一份 checklist:每搭一段、回头核对一次延迟预算瀑布图(第二节图),看实际延迟与目标的差距出现在哪一段。HFT 系统调优的工作循环就是这两张图之间的反复对照:调一处、量一次、回到瀑布图上看新瓶颈在哪、再调下一处。


九、写在最后

HFT 这件事的核心矛盾是:策略上限来自市场结构,工程下限来自物理学。市场结构决定了某一类速度优势能赚多少(队列优势的 PnL、跨市场套利的价差大小),工程能力决定了你站在哪一档的延迟分级里能不能拿到这份钱。两端不匹配,任何一端再强也变不成业绩——再快的系统抓不到没机会的市场,再聪明的策略架不到 200 微秒的延迟。

理性的工程路径是按需分级:不是所有量化策略都要 HFT 化,绝大多数策略在毫秒级就足够,把 colocation、FPGA、内核旁路这些工具留给真正吃延迟的那一档。盲目追求「我们也要做 HFT」会把工程团队拖进无尽的硬件维护与协议跟进,最后发现策略本身的 PnL 撑不起这套基础设施。

对于本系列的读者——以 Python 为主力工具的量化工程师——HFT 一章的价值不在于「让你能写 HFT」,而在于:

下一篇《交易系统架构》会从 HFT 的单机极限退一步,回到一般机构的多机分布式交易系统:策略服务、订单服务、风控服务、回报服务、市场数据服务、跨机房部署、容灾切换。延迟不再是纳秒级,但正确性、可观测性、可恢复性成为新的工程主线。两个层级的对比能让读者更清楚地看到「为速度付的代价」与「为可靠付的代价」是两种完全不同的工程取舍。


参考文献

  1. Aldridge, I. (2013). High-Frequency Trading: A Practical Guide to Algorithmic Strategies and Trading Systems (2nd ed.). Wiley.
  2. Durbin, M. (2010). All About High-Frequency Trading. McGraw-Hill.
  3. Lewis, M. (2014). Flash Boys: A Wall Street Revolt. W. W. Norton.
  4. 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.
  5. Menkveld, A. J. (2013). High Frequency Trading and the New Market Makers. Journal of Financial Markets, 16(4), 712–740.
  6. Hasbrouck, J., & Saar, G. (2013). Low-Latency Trading. Journal of Financial Markets, 16(4), 646–679.
  7. Laughlin, G., Aguirre, A., & Grundfest, J. (2014). Information Transmission Between Financial Markets in Chicago and New York. Financial Review, 49(2), 283–312.
  8. DPDK Project. Data Plane Development Kit Documentation. https://doc.dpdk.org/
  9. AMD / Solarflare. OpenOnload User Guide. https://github.com/Xilinx-CNS/onload
  10. AMD / Solarflare. EF_VI User Guide. https://github.com/Xilinx-CNS/onload
  11. Linux Kernel Documentation. AF_XDP. https://www.kernel.org/doc/html/latest/networking/af_xdp.html
  12. IEEE. (2019). IEEE 1588-2019: Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems.
  13. Intel Corporation. Intel 64 and IA-32 Architectures Optimization Reference Manual.
  14. Drepper, U. (2007). What Every Programmer Should Know About Memory. https://akkadia.org/drepper/cpumemory.pdf
  15. Vyukov, D. 1024cores: Lock-Free Algorithms. https://www.1024cores.net/
  16. LMAX. (2011). Disruptor: High Performance Alternative to Bounded Queues for Exchanging Data Between Concurrent Threads. https://lmax-exchange.github.io/disruptor/
  17. Real Logic. Simple Binary Encoding (SBE). https://github.com/real-logic/simple-binary-encoding
  18. Lockwood, J. W., et al. (2012). A Low-Latency Library in FPGA Hardware for High-Frequency Trading. IEEE Hot Interconnects.
  19. Leber, C., Geib, B., & Litz, H. (2011). High Frequency Trading Acceleration Using FPGAs. International Conference on Field Programmable Logic and Applications.
  20. 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.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

系列导航

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-01 · quant

量化交易

从因子研究到生产执行的量化交易全栈工程。覆盖市场微结构、数据管线、因子构造、组合优化、回测方法论、执行算法、做市策略、高频架构到生产运维。面向策略研究员与工程师。

2026-05-01 · quant

【量化交易】量化交易全景:从信号到订单的工程链路

量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。

2026-05-01 · quant

【量化交易】市场结构:交易所、做市商、暗池、ECN

系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。

2026-05-01 · quant

【量化交易】市场微结构:订单簿、价差、流动性、冲击

系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。


By .