开发高频交易系统
从头开始使用 C++或 Java 基础实现高频交易
塞巴斯蒂安·多纳迪奥
开发高频交易系统
从头开始使用 C++或 Java 基础实现高频交易
塞巴斯蒂安·多纳迪奥 苏拉夫·戈什 罗曼·罗西埃
开发高频交易系统
版权所有 © 2022 Packt Publishing
版权所有。本书的任何部分未经出版商事先书面许可,不得以任何形式或方式进行复制、存储于检索系统或传播,简短引用于关键性文章或评论除外。
这本书在编写过程中已尽力确保所提供信息的准确性。但本书中的信息是在不作任何明示或暗示担保的情况下出售的。作者、Packt Publishing 以及其经销商均不对本书直接或间接造成的任何损害承担责任。
帕克特出版社已尽力提供本书中提到的所有公司和产品的商标信息,通过适当使用大写字母。但是,帕克特出版社不能保证这些信息的准确性。
出版产品经理:Heramb Bhavsar 内容开发编辑:Shreya Moharir 拉胡尔·林巴齐雅 萨菲斯编辑 项目协调员: 法琳·法蒂玛 校对员: Safis Editing 索尼·特加尔·达鲁瓦勒 制作设计师:罗尚·卡瓦莱 市场营销协调员:普里扬卡·马特里
首次发布:2022 年 6 月 1160622
由 Packt 出版社出版。 禮服廣場 35 利弗里街 伯明翰 英国 B3 2PB.
贡献者
关于作者
塞巴斯蒂安·多纳迪奥拥有二十年高性能计算、软件设计和金融计算的经验。目前是彭博首席技术官办公室的一名架构师,他有广泛的专业经验,包括担任外汇/加密货币交易公司的首席技术官、工程主管、定量分析师以及一家高频交易对冲基金的合伙人。塞巴斯蒂安在过去十五年里在哥伦比亚大学、芝加哥大学和纽约大学等多所学术机构教授过各种计算机科学和金融工程课程。塞巴斯蒂安拥有芝加哥大学颁发的高性能计算博士学位、金融管理硕士学位和分析学硕士学位。他的主要热情是技术,但他也是一名潜水教练和一名资深的攀岩者。
苏拉夫·戈什在过去十年里曾在几家专有的高频算法交易公司工作。他为世界各地的交易所建立和部署了极低延迟、高吞吐量的自动化交易系统,涉及多种资产类别。他专长于统计套利做市和交易全球最流动期货合约的配对交易策略。他现任一家位于巴西圣保罗的交易公司的副总裁。他拥有南加州大学计算机科学硕士学位。他的兴趣领域包括计算机架构、金融科技、概率论与随机过程、统计学习与推断方法,以及自然语言处理。
罗曼·罗西耶拥有超过 19 年的经验,主要担任金融行业软件架构师,专注于低延迟、高性能的 Java 软件设计和开发。他目前是 HCTech FX 专有交易引擎的首席架构师。他还建立并领导了 HCTech 的软件开发团队,负责开发 FX、期货和固定收益的 HFT 平台架构。在加入 HCTech 之前,罗曼曾担任 Currenex 实验室的总监,领导团队负责 Currenex 智能定价系统的开发。罗曼拥有瑞士洛桑联邦理工学院通信系统专业的硕士学位。
关于评论者
约翰·里佐运用他对技术和构建复杂计算机系统的热情,在整个职业生涯中为金融市场提供支持。在过去的 17 年里,他担任过多个技术领导角色,包括首席技术官、董事和基础设施架构师,在金融服务行业的各种对冲基金和其他公司任职。他创办并出售了一家公司,该公司开发了帮助管理贷款生命周期和银团贷款市场各个方面的系统。如今,他专注于彭博社首席技术官办公室的基础设施安全。
菲尔·韦尚在彭博社从事与身份、认证和数据科学应用于运营安全挑战相关的项目。在此之前,菲尔共同创办了一家专注于高速数据包捕获和分析的初创公司。他还开发了高频交易系统,设计并实现了身份和安全基础设施设备的固件,构建了合成孔径雷达数据处理工具,并从事承运商路由器的数据平面流量工程。他的主要兴趣是开发与商业问题相关的威胁模型,设计安全的嵌入式系统,以及努力提高我们日益互联世界中的个人隐私保护。
目录
序言
第 1 部分:交易策略、交易系统和交易所
1
高频交易系统的基础
高频交易的历史
1930 年代后期 现代时代 为什么有高频交易? 什么让高频交易与常规交易如此不同? 暗池效应 谁进行高频交易? 开始高频交易需要什么? 高频交易策略 资产类别 流动性
四个逐个数据和数据分布 流动性回扣 12 匹配引擎 13 做市 捡漏 统计套利 15 套利延迟 16 新闻 17 的影响 动量点火 17 返利策略 18 18 非法活动 摘要 20
2交易系统的关键组成部分 理解交易系统...22 交易系统架构 … 24
连接交易所的网关 25 与交易所交易的交易系统 检查通信 API ... 29 订单管理... 30 订单簿考虑...32 制定交易决策的策略 世界卫生组织 关键组件 ... 36 非关键组件 ... 37 指挥与控制…37 服务 ... 38 总结...39
3理解交易交换动力 架构一个大规模处理订单的交易交易所…交易所历史 42 交易所特性理解 42 交换架构 43
44一般订单簿和撮合引擎...46 最佳价格方案... 47 部分填写方案…48 无匹配场景...49 同样价格的多个订单...50 摘要 ... 53 如何设计高频交易系统
4高频交易系统基础 - 从硬件到操作系统 了解高频交易计算机...58 内存管理 中央处理器,从多处理器到多核心
分页内存和页表 主存储器或 RAM ... 62 系统调用 共享内存 64 输入/输出设备 ... 65 使用操作系统进行高频交易系统 用户空间和内核空间...66 进程调度和 CPU67 缝纫... 71 干扰管理…72 编译器的作用 可执行文件格式 … 74 静态链接与动态链接 总结...75
5
动态网络
高频交易系统中的网络理解...78 学习网络概念模型...78 网络通讯 在 高频交易系统 理解开关的工作原理…82 重要的协议概念 ... 89 使用以太网进行高频交易通信...90 使用 IPv4 作为网络层 UDP 和 TCP 用于传输层 为高频交易交易所设计金融协议
6高频交易优化-架构和操作系统 表现心智模型 112 理解上下文 112 上下文切换类型 为什么上下文切换是好的...114 步骤和操作涉及到一个上下文 开关操作 … 115 为什么上下文切换对高频交易不利?116 避免或最小化上下文切换的技术 构建无锁数据结构 修复协议…95 内部网络与外部网络... 101 理解数据包生命周期...102 理解发送/接收(TX/RX)路径中的数据包生命周期...104 接收数据包的软件层... 105 监测网络 … 105 数据包捕获和分析 ... 106 时间分配的价值... 108 时间同步服务…109 总结... 110 为什么需要锁(非高频交易应用程序)... 118 同步机制类型 使用锁存在的问题和效率低下 预取并预分配内存...127 存储器层次结构 … 128 预取以提升性能的替代方法...131 动态内存分配 … 133 基于预分配的动态内存分配替代方案...134 总结 … 135
7
Comparing kernel space and
138What is kernel and user space?139Investigating performance - kernelversus user space140Using kernel bypass141Understanding why kernel bypass isthe alternative142Presenting kernel bypass latencies142Learning aboutmemory-mapped files143Using cable fiber, hollow fiber,and microwave technologies146Evolution from cable fiber to hollowfiber to microwave147
中空纤维的工作原理 微波炉如何工作...148 深入了解日志和统计数据...150 高频交易中登录的需求 ... 150 高频交易中在线/实时统计计算的需求...150 测量性能 … 152 衡量绩效的动机...152 Linux 工具用于测量性能...153 自定义衡量性能的技术…156 摘要…161 第 3 章:高频交易系统的实施
8
C++ - 追求微秒级延迟 C++ 14/17 内存模型
f
f
f \boldsymbol{f} ...166 什么是内存模型?… 166 需要一个内存模型 C++ 11 内存模型及其规则...68 C++内存模型原则 去除运行时决策...176 去除运行时决策的动机...177 虚函数 性能开销 … 179 动态内存分配 运行时性能损失 … 184 有效地使用 constexpr 影响性能的异常 减少运行时间的模板 188 什么是模板?… 188 模板特殊化
τ
τ
tau \boldsymbol{\tau} … 189 为什么使用模板? 模板的缺点 静态分析类型 模板的性能... 192 标准模板库(STL)…193 静态分析 … 195 C++静态分析 静态分析的需求 静态分析步骤...197 静态分析的利弊 构建外汇高频交易系统 摘要
9
Java 和低延迟系统的 JVM Java 基础入门 实时性能指标 … 220 减少影响 的 GC =(予 207 如何保持 GC 事件...208 预热 JVM 分层编译在 JVM...214 优化 JVM 以提升启动 性能 测量 Java 软件的性能 为什么 Java 微基准测试很难创建?… 219 Java threading
τ
τ
tau \boldsymbol{\tau} … 221 使用线程池/队列...222 高性能 任务队列 队列 … 227 循环缓冲区 228 日志和数据库访问 ... 230 外部或内部螺纹?…231 总结... 232
10
Python - 解释型但开放于高性能 Python 简介 利用 Python 进行分析...234 为什么 Python 慢?… 236 我们如何在 Python 中使用库? Python 和 C++用于高频交易 ... 239 在 Python 中使用 C++ 使用 Python 和 C++…240 Boost.Python 库... 241 使用 ctypes/CFFI 加速 Python 代码...243 施维格 … 244 提高 Python 代码在高频交易中的速度...246 总结…248
11
高频 FPGA 和加密
使用 FPGA 降低延迟 ov…250 高频交易的激烈竞争速度的演化...250 现场可编程门阵列简介 ... 251 钻入 FPGA 交易系统...254 现场可编程门阵列交易系统的优势...256 现货交易系统的不利因素... 257 关于 FPGAs 的最后话语...258 探索与加密货币的高频交易 什么是加密货币?259259 加密货币交易是如何运作的? 索引 什么是区块链?… 260 什么是加密货币挖掘?… 260 传统资产的相似之处 贸易和加密货币交易... 260 传统资产交易与加密货币交易的主要差异是 265 与加密货币交易所交易...267 加密货币中的高频交易策略...271 建立加密货币交易的高频系统 如何在云端构建交易系统 ... 275 总结...280
序言
交易市场的世界是复杂的,但利用技术可以更简单。当然,你知道如何编码,但从何处开始?你应该使用什么编程语言?如何解决延迟问题?《开发高频交易系统》一书解答了所有这些问题。
这个实用指南将帮助您导航算法交易的快节奏世界,并向您展示如何从复杂的技术组件构建高频交易系统,这些组件由准确的数据支持。
从高频交易(HFT)、交易所和交易系统的关键组件开始入门,本书迅速进入优化硬件和操作系统(OS)以实现低延迟交易的细节,如绕过内核、内存管理和上下文切换的危险。监控系统性能至关重要,因此您还将掌握日志记录和统计的知识。
当您超越传统的高频交易编程语言(如 C++和 Java)时,您将学会如何使用 Python 来实现高性能水平。没有潜入加密货币的交易书籍就不算完整。
到本书结束时,您将准备好使用高频交易系统进入市场。
这本书是为谁而写的
这本书适用于软件工程师、量化开发人员或研究人员以及 DevOps 工程师,他们希望了解高频交易系统的技术方面以及实现超低延迟系统所需的优化。具有 C++和 Java 的工作经验将有助于您更容易掌握本书涵盖的主题。
这本书涵盖
第 1 章,高频交易系统的基础知识,概述了高频交易的历史。您将了解市场参与者、基本的 HFT 要求(低延迟连接和基础设施)、HFT 与非 HFT 的交易时间范围,以及持仓期/头寸管理(HFT 与超 HFT)。我们还将详细说明赚钱的 HFT 专有策略。
第 2 章,交易系统的关键组成部分,深入解释了交易系统的工作原理。您将了解市场数据如何进入系统,以及处理数据和向交易所发送订单所需的不同功能。
第 3 章,了解交易交易所动力学,介绍了交易所如何成为市场微观结构的一部分。我们将首先介绍交易所的一般基础设施,并讨论匹配引擎的工作原理以及订单是如何匹配和传播给所有市场参与者的。
第 4 章,HFT 系统基础 - 从硬件到操作系统,阐明了硬件和操作系统如何协同工作。您将对软件与操作系统和硬件的交互功能有清晰的了解。本章将从处理器到交易系统进行讲解,包括操作系统、网络、操作系统调度程序和内存在内的所有层次。
第 5 章,网络运动中,表达了网络如何为高频交易提供帮助。你将对网络堆栈的功能以及在交易系统和交易所之间进行通信时的使用有一个清晰的理解。
第 6 章, 高频交易优化 - 架构和操作系统, 阐述了如何从一个常规交易系统创建一个高频交易系统。本节将涵盖多种现代技术, 以实现专为高频交易应用程序的最优低延迟性能。我们将讨论操作系统特性及其调度程序, 并深入研究操作系统的内核功能。
第 7 章,HFT 优化-日志记录、性能和网络,涵盖了交易系统的一个关键部分:日志记录和网络。您将了解日志记录如何帮助监控 HFT 系统,我们将学习如何在 HFT 环境中提高其效率。最后,我们将介绍如何使用网络优化与交易所的通信。
第 8 章,C++ - 微秒延迟的追求,定义了在超低延迟系统中优化缓存、内存和代码执行来使用 C++的情况。您将学习现代 C++功能和技术来有效地编写超低延迟代码。
第 9 章,针对低延迟系统的 Java 和 JVM,详细介绍了在超低延迟系统中优化垃圾回收、通信和数据结构方面使用 Java 的情况。
第 10 章, Python - 解释性但开放高性能, 说明如何在 HFT 系统中使用 Python。本章解释如何在 Python 中创建和使用 HFT 库。
第 11 章,高频 FPGA 和加密,描述了如何使用现场可编程门阵列(FPGA)创建更快的 HFT 系统。它将介绍在云中为加密货币构建 HFT 系统。
从这本书中获得最大收益
这本书假定您熟悉编程、硬件架构和操作系统。因为这本书将讨论减少交易延迟所需的优化,所以拥有计算机工程的基本知识是至关重要的。
大多数高频交易系统都运行在基于 Unix 的操作系统上。我们建议您使用 Linux 操作系统来应用本书中的知识。
这本书是来自许多计算机工程和金融领域的知识宝库。我们建议您阅读其他帕克出版社的书籍,如下所列:
我们也建议您阅读《编译器:原理、技术与工具》和《计算机体系结构:量化方法》等书籍。这些书籍将为您提供更深入的优化 HFT 的知识。
下载彩色图像
使用的惯例
本书中使用了许多文本约定。 代码在文本中:指文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这里有一个例子:"它为一个生产者到一个消费者(OneToOneRingBuffer)或多个生产者到一个消费者(ManyToOneRingBuffer)提供解决方案。" 以下代码:
/* Put header files here or function declarations like below */
extern int add_1(int n);
extern int add(int n, int m);
任何命令行输入或输出都写为如下所示:
>>> import math
>>> math.add_1(5)
6
粗体:表示新术语、重要词语或屏幕上显示的词语。例如,菜单或对话框中的单词以粗体显示。例如:"Load Data 组件(注释 1)将帮助获取历史数据。"
小贴士或重要说明
出现如此。
联系
我们的读者反馈总是受欢迎的。 如果您对本书的任何方面有任何疑问,请发送电子邮件至 customercare@packtpub.com,并在邮件主题中提及书名。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感谢您提供该位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供相关材料的链接。
分享您的想法
读完《开发高频交易系统》后,我们很想听听您的想法!请点击此处直接前往亚马逊评论页面,分享您的反馈。 您的评论对我们和科技社区很重要,将帮助我们确保我们提供优质的内容。
第 1 部分:交易策略、交易系统和交易所
高频交易(HFT)的历史概况,市场参与者,HFT 的基本要求(低延迟连接和基础设施),HFT 与非 HFT 的交易时间范围,以及持仓期/头寸管理(HFT 与超 HFT)。我们还将讨论 HFT 的地点。本书不是关于交易或 HFT 的业务,而是关于如何使用 Java、C++和 Python 具体实施 HFT 系统。您将了解交易系统的工作原理以及可以运行的交易策略。
这部分包含以下章节:
第 1 章,高频交易系统的基础
第 2 章,交易系统的关键要素
第 3 章,理解交易交易动力学
高频交易系统的基础
欢迎来到开发高频交易系统! 高频交易(HFT)是一种自动化交易形式。在过去 20 年里,HFT 在媒体和社会中越来越受到关注。2014 年,迈克尔·刘易斯撰写的一本名为《闪电男孩:华尔街的反抗》的书在纽约时报最畅销书榜上占据榜首 3 周。它涉及对 HFT 行业及其对交易世界的影响进行的调查。学者、金融界和非金融界都对这种交易形式感兴趣。与此同时,这个全新的交易时代也引发了很多恐惧,并让机器获得了越来越多的控制权。
本书的目标是回顾高频交易(HFT)是什么以及如何从技术角度构建这样的系统。高频交易是一个涉及计算机架构、操作系统、网络和编程等多学科知识的复杂事务。通过阅读本书,您将了解如何从头开始构建交易系统,并使用最先进的技术选择来优化速度和可扩展性。我们将本书分为三个主要部分。
在第一部分中,我们将介绍高频交易策略是如何运作的,以及我们可以预期从高频交易中获得何种交易。然后我们将介绍高频交易系统的功能。我们将以对交易所如何运作的描述来结束这一部分。
在本书的第二部分,我们将解释操作系统和硬件的理论,以及优化交易系统所需的知识,同时考虑硬件和操作系统的特性。
这将详细解释如何使用 C++、Java、Python 和 FPGA 来创建一个 HFT 系统。我们还将把这些知识扩展到加密交易,并将探讨如何在云中建立交易系统。
在这一章,我们将讨论我们是如何进入高频交易的。我们将回顾哪些交易策略适合高频交易。我们将详细解释是什么使高频交易与普通交易如此不同。 我们在本章的目标是涵盖以下主题:
高频交易的历史
高频交易
参与者有谁
高频交易中有效的交易策略
高频交易的历史
让我们讨论 1930 年前的交易所和金融市场的历史。 当我们谈论高频交易时,很难给出它开始的确切日期。我们需要回到人类接触贸易的原始时代。在现代现金发明之前,古人严重依赖互赠经济进行产品和服务的交易。根据彼得·沃森的说法,远距离贸易可以追溯到近 15 万年前。随着人口、货物和金钱的增加,贸易逐渐成为人类最重要的活动之一。赚钱显然意味着更多的业务。其中一个参数是速度。如果你进行更多交易,你就会赚更多钱。许多故事描述了商人渴望获得更好的交通工具等技术,以便更快地成交或更快地获取消息,从而利用那些无法获得这些新技术手段的人。
我们没有等待很长时间就看到了涉及那些在技术优势方面领先于他人的不公平贸易案例。1790 年,一位乔治亚州代表在美国众议院发言,揭露了高速交易者的行为。确实,国会正在讨论财政部长亚历山大·汉密尔顿的提议,即美国政府吸收各州在革命期间积累的前期债务(1790 年资金法案)。立即学会该决定的交易者马上购买或租用快船。他们的目标是超越信差,买入旧债,因为法案的通过会提高市场价值。在 20 世纪,高频交易或 HFT 的概念出现。
1930 年代后期
交易是商品交换的过程。它可以涉及金融产品、服务、现金、数字资产等。交易的目标之一是从这些交易中获利。交易数量与资产交换产生的金钱数量相关。当我们设法提高交易次数与时间的比率时,我们可以提高长期盈利能力。因此,提高交易次数的能力至关重要。交易参与者很快认识到他们需要缩短交易时间,并开始集中在某些特定场所。他们习惯在这些地点下订单,现在被称为交易所(或交易大厅)。高速自动化交易的扩张参与了一些主要事件。
1969 年:Instinet 是首批自动化系统基础设施之一。它加快了高速交易的结合。
1971 年:全国证券交易商协会自动报价系统(纳斯达克)于 1971 年创建,进行电子交易。
这是世界上第一个电子股票市场。最初,它只用于发送报价。
1996 年:岛屿 ECN 是美国股票交易的开创性电子通信网络,而 Archipelago 则通过创建 Archipelago Exchange(ArcaEx)促进了在美国交易所的电子交易。
2000 年代:10%的交易是 HFT 交易。
金融行业在 2000 年代初期吸引了越来越多的技术人才。通过引进这些技术人才,该行业开始快速发展。自动化、吞吐量、性能和延迟成为交易公司熟知的词语。高频交易交易量超过市场交易量的
10
%
10
%
10% 10 \% 。到 2009 年,
2
%
2
%
2% 2 \% 个交易公司占据了
75
%
75
%
75% 75 \% 的股票交易量。如今,仅有少数公司如 Virtu、Jump、Citadel、IMC 和 Tower 仍在从事高频交易。
现代时代
1930 年代后期关注股票市场(和商品市场的交易所)的透明度和监管。现代时代突出了电子交易,并提高了透明度。2000 年,美国证券交易委员会(SEC)提出了中央限价订单簿(CLOB)。CLOB 是一个透明的系统,用于匹配参与者之间的订单。更多的交易所(如 Island 和 Arca)进入了交易领域。交易公司、对冲基金和电子交易参与者的数量不断增加。他们创建了自己的技术栈,以更快地交易并保持竞争力。10 年后,只有少数交易公司设法保持竞争力,成为占所有股权交易量
75
%
75
%
75% 75 \% 的
2
%
2
%
2% 2 \% 。
高频交易(HFT)的必备技能需要大量投资:资金、人员和时间。它是低级系统专业知识和量化交易专家的结合,也吸引了聪明的投资者(投资者日益精通技术)。能够为设计超低延迟系统创建出色代码的工程师非常昂贵。只有少数工程师拥有这些技能。这种系统的性能要求专门的硬件。路由器、服务器和网络设备也很昂贵。因此,经验和进入门槛将阻碍许多新人进入,并限制竞争。除了我们之前讨论过的五家公司外,还有一些小型交易商店,利用他们在市场结构或某些技术事实中发现的优势来交易高频交易策略。大型高频交易公司是负责移动大部分股票交易量的公司。如今,高频交易估计占到美国股票(股票)交易量的至少 70%。自 2009 年顶峰以来,高频交易的市场份额和盈利能力都有所下降。
继 2015 年后,数字货币的增长为高频交易者开辟了新的机会。如今,我们可以看到许多知名的加密货币交易所,如 Coinbase、Binance 和数百家其他加密货币交易所,正在大幅增长应用高频交易策略。 这个现代时代确实将技术和自动化交易稳固下来了。交易模型是数据驱动和模型驱动的。市场数据业务无疑成为交易的重要组成部分。交易所和交易公司开始通过生成或收集市场数据赚钱,这些数据是任何算法交易员的原材料。
为什么有高频交易?
高频交易旨在每秒完成大量交易。通过这种方式,公司可以更快地对变化的市场做出反应。他们可以利用比没有这种速度时更多的机会。此外,大型机构从高频交易中获益,通过向市场提供大量流动性获得微小但可观的优势。他们下单数以百万计,这是他们系统所能胜任的。他们帮助市场,因此能够提高有利可图的交易收益,并获得更好的价差。由于回报率很低,他们必须进行大量交易才能获益。除了这些收入,他们还将获得交易场所提供的返点或优惠交易费,以吸引高频交易公司。
什么让高频交易与常规交易如此不同?
高频交易应该具有尽可能最短的数据延迟(时间延迟)和最高水平的自动化。高频交易与算法交易和自动化交易有关。因此,参与者选择在具有高度自动化和交易平台集成的市场进行交易。公司利用编程有精确算法的计算机来寻找交易机会并在算法交易中执行订单。为了提高交易速度,高频交易员使用自动化交易和快速连接(以及撤销或修改)。这是由于交易公司所拥有的技术以及交易所的技术。以下交易所已投资数亿美元用于高频交易技术:
纳斯达克交易所,位于纽约市,是世界上第一个电子股票交易所。其所有股票均通过计算机网络进行交易。它在 1971 年通过取消实体交易场所和现场交易的要求,革新了金融市场。它是世界第二大股票交易所,市值排名。纳斯达克综合指数的一半由科技公司构成。消费行业占综合指数的不到 20%,排名第二,其次是医疗保健行业。
纽约证券交易所(纽交所),位于纽约市,是世界最大的股票市场交易所。2013 年,洲际交易所(ICE)收购了纽交所。
伦敦证券交易所(LSE),位于英国伦敦,是欧洲最大的证券交易所,也是英国主要的股票和债券交易所。它成立于 300 年前左右。
东京证券交易所(TSE)是日本最大的证券交易所,总部位于东京。它成立于 1878 年。该交易所拥有 3,500 多家上市公司。由日本交易集团运营的 TSE 是世界上最大和最著名的日本公司的所在地,包括丰田、本田和三菱。
芝加哥商品交易所(CME),有时也被称为芝加哥商品交易所,是位于伊利诺伊州芝加哥的一家受监管的期货和期权交易所。CME 交易的行业包括农业、能源、股票指数、外汇、利率、金属、房地产和天气。
直接边缘,泽西城。它的市场份额迅速上升至美国股票市场的第十位,每天通常交易超过 20 亿股。更好的替代交易系统(BATS)全球市场是一家总部位于美国的交易所,交易各种资产,包括股票、期权和外汇。2017 年,CBOE 控股公司收购了它,它于 2005 年创立。BATS 全球市场在被收购前是美国最大的交易所之一,以为经纪交易商、零售和机构投资者提供服务而闻名。
芝加哥期权交易所
所有先前的交换都受到多个层面的控制:
交易限制
交易系统透明度(市场参与者之间共享的关于架构细节以及订单处理方式的信息)
被接受的金融工具类型
发行人的限制
大多数监管交易所的订单规模是一个问题。大宗交易对市场有重要影响(可能会产生市场冲击)。交易者使用备选交易系统(ATS),与传统交易所相比,这些系统受到的监管要少得多(不需要透明度)。暗池是最常见的 ATS 类型。美国目前有大约 30 个暗池,占美国合并交易量的四分之一。
暗池对高频交易商是有益的,因为他们能够满足速度和自动化需求,同时缩减了费用。这不是其他任何类型交易的情况,这使高频交易与一般交易不同。在下一节中,让我们了解更多有关暗池的信息。
暗池效应
为了财务安全,在暗池中买卖订单不会显示(价格和交易量)。换言之,暗池是不透明和匿名的,因为订单簿不会公开。由于在这种交易交易所无法看到订单规模,下大单的投资者不会影响市场。由于其他参与者看不到订单规模,暗池以固定价格执行这些大单。这减少了交易交易所带来的负面滑点。
暗池池被要求在交易发生后通知交易,尽管缺乏交易前透明度。
高频交易和暗池有着复杂的互动。暗池的兴起部分是由于投资者寻求保护,以免受到高频交易员在公开交易所的欺诈性活动的影响,而高频交易员也发现无法通过"扫描"来了解暗池中的大单。暗池引入了市场透明度的缺失,使得能力不足的参与者(即卖方)能够跟上当时的商业惯例,这些惯例与最先进的状态并不一致。当然,海因·博德克写了两本书(《高频交易的问题》和《市场结构危机》),探讨在暗池中发现不寻常的订单类型。
另一方面,一些黑暗池鼓励高频交易者在他们的交易所交易。高频交易策略增加流动性和订单成交的可能性。黑暗池帮助高频交易实现其速度和自动化需求,同时仍有较低的开支。高频交易者对黑暗池中订单规模的减少负有责任。黑暗池受到定位隐藏大单的探针交易策略的影响。
因此,如果存在这些高频交易策略,暗池的利益可能会受到损害。例如,在 2014 年,纽约州总检察长起诉巴克莱银行的暗池业务,指控其误报了巴克莱银行在暗池中的交易量。在 2016 年,巴克莱银行向美国证券交易委员会支付了数百万美元罚款,并向纽约州支付了数百万美元。
暗池可以施加某些限制,以防止高频交易者从事掠食性行为。目标是减少 ping 交易策略。 2017 年,Petrescu 和 Wedow 施加了最低订单量,以最小化这种类型的策略。
我们可以花更多时间讨论高频交易对暗池的影响的利弊,但最终我们会说,拥有更多流动性和更快的执行速度的优势足以让一些暗池支持高频交易。只要投资者充分了解交易场所的工作原理,从而做出有依据的判断,这对投资者来说是公平的。
我们已经讨论过主要交易所的位置。现在我们将在下一节介绍高频交易参与者。
谁进行高频交易?
每个人都有。从买方到卖方,从 ECN 到交易商和券商市场,它们都使用高频交易。高频交易由自营交易业务主导,涵盖了广泛的产品,包括股票、衍生品、指数基金、交易所交易基金(ETF)、货币和固定收益工具。自营交易业务占现有高频交易参与者的一半,多服务券商自营交易部门占不到一半,其余是对冲基金。凯盛集团(由 Getco 和 Knight Capital 合并而成)和主要银行的交易部门是该领域的主要参与者。一些新型场所(如 Dealerweb 的 OTR Exchange 和 IEX)正寻求提供一个卖方交易商觉得安全的交易场所,并由高频交易提供流动性。
高频交易已成为市场的主要参与者。他们也在抓住零售流。Citadel 正在控制大部分的零售流。
开始高频交易需要什么?
参与 HFTs 的人必须具备以下条件:
快速计算机:HFT 在大多数情况下关注单核处理能力,策略通常不使用并行处理。
在美国,我们使用共同位置。这是一个所有高频交易参与者都有他们的生产服务器的地方。他们将支付将他们的计算机与交易所的计算机服务器共同位于同一数据中心,以降低延迟并缩短完成交易所需的时间,甚至是微秒。连接所有市场参与者交易系统与服务器的电缆长度相同,以确保没有参与者拥有优势。美国证券交易委员会已发布广泛征求有关共同位置费用以及影响股票市场结构的其他问题的反馈的请求。为确保市场参与者之间的公平性,重要的是共同位置费用应合理定价。美国证券交易委员会邀请共同位置方报告他们的费用。
低延迟:在高频交易中,延迟指从数据到达交易员计算机、交易员根据数据下单以及订单被交易所接受的时间。订单可能在最有利的时候与其他交易员下的众多订单一起进入市场。在这种情况下,存在与大量其他人竞争的危险。订单的盈利性可能不如预期。高频交易员能够以难以置信的快速速度下单,这得益于被宣传为低延迟或超低延迟的技术。使用专门设计用于减少数据从一个地方到另一个地方的传输延迟的设备很重要。
计算机算法,它们是 AT 和 HFT 的核心,以及实时数据源,可能会损害收益。
在前几个部分中,我们了解了高频交易商在哪里进行业务。我们还讨论了进行更快交易的技术前提。现在让我们深入了解什么是 HFT。
高频交易策略
高频交易策略是算法交易策略的一个子集。它们以微秒(有时纳秒)的速度执行。这些策略必须意识到这一时间限制才能高效执行。它们部署尖端技术来比竞争对手更快获取信息。这种策略的主要目标是 tick-to-trade,即对市场数据作出响应的时间。正如我们将在下一章解释的那样,在尖端机器上托管交易策略很重要,它们还必须在托管环境中运行。
我们将定义应用程序的领域和一些术语,以在讨论高频交易策略时使用。
资产类别
高频交易策略可以应用于任何资产类别,如股票、期货、债券、期权和外汇。我们也有使用高频交易策略交易加密货币,尽管速度的定义不同(因为结算时间的原因)。
流动性
玩家对某种资产产生交互意愿被称为流动性。 我们将深度定义为给定资产的价格水平数量。我们会说一本书很深,当它有很多层(层)给定资产。我们将定义一本书是大或广,如果每层的成交量很高。如果一本书很深或很大,我们将定义给定资产的流动性为流动。这一陈述的结果是,无论何时交易者想进行交易,都会更容易买卖这种资产。结果,交易者都想要拥有大量流动性的交易所。加密货币交易所目前很难找到流动性。
逐笔数据和数据分布
高频交易每微秒都会生成订单。由于有很多参与者,这可能会产生大量数据。当我们研究高频交易数据以创建交易策略模型时,数据存储将是关键。
每个交易日,在流动性市场上会产生成千上万的价格变动(从一个订单到另一个订单),这构成了高频数据。这些数据在时间上呈现随机分布。高频交易数据呈现出肥尾分布。这意味着交易策略需要考虑可能出现大量损失的可能性。
市场数据的分布可分为两类:
波动率聚集:大变化会导致后续大变化,无论是正负,而小变化则会导致后续小变化。
远程依赖(长期记忆)指的是当两个位点之间的时间间隔或空间距离增加时,它们之间的统计依赖性衰减的速度。
流动性回扣
为了支持股票流动性的提供,大部分交易所都采用了 maker-taker 模式。在这种安排中,放置限价单的投资者和交易商通常会从交易所获得一定的返佣,因为他们被认为为股票流动性做出了贡献,即'makers'。
那些下市价单的人被认为是提供流动性的接受者,交易所会对他们收取小额手续费。尽管这些回扣通常只有每股几分之一美分,但对于高频交易者每天交易数百万股的情况来说,这些回扣加起来可能会是一大笔数额。许多高频交易公司使用旨在尽可能利用更多流动性回扣的交易技术。
匹配引擎
交易所的交易系统的核心,自动匹配买卖订单的软件程序,取代了交易所专业人士传统的工作,被称为"撮合引擎",对于保证交易所的高效运营至关重要,因为它负责匹配所有股票的买家和卖家。撮合引擎存储在交易所的计算机上,这也是为什么高频交易企业力求靠近交易所服务器的主要原因。我们将在第 3 章"了解交易所动态"中学习相关内容。
做市
在深入探讨做市商是什么之前,我们需要解释市场参与者和做市商之间的区别。
做市客/做市商
图 1.1 代表了交易所的限价订单簿。当交易策略下达的订单接近订单簿顶部(表示买入和卖出的最佳价格),我们称这是一个进攻性订单。这意味着该订单很可能与其他订单配对成交。如果订单执行,这意味着市场流动性被移除;它是一个市场接手者。当交易者以订单簿顶部的卖出价格下达买入订单时,我们称之为跨价。如果订单较为被动,该订单将不会从市场中移除流动性;它是一个做市商。
图 1.1 - 订单簿 - 被动/主动订单
让我们看看做市商策略。
做市商策略
交易公司可以在交易所提供市场制造服务。随着时间的推移,做市商协助买家和卖家配对。市场做市商并不是基于其相关资产购买或出售证券,而是维持持续的买卖报价,并从报价差中获利。
为减少长期持有股票的风险,每次购买都应该配合销售,每次出售都应该配合购买。如果某股票交易价格为
$
100
$
100
$100 \$ 100 ,"做市商"可以同时保持在
$
99.50
$
99.50
$99.50 \$ 99.50 的买入价和
$
100.50
$
100.50
$100.50 \$ 100.50 的卖出价。如果他们成功找到买方和卖方,即使没有其他人想买入,也能让那些想立即卖出的人实现出售,反之亦然。
做市商,换句话说,他们提供流动性-他们使交易更加简单。对于最交易活跃的股票,这种技术并不重要;然而,对于较小的公司(交易量小于大公司),增加交易量以促进交易可能是关键。做市是许多高频交易企业使用的一种方法。他们通过快速变更报价并进一步缩小价差来与其他人竞争:因为他们的做市业务可以迅速扩大到大量交易,所以他们愿意每次赚的钱更少。然而,高频交易公司的技术可用于其他目的,如套利(利用相关证券之间的细微差异赚钱)或执行(分解大型机构的交易以最小化市场影响)。我不会深入讨论太多,因为重点是高频交易能做的不仅仅是做市。速度才是最重要的。
做市可以通过订单流分析来实现
大量的买卖可以推动市场价格,基于动量。
流体的流动(买单和卖单有多大:小、中或大)。
动量耗尽(当订单流量枯竭时,这可能预示着价格反转)。
做市交易是高频交易者最广泛使用的交易策略。我们将在下一节讨论其他 HFT 策略。
炒股
皮肤掉落是一种交易方法,专注于从细微价格波动中获利并迅速转售。皮肤掉落是日内交易中用来描述一种专注于从微利中产生大交易量的技术。皮肤掉落需要严格的出场计划,因为一次重大损失可能会抵消交易者努力挣得的所有小赢利。要使这种技术奏效,你需要必要的工具,如实时行情、直接接入券商和进行大量交易的耐心。
寻找短期交易机会是一种概念,大多数股票将完成趋势的第一阶段。但从那里去向何处仍不清楚。一些股票在该早期阶段后停止上涨,而其他股票继续上涨。目标是尽可能从更多小额交易中获利。另一方面,让获利持续的思维旨在通过扩大获胜交易规模来最大化良好交易结果。通过增加获胜比例来弥补收益幅度,这种技术实现了结果。很少有长期交易者能够在仅赢得
50
%
50
%
50% 50 \% 或更少交易的情况下获得良好利润 -区别在于获胜交易的收益远大于亏损。
统计套利
有效市场假说(EMH)声称金融市场在信息上是有效的,这意味着交易资产的价格是准确的,并且在任何一个时刻代表所有已知信息。基于这一假设,如果没有任何基本消息,市场不应该波动。然而,这并非如此,我们可以用流动性来解释。
在一天之中,许多大型机构交易实际上与信息无关,而是与流动性有关。认为自己过度暴露的投资者会积极对冲或出售头寸,从而影响价格。寻求流动性的投资者常常愿意支付溢价以退出头寸,为流动性提供者带来利润。尽管这种从知识中获益的能力似乎违背了有效市场理论,但统计套利正是建立在此基础之上的。
统计套利旨在通过利用价格和流动性之间的相关性来获利,该策略基于统计模型对资产的预期价值来识别资产的被错误定价。
同一证券在不同场所的短期价格差异,或相关证券的短期价格差异,被用于统计套利,通常称为统计套利。统计套利基于这样的假设:证券市场存在价格差异,但会很快消失。由于价格差异可能只持续数秒,算法交易非常适合统计套利。
在多个场所交易同一证券时,例如,算法跟踪证券交易的所有位置。当价格差异出现时,算法在较低的市场买入,在较高的市场卖出,从而获得利润。由于此类差异的机会窗口很小(不到 1 毫秒),算法交易非常适合这种形式的交易。
统计套利在投资于关联证券时变得更加具有挑战性。一个指数和该指数内的单一股票,或一只单一股票和同一行业内的其他股票,都是相关证券的示例。在关联证券中,统计套利方法需要收集大量历史数据,并估算两个市场之间的典型关系。当存在偏离常态的变化时,该算法就会执行买入或卖出操作。
延迟套利
现代股票市场复杂,需要高度技术的系统来管理大量数据。由于其复杂性,数据不可避免地以不同的速度被处理。延迟套利利用市场参与者的不同速度。延迟套利旨在利用高频交易者更快的速度,通过利用高速光纤、优越的带宽、共同位置的服务器以及来自交易所的直接价格源等方式,在其他市场参与者之前下单。
低延迟套利背后的假设是,在美国,用于确定所有美国证券交易所的国家最佳买卖价差(NBBO)的综合数据源比高频交易者可获得的直接数据源更慢。HFT 程序的算法可以比许多其他市场参与者更快地读取交易数据,在证券信息处理器(SIP)数据源(这是综合的美国证券交易所价格数据源)之前的一小部分秒内看到价格变化,这主要是由于其更快的速度。这实质上为 HFT 软件提供了在其他市场参与者之前获取价格走向信息的能力。
新闻影响
信息是所有交易的核心,它被用于作出财务决策。算法交易系统利用新闻数据来生成交易决策的做法被称为信息驱动策略。
算法已经被开发出来阅读和分析来自主要新闻机构的新闻报道以及社交媒体上的内容。任何有可能改变市场价格的新闻都会导致算法进行购买或者出售。
高频交易者已经习惯于使用以信息为驱动的方法,以至于某些新闻机构现在以一种使计算机能够轻松分析它们的方式来包装他们的新闻稿。他们使用预先确定的关键词来描述有利或不利的事件,例如,这样一个算法就可以根据新闻稿中的关键词采取行动。在计划发布之前,新闻提供商还将新闻报道放在关键地理区域(如主要金融中心)的服务器上。这减少了数据从一个位置移动到另一个位置所需的时间。对于这种服务,新闻服务提供商收取额外费用。
根据先前推特上关于美联社被黑的报道,信息驱动型举措使用社交媒体正在增长。2013 年,一名黑客发推特称白宫发生爆炸,总统受伤,全球股票市场顿时暴跌,因算法从可信来源获取了坏消息并开始抛售。
下一步,让我们学习动量点火交易技术。
动量点火
您有机会进行金融交易,如果您发送到市场的订单可能导致价格变动,而您知道这一点。动量启动交易技术的目标是实现这一点。目标是让其他算法和交易员开始交易某只股票,从而引起价格变动。从本质上说,动量启动方法试图欺骗其他市场参与者相信即将发生大幅价格变动,从而引发他们的交易行为。结果,价格变动成为一种自我实现的预言:交易员相信价格将会变动,他们的行为也导致了这一变动。
向订单簿中发送大量订单并随后取消这些订单是动量引燃方法。这会营造出股票交易量出现巨大波动的假象,可能会引发其他交易者下单,从而导致短期价格趋势的开始。在尝试点燃市场行情之前,动量引燃方法包括执行真实的目标交易头寸。这意味着完成一笔对市场影响不大的交易。这使采用动量引燃方法的交易者能够在价格运动开始之前进入市场。在交易完成后,提交大量订单并随后取消这些订单,希望其他交易者跟随并推动价格变动,从而设置动量引燃。
交易员使用动量引爆技术后,当价格开始上涨时,便退出了最初的头寸并获得利润。
动量点火方法需要使用特定的订单类型,交易者只能利用能在短时间内发送和取消大量订单的算法来执行它们。
返利策略
市场订单交易者必须向交易所支付费用,而限价订单在增加流动性时则获得返现。因此,特别是从事高频交易的交易者会提交限价订单来建立市场,从而为交易所创造流动性。对于下大量限价单的交易者来说,这种定价方式风险较低无疑很有吸引力。
交易商-制造商定价
正在 Ping
通过下单少量可执行订单(通常为 100 股)来了解交易交易所和暗池中大订单的策略。
为了减少大订单对市场的影响,买方企业利用这种交易技术将大订单拆分成多个小订单。这种算法将这些订单缓慢地输入交易所。为了检测到这些大订单的存在,高频交易公司为每只上市股票安排 100 股的买卖报价。
这些 ping 交易将提醒高频交易参与者大订单的存在。高频交易商将利用这些信息确保从买方获得无风险利润。
一些重要的市场参与者已将 "pinging" 与"诱饵"进行了比较,因为其主要目标是诱使大型机构出具其庞大的订单,从而暴露它们的交易意图。
非法活动
美国证券交易委员会(SEC)、联邦调查局(FBI)以及其他监管机构近年来对指称的高频交易(HFT)违规行为进行了打击。以下章节是可能的违规行为的例子。
前跑
基于尚未公开发布的信息下达交易订单被称为"提前交易"。证券交易委员会(SEC)和金融业监管局(FINRA)已将这种做法定为非法。有人将"提前交易"一词用于描述高频交易公司利用算法交易技术来识别给定工具的大量新订单的做法。在这些大量订单进入市场之前,我们下单以从中获利。高频交易公司可以在购买资产后立即售出,从中获得收益。尽管这种交易方式在法律上是合法的,但监管者对此表示关切,未来可能需要予以规制。
欺骗
欺骗交易策略是非法的。它包括发送无意执行的订单,只是为了让其他市场参与者对这些订单做出反应。他们可能会发送订单来达到这个价格水平。同时,初始订单被取消,并且欺骗者从市场剩余的其他订单中获利。
多德-弗兰克金融改革法案第 2010 年修订版具体针对这种做法,甚至在那之前,金融业监管局的规定就禁止这种旨在误导市场的订单行为。立法者于 2014 年披露的首起刑事"闪欺"案涉及一名芝加哥交易员被指操纵期货市场。
层叠
分层与欺骗是一样的,只是订单是在不同价位下下单,以营造某种证券有很大需求的假象。这一策略的结果与常规欺骗一样。由于技术的急速进步,大规模的市场操纵可能在几秒内就发生。分层与普通欺骗一样,通常都是非法的,在 FINRA 规则中被禁止。
即使这些策略现在被禁止,我们也需要记住,一些交易所的监管程度较低或没有监管。我们将在第 11 章"高频 FPGA 和加密货币"中了解到,这些策略仍然可以发挥作用。
摘要
我们在本章回顾了高频交易(HFT)的起源。我们分析了 HFT 相比一般交易有何独特之处。我们还讨论了任何 HFT 交易系统都能支持的不同策略类型。我们谈论了交易系统的历史。本章的目标是让您深入了解什么是 HFT 以及我们可以使用哪些交易策略。
在下一章中,我们将讨论交易系统的主要功能。我们将描述如何建立交易系统。
2
交易系统的关键组成部分
在上一章中,我们学习了如何创建高频交易(HFT)策略。在本章中,我们将研究如何将这些策略转换为实时软件,以连接交易所并实际应用您先前学习的理论。我们将描述一个能够交易资产的交易系统的功能。
在本章中,我们将涵盖以下主题:
理解交易系统
制作交易系统与交易所交易
订单管理
制定交易决策的策略
到本章结束时,您将能够设计一个交易系统,将交易系统连接到交易所,并构建限价单薄。
理解交易系统
为 HFT 交易设计交易系统不仅需要编程和交易知识,还需要其他更多技能。本书后续章节将深入描述这些部分,这将为您在设计 HFT 系统中带来优势。在本节中,我们将讨论交易系统设计的基础。设计系统最关键的部分之一是详细描述需求。交易系统的目标是支持您的交易想法。任何交易策略都始于获取数据,并最终根据这些数据做出决策。交易系统将监督收集市场数据(即价格更新)并向交易所发送订单。此外,它还将收集交易所有关订单的信息。这些市场更新可能代表订单的任何状态:已取消、已拒绝、已完成或部分完成。它还将计算衡量您投资组合绩效的指标(如利润和亏损、风险指标或交易系统不同过程的信息)。
在决定是否创建这种类型的软件时,我们需要记住以下几点:
资产类别:了解将在交易系统中使用的资产类别将改变该软件的数据结构。每个资产类别都是独特的,都有自己的特点。建立一个用于美国股票的交易系统与建立一个用于外汇(FX)的系统是不一样的。美国的股票主要在纽约证券交易所(NYSE)和 NASDAQ 两个交易所进行交易。这两个交易所共有大约 7,000 家上市公司(代码)。与股票不同,外汇包括 6 种主要货币对、6 种次要货币对和 6 种 exotic 货币对。我们可以添加更多货币对,但不超过 100 个。与美国股票市场只有两个主要交易所不同,外汇市场将有数百个交易所。符号数量和交易所数量的变化将改变交易系统的架构。
交易策略类型(高频,长期头寸):软件体系结构将受交易策略类型的影响。HFT 策略需要在非常短的时间内传输订单。对于美国股票而言,标准交易系统将决定在微秒内发送订单。芝加哥商品交易所(CME)交易系统的延迟在纳秒级别。根据这一发现,技术将在软件设计过程中发挥重要作用。如果我们只考虑编程语言,Python 不适合速度,我们会更喜欢选择 C++或 Java。如果我们希望采取长期头寸,例如持续数天的头寸,交易员比其他人更快获得流动性的速度无关紧要。
用户(或交易技术)数量:随着交易者人数的增加,交易策略的多样性也在增加。这表明订单数量将会增加。在向交易所提交订单之前,我们必须确保即将发送的订单是有效的;我们必须确保特定工具的总头寸尚未达到。
交易策略正受到交易行业不断增加的规则的调节。我们将测试我们希望发送的订单的合规性,以确保我们的交易策略符合法规。所有这些测试都将增加计算所需的时间。如果我们有大量订单,我们将不得不按顺序对一种工具执行所有这些验证。如果程序速度不够快,订单处理时间会更长。用户越多,交易系统就必须越具有可扩展性。
这些变量改变了您对即将创建的交易系统的思考方式。让我们在以下部分讨论一个简单的交易系统的设计。
交易系统架构
以下架构图表示交易系统架构。在该图左侧部分,我们可以看到交易场所。交易场所是一个更广泛的术语,用于任何为多方的证券和/或衍生产品匹配买卖订单的平台。换言之,交易场所可以是交易所、ECN、聚合器或银行。交易系统与交易场所进行通信,以从所有参与者那里收集价格更新信息并发送订单。为此,交易系统需要一个称为网关的软件组件,用于确保交易系统与交易场所之间的通信。账簿构建器将从网关收集的数据构建限价单薄。最后,策略将通过订单管理器将订单发送到交易场所。订单管理器负责收集来自系统策略的所有订单,并跟踪订单的生命周期。这些组件都是将订单发送到市场的关键路径的一部分。
图 2.1 - 交易系统架构设计 此外,我们还观察其他不太关键的服务,如命令和控制,负责启动系统的组件。在算法交易中,查看器非常关键,因为它们将为您提供系统所有组件、订单和交易以及您认为监控交易策略很重要的指标的状态。算法交易自动化了交易。因此,跟踪您的交易系统和交易策略的健康状况非常重要。在高频交易中,几微秒可能会造成巨大损失,这一点尤为重要需要明白。拥有能够有效报告警报的查看器和用户界面至关重要。
我们将更深入地讨论交易系统的关键组件。
连接贸易交易所的网关
网关是交易系统中与交易所和交易系统通信的组件。它们是至关重要的,因为它们在执行时间方面最贪心。按设计,它们必须从网络获取数据并将该数据提供给系统的其余部分。这种操作对系统资源和操作系统来说是一种需求。价格更新由交易系统收集,然后代您传输订单。为此,您必须首先编写所有您在没有交易系统的情况下进行交易时会执行的程序。如果您想通过低买高卖赚钱,您必须首先决定要交易的产品。在选择了这些产品后,您应该从其他商家那里获得订单。
其他交易商将通知您他们愿意交易金融资产的意愿,并确定规模、价格和数量。一旦收到足够的您想交易的产品的订单,您可以选择与之谈判交易的交易商。这种物品的价格将影响您的选择。如果您计划在未来转售这种物品,您将需要以低价获得它。当你达成价格协议时,告诉其他交易商你想以列出的价格购买。当交易完成时,你就拥有了这个产品。
数据收集
网关从您选择的交易场所(交易所、ECN 和暗池)收集价格更新。这个组件(在下图中显示为网关)是交易系统中最重要的组件之一。这个组件的任务是将交易所的账簿信息输入交易系统。这个组件将连接到网络,并能通过交易所接收和发送数据流进行通信。
贸易系统的大门位置如下图所示。它们是贸易系统的输入和输出。
图 2.2 - 负责收集价格更新和发送订单的网关 以下点在前述图中有描绘:
交易商、交易所、电子通讯网络和暗池由场所表示。
不同的协议可用于连接场地(它们用箭头表示)。
有线网络、无线网络、互联网、微波和光纤都是传输数据的选择。在速度、数据损失和带宽方面,这些网络媒体各有自己的特点。
价格更新和订单的箭头是双向的,因为我们可以发送数据到各场所/从各场所接收数据。
要开始接收价格更新,网关将与场所建立网络连接,验证自身,并订阅某个金融工具(我们将在下一节中解释这部分内容)。
订单处理网关也接收和发送通信。当下订单时,它通过网络转发给场地。
如果场所收到此订单,将发送确认。当此订单与匹配订单相符时,将向交易系统发出消息。如果场所未收到订单,则不会发送确认。由交易系统宣布订单超时。在这种情况下,交易员需要介入并检查系统中出现的问题。
制作交易系统与交易所交易
交易系统包含多个功能组件,负责交易和风险管理,以及监控一个或多个交易所的交易过程。一旦编码,交易策略就成为交易系统的一部分。作为输入,您需要价格数据,作为输出,您的订单。这将产生交易信号。我们需要网关来完成这个流程,因为它们是最重要的组件。
交易系统的功能组件、网关接口以及交易系统与外部世界的交互情况如下图所示:
图 2.3 - 交易系统的功能组件 网关根据定价和市场反应收集和发送订单。它们的主要功能是建立链接,并将从外部世界接收的数据转换为交易系统所需的数据结构。
以下点在前述图中有描绘:
这个交易计划将在您应用交易策略时存在于您的机器上。交易将在另一台计算机上进行。
因为这两个设备位于不同的位置,它们必须通过网络连接。
系统使用的通信方式可能会根据其位置而有所不同。
如果交易系统共处于同一设施(机器在同一场所),则将使用单根电线,这将最大限度地减少网络延迟。
采用云解决方案的话,互联网可能成为另一种通信模式。在这种情况下,与直接连接相比,通信将明显更慢。
查看以下图表,它显示了网关之间发生的通信
图 2.4 - 交易所与交易系统之间的通信
我们从前面的图表中理解以下几点:
当我们更仔细地检查由网关处理的通信时,我们可以看到这些场所可能使用各种不同的协议。
将协议转换为交易系统数据结构,网关需要能够处理各种协议。
我们学习了交易系统如何连接到交易所。我们现在将讨论如何进行通信以及我们使用哪种协议来接收市场更新和发送订单。
查看通信 API
通信协议
网络协议
用户数据报协议(UDP)和传输控制协议(TCP)
互联网协议(IP)
软件协议
通信应用程序接口(API)
交易
网络基础
网络负责允许计算机相互连接。要共享数据,网络需要物理层。要实现某一级别的速度、可靠性或甚至安全性,选择恰当的介质(通信层)至关重要。我们在贸易金融中使用以下术语:
电线
光纤:更多带宽。
微波: 它安装简单,带宽很大,但很容易受到暴风雨的影响。
根据您选择的交易技术类型,媒体将会发生变化。在开放系统互联(OSI)模型(在第 5 章"运动中的网络"中开发)中,选择适当的媒体是网络第一层的一部分。物理层就是这一层的名称。在此之上还有六个层,描述了通信的类型。
像大多数的通信一样,金融业也在使用 IP。这是 ISO 模型网络层的一部分。这个 IP 建立了网络分组路由的规则。传输层是我们将要讨论的最后一层。TCP 和 UDP 是银行业中最广为人知的两种协议。这两种过程是相互对立的。TCP 是一种允许两台机器相互通信的协议。最初发送的所有消息都将首先被传送。UDP 缺乏确定网络分组是否被网络接收的方法。所有的交互都将使用 TCP 或 UDP 作为它们的协议。
在第 5 章节,移动网络中,我们将深入研究这些协议。让我们在下一节学习订单簿管理。
订单管理
数据处理的主要目标是将限价单簿从交易所复制到您的交易系统。簿记员将负责收集定价并对其进行分类,以便将您获得的所有簿本整合到您的策略中。
定价变更由网关转换,然后传递给图书生成器,如下图所示。图书生成器将使用网关从场馆收到的图书,以及收集和排序任何定价变更。
图 2.5 - 图书建设者从网关接收价格更新
在以下图示中,我们使用某金融产品订单簿的示例。订单簿包含两部分,一部分为买单,另一部分为卖单。对于每一部分,我们将存储订单的场所、数量和价格。每个场所将发送其自身的订单簿。订单簿构建程序的目标是综合考虑三个场所的订单簿来创建一个订单簿。本图示中所表示的数据为人工生成的。
图 2.6 - 交易系统从三个不同的场所组成账簿 以下在图表中描述:
您可以看到这些书中每一行都有一个订单。
例如,在场地 1 的报价单上的交易员准备购买 1,000 股
$
1.21
$
1.21
$1.21 \$ 1.21 。另一方面,有一份急于出售的人员名单。
报价(或询价)价格几乎总是高于出价价格。确实,如果你可以以低于销售价格的价格购买,那将太简单了。
书籍建设者的工作是从三个地方收集三本书,这些地方是由大门收集的。书籍建设者组织并整理这三本书。
我们已经学习了交易系统如何获取价格更新以及如何建立限价订单簿。我们现在将详细解释订单簿的不同功能。
订单簿考虑
股票限价买卖委托簿收集所有价格更新(订单)并以方便交易策略工作的方式排列它们。交易所利用委托簿跟踪出价和报价。在交易时,我们从交易所接收委托簿以确定资产价格的指示,确定最佳价格,或仅仅了解市场状况。我们必须利用网络传达对交易所委托簿的变更,因为交易所位于另一个平台/服务器。我们有两种方法可以做到这一点:
以首次发送整本书的方式。发送整本书会消耗大量时间。事实上,如果我们有大型交易所(如纽约证券交易所或纳斯达克),一秒钟内会有数百万个订单被发送。如果每次交易所收到新订单时都发送整本书,网络将会饱和,价格更新需要花费太长时间。
本书是交易系统的关键部分,它将为交易策略提供信息以决定何时发送订单。订单簿包含当前在交易所的出价和订单。当价格更新发送到我们的交易系统时,其他市场参与者会同时收到相同的更新。所有其他市场参与者也可以决定在此价格更新后运行。当交易所收到许多订单时,首先收到的订单将首先执行。这就是为什么订单簿在延迟中起重要作用,必须优化订单簿的所有操作。
订单生命周期的以下操作需要处理:
插入:插入是一个将新订单添加到订单簿的簿记操作。这应该是一个快速的操作。由于我们必须对收到的价格更新进行报价和报价排序,因此我们选择用于此操作的方法和数据结构是至关重要的。要插入新的订单,我们需要使用具有
O
(
1
)
O
(
1
)
O(1) O(1) 或
O
(
log
n
)
O
(
log
n
)
O(log n) O(\log n) 复杂度的数据结构。
修改订单
取消:使用订单 ID,取消允许将订单从簿册中撤回。
所选数据结构及与之相关的方法对性能有重大影响。如果您正在创建一个 HFT 系统,您将需要做出适当的决策。我们在 HFT 中实施的订单簿称为基于订单的订单簿。由于这是系统中的关键组件,因此非常重要要考虑此订单簿执行的复杂性。
有效的数据结构来模拟订单簿必须确保以下几点:
不断的查找,快速的数量更新:订单簿为一种特定工具存储了大量的订单。大型交易所每秒可能有数百万个订单。由于订单越来越多,保持订单 ID 的查找时间恒定很重要。我们每秒需要查找数百万个订单 ID 来更新这些订单。此外,我们需要快速检索最佳价格的订单。按价格查找订单的复杂度不能是线性的。因此,我们将使用快速索引(以对数时间查找特定价格的订单)。
以价格顺序迭代:当购买或出售大量时,我们可能需要找到许多订单才能达到给定的交易量。在这种情况下,我们将从最优价格开始,然后转到次优价格,并将继续这样做。在这种情况下,达到下一个最优价格的执行速度也非常重要,需要非常低的复杂性。
以恒定时间检索最佳买入价和卖出价:由于我们主要处理最佳价格,因此我们需要拥有一种数据结构,能够返回买入订单和要约订单的最佳订单。
我们需要考虑以下几点:
组织订单标识符以在巨大的关联数组中订单信息(对于 C++,可以是 std::unordered_map 或 std::vector)。
订单元数据包括对订单簿和它所属的价格水平的引用,因此,在检查订单后,订单簿和价格水平数据结构只需一次解引用即可。在使用订单执行或订单减少操作时,如果有价格的引用,就可以进行
O
(
1
)
O
(
1
)
O(1) O(1) 减少。如果您希望跟踪时间优先级,可以保留指向队列中下一个和前一个订单的指针。
因为大多数变化发生在书籍内部,为每本书的价格水平使用向量将导致最快的平均价格查找。因为期望的价格通常只是从内部几个层次,而线性搜索在分支预测器、优化器和缓存上更简单,从向量末端线性搜索平均比二进制搜索更快。当然,病理性订单可能存在于书籍之外,攻击者可能理论上在书籍末端传输大量更新以降低您的实现速度。但是,在现实中,这通常会产生一种缓存友好的、几乎可以用于插入、查找、更新和删除(在最坏情况下使用内存复制)的实现。
这具有极低的常量插入、查找、删除和更新的最佳情况行为。不幸的是,由于缓存、TLB 和编译器友好性,您可能会以低概率实现最坏情况行为,但仍有非常出色的常量。在最佳出价和要约(BBO)更新方面,它也非常快速,几乎理想。
如何在高频交易中实施图书的实现。我们需要深入了解计算机操作系统和编程。在下一节中,我们将深入探讨使用这些组件来实现最佳性能。
制定交易决策的策略
交易策略就是系统的大脑。这里是我们将我们的交易概念算法付诸实施的地方。让我们来看看这个图表:
图 2.7 - 交易策略从订单簿获取数据,作出交易决策
这种策略的信号组件只专注于产生信号。但是,拥有意图(信号)并不能保证您会获得您感兴趣的流动性。例如,在高频交易中,很可能会由于交易速度而被拒绝您的订单。
执行策略的部分将负责处理来自市场的响应。这部分决定了对于市场的任何响应该采取什么行动。例如,当订单被拒绝时应该发生什么?您应该继续努力获得等同的流动性和另一个价格。
在这一节中,我们学习了交易策略;现在,我们将学习关于订单管理系统(OMS)的所有内容,这是交易系统中最关键的最后一个部分。
世界卫生组织
战略提交的订单由 OMS 收集。订单生命周期由 OMS 跟踪(创建、执行、修改、取消和拒绝)。OMS 收集交易策略订单。如果订单无效或格式错误,OMS 可能会拒绝(数量太大、方向错误、价格错误、未偿还头寸过多或交易所无法处理的订单类型)。当 OMS 中发现错误时,该订单不会离开交易系统。拒绝会更快发生。因此,交易策略可以比订单被交易所拒绝时更快做出反应。让我们看看下图,它描绘了 OMS 的关键特征:
图 2.8 - 订单管理器收集交易系统中的所有订单 让我们现在讨论交易系统的关键组成部分。
关键组件
交易系统的关键组件包括网关、簿记员、策略和 OMS。它们集合了您开始交易所需的所有功能。我们通过聚合所有重要组件的处理时间来计算交易系统的速度性能。当价格更新进入交易系统时,我们启动计时器,当由此价格更新生成的订单离开系统时,我们停止计时器。这个时间段称为 tick-to-trade 或 tick-to-order 期。
综合订单管理系统(OMS)收集策略提交的订单。订单生命周期由 OMS 跟踪(创建、执行、修改、取消和拒绝)。OMS 收集交易策略订单。如果订单无效或格式错误,OMS 可能会拒绝(数量过大、方向错误、价格错误、未结头寸过高,或交易所无法处理的订单类型)。当 OMS 发现错误时,订单不会离开交易系统。拒绝会更快发生。因此,交易策略可以比订单被交易所拒绝时更快地作出反应。
非关键组件
非关键组件是那些与提交订单的选择无直接关系的组件。它们更改设置、收集数据并报告这些数据。在设计策略时,例如,您将需要一组实时更改的参数。您需要一个可以将数据传输到交易策略组件的组件。我们将使用一个名为命令和控制的组件来处理这个。
指挥与控制
交易者与交易系统之间的联系被称为命令和控制。它可能是一个命令行系统或接受交易者订单并将其路由到必要组件的用户界面。请看以下图表:
图 2.9 - 贸易系统用户界面
我们覆盖了负责与所有交易系统组件交互的命令和控制服务。我们现在将看到交易系统的其余功能。
服务
可能会添加额外的组件到交易系统。我们将讨论以下组件(这不是一个详尽的列表):
此服务器保持跟踪所有交易。它更新所有交易金融资产的头寸。例如,如果以
$
1.2
$
1.2
$1.2 \$ 1.2 的价格进行了 100,000 EUR/USD 的交易,名义头寸将为
$
120
,
000
$
120
,
000
$120,000 \$ 120,000 。如果交易系统组件需要 EUR/USD 的头寸数量,它将订阅位置服务器以获取头寸更新。订单管理器或交易策略可能需要在允许订单出去之前了解此信息。如果我们想将某一资产的头寸限制在 200,000 美元以内,另一个获得 100,000 EUR/USD 的订单将被拒绝。
日志系统:这将从组件收集所有日志并编写文件或修改数据库。日志系统有助于调试、查明问题原因,并提供报告。
观众(只读用户界面视图):这些显示交易的视图(如头寸、订单、交易和任务监控)。
控制查看器(交互式用户界面):这些提供了一种修改参数和启动/停止交易系统组件的方法。
新闻服务器:这从许多新闻公司(如彭博社、路透社和 Ravenpack)收集新闻,并实时或按需向交易系统提供这些新闻。
这个部分涵盖了交易系统的关键和非关键组件。我们现在将总结我们所学到的内容来结束这一章。
摘要
我们在本章学习了如何创建交易系统。我们创建的交易系统包括您设计交易系统并开始交易所需的所有必要组件。
学习如何构建交易系统需要多年时间。由于资产类别之间存在差异,您更有可能成为一个资产类别的专家,而不是另一个。我们创建了交易系统应该具备的最基本特征。我们必须学习如何将这个组件链接到交易系统,使其完全发挥作用。
在下面的章节中,我们将详细解释交易系统应如何实施,尤其是与操作系统和硬件相关的内容。在下一章中,我们还有更多知识要学习关于交易交易所。
3
理解交易交换动力
在之前的一章中,我们学习了如何创建高频交易(HFT)系统。我们非常集中地研究了交易系统的关键组成部分。我们还详细回顾了如何创建订单簿,这基本上是对交易参与者提供的所有内容的复制。在这一章中,我们将研究交易所的运作方式。 我们将描述交易所的功能组件,并深入关注匹配引擎。了解交易所的匹配引擎的工作原理是您在创建高频交易策略时必须完成的最重要任务之一。
本章将涵盖以下主题:
在第 2 章《交易系统的关键组件》中,我们对如何设计交易系统有了一个不错的想法。我们详细介绍了如何设计账本、创建交易信号以及接收市场反馈。在本章中,我们将深入解释交易所的运作方式。
构建大规模交易所以处理订单
现有所有者可以在证券交易所与潜在买家进行交易。交易所不是主要市场:它们可以是二级或三级市场。在证券交易所交易的公司并非每天都购买和出售自己的资产。他们可能在必要时回购股票或发行新股。在证券交易所,我们从另一名股东那里购买股票。当我们出售股票时,我们将其卖给另一名投资者。
交易所历史
在 16 世纪和 17 世纪,第一批证券交易所在欧洲出现,主要位于安特卫普、阿姆斯特丹和伦敦等港口城市或商业中心。然而,由于少数公司没有发行股票,这些早期的股票市场更类似于债券交易所。大多数早期公司被视为半公共企业,因为政府必须允许它们进行商业活动。
纽约证券交易所(NYSE),允许进行股票交易,最初出现在 18 世纪末的美国。费城证券交易所(PHLX)被认为是美国第一个证券交易所。1792 年签署《纽约证券交易所法》后,纽约证券交易所诞生。
随着当代股票市场的引入,监管和专业化的新时代开始了,确保股票买家和卖家能够相信他们的交易将以可接受的价格和合理的时间框架内完成。如今,美国和全世界有几个股票市场,其中许多电子互联。因此,市场变得更加高效和流动。当然,股票是最著名的交易资产类别;然而,外汇、固定收益、期货、期权、加密货币以及许多其他类型的资产类别也在交易。
股票交易所的股票价格可以通过各种方法确定。进行拍卖是最常见的做法,买家和卖家出价购买或出售。报价(或问价)是某人希望购买某物的价格,而出价是他们希望出售某物的价格。当出价和报价相等时,就会进行交易。
了解交易的特点
证券交易所(交易所)具有多种不同的特征。交易所是一个市场,将各种市场参与者聚集在一起,以简化交易、降低风险并有助于价格发现。交易所由几个组成部分组成。以下是一般的主要系统:
这些是市场参与者在交易所交易的公司。它们基本上是经过首次公开募股程序成为公众公司的私营企业。估值、流动性和合规费用都是选择上市交易所时需要考虑的重要因素。
撮合引擎:这将类似于旧的矿坑,经纪人站在附近对彼此大声叫喊指令。现在它已完全自动化为一个撮合引擎算法,负责处理交易。在市场上,引擎会发布订单簿(待成交订单)并正确进行撮合。这些交易被撮合和完成的速度有所不同,以纳秒为单位来计量。交易引擎确定价格的方式在不同交易所略有不同。在一般订单簿和撮合引擎部分,我们将更深入地解释它。
交易后:付款和结算,以及交易对账都是这个过程的一部分,以确保所有订单都正确匹配和完成。本质上,这是繁琐(但必要)的后端工作。
市场数据:交易所处理大量数据。它被出售给各种市场参与者。交易价格、交易量、公司公告/文件等都是这种情况的例子。我们在第 1 章"高频交易系统基础"中定义的共同位置也由于高频交易变得很普遍。因此,数据的访问速度也成为了一种营销对象。
市场参与者:结算会员和交易会员是市场参与者。每个参与者都有自己的一套资格要求,结算会员的要求更为严格。
交易所成员还会存入抵押品,以保护自身免受成员破产的影响。交易涉及两方,如果一方违约,交易所会与另一方完成交易。因此,交易所需要抵押品来对违约方提出索赔,同时维护市场稳定。经纪商和自营交易商是最常见的交易成员。清算会员是协助交易清算的重要参与者。
根据资产类别,不同的监管政策也不尽相同。不同的交易所将根据管辖区域制定不同的监管政策。这样做是为了防止洗钱和市场操纵,如内幕交易和操纵市场。 交易所也可以监控公司公告,以确保所有必要的披露都得到满足,从而促进透明的市场。除了管理市场参与者,交易所还必须处理内部合规和政府当局。 我们已经学习了交易所必须具备的特征;现在我们将详细讨论交易所的架构。
交换体系结构
交易平台负责执行从买方投资组合管理人处收到的订单,在执行过程中管理和监控订单,并提供对多个场所的电子访问。在卖方,需要支持处理客户订单和维护交易头寸。
交易架构提供买卖交易功能,并必须满足以下业务需求:
支持前台、中台和后台交易能力,以及基本和复杂的基于规则和算法的交易技术。
支持对前述策略在开发生命周期中进行回测和实时执行。
显示交易和出入账单用户界面(桌面应用程序、基于 Web/移动应用程序)。
支持以服务(TaaS)商业模式提供公用事业,并通过开放 API 进行交付。我们在前一章中谈到了使用 FIX 协议与交易系统和交易所进行 API 集成。
支持与广泛的外部各方进行全球一体化。
支持广泛的金融产品。
应该高度可扩展。
我们在下图中展示了一个交易所的主要功能(我们表示了三家公司的三个队列:特斯拉、微软和苹果)
交易交易架构 在这个图中,我们可以看到一个交易系统 T1 连接到交易所。正如我们在第 2 章解释的那样,交易系统的关键组件包括与交易所的价格更新和订单两个连接。当一个订单被发送到交易所时,它将遵循以下步骤:
根据资产类别和工具,它将被路由到队列。每个队列都是为一个给定的价格和一个给定的符号而创建的。
撮合引擎一次处理一个订单。
如果委托簿发生变化(由撮合引擎处理),该变化将通知交易员,并提供给所有市场参与者(如发生交易,每次更新将发送至清算/交易后处理)。
快速交易中交易员与交易所之间的通信必须高速进行。因此,用于传递消息的协议选择至关重要。基于字符串的协议(如 FIX 协议)是不够的。可以在微秒内进行交易的大多数交易所使用二进制协议。交易系统设计用于快速向交易策略提供数据。对交易所而言,目标是向撮合引擎提供数据。我们将在下一节深入描述撮合引擎算法。
交易明细册和撮合引擎
数百万投资者和交易者构成了整个市场,他们对某只股票的价值有不同的看法,因此出价购买或出售也不尽相同。在交易日中,这些投资者和交易者通过买入和/或卖出股票把意图变成行动,产生了分分钟的波动。
证券交易所为买卖者提供交易平台。普通人需要券商代理才能进入这些市场。券商充当买卖双方的中间人。
最初,在交易所上股票买卖双方的匹配是手动完成的,但现在更频繁地使用计算机化的交易系统。呼喊交易系统是一种手动交易形式,交易商利用语言和手势交流在交易场或交易所地板上购买和销售大量股票。这种交易形式已被电子交易平台所取代。这些技术可以比人类更有效率和更快地匹配买卖双方,从而带来包括交易成本更低和交易执行更快速等主要优势。
买家和卖家之间交易的意图保存在我们所谓的订单簿中。这个订单簿与我们之前描述的交易系统中的订单簿是相同的。它包含所有市场参与者的出价和报价。将匹配买家和卖家的过程由匹配引擎处理。这种算法将买单和卖单配对以执行证券交易。匹配引擎有不同的算法来描述订单如何匹配和填充的顺序,这取决于交易路由的位置。
匹配引擎算法在图 3.2 中描述。输入是来自交易员的订单(1)和订单簿(2)(其中包含已在交易所下单的订单)。该算法将返回交易列表(3)和剩余订单列表(4)。每个进入系统的订单都将逐一处理。
图 3.2 - 具有输入和输出的匹配引擎算法 当使用高频交易策略时,纳秒对于获得利润很重要。在这本书中,我们将详细学习如何优化交易系统以获得最佳性能。与此同时,对交易所的理解是必要的。正如我们之前所描述的,交易所是一台服务器,接受来自交易系统的连接并运行匹配引擎算法,该算法在订单簿上运行,订单簿是收集所有订单的结构。由于所有交易所都有自己的匹配算法,因此了解您在交易时将遇到的基本情况很重要。
在所有以下场景中,我们将解释订单进入交易所与订单簿之间的关系发生的情况。我们首先将学习最基本的情况,即以最佳价格进行匹配。
最优价格方案
默认情况下,匹配引擎会始终尝试为给定订单找到最佳价格。
根据图 3.3 中所示,匹配引擎算法寻找可用的最佳价格。在此图中,我们可以看到订单#1 进入交易所。该订单将与订单#3 匹配,因为订单价格对买家更有利。事实上,买家想以
$
1
0
0
$
1
0
0
$100 \mathbf{\$ 1 0 0} 的价格购买某项资产。交易所有这项资产以
$
9
9
$
9
9
$99 \mathbf{\$ 9 9} 和
$
1
0
0
$
1
0
0
$100 \mathbf{\$ 1 0 0} 的价格供应。匹配引擎将与可用的最佳价格
$
9
9
$
9
9
$99 \mathbf{\$ 9 9} 进行匹配。
图 3.3 - 最佳价格情景 在此上下文中,该算法的结果将是在
$
9
9
$
9
9
$99 \mathbf{\$ 9 9} 之间进行交易,涉及订单
#
1
#
1
#1 \boldsymbol{\# 1} 和#3。订单#2 将保持不变。
在这个示例中,数量为 100。我们需要了解当两个匹配订单的数量不同时会发生什么情况。
部分填充场景
在图 3.4 中的示例中,我们有订单#1 数量为
4
4
4 \mathbf{4} 以及订单#3 数量为
1
1
1 \mathbf{1} 。在这种情况下,为了填补订单#1,我们需要再有三份股票。这个交易所的订单簿中没有足够的数量来满足这笔交易。因此,订单#3 和订单#2 将被填满,剩余数量
1
1
1 \mathbf{1} 来自订单#
1
1
1 \mathbf{1} 将保留在交易所。这就是为什么在这种情况下算法的输出是两个已填满的订单和一个剩余的订单。
图 3.4 - 部分填充情景 在前两个例子中,我们有匹配的流动性。事实上,对于要求的价格,我们有一个与该价格相匹配的流动性。现在我们需要研究当流动性无法与另一个流动性相匹配时会发生什么。
没有匹配的情况
在图 3.5 所示的情况下,我们有订单#1 进入系统,订单簿有两个订单,#2 和#3。由于买入价格为 98 美元,远低于参与者准备出售的价格,订单匹配引擎将不会匹配任何订单。订单#1 将留在交易所:
图 3.5 - 无匹配情况
在前一个场景中,我们在书本中有不同的价格水平。我们需要研究如果流动性的价格相同会发生什么。
多个订单,价格相同
在图 3.6 中描述的场景中,我们有两个价格相同的订单和一个价格相同的新订单。订单的成交方式取决于撮合引擎的配置。
图 3.6-订单簿中具有相同价格的多个订单 匹配引擎的算法在决定我们想要在交易中鼓励什么样的行为方面至关重要。接下来的章节将讨论这些算法最流行的两种实现。
让我们讨论不同类型的算法。
先进先出
时间/价格优先,也称先进先出(FIFO)是最广泛使用的算法。从图 3.1 所示的交易架构可以看出,订单是按价位存储在队列中的。一旦订单进入撮合引擎,它们会被贴上进入系统的时间戳。因此,不会有两个订单拥有相同的时间戳。使用 FIFO 算法时,我们会将新订单与时间戳较早的订单进行撮合。在这种情况下,订单#3 在市场上停留的时间比订单#2 长,因此订单#3 会先与新订单#1 进行撮合。任何对订单的修改都会导致其在执行顺序中丢失优先级。根据不同交易所的规则,改变某个订单的数量可能会使其失去优先级。但对于所有交易所来说,如果价格发生变化,订单都会失去优先级,因为需要重新排入 FIFO 队列。
图 3.7 - FIFO 算法 即使 FIFO 算法是使用最广泛的算法,也有许多其他算法可以使用。我们将讨论的最后一种算法是纯比例算法。
纯比例制
订单使用按比例算法填充,考虑定价、订单批量和时间。市场参与者的订单会按数量平均分配到匹配的对手单。
图 3.8 - 纯比例
图 3.8 显示,以价格
$
1
0
0
$
1
0
0
$100 \mathbf{\$ 1 0 0} 购买的订单将与两个同价订单成交,不论其时间戳如何。交易所使用这种算法来鼓励参与者下单,即使这些参与者没有最快的参与者那么快。
为了鼓励交易,pro-rata 算法经常与其他算法一起使用。它通常用于激励市场参与者采取特定行为。
按比例分配
图 3.9 - 按比例算法变体 图 3.9 解释了比例算法变体。在图 3.8 中,订单簿中的所有订单都以相同的数量填满。在这种变体中,我们在填写订单时更多地考虑了订单簿中更早的订单。通过这种方法,交易所仍将鼓励参与者进行交易,即使他们反应不够快,但反应更快的参与者将获得更多的交易量。
任何异国配置都可以添加到此算法中。例如,如果我们想鼓励更大的订单,可以通过用更大的数量填充订单来引入权重。
让我们结束对匹配引擎可能遇到的不同场景的报道。我们现在将通过总结我们讨论的内容来结束这一部分。
摘要
正如之前所述,一纳秒可以在高频交易中创造一条边缘。通过了解交易所的微观结构(包括优先队列和匹配引擎),您将有助于设计交易策略。您现在知道,修改订单价格会导致订单在队列中丢失优先级。我们还了解到,根据交易所的不同,修改订单数量也可能产生相同的结果。这一章向您展示了如何设计交易所。我们深入了解了匹配引擎的工作原理。在下一章中,我们将解释硬件和操作系统如何在高频交易系统和交易所中运作。
如何设计高频交易系统
这部分旨在为您提供高频交易(HFT)系统的基础知识。本书提供了一步一步的指南,优化代码和操作系统(OS),以创建超低延迟软件。它将描述获得交易系统、操作系统和硬件协同工作的主要优化。
这部分包含以下章节:
第 4 章,高频交易系统基础 - 从硬件到操作系统
第 5 章,移动网络
第 6 章, HFT 优化 - 架构和操作系统
第 7 章,HFT 优化 - 日志记录、性能和网络
4
高频交易系统基础 - 从硬件到操作系统
在上一章中,我们学习了交易所如何运作。我们回顾了交易所的功能组件和匹配引擎。本章将解释高频交易(HFT)系统的基本硬件和操作系统(OS)。
本章将涵盖以下主题:
高频交易计算机
使用操作系统进行高频交易
编译器的作用
我们将在后续章节中了解,高频率是相对于交易策略和交易资产的类型以及您交易所的功能而言的。实现 100 微秒的交易延迟需要仔细的编程和对底层硬件的深入理解。您需要编写最佳代码来充分利用 CPU 和内存架构,并最大限度地减少 I/O 操作的开销。本章重点介绍如何获得基线(我们应该拥有的硬件和操作系统),以建立一个能够实现良好性能的自动交易系统。后续章节将帮助我们完善这一点,应用不同的优化技术来提高交易系统的延迟,甚至达到 10 微秒以下的延迟。
假设你想深入了解现代计算机系统架构的工作原理。在这种情况下,约翰·亨尼西和大卫·帕特森合著的经典著作《计算机架构:定量方法》对此进行了详细解释,并开发了统计模型来帮助理解性能权衡。在本章中,我们将关注高频交易系统需要的组件。以下部分将介绍这种系统所使用的硬件。
高频交易计算机
对于任何低延迟交易策略,很容易想象您可能需要某些专门的计算机硬件。事实并非如此-大多数硬件都是普通的现成硬件。对于大多数情况,如何配置硬件更为重要。图 4.1 显示了一个主要 CPU,代表了 HFT 系统开发人员如何看待 CPU 的架构。
图 4.1 - 主 CPU
正如我们在前几章中讨论的,在高频交易系统中,服务器的目的是处理基线交易功能:接收市场数据、执行算法模型和向交易所发送订单。这个系统有一个网络接口,可以向业务发送和接收数据,或与公司内部其他交易系统进行通信。第 5 章"运动中的网络"聚焦于此。一旦数据包脱离网线,中央处理器(CPU)就要做最繁重的工作。数据包将从网络接口进入主机内存,然后 CPU 会将数据包的内容提取到缓存中,以便执行核心进行解码和处理。
为了实现低延迟,您需要考虑您的软件在 CPU 中的执行方式,以及数据如何从各种硬件组件流向您的交易系统和算法进行处理。在以下几节中,我们将研究 CPU 的工作原理以及影响您软件性能的一些 CPU 微结构细节。
CPU、从多处理器到多核心
中央处理器是一个或多个处理器内核的集合,它们拉取并执行程序指令。这些指令可以作用于存储在内存中的数据,或与连接的设备进行交互。这些设备通常被称为输入/输出 (I/O) 设备,通过某种扩展总线(如 PCI Express (PCIe))连接到 CPU。过去,要实现多处理,需要在单个计算机中使用多个物理芯片。在过去的十年里,为了应对摩尔定律带来的缩放限制,大多数出货的 CPU 都在单个硅芯片上集成了多个内核。随着硅特性(如晶体管)的不断缩小,在制造过程中 fully functional 的芯片产出(收益)也成为一个问题,因此正在向多个芯片(有时称为晶元)集成在单个封装中的方向转变。
中央处理器核心擅长执行许多小型逻辑操作。例如,中央处理器可执行基本算术运算(加、减、乘、除)和逻辑运算(与、或、非、异或、位移)。更专门的操作,如 CRC32、高级加密标准(AES)算法的步骤,以及无进位乘法(如 PCLMUQDQ),也被直接实现在某些中央处理器核心上。中央处理器还可以处理从内存加载或从输入设备读取的信息。中央处理器还可以根据它计算或从其他地方读取的信息改变其执行路径。这些控制流指令是高级语言构造(如条件语句或循环)的基础。
当市场数据到达网络接口时,CPU 会处理它。这意味着解析通过网络发送的数据、管理交易系统不同功能部分的这些市场数据,并可能根据这些市场数据向交易所发送一个已触发的订单。在第 2 章"交易系统的关键组件"中,我们描述了基本元素。我们讨论了网关、订单簿和交易策略都是一起工作的组件,以触发一个订单。使用单个 CPU 执行核心,这些操作每一个都必须按顺序进行,这意味着一次只能处理一个数据包。这意味着数据包可能会排队等待前一个数据包完成处理;这会增加消息等待时间,然后交易系统才能向交易策略提供这些新数据。为了降低延迟,我们希望有许多处理单元并行工作,尽快处理已处理的市场数据,然后转移到刚刚到达的下一条消息。并行计算系统从计算起源就一直存在,尽管在早期主要用于高度专门的科学计算应用。在 1990 年代,多插槽服务器开始普及,在同一主板上有两个或更多 CPU。由于单 CPU 核心无法由于摩尔定律的限制而扩展性能,CPU 供应商开始在单个芯片上添加多个处理核心。现代服务器可以有多个 CPU 插槽,每个插槽都有多个 CPU 核心,在单台机器上实现了相当大的并行性。
图 4.2 描绘了一个现代多插座系统架构。内存或 I/O 设备直接连接到特定的 CPU 插座。它们被称为是与 CPU 本地的。其他 CPU 可以通过互联连接(如英特尔的 Ultra Path Interconnect 或 AMD 的 Infinity Fabric)连接到单一系统。假设一个 CPU 试图访问连接到不同 CPU 的内存或 I/O 设备,这被称为访问远程资源。当我们比较一个 CPU 访问其本地资源与通过互联连接访问远程 CPU 所需的时间时,我们发现互联连接要慢得多,仅访问本地内存。我们称这种访问时间为非均匀的,并将这些架构称为非统一内存访问(NUMA)。cache-coherent NUMA 或 ccNUMA 一词指的是,即使另一个 CPU 核心已修改了数据,CPU 核心也能保证对内存有正确的视图。NUMA 架构可以扩展到大量的核心。可以将每个 CPU 视为一个单独的计算机系统,通过网络互相连接。
图 4.2 - 一个四路 NUMA 体系结构。注意在此配置中 CPU 形成一个完全连通的图。 图 4,2 还代表了一些其他组件。PCIe 是一个连接其他设备的总线,如网络接口卡(NIC)。在 NUMA 体系结构中存在多个 CPU 时,额外的 CPU 可以通过在互连总线上请求数据来共享数据。
超线程和同时多线程
同时多线程,在英特尔 CPU 上称为超线程,是一种技巧,其中 CPU 跟踪多个并行执行状态(在超线程的情况下为两个执行状态)。当一个执行状态需要等待高延迟事件(如从更高级缓存或 RAM 获取数据)时,CPU 会切换到另一个执行状态,以等待获取完成;这是一种由 CPU 自身管理的自动线程,使每个物理核心显示为多个虚拟核心。
使用超线程(Hyper-threading)可以将物理核心数量翻倍,但这会引入难以控制的延迟,表现为上下文切换。
图 4.3 描绘了在一个多线程核心中使用超线程的情况。我们可以观察到,如果一个任务需要等待访问内存段而同时运行另一个任务,硬件可以模拟并发执行。如果一个系统调用(或中断)需要访问内核,所有任务都将暂停。
图 4.3 - 超线程 超线程的主要问题是它消除了任务切换(在软件层面)的控制,这可能导致更高的抖动和更高的延迟。
主随机存取存储器
主内存是一个大型的、非持久性的存储器,用于存储程序指令和数据。主内存是从 I/O 设备(如网卡或存储设备)读取数据的第一个目标。现代主内存可以以高吞吐量返回数据突发,但在请求某地址的数据并使数据可用时需要承受延迟的代价。
在典型的 NUMA 架构中,每个 CPU 都有一些本地 RAM。许多配置将有相同数量的 RAM 连接到每个 NUMA 节点,但这并非硬性要求。对 RAM 的访问延迟,特别是在远程 NUMA 节点上,可能相当高。因此,我们需要其他方法来隐藏这种延迟或缓冲数据,靠近执行某些代码的 CPU。这就是缓存发挥作用的地方。
缓存
现代处理器,具有许多个核心,拥有针对每个核心的本地缓存和针对单个插座上所有核心共享的缓存。这些缓存旨在利用程序通常在特定时间窗口内操作同一内存区域的数据的事实。这种数据访问的空间和时间局部性为 CPU 隐藏访问 RAM 的延迟提供了机会。
图 4.4 显示了一个现代多核 CPU 的典型缓存层次结构。L1 缓存分为两部分:数据缓存和指令缓存。L2 和 L3 缓存将自由混合指令和数据,就像主存储器一样。
图 4.4 - 缓存系统 我们现在将讨论缓存系统的结构。
缓存结构
不是一次从主存储器中读取单个单词或字节,而是每个缓存条目通常存储特定数量的单词,称为缓存行。整个行同时被读取和缓存。当读取一个缓存行时,需要驱逐另一个行以腾出空间。被驱逐的行通常是最近最少使用的,但也存在其他方案。不同级别的缓存有不同的缓存行大小,这是 CPU 设计本身的特性。为了提高缓存命中率,主要是在访问许多相关数据结构时,有许多细节需要考虑如何将数据结构对齐到缓存行大小。
一级缓存
一级缓存(L1 缓存)是计算机系统中最快的可用内存,它置于 CPU 执行单元附近。一级缓存包含 CPU 最近访问和加载到寄存器中的数据。CPU 供应商决定一级缓存的大小。
一级缓存被分为数据缓存和指令缓存。指令缓存存储 CPU 必须完成的操作信息,而数据缓存存储将要执行的过程中的数据。
二级缓存
二级缓存(L2)比一级缓存(L1)慢但更大。现代 L2 内存缓存以兆字节(MB)为单位进行测量,而 L1 缓存则以千字节(KB)为单位进行测量。L2 存储容量的大小取决于 CPU。然而,它通常在 256 KB 和 8 MB 之间。大多数现有 CPU 的 L2 缓存都大于 256 KB,这现已被视为较小的容量。目前最强大的 CPU 拥有超过 8 MB 的 L2 内存缓存。L2 缓存在性能方面落后于 L1 缓存,但仍远快于系统 RAM。L1 缓存通常比 RAM 快 100 倍,而 L2 缓存则比 RAM 快约 25 倍。
三级缓存
第三级缓存是最大的,但也是最慢的。第三级缓存包含在现代 CPU 中。第三级缓存更类似于一个全局内存池,整个芯片可以利用它,而 L1 和 L2 缓存专属于芯片上的每个核心。第三级缓存是我们所谓的受害者缓存:从某个核心的 L1 和 L2 缓存中驱逐的任何缓存行都将传送到第三级缓存,然后再送到主内存。它是一个通常完全关联的缓存,位于 CPU 缓存的填充路径中,存储从该级缓存中驱逐的所有块。所有核心都共享现代 CPU 上的第三级缓存。
共享内存
今天的大多数计算机系统,特别是那些带有多个插座的系统,创造了一个单一设计的幻觉,拥有一个主内存池。我们将它们称为共享内存系统,其中在任何 CPU 上运行的程序都可以访问连接到另一 CPU 的任何内存,就像它是运行代码的 CPU 的本地内存一样。
今天,有两种共享内存模型:统一内存访问(UMA)和非统一内存访问(NUMA)。UMA 使用单一内存控制器,所有 CPU 通过这个单一内存控制器与系统内存通信。在大多数情况下,内存控制器是一个独立芯片,所有 CPU 直接连接到该芯片。在 NUMA 架构中,存在多个内存控制器,内存被物理连接到特定的插槽。NUMA 架构相比 UMA 的主要优势是,NUMA 系统可以更快地扩展到更多 CPU,因为互连 NUMA 节点比连接多个 CPU 到单一系统内存池更简单。
在 UMA 的情况下,随着更多微处理器的加入,共享总线变得拥挤,成为性能瓶颈。这严重限制了 UMA 系统扩展可用 CPU 数量的能力,并增加了每个 CPU 核心等待主内存请求得到服务的时间。
所有现代多插槽服务器都采用 NUMA 架构。每个 CPU 插槽都有一个物理连接的内存池。由于每个 CPU 都有多级缓存,因此 CPU 可能会缓存另一个 CPU 内存中的旧版本数据(甚至另一个远程 CPU 可能已修改了本地 CPU 的内存)。为了解决这些情况,我们需要缓存一致性协议。这些协议使 CPU 能够确定它是唯一拥有者、共享者还是拥有本地修改版本的特定内存区域,并在其他 CPU 试图访问相同的内存位置时与之共享信息。理想情况下,应用程序应编写为很少需要使用这些协议,特别是在延迟和吞吐量很重要的地方,因为同步这种所有权的成本很高。
面向高频交易系统的 SMP 和 NUMA 系统通常被应用,在该系统中,处理可以在单个内存位置中分布在多个处理器上进行。在设计用于在交易系统组件之间传递消息的数据结构和系统时,必须考虑这一点。
输入/输出设备
计算机连接有许多不同类型的输入/输出设备,如硬盘驱动器、打印机、键盘、�老鼠、网卡等。我们在高频交易中应该考虑的主要设备是第 5 章"网络运动"中描述的网卡。大多数输入/输出设备通过外围组件互连快速通信(PCIe)连接到 CPU。PCIe 设备直接与 NUMA 基础设施中的特定 CPU 相关。在构建交易系统时,您需要考虑您的网络代码(如行情数据网关)是否保持在与网络设备连接的 CPU 上,以最小化延迟。
我们总是试图限制使用的设备是硬盘。访问硬盘上的数据成本很高,在高频交易系统中很少使用。但是,在回测交易策略时,信息会存储在磁盘上。我们需要以特定的方式存储数据,以确保快速访问。我们不会在本书中讨论这一部分,因为它不是特定于高频交易系统的内容。
使用操作系统进行高频交易系统
高频交易(HFT)软件运行在操作系统之上。操作系统是对硬件的一种抽象,隐藏了启动可执行文件、管理内存和访问设备的细节。减少延迟的一种技术是在适当的地方打破这种抽象,直接与硬件进行交互。这些应用程序充当用户(程序员)和硬件之间的接口。
操作系统具有若干主要功能,包括以下:
抽象访问硬件资源
进程调度
内存管理
存储和访问数据的方式
与其他计算机通信的方式
干扰管理
对于高频交易系统而言,主要的关键功能是流程安排。我们将在以下章节中详细描述任务安排的过程。
用户空间和内核空间
操作系统的核心是它的内核。内核是一个高度特权的代码块,位于应用程序和硬件之间。内核通常提供许多服务,从管理网络和通信的协议栈到在设备驱动程序的形式上为硬件设备提供抽象。内核高度特权,可以控制系统的工作方式,包括从任意物理内存地址读写,创建和销毁进程,甚至在向系统上运行的应用程序提供数据之前修改数据。必须小心保护内核,只有可信的代码应该在内核上下文中运行,也称为内核空间。
用户空间是应用程序运行的地方。用户空间进程是一个独立的虚拟内存空间,具有多个线程。用户空间进程往往具有较低的权限,需要内核提供特殊支持才能访问设备、分配物理内存或修改机器状态。交易系统运行在用户空间,但构建低延迟交易系统的一个挑战是最小化硬件和交易系统之间的抽象层数。毕竟,执行更多代码来转换数据格式、切换上下文到内核或其他进程来传递消息,或处理硬件状态的不必要变化,都会浪费时间,而不是运行关键的交易系统代码。
地址空间的分离是一个重要的概念。这部分与操作系统如何分配内存以及 CPU 如何理解内存有关,同时也是一个安全和稳定性的特征。未经明确许可,一个进程不应该能够影响内核或其他进程。没有共享内存或类似的通信技术,用户空间中的进程很难直接相互交互。这同样适用于内核,进程很难直接与内核交互。内核被设计用来谨慎地保护其敏感资源和数据结构。
进程调度和 CPU 资源管理
任何软件都是先编译并存储在耐用的长期存储设备(如固态硬盘或机械硬盘)上。当我们想要启动交易系统(或任何软件)时,我们调用存储在磁盘上的一个或多个可执行文件。这会导致操作系统创建一个或多个进程。
操作系统将软件加载到主内存中,创建虚拟内存空间,并启动一个线程来执行刚加载的代码。这个包括软件运行、虚拟内存空间和一个或多个线程的组合称为进程。加载完成后,操作系统最终会调度进程的主线程。调度程序负责确定与进程相关的线程何时何地执行。调度程序可以管理跨多个执行核心的线程执行,这些线程可以在多个物理 CPU 插槽上并行调度。正如前一节中描述的现代计算机系统,调度程序是对这些 CPU 核心的抽象。
当系统中的进程数量超过可用的执行核心数量时,调度器可以限制线程的执行时间,然后切换到另一个等待的线程。这种方式称为多任务处理。从一个进程切换到另一个进程的过程称为上下文切换。
上下文切换是一项昂贵的操作。操作系统保存被切换出的进程的执行环境,并恢复被恢复进程的环境。如前所述,交易系统利用多核来实现实时并行。物理执行核心越多,并行运行的线程越多,通常一个线程对应一个执行核心。
在现代操作系统中,有两种传统的进程调度方法:
抢占式多任务处理:Linux 和大多数操作系统实现了抢占式多任务方法。抢占式多任务处理旨在确保一个进程不能垄断整个系统。每个进程被分配一段特定的运行时间,这段时间称为时间片。一旦时间片用完,调度程序就会停止该进程的运行。除了防止进程占用过多时间外,这种方法还允许调度程序做出全局处理决策。许多抢占式多任务调度程序都试图了解 NUMA 拓扑结构,并尽可能将线程执行在靠近其资源的地方,但实现这一点通常很困难。
协作式多任务处理:这与其他方式有所不同,主要是因为它允许一个进程在自愿停止运行之前一直运行。我们称自愿停止运行的过程为交出控制权。这种方法通常用于实时操作系统,因为工程师不希望对延迟敏感的代码受到调度程序或其他正在运行的任务的干扰。您可以想象,如果某个关键安全过程的实时控制系统出现问题,或者某个订单延迟到达市场而被别人抢先,这将是灾难性的。像 Linux 这样的操作系统提供了一种形式的协作式多任务处理,如果谨慎使用,这对于延迟敏感的代码非常有用。通常,这是为了支持使用 Linux 的实时应用程序。
几乎所有任务调度程序实现都提供了多种机制来调整调度程序的行为。这可以包括针对每个进程的指导,如优先级、NUMA 和执行单元关联性、关于内存使用的提示、I/O 优先级规则等。Linux 允许将多个调度规则应用于正在运行的进程,使某些任务组可以使用实时调度规则。这些设置在设计低延迟系统时很有帮助,但需要谨慎使用;优先级设置错误可能导致优先级翻转或其他死锁情况。
调度程序在其默认配置中始终使用相同的公平性来处理所有资源和进程。这可以保证从一组请求中满足每个请求,并在预定的时间内完成,即使调度请求原语是不公平或随机的。在第 6 章"高频交易优化 - 架构和操作系统"中,我们将解释如何通过限制上下文切换次数来为高频交易系统专门设计进程调度。
内存管理
为了执行,软件需要将其指令和数据可用于内存中。操作系统指示 CPU 哪些内存属于哪些进程。
操作系统必须跟踪已分配的内存区域、将内存映射到每个进程、并指定要为给定进程分配多少内存。
内存管理单元访问的地址空间称为物理地址空间。这是您计算机上可用的物理内存。CPU 将为执行的进程分配此空间的某些部分。这些细分的空间称为虚拟地址空间。内存管理单元的工作是实时将该空间从物理映射到逻辑,以便 CPU 能够快速确定虚拟地址对应的物理地址。
页式内存和页表
现代操作系统不知道进程访问或存储的对象或数据。相反,操作系统关注基本的系统级内存单元。操作系统管理的最基本的内存单元是页。页是物理内存中大小统一且对齐的区域,其大小通常由 CPU 体系结构决定。使用嵌入在 CPU 中的称为内存管理单元(MMU)的硬件,可以将页映射到特定的虚拟地址。通过将分散的物理页重新映射到连续的虚拟地址范围,应用程序开发人员不必考虑硬件如何管理内存或页面在物理内存中的位置。操作系统执行的每个进程都将有其页面映射,称为页表。
图 4.5 表示使用页面的过程。任何物理页面都可以存在于多组页面映射中。这意味着不同的线程,可能在不同的 CPU 核心上运行,可以在其地址空间内访问同一个内存页面。
图 4.5 - 页面和进程
将物理和虚拟地址相互转换是由 CPU 内部的硬件自动完成的。因此,如果被转换的信息位于离 CPU 很近的快速位置,转换性能就会得到改善。事实上,用于存储这些信息的页表通常位于 CPU 内部的专用寄存器中,但这只有在页表很小的情况下才可能。CPU 内部还有一个专门用于页表的高速缓存,称为转译旁路缓冲区(TLB)。由于页表是一种存储在内存中的数据结构,如果某个进程的地址空间太大而无法完全容纳在 TLB 中,大多数 CPU 都会从其他 CPU 的缓存甚至主存中自动拉取相关的页表数据到 TLB 中。
页码调度可能会降低进程的性能。当在 TLB 中发生缓存失误时,操作系统必须从内存的其他地方加载数据。在高频交易(HFT)系统中,我们有时通过增加页面大小来最小化 TLB 缓存失误的影响。大于标准基页大小 4KB 的虚拟内存页称为巨页。对于大型数据集上的频繁访问模式,巨页可以提高内存速度。巨页也有成本 - 跟踪巨页的 TLB 有时可能比管理标准页的 TLB 小数个数量级,这意味着如果你有很多巨页映射,可能需要更频繁地访问内存。因此,必须谨慎使用巨页。
系统调用
系统调用意味着用户空间应用程序请求操作系统内核提供服务。系统调用是应用程序与操作系统沟通的方式。系统调用是软件向操作系统内核发出请求,以执行一些敏感的操作,如操纵硬件状态。现代操作系统上的一些关键系统调用,用于处理进程的创建和终止、管理磁盘上的文件、管理输入输出设备,以及与外部世界进行通信。
当系统调用被请求时,如果请求被允许,内核将执行该操作。对于许多系统调用,如果调用成功完成,应用程序将收到来自内核的某些响应。一旦系统调用完成,如果请求任务在其时间片内还有剩余时间,或者没有更高优先级的任务等待 CPU 时间,调度器就可以安排该任务恢复执行。内核在过程完成后,将结果提供给应用程序,并从内核空间向用户空间传输数据。
某些特定的系统调用,如获取系统日期和时间,可能需要几纳秒才能完成。一个更长的系统调用,如连接到网络设备或与磁盘上的文件交互,可能需要几秒钟。大多数操作系统为每个系统调用启动一个单独的内核线程,以最小化瓶颈。多线程操作系统可以同时处理多个系统调用。高频交易系统将大量使用并发执行的概念,例如使用线程。
现代 Linux 版本提供了虚拟动态共享对象 (vDSO),将一些特殊的内核空间函数导出到用户空间,特别是那些与检索当前系统时间有关的函数。vDSO 的威力在于这些函数在内核的控制下执行,因此了解硬件的具体情况,直接在用户空间进程中运行。与需要整个内核调用 (因此需要完全的上下文切换) 的 open 和 read 系统调用不同,像 clock_gettime (至少在 CLOCK_MONOTONIC 的情况下) 这样的函数调用开销很低,因为调用是在 vDSO 中进行的。
线程
任何流程中工作的最基本分工是一个线程。通过在许多线程上完成工作,可以实现并行。单个进程内的所有线程共享一个公共的虚拟内存空间。每个进程,由一个或多个线程组成,都有自己独特的内存空间。交易系统的主要活动是在不同的功能(潜在的进程)之间共享数据,以决定发送订单。在优化并发功能之间的通信时,将考虑使用线程或进程。这也会影响您在线程和进程之间传递数据的方式。在线程之间传递的数据可以利用内存分配的并发性,允许您只需给另一个线程一个指向数据结构中消息的指针即可传递数据。在进程之间传递数据,要么需要两个进程映射的共享内存池,要么需要将消息序列化到某个队列中,如第六章"高频交易优化 - 架构和操作系统"中所讨论的那样。
除了能共享内存外,线程的响应时间比进程更快。如果一个进程被分成多个线程,其中一个线程的输出可能会在完成执行时立即返回。它们的上下文切换时间也比进程更短。
因为系统调用是任何高频交易系统所需要的,所以抵消这一成本是至关重要的。我们将在第 6 章"高频交易优化架构和操作系统"中看到如何从线程和进程中获益。
干扰管理
外围设备如何提醒 CPU 发生了什么事情。CPU 会暂停一个处理核心,并切换为分配给该设备的中断处理程序的上下文。限制能够创建上下文切换的中断的数量;我们也将在第 6 章中回到这个问题,HFT 优化 - 架构和操作系统。
图 4.6 展示了在单核模型中使用中断(或系统调用)对 CPU 执行任务的影响。我们可以观察到调度器会在当前运行的任务和内核中的中断上下文之间切换。内核花费更多时间处理中断请求,这就减少了可用于运行用户任务的 CPU 时间。
图 4.6 - 任务调度中的中断或系统调用的影响 图 4.7 显示了两个 CPU 核心的好处,这两个任务不需要共享时间。这个例子表明,如果我们将任务固定在给定的内核上,我们将减少上下文切换的次数,并减轻内核中断的影响。这个例子还假设中断请求只由一个核心提供服务。因此,只有任务 1 会被打断来为硬件服务。
双 CPU 核心的优势
第 6 章,HFT 优化 - 架构和操作系统,详细介绍了将任务固定到指定核心的任务调度。
在第一和第二部分中,我们了解了硬件、操作系统以及它们在高频交易系统中的作用。现在我们将解决最后一个部分:编译和库,它们也是高频交易系统不可或缺的一部分。
编译器的作用
编译器将人类可读的源代码翻译成另一种语言,通常是机器专用语言,但也可以是虚拟机语言。它们将高级语言翻译成较低级别的语言。它们可以生成中间代码、汇编语言、目标代码或机器代码。它们还在加快软件运行时方面发挥了重要作用。编译器通过改进抽象和硬件高效执行开发人员所需表达的内容而变得越来越智能。新的编程范例被添加以改善软件工程。在 20 世纪 90 年代,Python 和 Java 使面向对象编程为每个人所用。我们建议读者查看由 Aho、Lam、Sethi 和 Ullman 撰写的编译器:原理、技术和工具(也称为龙书)一书,这本书将深入解释编译器的设计方式。
在高频交易系统中,编译器可以帮助优化我们花费大部分时间的代码部分:循环。Steven Muchnick 编写的《高级编译器设计与实现》描述了编译器可以执行的循环优化。我们必须谨记,高频交易系统的关键部分是时空权衡(增加内存使用和缓存利用,同时减少执行时间)。我们可以谈谈使用这种范式进行优化的一些示例:
循环展开是这种权衡的一个例子。因为循环展开时迭代次数减少,退出检查的开销也减少了。此外,分支指令更少,这可能会根据架构的不同而产生开销。对于完全展开的循环,没有退出测试。循环展开可能会导致编译器进行进一步的优化(例如,在上述完全展开的版本中,所有数组偏移量都是常量,这是编译器可能能够利用的)。
函数内联可以将函数调用替换为该函数本身的汇编代码,从而为更多的汇编代码优化提供机会。
表和计算。编译器可以帮助创建数据结构以避免重新计算。他们将保持已计算值的值。
编译器的主要功能是生成可由操作系统运行的可执行文件。我们现在将讨论可执行文件格式。
编译器和链接器将高级程序转换为适合目标操作系统的可执行文件格式。操作系统解析可执行文件以确定如何加载和运行程序。在 Windows 上,这是一个可移植的可执行 (PE) 文件,而在 Linux 上,这是一个可执行和可链接的格式 (ELF) 文件。每个操作系统都有一个加载器。加载器确定将程序的哪些块从磁盘加载到内存。加载器分配可执行文件将使用的虚拟地址范围。然后,它将从入口点开始执行(以 C 语言为例,即 _start 函数),该函数然后调用程序员定义的主函数。就内存而言,重要的是要记住,操作系统通过使用虚拟内存来保护进程之间的隔离,正如我们在讨论虚拟内存和分页时所描述的。每个可执行文件都在自己的虚拟地址空间内运行。
静态链接与动态链接
现代系统上许多可执行程序依赖于外部代码库,通常由第三方提供,例如操作系统供应商。为了处理这些外部依赖关系,程序可以通过两种方式集成该代码。第一种是静态链接所有代码,构建一个独立的二进制文件。第二种是动态链接外部代码,要求操作系统检查可执行文件,确定运行程序所需的库,并将其单独加载。
链接器将应用程序代码和依赖项编排为一个单一的二进制对象,并采用静态链接。由于此二进制对象包含所有依赖项,因此没有机会让程序利用同一库被多个程序重复使用,从而需要在运行时单独加载所有代码。例如,在 Linux 上,许多程序都使用 glibc 库。如果这些程序进行了静态链接,它们将浪费大量内存来重复存储相同的依赖库。静态链接有一个重要优势:编译器和链接器可以协作来优化所有函数调用,即使是从外部库中获取的对象也是如此。
动态链接允许链接器创建一个更小的二进制文件,其中依赖库的位置已被存根替换。动态链接器将在应用程序启动时从磁盘加载适当的共享对象来加载该库。只有当需要该依赖项时,它才会被加载到内存中。如果多个正在运行的进程使用同一个库,该库的代码内存可以在多个进程中共享。然而,这种效率是以成本为代价的:通过过程链接表(PLT)调用库函数时会间接引用。这种间接方式可能会带来额外开销,特别是当频繁调用库中的短函数时。典型的 HFT 系统将尽可能使用静态链接来避免这种开销。
摘要
在这一章中,我们开发了一个概念性模型,解释电脑如何工作以及如何考虑各组件对整体性能的影响。硬件和软件的各个部分必须以理想的方式协作,为交易系统服务。这通常需要了解硬件和软件之间的交互方式,从而避免低效算法或交互带来的负面影响,并进行优化。
我们正在使用本章中开发的基本原理作为基础。接下来的章节将致力于优化操作系统、内核和面向高频交易系统的应用程序。这将包括减少上下文切换的影响、安全访问共享数据结构的技术,以及其他降低底层硬件和软件中低效组件的方法。
下一章将重点讨论网络。我们将解释网络卡在高频交易系统中的作用,以及如何优化与交易所的通信以降低延迟。
动态网络
在前一章中,我们深入讨论了硬件和操作系统。任何交易系统都必须从交易所收集数据,并根据这些数据做出决策。为此,通信在高频交易(HFT)系统的性能中至关重要。在本章中,我们将深入探讨交易系统如何进行通信,如何在 HFT 系统中使用网络,以及如何监控网络延迟。
在本章中,我们将涵盖以下主题:
高频交易系统中的网络理解
高频交易系统之间的网络通信
重要的协议概念
为高频交易交易所设计金融协议
内部网络与外部网络
理解数据包的生命周期
监控网络
时间分配的价值
以下部分将介绍网络基础知识;我们将学习我们稍后要优化的基本知识。
高频交易系统中的网络理解
交易系统接收市场数据并向交易所发送订单。交易系统内的众多流程分布在不同的机器上,需要彼此进行通信-例如,负责跟踪一种工具头寸的流程需要向所有组件发送有关给定资产头寸的信息。网络定义了设备如何互相连接。网络是为了将数据从一台机器传输到另一台机器(延伸到交易所)所需的。 所有高频交易系统的基础都是网络,必须像仔细考虑软件系统的设计决策一样仔细考虑网络。
网络接口卡(NIC)
学习关于网络概念模型
开放系统互联(OSI)模型无疑是现代网络环境中计算机相互通信方式的最常见描述。OSI 模型是一个概念性框架,用于描述网络系统的功能。以下屏幕截图描绘了 OSI 模型的完整七层以及每层通常涉及的相关操作:
7
应用程序
高级 API
6
演示
应用程序和会话层之间的翻译;编码、压缩、加密。
5
会话
管理与其他端点的通信,例如重传。
4
运输
确保数据段在端点之间的可靠传输。
1
π
∑
0
2
0
π
1
1
1
π
∑
0
2
0
π
1
1
{:[(1)/(pi)],[sum_(0)^(2)],[(0)/(pi)],[(1)/(1)]:} \begin{aligned}
& \frac{1}{\pi} \\
& \sum_{0}^{2} \\
& \frac{0}{\pi} \\
& \frac{1}{1}
\end{aligned}
3
网络
地址管理、交通控制、路由。
2
数据链路
可靠的数据帧传输在物理角度来看
1
物理的
从原始位到适合于介质的任何形式的转换。
https://cdn.mathpix.com/cropped/2024_11_08_c298ff4244b140b858afg-095.jpg?height=140&width=44&top_left_y=1569&top_left_x=404 7 Application High-level APIs
6 Presentation Translates between Application and Session layers; encoding, compression, cryptography.
5 Session Manages the communication with the other endpoint such as retransmission.
4 Transport Ensures reliable transmission of data segments between endpoints.
"(1)/(pi)
sum_(0)^(2)
(0)/(pi)
(1)/(1)" 3 Network Addressing, traffic control, routing.
2 Data Link Reliable data frame transmission between endpoints from a physical point of view.
1 Physical Conversion from raw bits to whatever is appropriate for the medium. | ![](https://cdn.mathpix.com/cropped/2024_11_08_c298ff4244b140b858afg-095.jpg?height=140&width=44&top_left_y=1569&top_left_x=404) | 7 | Application | High-level APIs |
| :---: | :---: | :---: | :---: |
| | 6 | Presentation | Translates between Application and Session layers; encoding, compression, cryptography. |
| | 5 | Session | Manages the communication with the other endpoint such as retransmission. |
| | 4 | Transport | Ensures reliable transmission of data segments between endpoints. |
| $\begin{aligned} & \frac{1}{\pi} \\ & \sum_{0}^{2} \\ & \frac{0}{\pi} \\ & \frac{1}{1} \end{aligned}$ | 3 | Network | Addressing, traffic control, routing. |
| | 2 | Data Link | Reliable data frame transmission between endpoints from a physical point of view. |
| | 1 | Physical | Conversion from raw bits to whatever is appropriate for the medium. |
图 5.1 - 七层 OSI 模型
这个模型分为七个独立的层,每个层都有特定的功能,只与相邻的层进行通信,不与所有其他层进行通信。这些层的详细说明在此处描述。
会话层、演示层和应用层:这三层可以重新组合(简化)因为这些是我们将在软件级别使用的层。在高频交易系统中,我们关注以下四层,因为它们提供了高级优化机会。
传输层:它管理交付并包含数据包中的错误。网络层负责序列化、数据包大小和系统之间的数据传输。在金融领域,我们主要使用两种协议:传输控制协议(TCP)和用户数据报协议(UDP)。
网络层:这一层从数据链路层接收帧,并使用逻辑地址将它们传递到预定的目的地。我们将使用称为互联网协议(IP)的寻址协议。与 IP 版本 6(IPv6)不同,IP 版本 4(IPv4)是一种轻量级且用户友好的协议,是金融行业中最广泛使用的协议。
数据链路层:该层通过使用奇偶校验或循环冗余校验(CRC)等技术检测错误,从而纠正可能在物理层发生的错误。
物理层:这是两台机器用来通信的媒体,包括光纤、铜缆和卫星。所有这些媒体都有不同的特性(延迟、带宽和衰减)。根据我们想要构建的应用程序类型,我们将使用其中一个。这个物理层被认为是 OSI 模型中最低层。它负责在发送方的物理层和接收方的物理层之间传递原始的非结构化数据位,无论是电子还是光学方式。网络集线器、布线、中继器、网络适配器和调制解调器都是物理层中找到的物理资源。
正如您在图 5.2 中看到的那样,我们经常同时提到几个图层。例如,我们可以将第 5-7 层称为软件层,将第 1-3 层称为硬件层,第 4 层则在两者之间。这种情况非常普遍,因此使用一个简化的模型来整合软件、传输和硬件层是很常见的,它看起来像这样:
应用程序
应用层、表示层、会话层
TCP/IP
网络层-传输层
物理的
开放系统互联模型第一层至第二层
Application OSI Layers 5-7
TCP/IP OSI Layers 3-4
Physical OSI Layers 1-2 | Application | OSI Layers 5-7 |
| :--- | :--- |
| TCP/IP | OSI Layers 3-4 |
| Physical | OSI Layers 1-2 |
图 5.2 - 简化 OSI 模型
现在,我们已经讨论了数据包在两台通信计算机上的网络堆栈中必须经历的高级分层路径,我们将讨论如何为 HFT 设计网络。
高频交易系统之间的网络通信
当设计师为高频交易系统构建网络时,他们关注不同的通信模式。因为微秒很重要,他们必须考虑使用微波网络或思科交换机而不是其他交换机的优势。
网络创新有潜力带来巨大差异,影响贸易。 以下图表描述了网络的抽象模型。当两个设备通信时,需要一个介质来传输数据。它们通过连接到网络设备(如交换机)的物理连接进行通信。交换机负责将数据包从网络的一个部分移动到另一个部分,在那里我们可以找到数据发送方发送的收件人。
图 5.3 - 网络的抽象模型 每个网络组件对网络延迟至关重要。这些都是造成延迟的原因:
网卡将计算机的信号转换为网络信号,反之亦然。网卡处理数据的时间可忽略不计,但并非零。网卡被选中用于低延迟数据路径以及其他功能,例如以下功能:
总线:总线将数据从计算机的一个组件传输到另一个组件。我们可以找到三种主要类型的总线:外围组件互连(PCI)、PCI 扩展(PCI-X)和 PCI Express (PCIe)。它们都有不同的速度。在 2017-2018 年,行业开始使用 PCIe 5.0,工作速率为 63 吉字节每秒(GB/s)。虽然 PCIe 6 已于 2019 年发布,但 PCIe 5.0 仍是 NIC 最快的总线。
网口数量:网卡可以有不同数量的网口:一个、两个、四个或六个。它可以让机器同时访问多个网络。但是,同一台机器上也可以有多个网卡。
网口类型:网卡可以有不同类型的连接。 Registered Jack-45 (RJ-45)端口是一种端口类型。 它使用名为 Category 5 (Cat5)或 Category 6 (Cat6)的双绞线电缆。 我们还可以使用同轴电缆,它连接到 Bayonet Neill-Concelman (BNC)端口。 最后一个是光纤端口,使用光纤电缆。
网络速度:标准支持的网络速度为单车道 100 千兆位每秒(Gbps),25 Gbps 和 10 Gbps 信号。其他任何东西(即 400 Gbps,
50
Gbps
,
40
Gbps
50
Gbps
,
40
Gbps
50Gbps,40Gbps 50 \mathrm{Gbps}, 40 \mathrm{Gbps} 等)都包括多个并行车道。10 Gbps 和 25 Gbps 以太网每天都在数据中心和金融应用程序中使用。
应用特定集成电路(ASIC):集成了与主机 PC 通过 PCIe 和网络本身进行接口的功能。
集线器
交换机:交换机在数据链路层(第 2 层)和有时网络层(第 3 层)工作;因此,它可以支持任何数据包协议。它的主要作用是过滤和转发数据包跨越局域网。
路由器:路由器至少连接两个网络,并促进一个网络上的主机向另一个网络上的主机传送数据包。在高频交易系统中,路由器位于交易系统的网关(第 2 章《交易系统的关键组件》中提到)。路由器能找到最佳方式将数据包从一个主机转发到另一个主机。
主要组件(如路由器、网卡和交换机)将在高频交易系统中引入延迟。
理解开关的工作原理
主交换机是高频交易网络中通信的主要支持。
图 5.4 - 开关的抽象模型 图 5.4 表示交换机的抽象模型。"入口"和"出口"是业界用来表示任何网络设备的输入和输出(I/O)的词语。交换机工作于网络第 2 层。它的主要功能是根据转发规则,将数据包从输入转发到输出,这可以在前面的图中看到,即入口接口和出口接口。交换机处理两种类型的操作,如下所述:
配置数据包转发:对于简单的交换机,模型仅仅是观察端口上的媒体访问控制(MAC)地址,并将流量切换到该端口。更复杂的交换机(即支持第 3/4 层)将允许对其他匹配模式(即 IP 地址或端口)执行操作。
正转/过滤决策:根据读取的配置表转发数据包,必要时删除数据包。
启动后一次性设置好开关,根据需要动态生成转发表,例如路由表更新时。
解析器是第一个处理新数据包的(数据包主体独立缓冲,无法匹配)。解析器定义交换机的协议,识别并提取头部信息。
以下提取的标头字段发送到匹配-动作表(将标头字段与要执行的动作相匹配的组件)。入口和出口在匹配-动作表中分开。入口匹配-动作决定出口端口(s)和数据包被路由到的队列,同时这两者都可能修改数据包标头。数据包可能会被转发、复制、丢弃或由入口处理触发控制流。出口匹配-动作根据实例修改数据包标头(例如,对于多播副本)。为了跟踪帧到帧的状态,可以将动作表(计数器、速率限制器等)链接到流。
元数据,被视为类似于数据包头字段,可以被数据包在各个阶段携带。所有元数据实例都是入口端口、传输目的地和队列,以及从表到表移动的数据,而无需修改数据包的已解析表示。
网络结构之外,网络指标的关键部分是速度。我们将在以下部分定义一些速度指标。
带宽、吞吐量和数据包速率/大小
带宽是两个主机之间交换的理论数据包数量。通信达到预定目的地的速度称为吞吐量。两者的主要区别在于吞吐量衡量实际数据包传输,而不是理论传输。通过查看平均数据吞吐量,您可以看到有多少数据包到达目的地。数据包必须有效地到达目的地才能提供高性能服务。不丢失任何数据包非常重要。例如,如果我们想通过增量更新建立订单簿,丢失一个数据包意味着订单簿失去一致性。
在评估和测量网络性能时,数据包大小和数据包速率是两个关键标准。网络性能取决于这些参数的设置。吞吐量随数据包大小的增加而增加,然后降至饱和值。增加数据包大小会增加发送的数据量,从而提高吞吐量。
下面的截图说明了网络吞吐量(以千位每秒(Kbps)为单位)与数据包速率(以字节为单位的每秒数据包数)的关系:
图 5.5-不同数据包大小的吞吐量和数据包速率 图 5.5 中所示的两行分别对应不同的数据包大小(512 字节和 1,024 字节)。数据包速率增加时,网络吞吐量会提高,因为提高数据包速率意味着增加数据量,从而提高吞吐量。此外,图表显示,随着数据包数量的增加,吞吐量会下降,直到接近饱和点。较大数据包的吞吐量增加速度快于较小的数据包,1,024 字节数据包的吞吐量峰值在 50 个数据包时达到。 当一个接口达到最大吞吐量时,多个入口接口试图向同一出口接口提交出站数据包可能会导致缓冲。
切换队列
交换机的主要功能是将数据包路由至正确的接收者。当有大量数据流入时,处理数据的时间可能超过数据进入交换机的时间。为避免数据丢失,关键是要有缓冲区。这个缓冲区将存储等待处理的数据。交换机的主要作用是在输入端口接收数据包,查找目的地以获取输出端口,然后将数据包置于输出端口队列中。大量数据流向特定输出端口可能会使输出端口队列饱和。如果队列中积累太多数据,将导致严重的延迟。如果缓冲区已满,数据也可能丢失(数据包丢弃)。如果市场数据包丢失,将无法建立订单簿,从而中断交易。下图描述了交换机的队列情况:
开关队列 队首阻塞(Head-of-line, HOL)是一个重要问题。这个问题发生在当许多数据包被一个即将离开队列的数据包阻挡在队列中时,可能会增加时延或数据包乱序。如果许多数据包被阻塞在一个队列中,交换机会继续处理其他流向另一个输出的数据包,这会导致数据包不按顺序被接收。
我们了解到排队会影响数据包交付;现在,我们将讨论两种主要的交换模式。
切换模式 - 存储转发与直通
开关必须根据交换机机制接收和审查各种字节,然后处理数据包并转发到正确的出口端口。这里详细说明了两种交换模式:
贯通式切换模式有两种形式,如下所示:
无缝切换
快速转换
商店和转发切换模式
根据以太网帧目的地 MAC 地址作出转发决策。它们解析以太网报头中的源 MAC 地址位;它们记录 MAC 地址并创建 MAC 表。交换机在帧可通过出口端口传输之前必须接收和审查的帧数据量因交换类型而有所不同,如以下屏幕截图所示:
7 字节
1 字节
6 字节
2 字节
46
−
1500
46
−
1500
46-1500 46-1500 字节
前言
SFD
目标 MAC 地址
源 MAC 地址
类型
帧负载
FCS
7 bytes 1 byte 6 bytes 2 bytes 46-1500 bytes
Preamble SFD Destination MAC Source MAC Type Frame Load FCS | 7 bytes | 1 byte | 6 bytes | | 2 bytes | | $46-1500$ bytes |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Preamble | SFD | Destination MAC | Source MAC | Type | Frame Load | FCS |
穿透
收到的字节
…
…
dots \ldots
接收的字节
穿透
收到的字节
Cut-Through
Bytes Received dots
Bytes Received
Cut-Through
Bytes Received | Cut-Through | |
| :---: | :---: |
| Bytes Received | $\ldots$ |
| Bytes Received | |
| Cut-Through | |
| Bytes Received | |
整个框架(最多 9200 字节) 图 5.7 - 根据收到的帧字节切换模式
图 5.7 展示了三种模式,并代表了应该接收多少信息。我们将在下面详细学习这些。
商店和转发模式:在转发帧之前,交换机必须完全接收该帧。基于目的地 MAC 地址查找来决定是否转发该帧。为了确认数据的完整性和准确性,交换机使用帧校验序列(FCS)字段。如果 CRC 值不匹配,该帧将被视为无效并丢弃。在传输帧之前,检查目的地和源 MAC 地址是否匹配。
默认情况下,接受大小在 64 字节到 1,518 字节之间的任何帧,其他大小的帧将被丢弃,导致比其他三种方法更高的延迟。
切通交换模式:此模式允许以太网交换机在接收到帧的前几个字节时做出转发选择。该模式有两种类型,如下所述:
无碎片切换:此模式要求在转发帧之前解析前 64 个字节。
快速转发开关:当交换机接收到帧的目的 MAC 地址时,仅需要前 6 个字节即可转发该帧。
我们看到了两种主要的交换模式;我们现在将描述交换机可以执行数据包转发的不同层。
第 1 层交换
物理层开关,也称为第 1 层开关,是 OSI 模型物理层的一部分。第 1 层开关可能是电子和可编程补丁面板。它所做的不过是在端口之间建立物理连接。链接是通过软件指令建立的,允许自动或远程配置测试拓扑。第 1 层开关不读取、修改或使用数据包/帧头来路由数据。这些开关对数据完全透明,延迟非常低。在测试环境中,端口之间的透明连接很关键,因为它们可以确保测试结果与直接使用连接线的一样准确。Arista/Cisco 是第 1 层开关的一个例子。
二层交换(或多端口桥接)
分层 2 交换机有两种功能,如下所述:
在第 1 层(物理层)传输数据
检查任何接收和发送的帧的错误
这种类型的交换机需要 MAC 地址来转发帧到正确的收件人。所有收到的 MAC 地址都会保存在转发表中。这个表允许交换机以非常高效的方式转发数据。与更高层级的交换机(高于 3 层)可以根据 IP 地址传输数据包不同,2 层交换机不能使用 IP 地址且没有优先级机制。
三层交换
第 3 层交换机是以下设备:
根据源地址和目标地址分析和路由数据包的智能 IP 路由的路由器
交换机
我们现在将讨论一个能够使用公共地址从许多私有 IP 地址路由数据的系统。
网络地址转换
将私有 IP 地址转换为公共地址的过程称为网络地址转换(NAT)。大多数路由器使用 NAT 允许多个设备共享单个 IP 地址。当机器与交换机通信时,它会寻找交换机的方向。这个请求被发送为一个包从机器到路由器,转发给企业。路由器必须首先将源 IP 地址从私有本地地址转换为公共地址。如果数据包包含私有地址,接收服务器将无法知道将信息返回到何处。由于 NAT,信息将使用路由器的公共地址而不是笔记本电脑的私有地址返回到笔记本电脑。
网络地址转换(NAT)是一种耗资源密集的操作,对于任何使用它的设备来说都是如此。这是因为 NAT 需要读取和写入每个 IP 数据包的报头和负载信息来完成地址转换,这是一个耗时的过程。它会增加中央处理器(CPU)和内存的消耗,可能会降低吞吐量并增加数据包延迟。因此,在实时网络中安装 NAT 时,了解 NAT 对网络设备(特别是路由器)的性能影响变得至关重要,尤其对于高频交易(HFT)来说。大多数现代交换机可以在 ASIC 中至少执行静态 NAT,但越来越多的交换机也可以执行动态 NAT,只有轻微的性能损失。
我们详细研究了如何传输数据包。我们现在将描述设定数据包转发规则的协议。
重要的协议概念
当两个设备需要通信时,一旦它们有了从发送方到接收方传输信号的方法,我们就需要有制定通信规则的协议。协议就像两个组件在系统中同意使用的语言;它设定了通信的规则。下图代表了交换和交易系统的网络基础设施:
图 5.8 - 交易交易所和交易服务器的交易网络基础设施
在第 2 章,交易系统的关键组件,以及第 3 章,了解交易交易动态,我们看到交易交易所、市场数据馈送处理程序和市场参与者是传统交易系统的三个主要组件。
通过网关服务器,报价撮合系统从市场参与者那里收到订单。数据处理程序从交易所获取数据,并以最短的延迟传送给关注的市场参与者。我们使用 FIX Adapted for STreaming (FAST)协议(在 FAST 协议一节中有详细介绍)来传输市场数据。
我们现在将讨论高频交易中的以太网协议。
以太网用于高频交易通信
以太网是用于连接有线局域网(LAN)或广域网(WAN)设备的最常用协议。该协议规定了设备之间的通信规则。
以太网规定网络设备如何构建和发送数据,以便其他设备在同一 LAN 或公司网络上识别、接收和处理该数据。这种协议高度可靠(抗噪音)、快速和安全,由 1983 年 IEEE 802.3 工作组设计。该技术一直在不断改进,以获得更好的速度。
100BASE-T,即快速以太网,至今仍在使用。
使用 IPv4 作为网络层
互联网协议在 OSI 模型的网络层运行,而 TCP 和 UDP 模型在互联网层运行。因此,此协议负责根据主机的逻辑地址识别主机,并通过底层网络在它们之间路由数据。
互联网协议寻址系统提供了一种唯一识别主机的技术。IP 采用尽力而为的交付方式,这意味着它无法保证数据包能发送到预期的主机,但它会尽最大努力去实现。IPv4 的逻辑地址是 32 位。
当使用 IPv4 协议时,我们可以使用三种不同的寻址模式,如下所述。
单播模式
只有一个指定的主机以这种方式接收数据。目标主机的 32 位 IP 地址存储在目标地址字段中。在这种情况下,客户端将数据传输到所需的服务器,如下图所示:
图 5.9 - 单播模式 如图所示,机器 A 向机器 C 发送信息。
广播模式
在此模式下,该数据包是发送到网段内的所有主机。目的地址字段包含特定的广播地址 255.255.255.255。当网络上出现此数据包时,主机有义务对其进行处理。在这种情况下,客户端发送了一个被所有服务器收到的数据包。由于几乎无法控制哪些机器会收到流量,并可能产生不必要的开销,因此广播很少用于高频交易(HFT)系统。您可以在以下示意图中看到广播模式的说明:
图 5.10 - 广播模式 正如先前图表所示,机器 A 将信息发送给所有机器。
多播模式
这种模式是前两种模式的混合,数据包并非发送给单个主机,也不是发送给该分段上的所有主机。该数据包的目标地址字段有一个独特的地址,以 224.x.x.x 开头,可由多个主机服务。主机将订阅特定的多播源。使用互联网组管理协议(IGMP),主机进行通信以通知上游服务它想订阅特定的源。许多交换机都有监控主机 IGMP 流量并窥探订阅源的逻辑。这使交换机能够确定应将多播流量复制给哪些主机。不实现 IGMP 窥探的交换机将多播流量视为广播流量。以下图表所示即为多播模式的示意图:
图 5.11 - 多播模式:机器 A 向机器 B 和 C 发送信息 在这一部分,我们回顾了网络层及其组件。现在我们将讨论传输层以及 UDP 和 TCP 协议。
UDP 和 TCP 用于传输层
基于以太网的 TCP/IP 或 UDP 是证券交易所和其他市场参与者使用的最常见的通信协议。非关键数据,如市场数据源,通常使用 UDP 传输以降低延迟和开销。TCP 和 UDP 协议族中最重要的协议之一是 IP。诸如订单等关键数据是使用 TCP/IP 协议传输。
传输控制协议(TCP)指定计算机如何连接到另一台机器,以及如何在它们之间传输数据。该协议是可靠的,并提供端到端(E2E)字节流网络传输。
数据报方向协议(UDP)用于广播和多播类型的网络传输。与 TCP 不同,UDP 不保证数据包传递。
主要差异概述如下:
传输控制协议是一种面向连接的协议,而用户数据报协议是一种无连接协议。
因为 UDP 没有任何机制来检查错误,所以 UDP 比 TCP 更快。
传输控制协议需要进行握手才能开始通信,而用户数据报协议不需要。
传输控制协议(TCP)检查错误并进行错误恢复,而用户数据报协议(UDP)检查错误并在出现问题时丢弃数据包。
用户数据报协议(UDP)没有传输控制协议(TCP)的会话、排序和交付保证特性。UDP 在延迟很重要的情况下使用,因为数据是以数据报的形式传递的。市场数据通常出于以下两个原因使用 UDP,如此处所述:
面向数据报的传递可以具有更低的延迟(但是如果有什么丢失,恢复会更复杂)。
多播不支持面向连接的协议(因为流量是多对多的通信类型)。
这通常通过应用层协议中的序列号以及提供带外机制来请求重传缺失的序列号来解决。如今,有一种趋势是使用 UDP 进行订单传输(UFO),这使我们能够更快地发送订单。
为高频交易交易所设计金融协议
让我们回到第 2 章"交易系统的关键组件"中介绍的以下图表。理解交易系统和交易所之间的通信机制很重要。
图 5.12 - 交易所与交易系统之间的通信 两个实体必须说同一种语言才能互相交流。为了实现这一点,他们使用网络中使用的协议。该协议被用于各种交易场所(有时称为交易场所)的交易。视交易场所而定,可能会有各种各样的协议。如果一个给定的交易场所和您的交易系统使用的协议相同,连接就可能实现。视交易场所的数量而定,一个场所通常会使用某种特定的协议,而另一个场所则使用不同的协议。交易系统需要建立在对其他协议的理解之上。尽管各个交易场所的协议存在差异,但它们建立连接并开始交易的过程是相似的,正如这里所概述的那样:
他们建立一个登录信息,指明了交易发起人、接收人以及连接将如何持续存在。
他们接下来询问他们从各公司期望什么,如订阅交易或接收价格更新。
他们也会收到订单以及价格变动。
他们然后发送心跳来维持连接。
最后,他们道别了。
金融信息交换(FIX)协议
修复协议
交易中最常用的协议是金融信息交换(FIX)协议,我们将在本章中讨论这一协议。它于 1992 年成立,目的是处理 Fidelity Investments 和 Salomon Brothers 之间的国际即时交易证券。其中包括外汇(FX)、固定收益(FI)、衍生工具和结算。这是一种基于字符串的协议,意味着人们可以阅读它。它是平台无关的,开放的,并有多种变体。
这里有两种不同的消息,如下所述:
行政通知,不包含任何财务信息
程序发送的消息以获取价格变动和订单
明文内容是一个键值对列表,类似于字典或映射。预定义的标签作为键;每个标签都是一个对应特定特征的数字。值可以是数值或文本值,与这些标签相关联。考虑以下场景:
我们希望发送一个订单的
$
1.23
$
1.23
$1.23 \$ 1.23 ,让我们想象价格标签上有 44 这个数字。因此,
44
=
1.23
44
=
1.23
44=1.23 44=1.23 将在订单消息中。
字符 1 分隔所有的对。这表示,如果我们使用在我们之前的示例中的 FIX 消息定义中对应于数量的标签 38 添加 100,000 的数量,我们将获得
44
=
1.23
∣
38
=
100000
44
=
1.23
∣
38
=
100000
44=1.23∣38=100000 44=1.23 \mid 38=100000 。
∣
∣
∣ \mid 符号代表字符 1。
8
=
F
I
X
.
X
.
Y
8
=
F
I
X
.
X
.
Y
8=FIX.X.Y 8=F I X . X . Y 是所有消息中使用的前缀。此前缀表示 FIX 版本号。版本号用 X 和 Y 表示。
10
=
nn
10
=
nn
10=nn 10=\mathrm{nn} 等于校验和。校验和是消息二进制值之和。这有助于检测感染。
以下是 FIX 消息示例:
8=FIX.4.42|=76| 35=A| 34=1| 49=TRADER1 | 52=20220117-
12:11:44.224| 56=VENUE1 | 98=0| 108=30|141=Y| 10=134
前述 FIX 消息中必填字段列举如下:
包含数字 8 和值 FIX.4.42 的标签。这与 FIX 版本号相同。
8 (开始字符串), 9 (消息长度)。
35(消息类型)、49(发送者公司 ID)和 56(发送者公司 ID)是高于 FIX4.42 的版本号。
标签 35 指定消息类型。
从标记 35 到标记 10 的字符计数由 body-length 标记表示,为 9。
检查和存储在第 10 个字段中。该值是通过将美国信息交换标准(ASCII)表示所有字节(直到检查和字段,这是最后一个字段)的十进制值乘以 256 来得出的。
现在我们知道了 FIX 协议是什么,我们将详细了解这个协议在交易中的应用。
协议用于 FIX 通信
要能够交易,交易系统需要两个连接: 一个接收价格更新,另一个下订单。 FIX 协议通过使用不同的消息满足这一标准,用于以下各种连接。
我们首先讨论价格变化的关系,然后描述订单的关系。
价格变化
当构建交易系统时,首要获取的是价格更新。价格更新是其他市场参与者的订单。交易系统将与交易所建立连接,订阅价格更新。我们将交易系统定义为启动方,交易所定义为接收方或接受方,如图 5.13 所示。
交易系统需要交易员选择交易的工具的价格。为此,交易系统与交易所建立连接,以订阅流动性更新。这里描绘了发起方(即交易系统)与接受方(即交易所)之间的连接:
交易所(接受方)
登录(开始会话以获取价格更新)
交易系统 (发起人)
登录确认
订阅一个或多个乐器
价格更新流(完整快照)
价格更新流(全快照)
Price update streaming (Full Snapshot)
OR | Price update streaming (Full Snapshot) |
| :--- |
| OR |
价格更新流(完整快照)
价格更新流(增量刷新)
再见
再见
Exchange (acceptor) Logon (Start a session to get price updates) Trading System (initiator)
Logon acknowledgement
Subscription to one or many instruments
Price update streaming (Full Snapshot)
"Price update streaming (Full Snapshot)
OR"
Price update streaming (Full Snapshot)
Price update streaming (Increment Refresh)
Bye
Bye | Exchange (acceptor) | Logon (Start a session to get price updates) | Trading System (initiator) |
| :---: | :---: | :---: |
| | Logon acknowledgement | |
| | Subscription to one or many instruments | |
| | Price update streaming (Full Snapshot) | |
| | Price update streaming (Full Snapshot) <br> OR | |
| | Price update streaming (Full Snapshot) | |
| | Price update streaming (Increment Refresh) | |
| | Bye | |
| | Bye | |
图 5.13 - 交易系统正在要求价格更新 以下截图显示了接受者和发起者之间发送的 FIX 消息:
图 5.14 - 使用 FIX 协议的交易系统获取价格更新
当交易系统接收到这些价格更新时,它会更新账本并根据信号下单。
订单
交易系统需要的第二个馈送是与交易所进行订单方面的通信。交易系统(发起方)将与交易所(接收方)建立通信。一旦建立了通信,发起方将向交易所发送订单。当交易所需要发送有关订单的更新时,它将使用此渠道进行沟通。
通过与交易所开启交易会话,交易系统将向交易所发送订单。当此主动交易会话处于打开状态时,订单通信将传递给交易所。交易所将使用 FIX 消息传输这些订单的状态。这在以下示意图中有所体现:
图 5.15 - 交易系统向交易所发送订单 发起方和接收方之间发送的 FIX 消息如下截图所示:
图 5.16 - 使用 FIX 协议向交易所发送订单的交易系统由于 FIX 协议是基于字符串的,解析器处理流数据需要一定时间。FAST 协议的开发目的是比 FIX 协议更快。
快速协议
快速协议是 FIX 协议的高速版本。市场数据通过位于 UDP 上层的快速协议从交易所或数据源传输到市场参与者。快速消息包含一系列用于传输元数据和负载数据的字段和操作符。快速协议旨在尽可能减少带宽使用,因此采用了多种压缩技术,如在此概述的那样。
第一个基本方法是增量更新,它只提供变化,例如当前股票价格和之前的价格,而不是持续传输所有的股票及其相关数据。
对于每个数据单词使用可变长度编码来压缩原始数据。虽然这些策略可以跟上提供者处理程序给出的增加的数据速度,但它们显著增加了处理复杂性。
压缩的 FAST 数据流必须在实时解码并分析,以将其转换为可处理的数据。如果处理系统跟不上数据流,则会丢失关键信息。
此外,解压数据流会增加必须成功处理的带宽量。因此,为设计高性能交易加速器需满足以下两个不同的标准:
首先,必须以最短的可能延迟对各种协议进行解码。
其次,它必须在给定的数据速率下维持数据处理。需要更深入地了解 FAST 协议,以检查现代交易系统的数据速率。
用于发送快速消息的 UDP 协议。多个快速消息被打包在单个 UDP 帧中,以减少 UDP 开销。快速通信不提供任何大小信息或框架定义。相反,每个字母都由模板指定,该模板必须在解码流之前理解。大多数馈送处理程序通过提供不同的模板要求来设计其 FAST 协议。必须小心,因为单个解码错误将丢弃整个 UDP 帧。模板定义一组字段、序列和组。这些组指定了一组字段。
快速属于一种针对提高带宽和通信速度而开发的协议簇,被称为简单二进制编码(SBE)。我们现在将讨论比基于字符串的协议更加高效的协议。
搔痒/疼痛协议
瘙痒和疼痛被视为二进制协议。疼痛通常通过 TCP 传输,而瘙痒则通过多播或 TCP 传输。瘙痒主要用于市场数据,而疼痛则用于订单。纳斯达克于 2000 年在一宗专利侵权诉讼影响 FAST 后创建了这些协议。基于瘙痒的交易所数据馈送在行业中广泛使用。由于许多不同于纳斯达克的交易所使用它,因此存在不同版本。芝加哥期权交易所(CBOE)也是另一个主要交易所,它广泛使用瘙痒的变种(如 CBOE 协议)。
芝加哥商品交易所(CME)市场数据协议。
期货交易所市场数据协议
中金所也创建了其为高频交易优化的 SBE 协议。 除了先前的协议之外,许多其他专有二进制协议以低延迟场所而闻名。所有这些协议都被视为负责连接/与交易所互动的外部协议。我们现在将讨论为外部网络和内部网络制定的交易所协议之间的差异。
内部网络与外部网络
从图 5.8 中可以观察到网络的两个不同层级。有一个层级是使用前期协议与外部世界交谈的层级,还有一个是公司内部网络(内部网络)中的网络交流。下图说明了贸易和交易网络中外部网络和内部网络之间的主要区别:
图 5.17 - 内部网络和外部网络 交易服务器与风险服务器通过内部网络进行通信,而前面图表左侧的交易系统将通过外部网络连接到交易所。
内部网络将用于以下活动:
在这个网络中,最小化主机(服务器)之间的跳数非常重要。最佳系统将是一个存在于网络接口卡(NIC)上的系统,我们将在第 11 章"高频 FPGA 和密码"中讨论这个系统。交换机和路由器的选择对网络延迟至关重要。
外部网络将用于交易所本地协议的订单输入和市场更新。
我们现在知道哪个网络可以被视为内部和外部。我们还知道数据包将使用哪种硬件从一个地方传输到另一个地方。在下一节中,我们将描述数据包的结构,并深入探讨数据包的生命周期以及数据包从一个点移动到另一个点时发生的情况。
理解数据包的生命周期
我们将使用铜线或光纤。这些线缆连接到网卡。这些线缆将传输来自交易所的市场数据包和发往交易所的订单。
我们首先需要讨论我们在这根电线上传递的哪个消息。这一部分将使用我们在本章中定义的 FIX 协议。让我们考虑以下 FIX 消息的示例:
8=FIX.4.2|9=95| 35=X| 34=5| 49=NYSE| 52=20160617-23:12:05.551|56=TR
ADSYS | 268=1|279=1|269=1|270=110| 271=5| 37=9| 10=209|.
这个 FIX 消息将成为图 5.18 所示数据包的有效负载。 数据包有两个主要部分。标头包含 OSI 模型各层的信息,以及如下所示的包含 FIX 消息的有效载荷:
图 5.18 - 分组报头 以太网层将代表数据链路层(如图 5.19 所示)、网络层的 IP 报头和传输层的 TCP 报头。FCS 是添加到此数据包的错误检测代码。正如我们之前所描述的,数据包(或帧)的每一层都包含每一层的信息。我们将在图 5.20 中说明这个数据包在机器上的处理方式,但首先,观察以下图表:
图 5.19 - 标头和 OSI 层 在下图中,您可以跟随一个传输市场数据更新的数据包在系统中通过的生命历程,直到抵达交易系统(此架构中运行的应用之一)
图 5.20 - 市场数据在操作系统中流动
我们将详细讨论获取数据包从网线到应用程序(我们的交易系统)所需的所有步骤。我们首先将讨论发送/接收路径。
接收/发送(TX/RX)路径中的数据包生命周期
交易系统通过交换设备发送数据包。铜线将该数据包传输到机器。数据包将按以下步骤进行:
网卡接收数据包并验证 MAC 地址(分配给网卡的唯一标识符(UID))是否与其 MAC 地址对应。如果是这种情况,该网卡将处理该数据包。
然后网卡验证 FCS 是正确的(校验和操作)。
当这两个验证完成后,网卡将使用直接内存访问(DMA)操作将数据包复制到接收数据的缓冲区(接收(RX)缓冲区)。
在图 5.20 中,RX 缓冲区是一个循环缓冲区(或环形缓冲区),这是一种使用固定大小缓冲区的数据结构,连接 E2E(主要用于避免使用锁定)。DMU 通过允许 I/O 设备直接向主内存传输或接收数据,从而绕过 CPU,从而加快内存操作。
网卡驱动程序会触发 CPU 中断,中断处理程序通常被分为上半部和下半部。上半部处理紧急任务,下半部处理其他处理任务。上半部管理中断确认和数据从网络移动到缓冲区以供下半部处理。处理器会从用户空间切换到内核空间,查找中断描述符表(IDT),并调用相应的中断服务例程(ISR)。然后再切换回用户空间。这些操作都在网卡驱动程序级别完成。
中央处理器将在空闲时初始化底层半部(软中断请求)。我们将从用户空间切换到内核空间。驱动程序分配一个套接字缓冲区或 SK-buff(也称为 SKB)。SKB 是一个内存数据结构,包含数据包头(元数据)。它包括指向数据包头的指针,以及有效负载。对于缓冲区(RX 缓冲区)中的所有数据包,NIC 驱动程序动态分配一个 SKB,用数据包头更新 SKB,删除以太网头,然后将 SKB 传递给网络堆栈。套接字是在软件层面上发送和接收数据的端点。
我们将现在解决网络层。我们知道网络层包含 IP 地址。该层将验证 IP 地址和校验和,并删除网络头。在验证 IP 地址时,地址将与路由查找进行比较。如果某些数据包被分段,此层将负责重新组合所有分段的数据包。一旦完成这一步,我们就可以处理下一层。
传输层特定于 TCP(或 UDP)协议。该层处理 TCP 状态机。它将数据包数据排入套接字读取队列。然后,最后,它将发信号表示可以在读取套接字中读取消息。
我们将通过讨论负责读写网络数据的软件层来结束本部分。
接收数据包的软件层
一旦有效载荷写入名为读取队列的读取套接字,剩下的唯一步骤就是让应用程序(交易系统)读取有效载荷。我们知道,操作系统会根据公平性规则安排应用程序从套接字读取数据。一旦交易系统(应用程序)读取有效载荷(在本例中为理解数据包生命周期部分中的 FIX 消息),它将开始解析消息的不同标签和值。
交易系统必须读取市场数据的所有步骤,高频交易主要关注操作在微秒或纳秒内完成所需的时间。因此,我们将探讨如何改进这一过程。
鉴于网络对速度至关重要,我们需要监控它。在下一节中,我们将讨论监控技术。
监控网络
网络对高频交易非常关键。从关键路径节省微秒来发送网络订单是关键。当网络建立并系统运行时,分析网络流量至关重要。在高频交易中,安全性并非真正的问题,因为网络大部分时间位于同一位置。我们将给予更多权重的监控部分是分析数据损失、延迟和中断的情况。我们需要确保网络畅通无阻,提供最佳性能。
数据包捕获和分析
捕获以检查或分析的以太网帧被称为数据包捕获。这个词也可以指由数据包捕获程序生成的文件,通常以.pcap 格式保存。捕获数据包是一种典型的网络故障诊断工具,也用于查找网络流量中的安全漏洞。数据包捕获提供关键的取证信息,有助于调查订单被拒绝的问题,这可能似乎是网络延迟问题。
数据包捕获及其工作原理
数据包可通过多种方式捕获。可通过网络设备如路由器或交换机上的专用硬件测试接入点(TAP)执行数据包捕获(将在后续章节中介绍)。最终目标决定所使用的方法。无论采用何种机制,数据包捕获都是通过复制网络中某处传输的部分或全部数据包实现的。
开始最简单的方法是从您的系统捕获数据包,但存在一些限制。网络接口默认仅处理发送到它们的流量。您可以将接口置于混杂模式或监控模式以获得更全面的网络活动视图。请记住,这种方法只能捕获部分网络流量。例如,在有线网络中,您只能观察到连接到您计算机的本地交换机端口上的活动。
端口镜像、端口监控和交换端口分析器(SPAN)是路由器和交换机上的功能,允许我们复制网络流量并将其传输到特定端口。许多网络设备都有数据包捕获功能,可用于从硬件的命令行界面(CLI)或用户界面(UI)诊断问题。
专用网络 TAP 可以适合于对特别大或繁忙的网络进行数据包捕获。TAP 是一种收集数据包的昂贵方式,但由于它们是专用硬件,所以不会影响性能。为了使 TAP 发挥作用,必须捕获双向(发送(TX)和接收(RX))数据。我们需要对 RX 和 TX 两侧进行探测以构建完整的图景。
以太网 TAP - 被动与主动 TAP 权衡
网络 TAP 是最精确的技术,用于监控和分析。有各种各样的网络 TAP,每种都有自己的优势,可确保网络正常运行时间和分析可靠性。这种方法可以是被动和主动网络 TAP。被动和主动 TAP 之间的区别可能令人困惑。
被动网络 TAP
设备的网络端口之间没有物理分离,称为被动网络监听端口(passive network TAP),如下图所示。这意味着即使设备断电,流量也可以继续在网络端口之间流动,保持连接。
图 5.21 - 被动网络 TAP 对于带有
1
0
/
1
0
0
1
0
/
1
0
0
10//100 \mathbf{1 0} / \mathbf{1 0 0} 米
(
1
0
/
1
0
0
M
)
(
1
0
/
1
0
0
M
)
(10//100M) (\mathbf{1 0} / \mathbf{1 0 0 M}) 铜接口和光纤 TAP 的网络 TAP 来说,这是正确的。光纤 TAP 通过将进入的光线分成两个或多个路径来工作,不需要电力。当使用时,10M 或 100M 铜 TAP 需要电力,尽管由于缺乏网络端口之间的物理隔离而完全被动。在这种情况下,在断电时链路仍保持操作,没有故障切换时间或链路恢复延迟。
主动网络监听端
使用 TAP 内部的电气组件,主动 TAP 在网络端口之间有物理隔离,不同于被动 TAP。因此,它们需要一种故障安全机制,确保即使 TAP 失电,网络仍能正常运行。该系统通过在设备开机时保持一组继电器处于开启状态来工作。当断电时,这些继电器切换到通过 TAP 的直接流量,确保网络运行。您可以在下图中看到这一说明。
图 5.22 - 被动网络 TAP 这两个 TAPs 将有助于捕获市场数据,以分析延迟并排查网络问题。获取这些数据本身是不够的,如果数据的时间不准确。在高频交易中,准确测量时间是至关重要的。在下一节中,我们将解释如何做到这一点。
时间分配的价值
高频交易需要时间竞争。时间是我们确保交易模型正确的最关键资源。由于我们发送的订单可能会根据到达时间而被执行或不执行,因此我们在构建交易策略时需要确信使用的时间与交易所使用的时间一致。我们需要使用时间同步服务来实现这种测量精度。
时间同步服务
在开始这一节之前,我们需要讨论获取精确时间的问题。在世界任何地方,我们都可以获得精确时间,而无需工程时间分配。我们使用全球定位系统(GPS)或全球导航卫星系统(GNSS),因为它们使用原子钟。
网络时间协议(NTP)服务是最广泛使用的同步计算机与时间服务器时间的服务之一。这个服务有几层称为阶层,在这里有更详细的描述:
从卫星定位系统获取数据的最高层
第 1 层:获取时间服务器的层,与第 0 层时钟有一对一的直接连接。您可以通过此层实现微秒级的同步。
第二层:连接到多个第一层服务器的层。
有多达 15 层可以获得不同类型的精度。返回的时间戳与 64 位时间戳一样大,并且可以精确地位于皮秒级别。不久的将来,将会有 128 位时间戳出现,我们甚至可能获得飞秒级别的精度。
精密时间协议(PTP)是一种基于网络的时间同步标准,致力于实现纳秒甚至皮秒级的精度。PTP 设备采用硬件时间戳技术而非软件,其设计目的是保持设备同步。PTP 网络提供远高于 NTP 网络的时间分辨率。与 NTP 设备不同,PTP 设备会记录同步消息在各设备中停留的时间,从而解决设备延迟问题。
这两种同步机制使用来自卫星的每秒脉冲(PPS)信号,提供了高度准确性。这些信号的准确度从每秒 12 皮秒到几微秒不等。
高频交易中时间的重要性
当在订单中插入时间戳时,高频交易者需要准确的协调通用时间(UTC)来跟随市场订单。大多数高频交易者在专用局域网上运行多个计算机系统,每个局域网使用单个 PTP 主钟,局域网上的每台计算机都与该主钟同步。主钟从外部来源获取时间,每个物理位置都需要有一个主钟。这些并行的高速计算机系统必须协调运作,以处理市场买入和卖出数据的算法。在日志文件和所有交易活动的网络分析中,也使用时间戳和计算机同步。
了解和分析局域网延迟对高频交易(HFT)的性能非常关键。如果时间同步不精确到微秒级,HFT 就无法优化硬件和软件。对于不共享机房的交易商,实时市场数据通过电缆、交换机和路由器传输,延迟在 1 毫秒到 5 毫秒之间。与不共享机房的交易商相比,共享机房的 HFT 将延迟降低到 5 微秒以下,从而拥有足够的时间来处理市场数据。
衡量市场行为和算法信号的精确度非常重要,因此测量的准确性必须非常高。
摘要
在这一章中,我们探讨了沟通和网络的重要性。我们学习了高频交易系统的网络组件。我们深入讨论了适用于快速通信的以太网协议。我们描述了金融协议的设计,并讨论了时间分配的价值。您现在已经掌握了理解高频交易网络的知识。
在下一章中,我们终于将开始讨论如何优化我们在本章和上一章中讨论过的所有 puzzle 部件。
6 高频交易优化 - 架构和操作系统
在上一章,我们概括了计算机的工作原理,重点介绍了与高频交易相关的主要组件。在本章,我们将讨论一些常用的计算机科学和架构优化技术,特别是在高频交易应用方面的应用。我们将对某些具体操作的内部机制、效率低下、缓慢和对高频交易软件造成的问题,以及用于解决这些问题的技术进行说明。
我们将讨论的一些操作和结构包括线程之间的上下文切换、用于并发访问共享数据结构的锁、以及内存管理/优化的动机和技术。
高频交易主要是一场军备竞赛,每个高频交易竞争者都试图尽可能快地执行交易。因此,掌握本章涵盖的主题的计算机科学基础知识将帮助读者了解如何生成极其优化且高性能的高频交易应用程序。我们将在本章中介绍以下主题:
理解上下文切换
构建无锁数据结构
预取和预分配内存
降低延迟微秒数的三个优化群组:
- ⚡️ 降低 1-10 微秒
- ⚡️ ⚡️ 降低 10-100 微秒
- ⚡️ ⚡️ ⚡️ 降低 100 微秒以上
⋅
⋅
* \cdot 三§゙ 低于 20 微秒
1
t
1
t
(1)/(t) \frac{1}{t} 低于 5 微秒
低于 500 纳秒 让我们开始吧!
理解上下文切换
计算机科学中的上下文切换是保存当前运行进程或线程的所有状态,并恢复即将运行的其他进程或线程的状态,以便从上次中断的地方继续执行的操作或任务集合。上下文切换的原理是支持多任务处理的现代操作系统的基石,它给人一种拥有比可用 CPU 核心数更多正在运行进程的错觉。
上下文切换
上下文切换可以根据我们关注的上下文切换过程的不同方面被分组成不同的类型。我们将简要讨论它们如下。
硬件或软件上下文切换
硬件上下文切换可以使用诸如任务状态段(TSS)之类的特殊硬件特性来保存当前运行进程的寄存器和处理器状态,然后跳转到另一个进程。在软件上下文切换中,当前的堆栈指针被保存,然后加载新的堆栈指针来执行新的代码。寄存器、标志、数据段以及所有其他相关寄存器都被推送到旧堆栈上,并从新堆栈上弹出。
硬件上下文切换需要特殊寄存器和/或处理器指令来实现,并且可以预期比软件上下文切换更快,这是由于可以利用特殊寄存器和指令。但是,在某些情况下,硬件上下文切换可能比软件上下文切换更慢,因为它需要保存所有寄存器。然而,现代操作系统选择在软件中实现上下文切换,这是由于更好的容错性和自定义保存和恢复哪些寄存器的能力。
在线程或进程之间切换上下文
在不同类型的进程切换中,另一种分类方式是是否发生在线程或进程之间。进程切换的延迟被称为进程切换延迟,而线程切换延迟则被称为线程切换延迟。如果线程或进程共享相同的地址空间,则上下文切换会更快,因为要执行的代码很可能已经由于共享地址空间而加载到缓存/内存中。在上下文切换期间,操作系统不仅需要从缓存和内存中删除和重新加载代码,还必须清理/刷新存储内存地址映射的数据结构,即虚拟内存空间。因此,线程之间的上下文切换通常更快,因为它们共享虚拟内存空间,不需要刷新和清理/无效化翻译后备缓冲区(TLB)。虚拟内存空间和 TLB 的细节是高级操作系统概念,超出了本书的范围,我们不会深入探讨这些主题。
上下文切换是有利的
上下文切换是大多数应用程序的有价值的功能,这些应用程序具有良好的用户输入、磁盘输入/输出(I/O)和 CPU 密集型处理的混合,如 Microsoft Office 套件、电子游戏或浏览器。在 HFT 生态系统中,某些(但并非全部)组件,如日志记录进程、图形用户界面(GUI)应用程序等,也能从上下文切换中获益。如前所述,在现代操作系统中支持多任务处理是不可能的,如果没有上下文切换。最重要的是多任务处理、操作系统中的中断处理,以及在用户模式和内核模式之间切换(通常在处理中断时调用)。我们将在以下部分对此进行讨论。
多任务处理
所有现代操作系统都有一个任务调度器,用于在 CPU 中切换一个进程和另一个进程。有几种原因可能导致正在运行的进程被切换出去。例如,当进程完成时,或者它被卡在 I/O 或同步操作上 - 在这两种情况下,都在等待来自另一个进程、线程或磁盘的输入。可以将线程或进程切换出去,以防止单个 CPU 密集型线程或进程占用所有 CPU 资源并阻止其他等待任务完成,这被称为 CPU 饥饿。
中断处理
中断驱动数据流在大多数现代架构中很常见。当进程需要访问资源时,例如从磁盘获取或写入数据(磁盘 I/O)或套接字/网络接口卡(NIC),进程不会占用 CPU 资源等待操作完成。这主要是因为磁盘和网络 I/O 操作比 CPU 操作慢得多,所以这将浪费大量的 CPU 资源。
在中断驱动的架构中,这样一个进程发起了 I/O 操作并被阻塞在该操作上。然后调度器将该进程切换出去并恢复另一个等待进程。在幕后,操作系统还与硬件安装了一个中断处理程序,该程序将在操作完成时中断正在运行的进程,并唤醒发起请求的进程,让它处理请求完成。
用户和内核模式切换
在上一节中,我们看到了一个中断驱动的示例,其中磁盘或数据包读取完成后,中断处理程序会唤醒发起请求的进程。在这一系列事件中,部分操作在内核空间中执行,即调用必要信号的中断处理程序,以及新进程获取一些 CPU 资源并开始处理数据。实际的数据处理通常发生在用户空间,并取决于应用程序本身。这并非唯一的内核和用户空间切换情况;运行在用户空间的进程调用的某些指令也会强制进入内核模式。对于大多数系统来说,这种切换不会引发上下文切换,但对于某些系统在用户和内核模式之间切换时可能会发生。在下一节中,我们将研究上下文切换操作涉及的一系列操作。对此有良好的理解很重要,因为这样就可以清楚地了解上下文切换在 HFT 应用程序中可能变得昂贵。
上下文切换操作涉及的步骤和操作
让我们来看看上下文切换中涉及的一些操作 - 具体是保存当前运行线程或进程的状态以及恢复下一个要运行的线程或进程的状态的任务,这由任务调度程序决定。请注意,本节提供了这些任务的总体概况,可能会缺少特定架构的一些具体细节;也就是说,每一种架构和操作系统都有自己的特例,但这个列表仍然可以作为涉及的步骤的通用列表。
保存当前进程的状态涉及将状态保存在通常称为进程控制块(PCB)的地方。它包含寄存器、堆栈指针寄存器(SP)、程序计数器(PC)和内存映射。还有各种表和列表用于当前线程或进程。
有可能有几个步骤来刷新和/或使缓存失效并刷新 TLB,它处理虚拟内存地址到物理内存地址的转换。
为即将运行的下一个线程或进程恢复状态是保存状态所需步骤的相反过程,即恢复要恢复的线程或进程的 PCB 中包含的寄存器和数据。
这节介绍了在执行上下文切换时涉及的各个步骤的高层视图。在实际应用中,根据硬件架构和操作系统的不同,可能还会有一些其他步骤,这可能会变得非常复杂和耗费资源。对于一个像高频交易(HFT)这样需要极低延迟的应用来说,这种开销会很大。我们将在下一节中看到上下文切换的缺点。
上下文切换对高频交易不利
现在我们对上下文切换有了一个良好的背景知识,让我们来看看为什么上下文切换对于高频交易应用程序来说并不理想。
默认 CPU 任务调度程序行为
对于高频交易(HFT),多核服务器的默认 CPU 任务调度算法通常并非最佳调度机制。不同的任务调度机制试图考虑诸如在可用线程和进程之间保持公平性、节省能源/提高能耗效率,以及通过优先运行最短或最长任务来最大化 CPU 吞吐量/效率等多方面因素。
这些任务通常与对高频交易应用程序来说至关重要的内容相冲突
我们宁愿不保护能源,并采取措施以防止服务器过热。
我们希望支持被超频的服务器,这再次并非节能。
我们想控制进程的调度/优先级,使得很低优先级的任务很少获得 CPU 时间,并/或被饥饿状态,而不是 HFT 应用程序获得尽可能多的 CPU 时间。
我们不会在 HFT 线程或进程已经消耗了大量 CPU 资源的情况下提前中止它,也就是说,没有必要公平对待,等等。
一般而言,通过更改内核和操作系统参数,以及使用多核服务器,其中关键的高频交易进程被固定到特定的隔离和专用核心上,以确保它们永远不会被抢占,通常还会将非高频交易进程移动到可用核心的特定小子集。
昂贵的任务在环境切换中
我们概述了在上下文切换时需要采取的操作,以保存即将被从 CPU 中移除的线程或进程的 PCB,并恢复下一个即将被调度到该 CPU 上的线程或进程的 PCB。任何工作比没有工作要更昂贵,但在上下文切换的情况下,某些步骤在计算上是非常密集的。我们在上一节讨论了任务调度,这也是上下文切换的开销之一。如果需要,在上下文切换期间刷新 TLB 和缓存也是昂贵的任务。执行上下文切换时,缓存失效也是另一项任务。我们在上一节中看到了 TLB 失效;缓存失效的工作方式与之非常相似。在缓存失效期间,已在缓存中编辑但未写入内存的数据将被写入内存。此外,当新代码替换旧代码使用的空间时,新代码必须从内存中取出并引入缓存,这需要的时间比访问缓存要长(称为缓存未命中)。这些缓存失效步骤会导致下一个线程或进程产生相当多的初始缓存未命中,从而导致在上下文切换后获得 CPU 资源的进程恢复缓慢。
避免或最小化上下文切换的技术
最后,让我们讨论如何设计和配置 HFT 的服务器/系统,目标是尽可能避免或最小化上下文切换。
将线程固定到 CPU 核心上
我们在"默认 CPU 任务调度器行为"部分讨论过这个问题,但是为了重申一遍,通过明确实现 CPU 隔离并将关键或 CPU 密集型线程(也称为热线程或旋转线程)固定到特定核心,可以确保热线程/进程上发生的上下文切换很少或没有。
避免导致抢占的系统调用
我们之前讨论过的另一个项目是,阻塞磁盘或网络 I/O 的系统调用会导致调用线程阻塞,并在数据请求完成时引起上下文切换和内核中断。为了最小化这些上下文切换,一个明显的解决方案是尽可能减少使用阻塞系统调用。另一种解决方案是使用内核旁路,我们在下一章中专门有一个名为"使用内核旁路"的章节。简单介绍一下,它完全避免了网络 I/O 操作(在高频交易应用中非常常见)的系统调用开销,而是使用 CPU 利用率来避免上下文切换。
这部分详细讨论了上下文切换。我们涵盖了以下主题:
上下文切换
应用程序从上下文切换中获益
任务涉及上下文切换
为什么上下文切换对于高频交易应用程序来说并不理想
最大化高频交易应用程序性能的技术,通过最小化上下文切换
在下一节中,我们将讨论另一个基本的计算机科学概念——锁。我们还将设计无锁数据结构,并了解如何避免使用锁来最大化高频交易(HFT)应用程序的性能。
构建无锁数据结构
在此部分,我们将讨论在高频交易生态系统中线程或进程之间共享的数据结构以及涉及的并发性和同步注意事项,尤其是考虑到高频交易系统的极高吞吐量和极低延迟要求。我们还将设计并讨论无锁数据结构的性能影响。无锁数据结构是一种在不同线程和/或进程中运行的生产者和消费者之间共享数据的机制。有趣的是,它通过完全避免锁来实现这一点,从而在高频交易中表现明显更好。我们将在后续部分更详细地研究这些问题。
当需要锁定时(非高频交易应用程序)
在传统的多线程/多进程编程范式中需要锁的原因。其根本原因在于需要允许对共享数据结构的并发访问,以及在共享资源上使用同步原语。我们将在下面的同步机制类型部分探讨包括互斥、信号量和临界区在内的同步原语。这些有助于确保某些线程不安全的代码部分在可能破坏共享数据结构的情况下不会并发执行。使用锁时,如果一个线程试图获取已被另一个线程持有的锁,第二个线程将被阻塞,直到第一个线程释放该锁。我们将在使用锁的问题和低效率部分看到使用锁的性能影响,以及我们如何克服它们,特别是在高频交易系统中。
同步机制类型
在本节中,我们将关注一些常见的同步方法,这些方法可以使用某种锁定/阻塞/等待机制实现并发访问。
内存屏障
内存屏障,又称内存栅栏或栅栏,用于指示编译器和处理器不要重新排序加载和存储操作。这提高了性能,对于单线程应用程序来说很好,但可能导致多线程应用程序出现奇怪、不可预测、不正确和不一致的行为。使用内存屏障禁止编译器和预处理程序重新排序特定关键部分的加载和存储序列,这可能会导致多线程高频交易环境中性能损失。内存屏障通常是同步原语和无锁数据结构的底层指令。
测试和设置
测试和设置是计算机科学的基本操作(基本代码/指令),它获取一个布尔变量指针,将其设置为 true,并以单个原子/不可中断的操作返回旧值。操作的原子性使其成为构建同步机制的完美基元。
获取并添加
获取并添加是一种原语,它接受一个指向变量的指针,将一个数字添加到变量中,并作为一个原子/不可中断的操作返回旧值。它用于构建需要计数的同步机制。
比较并交换
比较并交换(CAS)是最广泛使用的基元。它仅在地址处的变量具有给定值时才将值存储到地址。获取值、比较和更新的步骤是一个原子操作。CAS 不会锁定数据结构,而是在更新成功时返回 true,否则返回 false。
使用锁的问题和低效率
在这部分,我们将研究使用锁进行同步以供线程和进程并发访问共享数据结构时所涉及的一些复杂性/问题和低效之处。
应用程序编程和调试技能要求
使用锁进行编程并非软件开发中的一项琐碎任务。从一个数据结构中删除某些内容并将其插入到另一个数据结构中的简单任务,需要管理多个并发锁,并且需要精心的软件支持和严格的验证,以确保已经映射和处理了所有边界情况,因为在多线程应用程序中很难重现边界情况。
重复一遍,与锁相关或由锁引起的错误取决于操作的时间和代码路径。总的来说,它们可能非常微妙且极难重现,比如死锁。因此,调试使用同步机制的应用程序是一项非常艰巨的任务。
在锁开销(使用锁所需的额外内存/CPU 资源)和锁争用(线程试图获取已被另一个线程/进程占用的锁的情况)之间达到最佳平衡非常重要。这取决于问题领域、设计、解决方案的实现以及底层架构设计。在应用程序的生命周期中,随着使用案例的变化,这些设计考虑以及如何实现和维护锁开销与锁争用之间的最佳平衡也可能发生显著变化。
使用锁需要额外的资源,例如用于锁的内存空间,以及用于初始化、销毁、获取和释放锁的 CPU 资源。如我们之前讨论的,即使是微不足道的任务,也常常需要多个锁和多个锁获取和释放操作才能正确执行,因此随着应用程序的复杂度增加,与锁相关的开销也会增加。虽然发生争用的机会很小,但只要我们使用锁来保护对共享资源的访问,就会产生额外的开销。然而,现代处理器通常能够在没有争用的情况下避免在锁获取或释放操作期间进行上下文切换。
锁争夺
当一个进程或线程试图获取一个已被另一个线程或进程持有的锁时,会发生锁竞争。细粒度锁(即锁定较小代码区域或数据结构片段的单独锁)的竞争较低,但锁开销较高(因为更多的锁需要更多的资源)。
需要考虑许多与锁争用有关的问题。等待获取锁的线程或进程必须等待直到锁被释放,这会引入排队延迟。更重要的是,如果等待队列中的某个线程或进程死亡、停滞、阻塞或陷入无限循环,整个系统就会中断,因为现在等待该锁的线程将永远等待下去,导致死锁。
死锁
我们描述了一种死锁场景的可能性,即如果持有锁的线程永远不完成,所有其他等待获取该锁的线程将永远等待。然而,经典的死锁定义描述为一种场景,至少有一个任务被卡住等待获取另一个进程正在持有的锁。一个简单的例子就是,进程 1 持有锁 A 并试图同时获取锁 B,而进程 2 持有锁 B 并试图获取锁 A。在没有任何外部操作的情况下,这两个任务将永远被卡住。
异步信号安全性、杀死容差和抢占容差
本节将重新探讨并扩展我们在应用程序编程和调试技能要求部分提出的关于锁定争用的担忧(一个线程试图获取另一个线程持有的锁的实例),如果持有锁的线程或进程死亡或无法完成任何原因,它会使整个系统崩溃,因为需要获取该锁的任何线程或进程都无法取得进展。线程在持有锁时死亡或崩溃,以及这种情况下系统会发生什么,被称为其杀伤耐受性。可能的影响可以从操作系统检测到死锁线程/进程之前浪费大量时间,到由于需要重启整个系统而造成进度损失,甚至完全停止处理。
信号处理程序(操作系统机制用于处理意外场景/代码路径)例如不能使用基于锁的原语,因为无法猜测应用程序在执行什么代码或已获得什么锁的情况下会收到异步信号。一个特殊的例子是 C 编程语言中的 malloc()和 free()函数。例如,如果一个线程在内存分配任务中持有锁,并恰好收到一个信号,那么上下文立即切换到信号处理程序,所以线程永远没有机会释放锁。然后,如果信号处理程序执行并调用 malloc(),需要一个锁,那么我们又陷入了死锁。在原始线程被抢占(或者在任何其他原因导致线程在持有锁时被抢占的情况下),这种组件或系统的预期行为被称为抢占容忍性。
优先级反转
优先级反转是一种场景,在该场景中,一个低优先级线程或进程持有一个它与一个更高优先级线程或进程共享的公共锁。持有锁的低优先级线程可能会减慢或阻止更高优先级线程或进程的进度。这是因为在某些情况下,持有锁的较低优先级进程可能不会被调度器选中运行,因为其优先级较低,但每当更高优先级进程被选中运行时,它都会被锁的获取阻塞,因而无法取得进展。
优先级继承是一种解决方案,当高优先级进程由于共享锁等情况而等待低优先级进程时,调度程序会为低优先级进程分配相同或最高优先级,以处理优先级倒置问题。优先级上限协议是一种针对单处理器系统的类似解决方案,旨在尽量减少最坏情况下的持续时间并可能防止死锁发生。
护送
编队指的是另一种情况,当使用同步原语时会导致软件/应用程序性能下降。如果多个进程/代码路径试图以相似的顺序获取相同的锁,然后某个较慢的进程首先获得锁,那么所有其他进程都将被拖慢(在取得进展方面),只能跟上第一个进程的速度,因为即使其他进程很快完成自己的操作,它们仍然需要等待较慢的领先进程获取锁。此外,如果持有锁的线程由于任何原因被切换出去(例如持有锁的线程调用了 I/O 操作、一个更高优先级的进程启动,或者调用了中断处理程序),那也将再次增加其他进程完成的延迟。
构建使用同步机制的应用程序是复杂的,每个锁实例和锁操作都会增加额外的开销,存在死锁风险,系统可能会在某些特殊场景下变慢,需要特殊解决方案。因此,锁通常效率低下且开销较大,而且由于其阻塞、解除阻塞和上下文切换的性质,通常不是高频交易应用程序的首选机制。
无锁数据结构原型
在本节中,我们将讨论无锁数据结构设计,以避免在高频交易应用程序中使用同步原语所带来的所有问题和低效问题。一般来说,设计通用的无锁算法是很困难的,所以通常的方法是设计无锁数据结构 - 无锁列表、栈、队列、映射和双端队列是一些例子。我们可以在高频交易系统需要不同线程和/或进程之间进行交互或数据共享的地方使用这些无锁数据结构。在本节中,我们将设计和理解无锁生产者-消费者数据结构的细节。生产者-消费者数据结构基本上是队列,生产者可以写入数据,消费者可以读取数据 - 在将数据传递给高频交易组件时是一个常见的任务。当只有一个生产者和一个消费者时,称为单生产者单消费者(SPSC),当有多个生产者和消费者时,称为多生产者多消费者(MPMC)。
有关无锁 malloc() 和 free() 的大量研究,迈克尔(PLDI 2004 年)的《可扩展无锁动态内存分配》就是一个例子。无锁的 malloc() 和 free() 在增加处理器时几乎完美扩展。它们还能很好地处理不同的争用水平,与其他专门的 malloc() 实现相比,它们具有非常低的延迟。正如我们在预取和预分配内存部分看到的,这对高频交易应用程序并非巨大优势,因为它们尽可能避免使用动态内存分配,但在需要使用无锁的 malloc() 和 free() 的情况下,它仍然非常有价值。
从根本上说,无锁数据结构的主要论点是最大化并发性。对于使用锁的容器而言,总是存在线程需要阻塞在获取锁的步骤上并等待并承担昂贵的上下文切换延迟才能取得进展的可能性,因为互斥锁的目标本来就是互斥。使用无锁数据结构,线程每次运行都取得进展,这实际上实现为一个自旋锁。自旋锁不会阻塞,而是不断检查锁是否可用。这种行为被称为忙等待。
串行和并行
无锁数据结构会是什么样子?我们将讨论一个简单的无锁 SPSC 示例 - 尽管我们在这里讨论的同样的思路可以扩展到 MPMC。我们可以通过在内存中为每个生产者设置不同的队列,并为每个消费者设置不同的位置跟踪变量,将 SPSC 改进为 MPMC。在 MPMC 设计中,每个生产者都会将新数据写入自己的队列,每个消费者都有自己的变量来跟踪最后消费的元素。有了这种设计,生产者可以独立地生产和写入数据,而不需要担心踩到其他生产者的脚,每个消费者也可以独立地从一个或多个这些队列中消费数据。
这里有一个图表描述了生产者和消费者如何在无锁 SPSC 设计中跟踪被写入和读取的内存槽
无锁单生产者单消费者
图 6.1 - 生产者和消费者在无锁 SPSC 设计中跟踪内存槽的写入和读取
在无锁 SPSC 或 MPSC 的最常见实现中,生产者将新数据添加到队列的末尾,消费者从队列的前端消费旧数据 - 因此它是一个先进先出(FIFO)式的队列。 队列的底层数据结构/内存存储通常被选为一个大型、固定大小的预分配数组,这有许多优点,如无需动态内存分配和连续的内存访问。 我们将在内存池部分进一步讨论这个问题。
生产商使用一个名为 last_write 的变量来跟踪数组中最后一次写入数据的索引,或即将写入数据的索引 - 从概念上来说,它们都完成了相同的工作。生产商添加节点并递增最后一个写入数据的槽位变量。消费者有一个名为 last_read 的变量,它用于跟踪最后一次读取数据的索引。消费者通过比较 last_write 和 last_read 变量来检查是否有新数据可读,并一直消费数据直到没有更多数据可消费。
在这里,生产者通常会使用 CAS 原子操作来递增 last_write(类似地,消费者会使用 CAS 原子操作来递增 last_read 变量),但在 SPSC 设置中这不是必需的,因为最坏情况下是新节点已添加到队列中,但 last_write 尚未更新当消费者检查数据时,这种情况下它将在下次消费者检查数据且 last_write 已更新时被读取。
当生产者和消费者读取数组的末尾时,索引会环绕并从 0 开始重新开始。维护的唯一不变量是 last_read <= last_write。当数组大小选择足够大且消费者足够快速处理数据,不会努力跟上生产者并/或减慢生产者将数据写入队列的能力时,此设计最为合适。
高频交易中无锁数据结构的应用
我们将最后一次重复这一点,但无锁数据结构(以及对它们的操作)在负载较重的情况下,吞吐量(无争用)和延迟(无开销)明显更高,与使用锁的替代方案相比。如果持有临界区锁的进程被切换上下文,它将阻塞需要访问该锁才能继续的其他线程/进程。这些线程必须等待原始线程再次被调度运行,完成临界区的任务,并释放锁。在无锁算法中,这一系列事件不会浪费任何时间,因为它们可以在不等待的情况下更改共享变量。原本想要修改变量的线程现在至少必须再循环一次并重试。
无锁单生产者单消费者(SPSC)、单生产者多消费者(SPMC)、多生产者单消费者(MPSC)和多生产者多消费者(MPMC)在高频交易系统中的各个地方都有使用。不列举所有应用,下面的章节涵盖了一些重要的应用,从这些示例中应该很容易推广到您想使用这些无锁数据结构的其他场景。
市场数据传播在关键路径上
根据一个策略正在从多少个市场数据源获取数据,我们可以为每个交易进程设置一个 MPSC 风格的配置,该进程从多个源通过无锁队列获取市场数据。如果每个交易服务器上有多个交易进程,那么该配置可以变成一个 MPMC 风格的设置,其中不同的市场数据源处理程序是多个生产者,而不同的交易进程是多个消费者。
订单请求关键路径
这里的设置与市场数据传播在关键路径一节中描述的情况完全相同,不同之处在于数据流是从交易进程到订单网关。这里,同样取决于交易进程和订单网关的数量,可以设置为无锁 SPSC、MPSC 或 MPMC。
日志记录和在线计算统计
日志记录是这些我们在本节讨论的无锁数据结构的另一个有趣应用。日志记录中的许多任务可能会很慢,并不适合关键性能路径,例如以下内容:
将数据格式化为某种人类可读的格式 - 这并不总是必要的,但当需要时,它涉及到缓慢的字符串操作。
将日志写入磁盘 - 这涉及使用超级慢的磁盘 I/O 操作。
计算运行统计数据 - 这可能会根据数据量和统计性质的不同而变慢。
由于这些任务的缓慢和非确定性性质,HFT 生态系统中涉及的不同进程常常会以最简单的格式将数据发送到单独的日志记录线程或进程,该线程或进程负责执行较慢的任务,而不是在主要执行路径上进行这些任务。在这里,无锁数据结构再次派上用场。我们将在第 7 章"HFT 优化 - 日志记录、性能和网络"的"深入探讨日志记录和统计计算"部分讨论日志记录和统计计算,因为这是 HFT 应用程序中特别重要的一个组成部分。下图显示了可以使用无锁队列进行高效数据传输的不同组件:
从市场数据馈送处理程序到交易过程
从交易过程到订单网关
另外从市场数据源处理程序、交易流程、订单网关和其他组件来将日志记录卸载到日志记录进程中
让我们看看下面的图表,它显示了不同类型的无锁队列:
图 6.2 - 不同类型的无锁队列(SPSC、SPMC 和 MPMC)在完整 HFT 生态系统中的布局和使用
在本节中,我们讨论了传统的计算机科学并发/同步机制以及它们在多线程应用程序中的必要性。我们然后探讨了为什么使用锁对高频交易应用程序来说是低效的。最后,我们设计了一个无锁数据结构,用于同时处理多个数据生产者和多个数据消费者,并看到它如何融入高频交易生态系统。在下一节中,我们将探讨一个对所有应用程序都很重要,但对高频交易应用程序尤其相关的话题 - 高频交易应用程序生命周期中的内存分配。
预取和预分配内存
在这个部分,我们将从提高内存访问和分配延迟的角度,特别是在关键路径上,看两件事关于 HFT 应用程序使用的内存访问和分配。
我们将从讨论内存层次结构开始 - 从访问速度超快但成本昂贵且容量有限的内存,到访问速度超慢但成本低廉且容量巨大的内存。我们还将讨论一些设计高频交易应用程序的策略,以针对最优内存访问延迟。
动态内存分配的工作原理、为什么在热点路径上动态分配内存效率低下,以及如何在不过多牺牲动态内存分配所带来的灵活性的情况下获取最大性能的技术。
存储器层次结构
首先,我们将讨论现代体系结构中的存储层次。我们将从距离处理器最近的存储开始,它具有最低的访问延迟(但最高的成本和最低的存储容量),并向外移动,从处理器寄存器到各级缓存,再到主内存(RAM),最后到应用程序驻留的磁盘存储,这些应用程序在启动时被加载到内存中。请注意,这些数字因体系结构和处理器的不同而有所不同,并且正处于不断变化的状态,因此这些只是对预期结果的大致近似,未来可能会发生变化。
首先,让我们看一下描述内存层次金字塔的图表,从顶部最快、最小、最昂贵到底部最慢、最大、最便宜
现代架构中的内存层次 从底部到顶部移动时,内存/存储选项变得更快、容量更小、成本更高。
处理器寄存器
处理器寄存器拥有最快速的访问(通常为 1 个 CPU 周期)。处理器寄存器组最多可以容纳几个字节的大小。
缓存
处理器寄存器之后是缓存银行,它由几个不同的缓存级别组成。我们将按从最快(最小)到最慢(最大)的顺序在此讨论这些缓存级别。
一级缓存
级别
0
0
0 \mathbf{0} (L0) 缓存 - 这是在处理器寄存器之后访问的。L0 缓存大约 6 千字节。
一级缓存
一级(L1)缓存(指令和数据缓存)的大小为 128 千字节。数据缓存的访问时间约为 0.5 纳秒,指令缓存的分支预测错误需要 5 纳秒。分支预测是一种高级计算机体系结构功能,处理器/操作系统根据以前的访问模式猜测下一个代码路径。当它猜对时,可以在需要之前预取代码,从而加快访问和执行。由于这是一种猜测,有时会不正确,这被称为分支预测错误。我们不会深入探讨分支预测的细节,因为那超出了本书的范围。
二级缓存
二级 (L2) 缓存 - 指令和数据 (共享) - 大小不等,可从 256 千字节到 8 兆字节。L2 缓存访问需要大约 5-7 纳秒。
三级缓存
第 3 级(L3)共享缓存 - L3 缓存的大小可以从 32 兆字节到 64 兆字节不等。L3 缓存是最大但也是最慢的,访问时间约为 12 纳秒。L3 缓存可以存在于 CPU 本身,但是每个核心都有 L1 和 L2 缓存,而 L3 缓存更多是一种针对芯片上所有核心的共享缓存。
二级缓存
4 级(L4)共享缓存 - 这些可以从 64 到 128 兆字节不等。访问时间约为 40 纳秒。
主内存
主内存(主存储器)的大小从 16 到 256 GB 不等。访问时间约为 60 纳秒。非均匀内存访问(NUMA)机器会出现非均匀访问时间。NUMA 是一个高级概念,在多线程环境中,访问时间取决于内存位置相对于处理器的位置,但深入研究 NUMA 已超出本书的范围。
磁盘存储
磁盘存储(辅助存储) - 这可达到太字节的大小。对于固态存储,访问速度约为 100 微秒,对于非固态存储,约为 300 微秒。
内存访问效率低下
在前一节中,我们讨论了内存层次结构和从不同存储设备获取数据时产生的延迟 - 基本上,当应用程序第一次请求数据时,它可能不会在寄存器或缓存中找到,甚至可能(我们将在"基于预取的提升性能的替代方案"部分讨论为什么使用"可能"这个词)不在主存中,在这种情况下,它会从磁盘加载到主存(产生 100-300 微秒的延迟),然后从主存加载到 L0 到 L4 缓存。数据不是一次加载几个字节,而是一次加载一个或几个页面(几千字节),因此对同一数据(或者位于相邻内存地址的数据)的后续引用会命中缓存,从而获得显著更低的访问延迟。
在最坏情况下,当我们在较快的存储级别遇到缺失时,需要访问较慢的存储、取回并缓存到较快的存储中,然后访问所请求的数据,这会导致数百微秒的访问延迟。
最佳情况是当数据已经存在于 L0/L1 缓存中,访问时间降低到低于一纳秒。平均情况下的延迟(称为平均访问时间)(AAT)),这就是我们真正试图优化的。这是在应用程序生命周期内的访问时间的平均值。
假设我们有一个虚拟的设置,其中主存储器的访问时间为 60 纳秒,L1 缓存的访问时间为 0.5 纳秒,但缺失率为
10
%
10
%
10% 10 \% ,L2 缓存的访问时间为 5 纳秒,缺失率为
1
%
1
%
1% 1 \% ,L3 缓存的访问时间为 12 纳秒,缺失率为
0.1
%
0.1
%
0.1% 0.1 \% ,那么 AAT 的情况如下:
AAT-no-cache
=
60
=
60
=60 =60 纳秒
芯片中的一级缓存:AAT-L1 = L1 命中时间 + (L1 缺失率 * AAT-no-cache) 纳秒
二级缓存:AAT-L2 = L1-命中时间 + (L1-失误率 * (L2-命中时间 + L2-失误率 * AAT-无缓存
)
)
=
0.5
+
(
0.1
∗
(
5
+
0.01
∗
60
)
)
=
1.06
)
)
=
0.5
+
0.1
∗
5
+
0.01
∗
60
=
1.06
))=0.5+(0.1^(**)(5+0.01^(**)60))=1.06 ))=0.5+\left(0.1^{*}\left(5+0.01^{*} 60\right)\right)=1.06 纳秒)
二级缓存:AAT-L3
=
0.5
+
(
0.1
∗
(
5
+
0.01
∗
(
12
+
0.0001
∗
60
)
)
)
=
1.012
=
0.5
+
0.1
∗
5
+
0.01
∗
(
12
+
0.0001
∗
60
)
=
1.012
=0.5+(0.1^(**)(5+0.01^(**)(12+0.0001**60)))=1.012 =0.5+\left(0.1^{*}\left(5+0.01^{*}(12+0.0001 * 60)\right)\right)=1.012 纳秒
这里的要点是,应用程序具有导致更多缓存命中(从而缓存失误更少)的内存访问模式,在访问内存方面的性能会明显优于具有导致更多缓存失误(从而缓存命中更少)的访问模式的应用程序。
在前一节中,我们讨论了缓存命中如何影响内存访问性能。因此,为了充分利用缓存性能,高频交易应用程序开发人员需要专注于编写缓存友好型代码,其中最重要的方面是位置原理,它基本上描述了为什么将相关数据集放在内存中彼此接近的位置可以实现更高效的缓存。你必须了解每个缓存的大小、每个缓存行可容纳的数据量以及 HFT 生态系统所在特定体系结构的缓存访问时间。
时间局部性
时间局部性指的是这样一个原则:如果某个内存位置被访问过,那么很可能在不久之后,会再次访问同一个位置。因此根据这一原则,缓存最近访问过的数据是有意义的,因为很可能会再次访问这些数据,而且在那时它们仍然会存在于缓存中。
空间局部性
空间局部性指将相关的数据片段放置在彼此附近的原则。通常,加载到 RAM 中的内存是以大块(大于应用程序请求的数量)获取的,对于硬盘驱动器和 CPU 缓存也是如此。这样做的原因是,应用程序代码通常是顺序执行的(而不是在内存地址之间随机跳转),因此程序很可能需要在先前获取的大块数据中的数据,从而产生缓存命中或内存命中,而不必从磁盘中获取。
适当的容器
在编写缓存友好的低延迟 HFT 代码时,仔细考虑所使用的容器很重要。通常提出的一个简单示例是在 C++标准模板库(STL)向量和列表之间进行选择。虽然从 API 角度来看,两者似乎都在为类似的目的服务,但向量的元素保存在连续的内存位置,在访问它们时比列表更加缓存友好,因为列表的元素并不一定存储在连续的内存中,而是分散在内存的各处。在这里,空间局部性的原理发挥作用,使得向量元素的访问比列表元素的访问性能显著更好。
缓存友好的数据结构和算法
这是前一个要点的一般化 - 在设计数据结构和算法时,意识到缓存和缓存性能是很重要的,并尝试以最大化缓存使用和性能的方式来设计,尤其是针对高频交易应用。
利用数据的隐式结构
另一个常见的讨论空间局部性时常提到的简单示例是经典的二维数组,它可以是列主序(列元素在内存中连续)或行主序(行元素在内存中连续)。我们应该考虑这一点并相应地访问元素 - 例如,对于列主序二维数组,访问同一列中的元素比访问同一行中的元素效率高得多,而对于行主序二维数组,情况恰恰相反。从主内存中提取并缓存在缓存银行中的数据是以块为单位进行的。因此,当访问矩阵的某个元素时,内存位置附近的元素也会被提取并缓存。利用这种顺序,我们可以执行更少的内存访问操作,因为需要访问后续元素的计算可以直接在缓存行中找到这些元素。
避免不可预知的分支
现代编译器、处理器流水线和体系结构在预取数据和重排代码方面非常出色,从而最大限度地减少内存访问延迟。但是,如果关键代码包含不可预测的分支,它们在性能方面就会受到影响,因为无法预取数据并利用时间或空间局部性进行预取。因此,与没有大量不可预测分支的代码相比,这会导致大量缓存未命中。
避免虚拟函数
在 C++中,虚函数通常在编写低延迟 HFT 软件时被尽量避免,主要是因为它们使许多特别重要的编译器优化变得不可能,因为编译器无法确定在编译时会调用哪个方法实现,所以无法内联方法,处理器也无法预取数据。这会导致在查找时出现更多缓存未命中,如果特定方法不经常被调用(否则方法主体很可能被缓存,不会导致缓存未命中)。虚函数的存在并不是缓存友好性最大的问题,但这仍然是需要注意的事项。
在下一部分,我们将看一看与内存分配和访问相关的另一个主题 - 动态内存分配。动态内存分配在复杂的大规模应用程序中非常常见,所以建立对这个概念的良好理解是很重要的。您应该建立一个良好的理解,即每次调用动态内存分配操作都涉及多个步骤,这可能会给高频交易应用程序带来性能损失。
动态内存分配
动态内存管理是一个优秀的功能,自 C 语言诞生以来就一直存在。它允许应用程序管理(分配/移动/释放)在运行时动态确定大小的内存块。然而,实际的内部实现和性能影响使其不太适合需要非常紧凑(操作延迟方差很小)性能的极低延迟 HFT 应用程序。在动态内存分配期间分配的内存存在于堆段中,当内存被释放时,它会返回到堆中。但是,在实践中,使用动态内存管理并非易事,应用程序实现中的小错误可能会导致微妙的内存泄漏问题和关键路径上的性能问题。
动态内存分配步骤
操作系统使用两个链表维护空闲堆内存块 - 一个是未分配的堆内存块的已释放列表,另一个是已分配的堆内存块列表。当有新内存块请求时,它会遍历已释放列表,直到找到足够大的内存块来满足请求,然后将部分或全部内存块从已释放列表移动到已分配列表,并返回已分配的内存地址给调用者。
程序生命周期过程中可能会有任意数量的内存分配和释放,顺序随机。由于这些操作,已释放内存块列表可能会出现空洞,当之前分配的内存块陆续返回到空闲列表时,新的动态内存分配请求可能会从这些空洞中获取。这种已释放堆块列表出现空洞的现象称为内存碎片化。在某些罕见情况下,可能会出现大量碎片,由于太多空洞的存在,内存被浪费,这些空洞也无法满足动态内存分配请求,因为每个空洞可能都太小。分配器会定期采取一些技术和策略来收集和合并这些内存块,以防止堆内存碎片化,但这已超出本章的讨论范围。
内存泄漏是动态内存分配的另一个问题,即分配了内存块但未使用,且应用程序从未释放,因此未返回到空闲池中。这种情况称为内存泄漏,会导致内存使用膨胀,同时由于大量内存占用而性能下降,甚至可能导致操作系统终止泄漏内存的进程和/或饿死需要动态内存分配的所有进程。
动态内存管理引入了应用程序复杂性、潜在的错误,以及由于跟踪/更新链表跟踪内存块而带来的开销和延迟,并且由于内存碎片化可能会产生性能问题。性能影响使得动态内存管理对于 HFT 应用程序来说并不理想。
预先分配的动态内存分配替代方案
在这个部分,我们将探讨在动态内存管理方面的一些替代方案。我们希望保留在运行时能够分配和释放任意数量元素的灵活性,但我们将尝试探索如何在不遇到操作系统提供的动态内存管理的性能问题的情况下实现这一点。
动态内存管理问题的一些解决方案列在这里。
限制内存到堆栈
限制动态内存分配在堆上是一个明显的解决方案。这意味着如果某些东西可以分配在栈上,那应该是首选。简单的技术,如限制元素数量的上限,在栈上使用局部变量来处理最多该数量的元素,可以帮助消除在编译时需要的元素数量未知时使用动态内存分配的缺点。
内存池
如果知道对象的类型(这是相当常见的情况;例如,包含市场数据中订单的详细信息(如价格、方向和数量)的订单对象类型),通常创建内存池并自行管理内存会更加高效。由于对象的类型和大小是已知的,通过使用模板,内存池实现可以变得通用但非常高效。此外,由于每个元素的大小是已知的,我们不必担心内存碎片。此外,我们可以使用 LIFO 堆栈式的释放/分配方案,而不是链表,这可能会带来更好的缓存性能。最后,使用大页可以提高 TLB 翻译效率,在我们预计创建许多特定类型对象的场景中很有帮助。
在现代架构和操作系统中访问内存的细节,内存层次结构的设计,常规访问模式的低效,以及为高频交易应用程序需求最大化性能的一些技术。我们还讨论了动态内存分配,在关键性能路径上使用它的缺点,以及在不影响高频交易应用程序性能的情况下使用它的技术。
摘要
我们讨论了各种计算机科学结构的实施细节,如上下文切换、同步和并发原语。我们还讨论了这些特性对高频交易应用程序的影响,发现通常最适合大多数应用程序的默认行为并不是高频交易应用程序的最佳设置。
最后,我们讨论了最佳高频交易生态系统性能的方法/工具/技术/优化。我们希望这一章能为您提供对高频交易优化技术及其对高频交易生态系统性能的影响的洞见。
对于希望建立高频交易系统和交易策略的开发人员和交易员来说,了解这些高频交易优化技术的使用至关重要。在高频交易中,最大的优势来自于超低延迟的算法/软件,要在这个领域有竞争力,您将需要采用这些技术。
高频交易优化技术/技术接下来的章节将继续讨论这一主题。我们将讨论适用于内核绕过技术、网络、日志记录和性能测量的高频交易优化。
在前一章中,我们研究了许多较低层次的高频交易优化任务和优化技巧及技术。在本章中,我们将继续讨论并探讨更多有关高频交易优化的话题。这里的重点将是内核和用户空间操作以及与之相关的优化。我们还将探讨内核旁路以及与网络、日志记录和性能测量相关的优化话题。
我们将讨论的一些操作和结构包括操作系统(OS)和服务器硬件级别的内存、磁盘和网络访问操作,以及不同物理位置(微波/光纤选项)之间的数据中心网络架构。我们还将讨论与实时性能测量相关的日志记录和统计指标等主题。包括上一章涵盖的主题,在本章结束时,您将对 HFT 交易架构中涉及的所有现代性能优化工具、技术和技术有非常好的理解。
在本章中,我们将涵盖以下主题:
用户空间与内核空间的比较
使用内核绕过
学习内存映射文件
使用光纤电缆、空心纤维和微波技术
深入研究日志和统计
衡量绩效
重要说明
为了指导您完成所有优化,您可以参考以下图标列表,这些图标代表了一组可降低延迟特定微秒数的优化: 低于 20 微秒 低于 5 微秒 您将在本章标题中找到这些图标。
了解这些主题很重要,因为没有现代高频交易系统没有应用这些技术来最大化性能。了解这些主题对建立有竞争力的高频交易业务至关重要。
内核空间和用户空间
我们在前一章中探讨了内核和用户空间的概念。为了让我们记忆一新,一些特权命令/系统调用只能从内核空间进行,这种设计是有意的,以防止错误的用户应用程序通过运行任何他们想要的命令来危害整个系统。从高频交易应用程序的角度来看,如果需要进行系统调用,就需要切换到内核模式并可能进行上下文切换,这会减慢其速度,尤其是如果系统调用在关键代码路径上频繁进行。让我们在本节正式总结这个讨论。
内核和用户空间
内核是所有现代操作系统的核心组件。它可以访问内存、硬件设备和接口等所有资源,实际上包括机器上的一切。内核代码必须是最经过测试的代码,才可以在内核模式或内核空间中运行,以维护机器的稳定性和健壮性。用户空间是普通用户进程运行的地方。操作系统内核仍然管理用户空间应用程序,并控制它们可以访问的资源。虚拟内存空间也被划分为内核空间和用户空间。尽管物理内存不区分这两个空间,但操作系统控制着对它们的访问。用户空间无法访问内核空间,但相反是可以的,也就是内核空间可以访问用户空间。下面的图表将帮助您理解这些组件的布局:
图 7.1 - 内核空间和用户空间组件之间的通信
当运行在用户空间中的进程需要执行如磁盘 I/O、网络 I/O 和受保护模式例程调用等系统调用时,它们通过系统调用来执行。在这种设计中,系统调用是内核暴露给用户空间进程的内核接口的一部分。当从用户空间进程调用系统调用时,首先会向内核发送一个中断以执行系统调用。内核会找到正确的中断处理程序来处理系统调用,并启动该处理程序。一旦中断处理程序完成,处理就会继续进入下一组任务。这不是内核和用户空间之间切换的唯一情况;运行在用户空间中的进程调用的某些指令也会强制进入内核模式。对于大多数系统来说,这种切换不会引发上下文切换,但对于某些系统而言,在用户模式和内核模式之间切换时可能会发生上下文切换。
总的来说,在内核空间运行的代码与在用户空间运行的代码的速度是相同的。性能差异主要出现在系统调用时 - 在内核空间执行的代码在涉及系统调用时会执行更快,而在用户空间执行的代码由于需要切换到内核/监管模式而执行更慢,这种切换也会触发更多的上下文切换。因此,对于用户应用程序来说,应该尽可能减少系统调用的使用,如果可能的话甚至完全消除。
另一个例子是 gettimeofday()和 clock_gettime(),它们在底层调用系统调用。由于高频交易应用程序需要频繁更新时间,这会导致大量系统调用。可以用 rdtsc()指令和某些架构上的 chrono time 调用来消除系统调用。
总的来说,在高频交易应用程序开发过程中,有很多机会可以消除或最小化(后者更加现实)从用户空间应用程序调用的系统调用。你只需要思考一下被调用的方法是否会触发系统调用,以及是否有更好的方式来实现相同的功能,但不需要调用系统调用 - 至少在关键的热点代码中是如此。
我们将在关于学习内存映射文件的部分看到消除系统调用的示例。通过将文件加载到内存中并允许进程直接对内存进行更改,并延迟/限制更改提交到磁盘的频率,该设计最大程度地减少了系统调用。另一个完全消除网络读/写操作系统调用的例子是内核旁路,我们将在"使用内核旁路"一节中讨论这一点。
我们将讨论使用该技术实现的延迟改进;然而,您应该知道一部分改进是通过消除不必要的数据缓冲区副本实现的。在我们深入探讨细节之前,我们将在这里介绍一些关于改进的数据。不使用内核旁路的 UDP 读/写时间在 1.5 到 10 微秒之间,使用内核旁路的范围在 0.5 到 2 微秒之间。TCP 读/写时间的性能提升也类似,只是稍微慢一点。让我们从讨论内核旁路技术的细节及其优势开始。
使用内核绕过
在这个部分,我们将讨论使用内核旁路技术来提高用户数据报协议(UDP)套接字处理来自交易所的入站市场数据更新以及传输控制协议(TCP)套接字发送出站订单流/请求到交易所的性能。从根本上说,内核旁路旨在消除在内核模式和用户模式之间以及从网络接口卡(NIC)到用户空间复制数据的昂贵的上下文切换和模式切换,这将大幅降低延迟。
网络处理由系统调用/中断驱动的非内核旁路设计,线程或进程想要读取 UDP 或 TCP 套接字上的传入数据阻塞在读取调用上,如前一章"理解上下文切换-中断处理"部分所述。这导致被阻塞的线程或进程被上下文切换出去,然后在套接字上有数据可用时被中断处理程序唤醒。我们在前一章讨论过,线程上下文切换以及内核模式到用户空间的切换是低效的,因为它在每个数据包读取上增加了延迟。
对于处理市场数据和订单响应的高频交易应用程序,一天中会发生数百万次此类数据包读取,因此延迟会累积起来并导致显著的性能降低。此外,数据从内核空间的网卡缓冲区复制到用户空间的应用程序缓冲区,所以额外的复制是另一个延迟来源。类似的复制机制存在于发出的 UDP 或 TCP 数据包上(TCP 是高频交易应用程序最常用的协议,但也可能发送 UDP 数据包,具体取决于应用程序的设计方式)。
理解为什么内核绕过是替代方案
消除传统套接字编程中引发延迟的替代方案,这对于高频交易来说并不合适,有两个方面:在用户空间中自旋一个 CPU 内核和输入输出数据的零拷贝。这两种方法都需要特殊的网络接口卡和相应的应用程序接口(API),支持这些特性,例如 Solarflare 网卡和 OpenOnload/TCPDirect/ef_vi API 支持内核绕过,Mellanox 网卡和 Mellanox 消息加速器(VMA) API,以及 Chelsio 适配器和 WireDirect/TCP 卸载引擎(TOE) API。让我们在以下部分更详细地了解它们。
用户空间旋转
轮询用户空间中启用了内核旁路的 UDP 和/或 TCP 套接字,而不是阻塞和上下文切换设计。这需要不断轮询和利用 CPU 内核。好消息是,轮询纯粹发生在用户空间,即没有系统调用或内核时间,而且现代 HFT 交易服务器中 CPU 内核很充足,所以这是一个很好的权衡。在这种设计中,NIC 缓冲区镜像到用户空间并不断轮询以获取新的数据包。
零拷贝
用户空间旋转之后,优化的第二部分消除了从 NIC 内核空间缓冲区复制到进程用户空间缓冲区的需求。这也是 NIC 的一部分,在数据包到达时(或数据包被发送出去时),NIC 缓冲区被直接转发/复制到用户空间;没有额外的复制步骤。这种缺乏复制被称为内核旁路术语中的零拷贝。
呈现内核旁路延迟
UDP 读取/写入时间在没有内核旁路的情况下介于 1.5 到 10 微秒之间,使用内核旁路的情况下则为 0.5 到 2 微秒。TCP 读取/写入时间的性能提升类似,只是略慢一点。峰值延迟在 UDP 和 TCP 读取/写入方面的性能提升更好。在数百万次 UDP 读取和数千次 TCP 读取/写入过程中,性能的提升会产生巨大的差异。
在此部分,我们介绍了内核旁路技术,将 NIC 读/写转移到用户空间。我们还讨论了高级内核旁路技术,并提供了可实现延迟减少的经验证据。尽管微秒级延迟减少看起来很小,但对于高频交易应用来说,这种差异已足够产生显著影响。我们将在第 11 章 "高频 FPGA 和密码" 的"使用 FPGA 降低延迟"部分更深入地探讨这一点,届时我们将了解纳秒级性能。在下一节中,我们将更详细地探讨使用内存映射文件,这允许我们消除/减少系统调用,并提高高频交易应用的性能。
学习内存映射文件
在这个部分中,我们将讨论内存映射文件,这是一个优雅的抽象概念,大多数现代操作系统提供,在易用性、线程或进程间的共享以及与普通文件相比的性能方面有一些优势。由于其优越的性能,它们被应用于高频交易生态系统中,我们将在"内存映射文件的应用"部分进行讨论,在此之前我们会先探讨它们的概念及其优缺点。
什么是内存映射文件?
内存映射文件是磁盘上文件的一个镜像(或全部),保存在虚拟内存中。它在虚拟内存中与磁盘上的文件、设备、共享内存或任何可通过 UNIX/Linux 操作系统中的文件描述符引用的内容有一对一的字节映射关系。由于文件和关联内存空间之间的映射,应用程序由多个线程/进程组成时可以通过直接读/修改映射到文件的内存来读/修改文件。在幕后,操作系统负责将内存中的更改提交到磁盘上的文件。它还会在磁盘上的文件发生变化时更新内存映射,以及其他任务。应用程序本身不需要管理这些任务。
在 C 语言中,内存映射文件是使用 mmap() 系统调用创建的,它允许我们通过读写内存地址来读写磁盘上的文件。这里支持的两种主要模式是私有进程模式(MAP_PRIVATE 属性)和共享进程模式(MAP_SHARED 属性)。在私有模式下,对内存映射所做的更改不会写入磁盘,但在共享模式下,对内存映射所做的更改最终会提交到磁盘(不是即时的,因为那样效率就和直接读/写磁盘上的文件一样低)。
内存映射文件的类型
存在两种类型的内存映射文件:
让我们在下面的章节中详细地看看这些。
持久化内存映射文件
持久化映射内存文件应被视为存在/将存在于磁盘上的内存映射。当应用程序完成对文件内存映射的处理时,则将更改提交到磁盘上的实际文件-我们一直在讨论的这一功能。这是一种便捷高效的处理大文件或需要将某些进程结束/日终数据保存到文件的方法。
非持久性内存映射文件
非持久性内存映射文件更像是只存在于内存中的临时文件,而不与磁盘上的实际文件关联。因此,它们根本不是文件,它们只是看起来像内存映射文件的内存块,它们主要用于临时数据存储以及使用共享内存在进程之间进行进程间通信(IPC)。在内存映射文件只是两个或多个进程通信的协议,而数据不需要保存/持久化的情况下,会使用这个选项。
内存映射文件的优点
让我们看看内存映射文件的一些优势 - 其中大部分与性能和访问延迟有关,这对高频交易应用程序非常重要。
改善 I/O 性能是首要收益。访问内存映射文件比对磁盘上的
read
/
modify
read
/
modify
read//modify \mathrm{read} / \mathrm{modify} 系统调用快得多,远远超过主内存中的操作,正如我们在上一章中所看到的预取和预分配内存、内存层次结构以及内存访问低效性部分所描述的。此外,由于操作系统负责重新加载/写入磁盘上的文件,它可以在系统不太忙于其他任务时高效和优化地执行该操作。
理解随机访问和延迟加载
访问磁盘上大型文件中的特定位置很慢,因为需要执行定位操作才能找到正确的读取/写入位置。但是,使用内存映射文件的话,就会快得多,因为应用程序可以直接读写文件中的内存数据。更新也是原地进行的,不需要额外的临时副本。在内存中定位位置很快,因为当页边界被跨越时,整个下一页都被调入内存(这很慢),但之后对该页的内存操作都非常高效。
内存映射文件的另一个好处是惰性加载,少量的 RAM 就可以支持大文件。这是通过在访问/修改数据时将小的页面大小的部分加载到内存中实现的。这避免了将大文件全部加载到内存中,从而引起缓存未命中和页面错误等性能问题。
优化的操作系统管理的页面文件管理
现代操作系统在内存映射和分页进程方面非常高效,因为这也是虚拟内存管理的关键系统。出于这个原因,操作系统可以非常有效地管理内存映射过程,选择最佳页面大小(内存块/块大小)等。
并行访问
内存映射区域允许从多个线程和/或进程同时读写文件的不同部分。因此,在这种情况下可以进行并行访问。
内存映射文件的缺点
我们在上一章"理解上下文切换-上下文切换中的耗时任务"部分中学习了缓存命中的概念。缓存命中丢失基本上就是当运行进程需要的代码/数据不在缓存银行中时,需要从主内存中获取。页面错误是一个类似的概念,只不过这里操作系统需要从磁盘上获取数据,而不是从主内存中获取。页面错误是与内存映射文件相关的最大问题。这种情况经常发生在内存映射文件没有顺序访问的情况下。页面错误会让线程等待直到 I/O 操作完成,从而减慢运行速度。如果地址空间可用性是个问题(例如在 32 位操作系统中),那么太多或过大的内存映射文件会导致操作系统耗尽地址空间,使页面错误情况更加恶化。由于前面描述的额外操作和地址空间开销,有时标准文件 I/O 性能可能优于内存映射文件 I/O。
内存映射文件的应用
使用内存映射文件最著名的应用是进程加载器,它使用内存映射文件将可执行代码、模块、数据和其他事物带入内存。
另一个著名的内存映射文件应用是进程间共享内存:IPC,我们在"非持久性内存映射文件"部分讨论过。内存映射文件是最受欢迎的 IPC 机制之一,用于在进程之间共享内存/数据。这在 HFT 应用程序中得到了广泛应用,通常与我们在上一章"构建无锁数据结构"部分讨论的无锁队列相结合使用。该设计用于建立高吞吐量和低延迟数据共享的通信通道。这通常是非持久性的内存映射文件选项。在 HFT 应用程序中,内存映射文件也用于使用持久性内存映射文件选项在运行之间保持信息。本节介绍了内存映射文件的概念、优势和应用,这些技术在高效和高性能的 HFT 生态系统中得到广泛应用。在下一节中,我们将从讨论同一服务器/数据中心上的进程之间的低延迟通信选项转移到不同位置的服务器之间的网络通信。
使用光纤电缆、空心纤维和微波技术
高频交易中的另一个关键(但极其昂贵)竞争领域是在不同地理位置设置数据中心之间的连接性,例如芝加哥、纽约、伦敦、法兰克福等。让我们来看看实现这种连接性的选项:
电缆光纤是标准选择 - 它们带宽高且丢包率极低,但比其他一些选项速度慢且成本更高。
中空光纤是一种现代技术,它是对实心电缆光纤的改进,可以提供更低的信号/数据在数据中心之间传播的延迟。
微波炉是另一个选择,但它通常用于非常具体的目的。它的带宽极低,在某些天气条件下和由于其他微波传输的干扰而遭受数据包损失。然而,微波是传输信息最快的方式,并且比其他两种选择更便宜。
现在,我们已经介绍了将要讨论的不同网络技术,在下一节中,让我们来看看这些技术的发展。
从电缆光纤到空心光纤再到微波
快速讨论电缆光纤向空心光纤和微波的演化是很重要的。高频交易(HFT)追求超低延迟推动了从电缆光纤到空心光纤再到微波技术的演化,因为 HFT 将随着技术进步而持续发展。空心光纤是光纤电缆演化的下一步。空心光纤由玻璃制成,携带编码传输数据的光束。但它们不像常规电缆光纤那样实心:它们是空心的(因此得名),并有平行的充满空气的通道(稍后我们会看到原因)。
微波炉是一种老旧技术,但它存在带宽不足和在雨天/恶劣天气下易丢失数据的问题。微波技术因可靠性和大多数应用程序拥有大量带宽而被放弃,取而代之的是纤维光缆。
然而,随着 HFT 的兴起,许多参与者意识到,通过使用微波网络比实体电缆快几毫秒或微秒来传输数据,可以获得数十亿美元的时延套利策略,尽管它们存在低带宽和更频繁的数据包丢失问题。
最后,高频交易竞争格局的另一个进步是空心光纤,它仍然支持高带宽和低数据包丢失的特性,但比实心光纤电缆稍快一些。几年前,一家名为 Spread Networks 的公司在芝加哥到纽约之间铺设了一条光纤网络线路,该路线的传输延迟为 13 毫秒。几年后,在同一路线上建立了微波网络,将传输延迟降至不到 9 毫秒。
如何中空纤维工作
中空光纤技术试图更好地利用光在空气中比在实心玻璃中传播快 50%的事实。但是,也存在一些设计局限性,因此在实际应用中,通过中空光纤传输数据的时间大约是通过标准光纤传输的
65
%
65
%
65% 65 \% 。如前所述,中空光纤电缆是空心的,而不是实心的,就像标准光纤电缆一样,它们有平行的充满空气的通道,允许光通过空气而不是玻璃传播。
在高频交易中,中空光纤电缆仅用于短距离传输,通常为几百米长度,用于连接数据中心与附近的通信塔,其余部分通过微波方式连接。使用中空光纤电缆可以减少几百到一千纳秒的延迟,虽然提升不算大,但仍足以对纯粹延迟套利交易策略的盈利产生显著影响。
微波炉如何工作
微波传输技术相当古老(可追溯至 90 年代),并由于光纤电缆传输更为合适而被弃用。但是高频交易(HFT)交易者已找到新颖的方法利用地理分布式数据中心之间的微波传输节省微秒和毫秒,从而以极小的时间差超越竞争对手获利。
因为传输速度较快的原因有两个:首先,光在空气中的传播速度比固体玻璃快 50%,其次,使用微波可以直线传输信号,而使用固体光纤电缆,在实际中是不可能做到这一点。使用固体光纤电缆,每次路径转弯或偏离最佳路径都会引入延迟。在实际中,微波网络使用直线视距传输,发送和接收微波天线必须能够相互可见。因此,在长距离情况下,地球曲率会造成需要额外建立几英里间隔的中继塔,而且中继塔需要尽可能高,以减少中继次数。
微波炉的优缺点
迄今为止,根据讨论,微波传输在固态/空心光纤电缆上的优势以及可能存在的一些缺点应该变得很明显了。让我们在本节中正式概括微波和光纤传输的优缺点。
优势
微波传输在高频交易(HFT)中如此有用的最重要优势就是传输速度。这使得 HFT 交易者能比竞争对手提前几微秒或毫秒执行交易并从中获利。基本上,在这个游戏中排在第二位就意味着完全失去市场竞争。
劣势
微波传输的一个缺点是其极其有限的带宽。极低的带宽可用性意味着高频交易(HFT)网络架构和策略需要以这样的方式进行设计,即只通过微波发送最重要/关键的数据,并采用工程技术尽量减小数据包有效负载的大小(我们将很快讨论这个问题)。
微波传输的另一个重要问题是传输链路的可靠性,特别是在任何影响信号质量的情况下,该信号都会变得无法使用。从山脉、摩天大楼、雨水、云层、飞机,甚至同频率运作的其他微波网络,都可能导致信号中断或失真/损坏。这需要额外的工程要求,如更大的碟形天线、碟形天线的疏水涂层、故障转移协议(通常与更可靠的传输方法如光纤电缆相结合)、网络(数据包)和 HFT 应用层的检测机制,等等。
微波影响
根据光速,卡特赖特(新泽西州)和奥罗拉(伊利诺斯州)之间发送信息的理论限制为 3.9 毫秒。该理论限制是根据卡特赖特和奥罗拉之间的最短直线距离和真空中的光速计算出来的。目前,微波服务提供商的最先进技术约为 3.982 毫秒。伦敦和法兰克福之间的高速光纤网络需要约 8.3 毫秒,而微波传输网络则需要更短的时间,约 4.6 毫秒,这意味着使用微波网络的竞争对手总是能战胜那些无法访问微波网络或没有最佳微波网络的高频交易参与者。
我们还没有弄清楚这个领域的未来会是什么样子,但正在努力利用激光束军事技术来进一步降低延迟。这可能会在英国和德国或纽约和纽约周边地区的交易场所之间出现。无论哪种情况,竞争都在进一步加剧,跨托管高频交易延迟套利交易参与者在纳秒的范围内继续战斗。
在下一节中,我们将转移到用于 HFT 系统中日志记录和统计计算的机制和技术。由于 HFT 应用程序在纳秒和微秒性能范围内交易,因此拥有一个极其稳健和高效的日志记录系统非常重要。同样重要的是一个统计计算系统,以深入了解策略/系统行为和性能。
深入研究日志和统计
日志记录(以某种格式和使用某种协议/传输从各种高频交易组件输出信息)和统计数据生成(离线或在线)是高频交易业务中不太引人注目的方面,但它们仍然非常重要。实施不当,它们也可能拖累系统或减少对系统的可见性,因此建立适当的基础设施非常重要。在本节中,我们将从高频交易生态系统的角度讨论日志记录和统计数据生成。
高频交易中日志的需求
大多数软件应用程序的日志记录旨在为用户和/或开发人员提供对行为和性能的洞见,并提醒他们可能需要关注的意外情况。对于高频交易应用程序而言,尤其是在每秒做出数千个复杂决策、多个复杂软件组件相互交互,以及大量资金涉及的情况下,适当的日志记录和日志基础设施都非常重要。高频交易应用程序生成的日志具有不同严重级别 - 关键错误、警告、定期日志、常见性能统计 - 且冗余程度也存在差异。日志类型越不频繁,它们可能越详细 - 但这并非必然要求。
高频交易中在线/实时统计计算的需求
高频交易应用程序每秒执行数千条指令,并做出数千个与处理市场数据、生成交易信号、生成交易决策和生成订单流相关的复杂决策,每秒处理所有这些。此外,高频交易策略通常不是寻求对少量交易工具进行少量交易并赚取大量利润,而是进行大量交易,涉及众多交易工具,但每笔交易收益微小。
给定高频交易策略的行为/性能性质,系统各个组件的摘要统计是评估系统功能的重要方式。这些摘要统计可以应用于软件延迟性能统计和交易信号输出统计(针对个别交易信号以及汇总不同信号和/或交易策略)。此外,还有与订单流和/或执行相关的统计、交易策略性能(盈亏)统计、交易费用统计、仓位规模统计、仓位持续时间统计、被动与主动交易等统计,以及任何能提供洞见的其他统计。这些统计可以连续在线计算或离线计算(交易会话结束时)。
日志和实时统计问题
高频交易应用程序中日志记录和统计计算的基本问题是它们非常慢。日志记录涉及某种程度的磁盘 I/O,正如我们在内存层次结构部分所见,这是最慢的操作。
离线/在线统计计算可能会很昂贵,因为这些计算本身就可能很复杂/昂贵。统计计算速度缓慢的另一个原因是它们通常涉及过去观测值的滚动窗口。这些属性使这两个任务都无法在热/关键线程上高效执行。
高频交易记录和统计基础设施设计
让我们讨论一个有效的高频交易系统的日志和统计基础设施的架构/设计。
首先,最好将日志记录和统计计算线程或进程从关键交易线程或进程中移出。然后我们可以通过调整睡眠时间、检查系统使用情况、决定日志记录和统计计算的实时性等因素,来控制这些线程的活跃频率 - 这些因素取决于特定高频交易系统的性质和预期使用情况。
我们将理想地避免使用锁定,而是使用无锁数据结构和非持久化内存映射文件来从关键线程传输数据到日志线程,通过将日志和统计计算线程或进程绑定到自己的一组隔离的 CPU 核心来避免热路径上的上下文切换,并使用持久化内存映射文件和控制磁盘写入时间来减少磁盘 I/O 的时间。
这是为 HFT 应用程序优化的日志记录和统计计算框架的总体架构。我们在上一章的无锁数据结构应用部分中看到了它的一些部分。然而,我们在实践中也看到了一些替代的设计选择。我们可以使用不同的接口,如 SQL 数据库,而不是平面文件,特别是在记录统计计算的结构化数据集时。我们还看到使用基于 UDP 和 TCP 的可靠组播发布方式的日志记录设置来通过网络发送日志记录,其动机是将它们放在一个单一的集中位置,发布到交易/监控 GUI 等。我们不为这个网络流量使用内核旁路,因为它不太敏感于延迟,总的来说,这不是我们见过的最流行的设计。
无高频交易优化的完整讨论,都不能缺少对性能测量的探讨。由于高频交易应用程序具有超低延迟特性,性能测量基础设施通常是在早期就构建起来的,并在整个高频交易系统的演变过程中一直保持。在本节中,我们将更详细地探讨为什么性能测量如此关键,以及衡量高频交易系统性能的工具和技术,以及我们可以从测量结果中获得哪些见解。
高频交易应用程序极度依赖于超低平均延迟性能和低延迟方差,因此定期测量其各个组件的性能是一项特别重要的任务。随着高频交易系统各个组件的变更和改进,可能会出现意外的延迟,因此没有一个健壮和详细的性能管理系统,这种有害的变更就可能未被发现。
高频交易(HFT)应用程序性能度量的另一个细微差别是,HFT 应用程序的各个组件本身在纳秒和微秒的时间范围内运行。其含义是,性能度量系统本身必须确保引入极少的额外延迟。这对确保调用性能度量系统本身不会改变性能至关重要。
由于这些原因,高频交易的性能测量工具和基础设施具有以下特点:
他们在测量中非常精确。
他们自己的营运成本极低。
他们经常为目标架构调用特殊的 CPU 指令,以提高效率。
他们有时会采用一些非平凡的方法来衡量性能,例如镜像网络流量并捕获它,在出站流量中插入字段以与入站流量相关联,以及在网卡和交换机上使用硬件时间戳。
在处理 HFT 应用程序(或任何应用程序)性能优化时,一个重要的原则是 80/20 法则,即程序在 80%的运行时间里只使用 20%的代码。这种启发式方法意味着某些代码块/路径很少被执行,因此不应该成为优化的目标(除非它们极度低效/缓慢),而某些代码块/路径被频繁执行,这些热点路径应该是优化工作的主要目标。找到这些热点路径/关键代码段的关键在于准确、高效和定期地测量性能。在下一节中,我们将介绍用于测量和分析基于 Linux 的应用程序性能的可用工具。我们将限定在 Linux 上可用的工具,因为它是部署/运行 HFT 应用程序的最常见平台。
在本节中,我们将看看 Linux 上可用的一些工具/命令,可以用来衡量 HFT 应用程序的性能。它们在各种方面都有很大差异:
易用性
准确性和精确性
测量的粒度(即测量整体应用程序性能、应用程序中的方法、代码行数、指令等)
测量过程中引入的应用程序开销,通过该过程跟踪资源利用率 - 缓存、CPU、缓存、栈内存、堆内存等
熟悉这些工具和命令很重要,因为应用程序性能测量是高频交易系统维护和改进的关键部分。接下来将介绍这些工具和命令。
Linux - 时间
GNU 调试器 - gdb
这是 GNU 调试器 (GDB)。虽然这不是一个传统的分析工具,但让应用程序运行并随机中断可用于查看应用程序花费大部分时间的位置。在特定代码部分中断的概率是该代码区域总时间的一小部分。因此,执行这些步骤(在 GDB 中随机中断)几次是一个很好的起点。您可以查看此链接以供参考: https://www . sourceware.org/gdb/ .
GNU Profiler - gprof
GNU 分析器(gprof)使用由编译器和运行时采样插入应用程序的仪器。用于测量的目的在函数调用周围添加额外代码的仪器(Instrumentation)用于收集函数调用信息,而对测量的采样用于在运行时收集分析信息。程序计数器(PC)通过中断程序的中断来定期检查,以检查自上次探测 PC 以来的时间。这个工具输出应用程序花费时间的位置,以及在执行过程中调用哪些其他函数的函数。它类似于 callgrind(我们将很快讨论),但不同的是,与 callgrind 不同,gprof 不会模拟运行。有工具可以可视化 gprof 的输出,如 VCG 工具和 KProf。您可以在此处访问 gprof: https://ftp.gnu.org/old-gnu/Manuals/gprof-2.9.1/ html_mono/gprof.html。
用于跟踪 Linux 内核和应用程序以获取有关在应用程序运行时调用哪些内核调用和应用程序方法的信息,以了解系统、库和应用程序性能的 Linux 跟踪工具包:下一代(LTTng)。您可以访问它:https://lttng.org/
内存分析工具, 性能分析工具, 性能分析工具
瓦尔格林套件及其一系列工具是一套全面的工具集,支持以下功能:
调试
分析
检测内存管理和线程问题
分析缓存和分支预测性能(cachegrind)
收集调用图和一系列指令的数据,并与源代码、函数调用者和被调用者、调用频率等进行相关分析(callgrind)
使用 Massif 分析堆栈使用情况,以尝试减少应用程序的内存使用/占用
它是一个用于调试和分析应用程序的仪器化框架。它充当虚拟机,不直接运行编译后的机器代码,而是模拟应用程序的执行。它还有许多可视化工具,用于分析 Valgrind 工具套件的输出(KCachegrind 就是一个非常好的可视化工具示例)。您可以在这里访问 Valgrind: https://valgrind.org/ 。
这是谷歌的另一组工具,可帮助分析和改进性能,也可以在多线程应用程序上工作。包括 CPU 分析器、内存泄漏检测器和堆分析器。你可以在这里访问它: https://github . com/gperftools/gperftools。
在本节中,我们研究了测量高频交易应用程序性能的现成解决方案。接下来,我们将探索定制技术来仪表高频交易代码并测量性能。这涉及添加/启用额外的架构/操作系统/内核参数,并在应用程序中添加额外的代码。
我们已经看到一些 Linux 工具可以帮助我们分析大多数应用程序。在 HFT 应用程序的关键部分添加自定义检测代码是很常见的。我们已经讨论了日志和统计计算,并且提到了 HFT 应用程序中不同组件/代码路径的延迟性能也是该应用的另一个用途。此外,被提供给日志/统计基础设施的 HFT 应用程序内部数据通常来自自定义时间戳/性能测量代码。
在本节中,让我们讨论一些额外的技术来测量高频交易应用程序的性能 - 如何使性能测量设置在运行之间尽可能一致,C++专用的检测库/函数,最后是 Tick-ToTrade(TTT),这是一种标准和重要的方式,可以以所需的粒度来测量高频交易系统的端到端性能。
在基准测试中获得一致的结果
在基于反复试验和精确测量的过程中,高频交易应用程序的性能测量需要精确和可重复,也就是说,实验过程本身不应该引入太多噪音/方差。现代 CPU、架构和操作系统功能旨在提高高需求下的性能,但它们引入了非确定性和更高的性能延迟方差。非确定性性能是指在相似的输入数据和代码路径下,由于应用程序开发人员无法控制的因素(如缓存中的数据、内存和指令集),会触发略有不同的性能。为了对高频交易应用程序的性能进行基准测试,我们需要采取措施尽可能减少这些功能引入的方差(通常通过关闭这些功能来实现)。总之,在进行基准测试实验时,我们禁用可能导致非确定性性能的潜在源。接下来将讨论一些主要的可能导致非确定性的功能。
英特尔睿频加速
涡轮增压是 Intel 处理器和架构特有的功能,当 CPU 负载很重时会提高 CPU 频率。虽然这对大多数应用程序来说是一个很好的功能,但在分析/基准测试极低延迟的高频交易应用程序时,它会在应用程序控制范围之外的各种时间点打开和关闭,因此最好禁用它。这可以通过基本输入/输出系统(BIOS)来实现,BIOS 基本上用于在启动时控制硬件参数。
超线程
超线程允许现代 CPU 内核在单个物理内核中同时执行两个线程。另一个特性是,对于大多数应用程序来说都很有意义,但对于基准测试 HFT 应用程序性能时除外。这里,一些架构资源(如 ALU、高速缓存等)并未完全复制。这意味着如果线程随机调度并偷取被测进程的资源,可能会观察到非确定性行为。这是另一个现代特性,在基准测试 HFT 应用程序时需要禁用,这是 BIOS 中的另一个配置选项。
中央处理器节能选项
当启用节能选项时,内核/操作系统可以决定是否更好地节省电力和节流。建议禁用此功能,以避免不可预测的次要 CPU 时钟降频导致性能降低(以及性能测量)。
CPU 隔离和亲和性
我们在前一章节中的"将线程钉到 CPU 核心"部分提到过这个问题,这是为了确保关键线程被钉定/绑定到特定的 CPU 核心,非关键线程没有机会中断这些线程并引起上下文切换。这会显著提高性能数据的确定性和降低方差。
Linux 进程优先级
地址空间布局随机化
地址空间布局随机化(ASLR)是一种安全技术,用于防止基于不同区域(代码、静态数据、常量数据、堆栈等)内存位置保持不变的漏洞利用。这种攻击的一个简单示例是,如果内存位置在应用程序执行过程中保持不变,则恶意病毒可能会窃取或破坏写入该内存位置的数据。ASLR 采用的简单解决方案是随机安排进程关键数据区域的地址空间位置。但这会引入不确定性和性能数据的差异,因此对于基准测试 HFT 应用程序的目的,需要禁用此安全功能。
测量数据统计
选择正确的统计数据来评估性能数据也是一个关键组成部分。这可能取决于许多因素,但主要因素是优化过程的目标:我们是要降低平均延迟,还是要降低所发生的最大或最小延迟,或者介于两者之间(如中位数、top 90%延迟等平均值或百分位数)?根据这些因素,我们可能需要计算并比较各种可用的统计度量,如平均值、中位数、方差、四分位间距、最小值、最大值、分布偏度等。
在下一节中,我们将讨论适用于开发 C++应用程序的 Linux 环境的一些其他性能测量技术,这是高频交易的最佳语言和平台选择。
C++/Linux 专用测量例程/库
获取系统时间
这已经在 C 语言中被使用了很长时间。它返回自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的时间(通常称为 Epoch 时间)。它返回秒和微秒,但不返回纳秒。这不再是现代高频交易应用程序 C/C++ 中的首选时间戳机制,因为这种方法需要调用系统调用,并且开销比更现代的时间戳机制大。
时间戳计数器 (TSC) 使用 rdtsc
这是一种高分辨率且低开销的获取 CPU 时间信息的方法,但在多核、多 CPU 和超线程处理器架构中已不再准确/使用。我们将看到的 chrono 库可以克服这里提到的限制。rdtsc()是一条 CPU 指令,它读取时间戳计数器(TSC)寄存器并返回自复位以来经过的 CPU 周期数。它不能直接用于提取当前时间,但可用于计算两次调用 rdtsc()之间经过的 CPU 周期数,然后利用 CPU 频率计算两次调用之间经过的微秒数。
时间
这是目前在 C++中使用的标准库,易于使用和移植,可访问多种时钟和分辨率,需要 C++11 或更高版本。std::chrono::high_resolution_clock 来自于< chrono> 头文件(位于 chrono 库内),包含一个名为 now()的方法,可使用不同的时钟分辨率获取当前时间,其中 high_resolution_clock 提供最高分辨率,非常适合用于测量高频交易应用性能。
端到端测量 - 从打到交易(TTT)
我们已经看到了许多性能测量方法,其中 HFT 应用程序在基准实验室和/或模拟环境中被剖析。但是,最终影响性能测量的关键在于应用程序在实际生产环境中的运行情况。我们使用前一节中提到的技术,结合无锁数据结构、内存映射文件,以及日志和统计信息部分的讨论,来构建端到端延迟测量系统。
我们测量系统关键路径上各个组件(跳跃)的延迟时间,从市场数据更新离开交易所基础设施开始,到达参与者的交易服务器网卡,被市场数据馈送处理程序处理,传输到交易策略,在交易策略内部的各个子组件(账簿构建、交易信号更新、执行逻辑、订单管理、风险检查等)中进行处理,然后通过订单网关发送,最终通过网卡发送至交易所。这被称为从触发到交易(Tick-To-Trade,TTT),其中触发是指进入的市场数据更新,交易是指发送至交易所的出库订单请求。
这里有一个图表,显示了一个 TTT 测量系统的例子。这假设所有的交易决策都是基于市场数据更新做出的,这并不一定是真的,但在这里被假设是为了简单起见。在关键路径上的各个跃点采集的时间戳差值可以用来推算系统各个组件的延迟。
图 7.2 - 从交易所到参与者再返回到交易所的往返路径上的跳跃。下表更详细地描述了往返路径上的不同时间戳。这概述了当单个市场数据更新从交易所生成并传递给市场参与者并被处理时的不同跳跃。在从参与者到交易所的路径上,它描述了对市场更新做出反应而发送的订单的不同跳跃。
时间戳
描述
t
1
t
1
t_(1) t_{1}
交易所从其基础设施发送市场更新时的发送时间 - 基本上就是当它将其放在网线上时。通常,这个时间戳可以作为市场数据流的一个字段获得。
Sending time when the exchange sends the market update out from its
infrastructure - basically, when it puts it on the wire. Often, this timestamp is
available as a field on the market data stream. | Sending time when the exchange sends the market update out from its |
| :--- |
| infrastructure - basically, when it puts it on the wire. Often, this timestamp is |
| available as a field on the market data stream. |
t
2
t
2
t_(2) t_{2}
参与者交易服务器上的网卡收到市场数据包的接收时间。
Receiving time when the market data packet hits the NIC on the participant'ts trading
server. | Receiving time when the market data packet hits the NIC on the participant'ts trading |
| :--- |
| server. |
t
3
t
3
t_(3) t_{3}
这是市场数据包被读取并传递到市场数据馈送处理程序的时候。
This is when the market data packet is read and delivered to the market data feed handler. | This is when the market data packet is read and delivered to the market data feed handler. |
| :--- |
t
4
t
4
t_(4) t_{4}
这个时间戳是在市场数据反馈处理程序处理了市场数据更新并为交易策略生成了更新后记录的。这个时间点是更新从反馈处理程序写入到共享内存(SHM)/内存映射文件供交易策略进程使用的时间。
This timestamp is taken after the market data feed handler has processed the market data
update and generated an update for the trading strategy to consume. This time is when
the update is written to the shared memory (SHM)/memory-mapped file from the feed
handler to the trading strategy process. | This timestamp is taken after the market data feed handler has processed the market data |
| :--- |
| update and generated an update for the trading strategy to consume. This time is when |
| the update is written to the shared memory (SHM)/memory-mapped file from the feed |
| handler to the trading strategy process. |
t
5
t
5
t_(5) t_{5}
这是交易策略从源处理器接收到市场数据更新的时候。
This is when the trading strategy receives the market data update over SHM from the
feed handler. | This is when the trading strategy receives the market data update over SHM from the |
| :--- |
| feed handler. |
t
7
t
7
t_(7) t_{7}
此时间戳在交易策略处理市场更新、产生交易信号、检查市场状况并决定发送订单请求后采集。这是交易策略将订单请求写入交易策略到订单网关的共享内存的时间戳。
This timestamp is taken after the trading strategy processes the market update, produces
trading signals, checks market conditions, and decides to send an order request out. This is
the timestamp of the strategy writing the order request to the SHM from trading strategy
to order gateway. | This timestamp is taken after the trading strategy processes the market update, produces |
| :--- |
| trading signals, checks market conditions, and decides to send an order request out. This is |
| the timestamp of the strategy writing the order request to the SHM from trading strategy |
| to order gateway. |
t
8
t
8
t_(8) t_{8}
这是当订单网关从交易策略通过 SHM 接收订单请求时的情况。
This is when the order gateway receives the order request from the trading strategy
over SHM. | This is when the order gateway receives the order request from the trading strategy |
| :--- |
| over SHM. |
t
9
t
9
t_(9) t_{9}
这是当订单网关完成处理订单请求并想要向交易所发送消息的时候。这个时间戳是订单网关在网卡上执行网络写入的时间。
This is when the order gateway has finished processing the order request and wants to
send out a message to the exchange. This timestamp is when the order gateway invokes
a network write on the NIC. | This is when the order gateway has finished processing the order request and wants to |
| :--- |
| send out a message to the exchange. This timestamp is when the order gateway invokes |
| a network write on the NIC. |
t
10
t
10
t_(10) t_{10}
这个时间戳对应于订单请求的 TCP 数据包被放在线缆上的时间。
This timestamp corresponds to when the TCP packet for the order request is put on
the wire. | This timestamp corresponds to when the TCP packet for the order request is put on |
| :--- |
| the wire. |
此时间戳对应于交易所在其基础设施中接收订单请求。此时间戳通常也可在交易所发回的 TCP 订单响应中获得,有时也作为与此订单请求相对应的市场更新生成的字段。
This timestamp corresponds to the exchange receiving the order request in its
infrastructure. This timestamp is often also available on the TCP order response sent
back by the exchange and sometimes also as a field on the market update generated
corresponding to this order request. | This timestamp corresponds to the exchange receiving the order request in its |
| :--- |
| infrastructure. This timestamp is often also available on the TCP order response sent |
| back by the exchange and sometimes also as a field on the market update generated |
| corresponding to this order request. |
Timestamp Description
t_(1) "Sending time when the exchange sends the market update out from its
infrastructure - basically, when it puts it on the wire. Often, this timestamp is
available as a field on the market data stream."
t_(2) "Receiving time when the market data packet hits the NIC on the participant'ts trading
server."
t_(3) "This is when the market data packet is read and delivered to the market data feed handler."
t_(4) "This timestamp is taken after the market data feed handler has processed the market data
update and generated an update for the trading strategy to consume. This time is when
the update is written to the shared memory (SHM)/memory-mapped file from the feed
handler to the trading strategy process."
t_(5) "This is when the trading strategy receives the market data update over SHM from the
feed handler."
t_(7) "This timestamp is taken after the trading strategy processes the market update, produces
trading signals, checks market conditions, and decides to send an order request out. This is
the timestamp of the strategy writing the order request to the SHM from trading strategy
to order gateway."
t_(8) "This is when the order gateway receives the order request from the trading strategy
over SHM."
t_(9) "This is when the order gateway has finished processing the order request and wants to
send out a message to the exchange. This timestamp is when the order gateway invokes
a network write on the NIC."
t_(10) "This timestamp corresponds to when the TCP packet for the order request is put on
the wire."
"This timestamp corresponds to the exchange receiving the order request in its
infrastructure. This timestamp is often also available on the TCP order response sent
back by the exchange and sometimes also as a field on the market update generated
corresponding to this order request." | Timestamp | Description |
| :---: | :--- |
| $t_{1}$ | Sending time when the exchange sends the market update out from its <br> infrastructure - basically, when it puts it on the wire. Often, this timestamp is <br> available as a field on the market data stream. |
| $t_{2}$ | Receiving time when the market data packet hits the NIC on the participant'ts trading <br> server. |
| $t_{3}$ | This is when the market data packet is read and delivered to the market data feed handler. |
| $t_{4}$ | This timestamp is taken after the market data feed handler has processed the market data <br> update and generated an update for the trading strategy to consume. This time is when <br> the update is written to the shared memory (SHM)/memory-mapped file from the feed <br> handler to the trading strategy process. |
| $t_{5}$ | This is when the trading strategy receives the market data update over SHM from the <br> feed handler. |
| $t_{7}$ | This timestamp is taken after the trading strategy processes the market update, produces <br> trading signals, checks market conditions, and decides to send an order request out. This is <br> the timestamp of the strategy writing the order request to the SHM from trading strategy <br> to order gateway. |
| $t_{8}$ | This is when the order gateway receives the order request from the trading strategy <br> over SHM. |
| $t_{9}$ | This is when the order gateway has finished processing the order request and wants to <br> send out a message to the exchange. This timestamp is when the order gateway invokes <br> a network write on the NIC. |
| $t_{10}$ | This timestamp corresponds to when the TCP packet for the order request is put on <br> the wire. |
| This timestamp corresponds to the exchange receiving the order request in its <br> infrastructure. This timestamp is often also available on the TCP order response sent <br> back by the exchange and sometimes also as a field on the market update generated <br> corresponding to this order request. | |
图 7.3 - 在交易所和市场参与者之间的往返路径上的不同跳数处捕获的时间戳的详细信息 该部分描述了高频交易生态系统中典型的端到端测量系统。我们还对市场参与者与交易所之间跳跃点采集的不同时间戳进行了详细调查。
摘要
我们讨论了各种计算机科学构造的实施细节,如内存访问机制、应用层网络流量访问、磁盘 I/O、网络传输方法以及性能测量工具和技术。
我们还讨论了这些功能对高频交易应用程序的影响,发现对大多数应用程序来说最佳的默认行为并不是高频交易应用程序的最佳设置。
最后,我们讨论了最佳 HFT 生态系统性能的方法、工具、技术和优化。我们希望本章为先进的 HFT 优化技术及其对 HFT 生态系统性能的影响提供了见解。
您应该对 HFT 生态系统中所有重要的优化注意事项有一个良好的了解。我们还详细讨论了您可以用来分析 HFT 系统性能并保持和提高性能的不同性能测量和优化工具和技术。
在下一个章节中,我们将深入探讨现代 C++编程语言的细节,特别是以构建超低延迟的 HFT 系统为目标,利用现代 C++提供的所有强大功能。
第 3 章:高频交易系统的实施
这部分将为您提供一个亲身体验,通过为您提供实施高频交易(HFT)系统的指南。我们将从 HFT 中最常用的语言 C++开始我们的旅程。然后,我们将继续探讨 Java 及其虚拟机。我们将解释 Python 如何使用 HFT 库。我们将通过描述可编程门阵列(FPGA)如何降低交易延迟,以及探讨加密货币中的 HFT 来结束本书。
这部分包含以下章节:
第 8 章,C++——追求微秒延迟
第 9 章、低延迟系统的 Java 和 JVM
第 10 章,Python - 解释性但开放于高性能
第 11 章,高频 FPGA 和加密
8
C++ - 追求微秒级延迟
在本章中,我们讨论了
C
+
+
C
+
+
C++ \mathrm{C}++ 中可用的一些特性和构造。在我们开始之前,有一个免责声明,即涵盖现代 C++(C++ 11/14/17)提供的大部分内容超出了单章(通常也超出了单本书)的范围,所以我们将集中讨论一些对开发、维护和改进多线程和超低运行时延迟 HFT 应用程序很重要的方面。在开始钻研本章之前,我们建议您精通
C
+
+
C
+
+
C++ \mathrm{C}++ 。我们建议一些书籍来实现这一目的,如 Bjarne Stroustrup 编写的《编程:使用 C++的原理和实践》。
我们将首先探讨现代 C++内存模型,它指定了在多线程环境中共享内存交互的工作方式,然后探讨静态分析,这是应用程序开发、测试和维护的一个重要方面。然后我们将深入探讨如何优化应用程序的运行时性能,最后将专门为顶级 HFT 生态系统中非常重要的模板划分一整个部分。
在本章中,我们将涵盖以下主题:
C++内存模型
删除运行时决策
动态内存分配
模板用于减少运行时间
静态分析
在本章结束时,您将能够优化您的 C++代码以用于高频交易系统。最后,我们将回顾一个行业案例。我们将讨论我们用来建立外汇(FX)高频对冲基金的技术。
重要说明 为了指导您完成所有优化,您可以参考以下图标列表,这些图标代表了一组可降低延迟特定微秒数的优化:
=
=
= = 低于 20 微秒
L
L
_(L) \underset{\mathbf{L}}{ } :低于 5 微秒 低于 500 纳秒 您将在本章标题中找到这些图标。
C++ 内存模型
C++ 14/17 内存模型
在本节中,我们将探讨现代 C++ (11、14 和 17) 的内存模型的定义和规范。我们将探讨它是什么、为什么需要它用于多线程应用程序以及 C++ 内存模型的重要原则。
什么是内存模型?
内存模型,也称内存一致性模型,指定了与共享内存交互的多线程应用程序允许和预期的行为。内存模型是共享内存系统并发语义的基础。如果有两个并发程序,一个写入另一个读取共享内存空间,则内存模型定义了读取操作允许返回的值集合。
内存模型(C++或其他)的实现必须受到内存模型规定的规则约束,因为如果无法从读写操作顺序推断结果,就不是一个明确的内存模型。另一种看待内存模型所施加限制的方式是,它们定义了编译器、处理器和体系结构(内存)允许的指令重排序。大部分关于内存模型的研究都试图最大限度地让编译器、处理器和体系结构优化自由发挥。
即使优化变得越来越复杂,它们也必须保持开发者想要完成的语义。它们绝不能违反内存模型的约束。
在这个时候,让我们正式定义几个术语。
源代码顺序
这是程序员在所选编程语言中指定的指令和内存操作的顺序。这是编译器编译代码之前的代码或指令集。
程序订单
这是在 CPU 执行编译后的源代码后的机器代码指令和内存操作的顺序。如前所述,编译器将尝试优化和重新排序指令作为优化过程的一部分,因此这里的指令和/或内存操作顺序可能会有所不同。
执行顺序
这是 CPU 执行的指令和内存引用的实际执行顺序。这与编译程序顺序不同,因为在这个阶段,CPU 和整体架构被允许重新排序编译器生成的机器代码中的指令。这里的优化取决于特定 CPU 和其架构的内存模型。 我们现在将讨论对于内存模型的需求。
需要内存模型
我们需要一个明确定义的内存模型的基本原因在于,我们编写的代码并不完全等同于编译过程后输出的代码,也不等同于在硬件上运行的代码。现代编译器和 CPU 允许指令乱序执行,以最大化性能和资源利用。
在单线程环境中,这并不重要,但在多线程环境中,运行在多核(多处理器)架构上,不同线程试图读取和写入共享内存位置会导致竞争条件,并可能导致指令重排序时出现未定义和意外的行为。正如我们在第 4 章中所见,HFT 系统基础 - 从硬件到操作系统,调度和上下文切换是不确定的。它们可以被控制,但当上下文切换发生在极其特定的位置时,优化可能导致指令以不同的顺序执行和内存访问发生,从而产生不同的结果。
拥有一个内存模型给优化编译器一个很大的自由度来应用优化。内存模型规定了使用同步原语(互斥锁、锁、同步块、屏障等)的同步屏障,我们在前一章 7 章 HFT 优化 - 日志、性能和网络中看到了。当共享变量改变时,在到达同步屏障时,这种改变需要对其他线程可见;也就是说,重排序不能打破这个不变量。我们现在将详细描述 C++内存模型是如何工作的。
C++11 内存模型及其规则
在我们深入探讨 C++11 内存模型的细节之前,让我们回顾一下前面两个章节。内存模型的目的如下:
通过共享内存的线程交互的可能结果。
检查程序是否有明确定义的行为。
为编译器代码生成指定约束。
C++内存模型对内存访问语义提供了最小保证。正如所料,编译器处理和优化(重新排序指令和内存访问)以及执行指令和内存访问的 CPU/架构所能接受的影响存在限制。C++内存模型本身对内存访问语义的保证非常弱,弱于您所期望的,也弱于通常在实践中实现的。实践中的内存模型反映了系统施加的规则,如 x86_64 的 total store ordering(TSO)或 ARM 的宽松排序。
对于 C++内存模型,在将数据从主内存(共享)传输到每个线程的内存时,有三个规则需要遵守。我们将依次讨论这三个规则。我们将在下一节中探讨内存排序以及顺序一致性。
原子性
我们在第 6 章 HFT 优化 - 架构和操作系统中的无锁数据结构部分看到过这个。在处理全局/静态/共享变量/数据结构时,需要明确哪些操作是不可分割的。
让我们介绍一些从 C++ 11 开始可用的构造来支持原子性,可以推广到不同类型的对象(模板),并支持原子加载和存储。我们将快速介绍
C
+
+
11
C
+
+
11
C++11 \mathrm{C}++11 提供的内存排序支持,并将在 C++内存排序原则一节中深入研究。
std::lock_guard
标准::lock_guard 是一个简单的互斥体包装器,它使用资源获取即初始化(RAII)原则来拥有作用域块内的互斥体。它试图在创建时立即获取互斥体的所有权,当创建它的作用域结束时,lock_guard 析构函数被调用,释放互斥体。为了方便起见,
C
+
+
11
C
+
+
11
C++11 \mathrm{C}++11 提供了 std::atomic
<
T
>
<
T
>
< T > <\mathrm{T}> 模板类,以支持类型 T 对象的原子加载和存储。
std::atomic
通用类 std::atomic
加载(std::memory_order order),它以原子方式加载并返回当前值
存储(T 值, std::memory_order 顺序),它会原子性地保存当前值
交换(T 值,std::memory_order 顺序),这与存储做类似的工作,但执行读修改写操作
对于整数和指针类型,它还提供了以下操作:
取回_添加(T arg, std::内存_顺序 顺序)
获取并减去(T arg, std::memory_order order)
对于整型类型,它提供以下额外的逻辑操作:
获取并(T arg, std::memory_order order),这类似于 fetch_add,只是它执行位与操作。
获取或(T arg, std::memory_order order),这就像是 fetch_ and,但它执行按位或运算。
获取_异或(T arg, std::memory_order 顺序), 这就像 fetch_ 一样, 除了它执行按位异或操作。
默认值 std::memory_order 顺序参数为 std::memory_order_seq_cst(顺序一致性)。这里可以指定其他值,而不是顺序一致性,以定义较弱的内存模型。不同选项及其影响将在
C
+
+
C
+
+
C++ C++ 内存模型原理的内存排序部分讨论。
原子操作的属性
原子操作的属性如下:
可以并发地从多个线程中执行操作,而不会出现未定义的行为。
原子加载要么看到变量的初始值,要么看到通过原子存储写入的值。
原子存储对同一对象的所有线程均以相同的顺序进行。
让我们看下一条规则。
能见度
我们在前一节简要讨论了可见性,我们提到当共享数据上发生读写操作时,一个线程对变量的写入效果需要在同步屏障的边界对读取它的线程可见。
让我们讨论有关一个线程对另一个线程所做更改可见性的规则。在以下情况下,一个线程所做的更改对其他线程是可见的:
写线程释放同步锁,然后读线程在此之后获取它。释放该锁将刷新所有写入操作,而获取该锁将加载或重新加载这些值。
对于原子变量,在写入端的下一个内存操作之前,写入的值会立即刷新,但读取端必须在每次访问前调用一个加载指令。
当一个写入线程终止时,所有写入的变量都会被刷新,因此与该线程的终止(join)同步的线程将会看到该线程写入的正确值。
以下是一些与可见性相关需要注意的其他事项:
当代码中长期没有使用同步机制来与其他相关的线程进行协调时,这些线程与共享数据成员的值可能会完全失去同步。
不使用原子性或同步的情况下,等待或检查其他线程写入值的循环是错误的。
在缺乏正确同步的情况下,可见性故障和安全违规并不能得到保证或要求,只是一种可能性。在实践中可能不会发生,只会极其罕见,或仅出现在某些架构或某些特定外部因素上。总的来说,几乎不可能完全确信没有基于可见性的错误。
我们现在将讨论指令顺序。
订购
由于编译器或 CPU 会重新排序内存访问,因此内存模型需要定义赋值操作的效果何时可能出现在指定线程之外的顺序
顺序一致性是一种 C++机器内存模型,要求所有线程的所有指令都像是按照程序或源代码顺序在每个线程上执行的。
内存顺序是我们稍后将探讨的另一个概念。它描述了内存访问指令的顺序。该术语可用于引用编译时或运行时的内存访问排序。内存排序允许编译器和 CPU 重新排序内存操作,因此它们不是按顺序执行的,从而实现了内存层次结构(寄存器、缓存、主内存等)的最佳利用,并最大限度地利用数据传输架构及其带宽。
让我们在以下部分学习 C++ 内存模型和内存顺序概念的原理。
c++内存模型原理
在现代 C++ 内存模型范式中,我们将探讨在多线程和多处理环境中访问和写入共享数据结构的不同选项。
内存顺序概念
内存模型在使用 HFT 中的线程时很重要,因为如果我们没有准确使用该模型,可能会改变软件的语义。我们首先介绍一些符号和概念,然后深入探讨不同的控制选项。
放松的内存顺序:在默认系统中,内存操作的顺序非常宽松,CPU 有很大的自由度来重排这些操作。编译器也可以以任何顺序安排它们输出的指令,只要不影响程序的表面执行。同一线程对同一内存区域执行的内存操作不会根据修改顺序重排。
获取/释放:所有读取存储值的负载获取操作与释放存储操作同步。释放线程中任何在释放存储操作之前发生的活动都会在获取线程中在负载获取之后发生的所有操作之前发生。
消费:消费是 Acquire/Release 的较轻量级变体。如果 X 依赖于加载的值,则在"存储-发布"之前的所有释放线程中的操作都发生在消费线程中的操作 X 之前。
在此部分中,我们探讨了内存排序的一些特性。现在,我们将着重定义
C
+
+
C
+
+
C++ \mathrm{C}++ 中的内存排序。
内存顺序
表 1 提供了对不同内存排序标记的简要介绍,我们将在后续章节中对此进行更详细的探讨。
内存顺序
描述
std::memory_order_relaxed
没有额外的内存排序限制。
内存顺序释放 std::内存顺序获取
std: : memory_order_release
std: : memory_order_acquire | std: : memory_order_release |
| :--- |
| std: : memory_order_acquire |
如果 load-acquire 看到 store-release 存储的值,那么 store-release 之前的存储发生在 load-acquire 之后的装载之前。
If load-acquire sees the value stored by
store-release, then stores before the store-release
happen before loads after the load acquire. | If load-acquire sees the value stored by |
| :--- |
| store-release, then stores before the store-release |
| happen before loads after the load acquire. |
内存顺序消费
依次加载的顺序。
Like memory_order_acquire but only for
dependent loads. | Like memory_order_acquire but only for |
| :--- |
| dependent loads. |
内存顺序_获取释放
结合负载获取和存储释放。
内存顺序_顺序一致性
顺序一致性;全局提供读写排序。
Sequential Consistency; provides read and write
ordering globally. | Sequential Consistency; provides read and write |
| :--- |
| ordering globally. |
std::memory_order Description
std::memory_order_relaxed No additional memory ordering restrictions.
"std: : memory_order_release
std: : memory_order_acquire" "If load-acquire sees the value stored by
store-release, then stores before the store-release
happen before loads after the load acquire."
std::memory_order_consume "Like memory_order_acquire but only for
dependent loads."
std: :memory_order_acq_rel Combines load-acquire and store-release.
std::memory_order_seq_cst "Sequential Consistency; provides read and write
ordering globally." | std::memory_order | Description |
| :--- | :--- |
| std::memory_order_relaxed | No additional memory ordering restrictions. |
| std: : memory_order_release <br> std: : memory_order_acquire | If load-acquire sees the value stored by <br> store-release, then stores before the store-release <br> happen before loads after the load acquire. |
| std::memory_order_consume | Like memory_order_acquire but only for <br> dependent loads. |
| std: :memory_order_acq_rel | Combines load-acquire and store-release. |
| std::memory_order_seq_cst | Sequential Consistency; provides read and write <br> ordering globally. |
图 8.1 - C++内存模型 这些内存顺序标记允许四种不同的内存排序模式:顺序一致性、松弛和释放-获取,以及类似的释放-消耗。让我们接下来探讨这些内容。
顺序一致性
顺序一致性(SC)是一个原则,它规定参与多线程应用程序的所有线程都同意内存操作的发生顺序或将发生的顺序。另一个要求是,该顺序与源程序中操作的顺序一致。在现代 C++中实现这一点的技术是将共享变量声明为带有内存顺序约束的原子类型
C
+
+
11
C
+
+
11
C++11 \mathrm{C}++11 。任何执行的结果都应该与所有处理器的操作按顺序执行的结果相同。
另外,在每个处理器上执行的操作也遵循程序顺序。在多线程和多处理器环境中,SC 意味着所有线程都在内存操作的顺序上达成一致,并且每次运行程序时都保持一致。
一种实现这种方式的方法,例如在 Java 中,是将共享变量声明为 volatile 类型,而等效的做法是将共享变量声明为带有内存顺序约束的原子类型。这样做之后,编译器会在幕后引入额外的指令,如内存屏障,来强制执行这些顺序约束(请参阅本章中的"屏障"部分)。原子操作的默认内存顺序是顺序一致性,即 std::memory_order_seq_cst 操作。
注意
虽然这种模式容易理解,但是它会导致最大的性能损失,因为它阻止了编译器可能尝试重新排序操作超出原子操作的优化。
放松性订单
放松顺序是 SC 的相反,使用 std::memory_order_relaxed 标记激活。这种原子操作模式不会对内存操作施加任何限制。然而,操作本身仍然是原子的。
释放-获取顺序
在释放-获取顺序设计中,原子存储或写入操作(也称为存储-释放),使用 std::memory_order_release,而原子加载或读取操作(也称为加载-获取),使用 std::memory_order_acquire。编译器不允许将存储操作移动到存储-释放操作之后,也不允许将加载操作移动到加载-获取操作之前。当加载-获取操作看到存储-释放操作写入的值时,编译器确保存储-释放操作之前的所有操作都发生在加载-获取操作之后的加载操作之前。
释放-消费顺序
发布-消费顺序就像发布-获取顺序,但在这里,原子加载使用 std::memory_order_consume,并成为原子加载-消费操作。这种模式的行为与发布-获取相同,只是在加载-消费操作之后的加载操作和依赖于加载-消费操作加载的值被正确排序。
我们已经看到,原子对象具有用于原子写入和读取共享数据的存储和加载方法,默认模式是顺序一致性。在底层,编译器会添加附加指令来创建存储器栅栏。我们还讨论了添加大量存储器栅栏会创建效率低下的代码,因为这会阻碍编译器优化。这些栅栏对于发布安全性也并非必要,在这种情况下,问题变成如何编写生成最少栅栏操作(因此产生更高效的代码)的代码。
这里就是编译器在访问内存和共享数据操作方面所知道的内容:
所有线程中的内存操作及其作用,以及任何数据依赖关系
哪些内存位置是共享的,哪些变量是可变变量,即可能由于另一个线程中的内存操作而异步更改。
所以,最小化内存屏障的解决方案是简单地告诉编译器哪些可变和共享位置的操作可以重新排序,哪些不可以。与以前一样,独立的内存操作可以以随机顺序执行,没有任何影响。
栅栏
编程中的栅栏是一种屏障指令。它们强制处理器对内存操作强制执行特定的顺序。这些操作将根据栅栏进行修改。使用栅栏可以在线程之间对内存操作进行排序。栅栏可以是释放或获取。如果获取栅栏位于释放栅栏之前,则存储操作会发生在获取栅栏之后的加载操作之前。我们采用其他同步原语,允许原子操作来确保释放栅栏在获取栅栏之前发生。
原子操作,atomic_thread_fence 操作具有 memory_order 参数,可取以下值:
如果 memory_order 是 memory_order_relaxed,这不会产生任何影响。
如果 memory_order 是 memory_order_acquire 或 memory_order_consume,那么它就是一个 acquire 栅栏。
如果 memory_order 是 memory_order_release,则它是一个释放栅栏。
如果 memory_order 是 memory_order_acq_rel,那么它既是获取栅障又是释放栅障。
如果 memory_order 是 memory_order_seq_cst,则它是一个顺序一致的获取和释放栅栏。
我们审查了栅栏操作的顺序。我们现在将通过讨论
C
+
+
20
C
+
+
20
C++20 \mathrm{C}++20 中的更改来结束本节。
C++20 内存模型更改
在内存模型方面,确实存在一些小的变化。在 C++11 内存模型正式化之后,发现了一些问题。老的模型是为了使不同的内存访问机制可以在通用架构上使用昂贵的硬件指令而定义的。具体来说,memory_order_acquire 和 memory_order_release 原本应该能够在 ARM 和 Power CPU 架构上使用轻量级的栅栏指令来实现。但遗憾的是,事实并非如此,对于 NVIDIA GPU 也同样如此,尽管十年前并没有真正针对它们。
所以从根本上来说,我们只有两个选择:
按原样实施标准。这是可能的,但会遭受性能下降的问题,这与我们最初引入这些内存模型的目的相悖,并将降低
C
+
+
C
+
+
C++ \mathrm{C}++ 的效率。
修复标准以更好地处理新架构,而不破坏内存模型的概念和想法。
方案二作为更合理的选择最终被
C
+
+
C
+
+
C++ \mathrm{C}++ 标准委员会采纳为解决方案
在本节关于内存模型的内容中,我们回顾了不同的模型。由于高频交易(HFT)进程并发运行,了解多线程软件环境下内存模型的工作方式很重要。为了实现 HFT 的高峰性能,我们现在需要学习如何通过消除运行时可能发生的决策来缩短执行时间。这将是我们下一节的主题。
移除运行时决策
作为一种编译语言,C++可以在编译过程中优化源代码,并生成尽可能多的在编译时解决的机器代码。在本节中,我们将探讨消除运行时决策的动机,了解一些在运行时解决的 C++构造,并了解一个超低延迟的 HFT 应用程序如何最小化或替换运行时决策。
动力去除运行时决策
对于高频交易应用程序来说,关键路径上的代码越少,并且越多地在编译时而非运行时得到解决,应用程序性能就越好。这里我们讨论了编译器、CPU 和内存架构在应用程序具有最少的运行时决策以及大部分代码可以在编译时解决的情况下所获得的性能优势。
编译器优化
如果编译器能在编译时解决源代码和常量/静态数据,就可以进行大量的编译时优化。在编译时解决意味着编译器在编译时就知道每个对象类型、每次调用时调用哪个方法/函数/子程序、执行每个方法需要多少内存以及存放位置等。编译时解决允许编译器进行以下诸多优化:
内联:这是编译器用被调用函数的函数体替换函数调用的地方。
废弃代码删除:编译器删除不影响程序结果的代码。
指令重新排序:这使我们能够打破依赖关系并更快地运行代码。
替换编译时宏:这些与内联非常相似,除了技术上来说,宏的使用在编译优化步骤之前的预处理步骤中被替换为宏的实际代码。
这导致生成比编译器无法优化由于编译时解析失败时快得多的机器代码。
CPU 和体系结构优化
编译器生成的机器代码不仅更加优化,而且在 CPU、流水线和体系结构硬件层面的预取和分支预测优化方面也能更好地工作。
由于中央处理器管道,现代处理器预取即将被访问和执行的指令和数据。当数据和指令在编译时已知时,这种方法效果明显更好-想想内联与非内联方法的区别。如果将要访问的对象和/或需要的方法在编译时未知(因为它们是在运行时解析的),这个过程很难正确执行,经常会预取错误的数据和指令从缓存或主内存。
另一个与预取相关的优化是分支预测优化,其中 CPU 尝试预测将要采取的分支(条件开关、函数调用等)。当 C++ 应用程序使用虚函数、运行时类型识别 (RTTI) 等时,这在动态解析的情况下更加困难。这是因为要么无法预测将要采取的分支,因为对象的类型可能不知道,和/或方法主体可能不知道或者大多数时候很难获取正确的分支。当分支预测不正确时,会产生惩罚,因为预取的数据和代码现在需要从 CPU 管线、缓存、内存等驱逐出去,然后需要在调用时获取正确的数据和代码。如果您想了解更多关于分支预测理论的知识,我们建议您阅读《计算机体系结构:定量方法》一书。
虚拟函数
虚函数是 C++特别重要的功能之一-动态多态性的关键。这是一个优秀的功能,可以减少代码重复,为控制和数据流程设计提供语义,并让我们拥有可以被覆盖和定制的通用接口。这是面向对象编程(OOP)设计中的一个重要原则,但遗憾的是它会带来运行时性能损失。由于虚函数的运行时解析和相关的运行时性能损失,HFT 应用程序通常会非常谨慎地使用虚函数,并尽量消除不必要的虚函数。本节将更详细地探讨 C++虚函数性能。
他们是如何工作的
虚拟函数是如何从编译器和操作系统的角度实现的。当编译器编译源代码时,它知道哪些函数是虚拟的及其地址。对于每个有至少一个虚拟函数的创建对象,都会创建一个表(称为虚拟表或 vtable)来保存该类型的虚拟函数指针。有虚拟函数的类型的对象都有一个指向该对象的虚拟表的指针,称为 vptr。当虚拟函数被派生类覆盖时,被覆盖的虚拟函数在 vtable 中的条目会指向派生类实现。在运行时,调用虚拟函数需要比非虚拟函数多几个步骤:运行时访问 vptr,找到该对象类型的 vtable,确定需要调用的函数地址,然后执行虚拟函数调用。
在先前的部分中,我们描述了虚函数是如何设置和调用的。由于运行时需要访问虚函数表,虚函数调用比非虚函数有稍微更多的开销,但是在这一部分中,我们将探讨使用虚函数时所带来的最大性能损失。
编译器优化
一个主要的性能损失原因是虚函数阻碍了编译器优化。总的来说,虚函数的地址取决于对象的类型,这通常在运行时才能确定。这意味着需要调用的虚函数的地址和内容也只能在运行时确定。因此,编译器无法内联该函数。这将节省函数调用和返回的一些指令。此外,内联还将消除未使用的参数和变量,从而消除函数调用前的一些操作。当在循环中调用具有不同虚函数的多个对象时,这种情况会更加严重。在这种情况下,不仅无法内联调用,循环展开也是不可能的,利用硬件进行性能优化也是不可能的。我们将在另一节中讨论这个问题,但这也会影响 CPU 管线和缓存性能。
预取和分支预测
我们之前提到过硬件如何尝试预取可能即将访问和执行的数据和代码。它还会尝试预测可能会执行的分支(也称为投机性执行),并尝试预取可能执行的分支的数据和代码。对于带有虚函数和虚函数调用的对象,在运行时真正确定对象类型和虚函数地址之前,它无法知道跳转目的地。
到这个时候,它已经根据它预测的分支预取了指令,并已经开始执行这些指令了。如果它的预测是正确的,那就很好,但如果不是,那么从预取开始做的所有工作都必须停止和倒转,正确的指令必须在取回完成后执行。
这使得程序在分支预测错误时变慢,不仅要获取正确地址的指令,还必须撤消预取和执行错误指令的影响。此外,虚函数越短,观察到的减速越大,因为分支预测错误的开销占总函数调用时间的比例越大。
我们在第 6 章"高频交易优化 - 体系结构和操作系统"中讨论了存储层次结构中不同缓存层的设计和性能优势。我们还提到,L1 和 L2 缓存有指令缓存,可缓存最近和最常使用的指令。还有另一个缓存,用于保存分支指令的比较结果,它被用来根据同一指令的先前执行情况预测分支目标,从而通过预取指令和推测性执行来加快计算。
缓存性能最佳当所需说明和分支结果在适当的缓存中,但虚拟功能(特别是每个对象类型都有不同实现的大型虚拟函数)在此处是有问题的。如果有一个基类指针的容器,而且每个指针都指向不同的对象类型或随机排列(即容器未按类型排序),这就更糟糕了。这很糟糕,因为大多数对虚拟函数的调用都会导致对潜在随机内存位置中的不同函数的调用。
因此,如果函数足够大,每次调用虚函数都会导致缓存驱逐先前函数调用的数据和指令,并加载新函数的数据和指令。这还要加上频繁支付的分支预测惩罚。虚函数可能会导致大量缓存驱逐和缓存未命中,从而严重影响性能。但并非总是如此,因为如果我们通过虚函数表调用一个函数,并在紧密循环中执行此操作,CPU 将利用分支预测和缓存局部性的力量来隐藏虚函数表访问延迟。始终很重要的是要分析代码中性能问题的发生位置。
图 8.2 更好地描述了这种情况。假设存在以下类结构,其中单个基类具有虚拟函数,该函数被不同的实现所派生,并覆盖虚拟函数。
图 8.2 - 虚函数:具有单个基类和多个派生类的类结构 让我们假设有一个基类指针容器,指向不同位置的派生类实现。如果代码试图循环遍历这个容器并调用虚函数,它将导致大量的缓存驱逐、缓存未命中,以及整体糟糕的运行时性能。这还不包括编译器无法展开循环和分支预测失败的开销。
图 8.3 展示了 vtable 如何通过为不同的虚函数使用不同的内存位置影响性能。
索引
基指针
主存储器 Derived1
Main memory
Derived1 | Main memory |
| :--- |
| Derived1 |
0
基础*
⟶
⟶
longrightarrow \longrightarrow 衍生 3
1
基础*
衍生物 2
...
...
衍生 N-1
...
...
DerivedX
⟶
⟶
longrightarrow \longrightarrow
N-1
基础*
N
基础*
衍生 N
Index Base pointers "Main memory
Derived1"
0 Base* longrightarrow Derived3
1 Base* longrightarrow Derived2
... ... DerivedN-1
... ... DerivedX longrightarrow
N-1 Base*
N Base*
DerivedN | Index | Base pointers | Main memory <br> Derived1 |
| :---: | :---: | :---: |
| 0 | Base* | $\longrightarrow$ Derived3 |
| 1 | Base* | $\longrightarrow$ Derived2 |
| ... | ... | DerivedN-1 |
| ... | ... | DerivedX $\longrightarrow$ |
| N-1 | Base* | |
| N | Base* | |
| | | DerivedN |
图 8.3-指向不同派生对象的基指针容器,这些对象位于随机内存位置
由于我们发现使用虚函数可能会损害性能,我们现在将讨论一种去除它们的方法:奇特反复出现的模板模式(CRTP)。
好奇循环模板模式(CRTP)
我们常常反对使用虚拟函数的 CRTP 方法。我们首先要说,虚拟函数在运行时发现接口实现,而 CRTP 则不是这种情况。CRTP 是静态多态的一个例子,与虚拟函数(动态多态的例子)相对。CRTP 是一种编译时结构,这意味着它没有运行时开销。它与公开接口的基类以及实现该接口的派生类一起使用。正如我们在本节中看到的,通过基类引用调用虚拟函数通常需要通过指向函数的指针进行调用,从而产生间接成本并阻止内联。
总结起来,我们学习了如何消除虚函数可能引入的延迟。使用虚函数需要非常谨慎。CRTP 是一种通过选择静态多态来避免使用虚函数的方法。
我们现在将介绍另一种可能导致运行时延迟的延迟类型。
运行时类型识别(RTTI)
虚函数前一节概述了在运行时解析对象和函数调用对性能的影响。该节概述的大多数性能损失适用于所有在运行时解析的对象类型。在 C++中,运行时类型识别(RTTI)是一个用于描述在编译时无法确定类型的对象在运行时进行类型检查的功能。
运行时类型信息
C++的 RTTI 是一种机制,它可以在需要时跟踪和提取关于对象类型的信息。这只对至少有一个虚函数的类才有意义,这意味着在运行时可能有基类指针指向不同类型的派生类对象。因此,RTTI 允许您从可用的基类类型指针或引用动态地找到对象的类型。当异常和异常处理被添加到 C++中时,这一机制被引入,因为知道对象在运行时的类型对异常处理很关键。因此,RTTI 允许应用程序明确检查运行时类型,而不是依赖于隐式处理运行时类型解析的动态多态性。
C++提供了 dynamic_cast 内置运算符,用于安全地在继承层次中向下转换基类对象。在向下转换指针时,它在成功时返回转换后类型的有效指针,在失败时返回 nullptr。dynamic_cast(base_ptr)尝试将 base_ptr 的值转换为 Derived*类型。在向下转换引用时,它在成功时返回转换后类型的有效引用,在失败时抛出异常。我们将在本章后面的"阻碍性能的异常"部分介绍这一内容。
另一个 C++内置运算符 typeid 用于获取对象的运行时信息,并将其返回为 std::type_info 对象。std::type_info 对象包含有关类型、类型名称、检查两个对象类型是否相等等信息。对于多态类型,typeid 运算符提供了有关派生类型的其他信息。typeid(*base_ptr) == typeid(Derived)如果 base_ptr 指向 Derived 类型的对象,则返回 true。
讨论与 C++ RTTI 机制相关的性能损失。
每个类和对象都分配了一些额外的空间,这不是很大的问题,但如果有很多对象,就会导致缓存性能降低。
类型标识符(typeid)调用通常比较缓慢,因为它通常需要获取不常访问的类型信息。
动态 cast 操作可能非常慢。它涉及到获取类型信息和检查转换规则,这可能导致异常,这些异常本身就非常昂贵(我们稍后会讨论这个问题)。
在以下部分,让我们了解动态内存分配。
动态内存分配
堆上的分配(或动态分配)是编程中很常见的。我们需要动态分配来获得在运行时分配的灵活性。操作系统实现了动态内存管理的结构、算法和例程。所有动态分配的内存都进入了主内存的堆部分。操作系统维护了几个内存块的链表,主要是自由列表来跟踪连续的空闲/未分配内存块,以及已分配列表来跟踪已分配给应用程序的内存块。在新的内存分配请求(malloc()/new)时,它会遍历自由列表以找到足够空闲的内存块,然后更新自由列表(通过删除该块)并将其添加到已分配列表,然后将内存块返回给程序。在内存释放请求(free()/delete)时,它会从已分配列表中删除被释放的块,并将其移回自由列表。
让我们回顾一下动态内存管理相关的性能损失,这使其无法用于关键/热点路径,特别是对超低延迟敏感的高频交易应用程序。
堆跟踪开销
动态内存分配/释放需要遍历空闲内存块列表,这不如使用现有的 CPU 寄存器或将额外的变量推入堆栈那样高效。因此,堆跟踪机制会增加一些开销,而且延迟往往是非确定性的,这取决于空闲列表的内容、内存碎片化程度、请求的内存块大小等等。总而言之,堆内存管理创造的元数据变得相当复杂,许多操作只是为了释放那个内存块。
堆内存碎片
在多次分配和取消分配大小不同的内存块后,堆内存可能会产生碎片化,即存在许多小内存块之间的空隙,导致空闲内存列表变长,从而使得无法满足比任何空闲块都大的分配请求,即使总的空闲内存是充足的。操作系统采用一些堆碎片整理技术来管理这些潜在问题,但这会带来性能成本。
动态分配的内存块通常会随机分布在堆内存中。这可能会导致缓存性能显著下降、更多缓存失效和缓存未命中等问题。应用程序开发人员应该意识到这一点,并尽量以有利于缓存的方式申请动态内存 - 通常是申请一大块连续的内存,并管理该内存中的对象,以提高缓存性能。
动态内存分配的替代方案/解决方案
并非 HFT 系统的所有部分都是时间关键的。因此,我们只需关注时间关键的热路径上动态分配和取消分配的速度。
大多数高性能动态内存分配技术都归结为将动态内存分配从关键路径中移开,要么是预先分配大块内存,要么是由应用程序自己管理(内存池)。内存池基本上是一种数据结构,应用程序在启动时分配一大块内存,然后管理这些内存在关键代码路径中的使用。这里的优点是,这样可以使应用程序使用非常专门的分配和释放技术,从而最大化特定用例下的性能。
另一种技术是彻底检查动态内存管理的使用情况,并尽可能减少它们,通常以牺牲一些可能使应用程序不太灵活或通用的假设为代价。
我们也可以重新定义 C++ 的 new 和 delete 运算符,尽管这不是推荐的做法 - 拥有自定义的 new 和 delete 方法(例如 my_new()和 my_delete()方法)并显式调用更好。我们也可以讨论定位 new,它为我们提供了调用 new/delete 大部分语义上的好处,但我们可以控制运算符放置对象的位置。缺点是你必须单独管理内存生命周期。
使用 constexpr 有效
在 C++ 中,constexpr 用于使函数在编译时运行 - 这不是一个保证,而是提供了这种可能性。constexpr 函数有几个限制:它们不能使用静态或 thread_local 变量、异常处理或 goto 语句,并且所有变量必须是文字类型并且必须初始化 - 简而言之,编译器需要在编译时解决整个函数体。
正如我们提到的,声明一个 constexpr 函数并不意味着它必须在编译时运行。这意味着该函数有可能在编译时运行。如果 constexpr 函数在常量表达式中使用,例如函数调用的结果被分配给一个 constexpr 变量,那么它必须在编译时进行计算。
常量表达式函数的好处与我们迄今所讨论的类似。允许编译器在编译时解决和评估函数意味着不会在运行时为评估该函数付出任何成本。
异常是 C++ 现代错误处理机制,旨在改进 C 语言传统的基于错误代码和 if/else 语句的错误处理方式。在本节中,我们将探讨它们带来的优势、缺点和性能代价,以及为什么它们不适合高频交易应用。
为什么使用异常?
让我们讨论使用 C++异常处理错误的原因和好处
使用异常进行错误处理使源代码更简单、更清晰,也更擅长处理错误。这是一个更优雅的解决方案,相比于随时间增长的深层嵌套的 if-else 语句,它不会造成意大利面式代码,也不需要为每个场景进行测试等。总的来说,需要针对每个错误码(及相关测试)进行处理会导致开发速度变慢。
在没有异常的情况下,优雅或干净地完成某些代码是很困难的。一个经典的例子是构造函数中的错误 - 因为它不返回任何值,我们如何报告错误?优雅的解决方案是抛出一个异常,这为现代 C++设计中的资源获取即初始化(RAII)原则奠定了基础。替代方案是设置一个错误标志,需要在构造函数返回后每次创建对象时检查,这很丑陋,需要大量的代码来进行检查。类似的想法也适用于普通函数,您必须返回一个错误代码或设置一个全局变量。返回错误代码可以工作,但每次添加一个新的故障情况,都需要更新多个位置的代码,并导致前面提到的 if-else 意大利面条代码。设置全局变量也有自己的一系列问题 - 该变量必须在函数返回后检查,对于不同的故障会有不同的值,难以维护,并且在多线程应用程序中会失败。
例外很难被忽视,不像返回错误代码,如果应用程序开发人员不小心,它们通常会被忽略。未能捕获异常会导致程序终止。
异常会自动传播到方法边界 - 也就是说,它们可以被捕获并重新抛到调用者堆栈中。
我们现在知道为什么从软件工程的角度来看应该使用异常。接下来,我们将解释性能影响是什么。
让我们讨论一下与使用 C++ 异常进行错误处理相关的一些复杂性、缺点和性能惩罚
异常处理需要纪律和实践,特别是对于习惯传统错误代码、if-else 语句驱动型错误处理的开发人员来说。因此,与任何其他编程构造一样,它需要优秀的开发人员和在应用程序设计期间的谨慎考虑。
就性能而言,C++异常的优点是,在不引发异常的情况下,没有额外的开销。然而,当抛出异常时,其开销与函数调用相比极其高昂,需要消耗数千个 CPU 周期。
对于高频交易,如果应用程序设计仔细,使得只有在最罕见和最关键的错误中才会引发异常,此时性能损失并不是问题。但是,如果对异常处理轻描淡写,并将其作为算法正常运行的一部分,则可能会导致严重的性能问题,原本认为很罕见的情况可能最终会经常发生,从而造成严重的性能下降。 为了继续影响性能的运行时决策,我们现在将讨论目标是实际替换任何运行时决策的模板,方法是生成多个专门版本的代码。
模板减少运行时间
在这一部分,我们将继续讨论在关键或热点路径上删除或最小化运行时决策的问题,并介绍另一个重要的
C
+
+
C
+
+
C++ \mathrm{C}++ 功能。我们将讨论什么是模板,使用它们的动机,它们的优缺点,以及它们相对于替代方案的性能。
模板
模板是 C++实现通用函数和类的机制。泛型编程是指在算法和类中使用泛型类型作为参数,以与不同数据类型兼容。这消除了代码重复和反复编写与数据类型无关的类似或共享代码的需要。模板不仅可以处理不同的数据类型,还可以根据需要的不同类型在编译时自动生成类和方法的源代码,就像使用 C 宏一样。然而,与宏不同的是,编译器可以检查类型,而不是像宏那样盲目地替换。
模板有几种不同类型:
函数模板:函数模板类似于普通的 C++函数,但有一个关键的区别。普通函数只能使用函数内部定义的数据类型,而函数模板被设计为独立于数据类型,因此可以处理任何数据类型。
类模板:类模板与常规类类似,不同之处在于它们具有一个或多个作为模板参数的通用类型成员。这些类模板可用于存储和管理任何类型的数据。我们不需要为不同类型创建新的类,而是定义一个可以处理大多数数据类型的通用类模板。这有助于代码的可重复使用,运行更快,更高效。
可变模板:这是另一种重要的模板类型,适用于函数和类。它支持可变数量的参数,与不可变模板(仅支持固定数量参数)相反。可变模板通常用于使用模板元编程创建功能性的、列表处理构造。
我们现在将讨论另一种先进的模板相关技术,即模板特化。
模板特化
到目前为止,我们一直在讨论一个单一的模板类或函数可以处理所有数据类型的这个想法。但是也可能会根据一些特定的数据类型进行定制行为,这就是所谓的模板特化。模板特化是一种机制,我们可以为特定类型定制函数、类和可变参数模板。当编译器遇到使用特定数据类型的模板实例化时,它会为该类型或类型集创建一个模板实例。如果存在模板特化,编译器将使用该特化版本,将传入的参数与指定的数据类型进行匹配。如果无法匹配到任何模板特化,它将使用非特化模板来创建实例。
为什么使用模板?
讨论使用 C++模板来减少运行时延迟的动机。
泛型编程
使用模板的主要优点显然是通用编程以及生成高效、可重用和可扩展的代码。使用模板实现通用编程范式的一种特别好的实现是标准模板库(STL)。它支持广泛的数据容器、算法、迭代器、函子等等,它们都是通用的,可以在所有数据类型上操作。
编译时替换
在编译时进行替换,程序中仅生成所需的类或函数主体 - 也就是说,只有在应用程序中使用了模板的数据类型才会在编译时产生此模板类的实例。在编译时知道参数也使模板类比运行时解析的对象或函数更加类型安全。
开发成本、时间和代码行数(LOC)
通过实现一个只需处理所有数据类型的类或函数,可以减少开发工作量、时间和源代码的复杂性。这也使调试更加容易,因为代码更少,并且集中在单个类或函数中。
比 C 宏和 void 指针更好
C 使用预处理宏和 void 指针来支持某种形式的泛型编程。但在每种情况下,模板都是一个更好的解决方案,因为它们的可读性、类型安全性和错误隐患都要显著更好。宏也总是内联展开,但使用模板时,编译器可以选择仅在适当时候展开内联,这有助于防止代码膨胀。宏也很笨拙,由于需要适应单个逻辑代码行,编写起来很困难,但模板在其实现中作为常规函数出现。
编译时多态
这是至少对于高频交易应用程序(除了这里提到的所有其他内容)最重要的应用程序之一。我们详细讨论了虚函数和动态多态性如何造成重大的性能损失。模板和它们提供的通用编译时多态性通常用于尽可能消除虚拟继承和动态多态性。通过将代码解析和构造转移到编译时而不是运行时,更重要的是允许编译器、CPU 和体系结构优化生效,从而显著提高了性能。
这是一种更高级的
C
+
+
C
+
+
C++ \mathrm{C}++ 模板使用方式,通常要么不被很好地理解,要么被滥用(通过将现有的代码结构转换为使用模板元编程,而这种做法通常是不必要和过早的)。模板元编程使我们能够编写在编译时展开为实际运行时使用的机器代码的代码,本质上是使用模板预先计算一个结果表,以备后用。表达式模板是另一种类似的高级模板用法,用于在编译时评估数学表达式,以产生在运行时更高效执行的代码。
模版的缺点
现在让我们看看使用模板的一些缺点和弊端。
编译器支持
历史上,许多编译器对模板的支持较差,这可能导致代码可移植性降低。此外,当检测到模板错误时,编译器应该如何处理也不太明确,这可能会增加使用模板时的开发时间。一些编译器仍然不支持模板嵌套。
模板是仅设置头文件的,这意味着所有代码都位于头文件中,而没有任何代码在已编译的库中。当进行更改时,需要对整个项目进行完全重建。此外,由于所有信息都暴露在头文件中,因此无法隐藏代码实现细节。
增加了编译时间
正如之前提到的,模板完全存在于头文件中,无法编译为库;它们在应用程序编译和链接过程中进行链接。我们从编译库中获得的优势是,当进行更改时,只需重新构建受影响的组件即可。但是,对于模板来说,并非如此,因此每次进行更改时,所有模板化的代码都必须重新构建。这会导致编译时间增加,随着应用程序复杂性的增长和模板使用的增加,这可能会显著提高并成为问题。然而,这是可管理的,并不是问题。
难以理解
模板让很多开发者(包括高级 C++程序员)感到困惑,因为使用的规则很复杂。比如模板中的名称解析、模板专门化匹配和模板部分排序等问题,都可能让人难以理解和正确实施。总的来说,泛型编程是一种不同的编程范式,需要时间、努力和实践才能适应 - 如果你习惯了 C++中的命令式编程(这是大多数程序员经常使用的),它并不会自然而然地掌握。总的来说,模板有很多优点,包括开发和调试速度,但要真正掌握它需要一段时间,因为学习模板需要一个相当陡峭的学习曲线。
很难调试
调试包含大量模板的代码可能很困难。由于编译器在替换模板实例化和调用时使用了替换的实现,因此调试器在运行时很难找到实际代码。这与调试内联方法在运行时很困难的性质类似,因为源代码与调试器看到的不完全一致。错误消息非常冗长,理解起来非常费时耗神。即使是大多数现代编译器也会产生大量无用且令人困惑的错误消息。
代码臃肿
模板在源代码级别进行展开并编译到源代码中。编译器为每个模板类型或实例生成额外的代码。如果我们有大量的模板化类和函数或者产生实例的不同数据类型很多,那么编译器生成的代码可能会非常庞大。这就是所谓的代码膨胀,也会导致编译时间的增加。过度使用模板的代码库对运行时性能造成的更隐晦的问题是,由于应用程序本身的体积如此之大,缓存性能可能会较差,因为缓存数据被淘汰、遗漏等的几率会更高。
模板的运行时性能尽可能高效和低延迟,因为它摆脱了运行时对象解析和函数调用,转而采用编译时解析。如前所述,这为编译器优化开辟了广阔空间,例如内联(以及其他许多优化),并且在执行时能更好地利用 CPU 和体系结构优化,如预取和分支预测,从而获得卓越的性能。
避免在同一个类声明中使用模板和虚拟关键字很重要。当第一次使用类模板时,它会创建所有成员函数的副本(应用于该新类型)。拥有虚函数意味着即使是 vtable 和 RTTI 也将被复制,导致额外的代码膨胀(除了模板已经造成的)。
标准模板库(STL)
让我们探索 C++ STL,它已经在最近的 C++ 应用程序中变得非常常见。还有一些类似于 STL 但改善了一些问题并增加了一些功能的变体和库。
标准模板库
标准模板库(STL)是一个被广泛使用的库,提供了容器和算法,使用模板支持所有数据类型。STL 是一个包含常用算法和数据结构的模板类的存储库,可以很好地与用户定义的类型以及内置类型一起工作,其算法与容器无关。它们是通过编译时多态实现的,而不是运行时多态。
常用容器
让我们探讨最流行和常用的 C++ STL 容器及其应用场景:
向量:这是很多应用程序的默认首选,具有最简单的数据结构(C 数组式连续内存布局),并提供随机访问。
双端队列:双端队列实现了双向链表,当需要在队列头部或尾部插入或删除元素时,性能比向量更好。双端队列在内存使用方面也很高效,通常只使用根据元素个数所需的内存。然而,访问随机元素比较慢,因为需要遍历列表并跨越可能的随机内存位置(缓存性能较差)。
列表是与 deque 类似的,也是作为链表实现的,并且具有类似的优缺点。不同的是,在添加或删除元素时,列表不会使引用元素的迭代器失效,这与 vector 和 deque 不同。
集合、无序集合和多重集合:这些是用于跟踪元素是否存在于容器中的关联容器。关联容器是专门设计用于快速轻松地存储和检索键所引用的值。无序集合使用元素的哈希值进行查找,平摊时间复杂度为常数,但没有排序。平摊时间复杂度为常数意味着在正常情况下,无论容器大小,操作都需要常数时间。集合和多重集合的键是有序/排序的。多重集合和集合除了前者允许保存具有相同值的多个元素外,其他方面是相同的。
无序映射和映射,以及无序多映射和多映射:这些也是前面讨论过的关联式容器,只是它们跟踪键值对。无序映射和映射保存单个键值对,区别在于前者没有键的排序,后者按键值排序。无序多映射和多映射与无序映射和映射类似,但允许每个键有多个值。
让我们看看 STL 在运行时的性能
模板库 STL 相比 C 风格解决方案或基于动态多态的解决方案,具有特别出色的运行时性能。另一种从 STL 获得性能的方法是优化用户定义的结构,使其在高频交易应用程序的环境中完全满足我们的需求。
有效地使用 STL 来构建低延迟的 HFT 应用程序需要开发人员正确理解 STL 的工作原理,并仔细设计程序。开发人员经常误用 STL,而不了解所涉及的计算复杂性,他们就责备该库。
使用 STL 库函数的另一个问题是,它们在内部分配内存,如果不小心创建和传递分配器给 STL 容器,可能会导致性能不确定,尤其是在高频交易应用程序中(如果使用默认的动态内存分配器)。
在这一部分中,我们研究了可以帮助减少延迟的数据结构,因为它们已经针对性能进行了优化。在接下来的部分中,我们将学习如何通过静态分析来提高性能。
静态分析
在本节中,我们将研究静态分析的开发和测试技术。这是一组帮助软件开发/测试/维护生命周期的工具和技术。它适用于所有软件应用程序开发过程,但特别适用于高频交易(HFT)应用程序,在这些应用程序中,迅速进行更改(适应不断变化的市场条件和效率是获利的关键)但非常谨慎地不破坏现有的预期功能(错误/错误/错误可能导致重大经济损失)是很重要的。
C++静态分析
静态代码分析意味着通过检查代码并使用工具自动检测错误来调试软件应用程序,而无需实际执行应用程序或提供输入。这也可以被视为一种代码审查式的调试过程,它检查代码并尝试检查代码结构和编码标准。拥有自动化的工具和流程来执行此操作意味着我们可以比开发人员团队进行更彻底的漏洞检查,同时验证代码。用于分析源代码并自动输出错误和警告的算法和技术与编译器警告的精神相似,只是向前迈进了几步,以发现动态测试在运行时无法发现的问题。静态分析工具在从基本语法检查器到能够发现微妙错误的工具方面取得了很大进步。
静态分析旨在发现软件开发问题,如编程错误、编码指南违规、语法违规、缓冲区溢出类问题及安全漏洞等。
让我们解释为什么我们需要静态分析。
需要静态分析
静态分析的动机是寻找先前未被动态分析(单元测试/测试环境/模拟旨在执行程序时发现错误)发现的错误和问题。因此,静态分析可以发现在动态测试期间未遇到的数据和场景下可能导致严重问题的问题,从而触发故障(可能是巨大的故障)。需要注意的是,静态分析只是一系列软件质量控制工具和实践中的第一步。
除了静态分析之外,动态分析还依赖于设置足够的测试场景,并提供足够的输入和数据,以期涵盖应用程序的所有用例。有些编码错误可能在动态分析期间不会出现(因为我们在编写或执行单元测试时没有考虑到它们)。这些就是动态分析无法发现的缺陷,希望静态分析能够发现它们。
静态分析的类型
静态分析的类型可以分为以下几种:
控制流分析:关注调用者-被调用者关系和调用结构中的控制流,如进程、线程、方法、函数、子程序、指令等。
数据流分析:这里的重点是输入、中间和输出数据的结构,类型的验证,以及正确和预期的操作。
故障分析:这试图了解不属于前两类的不同组件的故障和失效。
接口分析:这旨在确保组件与整体管道相协调 - 它们全面且正确地实施接口。在高频交易中,这意味着交易策略过程被正确地实施,拥有所有必需的接口,以在模拟和实盘交易中正确和最优地运行。
另一种分析静态分析类型的方式如下:
这里的目标是回答问题,代码是否正确?
化妆品:问题在于,代码看起来是否一致?它是否与所需的编码标准一致?
设计:这里的问题是,根据既定的公司范围标准,各组件(如类结构、方法大小和组织)是否被正确设计?
错误检查:这是自解释的,重点关注缺陷、故障和代码违规。
预测性:这是更高级的,但目标是预测应用程序在执行时的行为,为动态分析做准备。
在以下部分,我们将介绍静态分析的步骤。
静态分析步骤
静态分析的目标是将其自动化,以便在应用于大型代码库时易于、快速、全面。因此,这个过程本身需要简单和算法化,以便自动化。一旦从开发人员的角度来看,源代码已准备就绪或半准备就绪,静态代码分析器就会检查代码并标记编译问题、编码标准问题、代码或数据流错误、设计警告等。假阳性很常见,因此静态代码分析器的输出需要由开发人员手动分析,一旦修复了所有真实(真阳性)问题,就会再次经过静态代码分析器,然后进入动态分析阶段。
静态分析远非完美 - 它会产生误报和遗漏问题 - 但它是一个很好的正交调试和故障排查工具,可以节省开发人员和代码审查人员的时间,从而产生更高效的工作环境。
静态分析的利弊
让我们来看看静态分析的利弊。利益已经讨论过了,所以我们在这一节中正式列举和概括它们。你也可能猜到一些缺点,但我们也将在这里正式讨论它们。
福利
我们提供静态分析的优点列表:
标准化和统一的代码:静态分析器工具起源于 linters,因此它们在标记新代码是否符合编码指南和设计标准方面非常出色。这产生了一个符合既定(公司或行业范围内)编码标准和设计模式的统一代码库。
手动代码审查对整个开发团队来说极其耗时。自动化静态分析可以在代码进入审查之前帮助消除许多问题。此外,它可以及早发现这些问题,错误总是更容易、更快地修复。总的来说,这将提高整个软件开发生命周期中整个团队的开发、审查和维护速度。
深度:我们之前提到过,构建单元测试或运行动态分析以覆盖所有边缘情况和所有代码执行路径是完全不可能的。在这些情况下,静态代码分析器做得要好得多,因为它们可以检查非平凡或深层的 bug 和错误。
准确性:另一个明显的优势是自动静态分析方法的准确性极高,远远超过手动代码评审和动态分析。准确性有助于彻底性和质量。
在大多数真实世界应用程序(尤其是 HFT 应用程序)中,存在许多移动元素,因此在模拟、测试或实验室环境中进行动态分析需要大量设置和资源。对于 HFT 来说,这意味着不同的流程和组件(feed 处理程序、订单网关、模拟交易所和记录仪),以及网络、IPC 和磁盘资源。这可能是痛苦、昂贵和耗时的。另一方面,离线进行静态代码分析时不涉及这些移动元素,因此很容易、便宜且快速。
我们详细描述了静态分析的好处,现在我们将讨论它的弱点。
缺点
静态分析的缺点如下:
考虑以下代码:
int area(int l, int w) {
return l + w;
}
这里的静态分析器可以检测到对于某些 1 和
w
w
w w int 值的组合,它们的总和会导致溢出,但它无法确定该函数计算面积的方式是否正确。
许多编码规则对于静态分析器来说太过复杂,无法静态执行或检测。它们可能依赖于外部文档,存在主观性,取决于公司或应用程序等。
我们之前提过:可能出现误报,浪费开发人员的时间。
就像编译过程一样,在整个应用程序上运行静态分析器需要时间。代码库越大、越复杂,运行所需的时间就越长。
尽管它们有用,静态分析器无法保证应用程序执行时会发生什么,所以静态分析可以补充但永远无法取代动态分析、单元测试、模拟或测试环境。
这些常常会影响静态分析器,因为源代码可能无法轻易获取或访问。
我们已经看到了静态分析的利弊,现在我们将考虑用来执行这种分析的工具。
让我们快速介绍一些最好和最知名的用于 C 和
C
+
+
C
+
+
C++ \mathrm{C}++ 的静态代码分析器:
Klocwork 是目前最好的 C++静态代码分析器之一。它可以处理大型代码库,拥有大量检查器,支持检查器自定义,支持差分分析(在大型代码库发生少量变更时提高分析效率),并与许多 IDE 和 CI/CD 工具集成。
C++
C++ Depend 是一个商业 C++ 静态代码分析器。它的优势在于分析和可视化代码库架构(依赖性、控制和数据流层)。它具有依赖图特性和监控功能,可报告构建之间的差异。它还支持规则检查器自定义。
伯乐软件
帕拉索夫特拥有一套商业化的测试工具,用于 C 语言和
C
+
+
C
+
+
C++ \mathrm{C}++ ,其中包括静态代码分析器以及支持动态代码分析、单元测试、代码覆盖和运行时分析。它拥有大量丰富的静态代码分析技术和规则。它还让您以有序的方式管理分析结果,从而提供了一套全面的软件开发过程工具。
普瓦斯工作室
这是另一个商业工具,支持大量编程语言,包括 C 和
C
+
+
C
+
+
C++ \mathrm{C}++ 。它可以检测非简单的错误,与流行的 CI 工具集成,并且有良好的文档记录。
编程软件
这 Clang C 和 C++编译器带有一个静态分析器,可用于使用基于路径的敏感分析来查找 bug。 找到一个适用于所有 HFT 系统各个方面的单一配方是不可能的。学习如何分析性能和运行静态分析有助于我们避免在 C++ 编码时可能犯的最大错误。通过消除代码可能存在的最大问题,我们可以专注于性能至关重要的事情。
通过结合静态分析和我们讨论过的运行时优化,如使用合适的内存模型和减少函数调用次数,我们将达到 HFT 系统可接受的性能水平。
使用案例 - 构建外汇高频交易系统
一家公司需要一个 HFT 系统,能在 20 微秒内发送一个订单。为此,该公司可以采取以下方法:
选择多进程架构而不是多核架构。
确保每个进程都被固定到一个特定的核心上,以减少上下文切换。
在共享内存中使用环形缓冲区(无锁数据结构)进行进程间通信。
使用 Solarflare OpenOnload 进行网络加速来设计网络栈。
增加页面大小以减少 TLB 缓存缺失的数量。
禁用超线程以获得对进程并发执行的更多控制。
使用 CRTP 减少虚拟函数的数量。
使用模板数据结构来消除运行时决策。
预先分配数据结构,避免在关键路径上进行任何分配。
发送假订单以保持缓存热量,并允许在最后一刻发出订单。
在任何交易系统中,订单数量远低于接收的市场数据量。从获取市场数据到发送订单的关键路径很少被执行。非关键路径数据和指令将占用缓存。因此,非常重要的是运行虚拟路径来通过整个系统发送订单,以保持数据缓存和指令缓存处于就绪状态。这也将保持分支预测器处于热运行状态。
优化的主要目标是减少耗费大量资源的操作。移除函数调用、使用无锁数据结构、减少上下文切换都是这一策略的一部分。
另外,在运行时做出的任何决定都会很昂贵。这就是为什么模板函数和内联函数将成为任何高频交易系统的通用代码的一部分。最昂贵的操作是那些涉及网络通信的操作。使用像 Solarflare 这样的端到端内核旁路可以优化交易系统内的网络延迟。通过使用这些优化,这家公司可以实现 20 微秒的从行情到交易的延迟。延迟分布非常重要,我们需要确保 20 微秒是延迟的上限。我们不应该考虑平均值,因为很难用这个值评估高延迟。
在高频交易中,某些策略在交易量大时会非常盈利。如果大部分时间交易系统按预期运作,但并不意味着系统会一直表现良好。当交易系统接收大量数据时,如果构建不当,最大延迟可能是平均延迟的 10 倍。我们需谨记,如果未在生产环境中自行测量,任何优化都不保证会更快。
摘要
在本章中,我们涵盖了现代 C++ 14、17 和 20 中许多适用于多线程和超低延迟应用程序的功能,这些应用程序涉及共享内存交互。我们还涵盖了应用程序开发的静态分析。最后,我们讨论了将尽可能多的决策、代码评估和代码分支从运行时转移到编译时的运行时性能优化技术。本章的内容解决了 C++ HFT 应用程序开发和性能的重要设计和开发决策及技术。
我们还看到如何为专门从事外汇的小型对冲基金建立一个高频交易系统。在下一章中,我们将了解 Java 在超低延迟系统(如高频交易系统)中的使用。
Java 和低延迟系统的 JVM
当人们思考高频交易(HFT)时,Java 并不常常浮现在脑海中。Java 虚拟机(JVM)的预热,以及它运行在虚拟机上以及臭名昭著的垃圾收集器一直是程序员的一大障碍。然而,如果聪明地了解这些局限性并编码,Java 仍可用于低延迟环境。届时,您就可以从 Java 所带来的所有优势中获益。
非常活跃和深入的免费第三方库提供。
一次写,到处编译和运行。
更大的稳定性,避免由于糟糕的内存管理而导致臭名昭著的段错误。
在本章中,您将学习如何调整 Java 以用于高频交易。运行时的性能在很大程度上取决于 JVM 的性能。我们将深入解释如何优化这一关键组件。
我们将涵盖以下主题:
垃圾收集
虚拟机预热
衡量 Java 的性能
Java 线程
高性能数据结构
日志和数据库(DB)访问
Important Note
In order to guide you through all the optimizations, you can refer to the
following list of icons that represent a group of optimizations lowering the
latency by a specific number of microseconds:
#(5): Lower than 20 microseconds
Z}:\mathrm{ Lower than 5 microseconds
: Lower than 500 nanoseconds
You will find these icons in the headings of this chapter.
在进入优化之前,我们想提醒您 Java 是如何工作的。在下一节中,我们将描述 Java 的基础知识。
介绍 Java 的基础知识
爪哇由太阳微系统公司于 1991 年创建。第一个公开版本于 5 年后发布。爪哇的主要目的是在互联网应用程序中实现便携性和高性能。与 C++不同,爪哇是跨平台的。JVM 确保任何架构和操作系统的可移植性。
图 9.1 显示了 Java 的编译链。我们可以观察到 Java 编译器没有产生可执行文件,而是产生字节码。JVM 将运行这种字节码来运行软件。 源
代码
→
→
rarr \rightarrow \begin{tabular}{c}
Java
编译器
→
→
rarr \rightarrow 字节码
→
→
rarr \rightarrow
Java 虚拟
机器
运行
系统
\end{tabular}
图 9.1 - Java 编译链 我们建议您阅读 Packt 出版社的《Java 编程入门》一书,该书由 Mark Lassof 撰写,以详细了解该语言的特性。在本章中,我们将讨论影响高频交易(HFT)性能的因素。正如我们在 C++中描述的,一个关键组件是内存管理结构。与需要开发人员手动管理内存的 C++不同,Java 拥有垃圾收集器(GC)。GC 的主要目的是在对象(内存段)不再使用时,从堆中释放这些对象。
图 9.2 描述了内存是如何被划分为不同的功能部分的。堆区域在 JVM 启动时被创建。为了改善数据结构,运行时的对象被添加到堆区域。在 GC 部分,我们将看到如何调整堆大小以提高运行时的性能。与堆一样,方法区域在启动时被创建,并存储类结构和方法。
图 9.2 - JVM 内存区域部分
当我们创建一个线程时,JVM 堆栈被使用。这部分内存用于临时存储数据。当 JVM 启动时,还会创建本地堆栈和 PC 寄存器,但它们不会是 HFT 性能的关键组件。
性能关键组件是垃圾收集器。JVM 会触发垃圾收集以自动回收堆中不再使用的对象内存。
图 9.3 代表 Java 堆内存的组织结构。垃圾收集将管理此内存部分的对象分配。
图 9.3 - JVM 堆内存模型 高频交易中需要记住的两个主要部分是伊甸园和旧内存。当我们创建新对象时,它们会进入伊甸园部分,而保留时间最长的对象将留在旧内存中。分配和释放对象需要时间,这就是为什么垃圾回收将成为高频交易中的主要性能组件。 我们现在将讨论垃圾收集。
减少 GC 的影响
在 1996 年 Java 发布时,其中一个重大承诺就是终结了众所周知的 C/C++开发者们熟悉的 segFault 错误。Java 决定将所有对象和指针的生命周期从开发者手中移除,并将其逻辑委托给 JVM。这就诞生了 GC。
垃圾收集没有单一的类型。已经开发了多个版本;所有这些版本都有不同的规格,可提供低延迟暂停、可预测性或高吞吐量。
优化 Java 的最重要的部分之一是找到最合适的垃圾收集器(GC)以及最佳参数。主要的 GC 算法有:
序列 GC:推荐用于小数据集或单线程且无暂停时间要求的情况。
并行/吞吐量收集器:推荐用于峰值性能而非暂停时间要求。
并发标记-清扫收集器:推荐用于最小化 GC 暂停时间。
建议最短 GC 暂停时间。
西南多收藏家:G1 GC 的改进版本,暂停时间不再与堆大小成正比。你可以在这里找到更多信息:https://wiki.openjdk.java.net/display/shenandoah。
实验性 GC (ZGC):建议用于高响应时间、很大堆和小暂停时间。您可以在这里找到更多信息:https://wiki 。openjdk.java.net/display/zgc/Main 。
艾西龙 GC(实验性):对于勇敢的人来说,这被称为 Java 的无操作 GC。它拥有最低数量的 GC 干预,且没有内存回收。它被推荐给了解自己应用程序中创建的对象的完整生命周期的人使用。
选择最佳 GC 以满足您需求的唯一方法是尝试它们全部。测量性能很重要,以确定哪些算法效果最好。在测试性能时,我们希望启用不同的 GC,并在与生产环境尽可能接近的环境中运行。每种 GC 算法还伴有许多选项,允许您控制和调整其行为。没有一种选项优于其他选项;您需要进行实验,找到对您的应用程序效果最好的选项。我们将在 GC 频率和持续时间之间找到平衡。您更喜欢每 30 分钟 2 毫秒的 GC 还是每 60 分钟 4 毫秒的 GC?这些都是您需要回答的问题。如需更深入地了解不同的 GC 算法和选项,您可以参考 Oracle 网站上的最新文档: https://docs.oracle.com/en/java/javase/18/gctuning/ index.html。
我们现在将探讨如何尽可能限制 GC 事件。
保持 GC 事件低和快
在高频交易中,我们希望限制 GC 的影响。当触发 GC 时,很难保持执行的控制。每当触发 GC 时,性能都可能受到影响。因此,保持 GC 干预次数较低非常重要,当干预不可避免时,我们需要尽快完成。减少 GC 次数和持续时间的最佳方式取决于编码风格。您创建的对象越多,对 GC 的压力就越大。请记住,在关键路径上创建对象可能最终导致删除该对象。关键是提前分配在整个软件执行期间存活的对象,以避免分配/释放触发 GC 的工作。您的主要目标是避免频繁创建短期对象。
您首先需要识别程序中的热路径(高频调用的函数或代码段);一旦识别出来,就需要查看该路径中的所有对象创建情况。如果您知道在很短的时间内会创建成千上万个对象,那么您需要考虑将这些对象添加到缓存池中。它们将首先被分配,随后使用。理想情况下,热路径是单线程的,在特定的核心上旋转(正如我们在第 6 章中解释的那样,HFT 优化 - 架构和操作系统中减少切换上下文的数量)。这种设计将不需要对池进行任何锁定,只需一个简单的计数器就足以获取和返回对象。在某些情况下,即使是多线程的,使用池也可能是个好主意,尽管您需要进行锁定。我们可以用较小的 GC 时间来抵消使用锁的成本。通过缓存,我们将增加堆大小。考虑对象创建数量与堆大小之间的权衡很重要。
我们将继续讨论其他 Java 特性,这些特性增加了分配的对象数量,可能会触发 JVM 的使用。
限制自动装箱效果
自动装箱
Java 默认提供了大量的集合、列表和集合实现,可满足大部分需求。不幸的是,这些结构只支持对象作为参数,大多数都需要在每次插入时创建新对象来实现所需的行为。编码时需要牢记这一点。您还可以使用一些第三方库(https://fastutil.di.unimi.it/或 https://github.com/real-logic/agrona),它们使用 Java 基本类型实现 Java 集合。根据您的需求,这可以大大减少程序中的对象创建。
您还需要注意由代码的 Java 类或任何第三方库创建的不同对象。您需要了解迭代器等对象的创建。每次调用它都会创建一个新对象。这就是为什么您可能会更喜欢使用支持简单循环的结构进行每次迭代。另一个例子是用于连接数据库的库。它们非常动态,但在每次插入时可能会创建大量对象。
可以调整 GC 以降低延迟,同时增加内存使用量。在 Oracle Java 11 之后,出现了 Epsilon 选项。此选项设置了一个 GC,它管理内存分配,但不实现内存回收机制。当可用的 Java 堆被耗尽时,JVM 将关闭。作为内存占用和速度的代价,Epsilon 提供了一种被动的 GC 方法,具有定义的分配限制和最低的延迟开销。然而,在 Java 语言中引入手动内存管理功能并非目标。
停止世界暂停
通常情况下,GC 不需要暂停所有线程。这意味着所有活跃的线程暂停并且只有内存清理线程在运行。这被称为暂停所有线程,因为在此期间,您的程序不会主动运行。也有一些 JVM 实现不会出现暂停。Azul Zing JVM 就是其中之一。它使用的算法决定了 JVM 是否需要暂停所有线程来进行垃圾回收。
标记-清扫-压缩(MSC)是一种标准算法,它在热点(代码的关键部分)中默认使用。它包含三个步骤,并以 STW 的方式实施。
遍历实时对象图以标记可访问的事物
扫描:搜索未标记的记忆
整理內存,將指定的東西移動
虚拟机应该在堆中移动项目时修复对此对象的所有引用。在整个重定位过程中,对象图不一致,因此暂停是必需的。对象图是一个列表,报告不同创建的对象之间的关系。它使他们能够知道哪些对象仍然需要,哪些对象没有被任何其他对象引用,可以被收集。
并发标记-清除(CMS)是 HotSpot JVM 技术,不需要 STW 暂停来收集老空间(不同于完整回收)。
对象管理系统不使用紧凑技术,而是使用写屏障(Java 堆中每次写引用时触发的机制)来构建并发标记阶段。缺乏压缩会导致碎片化,如果后台垃圾收集速度不够快,程序可能会被暂停。在某些情况下,对象管理系统会使用"全停顿-标记-压缩"垃圾收集。
我们现在将讨论 Java 基本类型如何改善垃圾回收。
原始类型和内存分配
除了之前减少 GC 干预的方法外,我们还可以考虑优化内存分配。当原始类型可用时,一种减少延迟的技术是使用它们。原始类型(通常称为基本类型)使用的内存比其对象等价物少,这带来了以下好处:
它允许将更多数据装入单个缓存行。如果处理器需要的数据不在当前缓存行中,将加载新的 64 字节缓存行。如果 CPU 无法预料到内存访问,检索操作可能需要 10 到 30 纳秒。
如果我们可以利用更少的内存,我们可以保持最大堆大小较短,这意味着收集器在进行完全垃圾收集时,需要搜索更少的活动根,并且对象数量较少。完整的垃圾收集可能很容易需要 1 秒/千兆字节。
基本数据类型可减少程序产生的废弃物。你制造的大部分物品最终都需要被回收。小型垃圾回收器非常快速地处理死亡物品;事实上,处理死亡对象几乎不需要任何时间,因为只有活动的事物才会在存活区域之间移动(并进入这些区域);尽管如此,复制活动对象仍需要利用资源来执行业务逻辑。
分配给基元比在堆上生成新对象快。在 Java 中创建对象非常快,甚至比 C 中的 malloc 还快,可以识别主存储器中的合适部分。在 Java 中,对象在预先存在的缓冲区(称为 Eden 空间)中的下一个可访问位置进行构建。
许多功能返回双精度值而不是单个数字,以构建定价系统。在理想的世界中,更偏好使用对象;然而,这并不可行,因为我们的计算偶尔会失败,我们必须生成错误或状态代码。如果我们需要返回一个对象,在某些情况下,它可能会被重复使用,因此我们可以将其作为参数传递给该函数。
我们看到原始类型如何帮助优化垃圾收集;我们现在将讨论内存的分析。
内存分析
我们谈论了如何将对象创建降到最低,但是您如何跟踪程序创建的对象数量呢?最好的方法是使用 Java 剖析器。有许多(授权或免费)解决方案可用:
这些软件将提供报告和图表;它们将成为性能问题的主要视觉证据。一些软件将报告大部分对象创建的热点区域。它们还集成了可能有助于找到代码中效率低下的地方的性能测量工具。不建议在生产环境中使用分析软件。这些工具会影响性能。它们应该在模拟环境中运行,我们可以在其中重现生产环境的模拟,以确保我们探索了代码中的所有路径。我们不需要用大量数据来轰炸它;分析的目的不是找出系统能处理多少数据,而是找出它使用资源的效率。
图 9.4 代表了 Java 分析器的输出结果。我们可以通过条形图看到代码哪个部分耗时最长。
图 9.4 - Java 分析器结果示例 在本节中,我们深入探讨了 GC 及其对性能的影响。我们了解了如何使用编码技术来限制其干预。我们现在将讨论 JVM 预热,这是实现高性能代码所需的。
预热 JVM
可编译语言,如 C++,之所以被如此命名,是因为所提供的代码完全是二进制的,可以直接在 CPU 上执行。由于解释器(加载在目标机器上)在运行时编译每一行代码,因此 Python 和 Perl 被称为解释型语言。在图 9.1 中,我们展示了 Java 处于中间位置;它将代码编译成 Java 字节码,在必要时可以转换为二进制形式。
这是为了长期性能优化而不在启动时编译代码的。Java 通过观察程序运行并分析实时方法调用和类初始化来构建经常调用的代码。它甚至可能会根据过去的经验做出一些有根据的猜测。因此,编译后的代码执行速度非常快。拥有最优执行时间的主要前提是函数需要被频繁调用。
在优化和编译一个方法之前,必须调用它一定次数以超过编译阈值(此限制是可配置的,通常约为 10,000 次调用)。未优化的代码在达到此阈值之前无法以最快速度运行。在获得更快的编译和获得更好的编译之间存在权衡(如果编译器在执行中做出的假设是错误的,就会产生重新编译的成本)。
我们重新回到了起点,当 Java 应用程序重新启动时,我们将不得不等待直到我们再次达到该阈值。HFT 软件具有不频繁但关键的方法,这些方法只被调用几次,但在它们被调用时必须非常快。
蔚蓝 ZING 通过允许 JVM 将编译方法和类的状态存储在配置文件中来解决这些问题。这项独特的功能称为 ReadyNow!
®
®
^(®) { }^{\circledR} ,确保 Java 程序始终以最大性能执行,即使在重新启动后。您可以在此网站上找到更多关于 ReadyNow 的信息:https://www.azul .com/products/components/readynow。
当我们恢复一个软件(如交易系统)时,使用现有的配置文件,Azul JVM 会记住之前的决策,并直接编译描述的方法,消除了 Java warm-up 问题。
我们也可以在开发环境中创建一个配置文件来模拟生产行为。之后,优化后的配置文件可以被部署到生产环境中,确保所有关键流程都已编译和优化。
近期,GraalVM 还开发了一个提前编译(AOT)项目。我们不会深入细节,但它将允许您将二进制代码预编译为本地代码。这将让您在启动时加快该过程,因为 AOT 本地代码将立即使用,直到分层编译启动。它在 Java 9 中被引入作为实验功能。我们现在将解释 JVM 如何使用分层编译优化运行时。
分层编译在 JVM 中
在运行时,JVM 理解并执行字节码。此外,即时(JIT)编译被用来提高性能。在 Java 的旧版本中,我们必须手动在两种可访问的 JIT 编译器类型之间进行选择。一种(
C
1
C
1
C1 \mathbf{C 1} )被设计用于加快应用程序启动,而另一种(
C
2
C
2
C2 \mathbf{C 2} )则提高了整体性能。
为了获得双赢的局面,Java 7 引入了分层编译。对于最常用的部分,JIT 编译器将字节码转换为本机代码。HotSpot JVM 之所以得名于此类部分,它们被称为热点。因此,Java 可以达到与完全编译语言相媲美的性能。JIT 编译器有两种类型:
客户端编译器(C1),它优化了启动时间
C2(服务器编译器)经过整体性能调优
与
C
1
,
C
2
C
1
,
C
2
C1,C2 \mathrm{C} 1, \mathrm{C} 2 相比,会更长时间地观察和分析代码。这使得 C 2 能够改进编译代码的优化。
使用 C 2 编译器编译相同的函数需要更长的时间和更多的内存。然而,它创建的原生代码比 C1 更优化。Java 7 首次引入了分层编译的概念。其目标是通过结合 C 1 和 C 2 编译器来实现快速启动和强大的长期性能。
图 9.5 展示了 JVM 的分层编译机制。当应用程序第一次启动时,JVM 解释所有的字节码并收集分析数据。获取的分析信息随后被 JIT 编译器用来定位热点代码。为了达到本机代码的速度,JIT 编译器首先使用 C1 编译频繁执行的代码部分。随着更多分析数据的可用,C2 接管了编译工作。C2 使用更为激进的优化算法重新编译了代码。
图 9.5 - JVM 中的分层编译
尽管 Java 虚拟机只有一个解释器和两个即时编译器,但如图 9.6 所示,共有五个编译层次。原因是 C1 编译器有三个操作级别,这三个级别之间的分析量不同。
图 9.6 - 编译级别 我们将更深入地描述 JVM 编译级别。
0 级 - 解释性代码
虚拟机在代码第一次运行时就可以理解所有 Java 代码。与编译型语言相比,这一阶段的性能通常较差。但是,在热身阶段之后,JIT 编译器就开始运行并编译热点代码。JIT 编译器利用在这一阶段收集的分析信息来进行优化。
一级 - 简单 C1 编译代码
虚拟机使用 Cl 编译器在这个级别编译代码,但不收集任何性能分析数据。对于被视为简单的函数(如算术运算),虚拟机使用编译级别 1。由于方法复杂性很小,C 2 编译也不会使其更快。因此,虚拟机认为对无法进一步优化的代码收集性能分析数据是无意义的。
二级 - 限制性 C1 编译代码
在第 2 级,JVM 使用带有温和分析的 C1 编译器编译代码。当 C2 队列已满时,JVM 切换到此级别。为了提高性能,目标是尽可能快地编译代码。然后,JVM 在第 3 级上进行全面分析重新编译代码。最后,当 C2 队列变得不太拥挤时,JVM 在第 4 级上重新编译。
3 级-完全 C1 编译代码
在第 3 级,JVM 使用 Cl 编译器完全分析编译代码。第 3 级包含在标准编译过程中。因此,除了基本操作或编译器队列满时,JVM 在所有情况下都使用这一级别的编译。在即时编译(JIT)中,最典型的情况是,解释后的代码直接从第 0 级跳到第 3 级。
4 级- C2 编译代码
在这个级别,JVM 使用 C2 编译器来构建代码,以实现最佳的长期性能。同样,第 4 级也包含在标准编译过程中。除了简单的方法外,JVM 在此级别编译所有方法。JVM 停止收集分析信息,因为第 4 级代码被认为已经完全优化。但是,它可能会决定对代码进行去优化,并将其返回到第 0 级。
总之,JVM 解释代码直到方法达到 Tier3CompileThreshold。然后使用 C 1 编译器编译该方法,同时仍在收集性能分析数据。当方法的调用次数达到 Tier4CompileThreshold 时,JVM 使用 C2 编译器对其进行编译。JVM 最终可能会决定对 C 2 编译的代码进行去优化。这意味着整个过程将被重复。
每个编译阈值与迭代次数相关联。要了解默认值,我们可以使用-XX:+PrintFlagsFinal 要求 JVM 打印它们。
intx CompileThreshold = 10000
intx Tier2CompileThreshold = 0
intx Tier3CompileThreshold = 2000
intx Tier4CompileThreshold = 15000
您可以使用 JVM 选项来更改这些值,以降低或提高它们。没有魔法数字,每个程序都是独特的,所以最好使用不同的参数组合来监控性能,并选择最能满足您性能要求的组合。
我们现在将展示如何在软件启动后尽快优化 JVM。
在高频交易中,我们不希望一运行软件就能获得最佳性能。我们将解释用于避免中间编译的不同方法。
分级编译
我们可以使用-XX:-Tiered Compilation 选项来运行 JVM。它禁用了中间编译层级,这样一个方法要么被解释执行,要么在最大优化级别(C2)进行编译。
保留代码缓存大小:
代码缓存的最大大小由 -XX:ReservedCodeCacheSize=N 选项指定。代码缓存的控制方式与 JVM 内存的其余部分相同:它具有初始大小(由-XX:InitialCodeCacheSize=N 给出)。代码缓存的初始大小由体系结构确定。此设置很有用,因为它对速度的影响很小。
编译阈值:
标准编译是由 -xX : CompileThreshold=N 选项的值触发的。Java 可以在客户端或服务器模式下运行,默认值取决于该模式;对于客户端应用程序为 1,500,对于服务器应用程序为 10,000。降低此数字可以加快编译到本机代码的速度。需要根据每个应用程序的需求进行调整;选择太小的数字,JVM 会生成带有有限剖析信息的本机代码,可能无法为长期创建最优化的代码。阈值是通过添加后向边循环计数器和方法进入计数器的总和来确定的,尽管这里只有一个标志。
热身您的代码
您可以编写自己的代码加温器。由于您知道程序中的热点路径,您可以编写一个包装器,执行该热点路径以达到字节码优化所需的最少迭代次数。在 HFT 系统中,市场数据处理通常不是问题,因为您将很快达到大量接收数据的迭代阈值。您应该关注不太频繁的事件,如新订单或定期调用的代码执行,但您不希望它们减慢您的热点路径。您需要非常谨慎地处理实时环境中的 JVM 预热。为了预热 JVM,我们可以利用各种技术。Java 微基准测试工具箱(JMH)是一个帮助我们适当实现 Java 微基准测试的工具包。对于 HFT,它可能是关键路径上的一个函数,例如负责发送订单的函数。一旦加载,它会不断执行代码片段,同时跟踪预热迭代周期。JMH 是由创建 JVM 的同一团队开发的。您可以在此处阅读关于它的信息:https://www.baeldung.com/java-jvm-warmup 。 要开始使用 JMH 的最快方法是使用 JMH Maven 原型创建一个新的 JMH 项目。我们将在以下部分深入了解 JMH 的细节。
微基准是一个有助于正确实现 Java 微基准测试的工具包。让我们现在详细讨论它们。
为什么 Java 微基准测试很难创建?
创建能够准确评估大型程序中一小部分性能的基准很困难。当基准测试在隔离环境中运行你的组件时,JVM 或底层硬件可能会对其进行各种优化。但是,当该组件作为一个大型应用程序的一部分运行时,某些优化可能无法使用。因此,设计不善的微型基准测试可能会让你认为组件的性能比实际情况更好。
编写一个良好的 Java 微基准测试通常需要避免在微基准测试执行过程中 JVM 和硬件优化,这些优化在真实的生产系统中不会进行。这就是它的目的所在。很难找到正确测量更大应用程序一小部分性能的基准测试。
我们不会深入探讨如何实现 JMH 的细节。网上已经有大量非常好的资源可供参考。一本必读的资料是 JMH GitHub(https://github.com/openjdk/jmh ),它不仅提供了安装 JMH 的说明,还有大量示例。JMH 框架可以帮助缩短应用程序的预热期。它也可以用于离线性能测量;它将帮助您做出设计决策。如果您在多个设计方案之间犹豫不决或考虑某些优化,JMH 可以帮助您评估性能并确认您的选择。正如我们在本书中多次提到的,优化的关键是确保准确测量性能,以确保优化发挥作用。我们现在将讨论如何测量实时性能。
测试驾驭系统只能让你决定什么是最佳设计来实现最佳吞吐量或速度。一旦你的代码发布到生产环境中,你需要跟踪应用程序关键部分的性能。
在高频交易(HFT)应用程序中,您始终会有一个或多个线程在内核上旋转。您可以保持一个简单的计数器,每次旋转时递增。如果由于任何原因,您在该循环中有一些代码开始行为不当并增加一些阻力,您将能够通过观察 RPM 行为的变化来检测到它。
要保持的下一个措施是延迟。您希望在代码的关键部分保持简单的延迟度量,如果您有一个分布式系统,您也希望测量不同进程之间的通信延迟。
我们需要在收集延迟和转速统计数据时保持谨慎。我们不想在捕获统计数据时造成比独立代码更多的开销和延迟。
为了这个原因,统计收集器应该有非常轻量和基础的逻辑。在逻辑中的任何时候都不能有锁或对象创建。你应该拥有累积的、增量的和描述性的(最小值、最大值、平均值和百分位数)统计收集器,因为它们将满足您的大部分需求。
这里是我们可以使用的不同类型的计数器:
累积式是一种简单的计数器,可以通过任何值进行递增。
递增计数器是一种简单的计数器,只能递增一个单位。
描述性跟踪收集期间的最小值、最大值、平均值和百分位数。
您现在需要收集该期间的统计数据并将其存储下来,以便能够分析这些数据。最好的行动方案是设置一个周期性线程,每隔 X 分钟(1 分钟是一个不错的数字)就会唤醒一次,从所有收集器那里获取并重置统计数据。然后将这些统计数据存储下来,以便进行可视化或分析。存储的方式也需要很智能。速度不是关键,但对象的创建是关键;日志或数据库存储将会生成大量对象创建。一个不错的解决方案是通过进程间通信(IPC)或用户数据报协议(UDP)将这些统计数据发送到一个不同的进程,让远程进程负责存储。向远程进程发送数据而无需创建任何锁或对象是很容易的。
要可视化存储的数据,一个好的选择是 Grafana 仪表板(https://grafana.com/grafana/)。它是一个前端,可以使用不同的插件链接到多个数据源。它将让您通过一个拥有大量图表选项和警报触发器的漂亮网站访问这些统计数据。原始数据将可在运行 Grafana 的箱子上或通过 web API 获得。下一节将讨论我们可以通过线程实现的性能。
Java threading
线程是 Java 中并发的基本单位。线程提供了通过并行执行多个任务或在等待输入/输出(I/O)时执行工作的优势,从而减少程序执行时间。
高频交易架构大量使用线程来提高吞吐量,正如我们在第 7 章"高频交易优化-日志、性能和网络"中提到的。并行执行任务需要创建多个线程。对于完全依赖 CPU 的程序而言,添加线程只会拖慢程序。如果是完全或部分依赖 I/O 的程序,添加线程可能会有帮助,但需要权衡添加线程带来的开销和增加的工作量。我们知道基础硬件(CPU 和内存资源)将限制此吞吐量。如果我们将线程数增加到某个限度(如核心数或线程单元数),通过增加内存占用(可能降低缓存使用率)或增加上下文切换次数,性能会恶化。如果我们发现交易系统的延迟受到影响且内存使用大幅增加,则需要监控线程数。
C++从微秒级延迟的追求为主题,我们在第 8 章中解释过,优化高频交易软件的经验法则就是要了解瓶颈所在。比如,如果一个算法容易受到多个 CPU 之间缓存争用的影响,就会严重影响线程的性能。在使用大量 CPU 运行高度并行化的算法时,缓存争用是一个非常关键的考虑因素。线程数量没有一个固定的魔法数字,目标是在实现最佳吞吐量的同时尽可能减少线程数量。
使用 Java 分析器将显示线程数量并列出将在交易系统中使用的 GC 线程。这种图形工具具有许多可能对发现正在影响线程工作的对象或数据结构的瓶颈点很有用的特性。在使用这个工具时,请记住任何分析器都可能会有干扰作用。有更简单的替代方法来查询应用程序中的线程数量;对于基于 Linux 的操作系统(OS),最好的是 htop (https://htop.dev/)。它将立即为您提供 Java 进程中正在运行的线程数量的视图。对于基于 Linux 的操作系统和其他有用的命令行工具,发送 kill -3 pid 命令将强制 JVM 转储您程序中所有线程的列表,以及它们在调用时正在执行的内容。这是一个很有用的工具,可用于诊断被阻塞的线程或意外行为。
使用线程池/队列
在 Java 中,线程被映射到系统级线程,而这些线程是操作系统的资源。创建过多的线程会影响性能,降低缓存效率,并且这些资源会很快耗尽。因为操作系统将处理这些线程的调度,所以很容易得出结论,这些线程将有较少的时间来实际工作。
线程池的目标是帮助资源和包含某种容量内的并行性。使用线程池时,我们编写并发代码,在提交任务时将并行调用。线程池将通过重用线程来执行这些任务。我们不会支付线程创建成本,并且我们能够限制线程数量。图 9.7 表示代码(提交者)将任务提交到线程池。线程池将处理这些任务。我们可以看到,线程数量很重要,因为它将增加这些任务的并发执行。
图 9.7 - 使用线程池的任务 保持两个不同线程池的良好做法:一个负责运行短期任务的较小线程池,一个负责长任务(如 I/O 访问、数据库和日志记录)的较大线程池,后者可能需要数秒,甚至用于永久旋转循环。
在 Java 中还有调度程序的概念,您可以调度任务在特定时间或固定间隔运行。调度程序通常在自己的线程上运行,当被触发时,会将任务交付给线程池。将任务添加到调度程序时,指定要分配的池很有帮助。您也可以直接在调度程序线程中指定几微秒内运行的任务。一个典型的示例是触发的任务在无锁事件队列中添加事件。无需在外部中介线程中处理添加逻辑。
我们现在将探讨可以帮助创建线程池的不同类型的执行程序。
执行器,执行器,和执行服务
执行人含有创建预配置线程池实例的方法。我们使用它们与 Java 中不同的线程池实现进行交互。ExecutorService 接口包括许多用于控制任务进度和管理服务终止的方法。我们可以使用该接口提交任务进行执行,并使用返回的 Future 实例管理它们的执行。
线程地图
在高频交易系统中,总有一个热路径,逻辑应该绑定到一个特定的循环线程,并将该线程固定到操作系统的一个内核上。如果一个线程不足以处理数据量,我们可以启动其他旋转线程,并将数据的一个子集分配给每个线程处理。旋转线程永远不应访问阻塞 I/O 或避免任何操作系统调用或锁定。操作系统调用可能会被来自系统上其他线程或操作系统本身的其他操作系统调用阻塞。
当涉及到线程亲和性时,我们还需要注意 CPU 架构:核心数量、非统一内存访问(NUMA)域和网卡核心绑定。如果线程需要通过共享内存或网络流量与其他线程或应用程序进行通信,它们应该全部运行在同一个 NUMA 域上。当进程运行在同一个 NUMA 域上时,它们将共享同一个内存缓存,并避免跨 NUMA 边界,这将影响性能。我们还想在操作系统级别隔离核心,这将防止操作系统在保留的核心上调度任何其他进程。在基于 Linux 的操作系统上,这是通过 isolcpu 功能完成的。它将排除指定的核心,不让操作系统访问。
实用线程(日志、定时器、数据库和其他)不需要绑定到特定的内核,只需确保它们不会在与操作系统共享的内核上运行。
这个库将允许您直接从 Java 将线程固定到特定的内核。它是一个非常出色的工具,具有简单的 API,让您在启动线程时获取一个内核。我们有多种选择,您可以要求获取套接字上的下一个可用内核,锁定特定的内核编号,或者预先分配一组内核,并按获取顺序将它们分配。
让我们看一个例子:
IRunnable command = new IRunnable() {
@Override
public void run() {
Affinity.acquireLock(true); //
Acquire the next free lock from the preallocated list
s_log.info("Cpu Lock:" + Affinity.getCpu());
s_log.info("On Next Core The assignment of CPUs is\n: "
+ AffinityLock.dumpLocks());
while (true) {
processInternalEvent();
}
}
};
基于 Linux 的操作系统提供另一种替代方案:我们可以使用 htop(请参见前面的引用)手动为每个线程分配一个内核。这不理想,因为这是一个手动过程,每次进程重新启动时都需要重新映射。这种解决方案的优点是核心分配并非最终,因此您可以将线程移动到不同的核心。这也是一种快速测试应用程序在绑定线程到核心时可能实现的性能改善或变化的方法。
任务集 (https://man7.org/linux/man-pages/man1/taskset.1.html) 在基于 Linux 的操作系统上也可用于限制 Java 应用程序空闲线程允许运行的内核。这将应用于主线程和所有未绑定到内核的 Java 线程。
图 9.8 中的图表为最佳情况。
域 1
核心 1
核心 5
核心 2
核心 6
核心 3
核心 7
核心 4
核心 8
Core 1 Core 5
Core 2 Core 6
Core 3 Core 7
Core 4 Core 8 | Core 1 | Core 5 |
| :--- | :--- |
| Core 2 | Core 6 |
| Core 3 | Core 7 |
| Core 4 | Core 8 |
域 2
核心 9
核心 13
核心 10
核心 14
核心 11
核心 15
核心 12
核心 16
Core 9 Core 13
Core 10 Core 14
Core 11 Core 15
Core 12 Core 16 | Core 9 | Core 13 |
| :--- | :--- |
| Core 10 | Core 14 |
| Core 11 | Core 15 |
| Core 12 | Core 16 |
孤立核心
有界核心 核心 5 = 网卡 核心
6
=
6
=
6= 6= 数据库/日志记录 内核 7 = 从池中释放线程
8
=
8
=
8= 8= 从池中释放线程
图 9.8 - 执行并行程序的理想场景 我们可以在一个 NUMA 域上运行整个程序。如果我们使用 tasket --cpu-list 6-8 启动 Java 应用程序,主线程和所有非绑定线程将被限制在 CPU 核心 6、7 和 8 上运行,保持它们在同一个 NUMA 域上。
在高频交易中,性能的主要组成部分是数据结构。我们现在将研究在 Java 中应该使用哪种数据结构来构建高频交易系统。
要实现高性能,任何交易系统都必须有并行工作的进程和线程。同时处理多个操作是节省时间的一种方式。进程之间的通信在速度和复杂性方面非常具有挑战性。正如我们在第 6 章中所看到的,HFT 优化 - 架构和操作系统,很容易创建一个共享内存段并在它们之间共享数据。我们知道,对数据访问的同步存在问题,因为共享内存没有任何同步机制。使用锁的诱惑很大,但这种对象会影响性能。
在这一部分,我们将对我们在 HFT 中使用的不同 Java 数据结构进行回顾。第一个是最简单的:队列。
队列
队列通常在头部和尾部存在写争用,且具有可变大小。它们要么已满,要么为空,但从未在写入数量等同于读取数量的中间状态下运作。它们使用锁来处理写争用。锁会导致内核中的上下文切换。由于任何上下文切换都会保存和加载给定进程的本地内存,缓存将丢失数据。为了最佳利用缓存,我们需要有一个内核在写入。然而,如果有两个独立的线程在写入,每个内核都将使另一个的缓存行失效。
在第 6 章, HFT 优化 - 架构和操作系统, 在使用无锁数据结构一节中, 我们展示了无锁数据结构是 HFT 的最佳选择。环形缓冲区或环缓冲区使进程或线程能够在不使用任何锁的情况下传输数据。我们首先要谈谈环形缓冲区。
环形缓冲区
环形缓冲区,环形队列,环形缓冲器
环形缓冲区,顾名思义,主要用作队列。它有读取和写入位置,消费者和生产者分别利用它们。当读取或写入索引达到底层数组的末尾时,会被重置为 0。问题出现在读取者慢于写入者的情况下。我们有两个选择:第一是在不再需要数据时覆盖数据,或者阻止写入者写入(可能会有写入缓冲区)。
增加循环缓冲区大小是可行的。但是,只有在读/写被阻塞的情况下才能执行此操作。在这种情况下,调整大小需要将所有元素移动到一个新分配的更大的数组中。这个操作非常昂贵和缓慢。它不能在关键路径上执行。
一个环形缓冲区的实现是由 LMAX 制造的,被称为 LMAX disruptor。
低延迟最大值断路器
撕裂器使用环形缓冲区。所有事件都以广播(多播)的方式传送到所有消费者,通过独立的下游队列并行消费。撕裂器类似于一个队列的多播图形,生产者将项目发送到所有消费者,通过独立的下游队列并发消费。消费者可以是一个事件处理程序链,让您为每个消费的消息运行多个处理程序。当我们仔细检查队列网络时,我们可以看到它实际上是一个称为环缓冲区的单一数据结构。它需要同步消费者之间的依赖关系,因为它们都是同时读取的。
生产者和消费者使用序列计数器来识别他们正在处理的缓冲区中的时隙。每个生产者/消费者都可以创建自己的序列计数器,但他们可以读取其他生产者/消费者的序列计数器。生产者和消费者检查计数器以确认他们希望写入的时隙未被锁定。
干扰器旨在解决 Java 进程中的内部延迟问题。它不是永久存储;消息保留在内存中,它的主要目的是减少两个或更多工作线程(作为生产者或消费者)之间的延迟。它比使用 ArrayBlockingQueue(这是 Java 中用于此目的的标准线程安全类)更快。如果我们使用高吞吐量系统,同时也必须确保每条消息都能尽快传递,那么这个库就会变得更有吸引力。 干扰器是一种无锁数据结构。其优点与我们在第 6 章"高频交易优化 - 架构和操作系统"的"使用无锁数据结构"部分中描述的一样。就 Java 而言,与 LinkedList 相比,干扰器确保元素存储在一个连续的内存块中,并且元素被预取/加载到本地 CPU 缓存中。内存中的每个逻辑项都被物理分配到下一个项,并且在需要之前就被缓存了。与 LinkedList 相比,分配时值会广泛分散在堆内存中,导致宝贵的 CPU 缓存命中率下降。还预先分配了一定数量的容器对象到环形缓冲区。
这些容器是引用类型或对象,它们将驻留在堆上,但一旦缓冲区空间被回收,它们将被重复使用。由于这种持续的重复使用,容器可以无限期持续,并且不会受到 GC 的暂停的影响。为了避免间接分配到堆,我们必须记住,这些对象上的嵌套值将被保留为原始值。
这部分研究了 LMAX 扰乱器;我们现在将审查 conversant 扰乱器,另一个无锁数据结构的候选者。
沟通者颠覆者
我们首先将讨论一种会话破坏器的设计。这种破坏器的底层数据结构是环缓冲区和队列。主要优势在于使用 CAS 比较以批处理的方式刷新整个队列。
对话双方并不需要任何特殊代码;它基于 Java BlockingQueue 接口,并且不是特定于某个领域的。它也可以在无需预先分配内存的情况下工作。在多线程版本中,推送-拉取一个对象的性能为 20 纳秒,而推送-拉取变体只需 5 纳秒。这种内在的快速性主要源于机械同理心和简单性。该实现考虑了硬件的特性。
对话中断器与 LMAX 中断器不同,并非 LMAX 中断器的分支。它们在设计和实现上本质不同,但功能相似。如果使用领域模型,采用 LMAX 中断器的实现是有意义的。对于将 Java BlockingQueue 接口作为应用程序核心的基本数据结构的典型用例来说,对话中断器是理想选择。
阿格罗纳循环缓冲区
这是由 Agrona 项目(https://aeroncookbook.com/agrona/concurrent/)提供的另一个可用解决方案。与 LMAX disruptor 类似,它使用了特定的接口,我们不能简单地用其替换实现 Java 队列接口的类。它提供了单生产者-单消费者(OneToOneRingBuffer)或多生产者-单消费者(ManyToOneRingBuffer)解决方案。它为空闲策略提供了多种选择,让您可以更好地控制读取器和写入器线程的行为。我们回顾了在 Java 中可以使用的不同无锁数据结构。我们现在将讨论本章的最后一部分,即与 Java 编程相关的日志记录和数据库访问。
记录和数据库访问
正如我们在第 7 章中解释的那样,高频交易优化 - 日志记录,性能和网络连接中所述,日志记录对任何交易系统都至关重要。它使用户能够调试策略,提高收益,比较理论和实际利润和损失,并将信息存储在数据库中。它始终需要昂贵的输入/输出。因此,不能在关键路径上进行日志记录。像 C++一样,需要一种特定的技术来实现 Java 中的性能。
在创建日志时,它可以将交易存储在数据库中或在平面文件中构建字符串。在生成要推送到日志系统的字符串时,创建字符串是一个非常耗时的构造过程。考虑系统的速度和对象的反应速度至关重要。例如,
log
4
j
log
4
j
log 4j \log 4 j zeroGC 是一个零对象创建日志框架,但它将在将其放入记录器线程队列(在本例中为 LMAX 的 disruptor)之前生成日志消息。您的主应用程序线程需要生成新闻,这将对性能产生成本。尽管如此,这仍然是 HFT 系统可接受的解决方案。
您可以开发自己的日志框架;我们仍然可以使用 disruptor,但将未格式化的日志消息存储在 disruptor 队列中,然后在消费者线程中执行字符串创建。您需要将对象添加到队列中,但将格式化的日志写入 appender 可以在非关键线程中使用缓冲区完成,而不需要生成字符串对象。
我们可以找到许多框架,包括 java.util.logging、log4j、logback 和 SLF4J。如果我们正在构建一个微服务或另一个拥有完整执行环境控制的应用程序,我们将负责选择一个日志记录框架。更改日志记录框架通常像搜索和替换一样简单,在最坏的情况下,可以在更强大的 IDE 中进行更复杂的结构化搜索和替换。如果日志记录速度很重要,这在高频交易中是如此,日志记录 API 将始终更好。
选择一个框架将意味着进行一些基准测试。在比较不同的框架时,我们需要记住,java.util.logging 的理念是,日志记录应该是罕见而不是规则,而
log
4
j
2
.
x
log
4
j
2
.
x
log 4j2.x \log 4 j 2 . x 和 logback 则设计了永久性的日志记录。
Java 实用程序日志记录的性能低于日志 API 框架,因为没有缓冲管理,这严重影响了日志记录速度。配置日志时,缓冲文件 I/O 对性能有最大影响。激活缓冲处理的缺点是,如果记录日志的过程失败,数据可能不会推送到磁盘。缓冲的另一个问题是,可能会使实时监控系统日志文件变得更加困难。如果各种日志框架都提供了在刷新之前的最长持续时间的保证,那就会很有帮助。每 100 毫秒刷新一次不会像每次记录日志后都刷新那样对系统性能产生太大影响,而且也能让人们实时关注日志。
一旦我们找到最好的日志基础设施,我们需要考虑将数据存储在哪里。我们可以使用系统日志框架来存储平面文件或数据库。
我们可以审视这些不同媒体的优势:
这对开发和测试环境很有利,但会影响生产环境的性能,因为 I/O 的数量会增加。如果需要使用文件,我们建议使用内存映射文件,这种方式速度极快但不会持久存储。
数据库:容易检索和查询但速度较慢。与数据库的交互会创建许多对象。可以将其发送在线路上(UDP 或共享内存)并由外部进程进行日志记录。缺点是需要确保远程进程持续运行。
系统日志:让您可以通过 UDP 套接字发送数据,速度快且内存使用量低。我们可以用多种方式配置系统日志。日志可以本地保存,也可以发送到中央服务器(https://github.com/syslog-ng/syslog-ng)。
让我们看看选择哪个过程来进行日志记录和数据库交互。
外部或内部螺纹?
当涉及到日志记录和数据库交互时,我们有两种选择:在主程序中使用专用线程保持逻辑,或者使用简单快速的通信技术(UDP 或共享内存)将信息发送到可靠的流程。
如果我们选择在主程序中使用逻辑,我们会增加可靠性,但同时也会因对象创建而给垃圾回收带来更大压力。
如果我们依赖于专门的外部进程,我们就有风险通过不安全的方式发送信息,因为我们无法保证没有人在另一端监听。好处是你的主程序不需要担心来自日志或数据库框架的对象创建。我们可以将所有信息从主程序以字节编码的消息形式发送,这可以零对象创建完成。现在我们可以自由地在专门的节点上编码,不必担心垃圾回收过于频繁。一个优秀的库来处理所有二进制编码可以在 https://github.com/real-logic/simple-binary-encoding 找到。
对于日志记录,完全编码大型消息可能太昂贵。一个解决方案是使用不同消息的字典,并为其分配数字代码。当我们需要将信息发送到专门的进程时,可以编码代码和携带信息的值。这可以在没有任何异议的情况下进行创建,并且速度惊人。另一方面,专门的节点可以自由地将数据插入数据库或重新创建字符串到
log
log
log \log 而不必担心对象创建对 GC 的压力。
摘要
这一章展示了 JVM 缓解了软件开发人员的生活,但却阻碍了交易系统的性能。我们证明了通过了解 JVM 在底层的行为,遵循良好的编码实践,以及调整 JVM,我们可以将 Java 作为 HFT 的可靠编程语言候选。我们详细研究了如何使用 Java 测量性能。我们知道测量执行时间是代码在优化后性能提升的唯一证据。与 C++ 一样,我们引入了高性能数据结构来帮助获得高性能代码。我们将这些数据结构与线程和线程池的使用相结合。我们最后讨论了日志和数据库访问,这在 HFT 中非常重要。
C++和 Java 是高频交易(HFT)中最常用的语言。下一章将讨论另一种编程语言:Python。我们将看到 Python 如何在高频交易中使用,并使用外部库运行得很快。
在这一章中,我们将向您介绍如何在高频交易(HFT)系统中使用 Python。使用 Python 来构建 HFT 系统存在问题,因为 Python 不是为速度和低延迟而设计的。由于 Python 是最广泛使用的语言,并提供所有必要的数据分析库,因此这种语言在量化交易中被广泛采用。我们将在本章学习如何在 Python 中使用 HFT 库。
我们将涵盖以下主题:
介绍 Python
蟒蛇在高频交易中的局限性
如何在 Python 中使用 C++库
我们将为您提供能够将任何
C
+
+
C
+
+
C++ \mathrm{C}++ 代码转换为可供 Python 使用的代码的工具。
我们将首先解释为什么我们应该使用 Python,即使在 HFT 中也是如此。
介绍 Python
这种语言用于软件开发。Python 是一种相对简单的编程语言,它是一种高级编程语言,使用类型推断,即解释型。与需要您专注于内存管理和正在编程的计算机硬件方面的 C/C++ 不同,Python 处理内部实现,如内存管理。因此,这种语言将使专注于编写交易算法变得更加容易。
这是一个灵活的语言,可以在任何领域构建应用程序。Python 已经广泛使用多年。Python 社区足够庞大,可以为您的交易策略提供许多重要的库,涵盖从数据分析、机器学习、数据提取和运行时到通信的各个方面;开源库的列表是巨大的。
Python 也包含在其他语言中看到的概念,如面向对象、函数式和动态类型在软件工程方面。Python 拥有大量的在线资源和大量的书籍,将指导您了解 Python 可能使用的每个主题。例如,您可以阅读由 Packt 出版社出版、Romano 和 Kruger 撰写的《Learn Python Programming》。在交易中,Python 不是唯一使用的语言。为了进行数据分析和构建交易模型,我们将希望利用 Python。我们将在生产代码中使用 C、C++或 Java。源代码将使用这些语言编译成可执行文件或字节码,因此程序将比 Python 或 R 快 100 倍。尽管这三种语言都比 Python 慢,但我们将使用它们来构建库。这些库将被封装,以便可以与 Python 一起使用。
蟒蛇是一种适合数据分析的语言,这使得这种语言可以适应创建交易策略。在 Packt 出版的《学习算法交易》一书中,我们描述了如何利用这种语言以及 pandas 和 NumPy 库来创建交易策略。
利用 Python 进行分析
用于数据分析的专业功能,Python 可以与 scrapy 或 beautifulsoup 进行网页抓取(数据爬取)。它拥有多种字符串库和正则表达式库,可以帮助清洗数据(数据清洁)。sklearn 和 statsmodels 库帮助开发者创建模型(数据建模)。Matplotlib 可以帮助数据可视化。
对于许多金融领域的开发者来说,Python 是首选的语言。在这一章节中,探讨 Python 这门语言是很必要的,因为它是全球使用最广泛的编程语言。提出 Python 是否能用于高频交易这个问题是合理的。然而,正如我们所看到的,由于 Python 的速度问题,使用它是很困难的。因此,我们可以考虑使用其他语言库来利用它们的速度优势。
图 10.1 表示构建交易策略的步骤:
图 10.1 - 创建交易策略的步骤 高频交易策略也是通过以下步骤制造的:
这部分不涉及任何编程,因为它只涉及获取我们将在市场上介绍的交易理念。在高频交易策略的情况下,我们可以使用统计套利策略为例。该策略将假设衍生金融产品及其相关资产收益是相关的。
在这一步骤中,我们可以开始获取市场数据来验证第 1 步。从这一刻开始,使用 Python 进行数据分析将会很有帮助。
任何数据转换,如标准化和清理数据以创建模型,都应该使用 Python、pandas 和 NumPy 库完成。
我们可以使用 scikit-learn 包来构建模型。它是一个基于 Python 的开源机器学习软件包,支持分类、回归、聚类和降维。在我们的 HFT 策略示例中,我们将能够通过使用回归模型来关联两种资产之间的收益率。
在第 5 步和第 6 步中,我们将使用收集的市场数据实施第一个模型,并对该模型进行回测。
第 7 步对于在此阶段促进交易策略中的速度需求是至关重要的。基于之前步骤中进行的数据分析,我们将能够确定进入和离开头寸所需的时间,从而制定一个有利可图的交易策略。
步骤 8 和 9 是交易策略的最终阶段,将是实际利润和预测利润之间的迭代。
从最后几个步骤来看,我们可以观察到速度很重要。我们需要了解为什么 Python 的执行速度很慢,这将无法用于交易策略的最后几个步骤。
为什么 Python 运行慢?
Python 是一种高级语言(高于 C 或
C
+
+
C
+
+
C++ \mathrm{C}++ );因此,它处理诸如内存分配、内存释放和指针之类的软件。Python 内存管理使程序员编写 Python 程序变得容易。图 10.2 描述了 Python 链。Python 代码最初被转换成 Python 字节码,内部发生字节码解释器转换,大部分内容对开发者隐藏。字节码是一种平台无关的底层编程语言。字节码编译的目的是加快源代码执行。源代码转换为字节码,然后在 Python 虚拟机中依次运行以执行操作。Python 虚拟机是一种内置功能。Python 代码在执行期间被解释,而不是编译成本机代码;因此,它稍微慢一些。
图 10.2 - Python chain Python 首先被转换为字节码。Python 虚拟机 (PVM) 然后对该字节码进行解释和执行。
Python 由于以下原因而缓慢:
与本地语言(如
C
/
C
+
+
C
/
C
+
+
C//C++ \mathrm{C} / \mathrm{C}++ )不同,Python 代码在运行时被解释,而不是在编译时转换为本地代码。Python 是一种解释性语言,这意味着我们创建的 Python 代码必须经过几个抽象步骤才能成为可执行的机器代码。
即时编译器(JIT):其他解释型语言,如 Java 字节码,比 Python 字节码运行更快,因为它们配有一个 JIT 编译器,在运行时将字节码翻译成本地代码,如第 9 章"Java 和 JVM 低延迟系统"中所述。Python 没有 JIT 编译器,因为由于该语言的动态性,创建一个 JIT 编译器是一项挑战性的任务。很难预测将为一个函数提供什么参数,这使优化变得困难。
全局解释器锁(GIL)通过要求解释器一次只运行一个线程来抑制多线程(即一个 Python 解释器实例内)。
由于我们现在了解为什么 Python 本身无法实现性能提升,我们将深入研究 Python 如何使用库。
如何在 Python 中使用库?
库帮助开发人员重复使用已创建和测试的特定功能的代码。我们通过下载针对特定平台预编译的版本并将其链接到我们的软件来使用这些库。通过这样做,我们不必重写代码,而且通过使用经过测试的代码,可以增加对我们实施的信任。库可以静态链接到软件,这增加了可执行文件的大小。它们也可以动态使用,这意味着可执行文件在启动时会加载这些库。在 Windows 上,我们可以通过动态加载库(.dll)扩展名找到这些库,而在 Linux 世界中,我们可以通过共享对象(.so)扩展名找到它们。Python 使用 import 命令使用动态库。
Python 标准模块数量很多。该库包括内置模块(用 C 编写)以及 Python 模块,前者可访问文件输入/输出等系统功能,后者提供标准化的解决方案来解决常见的编程问题。部分模块专门设计用于提高 Python 应用程序的可移植性,通过将平台特定性抽象为平台中性的应用程序编程接口(API)来实现。Python 还有许多其他有助于软件开发的库,我们在此介绍其中一些。
谷歌的 TensorFlow 库是与谷歌大脑团队合作创建的。它是一个开源的高级计算库。它也应用于深度学习和机器学习算法。它包含了许多张量操作。
马特普洛特是一个允许您绘制数字数据的库。这个库在数据分析中显示图表很有用。它通常用于设计交易策略,通过显示重要指标(如利润和损失)的可视化表示来查看它们的表现。
熊猫:对于数据科学家来说,熊猫是一个重要的库。它是一个开源的机器学习软件包,拥有一系列分析工具和可配置的高级数据结构。它简化了数据分析、处理和清理。熊猫支持排序、重新索引、迭代、连接、数据转换、可视化、聚合以及其他操作。
NumPy: Numerical Python 是该程序的名称。它是最广泛使用的库之一。它是一个著名的机器学习包,可以处理大型矩阵和多维数据。它具有内置的数学函数,可进行快速计算。NumPy 被 TensorFlow 等库内部使用,以执行各种张量运算。此库最重要的组件之一是数组接口。
科学 Python 是一个高级科学计算软件包,是开源的。这个库基于 NumPy 扩展,使用 NumPy 进行复杂的计算。数值数据代码保存在 SciPy 中,而 NumPy 支持对数组数据进行排序和索引。它也被工程师和应用开发人员广泛使用。
奇虾是一个用于从网页上抓取信息的开源工具包。它允许快速的网页抓取以及高级的屏幕抓取。它也适合于数据挖掘和自动化数据测试。
科学学习是一个著名的 Python 工具包,用于处理大量数据。科学学习是一个开源的机器学习库。它支持广泛的监督和无监督方法,如线性回归、分类和聚类。 NumPy 和 SciPy 通常与该程序包一起使用。
我们现在知道 Python 可以高效地与库协作。让我们现在解释 C++HFT 库如何与 Python 协作。
Python 和 C++用于高频交易
在之前的部分中我们展示了,Python 太慢而无法满足高频交易的需求。C++则要快得多,是获得低延迟的首选语言。本节我们将介绍如何将这两种语言集成在一起,将两个世界统一起来。一方面,Python 为开发者提供了便利和灵活性,另一方面,
C
+
+
C
+
+
C++ \mathrm{C}++ 使代码能够实现高性能和低延迟。在高频交易中,我们需要由定量研究人员和程序员构建用于生产环境的高频交易策略。拥有一个可以使用 C++库的 Python 生态系统将使策略师专注于研究并在生产环境中部署代码,而无需其他资源。我们将解释如何为不同的 C/C++库提供标准接口。这些 C/C++库将成为 Python 模块,换句话说,我们会在需要时将它们作为动态库加载到内存中。
让我们先谈谈动机。
在 Python 中使用 C++
我们希望使用 C++和 Python,原因如下:
我们已经拥有一个庞大、经过广泛测试且可靠的 C++库,我们希望在 Python 中使用它。这可能是一个通信库,或者是帮助实现特定项目目标(如高频交易)的库。
我们想将 Python 代码的关键部分转移到 C++ 中以提高执行速度。C++ 具有更快的执行速度,同时也让我们避免了 Python GIL 的限制。
我们希望利用 Python 测试工具来广泛测试他们的系统。
我们现在知道使用 C++ 的动机,我们将解释如何在下一步实现它。
使用 Python 与 C++
使用 Python 和 C++的两种主要方式:
使用 C++库扩展功能需要使用 import 命令。我们将提供带有 Python 接口的
C
+
+
C
+
+
C++ \mathrm{C}++ 库。函数原型在 Python 中,实现在 C++中。这等同于创建一个动态加载的共享
C
+
+
C
+
+
C++ \mathrm{C}++ 库,将在软件启动时加载。这个库将在代码的关键部分使用。
嵌入是一种技术,用户运行一个调用 Python 解释器作为库过程的 C++应用程序。这相当于为现有程序添加脚本功能。嵌入是在程序启动后向 C 或 C++程序添加调用的过程,以设置 Python 解释器并在稍后调用 Python 代码。
扩展模块是频繁使用高性能代码的最佳方法。创建 C++库并使用它将是我们提出的代码解决方案。嵌入需要比简单扩展更多的努力。与嵌入相比,扩展为我们提供了更多的力量和自由。当我们嵌入时,许多有价值的 Python 工具和自动化技术变得更加困难,甚至不可能使用。
一个对象到另一个对象的映射称为绑定。绑定用于将一种语言与另一种语言链接起来。例如,如果我们在 C 或
C
+
+
C
+
+
C++ \mathrm{C}++ 中创建一个库,我们可以将该库与 Python 一起使用。这些库的修改需要重新编译它们。
当现有的 C 或
C
+
+
C
+
+
C++ \mathrm{C}++ 库被设计用于特定目的时,需要从 Python 中使用它们时,就会使用 Python 绑定。
要了解为什么需要 Python 绑定,请考虑 Python 和 C++如何存储数据以及这可能产生的问题。C 或
C
+
+
C
+
+
C++ \mathrm{C}++ 以最小可行格式在内存中保存数据。如果使用 uint
8
_
t
8
_
t
8_t 8 \_t ,则存储数据所需的空间为 8 位,如果未考虑结构填充。
在另一方面,Python 使用在堆中分配的对象。在 Python 中,整数是大整数,其大小取决于它们的数据。这意味着对于每个跨边界传输的整数,绑定都会将 C 整数转换为 Python 整数。
在将数据从 Python 传输到 C 或反之而言时,Python 绑定执行一种类似于编组的方法,通过将对象的内存表示形式改变为可接受的数据格式来进行存储或传输。
一窗工具库
这个库允许您将 Python 和 C++结合起来。它使您能够在 Python 中使用 C++对象和函数,反之亦然,而无需使用 C++编译器以外的任何其他工具。您无需修改 C++代码。该库旨在封装 C++API 而不会产生干扰。
为了说明这个工具的工作原理,我们将使用这个示例。这个示例说明了如何使用 C++编译器编译 add 函数以及如何在 Python 代码中使用它。让我们假设我们想使用定义如下的 C++add 函数:
int add(int x, int y)
{
return x+y;
}
这个函数可以通过编写 Boost.Python 包装器暴露给 Python。
#include <boost/python.hpp>
BOOST_PYTHON_MODULE (math_ext)
{
using namespace boost::python;
def("add", add);
}
我们将使用前面的代码创建一个共享库,生成一个.dll 或.so 文件。我们将能够通过使用 import 命令在 Python 中使用这个库。
>>> import math_ext
>>> print math_ext.add(1,2)
3
正如前面的示例所示,Boost.Python 库非常易于使用,而且功能全面。它允许我们执行 C-API 提供的几乎任何操作,但使用的是 C++。有了这个包,我们不必编写 C-API 代码,当我们绑定代码时,它要么完美编译,要么失败。
如果我们已经有一个 C++库可以绑定,这无疑是当前可用的最可接受的选择之一。但是,当我们只需要重建一个简单的 C/C++代码时,建议使用 Cython。
下列输入为:Cython
使用 Cython 编程语言是一种 Python 超级集合,可以让程序员运行
C
/
C
+
+
C
/
C
+
+
C//C++ \mathrm{C} / \mathrm{C}++ 功能并在变量和类属性上声明
C
/
C
+
+
C
/
C
+
+
C//C++ \mathrm{C} / \mathrm{C}++ 类型。这样可以让编译器从高效的 Cython 代码生成 C 代码。C 代码只生成一次,然后用所有重要的
C
/
C
+
+
C
/
C
+
+
C//C++ \mathrm{C} / \mathrm{C}++ 编译器编译。从技术上讲,我们在.pyx 文件中编写代码,这些文件被翻译成 C 代码,然后编译成 CPython 模块。Cython 代码可以类似于标准 Python(而纯 Python 文件是有效的.pyx Cython 文件),但它包含额外的信息,如变量类型。Cython 可以通过使用这种可选类型创建更快的 C 代码。Cython 文件中的代码可以调用纯 Python 函数和 C 及 C++函数(以及 C++方法)。
我们将说明如何将 add 函数转换为更优化的函数。该函数将由 C/C++编译器编译,然后我们将在常规 Python 代码中使用该函数。
让我们重复使用我们之前用 add 函数的例子,并将此代码保存为 add.pyx。
def add(a,b):
return a+b
add(3,4)
然后,我们创建了setup.py 文件,它就像一个构建自动化工具,如 Makefile 一样:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("add.pyx")
)
我们将使用命令行构建 Cython 文件:
$ python setup.py build_ext --inplace
它将在 Unix 中创建 add.so 文件,或在 Windows 中创建 add.pyd。 我们现在可以使用 import 命令使用这个文件。
这是一个如何基于 Python 代码编译 C/C++代码的示例。 当为 C 或
C
+
+
C
+
+
C++ \mathrm{C}++ 设计 Python 绑定时,Cython 是一个非常复杂的工具,可能为您提供大量的功能。它提供了一种类似 Python 的技术来构建手动管理 GIL 的代码,这可能大大加快某些问题的速度,但我们并没有在这里完全探讨。然而,由于这种类似 Python 的语言并非完全是 Python,因此需要一些学习成本来了解 C 和 Python 的哪些部分应该在何处使用。
使用 ctypes/CFFI 加速 Python 代码
用于创建 Python 绑定的标准库中的工具。 使用这个工具,我们加载 C 库,并在 Python 程序中调用该函数。 要在 ctypes 中创建 Python 绑定,我们可以按照以下步骤操作:
加载您的库。
包装输入参数。
返回类型 ctypes。
事实上,ctypes 是标准库的一部分,使它在以前的工具中具有重要优势。它也不需要进一步的步骤,因为 Python 应用程序处理了一切。此外,所采用的原理很简单。然而,缺乏自动化使越来越复杂的工作更加复杂,这与我们之前看到的工具不同。
C 外国函数接口为 Python 称为 CFFI。Python 绑定是使用更自动化的方法创建的。我们使用 CFFI 以各种方式设计和利用 Python 绑定。可以选择两种模式:
API 与 ABI:API 模式使用 C 编译器生成完整的 Python 模块,而应用程序二进制接口(ABI)模式直接导入共享库并与之交互。在不使用编译器的情况下正确获取结构和参数是一项挑战,手册中强烈建议使用 API 模式。
外联与内联:这两种模式之间的差异是速度与便利性的权衡。
Python 绑定在内联模式下每次运行脚本时都会被编译。这很有用,因为它消除了第二个构建阶段的需求;然而,它会降低软件的速度。
出现线外模式需要一个额外的阶段,在这个阶段中,Python 绑定被生成一次,然后在每次执行应用程序时使用。这要快得多,尽管这可能不是一个因素。
相比 CFFI,ctypes 似乎需要较少的工作量。尽管这对于简单的用例来说是正确的,但由于大部分函数包装都是自动完成的,CFFI 在更重要的项目中的扩展性要远远好于 ctypes。使用 CFFI 的用户体验也有很大不同。你可以使用 ctypes 将一个现有的 C 库导入到 Python 应用程序中。另一方面,CFFI 会生成一个新的 Python 模块,可以像任何其他 Python 模块一样加载。
简单的外包装
简化包装器和界面生成器(SWIG)与之前列出的其他工具完全不同。它是一个全面的工具,用于为各种语言创建 C 和代码绑定,而不仅仅是 Python。在某些应用程序中,开发多种语言绑定的能力可能非常有价值。当然,这也增加了复杂性。SWIG 的配置文件相当繁琐,需要一些时间才能正确设置。
我们用下面的代码说明 SWIG 的使用。假设我们有一些 C 函数想要添加到 Python 中:
/* File : math.c */
int add_1(int n) {
return n+1;
}
int add(int n, int m) {
return n+m;
}
关于 SWIG 的配置文件由一个接口文件构成。我们现在来介绍这个接口。
编写接口文件
使用这种方法,我们编写一个接口文件,SWIG 将使用它。 这是界面文件示例
/* example.i */
%module math
%{
/* Put header files here or function declarations like below
*/
extern int add_1(int n);
extern int add(int n, int m);
%}
extern int add_1(int n);
extern int add(int n, int m);
我们将在以下部分构建一个 Python 模块。
构建一个 Python 模块
将 C 代码转换为 Python 模块的最后一步如下:
unix % swig -python math.i
unix % gcc -c math.c math_wrap.c \
-I/usr/local/include/python3 . 7
unix % ld -shared example.o example_wrap.o -o _example.so
我们将使用导入命令来使用这个模块:
>>> import math
>>> math.add_1(5)
6
>>> math.add(5,2)
7
我们可以谈论在 Python 中使用 C/C++的许多其他方式。这部分旨在建立一个全面的列表,列出将 C++库迁移到 Python 使用的所有方式,但解决 Python 速度问题,使用高速库。
提高 Python 代码在高频交易中的速度
我们在前几章中定义的关键组件必须以高速运行。使用我们之前描述的任何工具都将帮助您创建 C/C++类似的代码并使用库创建高性能的 Python 代码。在利用 NumPy 和 SciPy 的向量化编程习惯的同时,开始构建新的 Python 算法是至关重要的,以避免循环代码。实际上,这意味着尝试用对应的 NumPy 数组函数来替换任何嵌套的 for 循环。目的是为了防止 CPU 浪费时间在 Python 解释器上,而不是为交易策略进行数值计算。
但是,在某些情况下,算法无法通过简单的向量化 NumPy 代码高效地表达。在这种情况下,建议使用以下方法:
找出 Python 实现中的主要瓶颈,并将其隔离在专用的模块级函数中。
如果有一个小而精心维护的
C
/
C
+
+
C
/
C
+
+
C//C++ \mathrm{C} / \mathrm{C}++ 版本的相同算法,您可以为其开发一个 Cython 包装器,并包含该库的源代码副本。或者您可以使用我们之前讨论过的其他任何技术。
将 Python 函数的版本放在测试中,并使用它来确保构建扩展的结果与易于调试的 Python 版本相匹配。
检查是否可以通过利用 joblib.Parallel 类为多处理创建粗粒度并行性,一旦代码已经优化(不是简单的瓶颈可以通过分析发现)。
图 10.3 描述了需要低延迟功能进行高频交易时对
C
+
+
C
+
+
C++ \mathrm{C}++ 函数的调用。
图 10.3 - Python 和 C++的交互 用于决定清算头寸的控制代码可以用 Python 计算。C++将负责使用这些库的速度执行清算。
关键组件包括以下内容:
这些都应该在
C
/
C
+
+
C
/
C
+
+
C//C++ \mathrm{C} / \mathrm{C}++ 或 Cython 中实现。 一些公司拥有编译器工程师,他们会投资于从 Python 生成
C
+
+
C
+
+
C++ \mathrm{C}++ 代码。在这种情况下,我们将有一个工具来解析 Python 代码并生成
C
+
+
C
+
+
C++ \mathrm{C}++ 代码。这个 C++代码将像其他任何代码一样被编译和运行,并且 Python 代码通常可用于编码交易策略。负责创建交易策略的大多数人拥有比 C++知识更多的 Python 技能。因此,对他们来说,完全在 Python 中开发要比将 Python 转换为 C++库(用于 C++交易系统)更容易。
在本节中,我们学习了如何使用 C++来改善 Python 代码。我们现在将总结本章内容。
摘要
在本章中,我们说明了如何在 HFT 中使用 Python。Python 不是性能和低延迟的语言,我们强调了如何使用其他语言(如 C++)达到相同的性能水平。您现在能够在您的 Python 代码中使用任何 HFT 专用的 C/C++代码。
我们现在将通过在下一章开启一些新主题来完成这本书,例如实现每笔交易低于 500 纳秒的延迟时间,并将高频交易系统拓展到加密货币。
高频 FPGA 和加密
欢迎来到本书的最后一章。在前几章中,我们了解了如何优化传统交易以获得一个高频交易(HFT)系统,其 tick-to-trade 延迟为 5 微秒。在下一节中,我们将讨论如何使用先进的硬件优化将该延迟改善到 500 纳秒。最后,我们将探讨传统交易与加密货币交易之间的差异,以此结束本书。
我们在这一章的目标是展示我们在前几章中使用的软件解决方案在实现低于 1 微秒的延迟方面存在局限性。使用一种特定的硬件,我们将向您展示这是可能的。第二个目标是将本书中解释的优化应用于加密货币。我们将通过将设计扩展到云端来阐述这一点。
在本章中,我们将涵盖以下主题:
现场可编程门阵列(FPGA)硬件可以用于高频交易(HFT),以减少延迟
如何使用高频交易技术交易加密货币
如何在云端构建交易系统
重要说明
为了指导您在所有优化措施中,您可以参考以下图标列表,这些图标代表了一组可以降低特定微秒数延迟的优化措施:
二低于 20 微秒
L低于 5 微秒 紧 低于 500 纳秒 您将在本章标题中找到这些图标。
降低 FPGA 的延迟
现场可编程门阵列(FPGA), 高频交易(HFT)激烈竞争的演化, 在现代 HFT 中使用 FPGA 的动机, FPGA 的工作原理, FPGA 交易系统的设计, 以及在 HFT 系统中使用 FPGA 的优缺点。
高频交易中激烈竞争速度的演变
高频交易(HFT)在这本书中受到了广泛关注,变得极其流行,也成为所有金融市场流动性和交易的重要组成部分。我们还看到,正如名称所暗示的,高频交易完全围绕着速度/延迟——即高频交易系统和算法分析市场数据信息、发送订单请求和执行交易的速度。
数据包从一点到另一点的总旅行时间。但是,在交易中,延迟指从参与者收到市场更新到他们能将订单发送到交易所所需的时间(纳秒/微秒/毫秒)。提高技术是在价格变动之前以最佳价格获胜交易的关键,或在其他市场参与者以成本进行交易之前,或在该价格的订单被取消之前。这对所有市场参与者来说都是如此,包括手动交易者、做市商、统计套利交易者或高频交易者。
高频交易中,速度是关键因素。参与者的系统越快速地处理和执行交易,获得的利润就越高。所以,这个领域存在着无休止的激烈竞争,竞争对手不断投入大量资金,开发更强大和更快速的交易解决方案,做到几纳秒内交易证券/衍生品/股票/金融工具。除了通过提高高频交易系统的速度来提高利润,那些跟不上持续技术创新而落后的公司将无法与之竞争,可能会倒闭。
为跟上步伐,投资银行、对冲基金和高频交易公司在更快的软件、最靠近交易所的协定位置设置以及最低延迟的网络方面投入大量资金 - 我们在第 7 章中看到了这些内容,包括高频交易优化 - 日志记录、性能和网络。参与者在优化硬件本身时会购买最快的服务器、处理器、内存和网卡。但即便如此也不再够用了 - 现在他们需要投资基于硬件加速的解决方案。硬件加速的方法是将交易系统中计算/CPU 密集型的部分卸载到定制处理器、图形处理单元(GPU)或现场可编程门阵列(FPGA)上。有许多解决硬件计算性能扩展的方案。但事实上,就高频交易系统和算法而言,FPGA 正在引领技术革命。FPGA 拥有令人兴奋且特定的特性,使其能以比即使是最高度优化的软件解决方案快上千倍的速度执行相对简单的交易策略/算法。
现场可编程门阵列
在本节中,我们将介绍 FPGA,然后研究 FPGA 的组件细节及其特性。FPGA 是可编程硬件(在本例中为芯片),虽然编程并不容易,但可以根据需求进行编程。它还可以根据需要进行重新编程,并且如前所述,具有超低延迟、高性能和高能效的优点。
可编程门阵列(FPGA)在性能谱中的位置
应用特定集成电路(ASIC)
可编程门阵列(FPGA)位于中央处理器(CPU)和专用集成电路(ASIC)之间的性能范围内。
什么是 FPGA?
现场可编程门阵列(FPGA)只是一个芯片,它包含数千甚至数百万个核心逻辑模块(CLB),其中 CLB 是 Xilinx 公司的术语。可以将其与笔记本电脑和智能手机中的微处理器进行比较,它们由称为查找表(LUT)的数百万个逻辑块组成。这些逻辑块包含与、或、非和异或等布尔逻辑运算,也被称为逻辑门。此外,LUT 通过充当一个
n
n
n n 输入函数来工作,该函数被化简为单个输出。这意味着 Xilinx 的 UltraScale CLB 具有六输入 LUT 体系结构(尽管这可以被重新配置为两个五输入 LUT,每个 LUT 有独特的输出)。因此,您可以在 LUT 中实现一系列布尔运算(即六输入函数可以化简为单个 LUT)。
在 FPGA 中,CLB 可以配置和组合以处理任务,但与 CPU 相比,它们不会被额外的硬件拖累而减慢速度。FPGA 在控制流密集的操作方面并不出色,但在数据流应用程序方面非常出色。CPU 遇到的困境是,FPGA 在运行方式上是天生并行的(你同时评估许多
n
n
n n 输入函数以并行方式达到相同的结果)。不过,它们在性能方面也是有限的。如果你的逻辑功能变得太复杂,你的数据总线变得太宽(因此降低时钟速率),就会面临严重的性能损失(你可以关闭设计的综合)。
可编程逻辑门阵列(FPGA)不运行代码,而是实现逻辑电路。FPGA 允许您直接在硬件中实现特定于应用程序的功能,可能比 ASIC 慢,但比在 CPU 上执行单一代码路径更快。FPGA 可用于执行非常特定的任务并极快地执行它们,还可以并行执行任务。如交易策略等算法直接在 FPGA 上实现。
第一个 FPGA 是在 1985 年发明的,芯片容量非常小,在上面实现逻辑非常困难。现代 FPGA 拥有数百万门的计数能力,可以容纳非常复杂和大规模的设计。主要供应商包括 AMD(制造了 Xilinx)、Intel(设计了 Altera)以及 Achronix 等。
现场可编程门阵列
在本节中,我们将讨论 FPGA 的重要特征,这将帮助您理解并明确地在下一节中看到为什么高频交易公司大幅受益于使用 FPGA。
可编程性
我们已经在上一节中讨论过,FPGAs 包含逻辑块/CLBs/LUTs,通过可配置开关和灵活的结构连接在一起。这使得它们相对容易编程(和重新编程),并能够支持复杂的交易算法。
容量
现代 FPGA 装备有数百万个可编程逻辑块,提供了巨大的容量。算法不仅可以非常复杂,而且还可以进行巨大的伸缩。容量是以物理学的代价获得的;信号需要跨越整个芯片传播。从概念上讲,处理器芯片只是 CPU 处理器所在的硅半导体材料。随着硅芯片变大,信号传播时间也会增加。容量很大但并非无限。相比之下,CPU 只有几个物理因素是其对手;成功实现 FPGA 需要掌握电气工程和逻辑设计的知识。
平行性
可编程门阵列(FPGA)与中央处理器(CPU)不同,没有固定的处理器架构,也就没有操作系统(操作系统)开销和中断等。在 FPGA 上,处理路径是并行的,因此不同的功能/操作不会争夺资源,而是可以并行运行。在现代 FPGA 上,单个芯片可以同时运行 10 多个代码路径,速度不尽相同。
这种并行架构在 FPGA 上至关重要,可以以最大容量和速度执行买卖交易。这里需要注意的是,算法或数学计算需要分解为一系列任务。只有这样,它们才能在不同的周期内并行处理。
现场可编程门阵列(FPGA)具有很强的韧性和高水平的服务能力。FPGA 稳定且自包含,使整个高频交易(HFT)基础设施运行顺畅,并使基础设施对非 FPGA 硬件和软件的变更具有抗压能力。
决定论
正如第 4 章所述,HFT 系统基础-从硬件到操作系统,在执行 CPU 上的指令时,存在着可能的非确定性(乱序处理器)。此外,操作系统和事件驱动中断可能导致许多控制路径,从而产生大量随机性。CPU 擅长于随着时间推移而演化/改变的通用任务,但不擅长于保证性能指标。
基于 FPGA,我们可以实现硬件算法,这导致了高度决定性。因此,当市场活动出现暴涨,网络可能被数据过载时,FPGA 可以快速处理并提供市场数据,且方差较低。FPGA 每次都会经历相同的状态序列,从而提供可重复和可预测的延迟/性能。
我们讨论了 FPGA 的特性;我们现在将讨论 HFT 系统如何利用 FPGA。
探索 FPGA 交易系统
基于现场可编程逻辑门阵列(FPGA)的交易系统利用低延迟、高频率和算法交易策略。这些系统需要在几纳秒内执行多种不同任务。以下小节描述了这种系统需要执行的一些任务。
市场数据
专用现场可编程门阵列(FPGA)的第一个任务是分析来自多个证券交易所的市场数据。这涉及构建网络协议栈以及解决诸如以太网层、互联网协议(IP)层、用户数据报协议(UDP)层和市场数据协议等组件的问题。
网络栈
在 FPGA 设置中,网络输入/输出(I/O)是在内核和操作系统层面处理的,因此在优化交易系统时,在 FPGA 上构建网络堆栈通常是第一步。这意味着处理以太网层、IP 层、UDP 层,以及在某种程度上处理传输控制协议(TCP)层。也有机会使用混合堆栈 - 有一个 TCP 堆栈运行在主机上以应对奇特情况,而 FPGA 可以接收 TCP 市场数据并对其进行解码,直到出现这些罕见的奇特状态。在 FPGA 中直接实现 TCP 是可行的,但很困难,通常也不是一个优雅的解决方案。
主动仲裁和复制
大多数交易所为了冗余和公平的原因,通过多个渠道传播市场数据。消费市场数据的 FPGA 馈送处理器需要处理 A/B 馈送仲裁(在高频交易中,为了在接收市场数据时提供冗余,很常见有 A 和 B 两个通信通道),并处理冗余的馈送。相反,交易所或市场数据提供商也需要使用类似的 FPGA 技术来处理和分发大量的市场数据更新。
馈送分析器和正规化器
可编程门阵列(FPGA)订阅处理器需要处理不同的市场数据协议 - 流式交易适应(FAST)协议是一个非常常见的例子。即使市场数据更新语义也可能因交易所而有所不同,FPGA 市场数据解析器和标准化器需要以最优方式处理这些情况。
交易信号
在从交易所接收市场数据后处理完成,下一步是为算法计算不同的交易信号,以找到交易机会。这些交易信号发现价格和/或订单中的错误定价,看看哪里存在盈利交易机会。
低延迟执行算法
根据新的市场更新更新交易信号/模型后,我们使用输出尽可能快地向市场发送订单,以利用瞬时交易机会。算法必须极其高性能,才能击败其他订单,即在其他订单抢占同一机会之前到达交易所。
考虑
基于现场可编程门阵列(FPGA)的交易系统需要易于自定义,并且能够在处理实时更新时进行优化和测试。另一个考虑因素是将交易算法尽可能地推近网络接口卡(NIC),并将系统延迟降到最低。在这个第一部分中,我们介绍了 FPGA,它们的特性,以及它们如何用于高频交易(HFT)系统。我们现在将谈论使用 FPGA 构建的交易系统的优势。
现货 FPGA 交易系统的优势
高频交易公司使用 FPGA 的优势
更高的利润
使用现场可编程门阵列(FPGA)的高频交易(HFT)算法可以在其他参与者之前以更好的价格执行订单,从而战胜其他参与者。FPGA 算法可以在其他参与者还未反应之前检测并利用机会,以最优价格执行交易,立即获得更高的利润。FPGA 的可编程特性意味着您可以快速改变算法行为和交易参数,保持领先地位。FPGA 交易系统的可扩展性使得同时执行多种金融工具的交易成为可能,从而带来更高的收益。基于 FPGA 的交易系统还使得高频交易公司能够创造更安全的交易环境,节省开支,进而获得更多收益。
安全合规
过去几年来,交易业务和高频交易(HFT)特别是在监管和风险管理方面面临更为严格的限制。因此,HFT 公司被迫寻找新的解决方案,以监控交易活动并检测/处理潜在损失。基于传统 CPU 的系统通常仅能查看/计算投资组合风险,而不能实时提供所需的安全性水平。FPGA 可以对投资组合进行接近实时的风险评估,并使公司满足监管机构提出的严格的风险/监管要求。
更便宜的维护
依赖 FPGA 意味着高频交易业务可以减少与计算机系统维护相关的费用。一个精心设计的 FPGA 可以取代大量通用 CPU,并通过节省能源成本、办公租金和系统冷却费用帮助降低公司开支。可重编程的特性还意味着该设备可在安装后进行修改以满足不断变化的需求,从而带来更多与维护相关的成本节省。
芯片阵列技术交易系统的缺点
拥有 FPGA 为高频交易公司带来的优势,我们已经讨论过了。现在,这一部分将会探讨 FPGA 系统存在的劣势。
硬件成本
现场可编程门阵列芯片很昂贵;它们有时候比传统服务器贵得多。传统服务器在运行许多非超级性能敏感的进程方面也是一种更加经济有效的解决方案。现场可编程门阵列芯片通常都是托管在普通服务器内部,所以现场可编程门阵列芯片并不能取代传统服务器。因此,根据应用程序,如果系统没有经过深思熟虑和周密设计,现场可编程门阵列芯片可能会带来许多硬件成本。
开发和调试成本
可编程逻辑门阵列芯片比传统中央处理器更难编程算法。Verilog 语言通常比 C、C++、Java 等传统语言更难开发、调试和排查算法。可用于可编程逻辑门阵列芯片的工具和 API 也更少,而且相当有限。调试可编程逻辑门阵列芯片算法也比调试传统算法更加困难,这是由于芯片上的日志能力有限以及决策路径复杂。
另外,对于传统软件开发语言,有许多有能力的开发人员,但由于可用性较低和成本较高,找到 FPGA 开发人员更加困难。正在进行持续的努力,使开发过程本身更加简单快捷。新的工具正在出现,如高级合成工具,可将算法转换为门级设计。它们有局限性,但可支持快速上市或至少原型制作。市场上有许多这样的工具,但一些流行的例子包括 Simulink(由 Matlab 提供)、ISE、Vivado、Vitis、ChipScope(由 Xilinx 提供)和 Altera Quartus(由英特尔提供;Altera 曾是一家被英特尔收购的公司)。
添加交易风险
通常,FPGA 驱动的高频交易系统和交易算法可以减少由于软件组件故障而造成的风险。但设计或测试不当的 FPGA 可能会产生相反的影响并增加风险。这可能是由于 FPGA 系统中的 bug,只有在特定的市场条件和/或交易参数下才会触发,从而导致 FPGA 行为不正确。FPGA 系统另一个因素是它们在反应和订单执行方面极为迅速,这意味着一个失控的 FPGA 系统问题可能会在传统的风险系统检测到问题并关闭它们之前造成重大损失。在所有这些情况下,公司都会经历巨大的交易损失、监管和合规处罚/罚款,甚至可能因为巨大损失而破产。
策略复杂度限制
正如在"什么是 FPGA?"部分中提到的,FPGA 最适合设计利用 FPGA 的并行架构和确定性特性来最大化算法性能的算法。此外,为 FPGA 构建和调试算法是一个昂贵、耗时且复杂的过程。这两个因素意味着,为了最大化 FPGA 的优势,可以构建的交易策略/算法的复杂性是有限的。因此,在 FPGA 上实现大量复杂的统计信号、机器学习组件和执行行为的非常复杂的算法是不现实/可行的。
最终关于 FPGA 的话
高频交易中速度竞争的激烈局面自 2000 年以来一直存在,短期内不太可能结束。高频交易公司需要继续采用新兴技术创新来维持市场优势,否则可能会遭受损失和灭亡。
现场可编程门阵列,我们在本节中讨论过,是这些新兴技术的一个例子。并行架构和确定性特性使现场可编程门阵列能够提供处理市场数据和订单执行中的最低延迟。使用现场可编程门阵列将高频交易引擎加速到纳秒级别,可带来许多商业利益,如增加交易量和利润增加。
第 6 章,高频交易优化-架构和操作系统,以及第 7 章,高频交易优化-日志,性能和网络,我们阐述了优化硬件和软件以用于高频交易的主要指导原则。我们在其他所有章节中都看到了如何用 C++、Java 和 Python 具体应用这些优化。FPGA 完成了本书关于加快交易系统的使命。我们现在将讨论性能方面的相反情况:加密货币的高频交易。
探索加密货币的高频交易
在这个部分中,我们将描述加密货币中的高频交易与传统资产中的高频交易之间的差异。关于加密资产,有许多其他资源,如 Packt 公司的《完整的加密货币和区块链课程》(视频),由 Ravinder Deol、Rob Percival 和 Thomas Wiesner 编写(https://www.packtpub.com/product/data/9781839211096)。我们将 focus 于解释加密货币中交易的运作方式以及交易所如何构建低延迟交易。自从比特币成为加密货币市场上的重要价值以来,加密货币就进入了交易世界。越来越多的公司被加密货币交易的好处所吸引。与其他任何资产一样,对冲基金和交易公司开始为数字资产建立高频交易系统。
什么是加密货币?
加密货币是使用计算机网络软件生产的数字资产。这个计算机网络不依赖任何中央机构,如政府或银行,来维护或维持它。比特币(BTC)和其他加密货币使用区块链技术,并跟踪谁拥有什么并产生不可篡改的交易记录。这是一个完全分散的系统。
加密货币
加密货币的单个单位被称为硬币或代币。有些被用作商品或服务的交易单位,而另一些则被设计用于帮助进行更复杂金融交易的计算机网络运转。
比特币的生产方式是通过挖矿。挖矿是一个耗时的过程。这个过程涉及计算机解决复杂的难题来验证网络交易的合法性。拥有这些计算机的所有者可能会获得新创造的比特币作为奖励。其他加密货币采用不同的方法创造和分配代币,其中一些的环境影响要小得多。
加密货币交易是如何运作的?
即使我们可以使用去中心化交易来购买加密资产,中心化交易所可能是新交易者最容易接触的方式。客户更相信中心化交易所作为第三方来监管交易。这些交易所通过收取各种服务费用和以市场价格销售加密货币来赚钱。
有针对更高级交易者的去中心化交易所,费用比中心化系统更便宜。它们更难利用,需要更多的技术专业知识,但可能提供某些安全优势,因为没有任何目标供网络攻击者攻击。点对点交易是交易加密货币的另一种方式。我们已经定义了数字货币是什么,现在我们将学习它们是如何构建的。
区块链
区块链是一个分布式数据账本。它维护每个加密单元的交易历史,显示资产所有权随时间的变化。当交易记录在块中时,新块被添加到链的前端。区块链文件始终存储在网络上的多台计算机上,而不是单个位置。它通常对所有网络用户可用。由于没有任何弱点容易受到黑客攻击、人为错误或软件错误,因此它既透明又难以修改。
密码学(高等数学和计算机科学的结合)连接块。任何试图更改数据的行为都会破坏块之间的加密链接,使网络上的机器能够迅速识别出其为假冒。
加密货币挖掘
加密货币挖矿指的是检查最近的加密交易并向区块链添加新的区块。挖矿计算机从一个池中选择待定交易并验证发送者是否有足够的资金完成交易。这是通过将交易信息与区块链的交易历史进行比较来完成的。第二次检查验证发送者是否使用其私钥授权现金转账。
采掘机将合法交易组装成一个新的区块,并尝试找到一个解决方案来建立与前一个区块的加密链接。当一台机器成功生成该链接时,它将该区块保存到其区块链文件副本中,并将该变更广播给整个网络。
传统资产交易和加密货币交易之间的相似性
我们首先将着眼于这两个世界之间的主要相似之处。
加密货币的价格是由什么驱动的?
像传统市场一样,加密货币市场也由供给和需求驱动。由于它们的去中心化性质,它们可能听起来不受影响于困扰传统货币的许多经济和政治问题。然而,我们仍然可以观察到经济指标和加密资产价格之间的某些相关性。
通过对比传统资产,我们可以注意到以下变量可能对其价格产生重大影响:
流通中的硬币总量以及它们被发行和移除的速度也会影响价格。
市值是指所有流通货币的总价值,以及用户认为该价值的变化。
加密资产在媒体中的感知以及它们的受欢迎程度。
集成:与现有基础设施集成的便利性。
监管事件和安全问题可能会影响加密货币和传统资产。
所有这些参数也可能是常规股票、债券或其他衍生产品价格波动的一部分。对于加密货币特有的因素描述如下。
生产成本
采矿是创造加密货币代币的过程。加密货币的功能能力要感谢去中心化的矿工网络。该协议创造加密货币代币以及交易双方支付给矿工的任何费用。随着采矿成本的增加,加密货币的价值也必须增加。如果采矿收入不足以支付他们的开支,矿工将不会进行采矿。由于矿工是运营区块链的必需品,只要存在需求,价格就必须上涨。
密码学交易
比特币和以太币,两种流行的加密货币,在各种平台上交易。最受欢迎的代币在每个交易所都上市。另一方面,一些较小的代币可能只在少数几个交易所上市,限制了某些投资者的获取。一些钱包提供商将从许多交易所编译任何加密货币的报价,但他们将收取费用,增加投资成本。此外,如果一种加密货币在小型交易所的交易量很小,交易所收取的差价可能对某些投资者来说过于宽。更多交易所上市一种加密货币可以增加准备和有能力获取它的投资者数量,从而提高需求。当需求上升时,价格也会上涨。
市场参与者和竞争对手
有数十种不同的加密货币,每天都有新的项目和代币推出。新的竞争对手进入障碍较低。但是,开发一种有效的加密货币也需要建立加密货币用户网络。如果一个有用的区块链应用能够快速建立网络,特别是如果它解决了竞争对手服务中的缺陷。如果一个新的竞争对手获得牵引力,它会耗尽现有企业的价值,导致现有企业的价格下降,而新竞争对手的代币价格上升。
加密货币特有的治理
加密货币网络很少遵循一套刚性规则。开发人员根据从社区收到的反馈来对项目进行更改。有些代币,被称为治理代币,允许其所有者在项目未来的发展方向上投票,包括代币的挖矿和使用方式。在对代币治理进行任何修改之前,都需要利益相关方达成共识。从工作量证明转向权益证明系统将使数据中心和人们地下室里的大部分昂贵的挖矿设备过时。作为结果,加密货币的价值肯定会受到影响。另一方面,更新软件以改善协议的漫长过程可能会限制比特币价值的上升潜力。如果一项能为比特币投资者带来价值的更新需要数月才能实施,这对现有利益相关方来说是不利的。
加密货币特定监管
加密货币是证券或商品。证券交易委员会(SEC)或商品期货交易委员会(CFTC)可能最终对加密资产进行监管。目前还没有明确的方法来确定哪个机构将对这种交易类型制定规则。然而,监管将有助于这些资产的交易。交易所交易基金(ETF)和期货合约等为投资者提供了更广泛的加密货币准入途径,从而提高了它们的价值。
此外,法规可能允许投资者通过期货或期权合约做空或对加密货币价格进行投机。这应该会导致更准确的价格发现和比特币价格波动性较小。与此同时,如果政府制定了反对加密货币的规则,加密货币的价值将大幅下降。
我们可以在哪个市场交易加密货币?
在大多数零售交易平台上都可以使用相同的基本交易订单类型:
大多数去中心化交易所(DEX)目前只提供市场订单,而大多数大型中心化交易所(CEX)提供全面的订单类型(市场、限价、止损等)。随着加密货币交易生态系统的发展,预计会有更多加密货币交易所涵盖这些功能。虽然在为用户体验而设计的平台上购买和出售加密货币可能非常相似,但在处理这两种不同的资产类别时,存在众多关键差异。
加密货币交易的价差是多少?
资产在订单薄中的买入和卖出价格之差。当您在加密货币市场开仓时,我们会观察到两个价格,这与其他许多传统市场非常相似。若您想开多头仓位,则以略高于市场价格的买入价进行交易。若您想开空头仓位,则以略低于市场价格的卖出价进行交易。
在加密货币交易中,什么是杠杆?
利用杠杆是一种无需支付全额交易价值的获取大量加密货币的方法。相反,我们支付一个小额保证金。当我们平仓时,交易规模的全部幅度决定了您的利润或损失。这可能与交易类似,只是加密货币的杠杆比传统资产高得多。由于加密货币非常波动,损失的风险非常高。
保证金
在杠杆交易中,保证金是一个关键组成部分。它是指开立和维持杠杆头寸所需的初始存款。请记住,您的保证金要求将根据您的经纪商和您在保证金交易加密货币时的交易规模而有所不同。保证金以整个头寸的比例表示。
传统期货合约与永续期货合约
传统期货合约有一个非常关键的到期日,在交易过程中用于确定资产价值,合约到期时开始进行结算程序。传统期货合约通常每月或每季度结算一次,合约价格在结算时将收敛于现货价格,所有未平仓头寸都将到期。加密衍生品交易所经常提供与常规期货合约类似的永续合约。
永续期货合约具有显著优势。交易者可以无到期日持有头寸,无需跟踪不同交割月份,与传统期货合约不同。交易者可以无限期持有空头头寸,除非被强平。因此,永续交易合约和现货市场的交易对非常相似。由于永续期货合约永不到期,交易所需要一个系统来确保期货和指数价格定期收敛:资金费率。
可以在未来市场进行空头交易,但需要先在现货市场存储相关加密资产的贝塔值。例如,如果我们想要进行 BTCUSD(比特币/美元)对的空头交易,需要先购买足够的 BTC 并存放在我们的账户中,这就是我们的贝塔。一些交易所提供借贷选择以鼓励更多交易,但需支付一定利率。
资金利率
资金费率是根据永续合约市场和现货价格的差异,定期向多头或空头交易者支付的费用。交易者将根据未平仓头寸支付或获得资金。加密货币资金费率避免了两个市场之间的长期价格差异。它在一天内被多次重新计算,而且取决于交易所,它可能会更频繁地被计算。
期货市场中的资金费率旨在衡量开仓的成本。
贷款利率(借贷所需币种的成本)和溢价(期货和现货市场之间的差价)是资金利率的两个基本组成部分。一些加密货币交易所的利率固定为每日小幅百分比。同时,溢价由永续合约价格和市场价格之间的差异决定。在极端波动期间,永续合约价格和市场价格可能存在差异。在这种情况下,溢价会上升或下降。利差较大意味着溢价较大。相反,溢价较低意味着两种价格之间的差距有限。资金利率可能对收益和损失产生重大影响,因为资金计算考虑了所使用的杠杆水平。即使在低波动性市场中,支付资金的交易者也可能由于高杠杆而遭受损失和清算。
另一方面,收集资金可能非常有利可图,尤其是在区间市场中。因此,交易者可以设计交易方法来从融资费率中获利,即使在低波动性市场中。融资费率鼓励交易者采取与永续合约价格与现货市场一致的头寸。
如果永续价格高于现货价格,多头需要向空头支付。相反,如果永续价格低于现货价格,资金费率为负,空头需要向多头支付。资金费率可能成为保证金的成本或收益。这最后一部分总结了传统交易和加密货币交易之间的相似之处。我们将在下一步讨论主要差异。
主要差异在于传统资产交易和加密货币交易
以下成分是传统交易和加密货币交易之间最重要的差异。
在加密货币交易中,什么是所有权?
股票是反映对发行公司所有权(或股权)比例的证券。股票所有者通常获得投票权或以股息形式分享发行人收益。相比之下,密码货币在使用方式和预期目标方面有很大不同。
许多数字资产,如以太币(ETH)、基本注意力令牌(BAT)和唯链令牌(VET),都是实用型代币,旨在在基于区块链的生态系统内使用,并不反映发行它们的公司的法律利益。许多加密货币,如比特币和稳定币,没有与实际公司活动相关的明确用例,旨在存储价值。虽然这些资产最好被视为类似于黄金的数字商品,但它们并不代表对公司或其活动的利益。
市场准入
股票交易通常受到大多数投资者的固定营业时间的限制。加密货币市场从不关闭,使任何人都可以在任何时间采取新的头寸并进入-或退出-市场,不管他们住在哪里。加密货币市场在亚洲时区和美国时区重叠时积极交易。
什么是发行限制?
公开上市公司可能选择发行新股,但受公司内部限制和适用的当地法律的约束。另一方面,加密货币的全部供应量由发行组织的内部条例或它所基于的区块链协议代码决定,而不是由法律或政策决定。此外,加密项目可以轻松透明地对其全部硬币供应量施加明确和不可更改的限制。
交易对
不像股票,通常使用法定货币购买和出售,加密货币可以使用交易对进行购买和出售,允许两种加密货币直接交易。由于 BTC 和 ETH 是最广为交易的加密货币,大多数交易对都包括它们中的一个。如果您想将一种加密货币兑换为另一种加密货币,您几乎肯定需要将您想交易的替代币兑换为更受欢迎的加密货币,如 BTC。然后,您可以将该 BTC 兑换为您选择的加密货币。
如果您希望在交换一种低市值货币和另一种货币时避免众多步骤,那么您可以选择使用许多自动做市商(AMM)执行这些类型交易的分散交易所(DEX)之一。虽然大多数股票经纪公司提供法定货币的入金和出金通道,但并非所有加密货币交易所都允许用户存取法定货币。这意味着您可能无法在某些交易所使用法定货币购买加密资产。
在加密货币交易中,流动性是什么样的?
在交易低市值币和代币或在较小的加密货币平台买卖时,投资者可能会面临流动性不足的问题。在股票交易中,特别是在处理微型公司或场外(OTC)仙股时,流动性问题也可能出现。
加密中是否存在任何透明度?
上市公司必须保持透明度,通常通过季度财务更新、年度报告、定期股东大会和其他正式方式来告知投资者过去的业绩和预期未来收益。尽管通过证券代币发行(STO)募集资金的公司可能需要承担类似的报告义务,但加密货币项目不受公众交易公司的监管审查。
许多加密市场不要求个人项目定期共享数据,这使得投资者和行业分析师难以充分分析特定加密项目的运作情况以及其资产是否值得投资。另一方面,许多加密项目试图在社区更新和开放治理方面保持透明。透明度是加密和区块链的主要概念之一,大部分优质项目都致力于坚持这一点。
关于高频交易的书籍,我们将集中精力学习如何在加密货币交易所进行交易。
使用加密货币交易
数字货币交易所(DCE)是加密货币交易所的另一个名称。它是一个网站,允许用户将现金转换为加密货币,反之亦然。大多数交易所主要专注于帮助我们将诸如 BTC 之类的加密货币换成 ETH 或其他加密资产。
尽管大多数交易所都在线运营,但也有几个实体地点。传统的支付方式和加密货币都可以在这些交易所进行兑换。这些替代方案类似于在国际机场看到的货币兑换亭,在那里你可以将本国货币兑换为其他国家的货币。
以下是最知名的加密货币交易所类型。
集中交易所(CEXs)与传统股票交易所类似,都是集中交易模式,交易双方通过交易所作为第三方完成交易,交易所通常会收取手续费。集中化意味着需要将资金托付给加密货币行业中的第三方。高频交易(HFT)加密货币交易主要在这种类型的交易所进行。
币安、Coinbase 和 KuCoin。
去中心化交易所(DEX)致力于忠实地遵守加密货币行业的纯粹目标。DEX 不需要中介来存储货币,它是一个买家和卖家直接买卖的在线市场。换句话说,DEX 使点对点交易更加简便。
您可以直接与其他市场参与者在去中心化交易所(DEX)上交换加密资产。智能合约和原子互换可用于进行交易。原子互换可让您在不经过中心化交易所(CEX)的情况下交易一种加密货币换另一种。智能合约是一段自动执行合同条款的代码。它是原子互换的基础。
我们将把加密货币提供给受网络控制的托管系统,而不是提供给 CEX。因为交易可能需要 5 天才能清算,所以托管系统仍在生效。作为买家,您的资金将立即从您的账户中扣除,但在加密货币交易清算之前,资金不会被发送到卖家的账户。
由于任何交易中都涉及延迟,在这种交易所进行高性能交易将会很困难。
去中心化交易所(DEX)的例子包括 UniSwap、PancakeSwap 和 SushiSwap。
混合加密货币交易所是下一代加密货币交易平台。它们是中心化交易所(CEX)和去中心化交易所(DEX)的混合体。混合交易所也被称为半去中心化交易所,包括链上和链下的部分。链下交易将您的加密资产的价值从区块链转移走。加密货币交易所的混合方法结合了 CEX 和 DEX 的优势。混合交易所特别旨在将 CEX 的功能和流动性与 DEX 的匿名性和安全性相结合。许多人认为,这样的交易所是 BTC 交易的真正未来。
混合交易所旨在为机构用户提供与常规交易所相同的速度、简单性和流动性。混合交易所的中央化方面与分散化对等方连接。市场参与者可以像在 CEX 上一样访问交易平台,然后与他们的对等方进行交易,就像在 DEX 上一样。然后,混合交易所提供区块链确认和记录交易。
杂合交易的例子包括 Qurrex 和 NEXT。 这些交易所不能在交易发生的共同位置找到,大多数情况下,它们位于云端。在"如何在云中构建交易系统"一节中,我们将不出所料地描述如何在云中构建交易系统。因此,我们在传统交易中通过与交易所位于同一位置而获得的延迟优势在加密货币交易中不会奏效。
可以知道交易所(或匹配引擎)所在的区域。因此,可以比其他参与者获得速度优势。
最近,一些交易所已决定将其撮合引擎放置在同址运作,邀请参与者加入他们以进行高频交易。
使用加密货币市场数据
加密货币交易所提供的数据包括 K 线数据、1 级数据和 2 级数据。K 线描述了股票价格的日波动情况,显示了当日的收盘价、开盘价、最高价和最低价,并说明了任何两个数值之间的差异和幅度。
对于 VIP 客户或某些主要市场参与者(取决于他们的交易量),每 IP 报价无限。但对于普通客户,每 IP 都有一些报价限制。一些较新的交易所使用数据分发快照,但较大的交易所使用数据流,从而创造一些套利机会。
对于高频交易,记录任何市场数据都非常重要。这是因为交易所之前经常遇到技术问题,我们观察到历史数据中存在间隙。交易所通常选择插值数据,这在执行回溯测试时可能效率低下。
了解交易费用
交易通过各种方式在交易所收费。客户被收取费用,这就是交易所赚钱维持业务的方式。最流行的技术是交易所收取我们交易金额的佣金。大多数交易所收取不到 1%的费用以保持竞争力。
与传统交易不同,加密货币交易中有许多交易交易所。缺乏足够的流动性。因为 DEX 比 CEX 更不常见,在 DEX 上与您买卖的其他订单进行匹配可能更具挑战性。这是一种恶性循环,因为只要 DEX 不太受欢迎,其流动性就会保持较低,而只要流动性保持较低,DEX 也可能保持不太受欢迎。这就是为什么目前在高频交易中 CEX 比 DEX 更受欢迎的原因。
此外,CEX 如果交易者增加流动性将给予返利或降低佣金。与传统交易一样,使用返利进行做市策略在此类交易所也能赚钱。
在加密货币中,交易费用是传统市场的 20 到 30 倍。在像币安和火币这样的交易所市场中,我们支付以报价币种计算的交易费(这意味着有大量套利空间);在非同质化代币(NFT)市场中,我们支付以 ETH 计算的交易费,称为 gas。
某些交易所,如币安和火币,通过向符合条件的做市商提供回扣来实现其做市奖励计划。这可以帮助获得回扣的交易策略。
不能及时以可接受的价格出售(或清算)投资,这很可能发生在任何加密货币交易所。对于任何交易资产来说,流动性都至关重要。外汇(FX)市场是全球最流动的市场,但任何市场包括外汇市场都可能存在流动性不足的问题。如果你以小额交易货币,你可以完成交易,因为价格不会发生变化。
加密货币也可能出现流动性不足的情况。这是导致比特币和其他加密货币极端波动性的问题之一。当流动性稀缺时,价格操纵的可能性更大。
当初次币币发行(ICO)发生在加密货交易所上时,交易所和正在进行 ICO 的公司将使用做市商来帮助出售他们的数字资产。当资产在交易所上时,做市商将帮助促进交易。加密货交易所在交易方面将发挥重要作用,人工增加流动性。一些交易策略,如欺骗,可能被使用,最优惠的价格可能无法获得。 加密交易所的顶部订单填补可能性远低于交易所。在高频交易中,我们需要与交易所相关的做市商竞争,这会使交易变得更具挑战性。
我们现在将谈论在加密货币世界中使用的交易策略类型。
高频交易策略在加密货币中
在第 1 章,高频交易系统的基础中,我们解释了 HFT 特定的交易策略。在本节中,我们将讨论加密货币特定的策略。
做市
做市交易
统计套利
这是在两个相同加密货币交易所之间的价格差异进行投注的行为。第一个发现这些不一致性的交易员通常会利用这些知识。要执行统计套利,您需要强大且快速的处理机器和最新的高频交易软件。价格平衡会产生对整个市场的平衡效果。
智能路由器交易
高频交易员可以同时访问流动性池,选择最佳订单路由目的地,并改善订单执行。对于某个订单,在预定义或实时市场中扫描最佳买入和报价,从而获得最佳价格。
最大化短期机会
割价交易是一个用来描述利用短期机会的词语。高频交易使用强大的计算机,拥有在短时间内执行多个订单的处理能力。
最大化交易量
交易员通过高频交易(HFT)利用自动化来获取优势。这些高频交易者不仅可以执行大量交易,还可从微小的价格波动中获利。
建立加密货币交易的高频系统
数据存储是所有行业的优先事项,因为计算机和移动用户的数量有所增加。今天,大小组织都依赖于自己的数据,并花费大量资金来保持数据的最新。这需要坚实的 IT 支持以及存储中心。并非每家公司都能负担得起内部 IT 设备和备份支持的高昂费用。云计算是一种更加经济的选择。云计算减少了用户的硬件和软件需求。
像许多其他领域一样,密码领域也开始大量使用云服务。我们在前几章中描述的 HFT 系统也可以在云上使用。
云类型
我们可以以不同的方式使用云
私有云:计算资源被部署在私有云中,为单一企业服务,由同一公司管理、拥有和运营。
社区云:社区和社区云中的组织使用计算资源。
公共云:这种云通常用于企业与消费者(B2C)的交互。计算机资源由政府、学术机构或企业拥有、管理和运营。
混合云:计算资源由不同的云连接;这种部署策略被称为混合云。
我们看到了不同类型的云解决方案;我们现在将讨论使用云进行交易系统的好处。
云计算的好处
大多数加密货币交易所采用云服务,因为它们可以带来成本节省和快速上市的潜力。云计算允许灵活利用所需服务,仅为所需付费。我们可以将 IT 运营作为外包单位运行,而不需要许多内部资源。此外,招聘工程师的成本很高,组建技术团队也非常耗时。
云计算的主要优势如下:
用户的 IT 基础设施和计算支出得到降低,从而达到更好的性能。
维护困难较少。
软件更新立即可用。
跨操作系统的兼容性已得到改善。
恢复和备份。
可扩展性和性能。
由于存储容量增加,数据安全性提高。
三种主要的云计算模式在以下章节中有描述。
软件即服务(SaaS)
软件即服务(SaaS)是一种软件分发模式,供应商或服务提供商托管程序并通过互联网向客户提供。作为面向服务的架构(SOA)或网络服务的基础性技术,SaaS 正成为更常见的交付范式。这项服务通过互联网向全世界人民提供。传统上,软件必须提前购买并安装在您的电脑上。然而,SaaS 消费者而不是购买软件,而是通过互联网每月订阅。无论是一两个人还是公司里成千上万的员工,任何需要访问某个软件的人都可以注册成为用户。所有联网设备都与 SaaS 兼容。会计、销售、开票和预算只是 SaaS 可以帮助您处理的一些关键任务。
平台即服务(PaaS)为开发者提供了创建应用程序和服务的平台和环境。用户可以通过互联网访问这项服务,因为它托管在云端。PaaS 服务会定期更新,并添加新功能。PaaS 可以帮助软件开发者、网页开发者和企业。它作为应用程序开发的平台。它包括软件支持和管理、存储、网络、应用部署、测试、协作、托管和维护。
基础设施即服务(IaaS)
基础设施即服务(IaaS)是一种云计算服务模式。它为用户提供了一个虚拟化环境,即云,来访问计算资源。它提供了虚拟服务器空间、网络连接、带宽、负载均衡器、IP 地址和其他计算基础设施。硬件资源群是从分布在多个数据中心的一系列服务器和网络形成的。IaaS 因此具有冗余性和可靠性。IaaS 是一种全面的计算解决方案。它是小型组织想要节省 IT 基础设施成本的一个选择。维护和购买新的硬盘、网络连接和外部存储设备每年需要大量资金。
我们现在将讨论管理虚拟机(VM)的组件:管理程序。
虚拟机监控程序
虚拟机管理程序是一种软件、固件或硬件,允许在计算机上构造和操作虚拟机。主机机器是一台运行虚拟机管理程序的计算机,每个虚拟系统都被称为客户机。CPU、内存和存储等资源被虚拟机管理程序视为一个池,可以随时在当前客户机或新的虚拟机之间重新分配。由于客户虚拟机独立于主机硬件,虚拟机管理程序可以更好地利用系统可用资源并提供更高的 IT 移动性。虚拟机管理程序也被称为虚拟化层,因为它允许虚拟机在多个系统之间轻松迁移。单个物理服务器可以支持多个虚拟机。 图 11.1 代表两种类型的管制程序:
VM
VM
VM
应用程序
应用程序
应用程序
图书馆
依赖项
图书馆
依赖项
库
依赖项
来宾操作系统
客户操作系统
客户机操作系统
虚拟机监控程序
基础设施
VM VM VM
Application Application Application
Libs Deps Libs Deps Libs Deps
Guest OS Guest OS Guest OS
Hypervisor
Infrastructure | VM | | VM | | VM | |
| :---: | :---: | :---: | :---: | :---: | :---: |
| Application | | Application | | Application | |
| Libs | Deps | Libs | Deps | Libs | Deps |
| Guest OS | | Guest OS | | Guest OS | |
| Hypervisor | | | | | |
| Infrastructure | | | | | |
类型-1
VM
VM
应用程序
应用程序
应用程序
库
依赖项
库
依赖项
库
依赖项
客户机操作系统
客户机操作系统
客户机操作系统
虚拟机监控程序
主机操作系统
基础设施
VM VM
Application Application Application
Libs Deps Libs Deps Libs Deps
Guest OS Guest OS Guest OS
Hypervisor
Host OS
Infrastructure | VM | VM | | | | |
| :---: | :--- | :--- | :--- | :--- | :--- |
| Application | | Application | | Application | |
| Libs | Deps | Libs | Deps | Libs | Deps |
| Guest OS | Guest OS | | Guest OS | | |
| Hypervisor | | | | | |
| Host OS | | | | | |
| Infrastructure | | | | | |
2 型
图 11.1 - 虚拟机监控程序类型
这些虚拟机监控程序直接在主机硬件上运行,以控制硬件并管理来宾操作系统。因此,它们有时被称为裸金属虚拟机监控程序。这种形式的虚拟机监控程序在企业数据中心或其他服务器环境中最为流行。
二型或托管的虚拟机监控程序
这些虚拟化引擎运行在标准操作系统上。在主机上,客户操作系统作为一个进程运行,并且客户操作系统通过 2 型虚拟化引擎与主机操作系统被抽象隔离。希望在个人电脑上运行多种操作系统的个人用户应该使用 2 型虚拟化引擎。交易系统将运行在 1 型虚拟化引擎的应用层上。云服务提供商将在不同地区和可用区提供不同的虚拟化引擎。目标是将交易系统和交易所部署在同一可用区,以实现低延迟操作。图 11.2 展示了云提供商如何组织其可用区和地区:
可用区和地区 在这一部分,我们回顾了云结构。这对加密货币的高频交易来说至关重要,因为你需要尽可能靠近加密交易所的可用性区域进行高频交易。
我们现在将研究如何在云端建立一个交易系统。
如何在云端构建交易系统
亚马逊网络服务(AWS)、谷歌云平台(GCP)和微软 Azure 是三大主要云服务提供商(CSP)。
服务提供商的选择将取决于成本和您熟悉的托管服务。在这一部分中,很难建议一家 CSP 优于另一家 CSP。它们在硬件、操作系统和可使用的开源软件方面提供的服务非常相似。它们都有专业的服务团队可以帮助您设计交易系统。如果我们可以推荐一家 CSP,我们将根据您的 HFT 策略的目标加密货币交易所位置来选择 CSP。
解决方案 1 - 在 CSP 上运行我们的 HFT
我们首先需要选择应用程序将运行的区域。该区域应该最接近交易所。我们需要在您选择的 CSP 上创建以下组件:
亚马逊弹性计算云(EC2)、谷歌计算引擎(GCE)或 Azure 虚拟机等计算服务器实例是在 CSP 基础设施上运行应用程序所需的。它可用于创建近乎无限数量的虚拟机。
很重要知道您将启动的 VM 类型。选择错误的硬件类型不会为 HFT 提供出色的体系结构。您需要考虑核心数量、内存、存储以及当然网卡类型。
我们需要应用我们在前几章中使用的相同规则。请始终记住,如果您想要在 HFT 中拥有出色的性能系统,拥有多个内核将有助于您获得更好的性能,前提是您构建的系统能够处理多个进程。一旦选择了硬件类型,您就需要选择要使用的操作系统。所有 CSP 都会提供类似类型的操作系统。由于在本书中,我们重点关注基于 Unix 的操作系统,我们建议使用任何 Linux 发行版。CSP 可以提供自己的操作系统版本,如 Amazon Linux 发行版。
云的优势在于为您提供灵活的数据存储方式。这部分不如计算实例重要,因为所有的处理都会在内存中发生。然而,我们必须收集日志以监控和调试系统,并且需要记录所有市场数据以构建模型。亚马逊 S3、Azure Blob 存储和 Google 云存储将为您的关键数据提供可扩展性、数据可用性和高可靠性。
一旦我们选择了硬件特性、操作系统和存储单元,我们就可以使用在本书前几章中构建的相同代码算法。
现在,我们知道如何将我们的软件迁移到云服务提供商的云上。我们也可以利用云服务提供商在软件方面所提供的优势。云服务提供商提供托管服务和开源软件,以便更快地为 HFT 系统启动。我们现在将解释如何从头开始构建一个交易系统,并利用托管服务。
解决方案 2 - 使用 CSP 提供的托管服务
托管服务或开源软件的优点是获得经过测试和运行的软件。您只需要将这些建筑块连接在一起。我们邀请您联系 CSP 以获取建立解决方案的建议,或阅读博客了解这些 CSP 在建立交易系统方面的经验。我们建议一个受云计算工作启发的实现方案。让我们看看以下图表:
图 11.3 - 算法交易和交易系统的关键组件
图 11.3 表示我们需要建立一个完整的交易系统的组件,这补充了我们在第 2 章"交易系统的关键组件"中提供的信息,在那里我们只集中在由市场数据触发订单发送的基本路径上。
算法交易引擎是该系统的核心,它允许用户使用历史和实时数据来搭建、测试和运行交易策略,同时也可以管理与其他解决方案组件的关系,并提供分析和报告功能。
市场数据适配器为引擎提供各种数据类型,包括实时市场数据、历史价格数据等。
交易所/经纪商适配器处理与交易所和/或经纪商的交互,例如下单、撤单和查询订单状态。
由算法交易引擎使用的数据存储提供持久且安全的数据存储库。
计算机服务提供商提供广泛的服务组合,以支持多种解决方案的开发。建议进行以下评估,以定义算法交易解决方案的架构:
交易速度:在第 2 章"交易系统的关键组成部分"中,我们解释了如何构建交易系统,并展示了在设计选择中,速度对构建高频交易系统的重要性。托管服务选择将基于延迟速度。您在 CSP 上有两种类型的软件:实时处理数据的软件和批处理数据的软件。托管服务(如 AWS Lambda、Azure Functions Serverless Compute 和 Google Firebase Cloud Functions)将能够快速响应任何事件(例如市场数据进入系统)。能够调用这些功能的事件引擎(如 AWS EventBridge、Azure Event Grid 或 Google Eventarc 服务)将是一个很好的组合。事件引擎将调用无服务器功能来处理交易系统事件。
需要数据和分析:设计交易策略模型需要以非常高效的方式存储市场数据。我们需要能够使用这些市场数据进行回测。数据量可能会快速增长,拥有可扩展的解决方案很重要。我们在上一节中讨论了存储实例。我们将在此存储单元之上添加一些服务,帮助您分析数据;AWS Athena、Azure Synapse Analytics 和 Google BigQuery 将帮助您查询和使用数据。
可扩展性和灵活性:通过事件驱动的方法,将算法交易引擎连接到市场数据适配器和外部交易所/经纪商,通过共同的 API 来实现,这比将所有架构组件解耦更为首选。
云服务提供商提供有关身份和授权的安全解决方案。对于在托管环境中运行的传统高频交易,我们不必像在云端运行软件时那样关注安全性。这就是为什么我们想在这一部分强调使用安全服务的重要性。AWS Identity、Azure Active Directory 或 Google Cloud Identity 将提供身份验证、授权、加密和隔离。
我们现在将提出一个可以在任何 CSP 上几天内建立的交易系统的设计
图 11.4 - CSP 上的交易系统架构 图 11.4 代表了使用我们讨论过的组件设计的交易系统架构。我们通过仅使用 Python Jupyter 笔记本作为用户界面来简化系统。它提供了足够的灵活性来启动进程和构建交易策略。
加载数据组件(1)将帮助获取历史数据。市场数据通过市场数据交易所获得,并存储在存储单元中。根据回测方法,可以使用任何数据源,如新闻或其他类型的交易数据源(即现有数据或内部生成的数据)。
数据目录组件(2)使我们能够参考系统中所有的数据。它将能够动态更新数据。
使用这个设计(3)中提供的 Jupyter notebook,我们使用 ML DevOps notebook 实例(AWS Sagemaker、Azure ML studio 和 Google Vertex AI AutoML)训练机器学习(ML)模型。这个架构的 notebook 直接从数据桶读取数据,并且可以根据需要修改或更改给定的 notebook。
在机器学习模型经过训练后(4),它被用于使用数据桶中的数据进行实际回测,如果结果符合您的标准,则可以部署该模型。
交易所实时收集数据的部分包含
5
,
6
5
,
6
5,6 \mathbf{5}, \mathbf{6} 和 7 个注释。
查看注释 8,例如任务调度、监控、警报和日志记录等功能在安装策略并与经纪商/行情自动通信后是必要的。当利润和亏损(PnL)超过预设水平时,可创建近乎实时的通知,并且警报可以是视觉和音频的。
这是我们如何在云端建立高频交易系统来交易加密货币。我们现在将总结我们所学到的内容来完成本章。
摘要
这章是这本书的最后一章。我们讨论了如何使用 FPGA 改善 tick-to-trade 延迟。我们研究了加密货币交易与传统交易的不同之处。我们学习了如何在云中建立 HFT 系统。
由于这是本书的结尾,我们想趁此机会提醒您,技术发展非常快。在这本书中,我们介绍了高频交易的基础知识。然而,我们需要记住,成为最快的高频交易公司需要耗费大量的金钱和时间。工程师、硬件、网络和连接是高频交易的关键部分。即使我们没有最快的架构,数据分析仍然可以帮助我们建立强大的高频交易策略,在系统的局限性内执行。本书概述了考虑高频交易优化技术来创建高频交易系统的基础知识。
我们正在结束这段共同的旅程。我们想感谢您阅读本书,并邀请您如果想讨论高频交易,可以与我们联系。
索引
A
有源网络试探器 108 寻址模式,IPv4 协议 广播模式 91 广播模式 92 单播模式 90, 91 地址空间布局随机化 (ASLR) 158 高级加密标准(AES)算法 59 阿格罗纳环形缓冲区 229 预先编译(AOT)项目 214 阿尔特拉 Quartus 257 替代交易系统(ATS) 9 亚马逊网络服务(AWS)275 美国标准信息互换代码(ASCII)96 阿帕奇 NetBeans 参考链接 211
API检查,用于沟通 29 应用程序编程 接口(API)模式 应用程序二进制 接口(ABI)模式 244 应用程序,无锁数据结构约 125 日志和在线计算, 126 统计学, 127 订单请求,在关键路径 126 上 市场数据传播,在关键路径 126 上 专用集成电路(ASIC) 81, 252 群岛交易所(ArcaEx) 5 数组接口 238 资产类别 12,22 关联容器 194 原子性 169 原子操作 属性 170 自动装箱 209 自动市场制造商 (AMMs) 266 平均访问时间(AAT)130
B
带宽 83 裸金属虚拟机 274 基本关注代币(BAT) 266 基本输入/输出系统(BIOS) 156
弹劾-孔尔拉 (BNC) 端口 81 更好的替代交易系统(BATS) 8 绑定 240 比特币(BTC) 259 区块链 260 260 块 书籍构建者 24 增強版 Python 庫 241, 242 广播模式 91 比特币/美元(BTCUSD)交易对 264 公交车 80 号
C
C++
Python, using with 240, 241 使用, 在 Python 240 C++11 内存模型 关于 168 规则 168 C++ 14/17 内存模型 166 C++20 内存模型的变更 C++高频交易库 用 Python 239 工作 C++内存模型 原则 172 C++ 运行时类型信息机制 性能损失 184 C++ 静态分析 195 C++标准模板库容器 双端队列 193 列表 193 地图 194 多重映射 194 多重集 194 设置 194 无序映射表 194 无序_多重映射 194 无序集 194 向量 193 光缆纤维 大约 146 进化,至空心纤维 147 缓存 129 缓存生成器 155 高速缓存线 63 缓存未命中 117 缓存系统 二级缓存 129 一级缓存 63, 129 二级缓存 64,129 64 KB L3 缓存 二级缓存 129 结构 63 调用图 155 缓存一致性 NUMA 60 集中交易所(CEXs) 263, 268 中央限价订单簿(CLOB)6 中央处理器 (CPU) 59, 89, 252 外国函数接口(CFFI) 大约 244 用以加速 Python 代码 243, 244 芝加哥期权交易所 交易所(CBOE) 100 芝加哥商品交易所(CME)8,23,100 芯片 59 芯片监测器 257 时间 159 环形缓冲区 227 静态分析器 200 类模板 189 云 社区云 272 混合云 272 私有云 272 公共云 272 云计算 福利 272 云计算,产品 基础设施即服务(IaaS)273 平台即服务 (PaaS) 273 软件即服务(SaaS) 273 云服务提供商(CSP) 275 硬币 259 枚 指挥与控制 24,37 商品期货交易 262 沟通 API, 审查 29 社区云 272 比较-并-交换(CAS)119 编译器 约 73 例子 73, 74 可执行文件格式 74 静态链接与动态链接 并发集合 引用链接 229 并发标记清除(CMS)207,210 常量表达式 使用 186 上下文切换 大约 67,112 线程之间,以及进程 113 硬件上下文切换 113 软件上下文切换 113 上下文切换,对高频交易的弊端 默认 CPU 任务调度器行为 116 昂贵的任务 117 上下文切换,功能 关于 114 中断处理 114 多任务处理 114 用户和内核模式切换 115 上下文切换操作 步骤 115 控制流分析 196 善于交谈的颠覆者 阿格罗纳环形缓冲区 229 运输 世界协调时 (UTC) 109 逻辑单元块(CLBs)252 计数器 累积计数器 221 描述性计数器 221 递增计数器 221 类型 221 199 C++Depend 199 CPU 饥饿 114 加密货币 259 高频交易,探索 259 透明度 267 加密货币,价格推动因素约 261 密码学交换 261 加密货币专属治理 262 加密货币相关监管 262 市场参与者和竞争对手 262 生产成本 261 加密货币交易所 268
DEXs 268混合交易所 268,269 268, 269 加密货币市场数据使用 269 加密货币开采 260 加密货币交易 高频系统,建筑 272 杠杆 263 边距 263 所有权 265 263 加密交易 工作 259
CSP高频交易,运行在 276,277 托管服务 类型 使用加速 Python 代码 243,244 奇妙重复的模板模式(CRTP)182,200 自定义技术,用于测量性能 关于 156 地址空间布局随机化 (ASLR) 158 C++专用测量例程/库 158 时间 159 一致的结果,在基准测试 156 CPU 隔离和亲和性 157 CPU 电源管理选项 157 数据统计测量 158 获取时间 超线程 157 英特尔 Turbo Boost 156 进程优先级 157 针对 Linux 的特定测量程序库/工具
滴答交易(TTT) 159, 160 时间戳计数器 (TSC) 159 直通交换模式约 87 快转切换 87 无需切换 87 循环冗余检查(CRC)79 希腊文 242
D
暗池 9 数据分析
Python, using 234 数据库 231 数据库访问 230, 231 数据清洗 234 数据抓取 234 数据分发 12 数据流分析 196 数据建模 234 数据可视化 234 死锁 121 去中心化交易所 (DEXs) 263, 268 专用网络 TAP 106 数字货币交易所(DCE) 267 直接边缘 8 直接内存访问(DMA) 104 磁盘存储 130 动态链接 75 动态加载库 (.dll) 238 动态内存分配 替代方案/解决方案 185 134 步 动态多态性 182
E
24 伊甸园空间 211 有效市场假说(EMH)15 嵌入 240 端到端(E2E)字节流 93 158 埃普西隆 GC 207 以太坊 (ETH) 266 以太网 大约 90 使用,针对高频交易通信 90 以太网 TAP 107 例外 大约 186 利益 187 缺点 187, 188 交易费 交易所 交易系统交易,赚取 27-29 交易所交易基金(ETF)10,262 可执行和可链接格式(ELF)文件 74 执行顺序 167 执行人 224 执行器服务 224 表达式模板 191 扩展模块 240 外部网络 对内网络 101 外部线程 231
F
失败分析 196 快速以太网 90 快转切换 87
联邦调查局(FBI)19 栅栏 175 获取并添加 119 光纤 29 现场可编程门阵列(FPGA) 约 101, 251-253 容量 253 特性 253 结论 258 决定论 254 降低 250 并行 253, 254 可编程性 253 文件 231 金融业监管局 权威机构(FINRA)19 金融信息交换 协议 95、96 金融协议 为 HFT 交易所设计 94, 95 先进先出(FIFO)50, 51, 124 针对流的固定协议 (FAST) 90, 99, 100, 255 外汇(FX)市场 22,270 FPGA 交易系统 大约 254 考虑 256 低延迟执行算法 255 市场数据 254 交易信号 255 FPGA 交易系统,优势 大约 256 更便宜的维护 257 更高的利润 安全合规 256 FPGA 交易系统,缺点 大约 257 添加交易风险 258 开发和调试成本 257 硬件成本 257 策略复杂性限制 258 无需切换 87 帧检查序列(FCS) 87 前沿运行 19 函数模板 188 资金利率 264,265 外汇高频交易系统建设 200、201
G
G1 GC 207 垃圾收集 算法 207 事件,限制 208 影响,减少 207,208 停止世界(STW)停顿 210 垃圾收集算法 参考链接 207 垃圾收集器 (GC) 205 汽油 270 盖茨 252 网关 关于 24,25 连接到交易所 25 数据收集 25-27 通用编程 189 获取时间 全局解释器锁(GIL) 237 全球卫星导航系统(GNSS)109 全球定位系统(GPS)109 GNU 调试器(gdb) 大约 154 参考链接 154
GNU 分析器 (gprof) 参考链接 154 谷歌云平台 (GCP) 275 Google 的 TensorFlow 库 238 高性能 C/C++ 的开源性能分析工具集 约 155 参考链接 155 格雷尔虚拟机 214 格拉法纳仪表板 参考链接 221 图形处理单元(GPU) 251 石墨 参考链接 221 来宾机器 274
H
硬件上下文切换 113 头排阻塞 85 高频交易通信 以太网 高频交易计算机大约 58 台 编译器 73 中央处理器 59-61 输入/输出设备 65 操作系统(OS) 65 随机存取存储器 (RAM) 62 共享内存 64 高频交易所 金融协议,为 94,95 设计 高频交易策略,在加密货币市场 大约 271 做市 短期机会、最大化 271 智能路由器交易 271 统计套利 271 交易量,最大化 271 高频系统 加密交易大厦 272 网络 78 高频交易(HFT) 关于 3, 9-11 探索加密货币 259 特征 7 激烈的速度竞争,进化 250,251 历史 4-6 网络通信,系统之间 80、81 参与者 10 跑步,在 CSP 276, 277 提高 Python 代码的速度,改进 246,247 相对于常规交易 7,8 中空纤维 关于 146, 147 进化,微波 147 工作 147, 148 虚拟化管理程序 275 热门路径 208
htop 引用链接 222 枢纽 81 大页 混合云 272 混合加密货币交易所 268 超线程 关于 61, 157 问题 62 使用 62 虚拟机监控程序 约 274 274 二型 275 非法活动 大约 19 前沿运行 19 分层 20 欺骗 19 流动性不足 270 收件箱 209 以信息为驱动的策略 17 基础设施即服务(IaaS)273 首次币币发行(ICO) 270 内联模式 对比线外模式 244 输入/输出 (I/O) 设备 59 电气和电子工程师学会 (IEEE) 802.390 洲际交易所, 有限公司(ICE) 7 利率 265 界面分析 196 内部网络 对外网络 101 内部线程 231 互联网组管理协议 (IGMP) 92 互联网协议 (IP) 29, 79 进程间通信(IPC)144,221 中断描述符表 (IDT) 104 中断服务例程 (ISR) 104 IPv4 大约 79 使用 , 作为网络层 90
IPv6 257 发行 限制 266 瘙痒协议 100
J
Java 基础 204-206 编译链 205 堆内存模型 206 Java 微基准测试引擎(JMH) 219 Java 微基准测试 问题,用于创建 219,220 Java 探查器 大约 222 参考链接 211 Java 软件 绩效, 测量 219 Java 线程 关于 221,222 参考链接 224
Java util logging 231Java 虚拟机 (JVM) 完整的 Cl 编译代码 217 有限 C1 编译代码 216 优化,以获得更好的启动性能 218 分层编译 谷歌 GitHub 参考链接 220 即时(JIT)编译 214 及时(即时)编译器 237 级别 C2 编译代码 217 解释代码 216 简单的 C1 编译代码 216
虚拟机预热 关于 213, 214 参考链接 219
K
内核旁路 作为替代 142 延迟 142,143 用户空间旋转 142 使用 141 零拷贝 142 内核空间 用户空间 138,140 键值对 95 杀死忍耐力 121 199
L
二级缓存 129 一级缓存 129 二级缓存 129 三级缓存 129 二级缓存 129 延迟 关于 220, 251 减少, 使用 FPGA 250 延迟套利 16 第 1 层交换 87 第 2 层交换 88 三层交换 88 分层 20 贷款利率 265 杠杆 263 库 使用,在 Python 238, 239
153 缓存生成器 155 调用图 155 神经元调试器 (gdb) 154 GNU 分析程序 (gprof) 154 155
LTTng 155 性能 154 时间 154 155 流动性 12,267 流动性回扣 12 低延迟最大值断路器 关于 228, 229 熟练的破坏者 229 局域网(LAN)81 锁竞争 120 无锁数据结构 应用程序 125 118 号楼 原型 123 探鎖_120 锁 需要 118 日志 大约 150, 230, 231 基础设施设计 151 问题 151 需要 150 伦敦证券交易所(LSE) 8 查找表 (LUTs) 252 小型时间线跟踪器(LTTng) 约 155 参考链接 155
M 主存储器 130 托管服务 从 CSP 277-280 使用 边距 263 市场准入 266 市场数据,FPGA 交易系统约 254 仲裁和复制 255 255 网络堆栈 255 做市商 13 做市 市场制造策略 14 市场接受者 13 标记扫描压缩(MSC) 210 匹配引擎 13 匹配引擎算法 约 46 先进先出(FIFO) 50, 51 按比例算法 51, 52 马特普洛特 238 梅兰诺克斯消息加速器(VMA)API 142 内存 预先分配 127 预取 127 存储访问 低效率 内存分配 211 内存屏障 119 存储层次 128 存储管理单元(MMU) 69 内存映射文件 大约 143 优势 144,145 应用程序 146 缺点 145 非持久化内存映射文件 144 持久内存映射文件 144 144 种 内存模型 大约 166 需要 167,168 内存顺序 收购/释放 172 消费 172 放松的内存顺序 172 内存排序 关于 171-173 放松订购 174 释放-获取排序 174 释放-消费排序 174,175 顺序一致性(SC) 173 内存分析 211, 212 微软 Azure 275 微波炉 关于 29,146,147 优势 148 不利因素 149 影响 149 工作 148 矿业 259 动量激发交易技术 17 提升运行时决策的动力 关于 177 编译器优化 177 CPU 和架构优化 177 广播模式 92 多生产者多 消费者 (MPMC) 123-125 多个生产商单一 消费者 (MPSC) 125
N
证券协会 交易商自动报价 (纳斯达克) 5,7 全国最佳买卖报价(NBBO)16 网络 监控 105 网络地址转换(NAT)88,89 网络 基础知识 29, 30 网络交易系统 78 网络接口卡 网卡(NIC) 61, 78, 141, 256 网络层 90.0.0.0 网络 TAP 大约 107 有源网络试探器 108 被动网络 TAP 107 纽约证券交易所 纽约证券交易所 7, 22, 42 不可替代代币(NFT)270 非持久化内存映射文件 144 非一致性内存访问 60, 64, 130, 224 数值 Python(NumPy) 238
0
开放式系统互联 开放系统互联参考模型 大约 30.78 应用层 79 数据链路层 79 网络层 79 物理层 79 表示层 79 会话层 79 传输层 79 操作系统,用于 HFT 系统约 65 CPU 资源管理 67,68 功能 66 干扰管理 72 内核空间 66,67 内存管理 68 分页内存 69, 70 页表 69, 70 进程调度 67,68 系统调用 70 线程 71 用户空间 66,67 订单簿 最优价格方案 47,48 多个订单,每个价格为 50 没有匹配场景 49 部分填充场景 48 订单簿管理 大约 30, 31 修改操作 32 取消操作 32 考虑 32-34 插入操作 32 订单管理系统 (OMS) 36 订单管理 24 噢哟协议 100 线外模式 对比内联模式 244 非处方药 (OTC) 267
P
数据包捕获 工作 106 数据包生命 理解,在发送/接收 104,105 (发送/接收)路径 数据包 大约 83 分析 106 生命周期 102-104 熊猫 238 并行程序 执行,场景 226 并行/吞吐量收集器 207 帕拉索夫 199 奇偶校验 79 解析器 82 参与者,高频 交易 (高频交易) 大约 10 必需品 10,11 被动网络 TAP 107 外围总线(PCI-X) 80 完美 大约 154 参考链接 154 绩效考核 关于 152 自定义技术,使用 156 153 动力 152, 153 性能测量工具 特征 153 表现心理模型 112 性能惩罚,虚函数大约 179 分支预测 179 缓存逐出和性能 180-182 编译器优化 179 奇怪重复的模板模式(CRTP)182 预取 179 运行时类型识别 (RTTI) 183 外围设备互连总线 快速(PCIe) 59, 65, 80 外围设备 互联(PCI)80 永续期货合约 相对于传统期货合约 264 持久内存映射文件 144 费城证券交易所(PHLX) 42 物理地址空间 69 物理层交换机 87 检查 18 平台即服务 (PaaS) 273 可移植可执行(PE)文件 74 预分配基础的替代方案,关于动态内存分配大约 134 内存,限制到堆栈 135 内存池 135 精确时间协议(PTP) 109 先发制人容忍 121 预取的基于替代方案,以提升性能 关于 131 适当的容器 132 缓存友好算法 132 缓存友好的数据结构 132 隐式结构,数据 132 的利用 空间局部性 132 时间局部性 131 不可预测的分支,避免 133 虚函数,避免 133 基本类型 210, 211 优先顺序继承 122 优先级反转 122 私有云 272 问题和低效,锁 大约 119 应用程序编程 120 异步信号安全性 121 运输 杀死忍耐力 121 先发制人容忍 121 优先级反转 122 技能要求,调试 120 过程链接表(PLT) 75 处理 67 进程控制块 (PCB) 115 处理器寄存器 129 进程调度 协作式多任务处理 抢占式多任务处理 进程切换延迟 113 剖析仪 参考链接 211 程序计数器 (PC) 115 程序订单 167 按比例算法 关于 51,52 使用,与其他算法 52 协议 89 协议,用于 FIX 通信约 96 期货交易所市场数据协议 100 快速协议 99,100 痒/痛协议 100 订单 98,99 价格变化 96-98 公共云 272 每秒脉冲 (PPS) 信号 109 分析 PVS Studio 200
Python 关于 234 不需要翻译 C++, 使用 240 图书馆,使用 238,239 执行缓慢的原因 236, 237 交易策略,建筑 235,236 用于数据分析 234 使用 C++ 240, 241 Python 代码 加速,使用 CFFI 243, 244 加速,使用 ctypes 243,244 速度,在 HFT 246,247 中提高 Python 模块 建筑物 246 Python 虚拟机(PVM)237
Q
量化交易者(quants)239 队列 227
R
随机存取存储器(RAM)大约 62 个缓存 62 个 缓存结构 63 现在就绪!
∘
∘
^(@) { }^{\circ} 大约 213 参考链接 213 实时性能 测量 220, 221 返现策略 18 接收(RX)缓冲区 104 注册 Jack-45 (RJ-45) 端口 81 放松订购 174 释放-获取排序 174
释放-消费排序 174,175 资源获取是 初始化(RAII)187 零售交易平台 基本交易订单类型 263 每分钟转数(转/分) 220 环形缓冲区 227 路由器 81 运行时决策 移除 176 运行时性能损失 关于 184 缓存性能 185 堆碎片化 185 堆跟踪开销 185 运行时类型识别
(RTTI) 178, 183
S
炒短线 15,271 调度程序 67, 223 科学学习 239 天梭 239 美国证券交易委员会 (SEC) 6, 19, 262 证券信息处理器 (SIP) 16 证券代币发行(STO) 267 顺序一致性(SC) 168-173 序列 GC 207 面向服务的体系结构(SOA) 273 共享内存模型 非统一内存访问 (NUMA) 64 统一内存访问(UMA)64 共享内存系统 64 共享对象 (.so) 238
沙男多亚收藏家 参考链接 207 信号处理程序 121 简单二进制编码(SBE)100 简单包装和接口生成器(SWIG) 关于 244,245 接口文件,写入 245 仿真链路 257 同时多线程 单生产者单消费者(SPSC) 123-125 套接字缓冲区 104 软中断请求(软中断) 104 软件即服务(SaaS) 273 软件上下文切换 113 软件层 网络数据, 读取 105 网络数据,写 105 源代码订单 167 欺骗 19 263 稳定币 266 堆栈指针寄存器(SP)115 标准模板库(STL) 关于 132, 189, 193 性能, 在运行时 194 静态分析 关于 195 福利 197,198 控制流分析 196 化妆品 196 数据流分析 196 设计 196 缺点 198, 199 错误检查 196 失败分析 196 正式 196 界面分析 196 需要 195, 196 预测性 196 197 步 工具 199, 200 静态链接 74 静态多态性 182 统计套利 统计学 大约 150 基础设施设计 151 问题 151 需要 150 在线/实时统计计算 150 统计收集器 220 标准原子 169, 170
std::lock_guard 169 股票经纪人 46 证券交易所 建筑学 44, 45 特征 43
listings 43 市场数据 43 市场参与者 43 匹配引擎 订单簿 46,47 后交易 43 法规 44 工作 46 股票份额 265 停止世界(STW)停顿 210 存储转发模式 87 制定战略,决定何时交易 大约 34 订单管理系统 (OMS) 36 0109 层 1109 层
2109 层 开关 81 交换端口分析器(SPAN)106 开关 前进/过滤决策 82 数据包转发,配置 82 工作 82, 83 切换模式 关于 86 穿透式切换模式 87 存储转发模式 87 切换排队 85 同步机制 比较-并-交换(CAS)119 获取并添加 119 内存障碍 119 测试和设置 119 系统日志 231
T
任务集 参考链接 225 任务状态段(TSSs)113 减少上下文切换的技术 线程,固定到 CPU 内核 117 模板 约 188 类模板 189 函数模板 188 表现 192 可变模板 189 模板,优势 编译时多态性 190 编译时替换 190 开发成本 190 通用编程 189 190 行代码 模板元编程 191 时间 190 模板,缺点 关于 191 比 C 宏更好,以及 void 指针 190 代码膨胀 192 编译器支持 191 难以理解 只有标题 191 增加了编译时间 191 很难调试 192 模板特化 189 测试接入点(TAP)106 测试和设置 119 线程映射 224, 225 线程池 使用线程 222,223 线程 221,222 线程切换延迟 113 吞吐量 83 逐笔数据 12 36 天的订单周期 交易期 36 滴答交易(TTT) 大约 156, 159-161 端到端测量 159 分层编译 219 分层编译,JVM 214-218 时间分布 108 时间, Linux 工具 大约 154 参考链接 154 时间片 68 时间戳计数器 (TSC) 159 时间同步服务 109 代币 259 东京证券交易所(TSE) 8 工具,静态分析 静态分析器 200 199 C++Depend 199 199 帕拉索夫 199 分析 PVS Studio 200 总商店订购(TSO)168 交易 现代时代 6 1930 年代后期时期 5 交易 API 文档、示例 参考链接 29 服务中的交易(TaaS) 商业模式 44 交易所 大约 5 架构设计 大规模订单 42 历史 42 连接到 25 个网关 交易大厅 5 交易对 交易策略 大约 23 建筑物, 在 Python 235, 236 执行第 35 部分 信号组件 35 交易系统 大约 22 建筑学 24 建筑物,在云端 275 命令与控制 37,38 关键组件 36 功能组件 27 非关键组件 37 服务 38 交易系统交易制作,与交易所 27-29 交易技术 23 传统期货合约与永续期货合约 翻译预读缓存(TLB) 70, 113 传输控制协议(TCP)关于 29、30、79、141 92,93 涡轮增压 156
U
订单的 UDP(UFO)93 单播模式 90, 91 统一内存访问(UMA)64 独特标识符(UID)104 UDP 关于 29,30,79,141,221 92,93 用户空间 内核空间 139,140
V
瓦尔格林关于 155
URL 155 可变模板 189 唯链通证(VET) 266 场馆 24 虚拟地址空间 69 虚拟动态共享对象 (vDSO) 71 虚函数 大约 178 工作 178 虚拟化层 274 虚拟内存空间 113 虚拟表 178 能见度 170, 171 可视化虚拟机 http://211.url 葡萄 257
W广域网(WAN) 90 电线 29 有线直连/TCP 卸载引擎
(TOE) APIs 142
Z
Z GC 参考链接...207
包†>
百客.com 订阅我们的在线数字图书馆,即可全面访问超过 7,000 本书籍和视频,以及帮助您规划个人发展和推进职业发展的行业领先工具。欲了解更多信息,请访问我们的网站。
为什么订阅?
减少学习时间,多花时间与 4,000 多位行业专业人士提供的实用电子书和视频一起编码
提高您的学习效率,获得专门为您设计的技能计划
每个月免费获得电子书或视频
全面搜索,方便快捷获取重要信息
复制并粘贴、打印和书签内容
帕克特在其网站上提供所有出版书籍的电子书版本,包括 PDF 和 ePub 格式。如果您购买了印刷版,您可以以折扣价升级为电子书版本。欲获取更多详情,请联系 customercare@packtpub.com。
在 www.packt.com,您还可以阅读免费的技术文章合集,订阅各种免费通讯,并获得 Packt 图书和电子书的专属折扣和优惠。
您可能也会喜欢的其他书籍
如果你喜欢这本书,你可能会对以下其他由 Packt 出版的书籍感兴趣:
学算法交易
塞巴斯蒂安·多纳迪奥和索拉夫·戈什 国际标准书号: 9781789348347
了解现代算法交易系统和策略的组件
使用 Python 在算法交易信号和策略中应用机器学习
基于均值回归、趋势、经济数据发布等因素构建、可视化和分析交易策略
量化并构建一个针对 Python 交易策略的风险管理系统
构建一个回测器来运行模拟交易策略,以提高您的交易机器人的性能
在实时市场部署和整合交易策略,以维持和提高盈利能力
《Python 量化交易秘籍》
普什帕克·达加德 9781838989354
使用 Python 来设置与经纪人的连接
使用 Python 处理和操作时间序列数据
获取交易所、市场板块、金融工具和历史数据的列表以与真实市场进行交互
理解、获取并计算各种类型的蜡烛图,并利用它们计算和绘制各种类型的技术指标
开发和改善算法交易策略的性能
对算法交易策略进行回测和模拟交易
在股票市场的实时交易时间内实施实际交易
帕克特正在寻找像您这样的作者
如果您对成为 Packt 的作者感兴趣,请访问 authors.packtpub.com 并立即申请。我们已经与成千上万与您类似的开发人员和技术专业人士合作,帮助他们与全球技术社区分享他们的见解。您可以提交普通申请,申请我们正在招募作者的特定热门话题,或提交您自己的想法。
分享您的想法
现在你已经完成了《开发高频交易系统》,我们很想听听你的想法!如果你从亚马逊购买了这本书,请点击这里直接进入该书的亚马逊评论页面,分享你的反馈或在你购买的网站上留下评论。
您的评论对我们和科技社区很重要,将帮助我们确保我们提供优质的内容。