Python-Forex-交易入门指南-全-

Python Forex 交易入门指南(全)

原文:zh.annas-archive.org/md5/7644c1146aa53bbae354e31828ebf7e7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

外汇交易在过去 20 年里一直很受欢迎,其受欢迎程度还在不断增长。这个市场主要吸引新交易者,主要是因为巨大的潜在利润和能够用相对较少的初始资金开始交易的能力。然而,与此同时,它也被称为最危险的市场之一,因为根据大多数监管机构的统计数据,超过 75%的外汇交易者完全损失了他们的资金。

这种情况发生的原因有很多。一些新进入外汇市场的交易者倾向于将其视为与股票交易相同,试图投资一个没有内在上涨潜力的市场。其他人则想利用高杠杆,受到多本“快速致富”书籍的启发,追求传奇般的利润,错误地计算他们的风险敞口,然后迅速损失掉全部存款。

但所有外汇交易新手面临的主要问题是缺乏交易想法和对市场为何可能这样做或那样做的理解,何时进行交易是合理的,何时可能有问题,以及何时应完全远离市场以避免几乎不可避免的损失。

算法交易可以解决这个问题,因为它基于可以在投入任何真实资金之前进行测试的规则。除此之外,交易自动化有助于降低操作风险,并提供了对执行的全权控制,这是手动下单无法实现的。

本书填补了众多零售经纪商宣传的“简单外汇”与真实外汇市场之间的差距,从专业算法交易者的角度出发,涵盖了所有其警告和陷阱。它引导读者通过开发自动化交易策略的所有必要步骤,这些策略在这个有争议的市场中至少有生存的机会。这不是为你提供复制、粘贴和运行的现成食谱的集合:市场不断变化,你必须适应并重新适应你的策略以应对变化。

本书的主要目标是提供对算法交易在外汇市场中可能实现、不可能实现以及可以合理预期的清晰理解。到本书结束时,你将具备专业交易员水平的外汇市场基本知识。同时,你将掌握在 Python 中实施交易算法的实用技能。尽管这本书只是外汇交易新手进入这个领域的第一步,但它不仅能帮助你作为一个零售交易员开始,还能最终在这个行业中找到工作。

这本书面向的对象

这本书是为那些熟悉 Python 编程并想尝试算法交易的人准备的。这不是一本 Python 教程:我假设你对这种语言和面向对象编程非常熟悉。虽然不需要特殊的编程知识,但每当使用到新的对象或结构时,都会进行详细解释。

不需要关于市场和交易的知识,也不需要任何先前的交易经验。我试图用最简单的术语解释内容,但又不简化主题本身,以帮助你扩展交易词汇并清楚地理解最基本术语的含义。

同时,Python 仅用于提供书中考虑的概念的工作示例。没有使用特定于 Python 的特殊编程技术。这意味着你可以轻松地将代码移植到其他语言中,或者自行开发,因此这本书对于使用其他平台(如 MetaTrader)的交易者来说也非常有用。

本书涵盖的内容

第一章开发交易策略——为什么它们不同,介绍了交易策略和自动交易的概念,并强调了交易应用的主要组件,如接收市场数据、做出交易决策和执行订单。

第二章使用 Python 进行交易策略,专注于使用 Python 进行市场分析、建模和实际交易。

第三章从开发者的角度看外汇市场概述,解释了市场是如何运作的,重点关注不同类型的市场参与者及其对价格的影响,并介绍了任何进一步研究所必需的核心术语。

第四章交易应用——内部结构是什么?,扩展了在第一章中提出的交易应用架构的初稿,并提供了其主要组件的更详细视图。

第五章使用 Python 检索和处理市场数据,提供了操作 tick 数据和订单簿数据的实际示例,解释了数据压缩的使用,并介绍了通用数据连接器的概念。

第六章基本面分析的基础及其在外汇交易中的可能用途,解释了基本面分析与技术分析之间的区别,并展示了最重要的基本面事件及其对市场价格的影响。

第七章技术分析及其在 Python 中的实现,考虑了多个最著名和典型的技术分析,解释了它们与基础价格时间序列的关系,并建议了在 Python 中的可能实现方法。

第八章使用 Python 进行外汇交易中的数据可视化,介绍了基本的绘图方法,这些方法用于可视化价格时间序列和其他图形对象,例如股票曲线和回撤。

第九章交易策略及其核心要素,介绍了 alpha 和 beta 作为关键性能指标,解释了资本管理中的基准测试,并讨论了最受欢迎的 alpha 生成交易策略。

第十章订单类型及其在 Python 中的模拟,介绍了交易订单的概念,解释了主要订单类型,并强调了与每种类型相关的风险。

第十一章回测和理论表现,讨论了回测作为任何交易策略开发的核心,回顾了交易应用架构,介绍了多线程,并提供了一个详细的用例来组织应用各部分之间的通信,解释了涉及的逻辑,并为使用实时市场数据进行回测的交易应用提出了工作代码解决方案。

第十二章样本策略 – 趋势跟踪,仔细考虑了从头开始开发简单交易策略所需的全部步骤,并提供了一些可工作的 Python 代码。

第十三章交易还是不交易 – 性能分析,介绍了评估交易策略性能所使用的非常基本的指标,并给出了使用第十二章中开发的策略的理论性能的实用示例。

第十四章接下来去哪里?,提供了一系列交易想法、技术设置和代码片段,您可以用它们来开发自己的内容。

为了充分利用这本书

请记住,书中打印的代码与解释交织在一起:这是有意为之,以便注释大多数行,确保没有重要的操作未被解释。因此,建议首先阅读带有解释的整个代码,遵循逻辑,理解其工作原理,然后再从书中或 GitHub 复制代码并运行。否则,您可能会尝试仅运行代码的一部分,这可能在没有上下文的情况下无法工作。

本书涵盖的软件/硬件 操作系统要求
Python 3.10 或更高版本 Windows、macOS 或 Linux

如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Getting-Started-with-Forex-Trading-Using-Python。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他丰富的图书和视频资源中的代码包可供在github.com/PacktPublishing/获取。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

代码块设置如下:

html, body, #map {
 height: 100%;
 margin: 0;
 padding: 0
}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”

小贴士或重要提示

它看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com 与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《使用 Python 开始外汇交易》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

如果您喜欢在路上阅读,但无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在每本 Packt 书籍都免费提供该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用中。

优惠不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的每日访问权限。

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问以下链接

https://packt.link/free-ebook/9781804616857

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱

第一部分:外汇交易策略开发简介

任何软件开发项目的成功取决于两个主要因素 – 您对技术的熟练程度以及您在主题领域的熟练程度。

第一部分 提供了当今外汇市场的必要概述,重点关注其独特性,这些独特性决定了在开发实际代码时使用的解决方案的选择。在第一部分结束时,您将获得对市场结构、其运作和关键风险的概念性理解,同时,您还将了解设计交易应用架构的方法,这些方法针对特定的市场相关问题。

本部分包含以下章节:

  • 第一章开发交易策略 – 为什么它们不同

  • 第二章使用 Python 进行交易策略

  • 第三章从开发者的角度概述外汇市场

第一章:开发交易策略——为什么它们不同

多年来,我在各种市场中进行交易,教育开发者进行交易,并教授交易者一些基本的编程和算法(algo)开发。最终,我得出结论,两组(开发者和交易者)面临的主要问题不是编程知识的缺乏,而是对主题领域,即金融市场及其机制的不正确或理解不足。

这对我来说是个很大的惊喜。我以为编码、调试和错误处理应该是主要障碍,但现实中,问题在于寻找有关市场、其结构和运营的有用、正确和充足的信息,尤其是在算法交易方面。我并不是说这些信息以某种方式未公开或对公众隐藏——绝不是这样。问题在于,很难找到一个来源,它不仅涵盖了从市场结构到资金和风险管理的一切,而且还显示了市场非常具体的特征与交易算法为了在这个市场中取得成功所应具备的较为不具体的特征之间的明确联系。

例如,许多作者认为,在交易策略中,入场和出场可以立即执行,并且在任何时刻都能以相同成功执行。然而,在现实市场中,情况并非如此。我看到许多策略仅在纸上有效,因为它们的开发者没有考虑到他们正在处理的是一个真实、复杂且具有许多限制和缺陷的结构,而不是一个抽象的理想化模型。

这个例子非常简单,但我希望它能解释许多研究人员和开发者忽视的最重要的一点:我们不是在处理具有相等价值的数据集和时间序列,在任何时刻都可以以相等可能性采取任何行动;我们正在处理一个非常复杂的结构,它具有许多使这个结构成为一个动态非平稳系统的特征。而要在算法交易中取得成功,这些特征必须得到考虑。

如果不了解市场是如何组织和运作的,我们的交易算法迟早会失败。

如果用更积极的方式来说,理解市场将有助于我们使交易算法更加稳健。

这本书是我谦逊地尝试弥合量化交易世界(由数字驱动)与现实市场(由人类驱动)之间的差距。我坚信,今天,任何长期成功的交易都只能通过掌握两者良好的知识来实现。

让我们从基础知识开始。我们将了解非常基本的市场术语和交易策略的基本概念,熟悉各种形式和方面的风险,特别关注订单和交易风险,并非常概括地提及市场数据处理。

在本章中,我们将讨论以下主题:

  • 交易策略 – 这一切都关乎你如何管理风险

  • 自动交易 – 运营风险和监管限制

  • 制定实际交易决策 – 交易逻辑和信用风险

  • 下单 – 交易风险

  • 交易应用的一般架构

交易策略 – 这一切都关乎你如何管理风险

让我们从定义开始,从源头开始。我知道这听起来像是一本学术教科书,但我保证这会很快变得更有趣。

根据Investopedia的定义,交易是“不同经济行为者之间自愿交换商品或服务。”这意味着,如果我用钱交换肉类或蔬菜,那么我就在一个杂货市场。如果我用钱交换未来买卖活牛或小麦的义务,那么我就在衍生品市场。如果我用钱交换另一种类型的货币,我就在外汇外汇市场FX 市场

在特定市场中我买进或卖出的东西被称为资产。如果我买进或卖出的东西可以交付(如小麦、黄金、股票甚至货币),它被称为基础资产。如果我买进或卖出一个义务或在未来以特定价格买卖基础资产的权利,那么它被称为衍生品

基础资产和衍生品在交易场所进行交易 – 以前,这些是像交易所这样的物理地点,但如今,更多是电子网络,在这里,交易者、流动性提供者、做市商和其他市场参与者匹配他们的订单。

如果我去杂货市场卖土豆,那么我就在卖方。如果我来买土豆,我就在买方。在金融市场中,买方市场参与者通常被称为价格接受者(因为他们只能接受卖方提供的价格),而卖方市场参与者被称为价格提供者

并非所有交易场所都欢迎所有人作为直接参与者。其中大多数通过一个经纪人网络运作 – 这些实体接受客户的订单并将它们路由到一个或多个交易场所,可能内部对冲客户头寸,有时甚至作为客户的交易对手方。

第三章“从开发者角度的 FX 市场概述”中,我们将更详细地考虑所有这些实体,以了解为什么我们应该特别关注它们的行为,以及如何实际使用它们来使我们的交易策略更加稳健。

让我们快速了解一下交易策略的本质,并看看系统化或算法交易业务中固有的主要风险。

交易策略 – 我们如何在金融市场赚钱

交易策略是一套规则,告诉我如果我是买方,何时买入和卖出资产;如果我是卖方,何时以及提供多少给市场。但无论如何,交易策略的最终目标是 赚钱

如果我是买方,我只能以下两种方式赚钱:

  • 我低价买入,高价卖出,或者相反(赚取价格差异)

  • 我买入并收到股息

前者被称为活跃交易,后者通常被称为投资,或获得被动收入。在这本书中,我们将只考虑活跃交易。

买方活跃交易策略主要有两大类。第一类被称为方向性交易,本质上就是买入、等待、卖出。如果我们能以高于买入价的价格卖出,我们就能赚钱。

买方交易策略的第二类是套利。这种策略识别出同一资产在不同交易场所被不同定价的时刻(所谓真实纯粹套利),或者当标的资产价格与衍生品、不同衍生品,甚至更复杂的由多种工具组成的设置(所谓统计套利stat arb)之间存在可交易的价格差异时。

听起来有点令人不知所措?别担心,我们现在将更详细地考虑每个案例。

交易应用 – 有什么能比这更简单?

到目前为止,如果你在应用开发方面有足够的经验,并对市场有合理的了解,你可能会大声喊出以下内容:

为什么我们需要所有这些?构建交易应用真是太简单了:你只需要获取市场数据,计算入场和出场订单,并将它们发送给 经纪人!

因此,从这个角度来看,建议的交易应用的一般架构可能看起来就像这样简单:

图 1.1 – 交易应用初始架构

图 1.1 – 交易应用初始架构

然而,正如我们很快就会看到的,这是一个过于简化的观点,它至少缺少一个使开发交易策略与其他应用程序开发不同的关键特征:它不包括风险管理。

那么,那个风险怎么办?

在讨论交易中赚钱的方法之前,让我们首先更关注避免损失 – 至少因为根据所有统计数据,超过 70%的活跃交易者,不幸的是,都在亏损。

在我们继续之前,让我指出,以下风险分类并不完全对应官方的法律和学术分类。这里使用这种非正式分类是为了简化,以便快速而舒适地整理出复杂的问题。

活跃交易中的所有风险可以大致分为三大类,如下所示:

  • 操作风险是指与你的交易方式相关联的风险,这取决于你自己的业务流程以及第三方,如经纪人、交易场所和监管机构。

  • 系统性风险是指与市场本身及其交易策略逻辑相关联的风险。

  • 交易风险是指使订单执行与预期不同的风险;这是许多在纸上看起来可行的策略在现实中无法赚钱的主要原因。

既然我们对交易策略的概念已经更加熟悉,并且知道任何系统交易者应该解决的主要问题是风险缓解,那么让我们更深入地探讨算法交易业务特有的风险。

自动交易 – 操作风险和监管限制

操作风险是指由于内部程序、人员、系统不足或失败,或外部事件(国际清算银行,巴塞尔银行监管委员会,《新巴塞尔资本协议操作风险支持文件》(巴塞尔:BIS,2002 年),第 2 页,www.bis.org/publ/bcbsca07.pdf)导致的直接或间接损失风险。

由于在这本书中,我们将主要讨论使用 Python 开发交易算法,而不是关于运营交易业务,因此在这样的背景下,主要的操作风险可能是你没有遵循自己的策略或随意干预算法交易过程。

另一个可能被视为操作风险(尽管它通常被视为资金管理)的风险是不当使用杠杆。本质上,杠杆是经纪人提供的信用额度,允许你购买比你账户中更多的资产。如果杠杆过高,你将面临无法进入市场的风险,或者在某些情况下,甚至更糟 – 损失快速缩水的头寸。

经纪人风险也可以归因于操作风险,因为经纪人是你进入市场的实体,为你提供信用额度以开设头寸,并执行清算和结算。一些经纪人还代表客户作为做市商,内部对冲他们的头寸,并作为他们客户的交易对手,这可能导致利益冲突,甚至更糟 – 如果经纪人没有足够的资本来执行这些操作,可能会损失资金。

最后,但同样重要的是,我们应该注意,在某些司法管辖区,算法交易和/或自动化交易可能完全或部分被禁止。因此,请始终与相应的市场监管机构联系,以确保你可以运行你的算法交易。

关键要点

总是对所有交易对手进行背景调查,尤其是你的经纪人。小心使用杠杆,并检查当地关于算法交易的监管文件。

关于操作风险就说到这里——至少对于快速入门来说——让我们继续讨论另一种对于任何交易活动都普遍存在,但对于算法交易尤其成问题的风险:基于错误市场数据的交易决策风险。

获取市场数据——质量和一致性是成功的关键

市场数据通常被认为对系统化交易的整体风险没有贡献。然而,这是一个巨大的错误。与市场数据相关的有两个关键风险:

  • 接收数据的问题

  • 接收数据的问题

在接下来的两个小节中,我们将更深入地探讨前面的风险。

接收数据——当规模很重要时

我们获取市场数据有两种形式:实时或历史。在两种情况下,我们都从数据供应商、经纪人或直接从交易所获取。区别在于实时数据用于实际交易(因为它反映了市场当前的实际情况),而历史数据仅用于研究和开发,以重建假设交易并估计交易算法的理论表现。

接收数据的问题主要与实时数据相关。

现在我们需要添加一些更多的定义,因为我们需要掌握一些常见的术语以便继续进行市场数据和订单处理。

以某一价格购买资产的请求被称为出价。这就像你走到市场上大声喊道:“我想以这个价格购买这个资产。有人愿意卖给我吗?

以某一价格出售资产的请求被称为要价报价。这意味着你准备好以你接受的价格将其卖给任何愿意的人。

在金融市场中,这两种请求都通过买方交易者通过限价单(参见第十章Python 中订单类型及其模拟,以详细讨论订单类型)来实现。

当另一个交易对手方同意以订单价格进行交易时,就会注册一条新交易,并将其信息包含在数据流中,并分发到数据供应商、经纪人和其他接收者。这样的记录被称为tick。换句话说,tick 是市场数据中的最小信息单元,通常包括以下字段:

  • date

  • time

  • price

  • traded volume

  • counterparty1

  • counterparty2

最后两个字段包含有关实际交易对手方的信息,通常不会公开或分发,以保护市场参与者。交易量指的是已交易的资产数量(合同数量,或者如果我们谈论外汇,就是金额)。

接收原始形式的市场数据的主要问题是它简单得过于庞大。有如此多的市场参与者和交易场所,仅一个资产(也称为“金融工具”)的所有交易流可能轻易达到每秒兆字节级别——接收它本身就是一项挑战(别担心,我们在这本书中不会处理这类数据流)。接下来,即使我们能够接收具有如此吞吐量的数据流,我们也需要以某种方式存储和处理这些数据,因此需要一个非常快速的数据库。最后,我们需要能够以足够的速度处理这些数据量,因此我们需要极其快速的计算机。

但有好消息。尽管有一些策略(主要是套利和高频交易),我们确实需要以所描述的格式(也常被称为时间和销售数据)的原始市场数据来识别交易机会,因为大多数方向性交易算法对每笔交易信息缺乏的敏感度远低于对信息的全面掌握。因此,数据供应商以压缩格式提供数据。这是可能的,因为大部分原始市场数据包含相同价格的连续跳动序列,去除这些序列不会扭曲价格走势。这种情况发生是因为可能有众多市场参与者几乎同时以相同价格进行交易,因此通过排除这些序列,我们虽然失去了每笔交易的信息,但保留了价格变化的任何信息。这种市场数据流通常被称为过滤清洁的。除此之外,一些交易是在买入价进行的,而另一些是在卖出价进行的,尽管买入价和卖出价保持不变,但这些交易形成了价格似乎不同的交易序列。然而,在现实中,它们始终处于买入价和卖出价之间的差价。这种差价并不意味着市场价格发生变化。这种现象被称为反弹,通常也会从清洁数据中排除。

一些供应商和经纪商甚至更进一步,发送市场数据的快照而不是过滤后的数据流。快照以固定的时间间隔发送,例如,每 100 毫秒或 1 秒,并且只包含以下信息:

  • 日期

  • 时间

  • 间隔开始时的价格(也称为开盘价,或简称为O

  • 间隔内的最高价格(也称为高点,或H

  • 间隔内的最低价格(也称为低点,或L

  • 间隔结束时的价格(也称为收盘价,或C

  • 交易量

因此,我们收到的不是数千个跳动,而是一个包含七个数据字段的跳动。这种方法大大降低了吞吐量,但显然对数据有一定的破坏性,快照数据可能不适合某些策略。

主要收获

在选择数据源时要小心,尤其是对于实时交易,并始终确保它包含足够的信息来支持你的策略。

接收到的数据——从批判的角度来看

在我们成功接收到数据后,我们应该确保其合理。通常,数据,尤其是分笔数据,可能包含错误的价格。这些价格可能是由于多种原因接收到的,我们将在第五章使用 Python 检索和处理市场数据中详细讨论。

错误,也称为非市场价格,可能会给系统性交易者带来麻烦,因为一个单独的错误报价可能会触发一个算法进行买入或卖出操作,而根据策略逻辑,这种交易本不应该发生。

有时候,如果将这些错误报价绘制在图表上,我们可以看到它们。人类眼睛直观地期望数据点在某个合理的范围内,并且很容易捕捉到异常值,如下面的图表所示:

图 1.2 – 在分笔图中看到的非市场价格

图 1.2 – 在分笔图中看到的非市场价格

如果我们收到快照或其他压缩数据,当我们没有收到报价时,可能会有缺失的间隔。这可能是因为以下原因:

  • 市场关闭(计划内或因紧急情况)

  • 数据服务器宕机

  • 连接中断

关键要点

一个健壮的交易应用应该有一个模块能够检查数据一致性以及连接持久性。

好的,我们现在已经了解了操作风险,也知道不正确处理市场数据可能造成的危害。还有其他什么吗?当然,接下来是主要风险:系统性。

制定实际交易决策——交易逻辑和信用风险

在趋势交易中,系统性风险主要在你或你的交易策略预期价格会向一个方向变动,但实际情况却是相反方向时显现出来。不用担心,这种情况对于系统性交易来说绝对是正常的,没有人能保证任何策略会生成 100%的胜率。

交易策略主要有两种类型:数据驱动和事件驱动。数据驱动策略分析价格时间序列(我们在检索市场数据——质量和一致性是成功的关键部分讨论过),以寻找某种模式或序列,然后触发订单。事件驱动策略等待某个事件发生——例如,在某个价格和一定量上的新分笔,或者政治新闻或经济指标的发布。在两种情况下,交易应用都应该有规则,不仅能够开仓,还能够平仓——再次强调,基于价格数据或事件(或两者)。

通常,如果一个策略产生了一些胜利和一些损失,它赚钱的途径只有两种:

  • 产生更多的胜利而非损失

  • 平均收益大于平均损失

如果你的交易算法没有处理市场与持仓相反情况的标准程序,那么在统计上具有显著数量的交易中,平均损失很可能会大于平均收益,使用这种策略赚钱将变得非常困难。

不要忘记外汇交易是使用杠杆进行的,这意味着你可以交易比你账户中实际拥有的金额多得多的资金。如果你的交易算法风险管理逻辑差,仓位大小不正确,当价格反向变动时,一个开放的头寸可能会迅速耗尽你的账户资金至零,甚至更糟——一些经纪商甚至允许你出现负数,你将背负债务而不是利润。

关键要点

系统性风险管理算法和仓位大小算法是算法交易应用的关键部分。

系统性风险非常重要,但对于系统性交易者来说,有一个好消息:在投入生产前,通过仔细测试策略并调整策略以最小化系统性风险,可以减轻这种风险。但还有一个在研发阶段难以减轻的风险:交易风险。

下单 – 交易风险

交易风险是套利首先面临的问题,但它们也影响方向性策略。简单来说,这是一个以下风险:

  • 在错误的价格进入或退出市场

  • 在错误的时间进入或退出市场

  • 以错误的大小进入或退出市场

  • 完全不进入或退出市场

所有四种情况在所有市场中都是可能的,甚至在流动性不足的时期相当频繁(参见第三章从开发者的角度看外汇市场概述,以了解更多关于流动性问题的讨论)。

关键要点

交易风险由一组算法管理,这些算法也是任何交易应用的基本组成部分。

嗯,我们已经穿越了各种风险,现在我们明白,一个具有简单直接线性逻辑的交易应用在现实生活中肯定不会奏效。现在,我们可以提出一些(不幸的是)更复杂,但(幸运的是)更现实的建议。

交易应用的一般架构

现在,我们可以改进我们最初的图(见图 1.1)来表示交易应用架构。尽管它仍然非常通用且处于较高层次,但它现在与最初我们提出的建议有根本性的不同:

图 1.3 – 交易应用更完整的一般架构

图 1.3 – 交易应用更完整的一般架构

在这里,我们可以看到实际的交易应用包含了许多块或模块,其中一些模块会接收来自其他模块的反馈。在我们收到市场数据后,我们应该对其进行清理并添加存储和检索功能,因为数据片段可能在后续的代码中被重复使用。然后,我们根据策略逻辑生成交易信号。但在将这些信号以订单的形式发送到市场之前,我们应该进行一些检查,以确保订单大小对策略和市场来说都是舒适的。之后,我们实际上进行交易——向市场发送订单并控制其执行。最后,我们跟踪开放的头寸并根据运行中的盈亏来管理风险。

摘要

在本章中,我们熟悉了外汇市场的核心术语和关键概念,了解了交易对手和交易,理解了市场数据内在的问题,回顾了各种风险,并草拟了交易应用架构的第一个原型。我们现在知道,一个稳健的交易应用更多的是关于风险管理,检查交易前后可能发生的各种情况,并在实时进行纠正。这就是交易应用开发与众不同的地方。

在下一章中,让我们看看如何在算法交易中使用 Python 如何帮助提高交易应用的研究和开发。

第二章:使用 Python 进行交易策略

Python 已经成为金融行业中广泛应用的开发语言的首选,并成为交易策略开发研究阶段的事实上的行业标准。然而,就像任何其他语言或,更确切地说,编程生态系统一样,它有其优点和缺点。因此,就像任何工具一样,了解其优点和缺点对于正确使用工具至关重要(比如说,不要试图用显微镜敲钉子)。

在本章中,我们不仅将考虑使用 Python 进行算法交易策略开发,还将学习研究和开发过程中的基本步骤,讨论市场建模和基于事件交易之间的区别,并指出在研究和开发过程中最常犯的错误。最后,我们将看到使用 Python 进行算法交易的局限性,以确保我们的期望始终与现实相符。

我们将快速深入探讨,不仅仅是对 Python 的表面概述,看看 Python 最强大的优势,如原生语言结构和高效的面向对象编程OOP),如何帮助使代码透明,并保持交易应用架构的模块化、灵活性和可扩展性。

到本章结束时,您将熟悉交易和算法交易的关键概念、Python 的应用以及用于交易策略研究和开发的多种集成开发环境IDE),并且您将了解使用 Python 效率不高的限制。

在本章中,我们将涵盖以下主题:

  • 使用 Python 进行交易策略开发的优点

  • 建模——预测未来并不一定意味着实际效益

  • 纸质交易和回测——系统交易员风险管理的一个基本部分

  • 使用 Python 进行交易策略开发的缺点

  • 真实交易——Python 面临的限制

技术要求

您需要 Python 3.10 或更高版本才能运行本章中的代码。

使用 Python 进行交易策略开发的优点

当我们今天说Python时,我们很可能不仅指的是最灵活的面向对象编程语言,还包括最强大和方便的交互式集成开发环境(IDE),如 Spyder 或 PyCharm,以及大量扩展此语言的库,使其几乎适合任何应用和基础研究的领域,从医学到天文学。在数字起决定性作用的金融行业中,Python 生态系统已经成为事实上的标准,这并不令人惊讶。

使用 Python 进行交易策略开发的优点是显而易见的,我们将在以下几节中详细探讨这些优点。

内存管理

Python 是少数具有强大内置内存管理服务(如垃圾回收和引用计数)的解释器之一。本质上,这意味着你不必关心你工作中遇到的任何复杂对象以及你的代码处理的数据量。当然,这大大提高了开发速度,尤其是当你处理大量数据集时——交易应用处理大量数据集。

交互式计算

Python 是一种解释型语言,这意味着两件重要的事情。

首先,你可以在任何时候停止代码的执行并检查运行时的环境,包括所有变量、函数和对象——这对于调试来说非常方便。

其次,你可以以交互式的方式使用 Python。这意味着在所有类 Unix 系统中,我们在控制台中运行 Python,可以逐个发送命令并立即从解释器那里获得响应。如果你开发交易应用,这允许你在将它们集成到最终代码之前,快速在小数据集上测试一些模块。这也允许你快速可视化数据集的任何部分,而无需重写和重新运行任何代码,这对于开发任何处理时间序列的应用程序来说是无价的。

在原生控制台中工作不如在 IPython 中方便——IPython 是一个支持内省、富媒体、语法高亮、自动补全和历史记录的命令行。作为 2001 年发布的一个开源项目,IPython 迅速将 Python 转变为 Matlab 的直接竞争对手,多亏了开源状态和庞大的贡献者社区,我们可以说,如今 Python 已经赢得了这场竞争。2014 年,一个基于 IPython 的衍生项目启动,其理念是开发一个通用的交互式计算环境,不仅适用于 Python,也适用于其他语言。这个项目被称为 Jupyter,如今这可能是最受欢迎的笔记本式交互计算环境。有时 Jupyter 甚至被认为是 IPython 的替代品;然而,这并不完全正确。DataCamp 发布了一篇优秀的文章,解释了两者之间的区别:www.datacamp.com/blog/ipython-or-jupyter.

)

集成和路由的便捷性

如果前两个优势并非特定于交易应用的开发,那么使 Python 成为交易研究和开发首选语言的是,你可以轻松地在研究模式生产模式之间切换。

如果你已经仔细规划了你的数据结构和代码(这正是我们将在整本书中学到的),那么交易应用(我们在上一章中制定的)的所有基本模块都将独立且可替换。这对作为开发者的你有什么优势?优势如下:

  • 它允许你使用历史数据开发交易逻辑,然后使用来自经纪商的实时流替换数据源。

  • 这允许你通过模拟订单执行来微调和调试你的应用程序,并让你使用与开发时相同的代码切换到生产环境。这降低了出错的风险。

  • 这允许你通过添加更多的执行场所、协议、API 和交易账户来扩展你的交易业务,同时保持其余代码不变。

此外,得益于 Python 环境的交互性,你甚至可以手动干预交易过程。例如,在紧急情况下,你可以发送一个FIX消息来关闭所有未平仓的头寸。当然,这不是最佳实践,甚至不是可以推荐的做法,但知道即使在最糟糕的意外情况下,你也能直接从熟悉的开发环境中采取紧急行动,这有助于增加安心感。

原生语言结构 - 列表和字典

使用 Python 进行数据处理的一个关键优势是它原生实现了两个强大的结构:列表和字典。

交易应用处理时间序列数据,拥有方便的工具来读取、访问、压缩和处理这些数据至关重要。使用列表、字典以及对象变得相当高效。让我们考虑一个例子。

假设我们有一些 tick 数据,并希望将其转换为 OHLC 数据点(有关 OHLC 的解释,请参阅上一章)。在交易中,这种数据点通常被称为条形图,我们可以如下定义:

class OHLC:
        def __init__(self, O, H, L, C):
                self.open = O
                self.high = H
                self.low = L
                self.close = C

然后,我们可以为我们的条形图创建存储:

class time_series:
        def __init__(self):
                self.ts = []
                self.last = -1
        def add(self, bar):
                self.ts.append(bar)
        def read(self):
                self.last += 1
                return self.ts[self.last]

在这里,我们可以定义一个本地的 Python 列表,用于存储 OHLC 条形图和self.last指针,该指针指向列表中最后读取的元素。然后,我们将添加read方法,该方法返回self.last指针指向的元素。

然后,在生产代码中,我们不是通过索引检索 OHLC 条形图,如下所示:

data = time_series()
i = some_index
price = data.ts[i].close

我们将检索收盘价,而不直接引用索引:

data = time_series()
price = data.read().close

这极大地简化了处理时间序列(或任何数据序列)的工作,因为我们不再需要在主代码中存储索引,更重要的是——我们减少了提前查看的风险。提前查看是交易策略开发中常见的错误。在进行回测时,策略代码必须只使用每个模拟交易过去和当前的价格数据。例如,如果策略模拟了 4 月 1 日的市场,它就不应参考 4 月 2 日的价格数据。同样适用于任何时间分辨率,甚至到毫秒。在研究阶段使用未来数据的策略通常无法在实际生活中工作(参见第五章**,使用 Python 检索和处理市场数据, 对进行回测时正确处理数据的详细讨论)。Python 允许你避免此类关键错误的便利性无法过高估计,因为这些错误可能会造成真实和重大的经济损失。

字典为在 Python 中存储和处理序列数据提供了另一种方式。由于每个数据点,无论是 tick 还是 bar,都有一个时间戳,我们可以使用时间戳作为字典中的键:

class OHLC:
        def __init__(self, dt, O, H, L, C):
                self.datetime = dt
                self.open = O
                self.high = H
                self.low = L
                self.close = C
class time_series:
        def __init__(self):
                self.ts = {}
        def add(self, bar):
                self.ts[bar.datetime] = bar

然后,使用以下代码通过其timestamp值检索特定的close价格将变得简单:

price = data.ts[timestamp].close

因此,即使是本地的 Python 语言结构也提供了快速、简单且极其高效的方式来处理特定的交易数据。然而,是库使得 Python 成为定量交易的不二选择。

Python 作为定量金融和交易环境的最终成功取决于许多库,这些库今天实际上是行业标准,有时与语言本身紧密相关,以至于许多开发者甚至没有单独考虑它们。让我们来看看这些库。

NumPy

最初于 1995 年作为 Numeric Python 发布,这个库今天几乎用于任何使用数学的应用。它正是将 Python 从一种编程语言转变为类似 Matlab 的、强大的、数值处理套件的库。

NumPy 为线性代数中的许多核心对象提供了实现,例如向量、数组和与之相关的操作。它提供了全面的数学函数、随机数生成器、傅里叶变换等。最好的是——它的核心是用 C 编写的,因此所有本地的numpy方法都运行得非常快。

Matplotlib

Matplotlib 于 2003 年发布,作为 Python 和 NumPy 的通用绘图库。它识别本地的 NumPy 对象,并生成几乎任何类型图表的打印质量图像。

以下特性使得 Matplotlib 在交易应用开发中特别吸引人:

  • 它会自动将数据系列缩放到适合图表,因此只需一个命令就可以轻松可视化任何市场或交易数据

  • 这些图表可以嵌入,并且可以输出到 IPython 或 Jupyter 等控制台

  • 图表是交互式的,因此你可以放大、缩小和拖动来探索细节,而不需要编写自己的图像处理工具。

pandas

人们认为,pandas这个名字来源于计量经济学中使用的术语面板数据。它也可以理解为Python 数据分析。这是一个引入 DataFrame 概念的库。你可以将 DataFrame 视为 Python 字典、列表和数据库的混合体。它使用关键字来访问记录,但保留项目的顺序,以便可以通过索引检索数据。同时,它还具备数据库典型的程序,如制作子集或切片。

在 DataFrame 的基础上,pandas 提供了在内存结构和不同文件格式之间读取和写入数据的功能,包括 CSV、JSON、SQL 查询和表格,以及 MS Excel,几乎涵盖了数据供应商、经纪人和交易所今天使用的所有格式。

如果我们再考虑这样一个事实,即 pandas 提供了一系列强大的功能,可以重建缺失数据,生成日期时间范围,转换采样频率,支持滑动窗口统计,等等——你就会明白,你得到了开发任何类型交易应用的终极工具箱。

尽管全面介绍 pandas 的所有强大功能超出了本书的范围,但我们将考虑 pandas 在第八章中的一些有用应用,即使用 Python 进行外汇交易数据可视化

NumPy 和 matplotlib 是 SciPy 的一部分——一个涵盖任何应用数学的综合性库,从优化和线性代数到信号处理和多维图像处理。

建模——预测未来并不一定意味着实际的好处。

为了保持讨论的一致性,我们需要在交易和建模之间划一条既薄又坚实的界限。有时,这两个术语会被严重混淆,不仅可能导致误解,还可能导致亏损。

建模是一种研究活动,旨在构建一个能够解释观测数据的模型。例如,托勒密提出了一个以地球为中心的太阳系模型,而尼古拉·哥白尼则提出了一个地球围绕其自身轴旋转并在椭圆轨道上绕太阳运行的模式——现在被称为日心模型。这两个模型都解释了观测到的数据:太阳看起来在地球周围移动,白天跟随黑夜,季节按顺序变化。然而,日心模型证明要精确得多,也更容易使用,因此另一个模型被废弃了。

在金融世界中,建模市场意味着找到一组能够解释观测价格行为的定量规则。乍一看,它看起来像建模任何其他物理过程,例如,同样的太阳运动。金融建模的主要问题,尤其是在交易中,是建模过程是非平稳的。

用非常简单的术语来说,一个非平稳过程没有恒定的均值或随时间变化的值的一致分布。这正是任何尝试使用经典统计学(从平均值到高斯分布)来计算公平价格或预测未来的任何尝试最终失败的原因。这也是为什么许多涉及机器学习元素的现代方法,尤其是基于线性回归的方法,在实际交易中也会失败的原因。

如果你想要了解更多关于非平稳过程的信息,包括它们是什么以及为什么基于统计学的成功交易如此困难,那么我建议从 Investopedia 的一个基本文章开始:www.investopedia.com/articles/trading/07/stationary.asp。或者,如果你在数学方面更为精通,M. B. Priestley 的《非线性与非平稳时间序列分析》是一本优秀的书籍。

回到正题——对非平稳数据进行建模不仅问题重重,而且往往提供实际上毫无用处的结果。在平稳过程的情况下,例如地球围绕太阳运行,我们可以使用我们的模型来预测未来——在绝大多数情况下都会是正确的。但是,对于非平稳过程,大多数模型将完美地解释过去的数据,即已经观察到的数据,但在预测未来的观察值时将遇到问题。因此,在我看来,市场建模领域属于学术研究,而将其用于实际主动交易是值得怀疑的。我知道存在相反的观点,但无论如何,在这本书中,我们将只关注事件驱动交易。

方法上的差异在于,在市场建模中,我们试图预测未来的价格变动,然后根据预测做出交易决策;而在事件驱动交易中,我们等待某个事件发生,然后立即通过下单来做出反应。例如,如果我们运行套利策略,那么我们会等待资产定价错误的(罕见)时刻。如果我们运行方向性交易策略,我们会等待某些经济新闻发布或价格开始快速变化的时刻,或者相反——当市场缓慢时,但无论如何——策略会对市场当前正在发生的事情做出反应,而不进行任何预测预测

scikit-learn

尽管如此,一些在市场建模中传统上使用的库存,尤其是在机器学习中,作为数据处理的一部分,在事件驱动交易策略中可以相当有用。因此,我们应该至少提到一个 Python 库,它是数据科学事实上的行业标准。

scikit-learnsklearn 是一个包含数据科学和机器学习中常用技术实现的库。它包括分类、回归、聚类和预处理等易于使用的实现,以及模型选择算法(交叉验证、网格搜索等)。拥有这样一个强大的库也增加了 Python 作为开发交易策略首选生态系统的选择。

现在我们已经熟悉了 Python 和众多库提供的强大功能,是时候深入挖掘任何类型的算法交易策略典型的研发过程了。

模拟交易和回测——系统交易者风险管理的重要组成部分

想象一下,我们已经使用了 Python 的所有功能并开发了一个交易应用。现在怎么办?是时候立即推出它并尝试赚钱了吗?不!在跳入泳池之前,确保里面有水是至关重要的,在我们的情况下,在将应用投入生产之前,确保它至少在理论上能够赚钱是至关重要的。

在本节中,我们将探讨模拟交易和回测——系统交易的两个基石,帮助我们了解新开发策略的潜在陷阱。我们将学习关于历史数据、交易模拟和订单处理,同时也会快速考虑一些现成的包,这些包可以简化这一部分的发展过程。

模拟交易和回测是什么?

在我们开发了一个交易算法,连接到数据源,并准备好发送订单后,是时候测试我们的设置了。这样的测试验证以下关键点:

  • 交易逻辑的一致性

  • 风险管理

  • 订单界面和错误处理

如果使用过去的市场数据(通常称为历史数据)进行测试,并将订单发送到模拟引擎,那么这被称为回测。如果使用实时市场数据进行测试,并将订单发送到 UAT 环境(由经纪人、交易场所或再次本地模拟提供),那么这被称为模拟交易

回测的目的是查看我们的策略在过去各种市场情况下的反应。这是绝对必要的,因为所有系统交易都是围绕这样一个想法建立的:如果过去发生了某种情况,那么它很可能会在未来重复。例如,如果非农就业人数远低于预期,股票价格会在一段时间内下跌。这一点得到了多年历史数据的证实。因此,我们可能会假设下次它太低时,股票价格会再次下跌。

纸化交易是必不可少的,因为这可能是唯一能够证明策略能够在实践中赚钱的测试,而不仅仅是理论上。还记得我们在前几节中关于建模和预测的讨论吗?构建一个在历史中完美交易但无法在生产中使用的模型是可能的(并且并不真的困难)。所以,任何交易应用都需要一定时间的纸化交易来检查它是否能在现实生活中赚钱。

Python 中的回测和纸化交易

Python 本身非常适合回测和纸化交易,因为它具有交互式计算的能力以及现成的数据处理和可视化库。我们可以编写一个订单执行模拟器,并在数据集中收集关于我们交易回报的累积数据——这需要一些时间和努力,但分析起来却非常简单:从统计方法到可视化表示,这最广为人知的是权益曲线。所有这些都可以通过在控制台中发出单个命令进行交互式完成。

在这本书中,我们将重点关注交易应用架构,因此主要考虑基于原生 Python 结构的解决方案,因为我们的目标是理解开发交易应用的所有阶段,直至核心逻辑结构。然而,正如数据处理的情况一样,存在开源和免费的库和框架,它们简化了 Python 中的回测,使得开发交易策略更具吸引力。对任何这些产品的详细审查或教程都不在本书的范围之内,但一旦你理解了研究和开发过程,你将能够轻松地将它们中的任何一个整合到你的工作流程中。

PyAlgoTrade

PyAlgoTrade 是一个框架,你可以用它来开发功能齐全的交易策略。它是一个非常成熟的产品,因为它是这一类中的第一个之一,它仍在积极维护和开发中。它遵循设计交易应用的模块化理念。因此,你可以首先开发一个策略,对其进行回测,然后切换数据源到实时数据,进行纸化交易,最后将输出从模拟订单执行切换到你选择的经纪商,进行实时交易。它支持免费的数据源,如 Yahoo! Finance 和 Google Finance,但如果你想要使用 PyAlgoTrade 进行外汇交易,你必须从第三方来源获取数据,将其保存为 CSV 格式,然后才能使用这个框架。

需要注意的是,PyAlgoTrade 支持实时 Twitter 事件处理,这意味着你可以为你的策略使用基于非价格数据的规则(例如,“当欧洲央行成员说些什么时,卖出欧元”)。

你可以在以下位置找到项目页面:github.com/gbeced/pyalgotrade.

)

bt – Python 的回测

bt 是另一个用于回测的框架,它更专注于投资组合交易(并行运行许多不同的策略和交易许多不同的市场)。

这种方法使 bt 接近了曾经流行的各种交易策略可视化构建器。当然,这大大加快了开发过程,因为你不需要在低级别上编写算法;你只需从库存中选择最合适的即可。然而,这种优势显然也是一个缺点,因为你被限制在只能使用框架开发者认为合适的工具。

然而,好消息是,与可视化构建器不同,你可以在 bt 中修改任何内容并编写自己的构建块,这给你带来了几乎与从头开始编写自己的策略相同的自由度。

bt 还提供了一套全面的统计工具,用于分析策略或投资组合的表现,并快速尝试各种交易算法的组合,以发现最适合特定市场的算法。

这个框架主要关注回测和纸面交易,所以如果你计划从同一应用程序中实时交易,你需要开发自己的订单生成和提交模块。

你可以在以下位置找到项目页面:pmorissette.github.io/bt.

)

Zipline

Zipline 可能是交易策略最知名的研究和开发工具。它由 Quantopian 开发,这是一个著名的项目,它为任何开发者提供了一个机会,通过提供开发交易策略的环境,使他们有机会成为量化交易员。它甚至为他们分配了一些资金,以防策略表现得到证明是可接受的。

Zipline 可以用作框架和独立应用程序,是一个具有纸面和实时交易能力的算法交易构建器和模拟器。你可以通过基于浏览器的 iPython Notebook 界面与之交互。

Zipline 附带了 10 年分辨率为 1 分钟的美国股票历史数据。这并不多,而且绝对与外汇交易无关,但它支持导入各种格式的数据,因此你可以使用第三方数据,例如来自你的经纪商。

由于 Zipline 是作为 Quantopian 生态系统的一部分设计的,其实时交易功能有限。

尽管 Quantopian 最初取得了成功,媒体上也有相当多的炒作,但在 2020 年,该公司破产并关闭了所有业务。Zipline 随后被卖给了 Robinhood。但新所有者对产品的开发兴趣不如其创造者,因此该项目现在主要以爱好者支持的分支形式存在,“官方”版本不再得到支持。

你可以在以下位置找到项目页面:zipline.io.

)

你可以在以下 GitHub 位置找到源代码:github.com/quantopian/zipline.

)

QSTrader

QSTrader 是由 QuantStart 开发的。它再次是一个具有实时交易功能的研发框架。

该框架严格遵循我们在上一章中看到的构建交易应用的模块化原则,这也是本书的主要焦点。这有助于简化一般开发过程——研究 | 回测 | 纸面交易 | 实时交易——因为负责策略逻辑的代码保持不变。

目前,QSTrader 支持基于 bar 的数据,但也可以使用 tick 数据。

您可以在以下页面找到该项目:www.quantstart.com/qstrader/.

)

您可以在以下 GitHub 源代码中找到该项目:github.com/mhallsmoore/qstrader.

)

在交易策略开发中使用 Python 的缺点

在赞扬了 Python 在算法交易中的优势之后,现在是时候提到它的重要缺点了。正如许多强大而通用的生态系统一样,这些缺点是其优势的另一面。

无论如何,Python 最令人烦恼的事情是速度,或者更确切地说,是它的缺乏。部分原因是 Python 是一种解释型语言;然而,对整体缓慢贡献更大的因素是弱类型和我们在开发代码时非常喜爱的相同高级内存管理。

对于不熟悉内存管理的读者,我建议从一篇简单的文章开始,例如www.geeksforgeeks.org/memory-management-in-python/,该文章还提供了进一步阅读的参考。简而言之,如果语言减轻了程序员声明变量的负担,那么每次引用变量时,都会执行一系列程序来确保引用是正确的。当然,这会减慢整个代码的执行速度。

交易策略开发的主要部分,在代码执行速度不足的情况下变得明显,就是回测阶段。在回测过程中,我们应该以足够细粒度的分辨率处理所有历史数据,对于特定的策略来说,有时这可能低至原始的 tick 数据。如您所记得,这类数据的数量可能达到每秒数千 tick,所以想象一下我们会在循环中重复多少次整个策略逻辑来处理去年收到的每个 tick!

但在研发阶段,这种缓慢可能只是令人烦恼:确实,没有人愿意等待几分钟、有时是几个小时,甚至在最糟糕的情况下是几天,才能看到他们策略的理论性能。然而,等待时间过长是一回事,而在生产环境中无法进行交易则是完全不同的事情,而实时交易正是 Python 面临其极限的地方。

实时交易——Python 面临的极限

如此一来,纯 Python 编写的交易应用不适合任何假设从收到市场数据到发送订单的时间最小化的实时交易活动。因此,传统的套利和许多高频交易活动(有时建议每秒发送数千个订单)肯定不适合 Python。

此外,即使是来自自动化内存管理的慢速交易策略也存在另一种风险。我们已经知道,交易策略依赖于价格时间序列,处理的市场数据量可能相当大。尽管原生 Python 和第三方库如 pandas 提供了确保数据持久性的数据结构,但在高吞吐量的交易环境中,实时更新数据可能会变得有问题。

有多种方法可以在一定程度上加速 Python。例如,有静态编译器如 Cython (cython.org),它可以帮助更快地执行 Python 代码,并为 Python 编写 C 扩展。还有运行时翻译器如 Numba (numba.pydata.org),它也能以与 C 相当的速度执行 Python 代码。此外,使用numpy结构代替 pandas 也有帮助,因为尽管在某些方面会降低便利性,但我们可以获得速度上的提升。然而,在这本书中,我们不会真正关注这个问题,因为我们从更少延迟关键、更简单的策略开始,这些策略有助于我们理解交易应用的一般开发。

总结来说,我们可以这样说,Python 生态系统是研究和开发任何类型交易策略的绝佳工具。如果交易策略满足以下两个标准,它也可以用于实时自动化交易:

  • 它不需要接收大量实时市场数据

  • 它对内部延迟(数据接收和订单提交之间的延迟)不敏感

在这本书中,我们将主要关注使用 Python 进行研究和开发,以及模拟交易。

摘要

在本章中,我们考虑了使用 Python 进行算法交易策略研究和开发的优缺点。我们考虑了使用原生 Python 数据结构处理市场数据的各种选项。我们了解了各种生态系统、第三方库和环境,它们可以加快开发过程。我们还了解了开发过程中最重要的阶段和旨在确保策略在实时市场中具有盈利潜力的基本程序。

然而,与任何领域的任何项目一样,在我们开始实际编码之前,我们应该熟悉主题。在我们的情况下,那就是市场本身,它的基本要素、结构以及组织,这些都是我们将考虑的因素,以便了解它是如何运作的,以及我们在构建稳健的交易应用时应该考虑什么。这正是我们将在下一章中要做的。

第三章:从开发者的角度看外汇市场概述

外汇市场长期以来对开发者来说一直非常有吸引力,主要是因为与这个市场相关的免费东西很多,比如市场数据、交易软件和各种第三方解决方案。然而,这些免费解决方案的质量通常非常低,实际上根本无法用于任何严肃的交易。

外汇市场交易的主要问题是其强烈的碎片化。从历史上看,外汇是一个没有专用中心的银行间市场。因此,交易场所不仅提供不同的交易平台,还提供不同的市场数据、不同类型的订单和不同的流动性接入。这种碎片化可能相当令人困惑,因此,为了避免犯错误,有时这些错误可能相当痛苦,获得足够的理解是至关重要的。

在本章中,我们将从开发者的角度审视这个市场,以发现一些重要且被忽视的特征,这些特征可能对任何 FX 算法交易项目的整体成功至关重要。我们将从这个市场的组织结构开始,看看它为开发者创造的障碍。我们将了解市场参与者的基本类型,为什么价格会变动,以及特定的变动是否是我们能从中赚钱的。我们还将考虑各种进入这个市场的方式,它们在各种实际交易应用中的优缺点,并估计这可能需要多少成本。

所有这些知识对于在 FX 算法交易中持续成功至关重要。这个市场是变化最快的市场之一。如果不理解这些变化可能如何影响你的交易策略的表现,从长期来看,在这个市场中生存将非常困难。在本章中,我们将涵盖以下主题:

  • 交易场所——金钱相遇的地方……其他金钱

  • 交易机制——再次,一些术语

  • 做市商——舒适、复杂、昂贵

  • 流动性提供者——支撑这个星球的巨鲸

  • ECN——看起来像一场公平的游戏,但真的是这样吗?

  • 聚合——寻找最佳价格

  • 交易外汇市场——交易什么和如何交易

  • 我为什么需要所有这些?

交易场所——金钱相遇的地方……其他金钱

首先,让我指出,尽管这本书整体上使用了口语化的语言,但我始终努力坚持传统的学术方法,遵循相同的范式:定义 | 逻辑结论 | 理论 | 实验 | 证明。如果没有适当的定义,尤其是在我们谈论学科领域的基础时,我们将无法得出逻辑结论,提出一个理论,或者测试并最终证明一个理论——在我们的情况下,这意味着赚钱。用更简单的话说,如果没有对主题的全面理解,我们将无法提出适当的方法来使用它。

因此,正如我们在第一章中提到的,金融市场通过称为交易场所的特殊市场促进资产买卖。这有点过于模糊,不太有用,对吧?那么,让我们具体来看看。

组织混乱——交易场所的类型

金融世界的交易场所是买家和卖家相遇的地方——无论是物理上还是电子上。存在不同类型的交易场所,每种都有其优势和劣势,并需要特殊的交易方法。

今天交易场所的正式分类基于许多标准,包括监管环境、市场参与者的类型、市场信息在他们之间的传播方式等等。因此,这种分类非常复杂,肯定超出了本书的范围。

好消息是,对于我们这些买方交易应用的开发者来说,并非所有这些标准都重要。我们将尝试提出一些非正式的标准,以开发者的视角来看待这个主题领域。

从结构角度来看,交易场所之间的关键区别在于市场价格的确定方式。为了更好地理解这一点,我们再次考虑我们的农民市场例子。

拍卖(公开叫价)

买家和农民站在人群中,大声喊出他们想要购买或出售的数量以及他们的价格。一旦双方找到对方,就会签订合同。

这正是许多世纪甚至数千年来交易一直进行的方式。在金融世界中,这样的市场被称为交易所,而买家和卖家叫价的地方被称为交易池。尽管这种方法看起来公平,但它有一个全球性的内在限制:市场无法物理上容纳所有愿意交易的人。因此,交易者很快变成了精英,他们可以无限制地获得流动性,并开始代表他人进行交易以获取佣金。当然,这样的特权位置要求人们行为不端,许多交易者无法抗拒与客户对冲以翻倍利润的诱惑——这反过来又导致了法规的变化和严厉的惩罚。尽管非常有趣,但这又是另一个故事。

在过去 15 年中,其自然形式的交易池交易几乎完全消失。电子交易使市场对几乎任何人(尽管有重要的限制和约束——一如既往)都更加民主化和可获取,因此不再需要交易池交易者。

尽管如此,这种市场结构,其中通过拍卖中多方之间的直接沟通进行价格发现,仍然非常受欢迎。现代交易场所虽然电子化运营,但使用相同的范式。

我们可以看到,在这样的市场中,买方订单可以有两种下单方式。我们只需大声喊出,“我要买!”或“我要卖!”这样的订单将以最先出现的价格执行。这种订单被称为市价订单(参见*第十章,Python 中订单类型及其模拟),如果将市价订单发送到市场,我们说我们在市价**买入或卖出。

或者,我们可以喊出,“我愿意以 100 美元或更低的价格买入!”或“我愿意以 200 美元或更高的价格卖出!”然后这样的订单只有在有卖家或买家愿意以这样的价格进行交易时才会执行。这种订单被称为限价订单(同样,参见第十章**,Python 中订单类型及其模拟,详情请见),将这样的订单发送到市场称为以特定价格买入或卖出。

注意

尽管限价订单有助于限制价格(避免买高卖低),但并不能保证这样的订单一定会被执行:可能没有买家愿意以你的价格从你那里购买,或者没有卖家愿意以你期望的价格向你出售。

就像任何拍卖一样,交易资产的 价格可能会迅速上涨或下跌。因此,对于我们作为算法交易开发者来说,记住在这样一个市场中遵循一些有用的指南是很重要的:

  • 在检查供应之前,永远不要在市价买入。这样的行为意味着“我愿意以任何价格买入”并且你将以最糟糕的价格成交。

  • 永远不要在市价卖出。始终指定你愿意卖出的价格。提前发送你的卖出订单,标明你愿意卖出的价格是至关重要的。

交易所和订单簿

买家和农民到达市场,但并不进入。相反,他们被一位官员欢迎,官员在册子上记录他们的出价(买入订单)和出价(卖出订单)。如果一位新的买家带着 10 美元的出价进入,而官员的册子上已经有 10 美元的订单,那么这位新来者的订单将被放入队列,并且只有在先前的订单被执行之后才会被填满。

同样,农民不会在货架上展示他们的商品;相反,同样的官员会记录他们的出价(他们愿意以什么价格卖出商品)。与买家类似,如果一位新的农民带着以 11 美元的价格卖出的出价进入,而已经有另一位农民愿意以 11 美元的价格卖出,那么新来者的订单将在现有订单之后记录在册,并且只有在先前的订单被填满之后才会执行。

只要买家出价低于出价,而农民不愿意以更低的价格卖出,就不会达成合同,也不会发生交易。

因此,在这样的市场中,交易发生的唯一方式如下:

  • 一个买家同意以更高的价格买入

  • 一个农民同意以更低的价格卖出

虽然从常识的角度来看可能有点尴尬,但这样的市场组织有一个明显的优势:所有买家和卖家都按照先进先出FIFO)的原则服务,并且有平等的机会。

以这种方式运作的市场也被称为交易所,而登记出价和要价的官员今天已被计算机取代,通常被称为撮合引擎。记录出价和要价的簿称为订单簿。簿中订单的信息称为市场深度DOM

注意

开放式喊价和订单簿交易可以在同一交易所和同一市场上共存。例如,股票交易时段从开盘拍卖开始,然后通过订单簿继续交易。

最高出价和最低要价之间的差额称为价差。当有人达成交易时,这被称为跨越****价差

从程序员的视角来看,我们有一个二维结构,其中价格沿垂直轴,订单沿水平轴。当新的订单到来时,它们会被发送到每个价格水平上的相应 FIFO 队列,并被称为限价订单图 3.1展示了这种结构的示意图,其中水平通道中的每个方块代表作为限价订单大小的发送到订单簿的合约数量:

图 3.1 – 订单簿的二维性质

图 3.1 – 订单簿的二维性质

重要提示

如果你发送了一个订单,并且已经有 10 个订单在待处理中,你的订单将只有在所有 10 个先前的订单都处理完毕后才会被处理。

这就是为什么许多使用限价订单的交易策略仅在纸上有效,也就是说,它们依赖于所有生成订单的保证执行,而在现实中,只有 40-50%的这些订单确实被执行。我们将在第十章**,Python 中订单类型及其模拟的详细探讨中探讨这个问题和其他问题。图 3.2*显示了交易所交易资产的典型 DOM。

图 3.2 – DOM 屏幕显示了每个价格水平的综合流动性(二级数据) – 来源:MultiCharts

图 3.2 – DOM 屏幕显示了每个价格水平的综合流动性(二级数据) – 来源:MultiCharts

这个窗口显示了综合流动性而不是单个订单:特定价格水平的所有订单的大小被汇总,总和显示在价格轴的左侧或右侧。

重要提示

二级数据通常不包含关于单个订单的信息,因此你永远不知道 1000 个合约意味着一个来自大量交易者的单一订单,还是零售交易者通过每个合约发送的 1000 个订单。

交易所实际上是股票(股票、交易所交易基金等)、商品和一些衍生品的行业标准交易场所。在 FX 世界中,主要是衍生品,如货币期货,在交易所交易。大部分现金、掉期和远期合约都是在场外OTC)交易的。

场外交易市场

农民来到市场,但并不展示他们的商品。买家在他们之间走动,询问价格。农民看着买家,根据买家的外观决定提供什么价格。

这种情况听起来可能确实很荒谬,但它很好地展示了场外交易市场是如何运作的。

如前所述,在交易所交易市场中,任何时刻的所有市场参与者都知道三个关键价格:

  • 最佳买价

  • 最佳卖价

  • 最后交易

这些价格对每个人都是相同的,专业市场参与者与非专业市场参与者之间的主要区别在于数据馈送延迟和数据压缩(有关市场数据及相关问题的讨论,请参阅第一章**,开发交易策略 - 为什么它们不同)。因此,在交易所交易市场中,这三个价格通常被称为市场价格

在金融世界中,OTC一词意味着只要价格满足买方和卖方,就可以在几乎任何价格下签订合同——无论之前的合同是在什么价格下签订的。

从本质上讲,这在很大程度上使市场价格的概念变得毫无意义,因为并非所有这些价格都被报告并公开。因此,在场外交易市场中,没有像交易所交易市场那样的公开报价,而是使用指示性价格,这是最后交易价格或最后几个交易价格的近似表示。

到目前为止,我想你自然会问,“哇,但是谁使用这些市场,为什么?”答案非常简单,可以在交易所交易市场的结构中找到。

希望你能从上一节中回忆起来,任何交易所交易市场都是基于订单簿的。订单簿中的订单数量形成了该市场的流动性。

现在,想象一下,当一个大型的市场参与者(如对冲基金、共同基金、投资银行等)带着购买大量资产的愿望进入市场,而这个数量超过了现有的流动性。这样的行动会在一瞬间将最后交易价格移动到之前的读数非常远,甚至更糟的是——使订单簿的一侧完全空出。当然,这是一个所有市场参与者和交易所本身都希望避免的场景。

因此,这就是 OTC 市场。如果你是银行交易员,你可以在自己的联系网络中找到你大宗订单的最佳价格,并且可以私下进行,而不需要太多的喧嚣。

场外交易市场的另一个重要特征是,它们比常规的交易所交易市场受到的监管要少得多。有人试图对这些市场进行监管,其中最引人注目的可能是 MiFID 和 MiFID II,但仍然,OTC 市场要宽松得多。如果你对市场法规及其对定价、流动性和整体交易的影响感兴趣,我建议从tokenist.com/investing/guide-to-forex-regulations-in-the-us/开始学习,那里解释了基础知识。我还建议阅读与 NatWest 交易和销售交付部门负责人 Phil Lloyd 的访谈www.natwest.com/corporates/insights/regulation/regulation-and-market-structure-what-to-look-out-for-in-2021.html,他提供了关于全球挑战、法规变化及其与市场关系的非常有趣的见解。

理解场外交易市场(OTC)的结构可以引导我们得出许多非常重要的结论。

首先,场外交易市场非常适合大宗交易。如果你是一个小型零售交易者,那么在 OTC 市场进行交易可能会遇到问题,因为价格是任意的——你可以确信小型交易者总是得到他们订单最糟糕的执行。

第二,如果你发送一个市价订单(一个立即购买,无论价格的订单),你实际上无法知道你的订单将以什么价格执行。你永远无法就交易的价格进行争论。

注意

如前所述的直接推论,你应该始终检查你的订单实际执行的价格,并在测试时从你模型的预期回报中减去一定金额。这将使理论结果至少稍微接近现实。

第三个,也许是最反直觉的结论是,在 OTC 市场中,对于同一金融工具可能会有多个不同的价格。每个流动性提供者、做市商、银行、基金以及任何其他市场参与者都为同一产品提供自己的价格,并且这些价格在同一个时间点同时存在,这是正常的。

这种市场分割可以通过套利策略来利用:寻找在不同交易场所同一工具报价不同的情况。在价格差异足够的情况下,他们通过同时在不同的交易场所买卖同一工具来赚钱。

注意

可以使用 Python 开发和测试套利策略,但无法实际运行它们,因为套利机会通常只持续几毫秒。套利策略的生产代码通常用 C 或 C++编写,或者使用 Numba 或 Cython(见第二章使用 Python 进行交易策略)。

第四,你永远不能在这个市场上提出自己的价格——与交易所交易市场不同,在交易所交易市场中,任何订单,即使是来自零售交易者的一个合约订单,都会进入同一订单簿,对每个人来说都是相同的。你只能接受有权将价格发布到这个市场的其他市场参与者的价格。这些市场参与者被称为流动性提供者做市商(尽管重要的是要注意,这些不是同义词),他们通常被称为价格提供者,而零售交易者(或任何只接受报价的其它交易者)被称为价格接受者

最后,对我们这些外汇交易员来说最糟糕的消息是,大约 90%的外汇市场是在场外交易(OTC)进行的。因此,我们必须接受这个现实,并找到合适的解决方案来在这个相当不友好的环境中生存下去。

流动性和订单限制

在金融世界中,交易场所不仅是一个买方和卖方相遇的地方,而且交易是在场所管理机构的监督下进行的。因此,从这个角度来看,一个传统的农贸市场不是一个金融交易场所,而证券交易所肯定是一个。关键的区别在于,在交易场所的交易总是受到监督或调节的。例如,如果有人进入一个杂货市场,并提出购买卖家当时拥有的任何产品,那么很可能会发生的情况是,农民会卖掉产品,市场将在下周日关闭。如果在金融世界中发生类似的事情,那将是一场真正的灾难。因此,交易场所总是调节进入的订单,如果请求的交易规模太大,订单将被拒绝(或者更准确地说,被视为阻塞订单并按特殊方式处理——见第十章**,Python 中的订单类型及其模拟)。

订单执行的另一个可能问题是部分成交。这发生在订单簿中的流动性不足以完全填满订单时,因此只有订单的一部分被填满。通常,你可以明确指定订单类型,以决定在流动性不足的情况下订单是被拒绝还是部分成交。在任何情况下,你都必须在你的交易应用中添加一个订单执行控制模块,该模块处理、拒绝和部分成交订单。

以非期望的价格完成订单可能是你最不希望发生的事情,但如果你使用市价订单(记住,市价订单意味着“我想要现在买/卖,不管价格如何”),这种情况还是相当可能的。现在,你理解了为什么订单可能会以意外的价格成交——那些相同的流动性问题。因此,在生产环境中,即使策略假设在市场上买卖,考虑使用限价订单而不是市价订单也是合理的。

订单将在第十章“订单类型及其在 Python 中的模拟”中详细讨论。

现在我们对最常见的交易场所类型有了一定的了解,是时候让我们熟悉它们的市场参与者了。这将使我们更好地理解交易是如何进行的,以及我们应该在市场上预期和避免哪些风险。

但在我们继续讨论市场参与者之前,让我们学习一些新术语。

交易机制——再次,一些术语

当双方会面并同意相互买卖时,就完成了交易。这些方被称为对手方

如果我用一百万欧元兑换成美元,并且汇率是 1.1,那么我支付了 110 万美元来持有 1 百万 EURUSD 的头寸。我将持有这个头寸,直到我以等值的美元卖回这些欧元,从而清算我的头寸,再次成为市场中性,直到我开设新的头寸。

如果我通过发送低于先前最佳出价的出价来提高要价,那么我向市场提供流动性

如果我同时提高要价出价,并且其他交易者成为双方的对手方,那么我就制造市场。制造市场意味着赚取价差(出价和要价之间的差额)同时保持市场中性。主要业务是制造市场的市场参与者被称为做市商

与成为市场或价格提供者相反,接受交易另一方的交易者被称为价格接受者。价格接受者只能以价格提供者提供的价格买卖,而且与价格提供者不同,价格接受者总是支付**价差

重要

如果一个价格接受者使用市价订单开设任何头寸,那么在头寸开设的瞬间,其运行利润就已经进入了负值区域。

这是因为,在大多数市场中,卖出价格总是高于买入价格,而价格接受者只能以卖出价格买入,以买入价格卖出。因此,如果你购买了一种资产,这意味着你支付了卖出价格。如果你想立即平仓你的头寸,你只能以买入价格这样做。如果自你进入市场以来市场价格没有变化,你将损失买入和卖出价格之间的差额——这就是价差。这就是为什么我们说价格接受者总是向价格提供者支付价差,而价格提供者反过来赚取价差。

如果我购买了一种资产,那么我就有一个多头头寸。这个术语来自股票交易,意味着你购买了一些东西,现在持有它(通常,投资者会长期持有股票)。如果我出售了一种资产,并且这次出售不是之前开立的多头头寸的平仓,那么我就有一个空头头寸。同样,这个术语有相同的起源,意味着你在没有拥有它的情况下出售了某物。

你可能会想知道如何卖出你并不拥有的东西。好吧,在金融市场这真的很简单。如果我们谈论股票交易,那么大多数经纪人都有一份库存,交易者可以从这份库存中借来出售。如果以这种方式出售的资产价格进一步下跌,那么交易者就会平仓头寸,将资产归还给经纪人,并从中获得一些利润。如果资产价格上升……好吧,交易者无论如何都会平仓头寸,将资产归还给经纪人,并支付差额——这意味着这次会亏损。

如果我持有头寸,那么我的利润(或亏损)将计算为平仓价格与购买价格之间的差额。如果我开了一个多头头寸,并且平仓价格高于开仓价格,那么我就赚了钱;如果它更低,那么我就亏损。对于空头头寸来说,情况是对称的:如果平仓价格低于开仓价格,那么我就赚了钱,反之亦然。

只要我持有开放的头寸,我的潜在利润或亏损可以在任何时刻重新计算,即使头寸尚未平仓。这种潜在利润或亏损被称为滚动利润或亏损滚动 PnLP/L)。

要能够向市场发送订单,我必须要么成为有权直接访问订单簿的交易所的成员(这相当昂贵),要么使用第三方服务,该服务接受我的订单并将它们带到市场。这样的第三方被称为经纪人

重要提示

在外汇交易中,经纪人和做市商之间存在着很多混淆。请仔细阅读以下部分,因为我们将要一次性澄清这种混淆。

如果我有 100 美元,欧元兑美元的汇率为 1.1,那么我可以购买大约 91 欧元。但如果经纪人给我提供信用额度,那么我可以用 100 美元购买从 3000 欧元到 9000 欧元,甚至更多。这个信用额度被称为保证金,而我能够不使用保证金购买的数量与使用保证金可以购买的数量之比称为杠杆率。通常,外汇市场上的杠杆率对非专业人士高达 30:1,对专业交易者高达 100:1。

如果我在保证金上开仓,并且市场价格变动对我不利,以至于我的交易账户中没有任何资金,那么我就进入了被称为保证金追缴的情况。从历史上看,这意味着经纪人会联系客户,要求向账户添加资金以维持仓位。这种做法现在很难找到,至少在零售经纪人中是这样。今天,经纪人更愿意强制平仓那些将客户的账户推入负区的仓位,否则,这不仅可能给客户带来问题,也可能给经纪人带来问题。

如果我在保证金上开仓,那么我的仓位大小被称为名义金额。这意味着,如果我使用账户中的 100 美元以 100:1 的杠杆在 EURUSD 上开了一个多头仓位,那么我实际上并不拥有 9000 欧元。我有一个名义上的 9000 欧元仓位,我必须在任何实际资金进入(或从)交易账户之前将其平仓。

如果我直接购买资产,那么它是一个现货资产。如果我购买一个给予我或使我未来有权或义务买卖资产金融工具,那么它是一个衍生品资产。实际上,衍生品可能比这更复杂,但它们主要只对专业市场参与者可用。关于衍生品的讨论超出了本书的范围。

如果我购买了一个在未来某个日期以某个价格买卖资产的义务,那么我就是在购买一个期货合约,或者简单地称为期货。这个日期被称为到期日或简单地称为到期。例如,当前资产的价格是 10 美元,但我可能购买一个保证在一个月、一个季度或任何其他时间段内以 9 美元将同一资产卖给我的期货合约。那么,这个月、这个季度或这个其他时间段就是该期货合约的到期期限。如果资产本身的价格在其到期日时高于 9 美元,那么我就赚钱了:期货的卖方有义务以 9 美元的价格将资产卖给我,而我可以立即以更高的价格将其转售。如果资产在到期日的价格低于 9 美元,那么我就亏损了,因为我现在有义务以 9 美元的价格购买资产,并且我可以随意处理它(最可能的是出售并承担损失)。

如果我购买在未来以特定价格购买或出售资产的权利,那么我是在购买一个期权。期权交易是一个相当复杂的话题,本书不予考虑。

关于术语就讲到这里。现在我们已经了解了关于关键市场参与者的所有需要学习的内容,我们需要知道他们做什么,以及我们如何实际上参与这个市场。

做市商——舒适、复杂、昂贵

“做市商”这个术语实际上有两个不同的含义,尽管它们非常接近。

在买家和卖家需要在单个市场(在大多数情况下,如我们之前所见,这是一个交易所)相遇的情况下——这种市场被称为双边市场。在双边市场中,做市商为市场提供流动性,这项活动将在流动性提供者——支持这个 星球 的鲸鱼部分进行考虑。

在场外交易市场中,情况不同。在这些市场中,只有专门的市场参与者可以发布他们的出价或要价,在某些情况下,它们甚至根本不发布。因此,寻找交易对手方进行交易可能比在双边市场中更加复杂,而且由于报价不是公开的,你可能会以非常令人惊讶的价格执行你的订单(当然,这不会是一个愉快的惊喜)。

因此,在这个时候,我们来看看做市商。在场外交易市场中,做市商既作为流动性提供者,也作为他们自己客户的对手方。

这意味着什么呢?

提供流动性的交易对手方

如果你来到一个交易所,那么你的订单对所有其他市场参与者都是可见的,任何人都可以接受订单的另一面。比如说,你只想买一份货币期货合约——那么你实际上可以从零售交易员和大型银行那里购买(但,你很可能永远不知道卖家是谁)。但是如果你与做市商交易,那么正是这个做市商——而不是其他人——将成为你所有订单的对手方。只有这个做市商才会始终从你这里购买并向你出售。

这意味着做市商是专业的市场参与者,为他们的客户提供市场报价。这些报价对不同客户而言是不同的,主要取决于客户的交易量和订单规模。

做市商是如何赚钱的?

做市商通过赚取价差(出价和要价之间的差额)来获得回报,在这方面,他们的业务与流动性提供者的业务非常相似。做市商总是以高于他们准备购买的价格出售。因此,在存在多个客户且这些客户产生持续订单流(几乎同时购买和出售的订单流)的情况下,我们的做市商会同时买入和卖出,赚取出价和要价之间的差额。

正如你所见,做市商的利润来源与他们的客户截然不同。作为价格提供者(见场外交易市场部分),做市商保持市场中性,而他们的客户由于客户购买了某物但尚未出售的情况,在市场上开仓和平仓头寸

然而,我从那里购买并随后将我的头寸卖出的做市商在我不持有头寸时保持市场中性,因为他们有多个客户,而且很可能在我从做市商那里购买 100 万欧元/美元的同时,另一个客户正在向他们出售 100 万欧元/美元。因此,做市商赚取了价差,我和另一个未知的交易者带着两个头寸离开了:我持有多头头寸,而那个未知的交易者持有空头头寸。

这看起来像是一顿免费的午餐(当然,对于做市商来说)和一种不公平的优势(再次,对于做市商),但在现实中,这仅仅是另一种有风险的业务——因为世界上的一切交易都是如此。想象一下这种情况:有人从做市商那里购买了东西,但没有人出售。现在,想象这种情况发生了一次,然后很快又发生了。当大多数交易者认为价格将朝着某个方向移动,并开始大量买入或卖出时,这种场景很容易出现。对于做市商来说,这意味着他们不再赚取价差。更糟糕的是,在这种情况下,做市商在市场上持有开放的头寸——而不是市场中性——并且变成了一个交易者,他们的损益现在取决于价格变动!此外,这个头寸立即为做市商创造了一个浮动的损失(浮动意味着该头寸尚未清算,因此损失可能会随着每个新点的变动而减少或进一步增加)。当然,做市商不喜欢持有头寸,因此他们保护自己。

市场风险以及做市商如何缓解它

做市商缓解市场风险有两个基本的选择。

首先,做市商可以在其他地方对冲他们的净头寸。为了更好地理解这一点,让我们来看一个例子。

在所谓的正常交易时间内,平均而言,开仓的多头和空头头寸的数量或多或少是相同的。正如我们已经知道的,OTC 市场总体上以及外汇市场特别缺乏公开可用的数据,但即使从公开来源,我们也可以看到这个说法非常接近现实。例如,FXSSI 有一些很好的工具,可以可视化交易者的情绪、开仓的多头和空头头寸的比例以及市场深度的快照。您可以在fxssi.com/tools/找到这些工具,并自行检查。特别注意开仓头寸比例(fxssi.com/tools/ratios)。这些图表报告了来自多个交易场所的头寸,很明显,交易场所的客户基础越大,开仓头寸比例的图表就越线性。这实际上意味着这个比例在时间上变化不大,并且新开仓的多头头寸与新的空头头寸是平衡的。

如果我们将所有多头头寸相加,并从这个总数中减去这个市场制造者目前开仓的所有空头头寸的数量,那么我们就得到了这个市场制造者的所谓净头寸。在正常市场条件下,并且假设市场制造者有足够的客户,这个净头寸总是接近零。

如果市场出现恐慌,或者由于任何原因,大多数交易者开始开仓的多头头寸多于空头(或反之),或者只有一个但规模非常大的交易者开仓了一个非常大的头寸,那么市场制造者的净头寸将大大超过零。在这种情况下,市场制造者将开仓另一个与自己的净头寸相等但方向相反的头寸。比如说,市场制造者有一个客户的净多头头寸为 100 万,那么市场制造者将开仓一个 100 万的空头头寸。在这种情况下,市场制造者保持市场中性。

你可能会想知道市场制造者如何为自己开仓。这是可能的,因为场外交易市场非常分散,有许多大型的和小型的市场参与者,他们可能直接或间接地连接。例如,市场制造者可以轻松地获取大型银行提供的流动性,因此可以在那里对冲他们的净头寸,但他们的客户,即零售交易者,实际上不太可能获得相同的流动性。

市场制造者为了保护自己可以采取的第二件事是增加点差。正如你将记得的,在 OTC 市场中,点差不是由所有市场参与者决定的,因为没有集中的交易所和单一订单簿。相反,任何价格提供者都可以提供自己的价格,市场制造者也不例外。通常,市场制造者能够获得比提供给客户的更好的买卖价格。从这个意义上说,市场制造者充当零售商,以较低的价格购买批发流动性,并以较高的价格向零售商重新销售。

当然,宽泛的点差对交易者来说并不好。然而,有几个原因,我们将在本节末尾更详细地讨论,这些原因可能会让我们考虑更宽的点差作为我们为了获得一些好处而支付的额外交易成本。

现在,我们已经到了引起许多交易者批评的非常关键的一点。

问题是,在 OTC 市场中,由于价格发现不统一,同一资产的价格在不同交易场所、一个价格提供者到另一个价格提供者之间都有所不同,尤其是在监管薄弱的 OTC 市场中,市场制造者对他们自己的客户进行欺诈的可能性相当大。

这种行为(或者更确切地说,这种不当行为)在 21 世纪初尤为普遍,当时外汇交易正变得越来越流行。许多自称为经纪人的市场制造者出现,提供荒谬的杠杆率,如 200:1、500:1,甚至 1000:1,承诺在真实外汇中为客户开设头寸,并接受微不足道的账户,如 100 美元、10 美元,甚至 1 美元。这些市场制造者的主要收入来源不是点差,而是他们的客户账户,因为在如此极端的风险条件下,超过 80%的客户在不到一个月内就完全损失了他们的资金。

显然,一个持续赢利的客户对这种市场制造者来说是一场灾难。任何以盈利平仓的头寸都是这种业务的直接损失。因此,这种类型的市场制造者经常通过提供更差的报价,甚至没有任何严重理由地拒绝他们的订单,来与自己的客户进行对赌。他们的唯一目标就是让成功交易者的交易变得非常不舒服,以至于他们宁愿关闭账户,在其他地方尝试运气。

这种不公平和恶劣的行为?

当然。

与市场制造者进行交易是否是零售和机构交易者在外汇市场上进行交易的唯一方式?

不,有其他选择,我们将在本章后面讨论它们。

但为什么还要与市场制造者进行交易呢?

好吧,原因不止一个,而且它们都很充分。

与市场制造者进行交易的原因

首先,自零售外汇交易初期以来,法规已经得到改善,如今,这种(不当)行为对于受监管的市场制造者几乎是不可能的。

此外,法规要求市场做市商明确称自己为市场做市商,经纪商明确称自己为经纪商。这结束了近二十年来两者之间的几乎所有的混淆,这损害了许多公平企业的声誉,以及外汇市场本身,至少在零售交易者的眼中。

第二,监管机构要求受监管的市场做市商必须保持流动性和在任何时候几乎都能报价市场,即使市场出现恐慌。这意味着你可以随时开仓或平仓,这与你在交易所开仓或平仓的方式正好相反。高级交易者甚至可以利用这一点,在重要经济新闻发布前后进行交易,市场流动性迅速蒸发,价格可能在不同交易场所之间差异很大——这为套利交易开辟了极好的机会。我们将在第九章**,交易策略及其核心要素*中考虑各种形式的套利。

为了能够在最灾难性的价格变动中生存下来,监管机构要求市场做市商拥有足够的资本。2015 年著名的货币崩溃,瑞士法郎在几分钟内对欧元升值近一倍,清楚地显示了哪些市场做市商满足了监管要求:他们仍在营业,而其他人则被淘汰。如果你对 2015 年 1 月 15 日发生的事情感兴趣了解更多信息,我建议阅读这篇文章(fbs.com/analytics/news/5-years-aninversary-of-the-notorious-eurchf-usdchf-crash-7595),这篇文章更多地关注事件的宏观经济方面,或者阅读这篇文章(fxssi.com/swiss-franc-15-january-2015),从更技术性的角度审视事件的顺序。

第三,市场做市商非常适合大量交易者。如果你需要填写超出当前订单簿典型流动性的订单,你只有两个选择:将你的订单分成几部分逐一填写(在此过程中没有任何保证价格保持不变)或者向市场做市商索要报价,一次性填写整个金额。这确实是一个很大的优势,不是吗?

第四,市场做市商通常提供的产品在双边市场中很难找到。一个很好的例子可能是二元期权(根据某个事件是否发生,结果为全有或全无的合约,类似于赌博)或结构化产品(例如,固定收益、货币和股票作为单一合约交易)。

最后,做市商可以提供与标的资产定价非常接近的衍生品,但技术上并不被视为该资产本身。最著名的衍生品之一是差价合约CFDs)——这是一种与做市商签订的合约,根据该合约,如果购买合约后标的资产价格上升(或者如果卖出合约后资产价格下降),做市商有义务向你支付价差。因此,CFD 通常作为资产的代理。使用 CFD 而不是标的资产的原因是,在某些司法管辖区,CFD 交易的任何收入都不征税。这不是考虑与信誉良好的受监管做市商进行交易的好理由吗?

当然,做市商并不是外汇市场中唯一重要的市场参与者。让我们来了解另一种同样重要的市场参与者类型。

流动性提供者——支撑这个星球的大鲸鱼

在上一节中,我们已经指出,做市商的一些活动与流动性提供者LPs)的活动相似。LPs 是一种市场参与者,其业务是通过向市场提供流动性来赚取价差,也就是说,始终在订单簿的两边同时维持买卖订单。因此,与做市商的情况一样,LPs 作为价格提供者赚取价差。

在双边交易所交易市场中,很难区分做市商和流动性提供者。然而,在场外交易市场中,这一点变得至关重要。

在场外交易市场中,做市商是一个为自身客户报价并为它们提供市场报价的实体。流动性提供者通常确实有客户,这些客户直接与他们交易,无论客户是小型零售客户还是大型基金。LPs 只为订单簿——或者多个订单簿——提供流动性,因为场外交易市场非常分散,同一金融工具存在多个交易场所(参见场外交易市场部分)。因此,典型的 LP 客户是银行、ECNs 和经纪商。

许多交易者认为 LPs 是市场的寄生虫,如果没有他们,定价将更加透明,他们的订单执行将更好,最终,他们将会赚更多钱(或者更确切地说,他们将会赚钱而不是亏损)。这是一个常见的错觉,我们将探讨原因。

你还记得我们在交易所和订单簿部分看到的典型订单簿吗?它清楚地显示了市场当前存在的流动性。在那个特定的例子中,每个价格水平都列出了几百个订单,对于一个大型市场来说实际上并不多。那么,如果一个大订单,比如购买几千份合约的订单进入市场会发生什么呢?

这样的订单将立即扫清订单簿,首先从最优卖价水平开始购买所有报价,然后是下一个价格水平,接着是下一个,再下一个,推动价格不断上升,越来越高。所有这些都会在千分之一秒内发生,以至于其他市场参与者没有时间做出反应。

相反,如果订单簿中有足够的流动性,那么价格波动就会变得更加温和,因为推动价格上下大幅变动变得更加困难。

注意

因此,LP 在 OTC 市场中扮演着非常重要的角色:他们促进了大量交易——不仅对大型交易者,而且对任何买方交易者,包括零售商。

同时,LP(流动性提供者)使得投机者的生活更加困难,因为市场流动性越大,价格波动越小,基于古老原则“买低,卖高”的买方投机获利就越困难。

每个投机交易者都必须明白,市场不是为了投机而发明的。任何金融市场的目的是促进商品或货币的交换,并找到它们的最优价格。通过投机赚钱的可能性只是市场的副作用,而不是市场本身的目的。

流动性和波动性——如何相互转化

随着 LP 数量的增加,市场价格变得越来越不稳定。波动性是市场价格和交易策略回报的关键指标之一。简单来说,波动性意味着价格移动的容易程度:波动性越大,意味着价格在同一时间段内预期会上下跳动的距离越大,而波动性越小,意味着价格在同一时间段内预期会移动到任何显著的距离。有许多方法可以衡量波动性,这个主题本身也非常复杂和多元(甚至有一个专门的术语波动性交易),但这超出了本书的范围。现在,只需记住,波动性意味着价格在任意方向移动的容易程度。

我们可以通过计算固定时间段内最高价和最低价之间的差异,并将其绘制成直方图来表示波动性。图 3.3显示了欧元对美元的波动性,以一分钟为间隔进行采样:

图 3.3 – 一天内波动性的简单表示(图表由 MultiCharts 提供)

图 3.3 – 一天内波动性的简单表示(图表由 MultiCharts 提供)

在这个示例中,我们可以清楚地看到,这个市场日内波动性(测量采样率小于 24 小时的波动性)是周期性的,并且在夜间总是较低。这是非常自然的,因为大多数大型市场参与者在正常工作时间进行交易。

然而,这个说明并没有给我们一个关于 LPs 如何影响波动性的概念。为了看到这种影响,我们需要看一个更大的图景。让我们绘制每日时间间隔的波动性相同表示,并查看其历史,追溯到 21 世纪初。图 3.4显示了欧元对美元的波动性图表:

图 3.4 – 每日波动性,EURUSD(由 MultiCharts 制作)

图 3.4 – 每日波动性,EURUSD(由 MultiCharts 制作)

在这个说明中,我们可以看到从金融危机(明显表现为日波动性增加)开始的多年周期,以及随后持续 4-5 年的波动性连续下降。

这种现象的一个可能解释是,在危机时期,LPs 和做市商是首先遭受需求或供应急剧增加的受害者,因为他们有义务在市场中维持流动性。因此,在卖方最初的一系列重大损失和买方的保证金调用之后,LPs 审查他们的风险指标,并开始从市场中撤出流动性。我们已经知道,当流动性下降时,波动性就会上升;这就是我们,作为交易员,观察到的剧烈价格波动,而世界其他地方称之为危机灾难

总的来说,LPs 和做市商在保持市场稳定和确保其效率方面发挥着非常重要的作用——即在任何时候都能以任何数量买卖资产的能力。

我们已经熟悉了两个最重要的卖方市场参与者,并且我们已经知道包括我们自己在内的许多买方市场参与者。但是,我们是如何找到彼此的呢?所有的订单都发送到哪里,又是如何匹配的呢?

ECN – 看起来像是一场公平的游戏,但真的是这样吗?

当我谈论场外交易市场的结构,特别是其中的价格仅由少数市场参与者提供,并且对于不同的买方客户可能会有很大差异时,许多交易员会说,“这并不公平!为什么外汇不能像其他有中央交易所的受监管市场一样工作,任何人都可以改善价格?”

对于这个问题,没有单一的简单答案。

首先,外汇市场是货币对货币的交易,而不是任何资产对货币的交易。它的原始目的是促进货币兑换,而不是使用各种金融工具进行投机交易。幸运的是,我们不必去交易所只是为了兑换一些英镑换成欧元,或者印度卢比换成美元。因此,场外交易对于这个目的来说是非常自然的,货币兑换店就是一个很好的场外交易例子。

其次,外汇市场由多种金融工具组成,这些工具在规格上相当灵活。例如,现金只占这个市场的一小部分,而远期合约占其日交易量的 50%。这些远期合约可以在银行和其他市场参与者之间直接进行,无需任何交易所的介入。我们将在本章稍后讨论外汇工具,在外汇工具部分。

然而,随着时间的推移和市场越来越计算机化,交易者认为这种市场设计还不够公平。许多专业买方交易员、做市商、LP 和经纪人都有同样的看法。买方交易员寻求更好的、更有效的方法来寻找市场中的最佳价格,而不仅仅是通过电话联系多家银行并与交易员交谈。卖方交易员热衷于提供最佳的买卖报价,因为这增加了赚取价差的盈利机会。

当然,这不仅适用于外汇市场,也适用于任何市场:争夺最佳执行的战斗有着悠久的历史。1969 年,第一个允许合格市场参与者进行买卖报价的电子网络被引入。这是一个革命性的改进,使得谈判价格和寻找最佳买卖报价比以前快得多。证券交易委员会SEC)认可了这个网络,并将其命名为电子通信网络ECN)。1975 年,SEC 通过了《证券法》的修正案(这是规范美国证券交易的主要法律文件),为 ECN 和电子交易的爆炸性增长打开了大门。

那么,ECN 究竟是什么呢?

ECN 的组织结构

如果你连接到 ECN 并从那里检索市场数据,你将看到与从交易所获得的市场深度数据非常相似的东西。你将看到每个价格水平上的多个价格水平和流动性。根据你的订阅,你可能只能看到顶级水平、前 5 个水平、前 10 个水平,或者整个订单簿。你可以等待你的交易策略所需的价格,然后发送一个买入或卖出的订单——这基本上就像你在交易所做的那样。

那么,我们可以说 ECN 是一个交易所吗?

不,不是这样。

ECN 与交易所之间的关键区别是其订单簿的可用性。在交易所,其订单簿对任何市场参与者发出的任何订单都是可用的。任何人都可以发送任何大小的订单,它将进入单一、通用的订单簿。在 ECN 中,只有合格的市场参与者可以向订单簿发送他们的买卖报价。在外汇市场中,这些主要是银行、LP、做市商和其他专业卖方市场参与者。简单来说,你很可能无法在 ECN 的订单簿中发送自己的买卖报价。

在这一点上,我们应该回顾一下正在交易所进行的交易机制描述。如果你发送一个限价订单——即以分别低于或高于当前最后价格的价格买入或卖出资产——那么你的订单将进入相应的价格级别,并添加到队列的末尾(见交易所和订单簿部分)。市场中的任何人都可以看到这个订单,因为在这个价格级别的流动性将会增加。

然而,如果你在 ECN 上做同样的事情,订单簿在视觉上不会改变。相反,你的订单被 ECN 匹配引擎保留,并且只有在最后价格接近这个订单时才会执行。没有人,包括你自己,会在订单簿中看到你的订单,你也没有能力改善最佳买入价和最佳卖出价。只有合格的 ECN 成员才能改善最佳买入价和卖出价。

ECN – 并非所有市场参与者都是平等的

因此,回到本节开头关于市场公平性的讨论,我们现在可以这样说,ECN 是一个公平的市场吗?好吧,如果与交易所相比——可能不是。但它解决了场外交易市场的主要问题:它促进了多个卖方市场参与者之间的价格发现,增加了他们之间的竞争,从而改善了卖方的最佳买入/卖出价。

使用 ECN 进行交易最初仅限于专业市场参与者,但超过十年,它几乎对任何人都是可用的。你只需要找到一个支持直通处理STP)业务模式的经纪人。STP 意味着经纪人不会作为其客户的市商行事,他们只将客户订单路由到 ECN。

让我们再次回顾一下使用 ECN 的交易。LP 和市场制造者提交他们的买入和卖出报价,而买方交易者消耗这种流动性。

现在,让我们进行同样的想象实验——对这种结构进行压力测试,就像我们对交易所订单簿所做的那样。让我们想象一个大量买方交易者带着一个订单进入,要买入或卖出比订单簿中现有的更多的资产。

在大多数情况下,这样的订单将扫清订单簿中的全部流动性,使其中一边暂时为空——这对 LP 来说是不太可接受的。所以,如果交易者只做一次,那么很可能会没有反应——LP 会重新平衡他们的订单簿,流动性重新出现在市场上。但如果这个交易者重复这个动作,那么这种活动被认为是掠夺性的,而交易者产生的订单流被称为有毒的。可能会随之而来一系列的制裁,从经纪人的电话到 ECN 的永久禁止。

你可能认为这个问题与小型交易者,如零售交易者,无关,但实际上是有关的。

让我们回顾一下有毒订单流量的定义:它是一系列系统性地、持续性地试图洗劫订单簿的订单,也就是说,消耗(出价或要价)两边的全部流动性。

在正常营业时间内,使用小订单量确实很难实现。但是,不要忘记并非所有 LP(流动性提供者)都全天候工作。此外,有时会有一些时刻,甚至持续几分钟的时间段,ECN 的流动性实际上非常接近零(参见本章“外汇工具”部分中关于银行结算和相关问题的解释)。如果在这样的时刻,即使是小型零售交易者发送一个小订单,也会完全耗尽微薄的订单簿,导致以荒谬的价格执行该订单,并且经纪人或交易所会打电话来询问交易者的行为,这些问题可能令人不快。

注意

如果你与一个 ECN 进行交易,在发送任何订单之前,一定要检查订单簿中的流动性,以避免真正不愉快的情况。

今天,在 ECN 上进行交易确保了第二高效的定价发现过程。但是,最有效的是什么,你可能想知道。好吧,我们现在将要讨论聚合

聚合——寻找最佳价格

你还记得吗?在一般情况下,任何场外交易市场,尤其是外汇市场,都是高度分割的?在相同的时间,有多个市场参与者为同一资产提供不同的价格。

这对任何市场参与者以及任何交易场所都适用,ECN 也不例外。如果 ECNs 是封闭系统,没有任何与外界联系,那么价格可能潜在地与外汇市场的其他价格非常不同。确实,如果我们有一个类似交易所的交易场所,尽管只有一小部分合格成员发布出价和要价,但价格将由供需决定,就像在任何其他市场中一样。这意味着价格接受者将是那些推动价格上涨或下跌的人。在一个封闭系统中,价格仅由该系统的成员决定。

从理论上讲,似乎没有任何东西可以阻止同一欧元兑美元的价格在一个 ECN 上是 1,在另一个 ECN 上是 2,在第三个 ECN 上是 10——只要第三个 ECN 的成员认为欧元应该值那么多!

然而——幸运的是——这并不是真的。如果每个 ECN 都交易一种单独的资产,比如一种独特的货币,那么的确,其价格将仅由这个 ECN 的供需决定。但是,只要他们都在交易相同的资产,就像我们的例子一样,有人可以在一个 ECN 以 1 美元的价格购买同一欧元,然后在另一个 ECN 以 10 美元的价格卖出。请确信这些聪明的交易者总是存在于任何市场中。他们被称为套利者,他们的交易策略被称为套利

此外,ECN 不是封闭系统。所有 LPs 都参与多个 ECNs 和其他流动性池;其中许多运行自己的交易台或作为其客户的做市商。此外,一个 ECN 还可以作为另一个 ECN 的 LP!

因此,除了集中市场之外,我们还可以看到一个非常碎片化、非常复杂的网络,其中市场参与者相互连接,并与其他群体相连,然后这些群体也相互连接,如此等等。买方交易者(套利者)和卖方市场参与者(LPs 和做市商)使整个外汇市场的价格比我们之前简化的例子中更加统一。

尽管我们不再期望从一个交易场所到另一个交易场所的价格差异达到 1000%那么剧烈,但由于外汇市场的内在碎片化,价格仍然在这里那里略有不同。例如,同样的欧元在一个 ECN 报价为 1.13458,而在另一个 ECN 报价为 1.13459,同时市场做市商可能报价为 1.13461,而银行报价为 1.13456。这种差异非常微小,按照描述在今天的 FX 市场中进行经典套利是非常困难的。然而,我们可以从另一个角度看待这种价格多样性:如果我们能够通过检查所有可用的价格来找到市场中的绝对最佳价格会怎样?

这就是价格聚合的核心思想。聚合器本身并不是一个交易场所,因为它没有自己的订单簿和流动性提供者(LPs)。相反,聚合器会扫描交易场所,寻找绝对最佳的买入和卖出价格,然后将订单路由到相应的场所。

由于聚合的性质,通常在极短的时间内,比如毫秒或更短,最佳买入价格会高于最佳卖出价格。想想看!你可以买入的价格低于你可以卖出的价格!难怪聚合器很快就成为了套利交易者的首选交易场所——而且不仅仅是他们。实际上,价格聚合是在碎片化的场外交易市场中获得最佳执行的方式。

不幸的是,目前聚合器仅对专业市场参与者,如银行和经纪人开放。因此,ECN 交易在更广泛的受众中仍然是效率最高的。

外汇市场交易——交易什么以及如何交易

我们已经讨论了很多关于在哪里可以进行交易的问题,但等等!我们完全忘记了讨论可以交易什么以及如何进行交易。让我们立即填补这个空白。

外汇工具

你可能听说过外汇市场是世界上最大的市场,日交易量超过 2 万亿美元。但你有没有想过在这样的巨额交易中,交易的是什么工具?你想象的是现金堆或金币吗?也许是从一个银行账户到另一个银行账户的即时转账?

当然不是,这个市场并不是这样运作的。当我们说外汇时,我们通常是指交易一种货币兑换另一种货币的义务。

如果我来到一家货币兑换店,用 100 欧元兑换等值的英镑,那么这样的交易会立即完成。作为卖方,我交付欧元,作为买方,商店交付英镑给我,这一切都是在达成交易的那一刻完成的。

然而,如果我想用 100 万欧元兑换相应数量的英镑,我宁愿打电话给我的银行询问价格。银行会很乐意提供一个特别报价,因为兑换的金额非常有吸引力。这个报价将远远优于现金兑换店能提供的任何报价。

现在明白了吗?这就是在 OTC 市场中同一资产可能存在多个价格的原因。

我甚至可以更进一步,与各种金融机构检查价格,特别是那些专门从事货币兑换和国际货币转移的机构——而且,很可能会找到我同意进行交易的最佳报价。

但金融机构给出的价格绝不是市场上可能给出的最佳报价。然后他们会去大型外汇市场,以高于从我这购买欧元的价格出售欧元兑换英镑。

这可能看起来简单直接,但实际上并非如此。

问题是我现在得到了一个交易,但我的钱从我的银行账户转移到选择的进行兑换的金融机构可能需要时间。汇率已经与我确定。因此,对于这家金融机构来说,能够在与我达成交易的那一刻立即对冲其头寸或清算它至关重要,而不是在稍后,因为市场价格可能完全不同,他们可能会因这种操作而遭受损失。

因此,金融机构将其报价放在所谓的远期市场上。远期合约是在合约签订时即确定价格,并在特定日期交付实物商品(或,在货币市场上,货币)的义务。例如,同一家金融机构可以出售一份远期合约,意味着,“我们现在将欧元对英镑的汇率固定在 0.85,但我会在稍后向您交付欧元。”

在一个工作日,金融机构可以与各种交易对手方签订许多远期合约。为了了解他们欠谁的债务以及其他人欠他们多少,有一个特殊的过程叫做结算。这是对所有市场参与者的相互义务进行净额结算,并计算他们的每日盈亏。

注意

对于大多数涉及美元的货币对,结算时间是纽约时间下午 5 点。在这一点上,特别小心进行交易非常重要,因为市场流动性几乎为零。总是最好在结算前 15 分钟和结算后 15-30 分钟内避免交易。

在众多远期合约中,交易量大的合约包括以下几种:

  • 今日TOD)- 一种在当天交易日的交割远期合约,或称 T

  • 次日TOM)- 一种在下一个交易日交割的远期合约,或称 T+1

  • 即期SPOT)- 一种在 2 个交易日后交割的远期合约,或称 T+2

截至 2022 年,所有外汇市场的日均交易量约为 6.6 万亿美元。即期是远期平均日均交易量中最大的一部分,价值约为 2 万亿美元。其他远期合约再贡献 1 万亿美元。剩余的 3.6 万亿美元主要被互换合约互换占据。

互换是非常有趣的金融工具,主要被制造商和进出口商用来对冲他们的货币风险。如果我与你签订一个互换合约,这意味着我给你一定数量的欧元,而你给我一定数量的英镑(或任何其他货币),并且我们固定汇率。然后,在一段时间后,通常是 1 年或更长时间,我们将交换我们的货币——而且最重要的是,在这一点上,我们将以最初交易的相同汇率交换。

因此,货币互换合约为我们每个人承担了货币风险,因为在我们的互换协议期限内,远期市场的汇率可能会发生相当大的变化。但这种风险可以通过溢价出售,这就是互换交易者赚钱的方式。

我们将不考虑这类金融工具。如果你对它们感兴趣并想了解更多,我建议从www.investopedia.com/terms/c/currencyswap.asp开始学习。

除了现金、远期和互换市场外,还有许多衍生品——这些合约可能或可能不涉及资产的实物交割。最著名的货币衍生品之一是货币期货,可以在芝加哥商业交易所CME)进行交易。

注意

对于零售交易者和活跃的投机者来说,最重要的市场是即期和货币期货。这两大市场将是本书进一步讨论的内容。

命名规范

让我们快速澄清货币市场如何使用其工具的名称。

国际标准(具体为 ISO 4217)假定每种货币都有一个三位数的数字代码和一个三位字母代码。因此,欧元的数字代码是 978,字母代码是 EUR。

外汇市场是一个特殊的市场,因为我们不是用钱购买商品,而是用钱买卖钱。因此,这个市场中的任何工具都是一个货币对及其相应的代码(通常称为股票代码),由两个字母货币代码组成。

货币对中的第一种货币是报价货币,第二种货币是报价货币。换句话说,第一种货币充当资产,第二种充当计量单位。例如,EURUSD 表示 1 欧元中有多少美元,USDJPY 表示 1 美元中有多少日元,等等。

注意

在外汇股票代码上要极其小心!始终记住你买的是什么,你卖的是什么,因为这很容易混淆。

外汇市场的价格以分数形式报价。例如,如果 EURUSD 报价为 1.0345,这意味着如果我卖出 10,000 欧元,我将得到 10,345 美元,而报价为 113.78 USDJPY 意味着我需要支付 11,378 日元来换取 100 美元。如果你将比率乘以相应的 10 的幂次来消除分数,计算起来非常简单。

在几十年的时间里,外汇市场大多有四位和两位的价格,例如前一个例子中的那些。最小的价格波动——即第四位或第二位的最小变化——被称为价格利息点(pip)。

然而,几年前,他们增加了一个额外的数字。所以,我们现在有 5 位价格,如 1.23456,而不是像 1.2345 这样的 4 位价格,同样,我们用三位数字代替两位数字。

注意

虽然现在大多数价格使用五位或三位数字表示法,但 pip 仍然被认为是第四位或第二位。所以,这个额外的数字被称为分数 pip 或分数价格。

这种额外数字被添加的可能解释之一可能是外汇市场一直在快速发展,大量的 LPs(流动性提供商)实际上用钱涌入市场。你还记得我们在“流动性和波动性:一个如何转化为另一个”部分中的讨论吗?好吧,流动性越大,波动性越低,如果我们继续使用旧的 4 位价格,那么价差(买价和卖价之间的差异)为零。反过来,这意味着卖方市场参与者不再能从流动性提供中获利。因此,引入更精细的价格水平是解决过度流动性问题的唯一合理解决方案。

因此,引入更精细的价格是一个可靠的指标,表明市场流动性在增长。这是好是坏?

初看之下,这无疑是好消息:流动性越大,签订合同就越容易,因此,交易者能更好地执行他们的订单。然而,如果每个买家和卖家都能立即满足他们的需求,价格就会停止波动。因此,高流动性市场对卖方有吸引力,但对于买方投机者来说,使用方向性交易策略赚钱变得越来越困难。

为了让您了解流动性如何影响买方投机者的表现:在 2001-2004 年,可以使用相当简单的趋势跟踪交易策略,如果这种策略每次交易平均赚取大约 10 个点,被认为是正常的。如今,在 FX 主要货币中,跟随趋势几乎完全失效,每次交易的平均利润下降到 1 个点。这是交易者为更有效地填充订单所付出的代价。

我该如何下单?

好吧,既然我们已经知道了我们可以交易什么,现在是时候弄清楚我们如何能够做到这一点了。

在现金兑换的情况下,情况大致如此:我可以去交易所商店,或者去银行。但别忘了,在这种情况下,我必须得到一个价格,而这个价格很可能与市场上最好的价格相差甚远。

如果我想从流动性巨大且价格极具竞争力的市场中获益,我需要能够以某种方式访问它。除非我自己是一家金融机构(这很可能不是情况),否则我必须使用各种中介机构的服务,这些中介机构将我介绍到市场并提供进入交易场所的途径。这些中介机构被称为经纪商

经纪商是市场参与者,其业务是从客户那里路由订单到交易场所,同时检查市场风险,并在必要时提供杠杆(参见交易机制:再次一些 术语 部分)。

同样,就像在市场做市商的情况下,许多人对外汇经纪商这个词都有负面印象。难怪在零售外汇交易的早期,许多不诚实的公司自称经纪商,但实际上,他们从未将任何订单路由到任何地方。这并不奇怪,1990 年代末现货外汇市场的最低交易量是 100 万基础货币,而在 2000 年代初下降到 10 万——低 10 倍,但对于小型零售交易者来说仍然太高。因此,那些经纪商提供了迷你合约微型合约,当然,这些合约从未离开这个经纪商的封闭系统。

因此,在零售业的早期,如果外汇交易经纪商想要与小型零售账户合作,他们只有两种选择:

  • 为其客户提供做市商服务

  • 对客户的头寸进行内部净额结算,并在市场中对净额进行对冲

不幸的是,许多所谓的经纪人选择了第三种选择——什么都不做,只是从他们的客户那里窃取金钱。因此,我们可以理解为什么提到外汇经纪人和一般意义上的“外汇”这个词有时会引起相当强烈的反应。

然而——幸运的是——时代已经改变。现在,大多数幸存下来的经纪人都是受监管的,至少可以保证客户账户的安全。当然,这并不是保证交易不会亏损,但这完全是另一回事。

除了这个之外,自 2000 年代初以来,外汇市场的结构发生了巨大的变化。如果在本世纪初,市场主要由银行组成,那么如今,买卖双方的参与者种类非常多样,几乎任何人都可以选择的交易场所数量相当可观。

今天,检查经纪人是否受到监管非常容易,你可以要求他们提供直接访问流动性池或 ECN 的权限——然后以相当类似的方式与专业市场参与者进行交易,就像你在中心化受监管的交易所交易一样。

注意

当我们用 Python 开发交易应用时,我假设我们直接连接到一个交易场所,例如 ECN,避免使用 MetaTrader 或专有交易平台,因为它们只会给我们的订单增加不必要的延迟。MetaTrader 可以用作监控工具,用于监视开放的头寸并检查订单执行的准确性。

这已经是一个相当长的章节,也是一次相当漫长的对外汇市场世界的探索——有时清晰直接,有时奇怪而诡异。但我想肯定的是,在你阅读上述所有信息的过程中,一定有一个问题已经在你的脑海中盘旋了很久。

我为什么需要所有这些?

嗯,实际上你不需要这样做。当然,你可以从不知名的地方快速下载不清晰的历 史价格数据,使用你不完全理解的工具开发一个模型,并使用这些数据进行优化,然后连接到一个不知名的经纪人并开始交易。问题是:这样的交易会成功吗?很可能是不会的。

如果你仔细阅读这一章,你很可能已经理解了原因。

首先,当你计划为你的模型使用某些历史价格数据时,你应该意识到你到底要使用什么:是最后成交价、买入价、卖出价,还是两者都有,或者其他任何东西。你应该检查历史数据是否包含正确的时间戳,以及没有哪个 tick 的时间早于前一个。你可能想确保你使用的数据包含交易量的信息——否则,你将无法开发出广泛的交易策略。你应该确保数据是干净且一致的,并且没有错误的报价,这些报价可能会影响你在开发策略时的参数。

其次,你应该仔细选择适合你策略的正确交易场所。它是否交易大量?它是否频繁交易?它是否试图在市场流动性稀薄的问题时期进行交易?对所有这些问题的回答将帮助你做出正确的选择。

最后(可能本应首先考虑的事情)——你应该确保你的策略产生的交易是可执行的,并且回报将覆盖所有交易成本,包括市场差价和你的经纪人向你收取的任何佣金。

因此,在这个时候,我建议你现在再次阅读这一章,并在未来不时地回到它,以保持你的发展与实际市场保持一致。

摘要

在这一章中,我们了解了市场可以组织和管理的方式。我们熟悉了诸如交易所、订单簿和流动性等关键概念,注意了各种因素对价格行为可能产生的影响,并发现了可能对交易策略表现产生负面影响的内在重要因素。在掌握所有这些基本知识后,我们准备深入研究交易应用架构的细节,看看我们应该使用什么来应对现代外汇市场的挑战。这就是我们在下一章将要做的。

第二部分:交易应用的一般架构及其组件的详细研究

熟悉主题领域对于任何软件开发都是至关重要的,第一部分介绍了外汇市场的基础知识和交易应用典型的关键特征。现在,是时候概述此类应用的架构,并更详细地考虑其主要组件了。

第二部分提出了用于检索和处理市场数据的几种解决方案,解释了基本面分析和技术分析之间的区别,从市场过程的角度关注了经典技术分析TA)指标的含义,并提供了这些指标在 Python 中的可能实现,这些实现与进一步的发展兼容。我们还将熟悉简单数据可视化的基础知识,这通常用于快速检查市场数据和计算的正确性,并生成入场和出场订单。

本部分包括以下章节:

  • 第四章交易应用——里面有什么?

  • 第五章使用 Python 检索和处理市场数据

  • 第六章基本面分析的基础及其在外汇交易中的可能用途

  • 第七章技术分析及其在 Python 中的实现

  • 第八章使用 Python 进行外汇交易中的数据可视化

第四章:交易应用:里面有什么?

几乎任何实现交易策略的应用程序都有一些或多或少标准的组件。让我们首先快速看一下典型交易应用的一种较为通用的架构,然后我们将更详细地探讨与使用 Python 开发交易策略相关的具体点。

第一章**,开发交易策略——为什么它们不同中,我们看到了一个典型的交易应用的非常通用的图。现在,我们将更详细地考虑其模块。我们将学习如何将我们的应用程序连接到数据源和交易场所,如何检索数据并检查其一致性,以及考虑有关交易逻辑和订单的重要点。

到本章结束时,您将了解如何开发高效、可维护和可扩展的交易应用的主要组件,以及如何避免由于市场数据处理错误、交易逻辑错误和对交易机制理解不足而出现的典型严重问题。

在本章中,我们将涵盖以下主题:

  • 让你的应用与世界对话——那阴暗的通信协议世界

  • 检索数据——垃圾进,垃圾出

  • 交易逻辑——这里的一个小错误可能代价巨大

  • 风险管理——你的安全带

  • 订单接口——确保你被正确理解

技术要求

您需要 Python 3.9 或更高版本来运行本章中的代码。

让你的应用与世界对话——那阴暗的通信协议世界

嗯,实际上,在这一节中,我可以说,“在 FX 自动化交易的世界里,每个设置都是独特的,所以去问你的经纪人。”当然,我不会这样做,但当涉及到将您的应用程序连接到做市商、电子通信网络ECN)或任何其他交易场所时,请始终记住我在本节开头所说的话。

如您从上一章所记得的,外汇市场在交易方面仍然是分割最严重的;因此,其计算机化基础设施也非常分割。尽管有交换金融信息的标准,但许多交易场所使用自己的方言,这意味着如果您想使用您的应用程序与不同的经纪人合作,将需要额外的工作。同时,许多交易场所提供自己的 API 和协议,这些协议与其他任何东西都不兼容,因此开发者永远不会失业,并且永远在适应他们的应用程序。

说了这么多,让我们从通常被认为是用于交易应用中唯一的行业标准通信开始。

FIX – 通用但过于灵活

在我看来,尽管其语法难以阅读,金融信息交换FIX)仍然是开始深入研究交易通信的最佳选择,因为其消息至少可以被人类阅读——因此,调试和学习我们的错误将确保学习曲线最陡峭。

不幸的是,FIX 被认为主要是一种专业用途的协议,并且任何经纪商的任何交易者都无法直接使用。这种歧视的一个可能原因是 FIX 允许你发送任何指令,而接收服务器只检查语法,而不检查消息的含义。因此,它可能不仅会损害交易者的账户,甚至可能损害市场本身。因此,大多数最大的做市商、经纪商和银行都要求交易者具有专业状态,才能允许他们使用 FIX。

然而,有好消息:如今,许多规模虽小但雄心勃勃的 FX 经纪商向零售交易者提供了许多以前仅限于机构的交易服务。这并不意味着他们的服务质量低:他们只是需要交易量,而很明显,自动化交易策略比人工交易者能更好地产生交易量。因此,实际上,任何人都可以通过 ECN 使用 FIX 获取机构流动性。

此外,谁知道呢,也许有一天你会发现自己在与银行或投资基金合作,那时即使对 FIX 的一般了解也会对你的职业发展大有裨益。

那么,FIX 究竟是什么呢?

FIX 试图创建一个真正通用的标准,几乎在任何需要传输金融相关信息的场合都可以使用。它被银行、经纪商、信息机构甚至保险公司等许多其他机构使用。当然,如此广泛的应用场景假设了极大的灵活性,以便根据需要定制 FIX,而这种灵活性反讽地使得 FIX 不如它所期望的那样标准化。

例如,一些交易场所可能要求各种标签(FIX 协议的基本元素)为强制性的,而其他则不要求。一些交易场所支持限价和止损订单,而其他只认可市价订单。因此,如果你想将你的交易应用从一个经纪商迁移到另一个经纪商,可能需要进行一些重构,因为 FIX 并不要求接受任何特定的订单类型。列表可以继续,但本质上问题是 FIX 旨在支持任何市场,但交易场所只使用其标签的子集,这些标签与其市场相关,经常根据他们的需求调整标准。

我认为最好将 FIX 不仅仅视为一种协议或 API,而更应将其视为一种具有简单语法的特殊语言,它允许你传达几乎任何含义。你也可以将其视为一个用于构建消息而不是应用的框架,而消息的含义将取决于上下文和环境。

让我们先看看 FIX 作为一种协议,并考虑其层。这将帮助你理解我们如何与 FIX 连接工作。

基本连接

在传输层,FIX 需要一个标准的套接字连接。再次,正如我在本节开头提到的,一切都非常个性化,因为不同的经纪人和交易场所可能对客户端连接到他们的方式有不同的要求。通常,你必须向经纪商或交易场所提供一个 IP 地址(或地址范围)以进行白名单并使用传输控制协议套接字TCP 套接字)进行连接。其他人可能需要复杂的授权机制,甚至强制使用 VPN。因此,你可能需要咨询你的经纪商或交易场所以获取详细信息。

学习基本的低级网络,例如如何在 Python 中建立套接字连接,并不是这本书的目标,所以如果你以前从未做过,我建议从 Real Python 的一个优秀指南开始(realpython.com/python-sockets),或者如果你更喜欢基于实际操作语言并包含大量实例的“深入”或“游泳”方法,你可以观看 Geeks for Geeks 的这篇教程(www.geeksforgeeks.org/socket-programming-python/)。重点是,在 Python 中建立套接字连接并不是火箭科学,而是一个简单的常规程序。

标签

一旦建立了套接字连接,我们就可以通过它发送和接收一些有意义的信息。与许多其他协议一样,FIX 基于消息。FIX 消息是一个纯文本(ASCII)字符串,它由非打印字符 0x01(SOH)分隔的块(子字符串)组成,没有像\n\r这样的特殊结束字符。每个分隔符之间的块遵循简单的语法:

TAG=VALUE

标签是一个数字,其值可以是任何不包含 0x01 字符的字符串,这是出于明显的理由:它将被解释为分隔符。不过,有一个重要的例外:如果标签意味着检索数据,那么用作数据的任何值都可以是任意字节序列。

例如,55=EUR/USD代表一个货币对、工具或符号(记住,它们都是同义词)。40=Limit表示消息包含一个限价订单(参见第三章**,从开发者的角度来看的 FX 市场概述,以了解订单类型)。

由于分隔符字符 0x01 是非打印的,许多作者使用特殊字符在 FIX 消息中视觉上分隔标签。我相信你会同意,以下结构的字符串更容易阅读:

8=FIX.4.4|9=73|35=A|34=1092|49=TESTSND|52=20220728-07:30:59.643|56=TESTTGT|198=0|108=60|10=133

相反,阅读以下字符串更困难:

8=FIX.4.49=7335=A34=109249=TESTSND52=20220728-07:30:59.64356=TESTTGT198=0108=6010=133

然而,在真实的 FIX 消息中,永远不要使用除 0x01 之外的任何其他分隔符!

消息结构

任何 FIX 消息都由以下三个逻辑部分组成:

  • 标准头

  • 主体

  • 标准尾迹

标准头始终由以下三个标签按此顺序发送,而不是其他任何顺序:

  • 标签 8 – 表示消息开始,并包含双方使用的 FIX 协议版本。

  • 标签 9 – 消息体长度

  • 标签 35 – 消息类型(例如,报价请求订单登录登出

标准拖车总是以标签 10 结束,其值是消息校验和。校验和是哈希函数返回的一小段数据——一种处理有意义数据块的所有位并根据特殊算法压缩它的函数。校验和用于确保数据块(在我们的情况下是消息)在无错误的情况下被传递。如果您想了解更多关于校验和、哈希函数和相关内容的信息,我建议从维基百科上一篇优秀的文章开始,该文章还提供了进一步阅读的参考文献(en.wikipedia.org/wiki/Checksum)。

根据 FIX 标准,每个标签在每个消息中只能出现一次。具有相同标签多次出现的消息将被目标计算机拒绝。

标签必须有一个值。如果没有为任何标签指定值,整个消息将被拒绝。

会话

使用 FIX 进行工作是有组织的会话。会话假设有两个计算机参与:发送消息的计算机(发送方,相应的标签称为SenderCompID)和接收消息的计算机(目标,相应的标签称为TargetCompID)。通常,会话是由客户端连接到服务器开始的,例如,交易员连接到经纪人或银行连接到 ECN。

会话是通过从发送方的计算机向目标计算机发送握手消息开始的。在目标计算机有回复的情况下,这条消息启动会话。这种消息类型(标签 35)是登录(A)。一个示例握手消息头将如下所示:

8=FIX.4.4|9=XX|35=A

在前面的消息中,XX代表整个消息长度(见消息部分中标签 9 的解释)。

现在,我们已经到了一个地方,我的初步论点(去询问你的经纪人详情)变得明显。问题是,除了这三个强制标签和结束标签 10 之外,登录消息中的所有其他标签都是可选的。这意味着我无法告诉你应该包括什么;也没有其他作者可以告诉你——除非是你的经纪人,因为是他们决定应该发送什么以及发送的顺序。所以,最好的学习方法是参考从你想要发送消息的交易所获得的文档。

任何 FIX 会话都以登出(5)消息结束,其最小形式如下所示:

8=FIX.4.4|9=5|35=5|10=166

构造消息

在这一点上,我想警告你避免许多交易策略开发者犯的一个非常常见的错误。他们认为他们的应用程序可能生成的 FIX 消息集是有限的;因此,他们将它们作为字符串硬编码,并在需要发送订单时选择合适的消息。

不要这样做!以下是不应该这样做的原因:

  • 首先,这种方法使得你的代码不可扩展。在需要为新的订单类型添加新标签的情况下,你需要重写整个应用程序。

  • 第二,这使得你的代码不可移植。在你想用另一个经纪人使用它的情况下,这个经纪人可能需要在登录消息或任何其他地方要求特定的标签集,再次——你将不得不重写你的应用程序。

  • 最后,即使你认为你能够记住每个 FIX 标签的含义,相信我,这只是一个危险的自我欺骗!

因此,在 Python 中构建 FIX 消息的合理方式是分块构建,使用标签的显式、可读性强的名称而不是仅仅使用数字。我们将为此目的使用原生的 Python 字典:

  1. 让我们从构建一个基本的 FIX 字典开始,将标签与它们相应的名称关联起来。我们将使用名称作为键,标签号作为值,因为这是我们构建消息的方式:

    fixdict = {}
    
    fixdict["start"]="8"
    
    fixdict["body_len"] = "9"
    
    fixdict["checksum"] = "10"
    
    fixdict["msg_type"] = "35"
    
  2. 现在,我们需要一个函数来在消息中将标签绑定在一起。不要忘记,消息中包含的标签数量可能会有很大差异,因此我们希望使用 Python 最强大的功能之一——任意 关键字参数

    def compose_message(fix_dictionary, **kwargs):
    
        msg = ""
    
        for arg in kwargs:
    
            msg += fix_dictionary[arg] + "=" + kwargs[arg] + "\001"
    
        return msg
    

在这里,我们假设我们将关键字参数作为<tag_name>=VALUE的配对传递,然后使用字典将可读性强的名称替换为标准的 FIX 标签号。

  1. 让我们用以下指令测试我们的代码:

    message = compose_message(fixdict, start="FIX.4.4", body_len="25", msg_type="A", checksum="56")
    

我们将得到以下结果:

8=FIX.4.49=2535=A10=56

注意,SOH 字符在此输出中不可见,但如果我们明确请求message变量的值,它将返回以下输出:

In[28]: message
Out[28]: '8=FIX.4.4\x019=25\x013
5=A\x0110=56\x01'

在这里,不可打印的字符清晰可见。

然而,每次我们需要组合消息时手动传递标准头是不合理的。因此,我们需要将其包含在函数中。

对于第一个标签8,很简单。我们可以在一个特殊变量中存储值,并在消息组合过程的最后阶段添加它。第一个标签总是只包含 FIX 协议的版本。但无论如何,我们应该计算消息体的长度和校验和,并将它们(长度和校验和)分别包含在标签910中。

重要

不要将消息体与消息本身混淆!标签9表示只有消息体的长度,即从标签9到标签10。在我们的例子中,消息体的长度是5(不是4,因为主体由字符35=A和一个不可打印的 0x01 组成)。

实现计算消息体长度的最简单方法是将 **kwargs 中的提供标签限制仅为消息体。这可以通过多种方式完成:

  1. 让我们先使用一个我们将称之为 fix_exceptions 的列表:

    fix_exceptions = ["8", "9", "10"]
    
  2. 我们只有在标签不在异常列表中时才会将标签添加到我们的编写消息中:

    if fix_dictionary[arg] not in fix_exceptions:
    
  3. 现在,我们消息编写函数的新版本将看起来像这样:

    def compose_message(fix_dictionary, fix_exceptions, **kwargs):
    
        msg = ""
    
        for arg in kwargs:
    
            if fix_dictionary[arg] not in fix_exceptions:
    
                msg += fix_dictionary[arg] + "=" + kwargs[arg] + "\001"
    
        return msg
    

如果我们使用前面的参数进行测试,我们将得到以下输出:

35=A

标签 8、9 和 10 现在被忽略,因为它们在异常列表中。由于与标准头和标准尾相关的标签被忽略,因此未被忽略的内容保留在消息体中。

  1. 太好了,我们现在可以计算它的长度并将其添加到消息中:

    msg = fix_dictionary["body_len"] + "=" + str(len(msg)) + msg
    
  2. 现在,让我们添加标签 8

    msg = fix_dictionary["start"] + "FIX.4.4"
    
  3. 最后,在以下代码中,我们计算字符串中所有字符的 ASCII 码之和,然后将这个和除以 256 并取余数(这里我们将使用 reduce 函数,它是 functools 的一部分,因此应该作为 from functools import reduce 导入):

    checksum = reduce(lambda x, y: x + y, list(map(ord, msg)) % 256)
    

然后我们将它添加到消息中:

msg += fix_dictionary["checksum"] + "=" + str(checksum)

现在我们来看看整个升级后的代码:

from functools import reduce
fixdict = {}
fixdict["start"]="8"
fixdict["body_len"] = "9"
fixdict["checksum"] = "10"
fixdict["msg_type"] = "35"
exceptions = ["8", "9", "10"]
def compose_message(fix_dictionary, fix_exceptions, **kwargs):
    msg = ""
    for arg in kwargs:
        if fix_dictionary[arg] not in fix_exceptions:
            msg += fix_dictionary[arg] + "=" + kwargs[arg] + "\001"
    msg = fix_dictionary["body_len"] + "=" + str(len(msg)) + msg
    msg = fix_dictionary["start"] + "=" + "FIX.4.4" + msg
    checksum = reduce(lambda x, y: x + y, list(map(ord, msg))) % 256
    msg += fix_dictionary["checksum"] + "=" + str(checksum)
    return msg

现在,让我们用以下输入来测试它:

message = compose_message(fixdict, exceptions, start="wrong version", body_len="10000", msg_type="A", checksum="78909")

我们仍然会得到正确的结果:

8=FIX.4.49=535=A10=178

注意,列表中找到的所有标签都被忽略,并且它们的荒谬值不会包含在消息中——这很好,因为否则这样的消息将被拒绝(在最好的情况下)。此外,现在当我们调用以下函数时,我们可以安全地省略除消息体之外的所有标签,这将给出与之前完全相同的结果:

message = compose_message(fixdict, exceptions, msg_type="5")

接下来要做什么

如果你与 FIX 一起工作,你肯定需要一个包含所有标签的全面字典及其含义解释的参考。其中一个最好的资源是 OnixS(他们开发直接市场接入的 SDK,因此 FIX 在他们产品中扮演核心角色毫不奇怪),可以在www.onixs.biz/fix-dictionary.html找到。如果你计划专业地与 FIX 一起工作,我强烈建议访问 FIX 社区官方网站www.fixtrading.org,并检查“标准”部分,在那里你可以找到有关技术标准、规范以及 FIX GitHub 的链接。

当谈到 FIX 的专业应用时,最重要且事实上的行业标准解决方案是 QuickFIX (quickfixengine.org)。它为包括 Python 在内的许多语言实现了 FIX 协议,通过减轻开发者编写低级 FIX 消息的负担,简化了交易应用的消息和数据检索开发。

对于 Python,也有几个现成的 FIX 实现,其中 Simplefix (pypi.org/project/simplefix/) 可能是最直接的。它不实现套接字连接或任何其他传输层功能,也不支持日志记录或确保消息持久性。它仅作为编码和解码 FIX 消息的方便包装器,提供易于阅读的函数。

还值得一提的是另一个建立在 FIX 之上的协议。它被称为FAST,代表FIX Adapted for STreaming。简单来说,这个协议旨在在不产生过多的处理开销或延迟的情况下,促进快速和大量消息的传输。如果你对学习更多关于 FAST 感兴趣,我建议从官方文档(www.fixtrading.org/standards/fast/)开始,并查看jettekfix.com/education/fix-fast-tutorial/上的 FIX Fast 教程。

现在你已经知道了如何编写 FIX 消息,你只需要填充它们的意义。也就是说,你需要添加任何基于市场数据的交易逻辑,生成订单,将它们转换为 FIX 消息,并发送给经纪人、ECN 或其他地方。你还需要能够接收并理解对你消息的回复。例如,你的经纪人可能在消息体中响应8=8,这意味着你的订单已成功成交(消息类型 8 表示订单执行报告,值 8 表示订单成交)。经纪人可能会发送8=5给你,这意味着你的订单被拒绝,你的代码应该能够处理这种情况。

如果你的经纪人不支持 FIX 或不向非专业交易者提供访问权限怎么办?在这种情况下,我们不幸地回到了本章开头我的声明,“在 FX 自动化交易的世界里,每个设置都是独特的,所以去问问你的经纪人。”*正如我们之前看到的,即使在高度标准化的 FIX 协议中,仍然存在一定程度上的灵活性,因此最好阅读你计划发送订单的交易所提供的文档。对于专有协议,情况更糟,因为没有标准,每个经纪人都会提供他们自己设计的 API,他们认为这是最好的方式。

然而,从本节中最重要的结论是,任何协议、任何 API 和任何框架都只用于传递某种信息,在大多数情况下是交易订单,并接收回复。如果你设计你的交易应用,使得数据处理、交易逻辑、风险管理以及订单模块之间有独立的接口,你将能够从一种协议切换到另一种协议,而无需重写整个应用——这正是本书将要深入探讨的内容。

现在我们对您的应用程序和交易场所之间交换信息的方式更加熟悉,是时候学习我们可以发送和从那里检索什么了。让我们从市场数据开始。

获取数据——垃圾进,垃圾出

FIX 协议在设计上具有通用性,因此不仅可以用于订单,还可以用于数据检索。然而,在大多数情况下,它实际上并不用于市场数据传输;相反,交易场所提供自己的专有 API 来从那里检索数据。

始终如一,在这个通信协议的阴暗世界中,一切都是个性化的,每个交易场所都提供自己的 API。然而,总的来说,所有经纪商的 API 都是实现为 REST 或 Websockets。前者适用于偶尔的报价请求,而后者最适合连续订阅,以便接收实时市场数据。

我提供的以下示例是从 FX 市场中的关键 ECN 之一 LMAX 的 API 中提取的。它们之所以出色,不仅是因为它们对任何客户,无论大小都开放,而且还因为它们是极少数公开传播实时市场数据的交易场所之一——而且他们完全免费。

在你开始使用 FX 市场数据之前,你应该理解和始终记住一个重要的事情:

演示环境中的数据始终与真实环境中的数据不同。

这意味着,如果你从 ECN 接收公开数据或开设一个演示账户以从那里接收市场数据,你应该准备好看到与真实市场略有不同的报价。好消息是这种差异并不显著;在我们的特定例子中,LMAX 通常是 0.1 到 0.5 个点,在纽约银行结算时间(纽约时间下午 5 点)附近可能会有轻微的差异增加(参见第三章**,从开发者的角度来看的FX 市场概述*)。因此,即使是公开可用的数据,对于大多数开发和甚至实时交易来说都是好的。

在你需要时不时地获取这里和那里的报价的情况下,那么 LMAX 的 REST API 通常是你的选择。

就像任何常规 REST API 一样,它支持一些端点,允许以最后交易价格和订单簿顶部价格的形式检索市场价格数据。它还支持检索 LMAX 演示服务器支持的所有工具(符号)的信息。

为了检索这样一个列表,让我们执行以下操作:

curl -i -X GET "https://public-data-api.london-demo.lmax.com/v1/instruments"

响应将为我们提供一个包含可用工具列表的 JSON:

[{"instrument_id":"eur-usd","symbol":"EUR/USD","security_id":"4001","currency":"USD","unit_of_measure":"EUR","asset_class":"CURRENCY","quantity_increment":"1000.0000","price_increment":"0.000010","ticker_enabled":true},...]

我移除了剩余的 JSON 以保持其紧凑性,但其余部分只是其他工具的类似记录的重复。让我们解析这个答案来了解其组成部分:

  • instrument_id:这是 LMAX 演示服务器支持的 FX 工具的正确名称(注意,名称中不允许有斜杠 /,因此它们被替换为破折号 -)。

  • asset_class:在这个 ECN 上交易的大多数工具是货币,但也有金属和能源的差价合约CFDs),所以请小心。

  • quantity_increment:这是订单大小的最小quant;1,000.000 欧元的增量意味着你可以发送一个价值 1,003,000 欧元的买卖订单,只需 2,000 EURUSD,但不能是 1,003,300 或 1,100,301 欧元。

  • price_increment:这是最小的价格波动。0.00001 意味着最小变化可以是在小数点右边的第五位,这种变化的数量是 1(所谓分数点)。

  • ticker_enabled:这意味着该符号可用于下单。

要检索特定符号的整个订单簿信息,我们可以使用以下方法:

curl -i -X GET "https://public-data-api.london-demo.lmax.com/v1/orderbook/eur-usd"

响应将包含 LMAX 为演示账户允许的深度内的买卖报价列表,以纯 JSON 格式呈现。

如果你想要从 ECN 接收连续数据,你可能想使用 WebSockets 而不是 REST:

  1. 首先,你需要安装 WebSockets,你可以使用 pip 来做这件事:

    pip install websocket_client
    

如果你使用 Anaconda 分发的 Python,你可以使用以下代码:

conda install -c conda-forge websocket-client
  1. 首先,我们需要导入 WebSocket 模块:

    import websocket
    
  2. 然后,设置我们将要订阅的 URL:

    url = "wss://public-data-api.london-demo.lmax.com/v1/web-socket"
    
  3. 然后,创建 WebSocket 并连接它:

    ws = websocket.WebSocket()
    
    ws.connect(url)
    
  4. 然后,将请求以常规 JSON 格式形成:

    req = '{"type": "SUBSCRIBE","channels": [{"name": "ORDER_BOOK","instruments": ["eur-usd"]},{"name": "TICKER","instruments":["usd-jpy"]}]}'
    
  5. 发送请求:

    ws.send(req)
    

观察响应:

print(ws.recv())

如果你正确地完成了所有步骤,你将看到类似这样的内容:

{"type":"SUBSCRIPTIONS","channels":[{"name":"ORDER_BOOK","instruments":["eur-usd"]},{"name":"TICKER","instruments":["usd-jpy"]}]}

这个响应意味着订阅已经设置,并且订阅的工具是整个订单簿的欧元和最后价格数据的日元。

由于这本书不是关于 WebSockets 的教程,如果你不熟悉这种网络连接,我建议阅读非常全面的教程websockets.readthedocs.io/en/stable/intro/index.html

你可以在 LMAX 官方文档中找到所有支持的 REST API 端点和 WebSocket 请求docs.lmax.com/public-data-api/

注意

请不要忘记,前面的例子只是为了说明目的。我希望这本书尽可能不涉及任何经纪商,因此我无法真正推荐任何特定的经纪商或 ECN。这里提供 LMAX 的例子,仅仅是因为他们有一个最简单且易于使用的 API。

其他经纪商和交易场所可能有不同的 API,有时更复杂,但订阅数据的根本原则是相同的。

因此,既然我们已经知道了如何从交易场所获取数据,接下来就是最重要的部分:数据处理。你在交易应用中对数据进行的所有操作都必须确保数据一致性。

通过市场数据的一致性,我们假设以下情况:

  • 如果接收到的 tick A 早于 tick B,那么 tick A 的时间戳应该早于 tick B 的时间戳。

  • 如果两个相邻 tick 之间的时间间隔显著大于平均值,那么对此必须有明确的逻辑解释。

  • 如果两个相邻 tick 之间的价格差距显著大于平均值,那么对此必须有明确的逻辑解释。

让我们在以下子节中详细考虑每个点。

tick 序列

当你开始处理真实市场数据时,你会对具有错误时间戳的 tick 数量印象深刻。这种现象有几个解释;最容易被理解的可能是因为 tick 的数量(我们记得从第一章中,tick 是价格更新,无论是新的出价、新的要价还是新的交易)如此巨大,以至于交易所或 ECN 自己的服务器实际上无法正确地按顺序处理所有这些,并为一批 tick 分配相同的时间戳。还有其他一些原因,这些原因更多地与交易所服务器和交易服务器或客户端计算机之间的延迟有关。无论如何,无论原因如何,错误的时间戳都是一个真正的问题,在我们开始处理数据之前,我们总是必须进行检查,并在其中存在任何不一致时纠正时间戳。

有几种技术可以解决时间戳问题,我们将在下一章详细讨论这些技术,这一章完全致力于处理和存储市场数据。

时间间隔

基本上,时间间隔是指在一定时间内没有接收到市场数据的情况。当然,问题是如何定义这个“一定时间”。是多长时间?一秒?一分钟?一小时?如果 5 分钟内没有市场数据更新,这意味着连接丢失,还是市场根本没有任何活动?

如果你以实时报价的形式接收数据,可以通过在你的市场数据检索实现中添加心跳消息来相对容易地解决这个问题。

一个心跳消息ping有些相似:发送一个内容为假的消息到服务器,服务器只是回复一些表示“好的,我还活着,并且可以很好地听到你。”的消息。这样的消息会自动以相等的间隔发送,这是一种简单而稳健的检查连接健康状况的方法。

如果你使用 FIX,这个协议原生支持心跳(消息类型 0,标签 35=0)。如果你使用其他 API——嗯……正如本章一贯的,你应该参考你的经纪商的文档,了解他们如何实现心跳(以及他们期望你使用什么)。如果经纪商没有保留特殊的心跳消息,你可能想不时地使用任何中性的请求,例如偶尔请求报价,并检查响应。

使用特殊心跳消息的关键优势是它在服务器运行时任何时候都有效。即使市场实际上关闭,并且尝试接收市场报价失败,心跳消息也会通过并得到响应。因此,使用心跳消息始终是首选的方法。

注意

如果你的经纪商或交易场所支持心跳消息,你可能想避免使用任何其他类型的请求,以确保连接是活跃的。在某些情况下,这种活动可能是某些 ECN 禁止的原因。

因此,对于实时报价,通过添加心跳消息可以相对容易地解决时间间隔的问题。但在我们处理历史数据的情况下,即不是代表现在市场状况的数据,而是表示过去某个时间市场状况的数据,我们该怎么办?在这种情况下,没有记录心跳消息(至少我不了解任何单一个例子表明情况相反),如果我们看到两个时间点之间有 1 小时的暂停,总会有一个问题是这些数据是否一致。

通常,这种一致性检查分为两个阶段:

  • 首先,我们识别时间间隔。这里的主要问题是如何判断两个时间点之间的暂停是否足够长,以至于变得可疑。我们将在下一章详细探讨这个问题。现在,我们只需假设任何大于平均数加两个标准差(这里的 sigma 代表标准差,我们将在第六章**,基本分析及其在 FX 交易中的可能用途)的暂停都被认为是可疑的

  • 接下来,所有可疑的暂停都将与已知市场数据可以暂停的情况列表进行核对。我们排除所有周末、银行结算前后的暂停,以及某些货币银行日的开盘,并将剩余的与已知事件的时间表进行核对,例如重要经济新闻的发布,这也可能导致市场报价的中断。

如果剩余的时间间隔问题列表不显著(为了清晰起见,我们假设它至少是原始列表的 10 倍小),那么我们认为这些数据总体上是可用的。如果未识别的时间间隔数量仍然很大,最好避免使用这些数据。

价格间隔

价格缺口是指两个相邻的 tick 之间存在异常的价格差异。当然,就像时间间隔一样,问题是我们如何定义这种差异为异常。在这里,我们可以使用与时间间隔相似的技术。如果两个相邻 tick 的价格差异大于平均值加 2 个标准差,那么这可能是潜在的价格缺口。

在价格缺口的情况下,我们经常使用 3 个标准差以上,因为目标并不是真正捕捉到价格快速跳动的每一个情况(在真实市场中这种情况可能相当常见,至少每天发生几次),而是隔离和过滤掉非市场价格

你可能还记得第一章中非市场价格示意图。所以,非市场价格是指完全、完全超出任何合理范围的东西,因此我们可以很容易地考虑 10、20,有时甚至 100 个标准差来过滤掉这些错误的报价。

你可能会想知道这些非市场价格的来源。非市场价格可能有多个原因:

  • 最常见的原因是所谓的大拇指效应,简单地说就是发送到市场的要价或出价中的错误。通常,由于报价中的额外 0 或缺失的数字,它可能是前一个市场价格的大 10 倍或小 10 倍。在事后看来,以这些价格进行的交易通常会被撤销,但这些报价会被记录在数据流中并存储为历史数据。

  • 一些数据提供商包括与交易无关的数据;例如,我见过一个案例,交易所将转移到保险基金的资金记录为市场价格数据。幸运的是,他们以零价格记录了这些ticks——幸运的是,因为这使得过滤变得非常容易。

  • 在罕见的情况下,数据提供商的数据库、软件或硬件可能会出现故障——由这种原因造成的错误报价是最难找到和过滤的。

在你成功连接到数据源、接收数据并过滤它之后,是时候用它做一些有意义的事情了:即分析数据并做出一些交易决策。这就是我们将在下一节考虑的内容。

交易逻辑——这里一个小错误可能会损失一大笔财富

交易逻辑显然是整个交易应用的核心。它是分析市场数据以寻找任何预定义的价格-时间模式(有时还包括其他数据,如成交量和对冲头寸,但这些数据通常在现货市场不可用)并生成订单的组件。本书的大部分内容都将致力于交易逻辑以及开发交易算法的各种方法,但我们不能不考虑一个已经让许多交易者损失数百万甚至数十亿美元的错误——我指的是前瞻性问题。

提前查看的现象仅限于项目的开发阶段,当交易算法使用历史数据进行优化或训练时,这被称为历史数据。正如你从上一节中记得的那样,历史数据是由你自己或第三方(如交易所、经纪人、数据供应商等)预先记录的。这些数据可能包含 tick,也可能被压缩成 1 秒或 1 分钟的快照。无论数据压缩如何,清洗后的数据集中的所有数据都是按时间戳排序的,不存在任何未来的数据会出现在过去数据之前的情况,或者相反,任何过去的数据会在未来数据之后被记录。让我们看看以下例子来了解这意味着什么:

1/27/2015,13:30:00,1.12947,1.12959,1.12941,1.12941,230,438,888,4,7,12
1/27/2015,13:31:00,1.12953,1.12970,1.12951,1.12965,400,240,650,9,4,14
1/27/2015,13:32:00,1.12944,1.12944,1.12883,1.12883,90,609,749,2,10,13
1/27/2015,13:33:00,1.12876,1.12907,1.12876,1.12894,589,170,909,5,4,12
1/27/2015,13:34:00,1.12902,1.12925,1.12902,1.12925,720,400,1120,9,4,13

在这个例子中,时间戳是正确的顺序。这块数据意味着该工具的收盘价(可以在每条记录的第六个位置找到)最初是1.12941,然后是1.12965,然后是1.12883,然后是1.12894,最后是1.12925

当我们开发和测试一个交易算法时,我们通过处理历史数据并据此做出交易决策来模拟交易。通常,模拟引擎会逐个处理来自文件、列表或 pandas DataFrame 的数据块,以模拟如果我们的算法在那个时间进行交易会发生什么。因此,我们必须确保在任何时候我们的交易算法都不能接收来自未来的数据。

让我们再次看看前面的例子。想象一下,我们在 13:32 时模拟了我们的交易算法的行为。在这个步骤中,我们和算法可能知道的信息只是当时的收盘价是 1.12883。我们不能知道——算法也不能知道一分钟后的收盘价将会是1.12894*。然而,如果你将价格存储在列表(或 pandas DataFrame)中,很容易通过索引引用未来的价格。所以,我可能写出如下这样的代码(以下示例假设current_position是遍历数据集的指针,而price_data是数据集本身):

current_position = x
if price_data[x + 1] > price_data[current_position]:
      order.buy(market)
if price_data[x + 1] < price_data[current_position]:
      order.sell(market)

在此代码中,我们假设buysell方法分别生成买入和卖出订单。如果我们使用此代码运行模拟,我们将拥有100%的盈利交易。没有任何一笔会亏损,因为我们比较的是未来的价格(第 2 行和第 4 行)与模拟时刻存在的真实价格。在我们的例子中,这意味着在 13:32 时,我已经知道 13:33 时的价格并将其与当前价格进行比较。好吧,如果我真的能知道的话...无论如何,没有人能预知未来,你应该确保你的算法也不例外。

注意

总是要确保你逐个引用价格,不要提前查看。使用队列或引用时间戳,但始终避免通过索引引用数据。

好吧,我们现在至少从表面上看知道了如何与市场沟通,如何检索数据并确保其一致性,甚至如何避免系统交易策略开发者可能在交易逻辑中犯的最大错误。现在,我们应该能够发送交易订单并保护自己免受众多不利情况的影响。

风险管理——您的安全带

在您的算法生成交易信号后,它应该通过风险管理。当交易逻辑回答“是否交易”的问题时,风险管理回答另一个问题:应该投入多少?

简而言之,风险管理包括分析每笔交易可能的最大不利波动、账户规模、杠杆和保证金作为风险财务组成部分,以及宏观经济因素和政治事件作为外部和非市场风险。仅举一例,在 2015 年 1 月瑞士国家银行决策之前或 2016 年美国总统选举之前关闭交易是明智的。

风险管理的话题非常广泛,我们将在学习更多关于交易策略和订单类型之后,在第十章**,Python 中的订单类型及其模拟中详细讨论这个问题。

订单——确保您被正确理解

最后,但同样重要的是,您的交易应用将有一个订单模块。此模块执行以下功能:

  • 它保持与执行服务器的连接,使其保持活跃。

  • 它将风险管理模块传递的交易信号转换为实际订单——使用 FIX 或任何专有 API。

  • 它处理来自经纪人或交易场所的所有类型响应。这些响应范围从仅仅是OK到部分成交和拒绝。

  • 它决定在您的订单被拒绝或部分成交的情况下应采取什么措施。

  • 如果交易逻辑青睐它,它会重新提交全部或部分订单。

您可能已经注意到,订单模块的一个职责是维护连接——就像数据处理模块一样。是的,这里没有错误,一个交易应用使用不同的连接来处理数据和订单是完全正常的。此外,当数据从数据供应商那里获取,而订单在交易所执行,或者从交易所获取并执行与市场做市商等时,这种情况非常常见——任何可能的组合。因此,订单模块也维护连接,因为这种连接是不同且独立的。

与风险管理一样,我们将在第十章**,Python 中的订单类型及其模拟中,与风险管理一起详细讨论订单问题。

摘要

在本章中,我们学习了如何连接到经纪商或数据供应商,检索实时市场数据,了解这些数据质量的要求,以及了解我们未来交易应用将包含的五个核心逻辑块。

在下一章中,我们将继续讨论如何高效地处理历史市场数据的细节,因为这正是研究和开发阶段所必需的。

第五章:使用 Python 检索和处理市场数据

如果你查看我们设计的交易应用程序架构的一般逻辑图,该图在第一章“开发交易策略——为什么它们不同”中有所描述,你会发现我们现在正从第一个名为Receive data的模块转向第二个模块,即Cleanup and filter,该模块具有存储检索功能。

在前面的章节中,我们多次提到,任何算法交易应用程序都是基于市场数据的,而算法交易的成功(即赚钱而不是亏损的能力)取决于数据的质量和一致性。因此,让我们继续确定我们真正需要收集哪些数据,以及如何确保收集到的数据是一致的,然后决定交易应用程序的内部格式,以及存储、更新、检索和删除数据(如果需要)的方式。

在完成这一章后,你将对处理和加工市场数据的最有效方法有一个清晰的理解,最重要的是,你将学会如何使你的交易应用通用,以便它可以连接到几乎任何数据源,无论是实时数据还是历史数据,而无需重写所有代码。

在本章中,我们将涵盖以下主题:

  • 数据导航

  • 数据压缩——将数量保持在合理的最低水平

  • 与保存的和实时数据一起工作——保持你的应用通用

数据导航

数据收集的问题始于一开始:每个数据提供商都提供自己的数据,很多时候是以自己的格式提供。一些数据提供商只提供压缩数据或快照(见本章后面),而其他数据提供商则广播逐 tick 数据;非常少的数据提供商也提供订单簿(市场深度DOM;见第三章“从开发者的角度来看的 FX 市场概述”)数据。

因此,首先也是最重要的,你应该决定数据粒度:你的交易算法是否需要逐 tick 数据或压缩数据,以及你是否需要 DOM 数据。在这个阶段,你可能对这些问题的感觉会有些迷茫,但不用担心——当你完成这本书的后续章节时,你会对你的数据需求有一个非常清晰的理解。

让我们考虑所有三种情况,看看我们如何实际处理 tick、快照和 DOM 市场数据。

Tick 数据和快照

我要重申,当我们谈论第三方数据、格式和协议时,关于支持什么以及第三方文档是如何完成的准确信息的最终来源是你的经纪人。

然而,一般来说,所有数据提供商都支持两种类型的市场数据:tick 和快照。

让我们快速回顾一下术语:

  • tick是指交易资产(如:买入价、卖出价(报价)和最新价)中的任何一种价格的变化。

  • Bid 是市场制造者、流动性提供者和其他价格提供者同意购买的价格——因此,这也是价格接受者可以出售的价格

  • Askoffer 是价格提供者愿意出售的价格——因此,这也是价格接受者可以购买的价格

  • 最后一个是最新实际交易的价格

让我们考虑一个例子。这是你可以从 LMAX 收到的示例 tick 数据(有关连接和检索此电子通信网络ECN)数据的更多示例,请参阅上一章):

{
    "type": "TICKER",
    "instrument_id": "eur-usd",
    "timestamp": "2022-07-29T11:10:54.755Z",
    "best_bid": "1.180970",
    "best_ask": "1.181010",
    "trade_id": "0B5WMAAAAAAAAAAS",
    "last_quantity": "1000.0000",
    "last_price": "1.180970",
    "session_open": "1.181070",
    "session_low": "1.180590",
    "session_high": "1.181390"
}

首先,前面的 JSON 指定了数据的类型。在这个例子中,typeTICKER,这意味着我们正在处理单个 tick。它后面跟着工具名称。

重要提示

许多数据提供者对同一工具名称使用不同的表示法。例如,EUR/USD 可以表示为 eur-usdEURUSDEUR/USD,甚至 @EURUSD。因此,请始终检查数据提供者的文档,并且不要忘记将提供者表示法中的工具名称替换为应用程序中使用的内部名称。

例如,如果你使用传统的 CCY1/CCY2 表示法,那么你可能想使用以下代码,它将 LMAX 使用的 ccy1-ccy2 表示法转换为传统表示法:

instrument_id.replace("-", "/").upper()

这里,instrument_id 是从数据提供者收到的带有 tick 的工具名称,replace() 是内置的字符串方法,用于将一个字符替换为另一个字符,而 upper() 是另一个内置方法,它将整个字符串转换为大写。

接下来是时间戳,时间戳带来了另一个不确定性,因为数据提供者使用不同的标准来表示时间戳。我们将在本节稍后讨论市场数据的时间戳。

以下字段是自解释的:

  • best_bidbest_ask 表示订单簿的顶部

  • trade_id 是在此 ECN 上最新交易的唯一标识符

  • last_quantitylast_price 是最新交易的大小和价格

  • session_opensession_lowsession_high 表示交易时段的第一个价格(当市场开盘时),以及从市场开盘到收到 tick 的时间内的最低价和最高价

如我们所见,tick 中的大多数字段都是自解释的,易于使用,但有一个重要的例外:timestamp

时间戳——注意比较苹果和苹果!

在前一节中,当我们分析 tick 的结构时,我们注意到它包含一个时间戳,而这个时间戳可能会成为开发者的另一个头疼之源。这是因为每个数据提供者都认为他们使用的是最方便的数据格式。一如既往,请参考提供者的文档,并参考任何关于处理时间戳的教程。如果你不熟悉时间戳及其标准,我建议从 Avinash Navlani 在 Dataquest 上关于在 Python 中处理时间戳的优秀教程开始(www.dataquest.io/blog/python-datetime-tutorial)。

无论如何,为了简化问题,时间戳是一个按照某种标准格式化的字符串,这种格式化通常是通过空格、特殊字符或普通字符来完成的。在前一节的 LMAX 示例中,日期部分通过字母 T 与时间部分分开,整个时间戳以字母 Z 结尾。

Python 提供了一个 datetime 库,它可以灵活地处理时间戳。这个库引入了同名的 datetime 对象,它具有多种方法可以将字符串转换为时间戳,反之亦然。在接下来的示例中,我们将使用 strptime() 将时间戳从字符串转换为原生的 datetime 对象。

通过使用 strptime() 和其他方法,你所需要做的只是指定输入时间戳的格式,使用正确的指定符(有关指定符的完整列表,请参阅 Python 文档中的 docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior)。

因此,让我们将 LMAX 示例中的时间戳转换为原生的 datetime 对象。首先,我们从 datetime 库中导入 datetime(是的,它看起来很荒谬,但又能怎么办呢?)然后进行转换:

from datetime import datetime
ts_str1 = '2022-07-29T11:10:54.755Z'
ts1 = datetime.strptime(ts_str1, '%Y-%m-%dT%H:%M:%S.%fZ')

在这里,我们假设 ts_str1 是从 LMAX 收到的 tick 中已提取的时间戳,作为 XML(JSON)。如果我们运行此代码并检查 ts1 的值,那么我们将看到以下内容:

In [21]: ts1
Out[21]: datetime.datetime(2022, 7, 29, 11, 10, 54, 755000)

这意味着转换已经正确完成,现在我们可以单独访问时间戳的任何组件。例如,ts1.day 返回 29(月份中的日期),而 ts1.microsecond 返回 755000,这实际上是 755 毫秒。

使用 datetime 对象的真正优势是它们可以像数字一样排序。例如,如果我们收到一个新的时间戳,它只是比原始时间戳晚 1 毫秒(ts_str2 = '2022-07-29T11:10:54.756Z'),并将其转换为新的 datetime 对象(ts2 = datetime.strptime(ts_str2, '%Y-%m-%dT%H:%M:%S.%fZ')),那么我们可以轻松地比较两个时间戳,如下所示:

In [27]: ts1 > ts2
Out[27]: False

随后,我们可以按升序或降序对它们进行排序。

重要提示

永远不要使用字符串作为市场数据时间序列的时间戳 ID。你将无法轻松按到达的确切时间排序数据,因此处理数据将极其低效。请使用原生的datetime或 pandas 时间戳(参见下一节)。

存储和检索 tick 数据

在 Python 中存储和处理 tick 数据主要有三种方法:

  • 使用高级对象

  • 使用底层对象

  • 使用 pandas

高级对象通常用于存储大量数据或处理整个历史数据(参见第二章**,使用 Python 进行交易策略)。在这种情况下,我们创建一个单一的对象,其属性包括列表(如果我们打算逐个存储数据样本并按索引引用它们)或字典(如果我们使用时间戳来引用数据样本)。

使用字典允许通过时间戳快速轻松地处理数据,因此这是存储市场数据的首选方法。让我们看一个例子:

  1. 让我们从创建一个通用类开始:

    class data:
    
        def __init__ (self):
    
            self.series = {}
    

这个main字典将存储所有使用时间戳作为关键字的数据样本。

  1. 现在,让我们添加一个方法,将新的数据样本添加到main字典中:

    def add(self, sample):
    
            ts = datetime.strptime(sample["timestamp"], '%Y-%m-%dT%H:%M:%S.%fZ')
    
            self.series[ts] = sample
    

在这里,我们假设数据样本的形式类似于 LMAX 使用的形式——即 JSON,其中包含ISO 8601格式的时间戳。由于 JSON(或通用的 XML)基本上与原生的 Python 字典相同,我们在字典中添加了一个字典。现在,通过时间戳作为关键字引用self.series将返回另一个包含数据样本本身的字典。这就是我们在本节开头提到的底层对象

重要注意事项

只有在数据馈送不发送具有相同时间戳的 tick 时,我们才能通过时间戳引用 tick。这种情况通常出现在来自交易场所的直接馈送中。建议的代码将始终用具有相同时间戳的最后一个接收到的值重写 tick 的内容,因此如果你真的需要保留所有存储的 tick,请考虑为它们添加唯一标识符UIDs)。本书中用于开发和测试目的的公共 LMAX 数据馈送从未发送具有相同时间戳的 tick。

  1. 因此,现在,让我们添加一个基本函数,通过时间戳查找数据样本:

    def get(self, ts, key):
    
            return self.series[ts][key]
    

注意,这里使用了两个关键字([ts][key]),一个紧接着另一个。这正是因为刚刚解释的数据结构:我们有一个字典中的字典,所以第一个关键字[ts]检索数据样本字典,而第二个关键字——[key]——实际上返回值。

这里需要注意的是,通过时间戳检索刻度的这种方法假设我们知道确切的毫秒级时间戳。如果我们不确定或者想要提取例如在 1 秒或甚至 1 分钟内到达的几个刻度,我们可以使用以下代码,它将返回一个时间戳以相同时间开始的刻度列表(实际上,我们在timestamp键中查找子字符串):

result = [(key, value) for key, value in self.series.items() if key.startswith("2022-07-29T11:10:54")]
return result
  1. 好的——让我们尝试一下我们的代码。让我们使用之前的例子——创建一个新的数据序列对象,向其中添加一个样本,并读取trade_id值:

    sample = {
    
        "type": "TICKER",
    
        "instrument_id": "eur-usd",
    
        "timestamp": "2022-07-29T11:10:54.755Z",
    
        "best_bid": "1.180970",
    
        "best_ask": "1.181010",
    
        "trade_id": "0B5WMAAAAAAAAAAS",
    
        "last_quantity": "1000.0000",
    
        "last_price": "1.180970",
    
        "session_open": "1.181070",
    
        "session_low": "1.180590",
    
        "session_high": "1.181390"
    
    }
    
    series = data()
    
    series.add(sample)
    
    timestamp = datetime.strptime(sample["timestamp"], '%Y-%m-%dT%H:%M:%S.%fZ')
    
    print(series.get(timestamp, "trade_id"))
    

如果我们运行此代码(不要忘记在开头添加from datetime import datetime),我们将得到0B5WMAAAAAAAAAAS,这确实是存储的刻度数据样本的交易 ID。

这样,我们可以轻松地实现创建、添加和从我们的存储中读取数据,而不需要使用任何数据库。当然,这种方法将在某种程度上限制通过其他键检索和聚合数据的能力——例如,检索所有具有相同价格或交易数量位于某个范围内的刻度。

记住

忽略或篡改数据,尤其是接收到的刻度或条形图顺序,很可能会让你开发出一个仅在损坏数据上才能工作的策略,而无法与真实市场数据兼容。

因此,对于大多数实际交易应用来说,最好只能通过时间戳以外的任何关键词来获取数据。然而,如果你从事某种学术研究并且确实需要特殊的数据检索模式,那么对你来说是个好消息:pandas 允许你做到这一点(我们将在第八章使用 Python 进行外汇交易中的数据可视化)进行简要介绍)。

存储市场数据的另一种方式是将数据存储在列表中,而不是字典中。这样,我们可以忘记时间戳,并逐个读取数据样本,使用索引——这在使用历史数据进行回测时特别有用。

然而,将市场数据存储在列表中存在一个显著问题。如果你需要添加任何过去的数据样本,你必须扫描列表以找到插入新样本的正确位置,而这个操作相当耗时。因此,使用以时间戳为关键词的字典总是更可取。

你可能会认为在过去的某个时间插入一个样本听起来很荒谬,而且几乎不需要。好吧,继续阅读这一章,在专门介绍清理市场数据的部分,你会看到这种能力是多么受欢迎。

订单簿(市场深度)

一些数据提供商(实际上并不多)非常慷慨,不仅提供顶部的订单簿(最佳买价和卖价),还提供一些市场深度(参见第三章“交易所和订单簿”部分,从开发者角度的 FX 市场概述*)。一般来说,接收订单簿数据与接收股票行情数据没有太大区别。唯一的区别是,股票行情数据包含每个买价、卖价和最后价格的单个值,而订单簿数据包含多个买价和卖价的值,并且不包含任何最后值。

存储和检索订单簿数据

如果我们查看 LMAX 在其 API 文档中提供的示例,我们可以看到表示订单簿信息的 JSON 可以被 Python 解释如下:

  • 最高级别:一个类似于股票行情数据的字典,但没有最后交易和会话信息

  • 低级别:一个按价格降序和升序排序的买价和卖价列表

  • 最低级别:另一个包含每个买价和卖价的实际价格和数量的字典:

    {
    
        "type": "ORDER_BOOK",
    
        "instrument_id": "eur-usd",
    
        "timestamp": "2022-07-29T11:10:54.755Z",
    
        "status": "OPEN",
    
        "bids":
    
          [
    
                {
    
                    "price": "1.181060",
    
                    "quantity": "500000.0000"
    
                },
    
                {
    
                    "price": "1.181050",
    
                    "quantity": "200000.0000"
    
                }
    
          ],
    
          "asks": [
    
                {
    
                    "price": "1.181100",
    
                    "quantity": "250000.0000"
    
                },
    
                {
    
                    "price": "1.181110",
    
                    "quantity": "350000.0000"
    
                }
    
          ]
    
    }
    

因此,我们可以使用与最初用于存储、添加和读取 tick 数据完全相同的代码。我们只需要添加另一个索引级别和关键字来访问实际值。例如,如果我们添加前面的示例,那么我们可以通过以下代码使用时间戳检索最佳买价:

best_bid = series.get(timestamp, "bids")[0]["price"]

这行的开头与上一节中来自股票行情数据的示例相同,然后是索引 [0],它用于检索最佳买价(由于买价和卖价都已排序,列表中的第一个元素总是包含最佳买价或卖价),最后是另一个关键字—price,用于从价格/量对中检索价格信息。

现在,我们可以检索 tick 数据,但请记住,它可能在内存或磁盘上占用太多空间。所以,只使用特定交易策略所需的数据量会更好。这就是我们进行数据压缩的原因。

数据压缩——将数量保持在合理的最低水平

在上一节中,我们已经考虑了数据提供商使用的一种最流行的数据压缩技术:快照。区别在于,一个 tick 代表一个单一事件(如新交易或买价或卖价的变化)和一个单一的价格值,但快照会丢弃关于单个 tick 的信息,并用每个周期内的以下价格来替换:

  • 周期第一个 tick 的价格(或开盘价

  • 周期最高价格(或最高价

  • 周期最低价格(或最低价

  • 周期最后一个 tick 的价格(或收盘价

例如,如果周期为 1 分钟,在这 1 分钟内进行了 100 次交易,那么快照将用仅仅 4 个价格来替换 100 个 tick(或 100 个价格)。

当在图表上绘制时,产生的快照被称为柱状图。非常频繁地,交易者和开发者使用柱状图而不是快照。从图形上看,柱状图通常表示为带有两个虚线的垂直线。正如你在以下图中可以看到的,指向左侧的虚线表示该柱状图所代表区间的开盘(第一)价格,指向右侧的虚线表示同一区间的最后(收盘)价格,而垂直线的顶部和底部分别代表区间的最高价和最低价。这些柱状图被称为开盘-最高-最低-收盘柱状图OHLC 柱状图

图 5.1 – 柱状图和日本蜡烛图作为数据压缩的视觉表示

图 5.1 – 柱状图和日本蜡烛图作为数据压缩的视觉表示

如果左侧的虚线低于右侧的虚线,我们说柱状图收盘价上升或在该时间段(在该柱状图期间)价格走势是上升的。如果右侧的虚线低于左侧的虚线,我们说柱状图收盘价下降或价格走势是下降的。

表示柱状图的另一种方式被称为日本蜡烛图。它们左右没有虚线,而是将开盘价和收盘价之间的范围绘制为矩形。为了显示开盘价是否高于或低于收盘价,我们使用颜色编码:通常是白色或绿色表示柱状图收盘价上升(收盘价高于开盘价),黑色或红色表示柱状图收盘价下降。

那么,数据压缩是好事还是坏事?

当然,答案取决于使用数据压缩的预期目的。一般来说,数据压缩可以让我们显著减少存储空间。仅为了给你一个概念:将 4 年的历史市场数据压缩成 1 分钟柱状图,如果保存为美国信息交换标准代码(ASCII)CSV 文件,大约需要 152 MB。同样时间段的时间戳数据,根据时间戳数据的类型(是否仅传输最后交易作为时间戳,或者包括每次买卖价格的变化),大约需要 1.4 到 3 GB。

除了这些,使用压缩格式的数据可以极大地加快回测过程(记住,回测意味着使用预先存储的历史价格数据来模拟算法生成的交易)。处理 10 万条记录比处理 100 万条记录快得多,这并不令人惊讶。

因此,当你使用数据供应商的历史数据或从你的经纪人的网站上下载时,很可能会将其压缩到 1 分钟、10 秒或 1 秒,但无论如何,不太可能是原始的时间戳数据。

在开发交易策略时,我们通常使用更少粒度的分辨率,例如 1 小时、4 小时、1 天,有时甚至 1 周或 1 个月。

然而,使用压缩数据对开发者来说有一个严重的缺点。由于单个 tick 已经消失,我们无法在单个条中说出哪个价格先出现,第二个,以此类推。在第十章,“Python 中的订单类型及其模拟”,我们将考虑与使用压缩数据进行交易模拟相关的一些严重问题,并看看我们如何最大限度地减少犯错的几率。

由于我们现在熟悉数据压缩的概念,让我们看看我们如何实际使用它。我们将从检索已压缩数据开始,然后看看我们如何在我们的代码中压缩数据。

获取压缩数据

一些数据供应商、经纪人和交易场所使用压缩数据进行实时流,但大多数用于历史数据。在这种情况下,您可以将其下载为 XML 格式,或者更常见的是,作为传统的 CSV 文件。内容(就像往常一样!)取决于数据供应商的自由意志,但至少应该包含时间戳和开盘价、最高价、收盘价和最低价(OHLC)数据。一些数据供应商还包括交易量数据,甚至包括上涨和下跌的次数(价格分别上涨或下跌的时刻),如下所示:

Date,Time,Open,High,Low,Close,UpVolume,DownVolume,TotalVolume,
UpTicks,DownTicks,TotalTicks
1/27/2015,13:29:00,1.12942,1.12950,1.12942,1.12949,200,150,639,3,2,8
1/27/2015,13:30:00,1.12947,1.12959,1.12941,1.12941,230,438,888,4,7,12
1/27/2015,13:31:00,1.12953,1.12970,1.12951,1.12965,400,240,650,9,4,14
1/27/2015,13:32:00,1.12944,1.12944,1.12883,1.12883,90,609,749,2,10,13

现在,让我们看看我们如何高效地读取、存储和检索历史压缩价格数据:

  1. 首先,我们需要做一些准备工作。显然,我们需要datetime模块,用于存储所有数据的存储空间(字典),以及用于存储单个数据样本的存储空间(另一个字典):

    from datetime import datetime
    
    historical_data = {}
    
    sample = {}
    
  2. 然后,我们需要读取数据。如果我们正在处理 CSV 文件,这可以非常容易地完成,如下所示:

    file_name = '/path/to/the/data/file'
    
    f = open(file_name)
    
  3. 让我们读取第一行(标题行),以避免在解析价格数据时可能出现的错误:

    f.readline()
    
  4. 接下来,我们将逐行读取文件:

    for line in f:
    
  5. 在循环体中,我们删除任何结尾的换行符,并将接收到的行解析为values列表:

        values = line.rstrip("\n").split(",")
    
  6. 现在,我们将日期和时间合并成一个单独的字符串,并将其转换为datetime对象:

        timestamp_string = values[0] + " " + values[1]
    
        ts = datetime.strptime(timestamp_string, "%m/%d/%Y %H:%M:%S")
    
  7. 然后,我们将其余的信息收集到一个字典中:

        sample["open"] = float(values[2])
    
        sample["high"] = float(values[3])
    
        sample["low"]  = float(values[4])
    
        sample["close"]= float(values[5])
    
        sample["UpVolume"] = int(values[6])
    
        sample["DownVolume"] = int(values[7])
    
  8. 现在,使用timestamp作为关键字,将新样本添加到全局数据集字典中:

          historical_data[ts] = sample
    
  9. 现在,如果我们检查数据集中的第一条记录,我们将得到以下结果:

    {'open': 1.12942, 'high': 1.1295, 'low': 1.12942, 'close': 1.12949, 'UpVolume': 200, 'DownVolume': 150}
    
  10. 现在,如果我想在某个特定日期和特定时间检索收盘价的特定值,我可能想使用以下类似的方法:

    historical_data[timestamp]['close']
    

其中时间戳表示您想要检索收盘价的时间戳。

在运行回测时,我们并不是通过时间戳来检索数据样本的;相反,我们希望有一个方法可以连续逐个按严格的时序顺序获取样本。Python 的本地字典通常包含未排序的数据;然而,有一个解决方案允许我们通过关键字排序数据——使字典键排序,并自动确保数据样本始终按正确的时序顺序排序。或者,您可以使用 Python 本地dict对象的子类OrderedDict,它实现了一个键预先排序的字典(您可以在www.tutorialspoint.com/ordereddict-in-python找到关于OrderedDict的简单教程)。

要做到这一点,我们使用内置的sorted方法,它默认按升序返回任何可迭代对象的排序值列表——这正是我们想要的:

for ts in sorted(historical_data):
    print(historical_data[ts])

上述代码将按严格的时序顺序打印从第一个到最后一个的所有数据样本(不要用大量数据集运行此代码,因为它将花费大量时间来显示!)。

现在,我们可以读取历史数据,按正确的时序顺序对其进行排序,并逐个检索——这就是我们为算法回测所需要的一切。

在 Python 中压缩市场数据

即使数据提供者提供了完整的价格数据流,我们仍然可能希望对其进行压缩,而不仅仅是节省磁盘存储空间。这种压缩的原因可能更为重要,例如,我们的交易算法可能能够生成一些信号或使用压缩的历史数据计算一些辅助指标。因此,我们还需要以类似的方式向算法提供压缩数据。尽管价格数据提供了更多信息,我们仍然可能希望对其进行压缩,以使其与交易逻辑兼容。

将数据从价格数据压缩到 OHLC(开盘价、最高价、最低价、收盘价)条的最正确的方法是有一个接收价格数据的方法,每个价格数据点重新计算 OHL(开盘价、最高价、最低价)值,然后在适当的时候添加 C(收盘价)值。例如,如果我们想将价格数据压缩成 1 分钟条,这个方法将在每分钟的开始处创建一个新的条,并在分钟间隔过去时完成它。让我们通过一个例子来更好地理解这一点:

  1. 假设我们有一个本地文件中的历史价格数据(我们将看到如何编写良好的代码使我们能够快速从文件切换到任何其他数据源):

    file_name = '<your_path_name>/EURUSD 1 Tick.csv'
    
    f = open(file_name)
    
    f.readline()
    

我们立即读取文件的第一行,并且永远不会将其用作第一行,因为它只是标题。

  1. 接下来,我们不要忘记导入datetime并创建两个字典——一个用于整个数据系列,另一个用于单个条目:

    from datetime import datetime
    
    bars = {}
    
    bar = {}
    
  2. 接下来,我们为我们的新形成的时间条设置分辨率。我建议在这里使用一个通用的测量单位。例如,如果您使用的是相对较慢的交易策略,那么您可能希望按分钟计算时间。为了获得更高的精度和粒度,您可能希望降低到秒。所以,让我们使用秒作为单位,形成 1 分钟的条目(1 分钟条目等于 60 秒):

    resolution = 60
    
  3. 接下来,我们应该从文件中读取另一行,仍然在主循环之外,以获取第一个时间戳。我们将比较所有后续的时间戳与这个时间戳,一旦两个时间戳之间的距离(时间)大于分辨率(在我们的例子中是 60 秒),我们就会开始一个新的条目:

    values = f.readline().rstrip("\n").split(",")
    
    timestamp_string = values[0] + " " + values[1]
    
    last_sample_ts = datetime.strptime(timestamp_string, "%m/%d/%Y %H:%M:%S.%f")
    

最后,主循环开始:

for line in f:
    values = line.rstrip("\n").split(",")
    timestamp_string = values[0] + " " + values[1]
    ts = datetime.strptime(timestamp_string, "%m/%d/%Y %H:%M:%S.%f")

到目前为止,循环体的代码与我们之前所做的完全相同——我们读取一行新内容并解析它。但是,然后我们比较接收到的时间戳与最后一个形成的条目或样本的时间戳:

    delta = ts - last_sample_ts

在这里,您可以感受到使用日期时间格式的时间戳的所有美丽。您可以像处理常规数字一样添加或减去它们。

神经质者的笔记

Python 支持多种处理日期和时间的类型。除了datetime之外,还有一个有用的类型——timedelta,它允许轻松定义时间跨度。如果您想了解更多关于timedelta及其如何高效使用的信息,请尝试这个教程:tutorial.eyehunts.com/python/python-timedelta-difference-two-date-time-datetime/

  1. 现在,当新标记的时间戳与上一个完全形成的条目时间戳之间的时间增量大于分辨率时,我们将当前条目添加到我们的bars全局字典中,并通过用相同的价格(最后标记的价格)替换其值来开始一个新的条目。别忘了在途中将字符串转换为数字:

        if delta.seconds >= resolution:
    
            bars[ts] = bar
    
            bar["open"]  = float(values[2])
    
            bar["high"]  = float(values[2])
    
            bar["low"]   = float(values[2])
    
            last_sample_ts = ts
    

或者,如果时间增量仍然小于分辨率(在我们的例子中,它小于 1 分钟),那么我们只需更新当前条目的值:

    else:
       bar["high"] = max([bar["high"], float(values[2])])
       bar["low"] = min([bar["low"], float(values[2])])
       bar["close"] = float(values[2])

看起来我们的编码工作已经完成了。好吧——让我们运行我们的代码,我们会立即得到一个错误:

File "/.../example5.py", line 36, in <module>
    bar["high"] = max([bar["high"], float(values[2])])
KeyError: 'high'

这意味着我们形成当前条目的字典中没有high关键字。这是怎么发生的?当然,这是在形成第一个条目时发生的:在我们保存至少一个条目之前,它的任何属性(开盘价、最高价、最低价或收盘价)都是不可用的。因此,我们通过添加一个try...except语句来修复我们的代码:

else:
        try:
            bar["high"] = max([bar["high"], float(values[2])])
            bar["low"] = min([bar["low"], float(values[2])])
            bar["close"] = float(values[2])
        except:
            print('first bar forming...')

现在,在形成第一个条目时,我们只能在屏幕上看到First bar forming...。您可以替换这条消息,或者通过将print语句替换为pass来完全删除它。

  1. 再次运行代码,我们可以看到现在它已经成功执行了。如果我们检查bars变量中的最后 4 条记录,我们可以看到这些条目的时间戳确实大约有 1 分钟的增量:

    datetime.datetime(2022, 8, 8, 18, 53, 8, 64000): {'open': 1.01973,
    
      'high': 1.01984,
    
      'low': 1.01972,
    
      'close': 1.01972},
    
     datetime.datetime(2022, 8, 8, 18, 54, 8, 347000): {'open': 1.01973,
    
      'high': 1.01984,
    
      'low': 1.01972,
    
      'close': 1.01972},
    
     datetime.datetime(2022, 8, 8, 18, 55, 10, 731000): {'open': 1.01973,
    
      'high': 1.01984,
    
      'low': 1.01972,
    
      'close': 1.01972},
    
     datetime.datetime(2022, 8, 8, 18, 56, 12, 81000): {'open': 1.01973,
    
      'high': 1.01984,
    
      'low': 1.01972,
    
      'close': 1.01972}}
    

但是等等!时间戳是正确的,但为什么所有条形图中的相应价格(比较开盘到开盘、收盘到收盘等)都是相同的?!

这里,我们有一个问题,这实际上是 Python 作为非常成熟面向对象语言的最突出特征之一。我们在代码中实际执行的动作序列如下:

  1. 我们创建object1bars字典)。

  2. 我们创建object2bar字典)。

  3. 我们使用new关键字将object2添加到object1中。

  4. 然后,我们修改object2

  5. 再次,我们使用new关键字将object2添加到object1中。

  6. ...然后重复这个循环。

看到了吗?我们原本以为每次想要向bars中添加新的条形图时,都会添加一个新的对象,但实际上,我们添加的是相同的对象——相同的bar字典——只是值被修改了。我知道一开始这可能很难理解,所以试着这样想:bars[ts] = bar这个赋值操作意味着将bar对象保存为bars对象中的链接,使用ts关键字。在这种情况下,一旦bar对象本身被修改,bars对象中对它的引用保持不变,所以当我们尝试从bars中检索bar对象时,我们总是得到修改后的值。

但我们实际上在这里想要做什么呢?当然,我们想要保存每个条形图单独,这样如果我们在bars对象外部更新bar对象,其新值不会影响已经存储在bars对象中的任何内容。

实际上,我们想要保存的是bar对象的副本,这样当我们未来更新bar对象时,它将保持未修改。为此,我们只需将bars[ts] = bar替换为bars[ts] = dict(bar)。在这里,dict是一个类,可以从任何可迭代对象、映射对象或关键字参数生成字典。在我们的情况下,它相当简化,因为我们使用一个现成的字典(bar)作为映射对象。

如果我们现在运行代码,进行这个编辑,我们可以看到,不仅时间戳是正确的,bars中的价格数据也是正确的:

datetime.datetime(2022, 8, 8, 18, 53, 8, 64000): {'open': 1.01982,
  'high': 1.02007,
  'low': 1.01982,
  'close': 1.02001},
 datetime.datetime(2022, 8, 8, 18, 54, 8, 347000): {'open': 1.01996,
  'high': 1.01998,
  'low': 1.01979,
  'close': 1.01981},
 datetime.datetime(2022, 8, 8, 18, 55, 10, 731000): {'open': 1.01977,
  'high': 1.01982,
  'low': 1.01965,
  'close': 1.01965},
 datetime.datetime(2022, 8, 8, 18, 56, 12, 81000): {'open': 1.01968,
  'high': 1.01971,
  'low': 1.01964,
  'close': 1.01968}}

因此,现在,我们有了由 tick 数据形成的 1 分钟条形图。

但是它们的 时间戳代表什么意思呢?

它们实际上参考的是哪个时间点呢?

那么为什么这些时间戳的秒值不是零(正如我们期望的 1 分钟分辨率那样),而且条形图 到条形图之间也略有不同呢?

为了回答第一个问题,我们当前的算法保存的是时间间隔的最后一次 tick 的时间戳。这是因为我们开始形成新条形图的触发器是if delta.seconds >= resolution。所以,if语句在接收到新 tick 时立即开始新条形图(并且不能早于!)新 tick 的时间戳与其直接前驱的时间戳之间的差值(称为timedelta)大于resolution。换句话说,如果我们将resolution设置为 1 秒,那么只有当我们收到一个时间戳与当前正在形成的条形图的开盘(开始)相差 1,000 毫秒或更多的新 tick 时,我们才会开始新的条形图。

这也回答了两个剩余的问题。由于接收到的第一个 tick(数据文件中的第一个 tick)的时间戳可能不是一个整数,并且分钟数没有零秒,所以我们不是从分钟的开始计算 60 秒,而是从任意时刻开始计算。

你可能会问,“这是好是坏?”。

两者都不是。

正确的问题应该是“它适合我的交易算法吗?”。

答案取决于你算法的逻辑。如果它只分析价格序列(或者如果数据提供者提供了量,那么可能是量),那么是的——这种方法工作得很好,并且很容易实现。然而,如果你的策略逻辑假设在分钟的真正边缘(或任何其他时间间隔)触发订单或进行计算,那么这种方法就不适用。

幸运的是,我们可以轻松修改我们的代码,使其生成具有精确 1 分钟分辨率的 K 线。问题是,没有通用的方法来做这件事,选择取决于你是与实时数据流还是与保存的 tick 数据进行工作。为了更好地理解这一点,让我们首先快速回顾一下从外部数据源检索数据并保持你的交易应用模块化和可扩展的问题。然后,你就会理解在这个范式下如何优雅地解决生成正确时间戳的问题。所有这些内容将在下一节中介绍!

处理保存的和实时数据——保持你的应用通用

在上一章中,我们快速了解了从 LMAX 获取市场数据的方法,LMAX 是外汇市场最重要的 ECN 之一。在不深入太多技术细节的情况下,我们可以假设大多数其他经纪商、交易场所和数据供应商使用的是基于套接字连接的更多或更少的相同协议和 API。因此,重新适配你的代码以从新的数据源检索数据应该不会是问题。

然而,从上一章,我希望你也能记住,尽管连接的传输层有相似之处,几乎每个数据源都有其独特的特性,这些特性只能在它的文档中找到(有时,不幸的是,只有在与它的技术支持直接交谈时才能找到)。

这意味着即使你为例如——金融信息交换FIX)协议版本 4.4 实现了一个经纪商,当你想要连接到另一个经纪商时,你很可能需要在你的代码或 FIX 字典中修改某些内容。对于专有 API,情况显然更加复杂,有时整个代码都应该重写(包括传输层之上的所有内容,如套接字连接)。

因此,在构建交易应用时,一个好的做法是确保它具有模块化结构,其中模块通过使用内部、内置的通用传输基础设施相互通信。这种基础设施应在模块之间传输价格信息,而不管它们的特定实现如何,即使它们由第三方提供。在这种情况下,一旦你想切换到另一个数据源,你不需要修改整个应用程序:你只需要编写一个新的模块,如果你愿意,还可以编写一个新的插件,该插件将连接到新的源,但以与应用程序其余模块兼容的相同内部格式提供数据。

通常,我们希望创建一个如图 图 5**.2 所示的架构:

图 5.2 – 交易应用最简单的数据传输基础设施

图 5.2 – 交易应用最简单的数据传输基础设施

然而,这种简单的架构存在明显的缺陷:

  • 它只支持一个数据源

  • 它只支持一个交易算法

因此,我们可能想要稍微升级我们的架构,为我们的交易算法添加多个数据流,如下所示:

图 5.3 – 使用单个交易算法的多个数据源

图 5.3 – 使用单个交易算法的多个数据源

好吧,这个架构揭示了一个新的问题:如何同步多个数据流?如果我们有多个来源,我们如何决定将哪个 tick 传递给算法?我们如何请求这些来源?如果我们从全局无限循环(例如从文件读取数据时的 for line in file 或套接字连接时的 while True)这样做,那么切换到另一个数据源将会非常困难,因为这几乎需要重写整个代码。

此外,如果我们想并行运行多个交易算法,使用多个数据源,我们很快就会陷入混乱:

图 5.4 – 以错误的方式使用多个数据源和多个算法

图 5.4 – 以错误的方式使用多个数据源和多个算法

现在,我们清楚地理解了构建稳健基础设施所需的内容。我们需要:

  • 能够在不相互干扰的情况下并行运行多个数据检索进程

  • 能够将接收到的数据存储在通用的弹性存储中,该存储正在填充新数据,并在较老的 tick 被交易算法处理后被清空

因此,我们正在讨论一个如图 图 5**.5 所示的架构,具有多个数据连接器,每个连接器都有连接、获取数据和将其写入某种队列的方法,以及多个交易算法(关于其内部架构我们目前一无所知),它们使用通用 弹性存储 交换信息,如前所述:

图 5.5 – 交易应用数据传输层的一个更好的架构

图 5.5 – 交易应用数据传输层更好的架构

Python 确实以非常流畅和方便的方式(一如既往)提供了对这两个问题的原生解决方案。运行数据连接器的并发能力由线程提供,而之前提到的“弹性存储”是一个队列。让我们对两者进行一些深入了解。

线程是程序代码的独立执行分支,它并发地运行与主代码或与其他线程并行。它只能在面向对象编程OOP)的范式内实现,在那里我们不再有一个单一的算法,它只有一个开始和一个结束点,而是有具有自己行为的对象,它们可以共存而不相互干扰或相互作用,这取决于其创造者的意愿。因此,线程是一个在与其他所有事物并行运行函数(方法)的对象。这正是我们从多个来源检索数据而不需要同步时所需要的,对吧?

队列...嗯,就像圣诞节时你在收银台看到的队列一样。顾客是按照先到先服务的原则被服务的。Python 队列中的数据被以同样的方式处理:任何进入队列的数据都会随着旧数据的处理而移动。一旦检索到新元素,它就会被从队列中移除,为新数据腾出空间。

为了更好地理解队列的工作原理,让我们考虑一个非常简单的例子:

import queue
q = queue.Queue()
q.put("Sample 1")
q.put("Sample 2")
print(q.get())

如果你运行这段代码,它将打印Sample 1,因为队列中的第一个元素是Sample 1字符串。然而,如果你在一个交互式控制台中再次输入print(q.get())(或者只是将另一个print(q.get())语句添加到代码中),那么这个第二个print语句将打印Sample 2,因为Sample 1已经被.get()方法检索并从队列中删除。

现在我们已经知道了线程和队列是什么,我们可以为我们的交易应用数据传输层的架构提出一个最终草案:

图 5.6 – 交易应用数据传输层架构的最终草案

图 5.6 – 交易应用数据传输层架构的最终草案

让我们现在尝试将其在代码中实现。

通用数据连接器

让我们从必要的准备工作开始:

  1. 我们需要指定源数据文件,读取标题,并进行一些导入:

    file_name = '<your_file_path>/EURUSD 1 Tick.csv'
    
    f = open(file_name)
    
    f.readline()
    
    from datetime import datetime
    
    import threading
    
    import queue
    
    import time
    
  2. 现在,让我们创建一个通用数据流,所有检索到的数据都将写入其中。当然,它是一个 Python 的queue类的实例:

    datastream = queue.Queue()
    
  3. 现在,让我们创建我们的第一个检索数据的方法,这次是从本地文件中检索:

    def get_tick():
    
        tick = {}
    
        values = f.readline().rstrip("\n").split(",")
    
        timestamp_string = values[0] + " " + values[1]
    
        ts = datetime.strptime(timestamp_string, "%m/%d/%Y %H:%M:%S.%f")
    
        tick[ts] = float(values[2])
    
        return tick
    

你能看到已经熟悉的代码吗?是的,当然——这只是一个复制了我们之前所做的,但这次被封装在一个函数中。到目前为止,与之前没有区别。区别在于以下两个函数:

def emulate_tick_stream():
    while True:
        time.sleep(1)
        temp = get_tick()
        datastream.put(temp)
def trading_algo():
    while True:
      temp = datastream.get()
        print('Received tick ', temp)

第一个函数emulate_tick_stream(),每秒从文件中读取一个新的 tick 并将其放入datastream队列中。

注意

这里添加的 1 秒延迟只是为了演示目的——真实的数据检索方法不应该包含任何延迟!

第二个函数只是一个原型——一个模拟的交易算法。它什么也不做,只是报告它成功接收了一个新的数据样本。

  1. 现在,是时候并行运行这两个过程了——检索和处理数据。Python 线程的时代到来了:

    data_source_thread = threading.Thread(target = emulate_tick_stream)
    
    data_receiver_thread = threading.Thread(target = trading_algo)
    

这样,我们创建了两个Thread类的实例,一个用于检索数据(data_source_thread),一个用于处理数据(data_receiver_thread)。作为唯一的参数,我们传递了希望在线程中运行的函数的名称。

注意

我们传递函数名,并不调用函数本身!如果你输入像threading.Thread(target = trading_algo())这样的内容,函数将在它被传递到线程中的那一刻被调用,所以线程永远不会真正启动!

  1. 我们现在需要做的就是同时运行两个线程:

    data_source_thread.start()
    
    data_receiver_thread.start()
    

就这样!现在如果我们在一个新的控制台窗口中运行代码,我们会看到类似这样的内容:

('Received tick ', {datetime.datetime(2022, 8, 8, 13, 50, 30, 446000): 1.01896})
('Received tick ', {datetime.datetime(2022, 8, 8, 13, 50, 31, 505000): 1.01895})
('Received tick ', {datetime.datetime(2022, 8, 8, 13, 50, 33, 619000): 1.019})
('Received tick ', {datetime.datetime(2022, 8, 8, 13, 50, 36, 793000): 1.01901})

每秒会出现一条新行(因为我们get_tick()函数中有一个 1 秒的延迟)。

即使在这个简单的例子中,我们也能看到使用面向对象的方法开发交易应用以及特别是使用线程和队列的强大功能。现在,你可以重写get_tick()函数以连接到完全不同的数据源,而你的其余代码将保持完全、绝对的不变!你可以用从套接字读取、从 REST API 读取或从你(或你的经纪人)可能想象的一切来替换从文件读取。只要你的新get_tick()函数返回相同格式的数据,它就不会影响你的其余代码。

现在,我们可以回到之前章节中突然中断的数据压缩讨论。

数据压缩再探讨

记住,我们将 tick 数据压缩成了 1 分钟的柱状图,但实际的分钟开始和结束并不是当秒数为零时,而是在一分钟间隔内的任意一点。那时,我们无法解决这个问题,因为我们只能以我们不知道的速度逐个从文件中读取数据样本。

但有了线程和队列的力量,当新柱状图在正好 00 秒开始时,我们可以以完美的精度形成正确的 1 分钟(或任何 N 分钟)柱状图。我们需要的只是添加一个新函数并在一个线程中运行它。

注意

注意现在定制数据处理是多么容易。我们不再重写整个代码——我们只需添加新的功能或修改一个函数。

因此,让我们创建一个函数,它将使用系统计时器将我们的传入数据流拆分成 1 分钟的柱状图:

  1. 我们将从一个用于当前正在形成的柱状图的字典开始(如前例所示),并且,正如在应该在工作线程内工作的函数中一样,我们从一个无限循环开始:

    def compressor():
    
          bar = {}
    
          while True:
    
  2. 然后,我们将从数据流中读取数据:

              tick = datastream.get()
    

注意,这个调用对任何数据接收者都是通用的,无论是交易策略、数据压缩器、数据库还是其他任何东西。

  1. 接下来,我们将检查当前时间(系统时间),如果其秒值为零,我们将保存形成的柱状图并开始一个新的柱状图:

              current_time = datetime.now()
    
              if current_time.second == 0:
    
                  bars[current_time] = dict(bar)
    
                  bar["open"]  = tick.values()[0]
    
                  bar["high"]  = tick.values()[0]
    
                  bar["low"]   = tick.values()[0]
    
                  print(bars)
    

代码的其余部分与前面提到的示例相同,它处理在第一根柱状图未完成时引发的异常:

            else:
                try:
                    bar["high"] = max([bar["high"], tick.values()[0]])
                    bar["low"] = min([bar["low"], tick.values()[0]])
                    bar["close"] = tick.values()[0]
                except:
                    print(str(current_time), ' bar forming...')

你可以看到,这个函数几乎完全复制了我们之前在Python 中压缩市场数据部分使用的代码,唯一的重大修改是现在,我们不是比较单个 tick 的时间戳,而是通过系统计时器来最终确定柱状图。一旦系统时间超过一分钟,这意味着其秒值为零,我们就开始一个新的柱状图。

实际上,时间同步是算法交易中最大的问题之一。交易服务器的系统时间可能与数据供应商的时钟不同,而经纪商或 ECN 的时钟可能又与两者都不同。幸运的是,在接下来的示例中,我们将只使用 1 秒的实时数据快照和 1 分钟的的历史数据柱状图,因此现在可以暂时忽略时钟同步的问题。

如果你使用预先保存的历史 tick 数据运行此代码,你会看到类似这样的情况:

{datetime.datetime(2022, 8, 10, 20, 4, 0, 287224): {}, datetime.datetime(2022, 8, 10, 20, 5, 0, 456837): {'high': 1.01947, 'close': 1.01947, 'open': 1.01918, 'low': 1.01918}, datetime.datetime(2022, 8, 10, 20, 6, 0, 639863): {'high': 1.0195, 'close': 1.01925, 'open': 1.01945, 'low': 1.01925}}

你可以看到,第一根柱状图没有价格数据,因为它是在第一分钟结束之前开始的。所有随后的柱状图都有 OHLC 值,这些值对应于系统计时器触发新柱状图时的最后几秒钟的 tick

现在,我希望你理解为什么来自经纪商的历史压缩数据总是有很好的圆形时间戳:它们并不对应实际的 tick;它们只是被系统计时器分割成柱状图。

你可以看到,这个方法与从经纪商、交易场所或类似来源接收的实时数据配合得非常好。它并不适合处理预先保存的历史数据,因为它可以从磁盘以严重超过从市场接收 tick 的实际速度读取。因此,对于历史数据,最好使用我们在本章前面Python 中压缩市场数据部分考虑的tick-to-tick 时间戳比较方法。

如果你不太熟悉线程和队列,并且想了解如何在你的交易应用中使用它们,我建议从这里开始一个简单的教程(realpython.com/intro-to-python-threading/),然后参考这里的官方 Python 文档(docs.python.org/3/library/threading.html)以获取技巧和深入了解。

摘要

在本章中,我们学习了如何处理实时和历史市场数据,更重要的是,如何以高效的方式进行。我们现在熟悉了基于时间的时间条,这在交易策略中应用最为广泛。我们还了解到了一种解决方案,它有助于保持我们的交易应用灵活,并能快速从一种数据源切换到另一种数据源,这在未来从测试切换到生产时将有所帮助。

现在,我们已经准备好将处理过的市场数据应用于我们的交易策略中。让我们看看在下一章我们将如何做到这一点。

第六章:基本面分析的基础及其在 FX 交易中的可能用途

我确信,几乎所有曾经对交易感到好奇的人都知道基本面和技术分析。正如许多流行术语一样,两者都存在很多误解。

很可能,你知道存在分析师这一职业,并且这个职业在金融行业中很受欢迎。你可能甚至考虑过自己从事这一职业,因为你听说现代分析师使用先进的计算机技术,并来自数据科学领域。然而,这两个概念之间存在一个关键的区别;因此,与每个概念一起使用的数学和计算机库存截然不同。此外,它们在实际交易算法中的应用也相当不同。让我们深入了解这两个概念,看看我们如何在我们的应用中使用它们(以及是否可以使用)。

我们将回顾基本面和技术分析的关键原则,了解市场如何对重要的经济、政治和其他事件做出反应,并熟悉分析价格行为的两种方法:一种基于非价格信息,另一种仅使用价格时间序列。

在本章中,我们将涵盖以下主题:

  • 基本面分析

  • 从基本面分析的角度看经济新闻

  • 政治事件

  • 行业特定信息

基本面分析 – 直观但实际应用有限

基本面分析的理念很容易理解:市场众所周知会对各种外部信息做出反应,因此让我们研究这种反应,以帮助我们预测市场行为。所涉及的外部信息取决于市场,可能存在显著差异。

让我们从显然对市场价格有明确影响的因素开始:经济新闻。

经济新闻

这是最知名的基本数据类型。用通俗的话说,经济状况越好,资产的价格就越高。例如,如果全国经济显示出增长,最流动的股票也会增长。是的,当然,有例外、细微差别等,但总体而言,主要宏观经济指标与股市增长之间存在正相关关系。

但等等,我们为什么在谈论股市,而我们真正感兴趣的是外汇FX)呢?

在外汇市场中,如国内生产总值GDP)、失业率核心价格指数CPI – 主要通胀指标)等经济指标并没有任何真正的长期影响。为什么?因为货币有一个使其与其他任何资产类别完全不同的特征:它们有利率

要理解利率的概念,我们应该回忆一下将资金带入经济的机制。中央银行或类似的机构(如美国的联邦储备)发行货币。在早期,这意味着铸造硬币,然后印刷纸币,而今天,这也(主要是)意味着在电子数据库中进行更改。

但无论如何,资金流入经济的路径都要经过银行。中央银行不会免费向银行提供资金——银行以随后被称为利率的利率向中央银行支付利息。银行也会以不同的利率向他们的客户——零售商、企业和其他银行——贷款,这些利率当然高于他们从中央银行借款的利率。而且当你的经纪人提供信用额度以便利保证金交易(参见第三章从开发者的角度看外汇市场概述,在交易机制——一些术语部分),他们并不是免费提供,而是以略高于他们可以借到的利率提供。

这种结构乍一看可能有点令人困惑,但你应该记住的关键点是以下这些:

  • 在银行结算日之间,你当天所做的任何事情都不会计入任何利息

  • 一旦你持有外汇头寸过夜,你要么支付利息,要么收取利息——这取决于交易货币对中货币之间的利率差异

让我们考虑一个例子。2007 年,日元可以以接近零的利率借出,而英镑的利率超过 5%,在 2007 年 7 月达到 5.75%。如果你在 2007 年购买了 GBPJPY——记住这意味着你通过卖出日元来购买英镑——你将每年获得大约 5%的利率差异,你将每晚收取这笔利息。相反,如果你在 2007 年卖出 GBPJPY,那么你将每晚支付利息。现在,将这些数字乘以经纪人提供的杠杆(而且回到 2007 年,在某些情况下,对于选定客户可以达到 700:1!)你可以想象一种外汇交易的圣杯,这被称为持仓交易

持仓交易

持仓交易指的是购买一种资产并持有相当长的时间,不仅旨在通过在更高的价格卖出资产来获利,而且还收集利息。如果你对了解更多关于持仓交易感兴趣,我建议从 Investopedia 上的入门文章开始:www.investopedia.com/articles/forex/07/carry_trade.asp

当然,套息交易不会持续太久,因为利率会变化,自 2008 年金融危机后引入的零利率政策ZIRP)以来,套息交易已经大幅下降。然而,理解市场上的钱都不是免费的这一事实,有助于理解为什么宏观经济指标对货币市场的影响不像对股票和股权那样具有长期性。

尽管如此,宏观经济新闻对货币市场确实有一定的影响。这里的要点是,尽管股票和股权更多地受到经济新闻背景的影响,但它们都是为了金钱而购买的。因此,尽管不是立即的,利率也会影响股票和股权的投资成本。因此,重要宏观经济新闻的发布(尤其是 GDP、失业率和 CPI)受到货币交易者的密切关注,这就是为什么我们通常预期在这些经济指标发布前后货币价格会有快速变动。

为了说明这一点,让我们回顾一下历史上的几个例子。

美国非农就业人数

美国非农就业人数(NFP)是最受关注的几个关键宏观经济指标之一。它代表除农业外全国所有行业的就业岗位新增数量。通常,这个指标读数越大,被认为国家的经济状况越好。因此,股市通常随着新就业岗位的增长而增长,国内货币相对于其他货币升值。

在外汇市场中,我们经常观察到在美国 NFP 发布时价格急剧变动。这通常发生在发布的读数与之前预测值有显著偏差时。读数本身是正还是负并不那么重要。例如,如果预测预测新就业岗位增长 5%,而实际发布的值只有 2%,那么尽管这个值是正的,市场反应很可能是负面的——因为这个值低于预期。图 6.1显示了 2022 年 8 月 5 日美国 NFP 发布后几秒钟 EURUSD 价格变动的良好例子:

图 6.1 – 美国 NFP 发布后 EURUSD 的急剧价格变动

图 6.1 – 美国 NFP 发布后 EURUSD 的急剧价格变动

这种变动可以用流动性来解释(参见第三章从开发者角度的外汇市场概述)。让我们一步一步地重建这种情况:

  1. 在新闻发布前几分钟,流动性提供者更倾向于在发布前从订单簿中撤回流动性,以避免可能的损失。我们已经知道,流动性越少,价格变动越快、越大。

  2. 在新发布的那一刻,订单簿中的流动性极低,几乎没有。价差可能达到正常水平的 10 倍,甚至更多。

  3. 新闻发布后,价格接受者发送的第一个订单可能会大幅移动价格。这通常在实时图表上观察时被视为一个非常快速的跳跃。

  4. 后续订单可能会进一步推动价格向原始方向移动,或部分或完全反转。

  5. 在新闻发布一段时间后,流动性返回市场,价格变动的幅度恢复到中值。

让我们放大 8 月 5 日发生的事情,并查看个别逐点数据。图 6.2显示了美国 NFP 发布时刻的历史数据逐点:

图 6.2 – 2022 年 8 月 5 日美国 NFP 发布时 EURUSD 的逐点图表

图 6.2 – 2022 年 8 月 5 日美国 NFP 发布时 EURUSD 的逐点图表

因此,我们现在可以看到,在事件发生时,流动性如此稀薄,以至于价格在一个逐点中下跌了 20 点。

注意

这意味着实际上在 1.0210 和 1.0230 之间没有任何交易机会。如果你在那个时刻尝试发送市价订单,它很可能会以更低的价格成交,因为很可能你不是流动性池中最快的交易者。

国内生产总值(GDP)

让我们考虑另一个类似事件的例子——这次是 2022 年 8 月 12 日英国 GDP 数据的发布。图 6.3显示了 GBPUSD(英镑兑美元)的 1 分钟图表,展示了事件周围的价格变动。我们可以看到,这一次,情况不同;我们可以看到事件发生后(图表上的 8:00 a.m.,GMT+2 时间),价格急剧变动,但随后几乎立即下降,价格迅速回到了事件之前的价格水平:

图 6.3 – 2022 年 8 月 12 日英国 GDP 发布,1 分钟图表

图 6.3 – 2022 年 8 月 12 日英国 GDP 发布,1 分钟图表

让我们通过查看实际交易数据来看看市场发生了什么。图 6.4显示了同一事件周围的价格变动逐点图表:

图 6.4 – 2022 年 8 月 12 日英国 GDP 数据发布期间 GBPUSD 的交易图表

图 6.4 – 2022 年 8 月 12 日英国 GDP 数据发布期间 GBPUSD 的交易图表

同样,正如美国 NFP 的情况一样,在这条经济新闻发布后,几乎没有流动性,因此,在发布后立即或发布前立即尝试交易真的是一个问题。我们可以看到,事件发生后 1-2 秒,价格稳定在一个比发布前大约高 16-20 点的价格水平。这意味着到那时,流动性提供者已经回到市场,但现在,他们的出价和要价比仅仅几秒钟前显著提高。

从基本面分析角度看的经济新闻

那么,在两种情况下,基本面分析师会说什么呢?让我们阅读一份典型的市场报告:

“今天每周的首次失业救济金申请人数为 234k,高于早上 8:30 发布的预期 220K,对美元相对于其对手货币的影响微乎其微。我们将在上午 10 点发布 4 月份的现有房屋销售数据。市场参与者将对费城联邦储备银行行长哈克和亚特兰大联邦储备银行行长博斯蒂克今天在达拉斯会议上对昨日联邦公开市场委员会会议纪要的评论感兴趣。”(来源:OFX,sitecore.prd.ofx.com/en-us/forex-news/daily-and-weekly-market-news/20180524/fomc-minutes-followed-up-by-fed-speakers-todays/.)

我们在这里只能看到一些已知事实的陈述,以及对市场参与者感兴趣的模糊提及。因此,这种分析对实际即时交易没有用处:它没有给我们任何关于可能的价格变动情景的想法,这些情景可能被利用。

机构基本面研究要复杂得多,包括许多因素,但长期来看,它们只能产生长期预测或投资建议。这是因为现代市场之间联系紧密,这些联系非常复杂(同时,影响市场价格的因素数量众多),使用宏观经济事件的基本面分析进行实际交易变得非常困难。

那么,这意味着我们可以安全地忽视基本面分析和宏观经济事件吗?

当然不是!

上述例子清楚地说明了在重要经济指标发布时,外汇市场在其微观层面通常会发生什么。虽然很难预测新闻发布后价格变动的确切方向,但我们可以说,几乎有 100%的概率,会有流动性不足和价格剧烈波动,总体而言,进行交易将非常危险。我从不建议您轻信我的话——我鼓励您下载历史外汇价格数据以及过去经济事件列表,并查看在重要指标发布时此类价格行为的其他例子。您自己会看到的:在 10 个案例中,有 9 个案例只证实了以下重要结论。

注意

请避免在重要宏观经济新闻发布时进行交易。如果您的交易逻辑建议您保持已开放的头寸,您可以这样做,但在事件前后直到流动性返回订单簿之前,请避免立即开仓或平仓。

政治事件

政治事件(如总统选举、战争、全球条约和宣言等)也会影响市场,此类事件前后价格行为与对常规经济新闻的反应有些相似。这并不令人惊讶,因为背后的机制是相同的——众所周知,这是一个重大事件,没有人愿意承担过度的风险,流动性提供者从账簿中撤出流动性,任何新的订单,即使规模很小,也可能瞬间将价格推至任何位置。

政治事件与常规经济事件之间的区别可能在于事件发生后价格波动的持续时间。让我们考虑几个例子。

美国总统选举,2016 年 11 月 8 日

在这一天,唐纳德·特朗普当选美国总统。他的胜利并不顺利:他是唯一一个在普选中失利但仍当选的第五位总统。因此,如果我们查看那天 EURUSD 的图表,我们可以清楚地看到市场情绪是如何从乐观(认为特朗普不会赢)转变为悲观(当开始变得明显他的胜率高于预期时)。图 6.5 以一分钟图表的形式讲述了这个史诗般的故事:

图 6.5 – 唐纳德·特朗普选举当天 EURUSD 的一分钟图表

图 6.5 – 唐纳德·特朗普选举当天 EURUSD 的一分钟图表

我们可以看到比我们在常规经济新闻发布期间观察到的价格波动更加稳定。首先,价格在 3-4 小时内上涨了近 300 点,这对于这个市场来说是非常大的,这种波动是由希拉里·克林顿将赢得比赛的预期所推动的。但随后,大约午夜时分,情况变得不那么明确了,当美国有线电视新闻网在凌晨 2:30(图表上标记为垂直红色线条)正式宣布特朗普获胜后,美元价格急剧上升(别忘了我们是在看欧元兑美元的汇率:美元越高,汇率越低——因此,当我们说“美元上涨”时,从视觉上看,EURUSD 的图表就会下降)。

英国脱欧,2016 年 6 月 23 日

2016 年充满了前所未有的政治事件,而英国脱欧无疑是其中之一。对于这次公投的可能结果没有明确的共识,但所有分析师都同意,如果英国离开欧盟,其国内货币(英镑,GBP)的价值将急剧下降。这正是发生的事情,我们可以通过查看那天 GBPUSD 的一分钟图表来回顾这一场景:

图 6.6 – 英国脱欧当天 GBPUSD 的一分钟图表

图 6.6 – 英国脱欧当天 GBPUSD 的一分钟图表

你可以看到,官方民调结果的公布导致 GBPUSD 汇率在 2 分钟内下跌了 600 点。这是整个外汇市场历史上主要货币对中最快的波动之一。这次急剧下跌在随后的 20-30 分钟内几乎完全被回补,再次,这并不令人惊讶,因为流动性正在回归市场。然而,随后的下跌价格运动持续了不止几分钟,甚至不止几小时,而是持续了数日。因此,至少从理论上讲,英国脱欧再次提供了一个投机交易的机会——在脱欧日卖出 GBPUSD,然后在几小时或甚至几天后平仓。

我们可以看到,重大政治事件与常规经济指标发布之间的主要区别是,前者的价格波动持续时间更长。因此,这些事件可能是基本面分析真正起作用并帮助我们获利的最理想时机。

现在我们已经考虑了所谓的“所有市场”的新闻,那么让我们快速了解一下仅影响某个市场,有时甚至某个特定资产的基本面因素。

行业特定信息

通常,这类基本面信息几乎不会影响货币汇率,因为其影响范围太窄。你可以将货币视为最大的市场指数,它包括一个特定国家的所有行业和经济的各个方面,并将它们与另一个国家的货币对进行比较。大多数发达国家都竭尽全力保持其经济的平衡和多元化。因此,如果只发生某些事情,比如,仅在微电子、汽车制造、农业或医疗保健领域,嗯,是的,它可能只会对汇率产生非常有限的影响,但这种影响将非常小,几乎可以忽略不计,因此,货币交易者通常不会考虑这类基本面信息。

尽管有一些明显的例外。有些国家的经济与仅一个或两个行业紧密相连。对于这些国家,特定于关键行业的基本面因素确实会影响整个经济。让我们考虑一个最突出的例子,看看行业特定新闻和单一商品的价格如何可能与国内货币汇率高度相关。

原油与加拿大元

加拿大经济与石油紧密相连是众所周知的。加拿大元兑美元汇率与原油价格表现出强烈的关联性并不足为奇。图 6**.7清楚地展示了这种关联。

注意

为了做到“货比三家”,我们需要在同一张图表中用相同的货币来报价这两个工具。然而,在下面的图表中并非如此。加拿大元的汇率以 USDCAD 表示,这意味着一加元中有多少美元。因此,顶部的图表显示的不是以美元计价的加拿大元,而是以加元计价的美元。而底部的图表显示了美原油的价格。因此,我们应该(在心理上)反转顶部的图表,以便两个工具(加元和美原油)都使用相同的货币(美元)报价。

图 6.7 – 加拿大元和油价显示出强烈的关联

图 6.7 – 加拿大元和油价显示出强烈的关联

在这个图表中,我们可以看到从 2018 年中到大约 2021 年底的加拿大元和美原油的每日图表。我们可以看到,底部的图表非常接近顶部图表的镜像版本,如果我们记住 USDCAD 的汇率是镜像到 CADUSD 的话,这显示了非常高的正相关。

此外,即使我们放大并查看日内数据,我们仍然会看到加拿大元和美原油之间强烈的关联。图 6.8展示了在 1 小时图表中这种关联的一个例子(别忘了,我们应该反转 USDCAD 的图表,以便比较两种价格都以美元计价)。

图 6.8 – 美元加元和美原油的日内(每小时)图表

图 6.8 – 美元加元和美原油的日内(每小时)图表

因此,我们可以假设来自石油行业的基本面因素可能影响 USDCAD,并且可能被用于交易这一货币对。

摘要

我们可以看到,基本面因素确实会影响货币的价格,但我们也可以看到使用这些因素进行自动化交易的关键问题,例如:(a)宏观经济新闻的发布大多导致价格运动的不可预测方向,这些运动通常不足以进行交易,并且持续时间不长;(b)政治事件导致价格运动时间较长,并且可能可以进行交易,但它们很少发生,手动交易它们比程序化交易更容易;(c)使用行业特定的基本面因素可能是最有希望的,但需要对相关行业进行彻底分析,并且仅适用于特定货币。

在任何情况下,系统交易者(那些基于一套规则而不是直觉或情绪来进入和退出位置的交易者)长期以来一直在寻找一种替代的、定量分析市场数据的方法,而不是定性基本面分析。这种定量分析可以开辟许多新的机会,因为它消除了人为的判断、情绪和任何偏见,因为所有交易决策都仅基于数字。这种定量分析被称为技术分析。我们将在下一章中了解更多关于这种分析类型的内容。

第七章:技术分析及其在 Python 中的实现

在上一章中,我们考虑了基本因素,并看到了它们如何可能影响市场价格。我们指出,尽管这种影响可能非常显著,并且可能对交易非常有利可图,但大多数时候,很难提出一个能够生成明确的交易规则(何时进入市场、朝哪个方向以及何时退出)的定量模型,而这些规则不需要人为判断。为了清楚起见,请注意,存在各种完全定量的方法来评估基本因素,甚至包括政治因素,但它们基于复杂的跨学科主题,如语义分析,因此需要对这些科学有扎实的知识。是否有可能避免这种复杂性,并找到一种仅使用价格数据来分析市场行为的方法?或者,也许,一些额外的数据,但仅以数字形式?

答案是肯定的,这种市场分析被称为技术分析。在本章中,我们将考虑其前提,了解最常见的技术指标,并发现它们如何被用来定量描述市场上发生的过程。我们还将考虑关键技术指标在 Python 中的实现,并介绍滑动窗口的概念,该概念将在所有未来的代码中使用。

在本章中,你将学习以下主题:

  • 技术分析——适合计算,但缺少真实市场过程

  • 动量和 RSI 作为衡量市场速度的指标

  • 数字滤波器和移动平均线

  • 范围指示器

  • 波动性指标

  • 在 Python 中实现技术指标

技术分析——适合计算,但缺少真实市场过程

技术分析(或简称TA)的基础主要思想是价格包含了一切。从这个角度来看,如果我们看到价格上升或下降,大或小,我们并不真正想知道这种运动的背后原因;相反,我们只是承认它是由于某些基本因素造成的,并试图仅关注观察到的价格运动相关的未来价格走势。

当然,技术分析可以分析一系列数据,而不仅仅是单个数据点。在这方面,技术分析研究有助于识别价格时间序列中的模式或重复的序列,它们彼此相似。技术分析建议,如果我们观察到过去已经看到的模式,那么接下来的价格走势也将类似于过去发生的情况;因此,我们可以做好准备并利用它。

使用技术分析(TA),我们不想也不需要知道影响价格的实际基本因素,我们只是在寻找幕后发生的事情的痕迹,然后根据这些痕迹的形式采取行动。

现在我们已经了解了基本面分析和技术分析基础上的差异,让我们看看这种差异如何影响任何市场分析的两大主要实践点:其时间范围和精度。

基本面分析侧重于宏观经济因素,这些因素在本质上总是长期性的,因此基本面预测的时间范围通常是几天以上,直至数年。相反,技术研究可能分析极其短暂的过程(例如,订单簿中的即时不平衡),因此,技术研究的预测范围可能为 1 秒、1 毫秒,有时甚至几微秒,这是正常的。

基本面分析和技术分析之间的主要区别总结在下表中:

基本面分析 技术分析
关键关注点 宏观经济、政治、行业新闻和投资者的情绪 价格、成交量、开盘兴趣、价差、流动性和其他定量参数
预测范围 天到年 微秒到天,很少见的是周和月
预测精度 非常波动 取决于时间框架

表 7.1 – 基本面分析和技术分析的关键区别

任何技术分析研究的基石是一个指标。它是由价格、时间、成交量以及/或任何其他可以定量测量的市场数据组合而成的。

指标通常以线条、点、直方图和其他图形对象的形式绘制在价格图表上,位于价格图表下方,或者两者都有。通常,技术分析师使用两个到五个指标,每个指标都显示所考察的价格时间序列的特定特征。图 7.1显示了这种技术分析指标组合在单个图表中的典型例子:

图 7.1 – 技术分析指标与价格图表的典型布局

图 7.1 – 技术分析指标与价格图表的典型布局

过度使用技术分析指标可能导致混乱,真的很难理解每个指标显示的是什么,以及这个混乱的作者真正想要实现什么。你可能无法相信,但下图中所示的例子不是我的幻想,而是我在各种交易论坛上亲眼所见多次的类似情况!

图 7.2 – 过度使用技术分析指标可能导致滥用

图 7.2 – 过度使用技术分析指标可能导致滥用

所有技术分析指标都有一个共同点 – 它们的值总是与它们所基于的原价时间序列保持同步。

这意味着如果我们基于 1 分钟图表构建一个指标,那么这个指标的价值将每分钟更新一次;对于小时图表,值将每小时更新一次,依此类推。

如您所见,如果我们使用 tick 图表作为计算技术指标值的源数据,那么这些值将在每个 tick 上更新。当我们处理实时数据流时,技术指标对于所有历史柱显示固定、未更改的值(嗯,我们无法改变历史,对吧?),但对于尚未完成的柱,会随着每个新 tick 的到来更新最新的读数。

不论它们的复杂性如何,几乎所有技术指标都可以分为四大主要类别:市场速度、数字滤波器、范围指标和波动性指标。让我们逐一考虑它们。

在我们继续之前,请注意一个重要事项

在所有以下关于技术指标讨论中,我们将使用术语当前柱。这并不意味着我们只谈论图表上最后的(最右侧)柱。这意味着当我们绘制图表、指标、回测策略等时,我们总是从左到右移动(想象中),从历史较老的数据到历史当前的数据,并且我们在遇到的每一根柱上计算任何东西。这根柱,对于我们在此时计算任何值的,被称为当前柱。我们使用这个术语是因为当我们将我们的开发上线时,当前柱实际上将意味着我们此刻收到的价格数据,所以我们不需要在我们的研究或策略代码中做任何修改。

在这个重要事项的提示下,让我们继续前进。

市场速度

这些指标试图回答问题,“价格移动有多快?”。确实,如果我们比较外汇市场的一个正常平均日与重要经济新闻发布或政治事件,那么我们可以清楚地看到差异。当然,能够定量评估这种差异会更好。我们将仅考虑这类指标中最著名的两个——动量和 RSI——但任何更复杂的市场速度指标都将不可避免地建立在相同的原则之上。

动量

这可能是最古老的技术指标,并且绝对是最简单的。一个简单的公式真的很难想象:

公式 B19145_07_001

在这里,C0 表示当前收盘价,而C-1 表示前一根柱的收盘价。

如果我们为每一根柱计算这些差异,将它们存储在一个数组中,并在图表下方绘制指标,我们会看到其值不再跟随价格变动,整体上看起来更像噪声,而不是一个清晰的趋势价格模式。然而,这种噪声非常有信息量。即使快速浏览图 7.3,我们也可以得出几个重要结论:

图 7.3 – 基本动量绘制在价格图表下方

图 7.3 – 基本动量绘制在价格图表下方

首先,我们可以看到动量的读数很少连续超过两个或三个数据点。由于动量的值是两个相邻柱子的当前和上一个收盘价之间的差异,我们可以说,在这个市场中,价格连续增长或下降超过三根柱子是不寻常的。换句话说,如果我们看到超过两根柱子收盘价上涨,我们可能会预期一根柱子收盘价下跌,而不是另一根柱子收盘价上涨。技术分析师将这些柱子称为纠正;它们甚至可能出现在相当长且稳定的趋势中,如图 7.3所示,但它们不会打破趋势。因此,总的来说,我们可以说这个市场真的很容易发生纠正。

其次,如果我们估计动量指标的范围,我们可以看到它在不同的市场阶段是不同的。当市场保持或多或少平稳(图表的开始部分)时,动量的幅度几乎没有超过 0.002 点,但随着上升趋势的发展,其绝对值增加到 0.004 – 也就是说,是闲置市场的两倍。因此,我们可以假设市场速度与市场阶段有一定的相关性,并且我们可以在自己的研究中潜在地使用它。

通常,动量指标有一个参数 – 我们计算差异的柱子数。在本节开头提到的经典公式中,我们比较了两个相邻柱子的收盘价;当然,我们可以比较当前柱子的价格与过去任何柱子的价格。如果我们继续对每一根柱子这样做,我们将得到一个修改后的公式:

在这里,n表示从当前柱子往回数的柱子数。

例如,如果我们把n设为 24,那么我们计算当前柱子和 24 小时前的收盘价之间的差异(对于一个小时图来说,这意味着严格 24 小时或 1 天前的价格),我们将看到一幅不同的画面,如图图 7.4所示:

图 7.4 – 基于小时图的 24 小时动量表示 24 小时的价格变动率

图 7.4 – 基于小时图的 24 小时动量表示 24 小时的价格变动率

我们可以看到,市场速度,或者说 24 小时的价格变动率,与前一张图表中我们考虑的两个相邻柱子的收盘价差异完全不同。这里不再有噪声;相反,我们观察到动量值的长周期变化。但之前我们做出的观察(关于市场速度与范围/趋势市场之间的纠正和相关性)仍然有效,因为这些现象的规模和比例只是增加了。

因此,总结一下,动量指标对于确定市场闲置阶段(当其读数接近零时)和高度活跃的市场(当其读数超过某个阈值时)是有用的。然而,我希望你能看到这个指标在当前形式下的明显不足——要决定当前市场是否活跃,我们必须指定我们刚才提到的这个特定阈值

这个阈值是什么?在第一个例子(图 7**.3)中,动量值从未超过 0.006,而在第二个例子(图 7**.4)中,它达到了几乎 0.02,这是一个数量级的差距。因此,如果我们说市场速度超过 0.004 是异常大的,那么我们必须同意在第二个例子中,市场有一半的时间处于异常大模式。

对于经典动量指标,没有这样一个作为单一唯一数值的阈值。它取决于市场、数据分辨率和动量周期。如果能自动调整阈值值,而不需要每次都手动进行,那就太好了,从而消除可能的人为判断偏差。

那么,我们如何在设置这样的阈值时消除主观性呢?

RSI

在上一节讨论的关键问题“是否足够大或仍然很小”的问题,在 1978 年由J. Welles Wilder Jr.为动量指标解决了。在他的著作《新技术交易系统概念》中,他介绍了相对强弱指数RSI),这是一种新的技术分析指标,他建议使用它来确定市场的超买超卖状态。在他的术语中,超买对应于价格增长过快的情况,基本上意味着我们之前讨论的市场速度太高。超卖区域对应于价格下跌过快的情况,意味着动量在绝对值上仍然大于平均水平,但带有负号。

RSI 通过归一化其值来解决为动量指标指定一个通用阈值的难题。归一化是一个将数据缩放以便所有值都适合一定范围的过程。例如,我们有两个数据集:

{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
{0, 0.1, 0.4, 0.05, 0.1, 0.3, 0.2}

我们希望能够比较苹果与苹果,因此我们希望将它们都缩放,使得每个数据集的最小值和最大值相同。通常,使用 0 到 100 的范围(这很方便:如果我们将数据归一化到这个范围,我们就可以将值视为百分比)。因此,如果我们将第一个数据集缩放到0:100的范围,我们将得到一个明显的序列:

{0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100}

但当我们缩放第二个数据集时,0仍然是0,最大值,即0.4,变成了100,因此缩放系数是100/0.4 = 250。然后,归一化后的数据集将如下所示:

{0, 25, 100, 12.5, 25, 75, 50}

现在我们可以比较,例如,两个数据集数据点的变化率,尽管最初第二个数据集包含的值比第一个数据集小一个数量级

因此,技术分析指标标准化的想法是自动缩放它们,使它们的值始终保持在0:100范围内,有时是-100:100。让我们为动量做这件事。

威尔·威尔德建议分两步计算 RSI:首先,我们计算相对强度RS),然后计算 RSI。

注意

不要忘记,与其他任何技术分析指标一样,RSI 使用周期,这是一个确定我们考虑的数据点数量的参数。在前一节中动量指标的第一个例子中,周期是 1,而在第二个例子中,它是 24。

要计算 RS,我们首先应该计算该期间的盈利亏损。在威尔德的说法中,盈利发生在柱状图收盘价上涨时,亏损发生在柱状图收盘价下跌时。然后,我们分别计算盈利和亏损的平均值。最后,我们计算该期间的相对强度:

公式 B19145_07_003

这个公式与原始动量类似,只是动量衡量的是价格的差异,而 RS 衡量的是比率。当我们想要摆脱原始值域的依赖时,使用比率总是更受欢迎,例如从 0.1 中减去 0.01 将得到 0.09,而从 100 中减去 10 将得到 90,很明显 90 和 0.09 相差4 个数量级,因此无法直接比较。然而,将 0.01 除以 0.1 和将 10 除以 100 会产生完全相同的结果,即 0.1,因此从市场速度的角度来看,这个值确实是相同的。

现在,我们通过保持 RS 始终在 0 到 100 的范围内来标准化 RS:

公式 B19145_07_004

如果我们现在将 RSI 与动量在同一张图表上绘制,我们可以看到这两个指标非常相似:

图 7.5 – 动量(顶部)和 RSI(底部)在同一价格图表上绘制

图 7.5 – 动量(顶部)和 RSI(底部)在同一价格图表上绘制

由于 RSI 被标准化到 0 到 100 的范围内,通常,高于 70 的值被认为是超买的市场,而低于 30 的值则被认为是超卖。在前面的图表中,超买的市场对应上涨趋势,而超卖状态的不存在可以解释为市场情绪强烈的看涨倾向。

市场速度指标比动量和 RSI 更多,但它们都用于确定以下市场状态:

  • 悠闲与活跃市场(价格移动缓慢或快速)

  • 超买/超卖区域(价格过高过低

  • 由于流动性问题导致的剧烈价格波动(动量图上的尖峰

因此,动量对于检测相对短暂的市场情况是好的。但关于长期的东西呢?有没有指标可以显示给我们更全局的情绪,长期趋势?让我们继续探讨另一类通常用于此目的的技术分析指标。

数字滤波器

如果你听过任何类型的音乐,我希望你确实听过,那么你很可能熟悉高通滤波器、低通滤波器和均衡器。任何汽车音响、桌面或移动音频播放器都提供这种功能。我确实希望,如果你在一生中至少调整过一次音频设置,你会记得如果你关闭高音并将低音提升到最大——那就是,你现在听到的不再是音乐,而是只有砰砰砰的声音。

然而,如果你想要专注于节奏、低音线,也许还有和弦和基音(而不是泛音)的话,截断更高频率的数据也可以非常有用。这正是许多技术分析师梦寐以求的——从市场价格中剥离所有噪声,只留下低音线节奏——也就是说,关键趋势和持续时间相对较长的主要价格变动,这可能会带来利润。

惊讶或不惊讶的是,所有执行这项工作的技术分析指标都和音频中的低通或高通滤波器完全相同,因为它们基于完全相同的数学原理。这就是为什么我们将它们都放在一个部分:数字滤波器。

移动平均线

移动平均线也是最早的技术指标之一。它是通过计算周期内的价格平均值来计算的,并且在每个柱上重新计算。

注意

当我们讨论技术分析指标时,我们总是使用两个术语:周期当前柱。所以,让我快速提醒你一下,一个周期指的是技术分析指标分析的数据点(tick、柱、任何其他时间序列样本)的数量,而当前柱会一个接一个地变化,从左到右,随着我们重建指标的历史值以及市场价格本身的历史值。

为了给你一个简单的例子,让我们考虑以下数据集:

A = {0, 1, 2, 3, 2, 1, 0, 1, 2, 3}

让我们从最左边的一个数据点开始计算三个数据点的平均值。对于原始数据集中的第一个元素,这是不可能的,因为我们只有一个数据点,而我们需要三个。第二个数据点也是如此。因此,我们可以从第三个数据点开始计算移动平均线,其值如下:

然后,我们移动到第四个数据点,并再次计算移动平均线的值,但这次,我们从我们的数据集中的第二个元素开始计算:

按照这种方式进行,我们将得到一个新的数据集,它表示连续三个数据点的平均值,并从左到右对每个数据点重新计算。因此,对于前两个数据点,我们没有任何值(因为原始数据点的数量不足以计算三个点的平均值):

= {NaN, NaN, 1, 2, 2.33, 2, 1, 0.66, 1, 2}

注意

我希望你们已经明白了:我们总是从数据集中取三个样本,计算平均值,将这个值存储在新数据集中,然后继续下一个样本。这种技术被称为移动窗口,是所有技术分析指标计算的基础。

通常,移动平均线(MAs)是基于柱状图的收盘价计算的,但没有任何东西阻止我们使用其他价格(如高、低或交易)或非价格数据(如成交量、流动性等)。

显然,周期为 1 的移动平均线等于原始时间序列。那么,周期更长的移动平均线的值将如何对应原始数据?

让我们以点而不是柱状图的形式绘制原始价格时间序列,其中每个点将表示柱状的收盘价。然后让我们绘制 3 个不同的移动平均线,分别以 2、3、5 和 10 个周期与原始序列一起绘制:

图 7.6 – 在 EURUSD 收盘价 1 小时图表上绘制的 3、7 和 13 周期移动平均线

图 7.6 – 在 EURUSD 收盘价 1 小时图表上绘制的 3、7 和 13 周期移动平均线

图 7.6 以柱状图形式绘制原始时间序列,以及 3、7 和 13 周期的移动平均线分别以虚线、交叉线和实线表示。我们可以看到,移动平均线的周期越小,其值越接近原始序列,周期越大,移动平均线值的变动就越平滑

这种平滑正是低通滤波器的效果。令人惊讶的是,使用简单数学计算的移动平均线确实是一个数字滤波器,与音频处理中使用的那些相当接近。移动平均线的周期越长,通过该滤波器的最大频率就越低。因此,如果应用于音频信号,这样的滤波器将只留下砰砰声,现在我们可以在市场价格中看到这种砰砰声。最终,使用 20、50 或 200 周期的移动平均线将只显示市场价格的长周期变化,将较小的价格变动视为高频噪声。

图 7.7 展示了 EURUSD 的相同小时图表,只有收盘价,以大黑点表示,并在其上方绘制了 20、50 和 200 周期的移动平均线:

图 7.7 – 在 EURUSD 收盘价 1 小时图表上绘制的 20、50 和 200 周期移动平均线

图 7.7 – 在 EURUSD 收盘价 1 小时图表上绘制的 20、50 和 200 周期移动平均线

在这个例子中,我们可以看到,周期最长的移动平均线(200)只随着增长率的一点点变化而增长,而周期较短的移动平均线则增长和下降,移动平均线的周期越小,其值就越接近原始序列。

因此,移动平均线是最简单的数字滤波器,旨在检测趋势而不是即时活动。它们在技术分析(TA)中用于解决以下问题:

  • 为了确定长期和中期趋势

  • 为了区分牛市和熊市(通常,当每日价格收盘价高于 200 周期移动平均线时,市场被认为是牛市,反之亦然)

  • 为了平滑偶尔或异常剧烈的价格波动

非常常见的是,移动平均线(MAs)与动量指标结合使用 – 例如,移动平均线可以用来判断市场是否看涨(收盘价高于 200 周期移动平均线),而动量可以确定市场速度高的时刻,因此,这可能是一个买入的好时机。

然而,你可能已经听说过那些在价格已经过高时买入或价格已经过低时卖出而损失财富的交易者。我希望现在我们已经彻底研究了市场速度指标和数字滤波器的设计,你已经发现了这里的瓶颈 – 过高或过低。但我们是怎样决定它已经过高或仍然不算高呢?答案将在下一节中揭晓。

范围指标

解决识别当前价格是否处于任何极端区域(过高或过低)的最常见方法之一是使用范围指标。通常,价格范围是最高价和最低价之间的差异,嗯(我确信你说过),在一个周期内。像往常一样,与技术指标一样,我们被限制在某个时间段内,一个沿着图表从左到右滑动的移动窗口。因此,当我们谈论范围时,我们必须定义这个范围是针对哪个周期计算的。

让我们看看图 7.8中显示的图表。矩形显示了 24 个周期内的最高价和最低价(由于这是一个小时图表,这意味着矩形对应 1 天):

图 7.8 – 24 小时价格范围,显示为范围百分比的 2 个收盘价,EURUSD,1 小时

图 7.8 – 24 小时价格范围,显示为范围百分比的 2 个收盘价,EURUSD,1 小时

两个收盘价用小箭头标记。通过这些收盘价画水平线,以可视化价格水平相对于范围而不是绝对值。因此,对于第一个(从左到右)收盘价,绝对值是 1.1260,但相对于范围,它正好是 50%。第二个收盘价是 1.1292,但相对于范围,它大约是 85%。

明白了?我们可以用相对价格水平来代替没有太多信息的绝对价格值,这些水平可以解释得和解释 RSI 值(见RSI部分)差不多。例如,我们可以说,当价格高于范围水平的 80%时,那么价格就是过高的,而当价格低于范围水平的 20%时,那么价格就是过低的。

最著名且广为人知的范围指标是 随机振荡器

随机振荡器

随机在交易者中是一个非常流行的词,但不幸的是,它经常被错误地使用。在数学中,随机 这个术语意味着各种随机过程。为了理解它的含义,让我们考虑一个例子。

如果你出去买一些面包,那么你很可能会走相同的街道,在同一个当地杂货店结束你的路线,并且花费的时间与平时差不多。这是一个 确定性过程 的例子——尽管在路上可能会有一些 波动 或偏差(例如,你决定走这条或那条街道),但整体路线和目的地保持不变。

现在,想象一下,你出去时没有特定的目标,在任何你喜欢的商店、酒吧或电影院停下来,在每个地方花费任意时间,并通过抛硬币来决定下一步去哪里。每次你进行这样的旅行,其轨迹、目的地以及在每个阶段和整个旅程中所花费的时间都会不同。尽管你旅行的区域是有限的,并且你访问了相同的地方,但由于你决策的 随机性,你实际移动的地图将会不同。这样的过程被称为 随机随机概率

许多研究人员也将市场价格变动视为随机或随机过程,他们提出了各种概率模型来描述价格行为。尽管这种观点也可以受到批评,但这种学术辩论显然超出了本书的范围,所以我们应该从这次简短的 抒情插叙 中吸取的主要观点如下:

  • 具有明确轨迹、目标和时间的过程被称为 确定性

  • 每下一步都有一定概率,并且你永远不知道它何时何地达到任何目标的过程被称为 随机

  • 市场价格的变化可以被视为一个 随机过程

  • 随机振荡器与 随机过程 没有任何共同之处。

随机振荡器在其原始形式中恰好显示了我们在上一节最后所考虑的内容——它显示了当前价格作为某个价格范围的百分比。因此,为了计算随机振荡器,我们应该选择一个周期(数据点数量),然后找到这个周期内的最高价和最低价,然后计算随机振荡器的值:

公式

在这里,P0 表示当前价格,L 表示该期间内的最低价格,而 H 表示该期间内的最高价格。

让我们再次打开一个货币对的每小时图表,但这次,为了改变一下,我们来看日元,并在其下方绘制这个指标,周期为 24(因此跟踪价格作为一天范围内的百分比变化):

图 7.9 – 美元/日元 1 小时图表下的基本 24 周期随机振荡器

图 7.9 – 美元/日元 1 小时图表下的基本 24 周期随机振荡器

图 7.9中,我们可以看到随机振荡器确实在 0 到 100 的范围内振荡(真是个惊喜)。在趋势期间(图片的右半部分),它倾向于保持在 50 以上,而在横向市场中(图片的中间部分)它可以全范围振荡。

那么,随机振荡器可能的用途是什么?

首先,它被用来回答价格是否过高过低的问题(我希望你能记住上一节末尾的这个问题)。例如,我们可以这样说,当价格高于 80 的水平时——这意味着价格高于价格范围的 80%——那么它确实过高。同样,低于 20%的范围可能意味着价格过低

其次,一些技术交易者喜欢使用随机振荡器及其类似振荡器来确定趋势。我们已经看到,在趋势期间,随机振荡器往往会保持在(对于上升趋势)或低于(对于下降趋势)范围 50%以上相当长的时间。因此,趋势是特殊的市场状态,其中价格修正幅度较小,持续时间比趋势方向的移动时间短得多。所以,价格确实会在范围的上下半部分保持许多小时、几天,有时甚至几周。

随机振荡器有许多修改版本。例如,将其值添加到移动平均线(我们记得我们可以将移动平均线添加到任何时间序列,而不仅仅是价格)给我们一个所谓的慢速随机振荡器。一些作者建议在同一图表上使用原始指标和平均指标,但本质仍然是相同的——这是一个范围振荡器,因此可以用来解决以下问题:

  • 识别价格是否过高过低

  • 当价格高于或低于 50%的范围时,识别趋势作为长期时期

  • 当振荡器从过低水平开始上升时,提出买入信号

  • 当振荡器从过高水平开始下降时,提出卖出信号

现在我们有一个速度指标(动量或 RSI),它告诉我们市场正在快速移动,一个数字滤波器(移动平均线)确认市场的主要趋势仍然是积极的,以及一个范围指标(随机振荡器)显示资产目前超卖,看起来现在是买入的好时机。

但是!

即使我们将进入市场的时机计算得非常完美,市场仍然保持随机过程(记得本节开头我们提到的吗?)并且价格可能一段时间内对你不利是正常的。这里的关键问题是这种不利的价格变动是否只是一个小幅修正,并且明智地等待直到红色变为绿色,或者你的交易决策是错误的(顺便说一句,这也是绝对正常的),你应该在账户被完全耗尽之前平仓亏损头寸。

通常,这个问题是通过将波动性研究添加到交易策略逻辑中解决的。

波动性指标

如果我们查阅 Merriam-Webster 对波动性的定义,第一个建议的含义将是快速且不可预测地变化的倾向。听起来不错,但我们如何衡量这种倾向,这种变化的能力?

维基百科(en.wikipedia.org/wiki/Volatility_(finance))提出了不同的定义:“波动性(通常用σ表示)是交易价格序列随时间变化的程度,通常通过对数收益的标准差来衡量。” 如果你对数学统计学不熟悉,这可能会听起来像一门外语,但别担心,让我们快速浏览一下概率论的理论。

让我们用同样的例子,在酒吧和电影院周围进行随机漫步。让我们测量起点(你的家,假设)和终点(你决定当天结束散步的地方)之间的距离。每天,你都会得到不同的值,因为你随机做出旅行决定。然而,我们可以计算平均值,并说平均每次散步你走了大约三公里。

恶魔总是在细节中,平均的恶魔被称为分散度。这是衡量某个随机变量的实际值与平均值或均值差异的程度。让我们考虑两个例子。

首先,让我们回顾一下当你去当地杂货店时的确定性过程。我们记录每次旅行的距离,并将它们放入数据集中:

S = {1.8, 1.9, 1.85, 1.79, 1.78, 1.81, 1.85, 1.82, 1.89, 1.2}

现在,让我们计算这个数据集的平均值:

公式

现在,从这个原始数据集的每个元素中减去这个平均值:

D = {0.03, 0.13, 0.08, 0.02, 0.01, 0.04, 0.08, 0.05, 0.12, -0.57}

我们可以看到,几乎所有记录的值与它们的平均值之间的差异都比平均值本身小一个数量级。唯一的例外是D数据集中的最后几个值:它与平均值的差异与它本身的值相当。这样的值被称为异常值,最可能是由错误的测量或你在路上遇到的某些异常情况所解释。

现在,让我们记录你每次在本地酒吧和电影院周围随机漫步时走过的距离。在某些情况下,你几乎在你离开家的时候就能找到一个不错的住处;在其他情况下,你走了相当长的一段路,但仍然不满意地回到家,因此你每次走过的距离有所不同。我们将得到一个类似这样的数据集:

S1 = {0.7, 2, 1.5, 0.3, 2.6, 1.1, 1.8, 0.45, 3.1, 2.9}

它的平均值是 1.645。如果我们现在像之前一样做(即从数据集的每个元素中减去这个平均值),我们会看到差异现在与平均值相当,在某些情况下,几乎超过了它:

D = {-0.94, 0.35, -0.14, -1.34, 0.95, -0.54, 0.15, -1.19, 1.45, 1.25}

当值与其平均值之间的差异远小于值本身时,这个过程被称为确定性过程,现在我们有一个更数学化的定义(尽管它仍然不是正式正确的)。相反,当值与其平均值之间的差异与平均值本身相当的过程是一个随机随机过程。

那么,回到市场相关的话题。如果我们对价格做同样的数学运算(例如,记录每个柱状图的价格变化,然后计算它们的平均值和差异),那么我们可以使用这个差异作为波动性的衡量标准。这确实符合 Merriam-Webster 的定义——所讨论的差异越大,值就越不可预测,价格的可能变化就越快。

在实践中,为了确定市场波动性,通常会使用稍微复杂一些的计算。用于估计“数据集的值与其平均值之间差异程度”的常用指标被称为标准差。如果您想了解更多关于它以及波动性概念背后的数学知识,我鼓励您首先阅读有关数学统计学的基础知识,以便熟悉术语和关键概念。维基百科上的文章(en.wikipedia.org/wiki/Mathematical_statistics)可以作为一个良好的起点。同时,我们以一种较为非正式的方式继续讨论,并记住标准差被用来估计任何随机过程以及特别是市场价格的波动性。

我相信你现在完全理解了标准差(或简称为stddev)也需要一个周期,就像其他任何技术分析指标一样。这个周期是我们用来测量波动性的数据集长度。因此,我们绘制的如下:

S = stddev(close, 24)

这可以用以下形式的方程表示:

公式图片

这里,ci 表示第 i 个柱状图的收盘价, 表示平均值,24 表示我们计算stddev的数据点(柱状图的收盘价)数量。

图 7.10显示了与上一个例子中相同的 GBPUSD 1 小时图表,但现在在下面添加了 24 周期标准差指标:

图 7.10 – GBPUSD 的每小时图表,显示收盘价的标准差

图 7.10 – GBPUSD 的每小时图表,显示收盘价的标准差

我们可以看到,波动性与市场价格的走势或趋势/横向市场制度之间没有明显的关联。尽管如此,我们可以观察到一些重要的观察结果:

  • 标准差始终为正;它不考虑价格变动的方向

  • 市场活动突然增加确实会增加收盘价标准差值

  • 价格在新水平上停留的时间越长(参见 7 月 27 日的价格跳跃以及次日的拥堵区域),标准差值增加得就越大

  • stddev指标中的最大值和最小值与价格图中的最大值和最小值不对应

很有趣的画面,但这里有一个值得怀疑的假设,我希望你已经喊出了,“等等!但我们测量的是错误数据集的标准差!为什么我们应该测量价格本身的标准差,而不是价格变动**的标准差呢?”。

事实上,从多个角度来看,市场过程可以被视为一个随机过程。我们可以将每个收盘价视为随机过程的独立值,或者我们可以将价格变动视为随机游走过程,我们只对价格自上次观察(柱)以来变化了多少感兴趣。因此,我们与其计算收盘价的标准差,不如计算每个柱的价格变动的标准差:

这里,Ci 代表当前柱,Ci-1 代表前一个柱,而 delta 符号代表每根柱的价格变动(正或负——这是与计算价格标准差的关键区别,因为它们只能为正)。

让我们看看收盘价的标准差和收盘价差异的标准差之间是否有任何区别。图 7.11仍然显示了 GBPUSD 的相同图表,其中两个标准差都绘制在其下方:

图 7.11 – 基于标准差波动性的两种版本指标

图 7.11 – 基于标准差波动性的两种版本指标

差异是微不足道的!波动性不再有平滑的变化。相反,我们观察到高波动性和低波动性突然从一个切换到另一个。现在这幅图更符合我们在市场上观察到的实际情况:

  • 急剧的价格变动总是立即导致我们的波动性指标跳跃

  • 高波动性通常意味着价格的反转

  • 长期趋势(图表的右侧)伴随着低波动性(惊讶!)

突然的价格变动会导致波动率急剧增加的事实被用于波动率突破策略;其理念是在已经开始的急剧价格变动方向上买入或卖出资产。而波动率在趋势期间相对较低的事实被用于各种趋势跟踪均值回归策略。

没有包括最著名且最受欢迎的技术分析指标之一,布林带,我们的波动率研究肯定是不完整的。

布林带

该指标由约翰·布林格在 1980 年代提出。其理念是确定资产价格是否位于正常范围之外。在这个指标中,正常范围被定义为平均值加减两个标准差(或两个sigma)。因此,布林带由两条线组成,一条始终位于价格之上(B+),另一条始终位于价格之下(B-):

B+ = 公式 B19145_07_013

B- = 公式 B19145_07_014

为什么是两个stddev而不是一个?如果你想了解正确、正式的答案,我建议你参考任何一本关于数学统计学的书籍或之前提到的相同维基百科文章。不深入细节,我们只需假设对于随机过程,90%的数据点都落在平均值± 2 sigma 范围内。因此,布林带的理念是在距离平均值2 sigma 的位置画线可以帮助隔离异常值——也就是说,那些位于±2 stddev 范围之外的价格。

让我们看看现实中的样子。图 7.12 显示了一个带有 20 期布林带的 AUDUSD 1 分钟图表:

图 7.12 – 带有布林带的 AUDUSD 1 分钟图表

图 7.12 – 带有布林带的 AUDUSD 1 分钟图表

此图表表示平均值或移动平均(中间的实线)和两条布林带(上方的灰色线和下方的灰色线)。粗体黑点标记的是位于上轨之上或下轨之下的收盘价。这两者都是异常值

技术交易者像使用其他技术分析指标一样,使用布林带用于各种目的,尽管如此:

  • 异常值可以解释为突破——价格开始向某个方向移动的时刻——因此可以用于尝试买入或卖出伴随该移动的策略

  • 异常值可以解释为流动性问题,因此被认为是均值回归的好点——因此可以用作买入或卖出当前价格变动的入场点

关键要点 – 技术指标的全部内容及其使用方法

技术指标的世界确实非常广泛。然而,其中大部分只是四种主要类型经典指标的变化形式:

  • 动量,或市场速度

  • 数字滤波器

  • 范围

  • 波动率

当你遇到一个承诺神奇结果的独特新指标时,别忘了这一点。首先对其进行彻底检查,你就会看到它实际上显示的内容。

不要忘记,没有任何指标能为你构建一个稳健的交易策略。它们仅用于定量识别市场中的各种情况,而如何实际利用这些情况在你的代码逻辑中,则取决于你作为交易策略开发者。

现在,是时候看看我们如何在原生 Python 代码中实现技术指标了。

在 Python 中实现 TA 指标

我相信你记得任何 TA 指标都使用一个特定时期作为参数。这个时期意味着我们考虑的数据点的数量。为了在每个条形图上计算指标,我们从最老的开始(图表最左边的一个),然后逐个移动,每次添加新的条形图来更新我们的数据集。

由于我们正在讨论一个所有 TA 基础都绝对必要的事情,让我在这里非常详细地说明——可能过于详细——但我希望以下概念和代码示例中没有任何歧义或误解。

让我们从时间序列处理的核心概念开始:滑动窗口。

滑动窗口

让我们回到上一节中考虑的随机游走(围绕酒吧和电影)的例子。整个数据集,或历史数据,由 10 个数据点组成:

S1 = {0.7, 2, 1.5, 0.3, 2.6, 1.1, 1.8, 0.45, 3.1, 2.9}

现在,如果我们只想分析最近 3 天的活动,那么我们会得到以下子集:

S1_1 = {0.7, 2, 1.5}
S1_2 = {2, 1.5, 0.3}
...
S1_8 = {0.45, 3.1, 2.9}

然后,我们逐个将 TA 指标应用于每个子集,从左到右,从旧到新。

这种技术被称为滑动窗口,是所有技术交易的基础,从绘制简单的指标到回测和优化。

提前查看

在使用滑动窗口处理历史数据时,主要问题是潜在的提前查看能力,或看到未来(参见第四章交易应用——里面有什么?,在交易逻辑——这里的一个小错误可能代价巨大部分,关于提前查看问题的讨论)。如果我们从S1数据集(参见本章的随机振荡器部分)重建你的随机游走动作,我们应该特别注意不要在分析今天发生的事情时使用明天的数据。让我们给我们的数据样本添加日期以使其更清晰:

S1 =  {{01/01/2001,0.7}
      {01/02/2001, 2}
      {01/03/2001, 1.5}
      {01/04/2001, 0.3}
      {01/05/2001, 2.6}
      {01/06/2001, 1.1}
      {01/07/2001, 1.8}
      {01/08/2001, 0.45}
      {01/09/2001, 3.1}
      {01/10/2001, 2.9}}

如果我们重建 2001 年 1 月 6 日或之前发生的事情,我们只能考虑前期数据。我们不能,也不应该,没有权利使用 1 月 7 日或之后的数据。如果我们这样做,我们将面临这种提前查看的问题。

现在,想象一下你正在开发一个交易策略,并想使用过去的数据对其进行测试。这种测试的目标是重建在特定时间那天,如果您的策略当时确实有效,会发生什么。如果你重建了你在 1 月 6 日会采取的策略,你就没有权利提前查看并使用 1 月 7 日的数据,即使是在测试日期后的 1 秒钟,因为你没有时光机(或者至少我是这样假设的),当你实时运行你的策略时,你也将无法从未来检索数据。

然而,意外地,提前查看是一个相当常见的错误,这就是为什么我在这里对此问题进行了如此详细的说明。记住,如果你的策略产生了不切实际的优秀回报,那么它们很可能确实不是现实的,并且是由 提前查看 引起的

那么如何在不经意间偶尔提前查看呢?好吧,如果你将价格时间序列存储在列表或任何类似的可迭代结构中,并通过索引检索数据来进行计算,这非常简单。在先前的例子中,我可以通过调用S1[4:7]来获取 1 月 7 日的数据,同时测试 1 月 6 日的策略。即使你使用字典,你也可以通过使用错误指定的日期时间索引来检索错误的数据。

但话虽如此,有没有一种方法可以保证你的代码永远不会尝试从未来检索数据?让我们来看看!

提前查看问题的最终解决方案

要提出这样的解决方案,我们应该回想一下,当我们使用实时数据时,我们实际上无法从未来获取数据,因为交易应用会逐个接收跳动或任何其他数据包。因此,如果我们想在开发测试阶段完全保证不提前查看数据,我们可能需要模拟实际传入的数据流,并像计划与实时数据而不是存储在磁盘上的历史数据一起工作一样编写其余的代码。

向这种通用架构迈出的第一步是在第五章中,使用 Python 检索和处理市场数据,我们建议使用队列和线程来适应接收大量传入的跳动。现在,让我们从相同的角度来看待计算技术指标的问题。

我相信你记得,任何技术分析指标都是分析价格时间序列的一个子集,而这个子集的长度被称为周期。当我们查看图表时,我们总是注意到,为了在历史价格数据上重建指标值,我们需要将这个周期从左到右移动,从最老的数据点到最新的数据点。我们将这种方法称为滑动窗口

但现在让我们思考一下,如果我们试图在市场数据实时传入时即时构建指标值,我们应该怎么做。没有历史,没有存储的值,只有实时跳动。那么我们如何创建这样的滑动窗口呢?

答案很明显:我们确实创建了一个窗口,但我们没有创建一个滑动窗口,因为没有可以滑动的东西。

让我们回顾一下队列是如何工作的(也参见第五章使用 Python 检索和处理市场数据)。一般来说,队列是一个具有以下属性的列表:

  • 当新元素到来时,它将被添加到队列的末尾

  • 当我们从队列中检索一个元素时,它将从开始处取出并移除

现在,让我们创建一个特殊的队列,其中最旧的元素(那些在开始处的元素)不会被检索,而是在新元素添加时自动移除。让我们看看图 7.13中的图解,看看它是如何工作的:

图 7.13 – 固定长度队列,新元素到来时自动移除最旧的元素

图 7.13 – 固定长度队列,新元素到来时自动移除最旧的元素

现在,让我们开始用价格时间序列填充这个队列,从最旧的数据点开始。那么我们得到什么呢?

让我们用前面例子中的S1样本数据来做这件事。想象一下,我们创建了一个长度为三的队列。然后,我们开始向这个队列推送数据点,随着新点的到来,移除最旧的点。我们将得到以下子集:

S1_1 = {{01/01/2001,0.7}
      {01/02/2001, 2}
      {01/03/2001, 1.5}}
S1_2 = {{01/02/2001, 2}
      {01/03/2001, 1.5}
      {01/04/2001, 0.3}}
S1_3 = {{01/03/2001, 1.5}
      {01/04/2001, 0.3}
      {01/05/2001, 2.6}}
...

然后依次类推。

这些子集现在是什么?

哇,这正是我们使用滑动窗口得到的结果!

现在,我们不再将数据存储在数据库或其他存储中,然后为了构建 TA 指标的目的检索它,而是在新数据到来时即时进行。在这种情况下,我们将永远无法提前查看,因为我们永远无法从未来接收数据。我们唯一需要做的是使用历史数据模拟数据源。

这个解决方案还有一个明显的巨大好处:如果我们使用模拟数据源开发我们的应用程序,那么我们可以切换到实时数据源,而无需更改任何一行代码。因此,这种方法不仅保证了我们测试的结果始终是诚实的,而且我们还通过从一开始就开发一个通用应用程序来节省了大量时间。

言语已经足够,让我们开始编码吧。

滑动窗口作为队列

现在,我们准备好使用 Python 队列实现滑动窗口:

  1. 让我们先为我们的滑动窗口创建一个类:

    class sliding_window:
    
        def __init__(self, length):
    
            self.data = ([0]*length)
    

在这里,我们为我们的滑动窗口创建一个容器并将其填充为零。

  1. 现在,让我们添加一个唯一的方法,该方法将新元素追加到这个窗口中,并立即删除最旧的元素:

        def add(self, element):
    
            self.data.append(element)
    
            self.data.pop(0)
    
  2. 现在,创建一个长度仅为5(用于演示目的)的该类实例:

    sw = sliding_window(5)
    

就这样!

  1. 现在,让我们使用我们在第五章中创建的代码,使用 Python 检索和处理市场数据。它已经包含了可以用于将任何数据从任何对象或模块传输到任何其他对象的全球队列(数据流)。我们现在只想添加两个函数:一个用于从压缩文件(条形图)中读取数据并将其发送到全局数据流,另一个用于从该数据流中读取条形图并将其推送到滑动窗口:

    def get_sample(f):
    
        sample = {}
    
        values = f.readline().rstrip("\n").split(",")
    
        timestamp_string = "0" + values[0] + " " + values[1]
    
        ts = datetime.strptime(timestamp_string, "%m/%d/%Y %H:%M:%S")
    
        sample["open"] = float(values[2])
    
        sample["high"] = float(values[3])
    
        sample["low"]  = float(values[4])
    
        sample["close"]= float(values[5])
    
        sample["UpVolume"] = int(values[6])
    
        sample["DownVolume"] = int(values[7])
    
        sample["Datetime"] = ts
    
        return sample
    

您可以看到,当我们学习如何处理存储在文件中的数据时,我们完全重新使用了在第五章中创建的函数,使用 Python 检索和处理市场数据。在这里,我们从 CSV ASCII 文件中读取一个条形图,解析它,并将其转换为字典。

  1. 现在,让我们将这个样本发送到全局队列:

    def emulate_bar_stream():
    
        while True:
    
            time.sleep(1)
    
            datastream.put(get_sample(f))
    

再次强调,这与我们在上一章中做的是类似的,唯一的区别是我们现在使用了一个不同的函数来从文件中获取数据。无论如何,结果是相同的:我们将新的样本放入全局数据流。

注意

不要忘记,这里的延迟只是为了调试和演示的目的,以模拟样本逐个到达应用程序。

现在,我们已经完成了模拟传入数据的代码。让我们看看它:从这一点开始编写的任何代码,以及从全局队列中检索数据,都将与任何特定的数据源无关。如果您想更换源或从测试切换到实时交易,那么您需要做的就是重新编写emulate_bar_stream()函数。其余的代码将保持不变。

最后,我们需要一个函数,它从全局数据队列中读取并执行一些有意义的事情。

  1. 在我们当前的情况下,我们将只取条形图的收盘价,将其推送到滑动窗口,然后调用任何计算技术分析(TA)指标的函数:

    def retrieve_bars():
    
        while True:
    
            sw.add(datastream.get()["close"])
    
            # calling a TA indicator function here
    
            print(sw.data)
    

从函数中向控制台输出内容绝对是一种不良做法,但在这里只是临时添加,以便在调试期间快速检查代码的正确性。

  1. 现在,让我们启动两个线程:一个用于从文件(或在未来,从任何其他来源)读取数据,另一个用于处理接收到的数据。别忘了导入threading模块:

    import threading
    
    data_source_thread = threading.Thread(target = emulate_bar_stream)
    
    data_receiver_thread = threading.Thread(target = retrieve_bars)
    
    data_source_thread.start()
    
    data_receiver_thread.start()
    

如果您一切操作正确,您应该在控制台看到类似以下内容:

[0, 0, 0, 0, 1.12949]
[0, 0, 0, 1.12949, 1.12941]
[0, 0, 1.12949, 1.12941, 1.12965]
[0, 1.12949, 1.12941, 1.12965, 1.12883]
[1.12949, 1.12941, 1.12965, 1.12883, 1.12894]
[1.12941, 1.12965, 1.12883, 1.12894, 1.12925]

您现在可以看到我们的滑动窗口是如何从右到左填充值的,将旧值推出——完全像屏幕上的条形图或 tick 图。所以,在任何给定时刻,我们都有一个就绪的、指定长度的滑动窗口,其中填充了我们需要计算指标或执行任何其他我们只能想象的事情所需的数据。

现在,让我们看看我们如何轻松地使用这种方法构建指标。

移动平均 – 实现

使用我们的设置计算移动平均(MA),我们只需要计算窗口内所有值的平均值:

def moving_average(data):
    return sum(data) / len(data)

确实很简单!我们不必担心任何参数,因为我们从sliding_window类实例化时已经指定了滑动窗口的长度,当我们用数据填充窗口时,也指定了数据类型(在我们的例子中,是收盘价)。

如果我们现在运行我们的程序,我们会得到以下类似的结果:

0.225898
0.45178
0.67771
0.903476
1.129264
1.129216
1.129208

这解释了为什么我们总是忽略长度为N的滑动窗口中的前N-1个值;直到窗口中的所有元素都填充了有意义的数值,指标值才具有意义,否则应该被忽略。因此,在我们的例子中N == 5,我们忽略前 4 个值。

随机振荡器——实现

现在我们来看看如何计算像随机振荡器这样的范围指标。由于这个指标需要每个柱状图三个值(highlowclose),我们可能需要稍微修改一下我们的代码:

  1. 首先,让我们将我们的滑动窗口的通用名称从sw改为close,并添加两个用于高点和低点的滑动窗口:

    close = sliding_window(5)
    
    high = sliding_window(5)
    
    low = sliding_window(5)
    
  2. 现在,让我们编写计算随机指标值的函数:

    def stochastic(high, low, close):
    
        max_price = max(high)
    
        min_price = min(low)
    
        return (close[-1] - min_price) / (max_price - min_price)
    

这里,close[-1]代表最后一个可用的收盘价(如果我们实时运行代码,我们将接收到的那个)。

  1. 最后,让我们稍微修改一下retrieve_bars()函数,使其向所有三个滑动窗口(highlowclose)添加数据点,并计算随机指标值:

    def retrieve_bars():
    
        while True:
    
            data_point = datastream.get()
    
            close.add(data_point["close"])
    
            high.add(data_point["high"])
    
            low.add(data_point["low"])
    
            ma = moving_average(close.data)
    
            stoch = stochastic(high.data, low.data,
    
                               close.data)
    
            print(close.data[-1], ma, stoch)
    

如果我们现在运行我们的代码,我们会得到以下类似的结果:

(1.12949, 0.22589800000000002, 0.9999911465250112)
(1.12941, 0.45178, 0.9998406501473985)
(1.12965, 0.67771, 0.9999557404620697)
(1.12883, 0.9034760000000001, 0.9992298840400107)
(1.12894, 1.129264, 0.1914893617022131)
(1.12925, 1.129216, 0.5212765957448215)
(1.12937, 1.129208, 0.6489361702128061)

同样,一如既往,我们应该忽略前N-1个值,所以合理的读数从第 5 行开始。

概述

好吧,这已经是一次漫长且——我希望——有趣的旅程了,现在是时候总结一下我们学到了什么。

技术分析假设价格包含了自身的一切,并试图找到重复的行为模式,这表明在类似模式之后的交易价格行动也将与过去已经发生的情况相似。

尽管乍一看它们的视觉多样性很大,但技术分析只有四大主要类别:市场速度或动量、数字滤波器、范围和波动性。每种类型的研究都可以用来检测市场中的某种情况,但没有任何一种研究可以单独产生一个现成的盈利交易策略。

所有技术分析的基础是滑动窗口,而技术交易的大敌是在开发和测试过程中提前查看。使用队列来模拟传入的数据流和组织滑动窗口可以一劳永逸地解决提前查看的问题。

此外,这种方法使你的应用程序可扩展、灵活且模块化,在测试后能够连接到实时数据源而无需重写交易代码。

现在我们有足够的数据点,我们肯定想可视化所有这些数据,以便能够快速检查结果,甚至进行实时交易。所以,让我们进入下一章。

第八章:使用 Python 进行外汇交易中的数据可视化

在前面的章节中,我们学习了如何接收和存储市场数据,如何处理它,以及如何计算各种技术指标。然而,处理大量时间序列数据时,经常会遇到由令人沮丧的错误引起的错误,例如,使用错误的数据源或错误的戳记。除此之外,当处理技术指标时,检查计算结果的可视化是非常明智的 – 例如,您想使用大周期移动平均来确定长期价格走势,但您犯了一个错误,输入了一个小的周期值,然后发现自己陷入调试中,因为找不到真正的长期趋势。使您的调查可视化有助于非常快速地识别各种错误,并节省大量时间。

在本章中,我们将学习如何使用行业标准库之一 matplotlib 来可视化数据,然后继续使用 mplfinance 库绘制条形图和蜡烛图,我们将看到如何创建具有实时更新和使用价格图表的附加图形的图表。

在本章中,将涵盖以下主题:

  • 绘图和绘图基础知识 – 如何使用 Python 中的图形库

  • 使用脚本或交互式控制台进行快速绘图

  • 可视化历史市场数据

  • 创建条形图和蜡烛图

  • 可视化实时市场数据

  • 向价格图表添加其他对象

技术要求

要运行本章中的实际示例,您只需要 Python 3.9 或更高版本。

使用 Python 绘图的基础知识

有许多库实现了 Python 中的图表功能,但在撰写本文时,其中两个是行业标准 – matplotlibplotly

  • numpy 数组,支持多种类型的图表,包括金融图表(这正是我们所需要的!),对图表对象有完全控制权,具有几乎无限的图表定制功能,并且可以与不同的后端一起使用。

  • matplotlib,因此两者之间的选择并不明显。在交互性和通过 API 与图表对象一起工作时,Plotly 一定胜出,但在速度和定制能力方面则输给了竞争。

我们选择哪一个?一般来说,如果您不打算开发商业级 GUI 应用程序,那么 matplotlib 是一个明显的选择,因为它易于使用,文档非常完善,并且拥有良好的社区支持。

在我们开始之前,让我们快速了解一下 matplotlib(以及一般类似的图表库)是如何工作的,并明确决定我们将要做什么,以及我们特别不打算做什么。我们将了解不同的后端,考虑实时数据和静态数据绘制的特殊性,并熟悉图形引擎的内部组织和它们与 外部世界(您的代码)的交互。

图形后端

任何与 Python 一起使用的绘图包都有各种后端——这些后端是渲染屏幕上图形的引擎。开箱即用,我们有四种后端可供选择:Agg、PSpostscript)、PDF(是的,用于文档的标准 PDF 格式)和 SVG(矢量图形的标准)。

后端有绑定——简单来说,是便于从 Python 调用后端的包装器。一些后端提供开箱即用的交互式工具和控制;在这种情况下,你只需绘制一个图表,然后你可以使用后端提供的内置控件进行平移和缩放。使用其他后端需要完全实现用户界面,如果你想要定制和润色你的应用程序以达到商业级水平,这很好;但如果你只想快速查看一些数据,这会很烦人。

我们将使用标准的matplotlib,因此你不需要在代码中使用任何特殊命令。

静态数据与实时数据及其相关问题

重要的是要注意可视化静态数据和动态数据之间的关键区别。我们所说的静态数据是指只读取一次、接收、计算等,并且不在实时中被修改的数据。通常,它用于研发阶段,当你开发、测试和优化你的交易策略时。在这种情况下,我们与存储在磁盘上的历史数据或通过 API 从我们的经纪人那里检索的数据一起工作——但在任何情况下,它都不是实时修改的。这意味着图表库都可以在没有开发者付出太多努力的情况下完美工作;一旦数据集形成,就可以用一条命令进行绘制。

然而,一旦我们切换到实时数据可视化,我们会遇到问题,因为我们需要定期更新图表或根据事件更新——例如,在收到新的 tick 时。在这里,我们可能会遇到与多线程相关的问题。为了更好地理解这些问题,让我们快速回顾一下。

你还记得我们在第五章“使用 Python 检索和处理市场数据”中讨论的多线程概念吗?并行运行多个进程并通过队列连接它们使我们的交易应用程序通用,从某种意义上说,我们只需开发一次所有逻辑,然后就可以在数据源之间切换,以便从研发过渡到生产。

然而,当我们向我们的应用程序添加图形时,我们有时会遇到问题,这些问题源于图形也在一个单独的线程中运行。这个线程由一个独立的机制控制,而不是我们用来组织数据接收和处理线程的那个机制。

因此,将绘图添加到另一个线程以使其与主线程完全分离是非常棘手的,至少可以说。所以,我们将牺牲我们应用程序的通用性,以保持图形使用的简单性。

关于线程、循环和进程终止的重要注意事项

多线程的问题在于线程的终止。如果你启动了一个线程,并且没有添加任何检查以确定是否继续运行或终止,那么它将几乎无限期地运行。为了保持你的代码正确,你可能想要在任何一个线程和/或整个程序(它也是一个线程)中始终添加一个退出条件。然而,当使用 matplotlib 时,你并没有对渲染图形的线程有明确的控制权,因此你可能想要使用操作系统的回调。因此,尽管有被编程纯粹主义者批评的风险,我个人在 IDE 内使用键盘终止来停止图形循环——考虑到在生产中,我们不需要这样做,因为我们永远不会在生产代码中包含图表。如何处理这个问题取决于你,但在我看来,越简单越好。

因此,我们将要做的是将一些基本的图表集成到主模块的主线程中。它将只用于快速可视化,没有任何意图将其用作通用的图表软件,并且在从开发模式切换到生产模式之前将被禁用或移除。

我们不会编写复杂的代码,以提供给我们应用一个复杂的 GUI,该 GUI 将完全模仿商业应用程序,例如 MetaTrader 或 MultiCharts。

足够的讨论了——让我们开始编码。

安装 Matplotlib

除非你使用的是干净的 Python 安装,否则你很可能已经安装了 matplotlib,所以在尝试安装之前先检查一下。只需在控制台中输入 import matplotlib 并查看结果。

如果你还没有安装 matplotlib,你可以使用标准安装:

python -m pip install -U pip
python -m pip install -U matplotlib

如果你使用 Conda 软件包,你可以使用以下命令安装 matplotlib

conda install matplotlib

通常来说,所有主要的第三方 Python 发行版,如 Anaconda、ActiveState、ActivePython 和 WinPython,都将 matplotlib 作为其一部分。

在你的代码中使用 Matplotlib

matplotlib 是一个相当大的库,我们实际上只需要其中的一部分——即进行绘图的模块。一如既往,我强烈建议使用 import 而不是 from ... import ——以保持命名空间分离:

import matplotlib.pyplot as plt

这导入了 pyplot 模块,它实际上创建并处理图表。让我们创建我们的第一个图表:

y = range(10)
plt.plot(y)
plt.show()

结果将看起来像以下图:

图 8.1 – 一个基本的 Matplotlib 图表

图 8.1 – 一个基本的 Matplotlib 图表

让我们看看实际上发生了什么:

  1. 图形库创建了一个图形——即所有后续图表都应该制作的画布

  2. 然后,在图中创建了坐标轴——在我们的例子中,是一个从 0 到 10 的刻度矩形框。

  3. 然后,一个图形对象被添加到坐标轴上——在我们的例子中,它是对角线。

  4. 最后,调用了 plt.show() ——这是实际上在屏幕上显示图表的方法。

多线程:回顾

当你运行这段代码时,你现在会看到一个单独的进程。这个进程是由plt.show()方法创建的,其名称是python。不要把它与 Python 解释器混淆!这个新的进程只处理当前在屏幕上显示的图表。如果你杀掉它,matplotlib主循环将终止,图表将消失,但其他线程将继续运行!所以,一个更好的主意是从 IDE 停止代码的执行,以杀死包括这个python进程在内的所有线程。

如您所见,我们在y列表中有 10 个元素,水平轴相应地进行了刻度,从 0 到 10。很明显,当前的图表显示了y = x的线性函数,其中参数是整数。然而,如果我们想计算分数参数的相同函数——比如说,相同的 10 个点,但取值范围在 0 到 1 之间,而不是 0 到 10 呢?

让我们试一试:

  1. 首先,像往常一样,我们导入pyplot

    import matplotlib.pyplot as plt
    
  2. 然后,我们导入numpy——这是 Python 的通用数学库,它增加了对向量和矩阵的支持,以及无数的数学函数。我们将使用arange,它在某种程度上类似于原生的 Python 范围,但支持分数步长:

    import numpy as np
    
  3. 接下来,我们确定要显示的范围:

    y = np.arange(0, 1, 0.1)
    
  4. 代码的其他部分没有改变——只是创建一个图表并在屏幕上显示:

    plt.plot(y)
    
    plt.show()
    

然后,你应该会看到一个类似于以下图表的图形:

图 8.2 – 在 0 到 1 范围内的线性函数的图表——错误的 X 轴刻度

图 8.2 – 在 0 到 1 范围内的线性函数的图表——错误的 X 轴刻度

但等等!我们的图表有些问题。是的,现在Y轴已经从 0 增加到 1,步长为 0.1,但X轴仍然从 0 增加到 10。这是因为我们绘制了新的数组,但没有做任何事情来告诉plt.plot()方法关于新的刻度,即X轴——默认情况下,这个方法假设我们绘制任何数组与它的元素索引相对应,这始终是一个整数数组。

  1. 让我们通过添加正确的X轴数据来修复这个错误:

    x = np.arange(0, 1, 0.1)
    

让我们也修改plot()方法的调用:

plt.plot(x, y)

现在,我们将看到正确的图表,如下面的图所示:

图 8.3 – 在 0 到 1 范围内的线性函数的图表——现在正确的 X 轴刻度

图 8.3 – 在 0 到 1 范围内的线性函数的图表——现在正确的 X 轴刻度

现在我们对plot()方法的工作原理有了一些了解,让我们绘制一些实际的市场数据。

市场数据的简单图表

在接下来的示例中,我们将只使用历史数据。我们将在本章的后面学习如何绘制从经纪人那里接收到的实时数据。

有许多方法可以读取和处理市场数据,其中一些在第五章“使用 Python 获取和处理市场数据”中考虑过。现在,我们将学习一些替代方法,以便你能够根据当前的研究和开发需求做出最佳选择。

让我们从最直接的方法开始,它只使用原生 Python 数据结构。正如我们在 第五章 中看到的,由于与 JSON 标准的完全兼容性以及能够通过关键字提取必要数据的能力,存储和操作市场数据的首选方式是字典。我们将从字典开始:

  1. 首先,我们仍然需要做一些导入:

    import matplotlib.pyplot as plt
    
    import csv
    

csv 模块包含非常方便的方法来读取和解析 逗号分隔值CSV)文件,这是存储历史市场数据的既定标准。

  1. 现在,让我们打开一个数据文件并创建一个 DictReader() 对象:

    f = open("/Volumes/Storage HDD/Data/LMAX EUR_USD 1 Minute.txt")
    
    csvFile = csv.DictReader(f)
    

DictReader() 方法将 CSV 文件解析为字典或一组字典,并返回一个 DictReader 对象。它使用文件的第一个行作为关键字源,所以请确保你在源数据中有它。在我使用的示例文件中,标题(第一行)看起来如下所示:

Date,Time,Open,High,Low,Close,UpVolume,DownVolume,
TotalVolume,UpTicks,DownTicks,TotalTicks

因此,这些就是在我使用 DictReader() 读取文件并解析它时将在字典中出现的非常关键词。

  1. 现在,我们需要将这个对象转换为列表:

    all_data = list(csvFile)
    

如果我们现在通过输入 print(all_data[-3:]) 来查看列表中的最后三个元素,我们会看到如下内容:

[{'Date': '11/12/2020', 'Time': '17:45:00', 'Open': '1.18136', 'High': '1.18143', 'Low': '1.18125', 'Close': '1.18140', 'UpVolume': '249', 'DownVolume': '494', 'TotalVolume': '743', 'UpTicks': '7', 'DownTicks': '5', 'TotalTicks': '12'}, {'Date': '11/12/2020', 'Time': '17:46:00', 'Open': '1.18140', 'High': '1.18156', 'Low': '1.18138', 'Close': '1.18154', 'UpVolume': '399', 'DownVolume': '299', 'TotalVolume': '698', 'UpTicks': '8', 'DownTicks': '4', 'TotalTicks': '12'}, {'Date': '11/12/2020', 'Time': '17:47:00', 'Open': '1.18154', 'High': '1.18156', 'Low': '1.18145', 'Close': '1.18155', 'UpVolume': '500', 'DownVolume': '650', 'TotalVolume': '1150', 'UpTicks': '5', 'DownTicks': '6', 'TotalTicks': '11'}]

因此,我们现在确实有了以 OHLC 条形图形式表示的 1 分钟数据,以及一些关于成交量以及每个区间(1 分钟)的刻度数的信息。每个条形图都由一个单独的字典表示,而字典被收集在一个列表中。

  1. 现在,我们需要提取我们想要绘制的仅有的数据 – 比如,收盘价。为了避免绘制过多数据可能引起的问题,让我们只绘制最后 100 个数据点。有许多方法可以做到这一点;我们将使用列表推导式:

    close = [float(bar['Close']) for bar in all_data[-100:]]
    

其余的代码都是相同的:

plt.plot(close)
plt.show()

如果你一切操作正确,你应该会看到一个类似于以下截图所示的图表:

图 8.4 – 简单的收盘价折线图

图 8.4 – 简单的收盘价折线图

  1. 太好了,但关于 X 轴标签怎么办呢?哦,它只是简单地计数数据点,从 0 到 100,但我们希望看到更有意义的内容。让我们回忆一下,plot() 方法实际上是在一个可迭代对象与另一个可迭代对象之间绘制,所以我们将从时间戳创建标签:

    time = [bar['Time'] for bar in all_data[-100:]]
    
    plt.plot(time, close)
    

现在,我们可以看到沿 X 轴的数字确实被替换了,但这个“替换”的内容真的很难阅读和理解:

图 8.5 – 使用时间戳作为 X 轴标签而没有适当的格式化会导致结果不规则

图 8.5 – 使用未正确格式化的时间戳作为 X 轴标签会导致结果不稳定

毫不奇怪,标签(时间戳)相当长,有很多,它们一个接一个地显示,使得结果无法使用。因此,我们希望以某种方式格式化它们,只打印每 10、20 或 100 个刻度处的标签,并将它们旋转以节省屏幕空间。

为了做到这一点,我们应该回忆一下由调用plot()方法触发的事件序列,这在本章的在代码中使用 matplotlib部分中有所解释。首先,创建了一个figure(一个空白画布),添加了一个axis(带有轴的矩形框),然后绘制了实际的plot。这三个对象都是通过单个调用plot()方法自动创建的。然而,为了能够修改图表的可视表示,我们需要直接访问axes对象。现在,我们想要重写整个绘图代码,以便分离上述三个对象。

  1. 因此,在我们形成了time列表并在调用plot()之前,我们添加以下内容:

    fig = plt.figure()
    
  2. 这创建了一个新的空图对象,并返回了对fig变量的句柄。然后,我们添加以下内容:

    ax = fig.add_subplot()
    
  3. 这创建了一个新的子图,或轴——我们想要定制的矩形。太好了——现在我们可以通过ax变量访问它。所以,让我们限制沿X轴打印的刻度标签数量:

    ax.set_xticks(np.arange(0, len(time) + 1, 15))
    

set_xticks()方法在这里只使用一个参数——一个可迭代对象,它指定了我们想要绘制的time列表中那些元素的索引。在我们的例子中,我们使用了一个numpyarange,它包含以 15 为步长的整数数字,以绘制每个 15 个时间戳。

  1. 最后,为了使我们的图表更加易于阅读,让我们将标签旋转 45°并绘制结果:

    plt.xticks(rotation=45)
    
    plt.plot(time, close)
    
    plt.show()
    

如果你一切操作正确,你应该看到以下这样的图表:

图 8.6 – 略微更好看的简单价格图表

图 8.6 – 略微更好看的简单价格图表

到目前为止,我们只为最后 100 个数据点绘制了图表,但如果我们想绘制过去某一天的市场数据呢?

  1. 现在,让我们重写整个代码,以便将我们迄今为止所学的一切内容放在一起:

    import csv
    
    import matplotlib.pyplot as plt
    
    import numpy as np
    
    f = open("/Volumes/Storage HDD/Data/LMAX EUR_USD 1 Minute.txt")
    
    csvFile = csv.DictReader(f)
    
    all_data = list(csvFile)
    

到目前为止,没有区别;我们只是导入了必要的模块并读取了数据文件。现在,我们想要从 2019 年 12 月 12 日午夜开始绘制 100 个条形图。因此,我们在字典列表all_data中找到与Date关键字相关值的字典位置:

starting_bar_number = 0
for bar in all_data:
    if bar['Date'] == '12/12/2019':
        break
    starting_bar_number += 1
  1. 然后,我们提取绘图所需的数据:

    close = [float(bar['Close']) for bar in all_data[starting_bar_number:starting_bar_number + 100]]
    
    time = [bar['Time'] for bar in all_data[starting_bar_number:starting_bar_number + 100]]
    

然后,我们实际上使用一些漂亮的格式进行绘图:

fig = plt.figure()
ax = fig.add_subplot()
ax.set_xticks(np.arange(0, len(time) + 1, 15))
plt.xticks(rotation=45)
plt.plot(time, close)
plt.show()

就这样!现在,我们可以享受我们的图表了:

图 8.7 – 指定日期和时间的数据的简单价格图表

图 8.7 – 指定日期和时间的数据的简单价格图表

在这里,我们省略了 matplotlib 的所有功能,包括标题格式化、使用颜色、多个子图以及更多内容——毕竟,这本书不是 matplotlib 教程。如果你对学习这个可靠的图表包感兴趣,我建议从其官方网站 (matplotlib.org) 开始,在那里你可以找到很多示例、教程和一般文档。

好的,现在我们知道了如何绘制基本的价格图表——简单的收盘价(或任何其他价格)与时间戳的简单线形图。然而,我们都知道在金融世界中,最常见的图表格式是柱状图或蜡烛图。我们如何使用 matplotlib 绘制这样的图表呢?

好吧,几年前,matplotlib 本身曾经有一个 finance 模块,支持相关的图表。然而,现在它已被弃用。因此,我们只有两个选择——要么使用 matplotlib 的 bar() 方法逐条构建金融图表,要么选择一个相对较新但功能强大的包,mplfinance。这个包提供了非常简单直接的方法来绘制价格图表,但它不会绘制存储在原生 Python 结构(如列表或字典)中的数据。相反,它使用 pandas 来处理市场数据。

那么,pandas 是什么,我们如何使用它?

使用 pandas 可视化静态市场数据

根据其官方网页 pandas.pydata.org 上的声明,pandas 是 “一个快速、强大、灵活且易于使用的开源数据分析和管理工具,建立在 Python 编程语言之上”。它最初正是为了处理时序数据,特别是市场价格数据而开发的。

与原生的 Python 列表或 NumPy 数组不同,pandas 使用 DataFrames 作为核心数据对象。你可以将 DataFrame 视为一个表格,其中列代表各种命名的时序数据(或任何其他序列),而行包含实际数据,第一行总是包含序列的名称。这与我们迄今为止使用的历 史市场数据文件非常相似吗?是的,这也使得使用 pandas 的学习曲线非常陡峭。

pandas 提供了添加、删除和重新排列列、创建和修改索引、切片和创建子集、合并和重塑 DataFrame 以及处理缺失数据的方法。

注意

pandas 是一个相当全面的包,提供了各种方法来处理数据,以满足非常不同的需求。这本书绝不是 pandas 教程;我们将只使用那些完成绘制市场数据任务所需的方法。如果你对学习 pandas 感兴趣,我建议从 www.w3schools.com/python/pandas/default.asp 上的简单教程开始。

安装 pandas

官方的 pandas 网站推荐安装 Anaconda,它自带 pandas,并将其他安装方法称为 高级。然而,它们并没有真正高级。你可以使用标准的 PyPI 安装:

pip install pandas

或者,你可以使用 Conda:

conda install pandas

使用 pandas 读取 CSV 文件中的数据

pandas 提供了一种直接从 CSV 文件读取数据的方法。让我们尝试以下代码:

import mplfinance as mpf # this is for future use with charting
import pandas
file_name = "/Volumes/Storage HDD/Data/LMAX EUR_USD 1 Minute.txt"
source_data = pandas.read_csv(file_name)
print(source_data)

当然,像往常一样,你想要将 file_name 的值替换为本地存储的历史数据 CSV 文件的实际路径。

如果你一切操作正确,你会看到以下输出:

               Date      Time     Open  ...  UpTicks  DownTicks  
TotalTicks
0         1/27/2015  13:29:00  1.12942  ...        3          2
           8
1         1/27/2015  13:30:00  1.12947  ...        4          7
          12
2         1/27/2015  13:31:00  1.12953  ...        9          4
          14
3         1/27/2015  13:32:00  1.12944  ...        2         10
          13
4         1/27/2015  13:33:00  1.12876  ...        5          4
          12
...             ...       ...      ...  ...      ...        ...
         ...
2136274  11/12/2020  17:43:00  1.18134  ...        4          7
          11
2136275  11/12/2020  17:44:00  1.18124  ...        7          4
          11
2136276  11/12/2020  17:45:00  1.18136  ...        7          5
          12
2136277  11/12/2020  17:46:00  1.18140  ...        8          4
          12
2136278  11/12/2020  17:47:00  1.18154  ...        5          6
          11
[2136279 rows x 12 columns]

你可以看到,第一行用于创建列名,其余部分形成了实际的数据。

注意,在从文件读取的数据前面,有一个额外的没有命名的列,它只包含从 0 到 2,136,278(在我的示例文件中)的整数。这是 DataFrame 索引。我们可以通过这些数字引用任何特定的记录——再次,这与我们使用列表时几乎一样。当然,这很不方便,因为我们希望能够通过引用特定的日期或时间范围来检索任何历史数据。幸运的是,pandas 提供了一种为 DataFrame 建立自定义索引的方法,所以让我们继续我们的代码。

为 dataframe 设置索引

首先,让我们从 datetime 字段形成时间戳:

source_data['Timestamp'] = pandas.to_datetime(source_data['Date']) + pandas.to_timedelta(source_data['Time'])

在这里,我们使用内置方法 to_datetime()to_timedelta(),它们将字符串值转换为单个 Timestamp 对象。

在 pandas 中引用列

在前面的代码中,你可以看到 pandas 如何允许你通过名称引用某个列。这非常类似于在字典中通过关键字查找值,但在 pandas 中,返回的是整个列,而不是一个标量值。

将值赋给一个不存在的列名(在我们的例子中是 Timestamp)实际上创建了一个具有该名称的新列。

创建了新列后,让我们将其设置为索引:

source_data.set_index(source_data['Timestamp'], inplace=True)

如果我们现在检查 source_data 的内容,我们会看到新索引已生成并添加到 DataFrame 中:

                           Date      Time  ...  TotalTicks
           Timestamp
Timestamp                                  ...
2015-01-27 13:29:00   1/27/2015  13:29:00  ...           8 2015-01-27 13:29:00
2015-01-27 13:30:00   1/27/2015  13:30:00  ...          12 2015-01-27 13:30:00
2015-01-27 13:31:00   1/27/2015  13:31:00  ...          14 2015-01-27 13:31:00
2015-01-27 13:32:00   1/27/2015  13:32:00  ...          13 2015-01-27 13:32:00
2015-01-27 13:33:00   1/27/2015  13:33:00  ...          12 2015-01-27 13:33:00
...                         ...       ...  ...         ...
                 ...
2020-11-12 17:43:00  11/12/2020  17:43:00  ...          11 2020-11-12 17:43:00
2020-11-12 17:44:00  11/12/2020  17:44:00  ...          11 2020-11-12 17:44:00
2020-11-12 17:45:00  11/12/2020  17:45:00  ...          12 2020-11-12 17:45:00
2020-11-12 17:46:00  11/12/2020  17:46:00  ...          12 2020-11-12 17:46:00
2020-11-12 17:47:00  11/12/2020  17:47:00  ...          11 2020-11-12 17:47:00
[2136279 rows x 13 columns]

太好了!现在我们已经通过时间戳索引了所有我们的数据点(分钟柱状图)。现在让我们创建一个简单的柱状图,用于之前使用的相同 1 分钟历史数据。

使用 pandas 和 mplfinance 创建简单的柱状图

当然,我们不想在一张图表中绘制所有数据。我在本章使用的数据文件包含大约 5 年的 1 分钟柱状图,或超过 200 万条记录,所以创建一个包含超过 200 万个柱子的图表将永远冻结渲染。让我们只为一段历史创建一个图表,指定日期、开始时间和结束时间:

sample_date = '23-03-2020'
start_time = '00:01:00'
day_close_time = '23:00:00'

我文件中的数据位于 GMT+1 时区,所以这里的 23:00 等于纽约时间的 17:00,这是外汇市场的银行结算时间(参见 第三章 中的 Trading the FX market – what and how 部分,从开发者的角度来看的外汇市场概述)。此外,请注意,一天中的第一个时间戳(start_time 变量)是午夜过后的 1 分钟;对于柱状图,时间戳表示最后一笔交易的时间或区间的收盘价(参见 第五章 中的 Universal data connector 部分,使用 Python 获取和处理市场数据,以获取数据压缩和时间戳的详细解释)。

在 pandas 中制作切片和子集

从 DataFrame 中提取子集最常用的方法是 .loc()。它的使用同样非常直观,因为它与原生 Python 列的切片非常相似;你只需要指定新子 DataFrame 的起始和结束索引,如以下伪代码所示:

sub_dataframe = original_dataframe[start : end]

在我们的实际代码中,它将看起来像以下这样:

all_day_sample = source_data.loc[sample_date + " " + start_time: sample_date + " " + day_close_time]

最后,我们希望去除所有不必要的数据,只保留 OpenHighLowClose。同样,使用 pandas,只需指定要保留的列的列表即可完成,其余的将被丢弃:

OHLC_data = all_day_sample[['Open', 'High', 'Low', 'Close']]

注意双括号;外层一对括号表示我们根据指定的列集创建一个子 DataFrame,而内层一对括号实际上指定了列表中的列。这一切都完成了——现在,是时候创建我们的第一个柱状图了。

使用 mplfinance 绘制市场图表

所有由 mplfinance 创建的图表默认都是柱状图,所以让我们从最简单的开始:

mpf.plot(OHLC_data)

如果你一切操作正确,你应该会看到一个像下面的图:

图 8.8 – 使用 mplfinance 绘制的默认柱状图

图 8.8 – 使用 mplfinance 绘制的默认柱状图

乍一看,它看起来像同一条线形图,但这是因为我们在相对较小的画布上放置了太多的柱子。在这里,我们可以利用 matplotlibmplfinance 是基于 matplotlib 构建的)默认使用的 TkAgg 后端是交互式的;你可以点击放大镜图标并放大图表的任何部分——比如,白天中间的峰值:

图 8.9 – 使用 TkAgg 后端的交互性进行缩放和放大

图 8.9 – 使用 TkAgg 后端的交互性进行缩放和放大

有几种选项可以自定义 mplfinance 图表。例如,我们可以绘制蜡烛图而不是柱状图,甚至可以向其中添加多个移动平均线:

mpf.plot(OHLC_data, type = 'candle', mav = (20, 50, 200))

这里,mav 代表 移动平均线,它们的周期由元组指定。结果如图所示:

图 8.10 – 使用 mplfinance 绘制的带有移动平均线的蜡烛图

图 8.10 – 使用 mplfinance 绘制的带有移动平均线的蜡烛图

现在,我们可以使用 Matplotlib 的pyplotmplfinance来创建以简单折线图或漂亮的条形图或蜡烛图形式的价格图表。然而,到目前为止,我们只处理了静态且不实时更新的保存数据。那么,当我们从经纪人或数据供应商那里实时接收数据时,我们该怎么办?让我们看看如何使用通用数据连接器方法来解决这个问题。

可视化实时市场数据

在我们继续之前,我强烈建议你重新阅读第五章中的使用保存和实时数据——保持你的应用通用部分,以及第七章中的滑动窗口部分,使用 Python 检索和处理市场数据在 Python 中实现技术分析及其应用。我们将使用相同的架构来创建市场数据的实时图表。

重要提醒

无论我们从实时数据源接收什么数据,都应该进入队列。这应该在单独的线程中完成。然后,数据从队列中读取到滑动窗口中,该窗口控制实际数据的数量——用于任何处理或绘图。

当我们处理静态历史数据时,我们使用了非常方便的方法,允许我们用一行代码将整个数据集读入内存,然后导航它。当然,任何便利性都是要付出代价的,在这种情况下,代价是冒着提前查看的风险(参见第四章中的交易逻辑——这里一个小错误可能损失一大笔钱部分,第七章通过使用滑动窗口和线程,并逐个将数据点喂入其中,无论它们是本地保存的还是从经纪人那里接收的,有效地解决了这个问题)。

所以,换句话说,到目前为止,我们在本章中所做的是方便的,但它与构建一个既适合研究又适合实时交易的全能交易应用的理念相矛盾。

那么,你们可能想知道,我们为什么要做所有这些?

有两个原因。

首先,别忘了我们只使用图表在研究阶段要么直观地确认一个想法,要么在生产中检查实时订单的一致性。当我们致力于交易想法的开发时,能够立即可视化某些历史数据会非常方便,尤其是如果你在一个交互式环境中工作,比如 IPython。这就是使用 pandas 与mplfinance可能是正确选择的地方。

其次,可视化不仅用于绘制市场数据,还(可能甚至更频繁地)用于绘制回测结果,在历史数据上运行模拟交易。由于回测按定义是某种刻在石头上的东西——也就是说,不会实时更新——因此,我们在此章前面考虑的方法将非常适合我们的目的。

总的来说,我们希望可视化实时市场数据,以检查其正确性,查看各种指标,以及/或跟踪订单执行。让我们看看它是如何完成的。

绘制实时 tick 数据

和往常一样,我们从几个导入开始:

import json
import threading
import queue
import matplotlib.pyplot as plt

json模块将帮助我们解析数据服务器的响应;我们已熟悉其他模块

然后,我们从websocket库导入建立 WebSocket 连接的方法:

from websocket import create_connection

接下来,我们创建一个实现滑动窗口的类(请参阅第六章基本面分析的基础及其在 FX 交易 中的可能用途中的滑动窗口部分):

class sliding_window:
    def __init__(self, length):
        self.data = ([0]*length)
    def add(self, element):
        self.data.append(element)
        self.data.pop(0)

然后,我们添加一个函数来创建并维护与市场数据服务器的 WebSocket 连接。此函数有三个参数:

  • 连接的 URL

  • 我们发送给服务器以订阅市场数据的消息

  • 我们放置传入 tick 的队列

如果你计划构建一个具有多个连接的复杂应用程序,你也可以将此函数实现为class方法:

def LMAX_connect(url, subscription_msg, ticks_queue):

创建连接:

    ws = create_connection(url)

然后,发送订阅消息,我们将在后面指定,将其放在函数代码之外(如果你将函数实现为class方法,你可能想将订阅消息作为参数传递或将其作为class属性):

    ws.send(subscription_msg)

订阅成功后,函数等待传入的 tick 并将它们放入队列:

    while True:
        tick = json.loads(ws.recv())
        ticks_queue.put(tick)
        print(tick)

print(tick)语句仅用于调试目的。所有准备工作都已完成,现在,让我们继续:

url = "wss://public-data-api.london-demo.lmax.com/v1/web-socket"
subscription_msg = '{"type": "SUBSCRIBE","channels": [{"name": "ORDER_BOOK","instruments": ["eur-usd"]}]}'

在这里,我们指定要连接的服务器和我们将发送以订阅市场数据的消息。请参阅第五章使用 Python 检索和处理市场数据,以获取 LMAX 数据结构的详细描述,以及第四章交易应用程序——里面有什么?中的检索数据——垃圾进,垃圾出部分,以刷新你对接收和处理数据的重要问题的记忆。

接下来,我们将创建队列以存储传入的 tick:

pipe = queue.Queue()

我们还将创建一个线程来检索数据:

data_receiver_thread = threading.Thread(target = LMAX_connect, args = (url, subscription_msg, pipe))
data_receiver_thread.start()

如果你一切操作正确并运行了代码,你将看到来自 WebSocket 的订单簿数据:

{'type': 'ORDER_BOOK', 'instrument_id': 'eur-usd', 'timestamp': '2022-10-28T09:12:26.000Z', 'status': 'OPEN', 'bids': [{'price': '0.995350', 'quantity': '1000000.0000'}, {'price': '0.995340', 'quantity': '2000000.0000'}, {'price': '0.995330', 'quantity': '500000.0000'}, {'price': '0.995320', 'quantity': '500000.0000'}, {'price': '0.995310', 'quantity': '1500000.0000'}, {'price': '0.987800', 'quantity': '1000000.0000'}, {'price': '0.985000', 'quantity': '13000000.0000'}, {'price': '0.980000', 'quantity': '13000000.0000'}], 'asks': [{'price': '0.995410', 'quantity': '500000.0000'}, {'price': '0.995420', 'quantity': '1000000.0000'}, {'price': '0.995430', 'quantity': '1500000.0000'}, {'price': '0.995440', 'quantity': '2000000.0000'}, {'price': '0.995450', 'quantity': '3000000.0000'}, {'price': '0.995810', 'quantity': '410000.0000'}]}

我们只想绘制书籍的顶部——也就是说,当前的最佳买入价和最佳卖出价——所以让我们添加另一个函数,该函数将解析传入的 tick 并将bidask值发送到相应的滑动窗口。我们实现此函数时没有参数,因为它与代码的图表部分共享数据结构(bidask滑动窗口):

def get_ticks(ticks_queue):
    while True:
        tick = ticks_queue.get()
        if 'bids' in tick.keys():
            bid = float(tick['bids'][0]['price'])
            ask = float(tick['asks'][0]['price'])
            bids.add(bid)
            asks.add(ask)
            print(bid, ask)

此函数从队列中获取跳动,提取买卖双方,并将它们发送到相应的滑动窗口(bidsasks)。让我们创建它们——首先,我们指定滑动窗口的长度(让我们将其设置为 60,这样将显示大约 1 分钟的数据,考虑到 LMAX 以大约每秒 1 跳的速度发送更新):

window_size = 60

然后,添加两个窗口,分别用于bidsasks

bids = sliding_window(window_size)
asks = sliding_window(window_size)

现在,我们将处理函数包装进一个线程中:

trading_algo_thread = threading.Thread(target = get_ticks, args = (pipe,))
trading_algo_thread.start()

如果我们现在运行到目前为止所开发的代码,我们将看到每秒大约更新一次的买卖对:

0.99626 0.99631
0.99626 0.99629
0.99624 0.9963
0.99624 0.9963
0.99624 0.99629

干得好!现在,我们想在图表中绘制实时数据。

到目前为止,最自然的做法是创建第三个线程,并在其中完成所有绘图,以保持三个过程(检索数据、处理和绘图)彼此以及与主线程的分离。

可惜,使用matplotlib(以及许多其他图表套件)来做这件事非常复杂(尽管并非不可能)。所以,不幸的是,我们必须接受图表只能在主线程中(轻易地)可用的事实。

首先,让我们等待滑动窗口中的所有数据都填充了有意义的值:

while bids.data[0] == 0:
    pass

然后,我们分别创建图形和坐标轴(就像我们在本章前面自定义坐标轴标签时做的那样):

fig = plt.figure()
ax = fig.add_subplot()

接下来,我们为我们的两个数据系列(买卖双方)添加两条线:

line1, = ax.plot(bids.data)
line2, = ax.plot(asks.data)

最后,我们启动主绘图循环,该循环将每秒绘制线条并刷新图形:

while True:
    line1.set_ydata(bids.data)
    line2.set_ydata(asks.data)

以下命令在图表上方和下方添加了小的边距,只是为了提高视觉感知:

    plt.ylim(min(bids.data) - 0.0001, max(asks.data) + 0.0001)

然后,我们实际绘制图表:

    fig.canvas.draw()

我们等待 1 秒钟,直到图形被渲染并出现在屏幕上;否则,循环会阻塞渲染,我们不希望在这次暂停期间让它运行:

    plt.pause(1)

简单!如果你一切都做对了,你应该看到以下类似的图形,每秒更新一次:

注意

填充整个滑动窗口需要时间。在我们的例子中,大约需要 60 秒才能出现图形。

图 8.11 – 一个简单的实时价格跳动图表,显示买卖双方

图 8.11 – 一个简单的实时价格跳动图表,显示买卖双方

好吧,现在我们可以绘制市场价格的实时图表,这已经变得相当简单直接——但仅以每跳的线条形式。如果我们想聚合数据然后绘制条形图或蜡烛图呢?让我们在下一节中找到解决方案。

绘制实时条形图或蜡烛图

在本章的早期,我们处理过柱状图和蜡烛图,我们知道最流畅的方式是使用mplfinance库。想法是在循环中使用mplfinance.plot()方法,并更新,类似于我们刚刚对 tick 数据的线形图所做的。所以,我们现在想要做的是添加一个新的函数,该函数将根据一定的规则将传入的 tick 数据流分割成柱,将形成的柱添加到 DataFrame 中,并将结果 DataFrame 发送到图表循环:

  1. 让我们从导入开始。一些导入的模块与上一个例子相同,因为我们需要它们从 WebSocket 连接中再次检索数据:

    import json
    
    import threading
    
    import queue
    
    from websocket import create_connection
    
  2. 然后,我们导入datetime方法,因为我们将会将字符串时间戳转换为datetime对象:

    from datetime import datetime
    
  3. 最后,我们有一些导入语句以方便图表绘制:

    import matplotlib.pyplot as plt
    
    import pandas
    
    import mplfinance as mpf
    

然后,我们将重用绘图实时 tick 数据部分的一些代码 – sliding_window类、urlsubscription_msg变量的赋值,以及pipewindow_sizebidsasks的初始化。我们还将重用LMAX_connect()函数而不做任何修改。

  1. 现在,我们将创建一个新的队列,我们将向其中发送用于绘图的 DataFrames:

    data_for_chart = queue.Queue()
    

我们还将创建一个新的函数,该函数将执行将传入的 tick 数据流分割成柱的任务:

def make_bars():
    bars = pandas.DataFrame(columns=['Timestamp', 'Open', 'High', 'Low', 'Close'])
    bars.set_index('Timestamp', inplace=True)
  1. 我们创建了一个空的 DataFrame,设置了列标题,并将Timestamp设置为索引。接下来,我们将时间框架(分辨率)设置为 10 秒,并初始化时间戳:

        resolution = 10
    
        last_sample_ts = 0
    

现在,就像往常一样,如果我们使用线程处理无限过程,我们就开始循环,在这个循环中我们读取一个 tick,提取其时间戳和最后出价(我们假设我们想要绘制出价;如果你想要绘制订单簿中的任何其他数据,只需在字典中选择适当的关键字和值即可),并且如果这是我们收到的第一个 tick,初始化即将到来的柱的openhighlowclose值,并将last_sample_ts设置为ts

    while True:
        tick = pipe.get()
        ts = datetime.strptime(tick['timestamp'], "%Y-%m-%dT%H:%M:%S.%fZ")
        last_bid = float(tick['bids'][0]['price'])
        if last_sample_ts == 0:
            last_sample_ts = ts
            open = high = low = close = last_bid
  1. 现在,我们指定启动新柱的条件。在这种情况下,一旦当前时间(ts)与上一柱时间(last_bar_sample)之间的差异大于存储在resolution变量中的值,我们就立即执行:

            delta = ts – last_sample_ts
    
            if delta.seconds >= resolution:
    
                bar = pandas.DataFrame([[open, high, low, close]], columns = ['Open', 'High', 'Low', 'Close'], index = [ts])
    
                bars = pandas.concat([bars, bar])
    
  2. 因此,一旦一个新的 10 秒间隔开始,我们就使用OpenHighLowClose值以及当前时间戳创建一个新的 DataFrame 柱,并将其添加到主 DataFrame bars中。该函数其余部分的代码相当明显;首先,我们再次初始化所有四个价格变量,更新最后柱的时间戳,并将 DataFrame 放入队列:

                last_sample_ts = ts
    
                open = high = low = close = last_bid
    
                data_for_chart.put(bars)
    

当然,如果条件不是true(柱子开启的时间没有超过分辨率阈值),我们只需更新价格变量:

        else:
            high = max([high, last_bid])
            low = min([low, last_bid])
            close = last_bid
  1. 诀窍已经完成;现在,让我们创建两个线程:

    data_receiver_thread = threading.Thread(target = LMAX_connect)
    
    data_receiver_thread.start()
    
    trading_algo_thread = threading.Thread(target = make_bars)
    
    trading_algo_thread.start()
    
  2. 创建图形并获取坐标轴句柄:

    fig = mpf.figure()
    
    ax1 = fig.add_subplot(1,1,1)
    

然后,运行绘图循环:

while True:
    chart_data = data_for_chart.get()
    ax1.clear()
    mpf.plot(chart_data, ax = ax1, type='candle', block = False)
    plt.pause(1)

mpf.plot()方法的block = False可选参数告诉渲染器在绘制后释放图表,并允许在其中添加或修改对象(这样我们就可以进行实时更新)。别忘了添加一个暂停(plt.pause(1));否则,循环将一直忙碌,不会让系统在屏幕上显示图表。

如果你运行这段代码,你首先会看到一个巨大的蜡烛图,因为我们还没有足够的数据:

图 8.12 – 实时蜡烛图初始视图

图 8.12 – 实时蜡烛图初始视图

然后,图表将每 10 秒更新一次,4 分钟后,你将看到以下截图所示的内容:

图 8.13 – 使用实时价格数据制作的 10 秒蜡烛图

图 8.13 – 使用实时价格数据制作的 10 秒蜡烛图

  1. 随着我们接收越来越多的数据,图表上会有越来越多的蜡烛,所以到了某个点,它将变得难以阅读。因此,你可能想在将 DataFrame 放入data_for_chart队列之前,添加一个限制并丢弃最老的一行,当新的一行到来时:
            if len(bars) > 100:
                bars = bars.iloc[1:, :]
            data_for_chart.put(bars)

在这里,我指定了显示 100 根蜡烛,一旦达到这个限制,较老的蜡烛将从屏幕上消失——这和在 MetaTrader、MultiCharts、TradeStation 或任何其他图表应用中几乎一样。

非常频繁地,我们需要在价格数据旁边绘制其他内容。这可能是一个技术指标、趋势线、标记一个入场或退场,或者任何其他内容。让我们看看在下一节中我们如何做到这一点。

向价格图表添加对象

如果我们知道它们的坐标,添加任何对象到图表并不困难,因为所有的matplotlib方法总是绘制一个类似于另一个数组对象。所以,基本上,我们添加任何特殊对象到图表所需要做的就是计算它们在列表中的位置,或者沿X轴的数组以及沿Y轴的相应值。

让我们考虑一个简单但很有价值的例子。在第三章,“从开发者角度的 FX 市场概述”中,我们了解到价格接受者只能以要价买入,以出价卖出。我们还看到,大订单可以移动价格几个点(pip),因为它消耗了订单簿中几个级别的流动性。因此,我们可以有相当大的信心假设,如果最佳出价在上一刻突然大于最佳要价,那么这可能是一个重大买入订单的迹象。反之亦然——如果我们观察到最佳要价低于上一最佳出价,那么这可能是一个重大卖出订单的痕迹。

让我们通过添加三角形标记来可视化这两种情况,分别指向上下,以表示假设的买入和卖出。为此,我们将使用我们编写的用于可视化实时 tick 数据的代码(见实时 tick 数据绘图部分),并添加几行:

  1. 首先,我们需要添加两个新的对象来显示标记。我们就在创建显示买卖报价的对象(line1line2)的下方添加它们:

    line3, = ax.plot(buy_signals_x, buy_signals_y, 'g^')
    
    line4, = ax.plot(sell_signals_x, sell_signals_y, 'mv')
    

这里的特殊修饰符表示图形的颜色和样式 – 'g^'表示一个向上的绿色三角形,'mv'表示一个向下的洋红色三角形。你可以在matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html#matplotlib.pyplot.plot找到pyplot.plot()方法的可选参数的完整列表。

  1. 接下来,我们需要计算这些标记的坐标(位置)。我们将在主绘图循环的非常开始处(在while True:之后立即)做这件事。让我们添加四个相应的列表来存储坐标:

    while True:
    
        buy_signals_x = []
    
        buy_signals_y = []
    
        sell_signals_x = []
    
        sell_signals_y = []
    

然后,在最佳买入价大于前一个最佳卖出价或最佳卖出价低于前一个最佳买入价的所有发生处填充它们:

    for i in range(1, window_size):
  1. 注意,我们从 1 开始计数,而不是从 0 开始,因为我们想将列表中的值与bids.data[0]bids.data[-1]进行比较,这实际上是列表中的最后一个元素,而不是前一个元素:

            if bids.data[i] > asks.data[i - 1]:
    
                buy_signals_x.append(i)
    
                buy_signals_y.append(bids.data[i] - 0.0001)
    
            if asks.data[i] < bids.data[i - 1]:
    
                sell_signals_x.append(i)
    
                sell_signals_y.append(asks.data[i] + 0.0001)
    

我们在Y轴上的值中添加了 1pip的边距,这样标记就会稍微远离主图表。

  1. 其余的与绘制买卖报价相同;只需添加对set_xdata()set_ydata()方法的调用:

        line3.set_xdata(buy_signals_x)
    
        line3.set_ydata(buy_signals_y)
    
        line4.set_xdata(sell_signals_x)
    
        line4.set_ydata(sell_signals_y)
    

就这样!如果我们现在运行我们的脚本,我们将看到绿色和洋红色的标记指向假设的大规模买入或卖出的确切位置:

图 8.14 – 添加了价差交叉标记的 tick 图表

图 8.14 – 添加了价差交叉标记的 tick 图表

哇!这个图表显示的是确切的买卖点吗?看起来我们找到了一个优秀的交易策略。现在,唯一剩下的事情就是自动化它,并寻找一个有可靠保险箱的银行来存储赚取的现金?

当然不是。只需看看下面的截图,你就会看到,有时候追逐大钱可能是一种相当令人失望的经历:

图 8.15 – 另一个展示价差交叉的例子 – 价格反转并朝相反方向移动

图 8.15 – 另一个展示价差交叉的例子 – 价格反转并朝相反方向移动

因此,正如我在整本书中一直强调的那样,没有免费的午餐,在交易中也没有圣杯,这意味着仅靠一条神奇的交易规则,不可能在相当长的时间内持续赚钱。相反,我们需要制定一个交易策略,这需要结合交易逻辑(我们何时进入和退出市场以及为什么,这通常包括许多规则)、资金管理(我们交易多少)和风险管理(事情出错时我们做什么,或者最好是预测这种情况并避免交易)。这正是我们在本书剩余部分将要考虑的内容。

摘要

在本章中,我们学习了使用 Python 处理图表的一般原则。现在,我们可以快速找到所需的历史数据部分,并将其绘制成线形图或蜡烛图。我们还学习了如何绘制实时市场数据并实时更新图表。最后,我们学习了如何向价格图表添加自定义图形,并发现跨价差确实可能是一个有价值的潜在交易信号。我们已经准备好可视化任何数据,无论是市场价格还是我们交易算法的表现,因此现在是时候进入交易策略的领域,了解它们是如何以及为什么有效,并为进一步的发展做出正确的选择。这就是我们在下一章将要考虑的内容。

第三部分:订单、交易策略及其表现

在前面的部分中,我们学习了外汇市场,了解了它们的运作方式,并发现了如何避免固有的风险。我们还考虑了大多数交易算法的基本组成部分,这些算法旨在解决该领域的特殊性。

第三部分 通过解释大多数交易策略背后的理念来推进,包括所有时代的经典策略,如趋势跟踪、动量、均值回归,以及更高级的套利和统计套利、市场制作和高频交易。我们还将学习最常见的订单类型、它们执行的典型问题以及减轻相关风险的方法。最后,我们将构建我们的第一个交易应用,对其进行测试,并了解量化交易者中最常见的错误之一。

本部分包括以下章节:

  • 第九章交易策略及其核心要素

  • 第十章订单类型及其在 Python 中的模拟

  • 第十一章回测和理论表现

第九章:交易策略及其核心要素

在前面的章节中,我们从两个角度考虑了算法交易algo)和系统化交易:我们学习了市场本身、其参与者、其运作方式以及所有这些如何反映在定价上;另一方面,我们在编程方面做了一些准备工作,因此现在我们可以检索和处理市场数据,构建技术指标,并进行一些图表和绘图。换句话说,我们已经拥有了未来交易应用身体的灵魂和骨架,现在是时候添加大脑和四肢了:生成订单的交易逻辑,以及最终将应用与最终目的地——交易所、经纪商或电子通信网络ECN)连接的订单执行控制机制。

在本章中,我们将考虑通常用于交易外汇市场的最重要的交易策略类别。我们将了解盈利来源,考虑典型的交易想法及其实际应用,并了解它们的技术要求。

注意,我们不会开发实现所有这些类型策略的实际代码。其中一些可能仅对机构交易员可用,而另一些则需要复杂且昂贵的基础设施,例如直接与交易所或 ECN 服务器放置的交易服务器,特殊硬件如现场可编程门阵列FPGAs)等。我们在这本书中提到它们有两个原因。首先,了解算法交易的替代方法肯定是有用的。其次,你永远不知道未来你会在哪里:例如,作为银行交易员或对冲基金的研究员,在这种情况下,这种知识,即使是非常表面的,也将非常有帮助。

在本章中,你将学习以下主题:

  • 阿尔法和贝塔作为盈利来源

  • 期权定价——最消耗科学的风险模型

  • 阿尔法经典——趋势跟踪、均值回归和突破

  • 套利——让我们从他人的错误中获利

  • 统计套利

  • 事件驱动交易策略

  • 做市商——从提供流动性和相关风险中获利

  • 高频,低延迟——Python 失败的地方

阿尔法与贝塔——广泛使用,广泛混淆

如果你曾经阅读过任何关于算法交易或系统化交易的文章或书籍,或者只是曾经听过 CNBC,你很可能已经听说过阿尔法贝塔生成程序(策略、系统、基金等)。正如经常使用的大量术语一样,使用它们的人并不一定真正理解它们的含义。让我们来澄清这个术语,因为理解它将真正有助于整理出千变万化的交易策略。

阿尔法——从价格变动中获利

根据 alpha(α)的一般定义(例如,您可以在 InvestmentU 上找到,见investmentu.com/what-is-alpha-investing/),alpha(α)“是衡量投资价值的一种特定测量,基于其相对于市场的表现。具体来说,alpha 衡量的是投资超越市场回报*的能力。””

听起来熟悉吗?如果您对市场和交易感兴趣,很可能是的(否则,您为什么会读这本书?)

但这清楚易懂吗?不,因为这个定义用一个未知术语(alpha)替换了另一个(beat the market,这通常也被称为优势)。

文章继续说“alpha 等同于异常回报率。”为什么异常?因为有效市场理论声称没有一种方法可以系统地超越市场,因此任何超额回报都可以被认为是异常的。这更有意义,因为我们现在理解 alpha 是一个比较策略、投资组合或投资回报与基准(在上一篇文章中称为广泛市场)的指标。

用更简单的语言来说,有一个基准和交易策略的回报。如果后者大于前者,那么该策略就产生了 alpha。

简单吗?是的,但我们必须提出两个重要的观点。

首先,在这里必须注意的是,通过超越市场,alpha 方法仅考虑对基础资产的操纵,例如卖空、退出头寸并在不同价格重新进入、改变投资组合中资产的比例等等。

注意

换句话说,产生 alpha 通常意味着对资产本身的积极操纵,而不使用衍生品或卖方活动,如做市。

第二,有一个经常被忽视的问题:我们确实需要那个我们将要比较交易策略回报的基准。

当我们谈论投资时,基准是显而易见的:一个股票指数。这类最受欢迎的基准之一是标准普尔 500 指数——500 家大型美国公司的指数。您可以从这些公司中购买股票,并通过持有它们来跟踪指数,例如,持有 1 年。或者,您可以选择购买与指数比例不同的股票,购买不同的股票,卖出一些股票并用其他股票替换,或者进行任何其他积极投资——但当你比较你在那一年的回报与指数回报时,并且如果您通过操纵市场敞口比指数本身做得更好,那么您可以说您产生了 alpha*。

为什么这种方法在股票投资中合理?

如果我们审视标准普尔 500 指数的整个历史,我们可以立即看出美国股市总是随着时间的推移而增长。是的,有经济衰退,甚至还有大萧条,但在非常长的时期内,如果你从这个指数中的所有公司购买股票并等待超过 30 年,那么你几乎可以肯定能获得利润。

不要轻信我的话:检查任何标准普尔 500 指数历史数据的来源,你自己就会看到。以下是由雅虎财经提供的半对数图表(你自己去yhoo.it/3U6I5rm查看):

图 9.1 – 标准普尔 500 指数的历史数据显示美国股市有很强的增长趋势

图 9.1 – 标准普尔 500 指数的历史数据显示,美国股市有很强的增长趋势

在外汇交易中,情况更为复杂和令人困惑,因为货币通常不作为长期投资工具使用——至少不像股票那样广泛。原因是货币具有利率,这使得它们与传统投资资产(参见第六章中的[“经济新闻”]部分,讨论[“基本分析及其在 FX 交易中的可能用途”],关于[“息差交易”]的讨论)有根本性的不同,而这些利率会根据中央银行的决定而随时间变化。因此,当你购买并持有股票时,通常能带来股息,或者在最坏的情况下,如果董事会决定今年不支付股息,你将一无所获。然而,如果你购买并持有货币,你很可能要为在投资组合中拥有这种货币的奢侈付费——如果一对货币之间的利率差异为负。

将 S&P 500 作为基准的使用和滥用

由于标准普尔 500 指数的巨大普及度,它经常被用作各种事物的基准,而不仅仅是股票投资。我甚至目睹过将这个指数与高频交易基金的业绩进行比较。当然,这与 alpha、beta 和其他指标的传统意义毫无共同之处。

因此,很明显,我们需要一个适合外汇市场特定情况的基准。那么,我们可以用什么呢?

外汇交易策略的最简单基准可能是货币本身。

将汇率作为基准

我们可以使用与股票指数完全相同的方法:比较交易策略的回报与同一给定时期内货币本身的回报。例如,如果我们查看 2021 年 1 月至 2022 年 1 月的历史汇率 EURUSD,我们可以看到它们从大约 1.2240 下降到大约 1.1290,下降了 8%。所以,如果你在 2021 年初购买了欧元并持有到同年年底,那么你的“投资”将亏损大约 8%(实际上,由于利率差异,亏损可能更大)。但如果你在利率下降时积极管理你的投资,关闭多头头寸并开仓空头头寸,你可能只会亏损大约 5%。从技术上讲,你甚至可以说你“打败了市场”,尽管从实际角度来看,这样的投资几乎没有什么意义。

外汇交易的好消息是你可以轻松地买卖货币——这与股票不同,卖空你并不拥有的股票(从价格下跌中获利)可能存在问题。因此,在外汇交易中产生 alpha 的最合理方式是根据市场价格走势的方向改变你的头寸方向。

但如果你交易的不是单一货币对,而是一篮子货币呢?在这种情况下,有没有可以用来评估 alpha 的基准?

是的,这类最被认可的基准之一就是美元指数USDX)。

常见的外汇基准——美元指数

美元指数(代号 DXY)是“美元相对于一篮子其他货币的表现的实时衡量指标”(参见 Forex.com,www.forex.com/en-us/news-and-analysis/what-is-the-dollar-index/)。如果我们从投资的角度来解释它,那么它与股票投资非常相似,但不同的是,你购买或出售的是美元相对于一篮子货币(目前包括欧元、英镑、瑞士法郎、加拿大元、日元和瑞典克朗)。

例如,如果 DXY 在年初的价值是 100,而到了年底,其价值是 110,那么这相当于 10%的收益。如果你在年初用这篮子货币购买了美元,那么你的投资收益将是 10%——同样,这是相对于同一篮子外国货币而言的。

美元指数对于评估外汇交易策略和投资组合非常有用,因为它已经消除了利率的影响,我们可以说它以最纯粹的形式呈现了外汇市场的潜在 alpha 值。

如果我们查看以下图表中美元指数的历史值,我们可以看到,它与标准普尔 500 指数截然不同,不能用作长期投资——这与任何单一货币的情况相同:

图 9.2 – USDX 的历史图表(由 TradingView 提供)

图 9.2 – USDX 的历史图表(由 TradingView 提供)

一般而言,如果你通过操作其中的货币(买入、卖出或调整篮子中的比例)超越这个指数,你相对于这个指数就产生了 alpha。

USDX 并非唯一的 FX 指数。许多大型机构提供他们自己的指数,旨在评估 FX 市场的各个部分或甚至各种交易或投资方法。让我们看看一些由最重要的国际银行提供的指数,因为这些指数不仅经常被用作被动投资的基准,也常被用作主动交易的基准。

银行指数 – 更多细节,更多困惑

这些指数有时仅起指示性作用;而另一些则是可交易工具。例如,德意志银行列出了许多 FX 指数(index.db.com/dbiqweb2/home.do?redirect=productpagelist&region=All&regionHidden=All&assetClass=FX&assetClassHidden=FX&returnStream=ALL&returnStreamHidden=ALL)。其中一些代表经典的买入并持有策略,而另一些则追踪德意志银行的自身主动投资。因此,如果你开发了一个产生 alpha 的策略,那么与这些指数之一进行比较更有意义。让我们考虑一个例子。

假设你开发了一个篮子交易策略,该策略购买低估的货币并卖出高估的货币。那么,将你的策略的表现与德意志银行 FX 动量美元指数(index.db.com/dbiqweb2/servlet/indexsummary?redirect=benchmarkIndexSummary&indexid=99800323&currencyreturntype=USD-Local&rebalperiod=2&pricegroup=STD&history=4&reportingfrequency=1&returncategory=ER&indexStartDate=20191103&priceDate=20221103&isnew=true)进行比较是有意义的,如果你的策略在较低波动性的情况下提供了更高的回报,那么太好了 – 你通过你的策略创造了 alpha:

图 9.3 – 德意志银行 FX 动量美元指数图表

图 9.3 – 德意志银行 FX 动量美元指数图表

总体来说,货币汇率并不倾向于随着时间的推移持续增长。这就是为什么使用单一货币或货币篮子的汇率作为基准,并不像使用股票指数作为投资基准那样有意义。

让我们总结一下。在 FX 交易的世界里,通过生成 alpha,我们假设我们将通过积极操纵投资组合中的货币,超越任何相关的 FX 指数——包括单一货币的买入并持有回报。从这个意义上说,几乎所有买方策略都属于这一类别(参见第一章,“开发交易策略——为什么它们不同”,以刷新你对买方和卖方市场参与者的记忆)。这正是本书将进一步考虑的策略类型。

主要收获

以最简单的术语来说,在 FX 交易中,我们所说的 alpha 是指无论价格上涨还是下跌,都能从中获利。因此,对于生成 alpha 的策略来说,最糟糕的市场就是价格没有足够波动的市场。

但这真的是一个问题吗?这些市场存在吗——那些价格没有实质性变化的市场?

当然,它们确实会。任何 FX 市场都可能或多或少地变得波动,这主要归功于流动性的变化,有时也归功于中央银行施加的特殊条件。例如,让我们看看 2012 年 4 月至 9 月欧元对瑞士法郎的图表,当时瑞士国家银行SNB)决定人为地将瑞士法郎的汇率与欧元挂钩:

图 9.4 – EURCHF 历史报价图表(来源:TradingView)

图 9.4 – EURCHF 历史报价图表(来源:TradingView)

我们可以看到,这个货币对在近 5 个月内仅在 26 个基点的狭窄范围内交易!仅作比较,截至 2022 年 11 月,同一货币对的平均每日波动范围为 60 个基点。显然,在这样的稳定市场中几乎不可能产生任何 alpha。

但有没有一种方法可以在价格没有变动的情况下仍然获利?

答案是肯定的,现在进入戏剧的第二个人物:beta。

Beta – 从波动性中获利

beta 的概念也起源于股票投资。它指的是投资组合实现的回报波动性——并且,就像 alpha 的情况一样,与基准相比。当然,在股票交易的情况下,这通常也是一个股票指数,S&P 500 再次是最常见的例子。

我们已经在第七章“波动性指标”部分讨论了市场波动性。投资或交易策略的回报波动性可以通过或多或少相似的方式进行研究。在投资和交易中,beta 是衡量你为了实现预期利润可能承担的损失风险的指标。

为了更好地理解回报的波动性,让我们考虑旅行的例子。比如说,你想从伦敦到纽约。从起点到达目标最直接的方式是乘坐直飞航班。这条路线将是我们的基准。你也可以享受一个两周的海上之旅,这将慢得多,也不那么直接。否则,你可以开车去法国,然后乘坐火车和公共汽车去挪威,然后乘船去冰岛,然后乘飞机去加拿大,然后从那里开车、乘火车或公共汽车去纽约。

你看出了区别吗?在所有三种情况下,你都是从起点到达目标,但你的路径波动性会有很大的不同。

返还情况也是如此:让我们绘制两种交易策略(或投资组合,这并不重要)随时间推移的假设回报。其中一种策略的波动性会小一些,另一种则会更大:

图 9.5 – 同样的起点和目的地,不同波动性的公路旅行或投资

很明显,右侧图表的回报波动性大于左侧图表,任何正常的投资者或交易者都会更喜欢后者。在风险度量语言中,我们说右侧图表的 beta 大于左侧图表的 beta

如果我们现在从上一节相同的视角来看外汇市场,我们可以看到,就像 alpha 一样,评估投资与基准之间的差异是很困难的,因为没有人像股票那样投资货币。所以在外汇交易中,beta 可以解释为市场的波动性本身,如果一个策略从这种波动性中获利,那么我们就说这是一个产生 beta 的策略。

我们如何从波动性中获利,而不是从价格变动中获利呢?

这可以通过使用最复杂但也是最强大的金融工具之一——期权来实现。

期权——稳定的收入与无限的风险

这个副标题听起来很荒谬,不是吗?稳定的收入怎么可能和无限的风险并存呢?

要理解它,我们首先需要了解期权是什么,以及如何进行交易。

期权是一种衍生品(参见第一章开发交易策略 – 为什么它们不同,其中对标的资产和衍生品进行了简要解释),它赋予其持有者在未来以特定价格买卖标的资产的权利,但没有义务。

我知道一开始理解起来真的很困难,所以让我们考虑一个例子。比如说,现在是十月,一公斤苹果的价格是 1 美元。我认为到十二月,它的价格会增长到 2 美元,但另一个市场参与者认为,即使价格上涨,也不会超过 1.5 美元。所以,这个市场参与者写了一个在十二月以 1.5 美元购买苹果的期权,并将这个期权以溢价卖给我。

现在,如果我是错的,12 月份一公斤苹果的价格确实低于 1.5 美元,那么我会损失我支付给对方的溢价金额,而对方将获得(正是这个溢价)利润,但如果是我的判断正确,一公斤苹果的价格是 2 美元或更高,那么我就实现了以 1.5 美元购买苹果的权利,并立即以 2 美元的价格卖出,从而获利。而这个利润显然是由卖给我这种权利的对方承担的。

如你所见,期权卖方纯粹从波动性中获利:对他们来说,价格走向真的无关紧要;只要波动性不超过某个水平,他们就能获利。

你能在这里看到问题吗?

对于期权卖方或写作者来说,利润总是固定的,等于溢价,但潜在损失是无限的。在我们的例子中,如果苹果的价格是 20 美元而不是 2 美元会怎样?这种意外和急剧的价格上涨可能会对期权卖方造成重大损失。

为了说明通过卖出期权赚钱的可能性,让我们回顾一下上一节中提到的 EURCHF 的例子。如果市场在一个狭窄的范围内交易,就有可能卖出购买该范围之上的期权(它们被称为看涨期权)和卖出该范围之下的期权(它们被称为看跌期权),并收取溢价,这可以作为稳定的收入。

但我们都知道,没有无风险的交易,如果市场突然超出范围(坦白说,这不是一个“是否”的问题,而是一个“何时”的问题),期权卖方可能会遭受严重的损失。

让我们看看同样的 EURCHF 是如何实际发生的。瑞士国民银行(SNB)维持了法郎与欧元的汇率挂钩,这导致市场交易在一个狭窄的范围内,正如我们之前看到的。但有一天——2015 年 1 月 15 日——SNB 决定将法郎从欧元中脱钩。结果,EURCHF 在瞬间下跌超过 20 个点(一个点等于 100 个基点)或几乎 20%,卖出看跌期权的期权卖方(看跌期权是卖出特定价格的权利)遭受了难以置信的损失,因为他们没有机会清算他们大量亏损的头寸。图 9.6展示了这一价格跳跃(或者说,更确切地说,是一次小行星般的下跌),你可以看到波动性在几分钟内超过了任何可想象和可信的极限:

图 9.6 – SNB 决定将法郎从欧元中脱钩后 EURCHF 的急剧下跌

图 9.6 – SNB 决定将法郎从欧元中脱钩后 EURCHF 的急剧下跌

总的来说,尽管不是无风险的,但无论价格的实际走势方向如何,都有可能在波动性上进行交易,这类策略被称为产生β的策略。

通常,期权交易者使用各种波动率模型来帮助计算安全的价格水平和撰写期权的适当溢价。这种方法被称为期权定价,它是算法交易中最科学密集的领域之一。我们不会在本书中考虑它,因为它需要相当高级的特定数学领域的知识。

现在我们已经熟悉了生成 alpha 和 beta 的想法,让我们考虑一些最受欢迎的 alpha 生成策略。

Alpha 经典策略 – 趋势跟踪、均值回归、突破和动量

让我们快速回顾一下生成 alpha 的想法:我们希望通过积极管理市场中的头寸来打败市场或表现优于指数(或在外汇交易中只是利率本身)。这意味着我们试图在预期价格上升时买入,在预期价格下降时卖出。

因此,为了成功生成 alpha,我们基本上只有两个选择:要么我们假设价格将继续按照已经确立的方向移动,要么我们预测一个变化。在前一种情况下,我们试图在价格上升时买入,在价格下降时卖出。在后一种情况下,我们试图在价格下降时买入,在价格上升时卖出。

仅供专业人士参考

在这里的所有讨论和例子中,我故意没有深入数学。我们专注于这些现象的定性方面,以便更好地理解它们的本质,我们将在本书的后面部分考虑一些描述这些现象的具体数学模型。我还故意留下了一些市场模型和交易策略没有实际应用,因为它们太复杂,无法在一个章节内考虑。

既然我们已经了解了我们的(某种程度上有限)选择,我们将详细考虑每个选项。让我们从一个试图加入已经确立的价格走势并保持其走势的经典策略开始。

趋势跟踪

这可能是迄今为止最著名的交易策略。它起源于股票市场,正如我们所知,股票市场往往会随着时间的推移而持续增长。因此,在交易股票时,跟随整体上涨趋势进行买入并持有确实是合理的。

有趣的是,交易中的趋势是那些没有严格正式定义的术语之一。一些作者将趋势定义为市场价格中一系列更高的高点和更高的低点,或者相反——更低的高点和更低的低点,但这样的定义是模糊的,因为它忽略了波动性(参见Beta – 从波动性中获利部分中的例子)。我们可以这样说,如果以下情况在给定时间段内适用,则市场处于趋势中:

  • 价格发生显著变化(即,观察区间开始和结束之间的价格差异的绝对值大于平均值)

  • 市场贝塔值低 – 即波动性小于价格变化

  • 观察的时间间隔相当大:其持续时间比单个样本的持续时间大几个数量级(例如,如果数据分辨率为 1 天,那么我们可以考虑 50-200 天的趋势时间跨度)

让我们看看几个例子。这些图表说明了趋势(在右侧图表中)和非趋势(在左侧图表中)市场:

图片

图 9.7 – 根据基于贝塔的定义(Multicharts 图表)的趋势(右侧)和非趋势(左侧)市场

此图清楚地显示了经典趋势定义的不一致性(更高的高点和更低低点或更低的高点和更低低点)。在两个图表中,价格都存在一定的下降趋势。在两个图表中,我们都可以看到一系列更低的高点和更低的低点。然而,考虑到贝塔值,可以清楚地看出左侧图表代表非趋势市场,而右侧图表描绘的是趋势市场。

如果我们将波动性计算为标准差,并在中间回归线之上和之下绘制,我们可以看到左侧图表中产生的回归通道宽度更大,而价格本身在右侧图表中的变化绝对值更大。为了简化分析,我们在每个图表下方添加了一个指标,该指标绘制了考虑期间标准差的最大值 – 因此,该线代表最新的实际波动值。您可以看到,在右侧图表中,alpha 值更大,beta 值低于左侧图表,因此我们可以说,根据我们的定义,右侧图表代表一个真正的趋势。

如果我们能够识别出趋势,最好是开始时,加入这个长期的价格运动,并在它反转之前退出,那么我们有可能产生 alpha。当然,由于我们是系统交易,我们需要一个技术交易设置,一套数学公式,以一定程度的概率确定趋势或非趋势市场。我故意现在不深入数学;本章的目的只是让你熟悉这些概念。有一些经典的技术设置用于识别趋势,大多基于移动平均线(见第七章技术分析和其在 Python 中的实现)和线性回归。我们将在第十二章样本策略 – 趋势跟踪中看到这种技术的实现。

均值回归

这种交易策略背后的想法最初在股票交易中再次出现。尽管从长远来看,市场总是倾向于增长,但在短期内,我们总能看到市场价格的起伏。因此,我们可以假设存在一个随时间增长的平均价格,并将偏差视为暂时的,因此价格最终会回到那个均值值。

从这个角度来看,即使是大萧条也只是一个偏差,事实上,如果你在 20 世纪 30 年代足够大胆地买入美国股票,并且活的时间足够长,直到 1950 年持有它们,那么你很容易就能使你的财富翻倍或三倍,至少是其名义价值(如果你不考虑通货膨胀,那么你可能会非常惊讶地发现,你不能用三倍的钱购买三倍数量的商品)。

因此,均值回归的关键在于观察的时间间隔。如果我们考虑一个较短的时间跨度,比如说,1 年或更少,我们可以在那里看到任何类型的市场效应:上升趋势(或牛市)、区间波动、平缓或下降趋势(或熊市)。然而,从长期角度来看,似乎几乎所有熊市都只是从不断增长的假设均值值中的一次暂时回调。

正如我们已知的那样,货币市场并不像股票市场那样表现出相同的长期上涨倾向,因此不可能简单地购买每一次显著的回调,并在 10-20 年内保持正面。相反,我们有一个优势:我们可以轻松地买卖任何货币与任何其他货币。因此,货币市场的均值回归概念略有不同:如果我们能识别当前价格与理论均值相差太远,那么我们就进入市场,朝着这个均值的方向进行交易,也就是说,与价格运动当前方向相反。

均值回归也经常被称为逆势交易(或者更准确地说,逆势交易是均值回归的一种变体),因为在逆势交易中,我们不仅买入或卖出,然后长时间持有头寸,而是相对于主要价格运动的方向进入市场进行相对较短时间的交易。以下图示了在相当稳定的趋势中进行的可能的逆势交易:

图 9.8 – 在长期趋势期间假设的逆势交易(由 Multicharts 图表)

图 9.8 – 在长期趋势期间假设的逆势交易(由 Multicharts 图表)

在这个图表中,大的灰色向下箭头标记了潜在的逆势入场点(即我们能够卖出英镑并买入日元的时刻),细黑线标记了如果我们保持逆势头寸这么长时间可能实现的潜在利润(假设我们在每条线的末端退出)。正如你所看到的,逆势交易的时间比趋势跟踪短得多,而且它们出现的频率也更高。

你可能会问:如果我们可以在 2020 年 9 月买入 GBPJPY 并在 2021 年 5 月卖出,为什么还要进行逆势交易?

这有两个严重的原因。

首先,货币市场的趋势相当罕见。趋势跟踪策略可能在 3 年内只进行 4-6 次交易,而且不能保证所有这些交易都会盈利(就像任何系统交易一样)。

其次,我们保持头寸的时间越长,市场最终与我们作对的风险就越大。除此之外,趋势跟踪头寸的回报波动性(回报的 beta 值)将很高,因为它会复制市场的波动性。因此,放置多个逆势交易,这些交易在市场上停留的时间相对较短,可能会提高整体策略表现的 beta 值。

货币市场还有一个非常有趣的特点:时间跨度越短,市场越具有均值回归性。使用每日数据,虽然数量不多,但可以找到真正长期的趋势。如果你考虑以 4 小时为间隔采样市场数据(你可以在第五章数据压缩 – 保持数量在合理最低值部分中刷新你对市场数据采样的记忆),那么识别可交易趋势就已经变得有难度了:在每日数据上有效的方法会产生大量虚假趋势。如果你将数据压缩到 1 小时、1 分钟或更短,你会发现趋势与非趋势市场阶段的持续时间比例会大幅下降,以至于均值回归几乎成为唯一可以成功使用的交易技术。

为什么会发生这样的事情?

在货币市场中,导致趋势跟踪在较短时间内失效的关键因素之一是它们都表现出强烈的日内周期性。这种周期性是由市场的性质决定的:它每周开放 24 小时,5 天,但市场参与者的活动在一天中的不同时间差异很大。

中午时分(从现在开始假设我们处于 GMT 时区)具有更高的流动性和更大的交易量,同样,在伦敦和纽约的工作时间内也会发布同等重要的经济新闻。夜间总是具有较低的流动性和较低的交易量,除了亚太地区外,没有重要的经济新闻。因此,日内价格波动不可避免地表现出强烈的周期性,这使得识别可交易的日内趋势相当困难。在图 9.9中,你可以看到波动性(仅指每个柱子的范围,即柱子的高点和低点之间的差异)的最简单表示,很明显,在 FX 市场中日内周期性非常强:

图 9.9 – 日内(1 分钟)价格图表和波动性(由 Multicharts 图表)

图 9.9 – 日内(1 分钟)价格图表和波动性(由 Multicharts 图表)

在这个图表中,我们可以清楚地看到(从左到右)看起来像趋势的东西,但它只持续了 2 天,之后转变为横向(区间)市场。同时,波动性(在价格图表下方简单地表示为每个柱子的高低差)保持这种强烈的周期性,如果你仔细观察图表,你会看到每天,无一例外,我们都有价格缓慢变化和快速变化的时期。因此,这个因素显著地影响了货币市场的整体日内价格行为,比趋势更倾向于均值回归。

与趋势跟踪一样,我们不会深入讨论特定交易设置来交易均值回归。通常,技术指标如 RSI 或随机振荡器被用来确定价格变动的方向以及超买超卖区域,然后一旦价格进入这样的区域,就会进行与该变动相反的交易。在第十四章“现在去哪里?”中,我将提供一些关于这些技术设置的链接,您可以在自己的策略中使用。

市场活动中突出的日内周期性应该让我们思考:有没有一种方法可以系统地交易这个过程?答案是肯定的,尽管不是直接也不是在所有市场中。让我们看看突破交易技术。

突破

趋势跟踪和均值回归都是试图交易价格本身的变化。突破是另一种不同的生物:它是试图交易波动性的变化。

突破背后的想法基于市场在狭窄范围内交易和突然出现剧烈价格波动的周期交替的假设。在波动率的语言中,这意味着低波动率期最终会转变为高波动率期。因此,突破市场模型假设价格波动只由这两个阶段组成:低波动和高波动,如果我们能识别前者,那么我们就可以在价格一旦离开其“舒适区”并加入新建立的价格运动时立即进入市场。在突破交易中,我们不在乎趋势、均值回归或任何其他因素;我们只关注从低波动率到高波动率的相对短期变化过程。

下图说明了典型的突破设置。我们可以看到,从 11:30 到大约 13:30,GBPJPY 在狭窄的范围内交易(在价格图表上由两条水平线表示),波动率也很低(低于下方的波动率图表上的水平线表示的平均值)。然后,发生了某种情况;价格离开了范围,并稳步上升,距离是原始范围宽度的三倍。波动率也增加了,并在整个运动过程中保持在平均值之上。

图 9.10 – 典型的突破日内交易设置(图表由 Multicharts 提供)

图 9.10 – 典型的突破日内交易设置(图表由 Multicharts 提供)

虽然突破交易看起来非常吸引人,对于日内交易来说,由于日内低波动和高波动期的内在交替,这似乎非常自然,但它并不是交易的圣杯——就像任何交易技术一样。我故意向您展示了一个理想的突破图表,但在现实中,这种情况在所有外汇市场中都相当罕见。此外,突破设置的界定非常模糊,具有许多自由度。

事实上,我们说价格首先应该在狭窄的范围内交易,但如何定义狭窄呢?然后,我们说价格应该离开这个范围,波动率也应该超过其平均值,即使在这样一个理想化的例子中,我们也可以看到许多假突破——当波动率超过平均值时,但价格却没有移动。

因此,为了利用突破机会,我们需要有一个稳健的正式数学描述,即什么是狭窄范围价格离开范围意味着什么,以及我们如何认为波动率是大于平均的。

现在,我们已经了解了最流行的 alpha 生成交易策略,这些策略就是我们接下来要深入探讨的内容。不幸的是,在一本书中不可能对各种交易策略进行同等详细的讨论(否则它将变成一部多卷本的百科全书!),但我们将会快速浏览它们,以便你知道系统性和算法交易的世界是极其广泛和多样化的。也许这会激发你对算法交易其他应用的兴趣,或者——谁知道呢?——也许这会帮助你实现金融职业。

套利——让我们从他人的错误中获利

在金融市场上的套利意味着利用同一资产在不同交易场所价格不同的情况。这种情况通常被称为定价错误(这个术语还有其他含义,我们将在下一节关于统计套利的下一节中回到这个话题)。由于外汇市场的巨大碎片化(参见第三章从开发者角度的外汇市场概述),定价错误并不少见,因此套利策略看起来相当直接:一旦我们看到,比如说,EURUSD 在 LMAX 的价格是 1.00012,而在 IS Prime 的价格是 1.00013,那么我们就同时在 LMAX 买入,在 IS Prime 卖出,赚取十分之一的点差。

我想你可以清楚地看到套利中存在的一些问题,这些问题直接源于其描述。

首先,单次交易的潜在利润非常小,因此您必须进行大量的交易才能持续盈利。别忘了交易成本:佣金、交易服务器维护费等等。您的交易应该覆盖所有这些成本;否则,您只会实现交易者所说的纸上利润

经典套利的第二个(也是主要问题)是套利者的数量——他们行动如此迅速,以至于在您的订单执行之前价格可能已经改变。然后,您有两个选择:要么等待希望价格回到交易有利可图的水平,要么以损失了结您的头寸。从这个意义上说,套利的风险类型与市场做市商的风险类型有些相似(参见市场做市商——舒适、复杂、昂贵部分第三章从开发者角度的外汇市场概述),尽管前者是生成 alpha 的买方交易活动,而后者是赚取 beta 的卖方交易活动。

外汇市场还特有一种独特的套利形式,被称为三角形套利。这种套利之所以存在,仅仅是因为我们不是用货币买卖资产,而是用货币相互交易。因此,我们可以在所谓的合成货币对中寻找定价错误的机会。让我们来看一个例子。

假设 EURUSD 的汇率为 0.99745。同时,USDJPY 的汇率为 146.336。如果我们使用这两个汇率来计算欧元对日元的汇率,我们将得到 EURJPY 的汇率为 145.963,但当我们查看实际市场时,我们发现特定交易场所 EURJPY 的实际汇率为 145.972。实际货币对(EURJPY)与我们的合成计算汇率之间的差异为 0.9 个点。因此,我们立即卖出 USDJPY(因此,我们拥有 JPY 并卖出了 USD),以及 EURUSD(因此,我们拥有 USD 并卖出了 EUR),这有效地在 EURJPY 上创造了一个空头头寸。同时,我们从被错误定价的汇率处买入 EURJPY,从中获利 0.9 个点。

当然,三角形套利容易受到与普通套利相同的疾病的影响:速度至关重要,潜在损失总是远大于潜在利润。因此,通常,套利是金融机构的领域,我们在这里只是简要地讨论了它,以便你知道它是如何工作的(并且让我再次重复,谁知道你明年会在哪里工作?)

统计套利

如前所述,套利基于错误定价的想法:资产被错误定价的情况。但要说某物定价是否正确,我们需要一个已知定价正确的参考,不是吗?

在经典套利中,这样的参考是资产价格本身,我们利用同一资产在不同交易场所之间的错误定价。统计套利stat arb)使用公允价值的概念来确定资产是否被错误定价。简单来说,在经典套利中,我们比较同一时刻存在的资产价格与另一资产价格。在统计套利中,我们比较资产价格与一个理论上的公允价值,我们期望价格在未来会回归到这个价值。

在某种意义上,统计套利是对均值回归概念的修改或扩展。确实,一个成功的均值回归策略是基于对各种价格-量-波动性设置的统计分析,假设如果价格偏离某个均值太远,那么它迟早会回归到这个均值。统计套利将均值替换为公允价值,这可以从多个因素中得出,从资金流动到政治局势。

统计套利最流行的应用之一是衍生品交易。我们已经在第三章交易机制 – 再次一些术语部分中简要提到了衍生品,从开发者的角度来看的 FX 市场概述。让我们更深入地探讨这个复杂但非常有趣的领域。

简单来说,衍生品是一种金融工具,它赋予其所有者购买或出售资产的权利,或者在某些日期以特定价格购买或出售资产的责任。注意衍生品的这两个强制性属性:在特定日期以特定价格。这使得衍生品的价格与资产(称为标的资产)不同——因为它们都有到期日,即最终结算应进行的日期。

如果你不太熟悉衍生品,这确实听起来很复杂,但实际上却相当简单。让我们来看一个例子。

假设今天是 1 月 1 日,EURUSD 的当前价格为 1.00000。我认为价格将在 3 月的第三个星期五增长到 1.02000。你认为它不会超过 1.01000。因此,你卖出期货合约,即以 1.01000 的价格出售欧元的义务,而我购买了这份合约,对我来说,这意味着在相同的日期——3 月的第三个星期五——从你那里购买欧元的义务。这个日期被称为期货合约的到期日,到期日相互买卖的过程称为结算

我认为在到期日我们的头寸可能发生什么应该是很清楚的。如果我说对了,欧元的价格确实上涨到了 1.02000,那么我将以 1.01000 的价格从你那里购买欧元,并立即以 1.02000 的价格卖出,这意味着我赚了 100 个点。随后,你必须以 1.01000 的价格将欧元卖给我,考虑到其市场价格现在是 1.02000,你将损失 100 个点。

到期日显著影响期货合约的价格。通常,到期日越远,与今天的价格或现货价格相比,相应的期货合约价格差异就越大。以下图表说明了这种差异:

图 9.11 – 不同到期日的 CME 外汇欧元期货价格(来源:CME)

图 9.11 – 不同到期日的 CME 外汇欧元期货价格(来源:CME)

你可以看到,到期日越远,相应的期货合约价格就越高。这意味着在这个快照时刻的市场整体情绪是看涨的——大多数交易者认为欧元将会随着时间的推移而增长。

当然,这种情绪不能直接交易——因为标的资产的价格可能会下跌,然后期货合约的价格也会下降。然而,价格差异为统计套利打开了大门:我们可以假设最终——到到期日——期货的价格应该回归到公平价值,这将等于欧元本身的现货价格。因此,购买到期日较早的合约并卖出到期日较晚的合约,或者相反,可以在衍生品市场之间进行一种统计套利。

通常,统计套利设置包括数百个市场,有时甚至数千个,寻找统计上确认的潜在定价错误(潜在——因为它们不会在某一时刻存在,但可能在将来实现)。我们不会详细考虑统计套利,因为这个领域如此之大,值得有一本单独的书来讨论。

然而,与经典套利不同,统计套利对任何交易者(只要有足够的资金来承担数十个同时开放的头寸)都是可用的,所以如果你对它感兴趣并想了解更多,我建议从一些数学知识开始,即学习关于相关性(en.wikipedia.org/wiki/Correlation)和协整(en.wikipedia.org/wiki/Cointegration),然后阅读 Sabir Jana,CFA 写的一篇简短但信息量很大的文章(medium.com/analytics-vidhya/statistical-arbitrage-with-pairs-trading-and-backtesting-ec657b25a368),其中作者解释了使用股票构建统计套利设置的基本原理——但同样的方法也可以应用于货币及其衍生品。

到目前为止,我们已经考虑了基于价格、成交量以及波动率等不同设置的 alpha 生成策略。然而,存在一类策略根本不考虑这种市场信息,或者至少部分不考虑。这些是事件驱动策略——而且不要把它们与应用程序的事件驱动架构混淆!

事件驱动交易策略

事件驱动策略主要依赖于非市场数据,如经济或政治新闻。我们已经考虑了这些事件对市场价格的影响(参见第六章基本面分析的基础及其在 FX 交易中的可能用途)。因此,事件驱动策略可以在重大新闻冲击市场时尝试进入,并在之后不久退出。

同一章节中也详细考虑了这类策略的问题:由于新闻发布时流动性不足,价格可能在任意距离上几乎向任何方向跳跃,因此你几乎没有机会在期望的价格上成交。同时,流动性的回归可能会将价格推向相反方向,几分钟内完全消除盈利的可能性(再次参见第六章中的例子,涉及英镑和英国 GDP 数据的发布)。

我可以确认,在 2000 年代初,曾经存在过盈利的新闻交易员,但自 2010 年以来,要使这种交易持续下去确实非常困难。因此,我们不会在我们的策略中出于任何目的使用非价格信息,除了可能是在潜在的高影响新闻发布之前停止策略的执行。

做市——从流动性提供和相关风险中获利

我们已经在第三章的“做市商——舒适、复杂、昂贵”部分详细考虑了做市,所以在这里没有必要重复。我们在这里仅为了保持一致性,将其作为一个卖方交易策略的例子提及。如果我们想要将做市归类为与 alpha 或 beta 生成策略相关,我们可能可以将它归类为 beta 生成策略。然而,与此同时,高 beta 值对做市是有害的。一般来说,做市要求交易者不仅要有足够的资金,还要满足各种监管要求,这使得这项活动主要对机构开放。

做市需要不仅能够直接访问订单簿,并有能力在那里更新最佳买入价和卖出价,而且还假定你能够比其他人更快地发送自己的买入价和卖出价以及匹配客户订单。通过这种方式,我们进入了低延迟和高频交易(HFT)的领域。

高频、低延迟——Python 的失败之处

如果不提及高频交易(HFT),我们的概述将是不完整的。其根源在于 2008 年的金融危机,当时流动性成为大多数,如果不是所有发达市场的主要问题。交易所开始向提供流动性的个人提供激励,取消了之前要求流动性提供者受监管的许多限制。因此,许多市场参与者开始提供流动性,或者更确切地说,在订单簿中展示了这种流动性——因为他们只发送了一个订单,几毫秒后从簿中撤回。换句话说,他们开始制造骗局,创造流动性的错觉。

当然,要想在这里取得成功,你需要能够每秒处理数千笔交易,并将延迟(即从订单发送到交易所到订单出现在订单簿中的时间)降低到绝对最小。这就是为什么高频交易(HFT)需要位于非常昂贵的数据中心、尽可能靠近交易所服务器的昂贵计算机。

高频交易受到了严厉的批评,尤其是在两次被称为 闪电崩盘 的事件之后:在这些情况下,价格在没有明显原因的情况下大幅下跌(超过 10%)。完全有可能的是,如果一位高频交易员发送了一个大订单,那么买方市场参与者决定与这个订单进行交易,并且高频交易员在交易前的一小部分毫秒内撤回了订单,那么买方订单就会扫清订单簿,将价格移动到当然既不是预期也不是希望达到的水平(参见 第三章 中的 “交易所和订单簿” 部分,从开发者角度的 FX 市场概述)。

因此,很明显,由于速度问题,Python 将会在高频交易(HFT)领域失败。大多数高频交易算法是用 C++ 编写的,有时也用 C 编写,其中最关键的部分有时是用汇编语言编写的。再次值得一提的是,像 Numba 或 Cython 这样的编译器可以显著提高 Python 代码的速度;然而,它们无法达到编译后的 C 或 C++ 代码的速度。除此之外,所有基于 Python 的解决方案对 FPGA 的支持都非常有限,因此有时专业的算法交易员会说 Python 只适合于 慢速 HFT 算法(这些算法以毫秒而不是微秒为单位工作)。

考虑到所有这些因素,我们在这本书中不会考虑高频交易策略也就不足为奇了。只是当你看到订单簿中一个不错的订单时,要意识到你并不总能抓住它以获取利润!

摘要

在本章中,我们学习了系统交易和算法交易的关键术语和概念。我们熟悉了 alpha 和 beta 作为投资中的风险指标,同时,作为算法交易中利润生成的方法。我们考虑了几种流行的 alpha 生成交易策略,并了解了它们的优点、缺点和相关的风险。我们还简要介绍了期权交易这一复杂领域,作为在市场 beta 上盈利的主要方法,并快速浏览了其他交易策略,如套利、统计套利、做市和 HFT。

现在我们知道了某些策略可能进入或退出市场的情况,我们通往第一个交易应用的道路上的最后一个障碍就是根据策略规则生成订单、将它们发送到市场并控制它们执行机制的机制。回忆一下本章开头提到的类比,现在我们已经为我们的交易应用添加了大脑,现在是时候添加它将能够移动的肢体了。这正是我们将在下一章中开发的。

第十章:订单类型及其在 Python 中的模拟

在上一章中,我们考虑了在 FX 交易中通常采用的一些经典交易策略。所有这些都可以自动化——也就是说,是否进行交易的决策可以仅基于定量数据,而将交易放入市场可以通过算法完成。因此,我们现在需要找到一种适当的方式来放置交易并控制其执行。

我们已经提到(见第一章开发交易策略——为什么它们不同),任何交易,无论是手动还是自动化,都可以通过使用订单在市场上进行:向经纪人(或任何其他中介)发出买入或卖出的指令。还有什么能比这更简单?然而,现实总是更复杂。你可能希望在某个价格进行交易,不低于某个价格,不超过从某个价格指定的 pip(点)数,等等。除此之外,在真实市场中,存在真实的流动性,这总是非常远离无限,因此你的订单可能会被拒绝,部分成交,或者以错误的价格执行,以及许多其他未预料到和不受欢迎的事情,如果你没有做好准备的话。

现在,是我们学习大多数 FX 流动性池通常支持的主要订单类型的时候了,考虑根据订单类型的不同执行差异,了解根据目标选择订单类型,并准备好概述订单模拟引擎的架构,该引擎模仿可能的执行问题,并随着交易逻辑一起,帮助你测试和改进你的策略,以便在实际投入真金白银之前适应现实生活条件。

在本章中,我们将涵盖以下主题:

  • 订单票据——你发送的即你得到的

  • 市场订单——控制交易风险的最大方式

  • 限价订单——保证价格,但不保证执行

  • 有效期限——更好地控制执行

  • 止损订单——最大未受控风险

  • 复合订单

订单票据——你发送的即你得到的

让我们从起草一份通用订单票据的原型开始——这是发送给交易场所的东西。

通常,订单以 FIX 消息(见第四章交易应用——内部结构是什么?)或根据场所规格的 JSON 格式发送。正如我们在第四章中提到的,每个场所都有自己的数据和订单接口,但订单的核心属性始终是相同的。

实际上,这些属性的列表相当合理。让我们准备一个空表单,并逐个填写其字段。

首先,每个订单都应该附带一个 ID。否则,我们或交易场所如何引用它?如果我们进行实时交易,那么交易场所将生成一个订单 ID 供我们接收,但如果我们运行回测并想要修改之前发送的订单,我们需要一个内部生成的订单 ID。无论如何,在我们的订单表格中,第一项是订单 ID

图 10.1 – 订单的第一个属性是订单 ID

图 10.1 – 订单的第一个属性是订单 ID

接下来,我们需要让场所知道我们想要在哪个市场进行交易。一如既往,请记住,每个交易场所都有自己的规定。例如,EURUSD 可以发送为EURUSDEUR/USDeur-usd等等。所以,在实际上发送订单之前,请检查场所的文档。让我们将第二个记录添加到订单单据中 – 工具 ID

图 10.2 – 添加了工具 ID

图 10.2 – 添加了工具 ID

我们还需要指定我们想要交易的数量或交易大小。交易大小可以用手数(再次,请参考场所文档了解一手是多少)或直接用货币来指定。

例如,在 LMAX,购买 1 手 EUR USD 的订单意味着购买 1,000 欧元并卖出相应数量的美元,而在 Interactive Brokers,卖出 100,000 美元 JPY 的订单实际上意味着卖出 100,000 美元并买入相应数量的日元。在指定订单大小时要小心!现在,我们的订单单据由三个记录组成:

图 10.3 – 订单中指定的交易大小

图 10.3 – 订单中指定的交易大小

当然,我们还应该指定交易的哪一方:我们是要买还是卖。一些场所仅使用指定符,而其他场所建议使用买入价卖出价来表示方向,所以正如你所见,咨询场所文档是算法交易中唯一稳定的东西。总的来说,现在我们的订单单据上有四个记录:

图 10.4 – 指定交易的哪一方

图 10.4 – 指定交易的哪一方

我们准备好了吗?看起来是这样,但实际上并没有。交易场所期望我们指定一些其他交易参数:我们应该说明我们希望如何执行订单,在什么价格,以及何时。这三个额外参数中的第一个被称为订单类型,我们通常区分市价订单限价订单止损订单。还有一些所谓的复合条件订单,它们本质上是由这三种基本订单类型的组合,但并非所有场所都支持。因此,我们只将详细考虑这三种主要订单类型。

市价订单 – 获取最大交易风险控制的途径

让我们从最简单(至少从第一眼看来)的订单类型开始:市价订单。市价订单是购买或出售一定数量的资产以市价的订单。通过市价,我们通常假设最佳买入价或最佳卖出价(参见第三章从开发者角度的 FX 市场概述,关于最佳买入价和卖出价的解释),并且大多数交易策略开发者仅使用最佳买入/卖出历史数据来测试他们的想法。因此,我们可以在我们的订单票证原型中添加另一条记录,这条记录代表订单类型:

图 10.5 – 指定订单类型

图 10.5 – 指定订单类型

我们已经看到(再次参见第三章从开发者角度的 FX 市场概述),流动性可能对实际订单执行方式有重大影响,并且当单个大订单在执行过程中可能移动最佳买入价或卖出价时,这种情况被认为是相当常见的。因此,在将订单发送到市场之前,确保订单将以最接近订单簿中立即看到的最优买入价或最优卖出价的价格执行是很重要的。

使用这种订单类型可以确保如果订单被执行,那么你将获得订单中指定的交易资产的准确数量。同时,它并不保证平均执行价格将与订单簿的顶部相同,因为这种执行方法允许购买或出售订单簿中目前存在的全部金额。

例如,让我们回顾一下第三章交易所和订单簿部分中的图 3.1)中显示的订单簿示例。想象一下,我们发送一个市价订单购买 1,000 份合约。它会被执行吗?是的,因为订单簿中有超过 1,000 份合约。但将以什么价格成交呢?

发送订单时的订单簿中,最优卖出价(2,149.25)有 155 份合约,然后是 2,149.50 的 306 份合约,接着是 2,149.75 的 291 份合约,最后是 2,150.0 的 532 份合约。因此,我们的订单将消耗前三个价格水平上的所有流动性,以及第四个价格水平上的 248 份合约。结果平均执行价格可以使用标准加权平均公式计算:

![公式 B19145_10_001.jpg]

在这里,P代表加权平均价格,p表示订单簿中的价格,而q表示数量,即在价格p下成交的合约数量。在我们的例子中,它将是大约 2,149.658,这比 2,145.25 要远得多,后者是我们发出订单时的最佳卖出价。

订单以更差的市场价格执行或根据现有流动性分部分填充订单的现象称为滑点

因此,当我们需要填补确切的交易规模时,市价单可能是有用的,但这也可能导致以低于预期的价格填补。那么,为什么我们说市价单是控制交易风险的最大途径呢?

原因在于,使用市价单,我们可以尽可能地对订单进行细粒度和精确的排序。确实,没有什么阻止我们开发一个算法,该算法首先检查订单簿中的流动性,然后只发送不会破坏流动性的订单。如果你需要用大额订单(例如,如果你为金融机构工作)来填补订单,你可以将这个订单分成几部分,并依次发送多个市价单,直到整个金额被填补——再次强调,不要过多地干扰订单簿。

如果我可以这么说的话,使用市价单的另一个好处是,这是唯一一种所有交易场所都接受而没有例外的情况。虽然我们将会考虑其他类型的订单,例如限价和止损,但请记住,它们并不总是由你计划与之合作的场所支持。

可能的执行问题

除了刚才讨论的流动性和平均价格填补问题外,市价单的主要问题是,这样的订单(如果没有指定有效期——见本章后面的内容)将以当前市场中的任何价格执行。是的,99% 的时间,它不会引起真正的问题,因为流动性相当充足,尤其是如果你交易主要货币对(构成美元指数的那些货币对,见第九章交易策略及其核心要素常见外汇基准——美元指数部分),但你记得在重要经济新闻发布时会发生什么吗?快速回顾一下第六章基本面分析的基础及其在 FX 交易中的可能用途中的图 6.2图 6.4,并刷新一下美国非农就业人数NFP)和英国 GDP 案例:在这些事件发生期间,相邻价格跳动之间的差距或距离可能达到几十个点。

因此,如果我们考虑美国 NFP 案例,并假设你希望在 NFP 发布前的一秒以市价单卖出 1.0230,那么订单很可能会在新闻发布后的第一个跳动时执行,即 1.0210——距离期望价格有 20 个点的差距!

好吧,看来市价单的情况大致是清晰的:我们说买入卖出,然后立即成交(无论是否有流动性相关的问题)。但如果我们想以某个特定价格买入或卖出,或者更精确地说,以不差于某个价格的价格执行我们的订单,我们该怎么办?嗯,这就是限价单的作用。

限价单 - 保证价格,但不保证执行

简要来说,限价订单是在指定价格或更好的价格买入或卖出指定数量的资产的一种订单。

这里“更好”是什么意思?

这意味着如果我发送一个以 1.0100 买入 EURUSD 的限价订单,那么任何低于 1.0100 的价格都将匹配我的订单。相反,如果我发送一个以 1.0100 卖出 EURUSD 的限价订单,那么任何高于这个水平的价格都将匹配。换句话说,通过使用限价订单,我表示我愿意以订单中指定的任何不低于该价格的价格买入或卖出

如果你以比当前市场价格更好的价格发送限价订单会发生什么?例如,以低于当前卖出价的买入限价订单?嗯,这取决于你的经纪商或执行场所使用的限价订单的具体实现。如果你在交易所交易(例如,如果你交易货币期货),那么你的订单将直接进入订单簿。

如果你交易现货或远期合约,那么这样的订单很可能会留在交易场所的系统里,直到市场价格触及订单水平,然后订单将被转换为市价订单。如果市场价格从未达到订单水平,订单可能会被取消,或者可能在实际意义上永远留在经纪商的订单簿中(当然,不是真的永远,每个经纪商都有自己的规则来处理被遗忘的限价订单)。

如果我们以比当前市场价格更差的价格发送限价买入订单会发生什么?例如,以高于当前卖出价的买入限价订单?在这种情况下,订单将立即执行,并且它将从订单簿的级别开始吸收流动性,推动价格不断上升,但这个过程将在触及限价时停止。因此,以低于市场价格的价格发送的限价订单可以被视为带有保护的市价订单

现在我们知道,除了市价订单外,我们还可以使用限价订单,如果我们使用它们,我们应该在我们的订单票证原型中指定一个新字段:订单价格

图 10.6 – 为停止和限价订单添加的价格属性

图 10.6 – 为停止和限价订单添加的价格属性

到目前为止,看起来限价订单在所有情况下都是最好的选择,但这真的是真的吗?当然,正如在交易中没有免费的午餐也没有圣杯一样,限价订单并不是执行问题的万能药。让我们深入探讨这个领域,因为限价订单的执行问题远不如市价订单的明显。

可能的执行问题

尽管市价订单的主要执行问题是它们保证了执行本身但并不保证执行价格,限价订单的主要问题正好相反:限价订单保证了执行价格(仅凭其定义)但并不保证执行本身。

事实上,让我们仔细考虑这两种情况:如果我们向交易所发送一个以更好的价格(即,对于买单限价低于当前价格,对于卖单限价高于当前价格)执行的限价单,以及如果我们向经纪人或 ECN 发送这样的订单。

如果我们与交易所合作,限价单会直接进入订单簿(实际上并不是真的直接:首先它们会通过经纪人的风险管理系统,检查你是否拥有足够的保证金来发送这样的订单,但在当前情况下这并不重要)。然而,我们应该记住,订单簿在现实中是二维的(参见第三章中的图 3.2从开发者角度的 FX 市场概述,在交易所和订单簿部分),并且我们的订单将始终以相同的价格水平放在当前订单队列的末尾。因此,当市场价格触及订单水平时,即有人实际上以订单价格进行了交易,并不能保证该交易的规模足以匹配同一价格水平上的所有订单——包括我们的订单。

备注

在算法交易项目的研究和开发阶段,最常见的错误之一是假设所有限价单都会被执行,即使它们的价位只被一个价位点触及。这种错误的假设通常会导致各种圣杯交易策略的产生,这些策略只在纸上有效。

如果我们与外汇经纪人或 ECN 交易,那么我们的限价单很可能不会进入任何订单簿,除了那个经纪人之外没有人会看到它——直到市场价格达到订单水平。此时,限价单将转换为市价单并实际上发送到市场。采用这种方法,我们可能会遭受与使用市价单交易时相同的疾病——可能以比限价单价格更差的价格执行,这与限价单的定义正好相反。

对于这个问题有几种解决方案,其中大多数解决方案都是由执行场所实施的。大多数方案在发送市场订单之前都会检查订单簿中的流动性:如果订单规模超过了最佳买价/卖价的流动性,那么订单只执行部分,直到实际可用的金额。这样,就模仿了传统交易所订单簿的传统行为。

听起来令人失望?

好吧,实际上,一些(不幸的是,并非所有)执行场所允许对订单执行有更大的控制。在发送订单之前检查订单簿中的流动性是一种好习惯,但与一些交易技术提供商合作,我们还可以使用特殊条件或指定条件来控制订单的有效时间。

有效时间 – 更好的执行控制

之前提到的指定通常被称为有效期条件,尽管,正如你将在本章稍后看到的那样,对于其中的一些,这并不真正明显或直观。

重要区分

通常,这些执行方法指定被称为订单类型。这不仅在一些经纪商的文档中可以找到,而且在交易书籍和学术研究中也可以找到。所以,当你遇到任何文档中的订单类型有效期时,一定要非常小心,并确保你确切地理解作者的想法:订单类型本身(市价、限价、止损等)、其有效的期间,或者订单应该如何在流动性方面执行!

在以下小节中,我们将详细考虑不同的订单指定。

立即成交或取消

附带在市价订单上的立即成交或取消IOC)指令意味着:我想以市价(即当前最佳买入/卖出价)最多买入或卖出 X 欧元、美元、手数、合约等,但价格不能低于这个价格,而且我不在乎是否不能得到全部的 X 欧元、美元等,只希望得到更少的数量。如果市场流动性不足以满足我的订单,那么就填入可用的部分,并取消剩余的部分。

换句话说,使用 IOC,我优先考虑价格 而非数量

例如,我发送一个 IOC 市场订单以购买 1,000,000 EUR USD。当前卖出价是 1.01234,当前卖出价处的可用数量是 500,000。那么,我将购买 500,000 EURUSD,以 1.01234 的价格,而我剩余的订单(另外 500,000)将被取消。

使用 IOC 订单可能是防止以不希望的价格成交的最好方法,但你应该记住,使用它可能会导致部分成交 – 因此,你的交易算法应该以某种方式考虑到这种情况。

全部成交或取消

全部成交或取消FOK)指令意味着:我想以市价买入或卖出正好 X欧元、美元、手数、合约等,但如果最佳买入/卖出价处的流动性不足以完成整个订单,则根本不执行此订单(或取消此订单)。

换句话说,使用 FOK,我优先考虑订单的完整性而非其执行

在相同的例子中,如果我发送一个 1,000,000 EURUSD 的市场买入 FOK 订单,但最佳卖出价处只有 500,000,那么整个订单将被取消(取消),并且不会执行部分成交。

你可能会想知道为什么 IOC 和 FOK 都被认为是有效期指定:两者都假设订单立即、即时执行。答案可能就在问题中:使用 IOC 或 FOK,我们指定订单应该尽快执行;这样,我们就指定了它的有效期。

大多数执行场所只支持市价订单的 IOC 或 FOK,但也有一些允许将 IOC 或 FOK 添加到限价订单中。这很有意义,因为一些执行场所会在限价订单的价格被市场触及时将其转换为市价订单,因此在这种情况下,市价订单的时间有效指定符变得绝对有效。

当然,还有其他时间有效指定符,可以为订单提供比 IOC 或 FOK 更长的生命周期。最常见的是当日有效取消前有效

当日有效(GTD)和取消前有效(GTC)

这两个指定符都假设订单可以部分成交,并设定了它可以成交的时间框架。

GTD(当日有效)订单试图以最佳买卖价填充可用的部分,剩余的部分将保留在交易场所的订单簿中直到当天结束(在大多数情况下,即纽约时间下午 5 点)。任何时刻,当价格回到订单水平时,订单的另一部分将被执行。

GTC 订单的行为方式类似,但没有明确指定订单保持有效的具体时间。交易者应该手动或自动地关注所有 GTC 订单。

在现实中,这两个指定符都与限价订单一起使用,仅仅意味着这样一个订单将保持有效的时间,因为如果我们通过检查流动性采取所有适当的措施,那么在大多数情况下,限价订单将立即被执行。

现在我们已经熟悉了时间有效指定符,是时候更新我们提出的订单票结构了:

图 10.7 – 添加了时间有效属性

图 10.7 – 添加了时间有效属性

到目前为止,我们已经讨论了市价订单(我现在买入)和限价订单(我以这个价格或更好的价格买入)。但如果我们有一个以这个价格或更好的价格的订单,那么应该存在一个以这个价格或更差的价格的订单,不是吗?是的,这种订单类型确实存在,它被称为止损订单。

止损订单 – 最大不可控风险

从本质上讲,止损订单是一种以指定价格或更差的价格买入或卖出指定数量资产的订单。

你可能会在这个时候问:我为什么要以低于我愿意的价格买入或卖出呢?

答案非常简单:更好更差只是指订单价格相对于当前市场价格的关系。

例如,如果当前 EURUSD 的价格是 0.99673,而我发送了一个在 0.9989 的买入止损订单,那么我的订单将在任何等于或高于 0.9989 的价格被执行。与限价订单类似,止损订单将存在于经纪商的订单簿中,直到市场价格触及订单价格,然后转换为市价订单。请注意,与限价订单不同,即使你与交易所交易,止损订单也永远不会立即发送到订单簿。这是非常自然的:如果我向订单簿发送限价订单,那么我就提高了市场的流动性,因为其他人可能成为我的交易对手,然后我在某种意义上成为他们的流动性提供者。但是,如果止损订单被发送到订单簿,它将立即与那里的某个限价订单匹配,并立即以不可预测的距离移动价格。

实际上,让我们考虑一个例子。假设我们有一个 EURUSD 的订单簿,当前市场价格(最佳买价/卖价)是 1.00000/1.00005。有一些限价订单以 0.99999、0.99998 等价格买入,还有一些限价订单以 1.00006、1.00007 等价格卖出。现在,想象一下有人发送了一个以 1.0020 买入的止损订单,并且这个订单被发送到订单簿。当然,卖方(那些在卖价一侧的人)会非常乐意以 1.0020 的价格卖出,而不是 1.0010 或 1.00008。因此,发送到订单簿的止损订单将触发最远离当前最佳买价/卖价的订单簿水平,这与订单簿的基本理念正好相反。

注意

并非所有执行场所都支持止损订单。许多机构流动性池只支持市价和限价订单(其中一些只支持市价订单)。因此,始终模拟本地止损订单是一个好主意。

因此,回到我们可能想要使用止损订单的原因的问题,现在我们理解了,当我们想要执行以下两个动作之一时,我们可以使用它:

  • 如果我们想要买入,则在高于当前价格的价格进入市场;如果我们想要卖出,则在低于当前价格的价格进入市场。

  • 当价格与你的头寸相反时退出头寸——也就是说,亏损。这种情况发生在当前价格低于多头头寸(买入)的入场价格,或者当前价格高于空头头寸(卖出)的入场价格。

如果我们使用止损订单从亏损的头寸中退出,那么它被称为止损订单

注意

很频繁地,停止订单与止损订单混淆。然而,停止订单可以用来进入和退出市场,而止损订单始终只用于退出亏损仓位。换句话说,停止订单是一个更广泛的概念,与一般交易相关,而止损是一个狭窄的概念,仅与交易逻辑相关。从交易场所的角度来看,用于开仓或平仓的停止订单之间没有区别,因此止损订单可能只会在特定经纪商的文档中提及,而不是交易场所的文档。

既然我们知道停止订单在一定程度上是人为的,并且实际上作为条件市价订单执行,我们就可以理解它们执行的典型问题。

可能的执行问题

当然,停止订单的主要执行问题与市价订单类似:停止订单的滑点可能是无限的。如果我们发送一个买入停止订单,那么它将在等于或高于订单价格的价格执行。这类似于卖出停止订单:它将在等于或低于订单价格的价格执行。

停止订单保证了执行,但不能保证价格——从这个意义上说,它们与市价订单有些相似。让我们再次回到第六章中的图 6**.2基本面分析的基础及其在 FX 交易中的可能用途,它说明了美国 NFP 数据发布前后交易(tick)的顺序。想象一下,我们在新闻发布前持有多头仓位,并在 1.02250 处设置了一个保护性停止(止损)订单。在新闻发布后的第一个 tick,这个停止订单将被执行——但不是在 1.02250 处。它将在第一个可用的价格执行,这可能是 1.02100 或更低(更低)。

总要记住,即使你放置了止损订单,这也并不意味着你通过订单的价格水平限制了你的损失。它可能会在请求价格之外执行,从而增加损失。

值得注意的是,一些做市商提供保证止损执行。这意味着在像美国 NFP 或类似情况这样的事件期间,当价格可能在一次 tick 内跳过停止订单价格时,他们仍然会在请求的价格执行订单,而不是市场价格。只有做市商才能做到这一点,因为他们为你这个价格接受者报价。当然,这里也没有免费的午餐,所以做市商会为此服务收取溢价。这种溢价通常意味着更宽的价差或更高的佣金。

使用保证止损订单是否有意义?嗯,这取决于你模型中的交易逻辑及其统计数据。如果你将止损订单作为退出头寸的常规手段,即使在平静的市场中也是如此,那么答案可能是否定的,因为你会在价差和佣金上损失更多。如果你只将其作为应对意外灾难性市场价格变动的保护措施,那么答案可能是肯定的,因为这种灾难性变动可以在一个 tick 内摧毁你的账户——只需回忆一下瑞士国家银行将瑞士法郎(CHF)汇率与欧元脱钩的案例,该案例在第九章“交易策略及其核心要素”中的图 9**.6中描述。

考虑到所有这些,经纪商处的止损订单大多被用作保护性止损订单,用于退出亏损头寸并削减持续亏损。如果你的策略逻辑假设随着价格变动进入市场,例如典型的突破策略,那么本地模拟此类止损订单并将其作为市价订单甚至限价订单发送到市场,以防止不良执行,总是一个更好的主意。

现在,我们可以提出我们的一般订单单据的最终草案,该草案将支持所有三种主要类型的订单(市价、限价和止损)以及发送订单到经纪商所需的所有基本属性:

图 10.8 – 订单单据现在支持所有类型的订单

图 10.8 – 订单单据现在支持所有类型的订单

与限价订单类似,IOC 和 FOK 不与止损订单一起使用,但 GTD 和 GTC 可以。除此之外,一些执行场所还提供触发止损订单的额外条件,即将止损订单转换为市价订单:无论是当买入价或卖出价触及订单水平时触发。然而,这个指定器并不常见。

市价、限价和止损订单是实际上被几乎所有交易场所接受的标准化订单。然而,一些场所提供其他类型的订单,这些订单通常被称为复合订单,我们将在下一节中探讨。它们并不常见,也不是必需的,但至少值得知道它们存在以及它们在非常一般意义上的工作方式。

复合订单

复合订单是指那些在执行过程中假设一定逻辑链的订单。这就是为什么它们也被称为条件订单。严格来说,此类订单不是一个单独的订单:它是一系列依次触发的订单。

作为最常见的例子,让我们考虑一个止损限价单。与止损或限价单不同,它需要指定两个价格:止损价和限价。如果将此类订单发送到执行场所,首先,该场所的匹配引擎会等待市场(最佳买价/卖价)价格触及订单的止损价。之后,订单将使用其限价价格执行,就像执行限价单一样。因此,止损限价单是两者的结合。

例如,如果当前 EUR USD 的价格为 1.01015,而我想要以 1.0102 的价格买入,我可以使用止损单进入市场——但我记得在止损单执行过程中,我可能会得到一个可能无限大的滑点。因此,我发送了一个包含两个价格的止损限价单:止损价为 1.0102,限价为 1.01025。然后,一旦市场(这意味着要价或出价变为等于或大于 1.0102;参见前一部分中关于止损订单的特殊执行条件说明)触及 1.0102,实际上就会执行一个 1.01025 的限价买单。因此,我将以 1.0102(止损价)和 1.01025(限价)之间的任何价格买入 EURUSD。

值得注意的是,止损限价单仅用于进入市场,永远不会用作止损单。止损单必须确保整个订单量被完全成交,但止损限价单,就像任何限价单一样,不能保证整个订单量的执行。

摘要

现在,我们已经熟悉了三种主要类型的订单,并且知道在哪些情况下我们更喜欢使用市价、止损和限价单,在哪些情况下我们宁愿避免使用它们。除此之外,从之前的章节中,我们记得如何接收和处理市场数据,如何使用技术分析,并且我们记得关键风险,并至少有一些关于如何缓解这些风险的想法。因此,我们为任何算法交易员在研究和开发阶段的最终、终极任务做好了准备:我们即将开始起草一个交易应用,这将接收数据,处理数据,生成交易信号,将它们转换为订单,将订单发送给经纪人,处理经纪人的响应,并收集交易统计数据,这些数据最终可以证明或反驳交易想法。这就是我们在下一章将要做的。

第十一章:回测和理论性能

这已经是一段漫长且希望是充满趣味的旅程——尽管有时也很艰难。我们用了十章来熟悉市场结构的所有基本要素以及构建系统化和算法交易基础的关键概念。现在,我们已经接近这本书的结尾。是时候将所有碎片拼凑在一起,开始开发我们的第一个既可以用于研究也可以用于生产的交易应用了。

我们将开发一个通用原型,您只需重新编写部分内容而不修改整个结构,就可以使用和重用。我们将追踪从接收 tick 到下单的所有路径——同时检查我们所有行动的一致性。我们将学习如何保持交易应用各部分的同步,并了解为什么这样做如此重要。最后,我们将收集一些示例交易策略的基本统计数据,并计算其理论性能——在整个研究和开发过程中最重要的逻辑点。

在本章中,我们将涵盖以下主题:

  • 交易应用架构——修订和改进

  • 多线程——方便但充满惊喜

  • 带有实时数据流的交易应用

  • 回测——加速研究

交易应用架构——修订和改进

第一章开发交易策略——为什么它们不同中,我们提出了一个交易应用的通用架构。简要来说,它包括以下组件:

  • 数据接收器:从市场检索实时数据或从本地存储检索历史数据的组件;参见第五章使用 Python 检索和处理市场数据

  • 数据清理:一个消除非市场价格的组件;参见第一章开发交易策略——为什么它们不同

  • 交易逻辑:交易应用的大脑,负责做出交易决策(参见第六章基本面分析基础及其在 FX 交易中的可能应用第七章技术分析及其在 Python 中的实现,以及第九章交易策略及其核心要素),通常还包含集成的事前风险管理

  • 订单界面:接收交易逻辑的交易信号,将它们转换为订单,并跟踪其执行情况;参见第十章Python 中的订单类型及其模拟

  • 交易后风险管理开放头寸管理,例如跟踪运行亏损和清算亏损头寸或所有头寸

总之,这个简化的架构列出了基本组件,但没有说明它们之间是如何相互通信的。当然,可以使用线性架构,其中所有组件都作为依赖的代码片段依次执行。这种解决方案很简单,但存在显著的缺点:

  • 您无法添加更多交易逻辑组件以并行运行多个策略

  • 您无法向多个交易场所发送订单

  • 您无法在交易逻辑中接收关于实际综合市场头寸的信息

  • 您无法重用相同的代码(至少在部分上)用于开发和生产

让我们暂时停下来,谈谈这四个缺点。

关于前两点,你可能可能会说,您不会运行多个策略并在多个交易场所进行交易,因为我们只是刚开始接触算法交易,而且跨场所和跨交易逻辑更像是一种机构活动。我可以说,在现实中,私人交易者做所有这些事情是非常正常的,但这两个点的重要性不如剩下的两个点。

要理解第三点的重要性,我们必须引入一个新术语:综合 市场头寸

想象一下,您有几种策略,它们都在同一个市场上交易——比如说,EURUSD。第一个策略购买了 10 万欧元,第二个策略卖出了 8 万,第三个策略购买了 5 万。这是为什么?这是一个相当常见的情况:例如,您运行了一个短期均值回归策略,一个长期突破策略,以及一个长期趋势跟踪策略(见第九章交易策略及其核心要素);它们独立生成交易信号,但只要它们都在同一个市场上交易,它们都会对当前交易的资产数量做出贡献。这个数量被称为综合市场头寸。

在我们的例子中,每个策略的个别头寸是 100,000 多,80,000 空,还有另一个 50,000 多,所以综合头寸是70,000 多。这是您的实际市场敞口,所有头寸规模计算都应该基于这个数字。

但这样的综合头寸的入场价格是多少呢?

第十章订单类型及其在 Python 中的模拟中,我们探讨了部分执行订单的平均执行价格。同样的方法可以用来计算综合市场头寸的平均入场价格。让我们用我们三个开放头寸的例子来做这个简单的数学计算。

假设第一个(100,000 多单)头寸是在 1.0552 时打开的,第二个(80,000 空单)是在 1.0598 时,第三个(50,000 多单)是在 1.0471 时。首先,我们计算这些价格乘以相应交易规模的和。别忘了将空单(实际上减少了综合市场头寸)计为负数:

S = 100000 * 1.0552 – 80000 * 1.0598 + 50000 * 1.0471 = 73091

现在,我们将总和,S,除以实际的综合市场头寸,MP,它等于70,000,我们得到平均入场价格:

公式图片

在我们的例子中,综合平均价格大约为 1.0442。乍一看,这似乎很荒谬,因为它远远低于实际交易价格中的最低价。但确保它是正确的其实非常简单。

假设当前市场价格为 1.0523。让我们计算每个头寸的运行利润或亏损(通常称为运行PnL或运行P/L;参见第三章从开发者角度的 FX 市场概述交易机制——再次一些术语部分):它只是当前价格与入场价格之间的距离乘以交易规模。在 1.0523 的运行 PnL 等于(1.0523 – 1.0552) * 100,000 = -$290,第二个头寸的运行 PnL 等于(1.0523 – 1.0598) * -80,000 = $600,第三个头寸的运行 PnL 等于(1.0523 – 1.0471) * 50,000 = $260。因此,对于综合市场头寸,运行 PnL 等于$570。

现在,让我们只使用一个综合市场头寸的价格和规模来做同样的数学计算。鉴于它是在 1.0442 时打开的,当前市场价格为 1.0523,其运行 PnL 为(1.0523 – 1.0442) * 70,000 = $567,这并不完全等于$570,仅仅是因为我们将平均价格四舍五入到第四位。因此,我们确实可以使用综合市场头寸的平均价格和结果交易规模,而不是分别计算每个头寸的 PnL。

知识分子笔记

这种将所有订单及其相应的交易量平均计算的综合头寸通常被称为量加权平均价格VWAP)。然而,VWAP 通常仅用于评估通过多个同一方向的头寸累积的头寸,只要我们讨论的是作为双边交易结果的网络头寸,我更喜欢使用综合,尽管它不是一个常规术语。

综合市场头寸对于正确实施风险管理至关重要。如果你不知道这个头寸,你就不知道你的运行利润或亏损,因此你不知道何时平仓亏损头寸——这可能导致灾难性的损失。此外,你可能甚至不知道要平仓多少,而是打开一个新头寸,而不是仅仅弥补损失。

即使你在单一市场中运行一个策略,了解实际市场中存在的确切市场位置也同样重要:不要忘记,由于多种原因,某些订单可能无法执行或以不同于预期的价格执行(见第十章订单类型及其在 Python 中的模拟)。因此,如果你不让你代码从经纪人向交易逻辑提供反馈,你可能很难管理你的头寸。

第四个缺点希望更加明显:如果我们能提出一个灵活、模块化和可重用的架构,那么它相对于每次想要切换数据源时都需要完全修改的东西具有优势。

因此,考虑到所有这些因素,我们建议如何使我们的交易应用架构满足所提到的所有要求?

我们已经知道解决方案,并在第五章使用 Python 获取和处理市场数据中相当成功地使用了它。这个解决方案是使用线程队列使应用组件独立工作。我强烈建议你通过参考该章节的与保存和实时数据一起工作 – 保持你的应用通用部分来刷新你对线程和队列的记忆。

现在,让我们重新绘制应用架构图,这次稍微低一些级别,更接近传输层,而不仅仅是业务逻辑。

和往常一样,我们将从开始:接收实时(订阅)市场数据。

市场数据组件

此组件应能够从几乎任何来源接收订阅数据,对其进行清理,将其转换为整个应用中使用的单一格式,并将它们放入数据队列:

图 11.1 – 订阅数据接收组件

图 11.1 – 订阅数据接收组件

这种方法的优点在于,一旦订阅数据被发送到订阅队列,我们就可以忘记它。这个过程现在与整个应用的其他部分隔离,如果我们需要更改数据供应商或经纪人,我们可以通过重写相应的模块来完成,而无需在其余代码中进行任何更改。

许多策略都需要订阅数据。例如,套利策略(见第九章交易策略及其核心要素)可以使用订阅数据来工作。然而,大多数交易策略使用基于压缩数据的逻辑,而不是订阅数据。因此,我们需要添加一个组件,可以将订阅数据聚合为条形图(见第五章使用 Python 获取和处理市场数据数据压缩 – 将金额保持在合理的最低限度部分)。

数据聚合组件

此模块不仅应该能够将实时 tick 数据聚合为 bars。当我们开发策略时,我们通常使用已经存储在本地并以压缩形式存储的历史市场数据,因此在测试运行期间没有必要浪费时间聚合 tick 数据。因此,我们必须将以下部分添加到我们的应用架构中:

图 11.2 – 从存储中读取 bars 或从 ticks 形成 bars

图 11.2 – 从存储中读取 bars 或从 ticks 形成 bars

同样,正如前一个案例一样,此过程被隔离于应用的其他部分,因此我们可以一次性实现它,直到我们需要修改将 tick 数据聚合为 bars 的方式。

接下来,我们应该实现交易逻辑。

交易逻辑组件

此组件可以使用 tick 数据和 bar 数据作为输入,并生成订单作为输出。此输出应进入交易应用的订单执行控制组件,因此再次使用另一个队列是很自然的:隔离订单执行组件与其他应用部分的订单队列。

然而,除了发送订单之外,我们还需要在交易逻辑和订单执行组件之间建立另一个连接。此连接应提供订单执行的反馈给交易逻辑。我们如何建立这样的连接?

此时可能首先想到的想法是再使用另一个队列。然而,在这种情况下,这并不方便。当您希望数据进入队列后立即触发某个过程时,队列是非常好的——换句话说,它们非常适合事件驱动的过程。但是,市场位置或权益值并不会自行触发任何过程:它们只是被交易应用的各种组件作为辅助值使用。因此,我们不会使用队列,而是创建一个对象来存储关于实施交易策略的所有必要数据,并将此对象共享给交易应用的所有组件。

此对象可以包含任何策略元数据,例如市场位置、权益时间序列(见下文)、运行 PnL、实现利润或损失、各种统计指标等:

图 11.3 – 存储交易策略元数据的对象原型

图 11.3 – 存储交易策略元数据的对象原型

最后价格指的是与上一个 tick(或 bar)收到的报价,它用于计算两个 tick(或 bar)之间的运行损益:如果头寸是多头且价格上涨,则运行损益也增加,如果头寸是空头且价格下跌,则运行损益仍然增加,依此类推。如果我们从策略开始到现在的每个 tick 或 bar 的运行损益变化进行汇总,那么我们将得到总损益,这通常被交易者称为权益。这是一个有点专业术语的词汇,因为正式来说,权益是归因于企业所有者的价值(例如,参见corporatefinanceinstitute.com/resources/valuation/equity/以获取详细信息),但在算法交易中,权益通常只指实现的损益,加上未平仓头寸的价值。

我们还可以在每个 tick 或 bar 上保存权益价值,从而创建一个时间序列。这个时间序列通常被称为权益曲线,它是交易策略性能最常见的说明:策略过去的表现以及何时以及赚了多少钱(或亏损)。这些信息也可以由交易逻辑使用,连同市场价格数据和市场头寸。

我们还包含了两个与资金管理相关的参数:初始资本杠杆。这些值可以用来检查我们是否有足够的资金进行交易,也可以用来确定我们订单的实际交易规模。

现在我们已经添加了一个在交易逻辑和订单执行组件之间传输策略元数据的通用对象,我们可以将交易逻辑组件添加到我们的架构图中:

图 11.4 – 交易逻辑和常见交易策略参数容器

图 11.4 – 交易逻辑和常见交易策略参数容器

需要添加的最后一个强制性组件是订单执行组件。

订单执行组件

此组件不仅实现了与经纪商的订单接口或本地模拟订单执行,它还将对策略性能进行一些基本分析——以满足交易逻辑的需求。它应该处理订单,将其发送给经纪商或本地模拟,接收执行状态,处理此状态(例如,如果订单被拒绝,决定如何操作:取消或再次提交),计算运行中的损益,并构建权益曲线。让我们将其添加到我们的图中:

图 11.5 – 订单执行控制模块及其与交易策略属性对象的交互

图 11.5 – 订单执行控制模块及其与交易策略属性对象的交互

让我们看看它是如何工作的。首先,我们从订单队列中接收一个或多个订单。这些订单是由交易逻辑生成并放入队列中的。然后,我们将订单发送给经纪人或本地模拟其执行并接收订单状态。如果订单已执行,则更新损益并添加另一个数据点到股本时间序列。如果订单被拒绝,我们将它返回到订单队列,整个过程将自动重新开始。

注意,策略元数据(市场位置、股本等)会随着每笔处理过的订单进行更新。这确保了在做出交易决策和控制实际市场敞口时的最终精度。

太好了!我们现在对整个交易应用架构有一个全面的了解。最令人愉快的是,它被分割成小而相对简单的组件。我们知道这些组件应该如何相互通信,我们知道数据格式,也知道它们应该按什么顺序操作,所以似乎我们知道了实现交易应用所需的一切。

但在我们开始编码之前,我想强调一下建议的架构的两个优势,这两个优势非常难以高估。

模块化架构的优势

首先,这种架构确保了你的交易应用在研究阶段(使用历史数据时)永远不会提前查看。在此阶段,我建议你刷新一下关于提前查看的记忆,这在第四章交易应用 – 内部结构是什么?交易逻辑 – 这里的一个小错误可能代价巨大部分中进行了详细讨论——我相信你会欣赏我们交易应用建议的架构。

其次,这种架构提供了一种灵活的模块化代码,符合通用交易应用的概念:你可以快速切换数据源和交易场所,并使用相同的应用程序进行研究和生产。

看起来我们已经涵盖了开始编写我们的第一个交易应用所需的所有内容。然而,有一个极端重要的问题却经常被许多开发者忽视:线程同步的问题。为了理解这个问题并找到正确的解决方案,让我们简要地抒情地谈谈多线程。

多线程 – 方便但充满惊喜

我们已经使用多线程(参见第五章使用 Python 检索和处理市场数据通用数据连接器部分),我们发现当开发模块化可扩展的应用程序时,使用多个线程会使生活变得容易得多。然而,我们从未探索过 Python 中多线程的实现方式。

两个概念经常被混淆:多进程多线程。它们之间的区别在于前者使用隔离进程的概念,每个进程都有一个 全局解释器锁GIL),从而能够使用单独的物理或逻辑处理器或处理器核心(所谓的 真正并行)来启用并行执行,而后者运行一个不关心处理器或核心数量的单个进程:它以小部分执行线程,允许每个线程运行几毫秒,然后切换到另一个线程。当然,从人类的角度来看,它看起来像进程是并行运行的。在大多数情况下,我们甚至不会考虑哪个线程在何时执行。但在实现事件驱动进程时,了解先发生什么变得至关重要:例如,如果我们试图在收到市场数据之前生成订单,最坏的情况可能会导致错误。

为了了解真正的多线程是如何工作的,让我们编写一些简单的代码,使用三个线程来模拟我们的交易应用的相关组件:

from threading import Thread
import time
def t1(): # A thread that emulates data receiving
    while True:
        print('Receive data')
        time.sleep(1)
def t2(): # A thread that emulates trading logic
    while True:
        print('Trading logic')
        time.sleep(1)
def t3(): # A thread that emulates order execution
    while True:
        print('Processing orders')
        time.sleep(1)
thread1 = Thread(target=t1)
thread2 = Thread(target=t2)
thread3 = Thread(target=t3)
thread1.start()
thread2.start()
thread3.start()

由于我们是逐个启动线程的(1、2,然后是 3),我们可能会看到显示“接收数据”、“交易逻辑”和“处理订单”的消息,并按此顺序重复。然而,当我们运行代码时,我们会看到一些不同的情况:

Receive data
Trading logic
Processing orders
Receive dataProcessing orders
Trading logic
Processing ordersReceive data
Trading logic
Processing orders
Receive data
Trading logic
Trading logic
Processing orders
Receive data
Trading logicProcessing orders
Receive data
Processing ordersReceive data
Trading logic

我们可以看到,尽管每种类型消息的平均数量大致相同,但它们出现的顺序几乎是随机的,使得输出变得混乱。这是因为默认情况下,没有任何线程有优先级,每个线程一旦有机会就会运行一小部分。

当然,这样的行为不适合交易应用:我们想确保首先接收 tick 数据,然后处理它,然后生成订单,最后将其发送执行 – 按照这个顺序,而不是其他任何顺序!

解决这个问题有几个方案。我们将使用两种:使用数据流作为同步的事件,以及使用 threading.Event() 对象在线程之间切换。我们将在接下来的章节中详细讨论每种方法。

让我们先实现一个与实时 tick 数据交互的版本交易应用,然后看看我们如何轻松地将它转换成一个强大的回测工具(如果你不记得回测的含义,只需跳回到 第二章使用 Python 进行交易策略什么是纸交易和 回测? 部分)。

带有实时数据流的交易应用

和往常一样,我们首先进行一些导入:

import json
import threading
import queue
from datetime import datetime
from websocket import create_connection

接下来,我们创建一个包含策略元数据的类(参见 交易逻辑 组件 部分):

class tradingSystemMetadata:
    def __init__(self):
        self.initial_capital = 10000
        self.leverage = 30
        self.market_position = 0
        self.equity = 0
        self.last_price = 0
        self.equity_timeseries = []

现在,我们准备三个(!)tick 数据队列:

tick_feed_0 = queue.Queue()
tick_feed_1 = queue.Queue()
tick_feed_2 = queue.Queue()

为什么是三个?这是在 多线程 – 方便但充满 惊喜 部分中解释的线程同步问题的一种解决方案。

第一个队列(tick_feed_0)将市场数据接收器与 tick 聚合组件连接起来,该组件形成条形图。每次第一个队列中有新的 tick 时,该组件就会被激活。组件完成后,它将相同的 tick 放入第二个队列(tick_feed_1)。

tick_feed_1 将 tick 聚合器与交易逻辑连接起来,并且只有在tick_feed_1中有新的 tick 时才会调用交易逻辑。但它在第一个组件完成工作后才能进入这个队列!因此,交易逻辑不能在处理完新的 tick 之前被调用。然后,类似地,交易逻辑组件将相同的 tick 放入第三个队列(tick_feed_2)。

tick_feed_2 将交易逻辑与订单执行组件连接起来,并且该组件只有在 tick_feed_2 中有新的 tick 时才会被调用。因此,使用三个队列将组件相互连接确保了操作的正确顺序。

重要提示

这种同步线程的方法只有在 tick 之间的间隔大于由它触发的所有线程完成工作的往返时间时才会有效。这对于大多数数据源都是有效的,因为我们通常每秒接收不到 10 个 tick,往返处理时间通常在 0.0001 秒左右。这种方法不适用于通过ITCH协议接收的重负载交易所市场数据,它有时每秒接收超过 10,000 个 tick。然而,这仅限于机构交易,我们在这本书中不考虑此类解决方案。

接下来,我们必须添加一个队列来处理聚合的市场数据(bar_feed),一个队列来存储订单(orders_stream),创建系统元数据类的实例,并指定连接到数据源所需的参数(在我们的例子中,我们使用LMAX作为市场数据源):

bar_feed = queue.Queue()
orders_stream = queue.Queue()
System = tradingSystemMetadata()
url = "wss://public-data-api.london-demo.lmax.com/v1/web-socket"
subscription_msg = '{"type": "SUBSCRIBE","channels": [{"name": "ORDER_BOOK","instruments": ["eur-usd"]}]}'

现在,我们可以在Plotting live tick data部分重用我们在第八章使用 Python 进行外汇交易中的数据可视化中开发的代码:

def LMAX_connect(url, subscription_msg):
    ws = create_connection(url)
    ws.send(subscription_msg)
    while True:
        tick = json.loads(ws.recv())

现在,我们必须将 tick 放入第一个 tick 队列。但在这样做之前,我们必须检查接收到的市场数据的一致性。我们在第一章中讨论了非市场价格,《开发交易策略 – 为什么它们不同》,所以让我们快速回顾一下:非市场价格太远于市场价格。当然,有时很难判断它是太远还是不太远,但本质上,我们至少可以过滤掉那些买卖价差(也称为价差)比正常情况下大几倍的 tick。这类事件相当罕见,但我幸运地捕捉到了在绘制 tick 图表时(见第八章使用 Python 进行外汇交易中的数据可视化)的一个这样的时刻。以下图展示了这样一个不良 tick,其买入价远低于应有的水平:

图 11.6 – 非市场价格

图 11.6 – 非市场价格

为了过滤掉至少这种类型的坏 tick,让我们添加一个简单的检查:如果价差大于 10 点,则跳过这个 tick:

        if 'instrument_id' in tick.keys():
            bid = float(tick['bids'][0]['price'])
            ask = float(tick['asks'][0]['price'])
            if ask - bid < 0.001:
        tick_feed_0.put(tick)

接下来,我们需要实现 tick 聚合器。在我们的例子中,让我们形成 10 秒的条形图,这样我们可以更快地测试我们的应用程序并检查一切是否正常工作(而不必等待 1 分钟或 1 小时的条形图完成)。

我们将仅使用市价数据来形成条形图以简化问题。为什么这是可能的呢?因为大多数时候(除了重要新闻发布、银行结算时间以及一周的开始/结束时段),价差(买价和卖价之间的差异)或多或少是恒定的。因此,如果我们想模拟订单的实际执行,那么我们可以使用实时买价和卖价在 tick 数据流中,但对于交易逻辑,我们可以使用仅使用一个价格的条形图。当然,对于某些策略,如套利,买价和卖价数据都是必不可少的(有时还包括最后成交价和这两个价格),但现在,我们正在构建一个原型,当你熟悉一般方法时,你将能够自定义你想要的方式。

在将 tick 聚合到条形图中时,我们几乎使用了与第八章中相同的代码,使用 Python 进行外汇交易数据可视化,在实时 tick 数据绘图部分,因此这里不需要太多的注释:

data_resolution = 10
def getBarRealtime(resolution):
    last_sample_ts = datetime.now()
    bar = {'Open': 0, 'High': 0, 'Low': 0, 'Close': 0}
    while True:
        tick = tick_feed_0.get(block=True)
        if 'instrument_id' in tick.keys():
            ts = datetime.strptime(tick['timestamp'], "%Y-%m-%dT%H:%M:%S.%fZ")
            bid = float(tick['bids'][0]['price'])
            delta = ts - last_sample_ts
            bar['High'] = max([bar['High'], bid])
            bar['Low'] = min([bar['Low'], bid])
            bar['Close'] = bid

我们创建了一个条形图,收到了一个 tick,并更新了条形图的高、低和收盘价。现在,一旦条形图开盘以来的时间大于或等于 10 秒,我们就开始一个新的条形图:

        if delta.seconds >= resolution - 1:
            if bar['Open'] != 0:
                bar_feed.put(bar)
                last_sample_ts = ts
                bar = {'Open': bid, 'High': bid, 'Low': bid, 'Close': bid}
        tick_feed_1.put(tick)

注意这个函数的最后一行。它将收到的相同 tick 放入tick_feed_1。这样做是为了触发下一个组件,即交易逻辑:

def tradeLogic():
    while True:
        tick = tick_feed_1.get()
        try:
            bar = bar_feed.get(block=False)
            print('Got bar: ', bar)

现在,是时候添加一些交易逻辑了。

注意

对于测试目的,我们不在乎我们的测试策略是否盈利——我们只想尽可能多地生成订单来观察模拟执行。

因此,让我们实现以下简单的逻辑:

  • 如果条形图收盘价上涨(close > open),则卖出

  • 如果条形图收盘价下跌(close < open),则买入

使用这个"策略",我们可能预期会快速生成许多订单,因此我们将能够测试我们的应用程序而无需等待太久:

            ####################################
            #      trade logic starts here      #
            ####################################
            open = bar['Open']
            close = bar['Close']
            if close > open and System.market_position >= 0:

在这里,我们正在检查条形图的收盘价是否高于开盘价,并且当前的综合市场位置是正的。我们这样做是因为我们不希望在同一个方向上打开多个仓位。换句话说,如果我们已经在市场上做多,我们只等待一个做空仓位打开,反之亦然:

                order = {}
                order['Type'] = 'Market'
                order['Price'] = close
                order['Side'] = 'Sell'

以下 if...else 语句检查我们是否是第一次开仓。如果是,那么在订单生成时我们没有任何当前市场仓位,所以在我们的例子中,交易量是 10,000。但如果已经有一个开仓,并且我们想要在相反方向上开一个新仓,那么我们首先应该关闭现有仓位,然后再开新仓,这实际上需要两倍的交易量。我们必须用 10000 来关闭,用 10000 来开新仓,这意味着交易量是 2 * 10,000 = 20,000:

                if System.market_position == 0:
                    order['Size'] = 10000
                else:
                    order['Size'] = 20000

最后,我们必须将订单放入订单队列:

                orders_stream.put(order)
                print(order) # added for testing

现在,我们必须对买入订单做完全相反的操作:

            if close < open and System.market_position <= 0:
                order = {}
                order['Type'] = 'Market'
                order['Price'] = close
                order['Side'] = 'Buy'
                if System.market_position == 0:
                    order['Size'] = 10000
                else:
                    order['Size'] = 20000
                orders_stream.put(order)
                print(order)
            ####################################
            #      trade logic ends here      #
            ####################################
        except:
            pass
        tick_feed_2.put(tick)

为什么我们使用 10,000 作为基础货币的交易量?

如果我们交易 EURUSD,一个报价为 4 或 5 位数的货币对,那么买入或卖出 10,000 欧元(见第三章从开发者角度的 FX 市场概述命名约定部分)意味着 1 个点值是 1 美元。因此,我们可以将我们的测试结果既解释为金钱,也解释为点数。由于外汇市场高度杠杆化(见第三章从开发者角度的 FX 市场概述中的交易机制-再次一些术语部分),使用点数计算所有 PnL 更方便,然后使用杠杆进行缩放。

注意,这个函数使用了一个 try...except 语句。原因是,我们使用了两个队列:tick_feed_1 用于接收 tick,bar_feed 用于接收实际的 bar。然而,tick 只在这个函数中用于触发其执行(见本节开头详细解释),而 bar 用于做出实际的交易决策。问题是,bar 通常比 tick 到达的频率低得多,所以我们不能等到 bar_feed 队列中有 bar;否则,我们应用程序的正常执行会被中断。这就是为什么我们在从 bar_feed 队列中读取时使用 block = False 属性。然而,如果 tick_feed_1 中有一个新的 tick,但 bar_feed 中没有 bar,那么尝试从那里读取会引发异常。因此,我们捕获这个异常,在我们的当前实现中,只是什么也不做,等待队列中出现新的 bar。

我们交易应用程序的最终组件是订单执行。我们通过在 tick_feed_2 中接收到的 tick 来调用这个函数,它是由 tradeLogic() 放入的:

def processOrders():
    while True:
        tick = tick_feed_2.get(block = True)
        current_price = float(tick['bids'][0]['price'])

每次收到 tick,我们都会更新交易系统的权益价值。记住,在交易者的行话中,“权益”是指在每个 tick 或 bar 上计算的所有 PnL 值的总和。如果我们持有多头仓位,并且当前价格高于前一个价格,那么在这个 tick/bar 上的权益价值会增加。相反的情况也是成立的:如果我们持有空头仓位,并且当前价格低于前一个价格,那么在这个 tick/bar 上的权益价值也会增加。

我相信你已经明白了:如果我们是多头,价格下降,或者如果我们是空头,价格上升,那么在这个 tick 或 bar 上,权益会减少。为了计算当前 tick 的实际权益值,我们将当前 tick 和前一个 tick 之间的价格差异乘以市场头寸的价值:

        System.equity += (current_price - System.last_price) * System.market_position
        System.equity_timeseries.append(System.equity)
        System.last_price = current_price
        print(tick['timestamp'], current_price, System.equity) # for testing purposes

现在,我们开始扫描订单队列,并按照它们出现的顺序执行订单。请注意,我们再次使用block = False属性,所以我们永远不会在订单队列中等待订单:如果在收到新的 tick 之前没有订单,我们就继续进行主循环:

        while True:
            try:
                order = orders_stream.get(block = False)

在我们收到订单后,我们应该进行风险管理检查:是否有足够的资金执行这个订单。为了计算可用资金,我们应该将当前权益(正或负)加到初始资本上,并减去目前开放市场头寸所需的保证金,即这个市场头寸的价值除以杠杆率:

                available_funds = (System.initial_capital + System.equity) * System.leverage - System.market_position / System.leverage

如何计算可用资金

我们在代码中使用可用资金的计算并不完全正确。问题是,在市场上有可能持有巨大的头寸,同时有一些正的运行 PnL。在这种情况下,我们的公式会表明我们有足够的资金,但事实上,直到这个巨大的头寸关闭,我们的交易账户可能没有足够的钱。所以,为了使这个计算完全精确,我们应该向系统元数据中引入另一个变量,它只计算已实现的 PnL(通过关闭的头寸计算)。然而,我们现在不会这样做,为了简单和透明起见。

现在,如果订单大小小于交易账户中的可用资金,我们可以执行订单。稍后,我们将编写一个单独的函数来模拟订单执行。在生产环境中,这个函数可以被替换为对经纪商 API 的实际调用:

                if order['Size'] < available_funds:
                    emulateBrokerExecution(tick, order)

在尝试执行订单后,其状态会变为'Executed''Rejected'(或任何其他由你的经纪商返回的状态),所以让我们决定如何处理它。当然,如果订单成功执行,我们只更新策略元数据(并打印结果以供测试):

                if order['Status'] == 'Executed':
                    System.last_price = order['Executed Price']
                    print('Executed at ', str(System.last_price), 'current price = ', str(current_price), 'order price = ', str(order['Executed Price']))
                    if order['Side'] == 'Buy':
                        System.market_position = System.market_position + order['Size']
                    if order['Side'] == 'Sell':
                        System.market_position = System.market_position – order['Size']

如果订单被拒绝,我们将它返回到同一个订单队列:

                elif order['Status'] == 'Rejected':
                    orders_stream.put(order)

再次强调,在现实中,你可能需要更复杂的订单处理,但这将取决于你将要运行的策略类型以及你的经纪商提供的订单状态类型。

最后,我们将添加except子句,以便在没有订单在订单队列中的情况下不发生任何事情:

            except:
                order = 'No order'
                break

我们几乎完成了!现在我们只需要添加一个模拟经纪商订单执行的函数。对于我们的模拟器第一个版本,我们将只实现市场订单的执行:

def emulateBrokerExecution(tick, order):
    if order['Type'] == 'Market':
        if order['Side'] == 'Buy':

是时候进行最后的预飞检查了:在发送订单之前确保市场有足够的流动性!

            current_liquidity = float(tick['asks'][0]['quantity'])

不要混淆买价和卖价!如果我们买入,我们检查出价(卖价)的流动性并在卖价处执行,而如果我们卖出,我们使用买价:

            price = float(tick['asks'][0]['price'])
            if order['Size'] <= current_liquidity:
                order['Executed Price'] = price
                order['Status'] = 'Executed'
            else:
                order['Status'] = 'Rejected'
        if order['Side'] == 'Sell':
            current_liquidity = float(tick['bids'][0]['quantity'])
            if order['Size'] <= current_liquidity:
                order['Executed Price'] = price
                order['Status'] = 'Executed'
            else:
                order['Status'] = 'Rejected'

现在,让我们回顾一下我们迄今为止添加的交易应用组件:

  • 交易系统元数据对象(class tradingSystemMetadata

  • 价格数据和订单的队列(tick_feed_0tick_feed_1tick_feed_2bar_feedorders_stream

  • 一个连接数据源的功能(LMAX_connect(url, subscription_msg)

  • 一个从刻度生成条形图的功能(getBarRealtime()

  • 一个做出交易决策的功能(tradeLogic()

  • 一个处理订单的功能(processOrders()

  • 一个在经纪人处模拟订单执行的功能(emulateBrokerExecution(tick, order)

我们只需要在我们的代码最后添加一个初始化并启动所有四个线程的块:

data_receiver_thread = threading.Thread(target = LMAX_connect, args = (url, subscription_msg))
incoming_price_thread = threading.Thread(target = getBarRealtime, args = (data_resolution,))
trading_thread = threading.Thread(target = tradeLogic)
ordering_thread = threading.Thread(target = processOrders)
data_receiver_thread.start()
incoming_price_thread.start()
trading_thread.start()

我们刚刚开发出了我们的第一个交易应用!现在是时候运行它并检查它是否如我们所期望的那样工作。我会运行它并等待第二个订单执行(因为我想要确保在策略在市场上有一个开仓位置以及没有开仓位置的情况下,我都提交了正确的订单)。如果你正确地重复了所有这些步骤,你应该会看到以下输出:

2022-12-12T12:03:20.000Z 1.05658 0.0
... (7 ticks omitted from output to save space)
2022-12-12T12:03:28.000Z 1.05664 0.0

我们从12:03:20开始,所以收到了九个刻度(记住,LMAX 不发送实际的刻度,而是市场数据的 1 秒快照)。在第 10 秒,我们形成一个条形图:

Got bar:  {'Open': 1.05658, 'High': 1.05668, 'Low': 1.05658, 'Close': 1.05666}

条形的收盘价高于条形的开盘价,因此根据我们的测试策略逻辑,这是一个卖出信号——而且确实,有条形图紧随其后:

{'Type': 'Market', 'Price': 1.05666, 'Side': 'Sell', 'Size': 10000}

注意,订单大小是10000,因为我们刚刚第一次开仓,我们在市场上还没有任何开仓。我们检查第 10 个刻度以确保其价格等于条形的收盘价和订单价格:

2022-12-12T12:03:29.000Z 1.05666 0.0

现在,我们可以看到执行报告:

Executed at  1.05666 current price =  1.05666 order price =  1.05666

到目前为止,一切顺利。让我们等待下一个条形图的形成:

2022-12-12T12:03:30.000Z 1.05663 0.2999999999997449
... (7 ticks omitted from output to save space)
Got bar:  {'Open': 1.05666, 'High': 1.05666, 'Low': 1.05663, 'Close': 1.05665}

我们很幸运:紧接着的下一个条形图以相反的方向收盘(收盘价小于开盘价),所以是时候生成一个买入订单了:

{'Type': 'Market', 'Price': 1.05665, 'Side': 'Buy', 'Size': 20000}

注意,这次订单大小是20000:我们需要关闭目前10000的开仓,然后使用剩余的10000开一个新仓。让我们检查刻度价格以确保条形的收盘价和订单价格是正确的:

2022-12-12T12:03:38.000Z 1.05665 0.09999999999843467

太好了,一切看起来都很正常。现在,让我们继续进行订单执行...

Executed at  1.05672 current price =  1.05665 order price =  1.05672

停止。那是什么?最后一个刻度的价格是1.05665,但订单是在1.05672执行的!为什么?

这是因为我们只使用买价形成条形图,并在实际市场价格处执行订单——卖价订单使用买价,买价订单使用卖价。第一个订单是卖出,所以我们使用了买价,所有价格(条形图、刻度、订单和执行)都一致。但第二个订单是买入,但我们仍然只使用买价来形成条形图——这就是为什么执行价格大于条形的收盘价。

市场价差的重视

这个问题完美地说明了在运行测试时考虑价差(买价和卖价之间的差异)的重要性。所以许多开发者忘记了这一点,只用买价进行测试 - 你知道,为了简单起见。这些测试不足以反映真实市场,而且经常产生只有在你能以相同价格买卖时才盈利的交易逻辑,实际上是在任何时候都假设价差为零。现在,你知道如何避免这个陷阱并确保你的测试始终是现实的。

在我们继续之前,让我们快速回顾一下我们的代码,看看它是否符合在第一章中概述的要求,开发交易策略 - 为什么它们 不同

  • 它过滤传入的 tick 数据流并排除非市场价格

  • 它是事件驱动的 - 一旦交易逻辑确认交易,它就会生成并执行订单

  • 它进行一些基本的风险管理检查 - 仓位大小、杠杆和可用资金

  • 它能够模拟不良订单执行并处理这些情况

  • 而且可能是主要的好处:这段代码永远不会 - 永远不会! - 窥视未来,无论是在测试中还是在生产中(参见第四章交易应用 - 它里面有什么?交易逻辑 - 这里的一个小错误可能会造成 巨大的损失部分)

因此,我们已经开发了一个适合严肃生产的健壮应用程序!当然,它可以进一步改进,但它的核心几乎不会改变。然而,我们没有经过测试的策略来运行。我们如何开发这样的策略?

这时我们可以使用我们之前提到的回测概念,几乎是在这本书的开头。

回测 - 加速研究过程

开发交易策略的过程(我的意思是交易逻辑,而不是应用程序)是一个无限循环:

  1. 提出一个假设。

  2. 编写代码。

  3. 运行测试。

  4. 如果结果不满意,调整参数并重复。

  5. 如果什么都没有帮助,寻找一个替代假设。

问题是:在第 3 步中,我们应该使用哪种应用程序进行测试?

当然,我们可以使用我们现有的交易应用,草拟一些策略逻辑,然后以测试模式运行它,就像我们刚才做的那样,收集订单并分析股票时间序列。但如果我们想在不同市场条件下测试策略,单个测试可能需要几天、几周甚至几个月。你认为这有点太长了?我同意。这就是为什么,出于研究和开发的目的,我们使用回测。

我们在第二章中讨论了回测,使用 Python 进行交易策略部分,在纸交易和回测——系统交易员风险管理的重要组成部分。本质上,我们不是使用实时数据流来模拟订单执行,而是使用预先保存的历史市场数据来模拟数据流本身。在这种情况下,我们可以显著加快测试速度,因为计算机可以每秒处理数十万个 tick 或柱状图,将数月的实时测试压缩到几分钟或几秒钟的回测中。当然,由于其本质,回测不能保证策略的未来表现,仅仅因为它使用过去的数据进行测试。但无论如何,它帮助我们了解策略在各种市场条件下的行为。一般来说,如果回测显示模拟的权益在过去大部分时间都在增长,那么我们可能会假设它将继续增长,反之亦然:如果我们看到模拟的权益随着时间的推移只呈下降趋势,或者最多在零点附近波动,那么我们应该非常谨慎地对待这种策略,因为它很难想象为什么它在投入生产后会突然开始赚钱。

我希望你能理解我们的想法:我们将使用保存的数据而不是实时数据来运行我们的代码,这样我们就可以在 1 秒钟内处理 1,000 或 10,000 甚至更多的历史数据秒数。

现在,我相信你会欣赏我们开发代码时采取的方法:如果你有预先保存的 tick 历史数据,那么你只需要修改一个函数——接收数据提供者提供的 tick 的那个函数——并让它从本地文件接收数据。

就这些。

这不令人印象深刻吗?是的,你可以使用相同的代码进行研究和生产,从而将出错的可能性降低到几乎为零。

然而,你并不总是能获取到历史 tick 数据。此外,对于使用更高时间框架的策略(例如 1 小时、4 小时、1 天、1 周等),等待我们的应用程序从 tick 中形成每个柱状图将是一种时间的浪费。因此,我们可能想要对我们的代码进行以下修改:

  • 它现在应该能够从本地文件读取数据,而不是从数据供应商接收数据。

  • 它应该能够处理已经压缩的数据(柱状图),而无需接收任何 tick 数据。

  • 它应该能够模拟订单执行,这可能发生在单个柱状图(例如,如果策略的逻辑基于 1 小时柱状图,那么我们应该能够模拟 hh:00 和 hh:59 之间的订单执行,其中 hh 代表小时值)的持续时间之内。

查看我们现有代码的架构,这似乎是一个相当直接的任务。然而,有一个需要注意的地方。

你还记得我们在现有代码中使用 tick 数据的方式吗?是的,我们将其聚合为条形图,但除此之外,tick 数据还作为系统时钟为整个应用程序的组件提供同步。如果我们根本不使用 tick 数据,我们如何同步它们呢?

在这里,我们可以使用另一种控制线程执行的方法——使用事件。

使用事件同步线程

让我们快速回到本章前面“多线程——方便但充满惊喜”部分中我们草拟的代码。那个代码的问题在于每个线程都在可能的时候运行,因此在一定程度上产生了随机的输出。而我们希望所有三个线程依次工作——t1t2t3,然后再次是t1,以此类推。

Python 的线程模块提供了几个非常高效的方法来解决控制线程的问题。其中之一是使用Event()对象。

一个threading.Event()对象被放置在线程的代码中,它就像交通灯一样工作。它有两种可能的状态:设置或清除。当事件被设置时,线程正常工作。当事件被清除时,线程停止。

除了清除和设置事件之外,还可以指示线程等待直到事件被设置。在这种情况下,线程等待事件,一旦它再次被设置,它就会继续工作。

如果我们希望线程以特定的顺序运行,那么我们应该坚持以下指南:

  • 我们需要的事件数量应该和线程数量一样多

  • 控制特定线程的事件应该在内部清除,但在外部设置

现在,让我们对代码进行一些修改。

首先,我们需要三个事件:

f1, f2, f3 = threading.Event(), threading.Event(), threading.Event()

在我们的例子中,f1将控制t1线程,f2将控制t2,而f3将控制t3

接下来,在t1()函数的末尾,我们执行以下操作:

  • 我们清除f1事件(它控制第一个线程)

  • 我们设置f2事件(为t2线程提供绿灯)

  • 我们将线程t1设置为等待f1事件再次被设置

修改后的代码将如下所示:

def t1():
    while True:
        print('Receive data')
        time.sleep(1)
        f1.clear()
        f2.set()
        f1.wait()

我们以同样的方式修改t2()t3()函数(以便每个线程控制其下一个邻居)并运行所有三个线程:

def t2():
    while True:
        print('Trading logic')
        time.sleep(1)
        f2.clear()
        f3.set()
        f2.wait()
def t3():
    while True:
        print('Processing orders')
        time.sleep(1)
        f3.clear()
        f1.set()
        f3.wait()
thread1 = threading.Thread(target=t1)
thread2 = threading.Thread(target=t2)
thread3 = threading.Thread(target=t3)
thread1.start()
thread2.start()
thread3.start()

现在,我们可以以完全正确的顺序享受输出:

Trading logic
Processing orders
Receive data
Trading logic
Processing orders
Receive data
Trading logic
Processing orders
Receive data

...等等。

注意

可能对于前两个执行循环,输出仍然可能以错误的顺序进行:这可能会发生,直到两个事件被清除并等待,而只有一个事件被设置。

既然我们已经熟悉了threading.Event()对象,现在是时候修改我们的回测应用了。为了清晰和易于使用,我将在这里重现其全部代码,并指出我们做了任何修改的确切位置。

带有历史数据馈送的历史回测平台

和往常一样,我们从几个导入开始:

import csv
import threading
import queue
import time
from datetime import datetime

然后,我们重用相同的tradingSystemMetadata类,并只为控制线程添加三个事件。我们称它们为F1F2F3(标志):

class tradingSystemMetadata:
    def __init__(self):
        self.initial_capital = 10000
        self.leverage = 30
        self.market_position = 0
        self.equity = 0
        self.last_price = 0
        self.equity_timeseries = []
        self.F1, self.F2, self.F3 = threading.Event(), threading.Event(), threading.Event()

接下来,我们需要数据和订单队列。由于我们不再使用 tick 数据来同步线程,因此不需要多个 tick 数据队列——我们只需要一个用于 K 线的队列,另一个用于订单:

bar_feed = queue.Queue()
orders_stream = queue.Queue()

接下来,我们必须创建系统元数据对象的实例,并将历史数据从文件中读取到all_data中。我们还必须启动计时器(time.perf_counter()方法)以跟踪各种操作所花费的时间——纯粹出于好奇。

注意,我们使用csv.DictReader()读取数据,这样我们接收到的每个 K 线都是一个字典——这确保了与我们在本章早期开发的原始生产代码的最大兼容性:

System = tradingSystemMetadata()
start_time = time.perf_counter()
f = open("<your_file_path>/LMAX EUR_USD 1 Minute.txt")
csvFile = csv.DictReader(f)
all_data = list(csvFile)
end_time = time.perf_counter()
print(f'Data read in {round(end_time - start_time, 0)} second(s).')

接下来,我们需要一个修改后的函数,该函数从读取的数据中逐个获取 K 线,将必要的字段从str转换为float,并将 K 线放入队列。我们还必须为了调试目的在第一个 10 个 K 线后中断此循环的执行:

def getBar():
    counter = 0
    for bar in all_data:
        bar['Open'] = float(bar['Open'])
        bar['High'] = float(bar['High'])
        bar['Low'] = float(bar['Low'])
        bar['Close'] = float(bar['Close'])
        bar_feed.put(bar)
        counter += 1
        if counter == 10:
            break
        System.F1.clear()
        System.F2.set()
        System.F1.wait()
    print('Finished reading data')

注意函数末尾的三个标志(System.F1System.F2System.F3):它们控制线程的执行,并确保首先读取一个 K 线,然后生成一个订单,最后执行——或者更确切地说,模拟这个订单的执行。

此外,请注意,我们不检查数据一致性,也不排除任何数据点:当我们处理保存的历史数据时,我们假设这些数据已经清洗过。

接下来是tradeLogic()函数。这里最好的消息是,其主要逻辑部分保持完全不变——在原始代码中的trade logic starts heretrade logic ends here注释之间不需要任何修改!我们只在这个函数的开始和结束处进行修改。

在开始时,我们必须添加一个try...except语句,当所有数据都被处理时,将终止相应的线程。为此,我们必须将get()方法的超时属性设置为1。这意味着get()将等待1秒,以在队列中出现新的 K 线,如果在 1 秒后没有 K 线,则生成异常。在异常发生时,我们只需中断循环,从而有效地终止线程:

def tradeLogic():
    while True:
        try:
            bar = bar_feed.get(block=True, timeout=1)
        except:
            break
        ####################################
        #     trade logic starts here      #
        ####################################
        ####################################
        #       trade logic ends here      #
        ####################################
        bar_feed.put(bar)
        System.F2.clear()
        System.F3.set()
        System.F2.wait()

我们省略了整个交易逻辑,因为它确实与我们用于我们交易应用第一个版本的逻辑相同。

注意,在函数代码的末尾,我们将 K 线返回到队列中:其数据将被订单处理组件所需要。并且,就像上一个函数的情况一样,我们设置F3标志,为下一个操作(订单处理)发出绿灯,清除F2,并停止交易逻辑线程,直到F2标志被设置。

接下来,我们必须相当大幅度地重写订单执行模拟器:生产版本和回测版本之间的区别在于,在回测时,我们只处理压缩数据,因此在每个 tick 上检查订单执行不再有意义。

回测期间模拟订单执行

让我们先模拟市场订单,因为它们最容易实现,并坚持以下指南:

  • 我们假设只有当交易逻辑在 bar 的收盘时间才能生成市场订单

  • 我们只在 bar 的收盘价时模拟市场订单的执行

  • 我们假设市场中的流动性总是充足的,因此我们不需要在执行订单之前检查它

  • 我们假设实际执行价格与请求的订单价格相同,因为我们没有实时 tick 数据来测试执行

考虑到所有这些因素,修改后的emulateBrokerExecution函数现在看起来会简单得多:

def emulateBrokerExecution(bar, order):
    if order['Type'] == 'Market':
        order['Status'] = 'Executed'
        if order['Side'] == 'Buy':
            order['Executed Price'] = bar['Close']
        if order['Side'] == 'Sell':
            order['Executed Price'] = bar['Close']

我们在这里没有添加任何标志,因为这个函数是在processOrders函数内部被调用的。让我们添加这个函数:你会看到它的逻辑与我们之前使用的非常相似,使用了实时 tick 数据:

def processOrders():
    while True:
        try:
            bar = bar_feed.get(block = True, timeout = 1)
        except:
            break

我们从一个类似的try...except语句开始,当 bars 队列中没有更多数据时,终止线程的执行。接下来,我们对系统元数据进行相同的更新,与之前不同的是,我们使用 bar 的收盘价而不是最后 tick 的价格:

        System.equity += (bar['Close'] - System.last_price) * System.market_position
        System.equity_timeseries.append(System.equity)
        System.last_price = bar['Close']

订单处理逻辑与 tick 驱动的代码非常相似,主要区别是没有风险管理检查(我们是否有足够的资金进行交易)和拒绝订单的处理:在回测期间,我们假设所有订单都得到了执行:

        while True:
            try:
                order = orders_stream.get(block = False)
                emulateBrokerExecution(bar, order)
                if order['Status'] == 'Executed':
                    System.last_price = order['Executed Price']
                    if order['Side'] == 'Buy':
                        System.market_position = System.market_position + order['Size']
                    if order['Side'] == 'Sell':
                        System.market_position = System.market_position - order['Size']
            except:
                order = 'No order'
                break
        System.F3.clear()
        System.F1.set()
        System.F3.wait()

在函数代码的末尾,我们再次添加相应的标志来控制线程的执行顺序。

好吧,这就完成了!我们现在必须做的就是检查回测所花费的时间(只是为了好玩)并启动线程:

start_time = time.perf_counter()
incoming_price_thread = threading.Thread(target = getBar)
trading_thread = threading.Thread(target = tradeLogic)
ordering_thread = threading.Thread(target = processOrders)
incoming_price_thread.start()
trading_thread.start()
ordering_thread.start()

但我们如何检查代码是否产生正确的结果呢?

当然,我们可以添加几个打印语句,就像我们在实时交易应用中做的那样,但回测的目标是不同的:我们希望在尽可能短的时间内处理尽可能多的数据,然后分析收集到的数据。5 年的 1 分钟历史数据 bar 超过 200 万个数据点,所以如果我们只是在每个 bar 上打印更新的权益价值,就会超过 200 万个打印——这将花费很长时间,因为print()是速度最慢的指令之一。那么,系统交易者如何分析策略的表现呢?

权益曲线和统计数据

当使用我们刚刚编写的代码运行回测时,我们保存一些基本统计数据:每个 tick 或 bar 更新的权益价值。如果我们绘制权益时间序列,我们得到一个权益曲线:这是交易系统随时间利润和损失动态的视觉表示。这样的图表是在回测完成后首先要检查的东西:

  • 如果权益曲线显示随着时间的增长,那么策略未来可能表现良好(但不是保证!)的机会就存在

  • 如果权益曲线随着时间的推移表现出稳定的系统性损失,这再次可能不是真的坏:考虑反转交易逻辑的规则

  • 如果权益曲线围绕零振荡,这可能是最坏的情况,因为这个策略逻辑未来不太可能赚钱

让我们在回测完成后,在我们的代码中添加绘制权益曲线的代码。我们将使用我们在第八章使用 Python 进行外汇交易中的数据可视化中讨论的技术,所以我建议在这个时候复习一下使用matplotlib

matplotlib的主循环不能在线程中运行(至少不容易),因此我们必须在主线程中添加图表(就像我们在第八章使用 Python 进行外汇交易中的数据可视化)中绘制实时 K 线图表时做的那样),并关注incoming_price_feed线程:当它存活时,我们只需等待并做任何事情,但一旦它完成工作,我们就绘制权益曲线。

因此,我们只需在代码开头的import部分添加import matplotlib.pyplot as plt,并在所有线程启动后,将其末尾添加以下简单的无限循环:

while True:
    if incoming_price_thread.is_alive():
        time.sleep(1)
    else:
        end_time = time.perf_counter()
        print(f'Backtest complete in {round(end_time - start_time, 0)} second(s).')
        plt.plot(System.equity_timeseries)
        plt.show()
        break

如果你一切操作正确,并且使用了与我相同的历 史数据文件,你将看到以下图表:

图 11.7 – 基于前 10 根 K 线的样本策略的权益曲线

图 11.7 – 基于前 10 根 K 线的样本策略的权益曲线

这看起来很棒,但我们如何确保这个结果是正确的?如果回测器模拟性能不正确,我们就不能依赖回测结果。

幸运的是,检查这个结果并不困难。如您所记得,我们故意使用了一个非常简单的测试策略,该策略几乎在每一根 K 线上都生成订单。因此,我们可以手动重建一个类似的权益曲线,例如使用 MS Excel 或 OpenOffice,并将其与我们的回测应用程序生成的图表进行比较。

让我们打开数据文件并删除不必要的列(UpVolumeDownVolumeTotalVolumeUpTicksDownTicksTotalTicks)。

图 11.8 – 源数据文件的前 10 根 K 线

图 11.8 – 源数据文件的前 10 根 K 线

接下来,我们必须重现策略逻辑:如果 K 线收盘价上涨(close > open),则我们买入;如果 K 线收盘价下跌,则我们卖出。我们将添加一个包含我们交易方向的新的列:

图 11.9 – 确定模拟交易的方向

图 11.9 – 确定模拟交易的方向

然后,我们必须添加一个列,通过将 K 线收盘价之间的差异乘以方向和交易量来计算每根 K 线的实际 PnL:

图 11.10 – 计算每根 K 线的回报

图 11.10 – 计算每根 K 线的回报

最后,我们必须计算每条柱状图的累计回报总和,这实际上就是权益时间序列:

图 11.11 – 计算权益时间序列

图 11.11 – 计算权益时间序列

现在,如果我们通过基于 I 列数据创建折线图来绘制权益曲线,我们会看到以下内容:

图 11.12 – 在 LibreOffice 中手动重建权益曲线

图 11.12 – 在 LibreOffice 中手动重建权益曲线

我们可以看到,权益曲线与我们代码绘制的曲线完全一致——这意味着我们的回测是可靠的!检查过一次之后,我们现在可以在进行任何测试时都信任其结果。

我敢打赌你迫不及待地想看到我们伟大策略的长期表现报告,而不仅仅是 10 条柱状图。记住,我们源数据文件中的 1 条柱状图代表 1 分钟,所以 10 分钟的回测并不具有代表性。让我们对前 1 百万条柱状图进行测试,这相当于大约 32 个月的历史。我们只需要修改代码中的一行:在getBar()函数中的if counter == 1000000:处将10替换为1000000

现在,我们还可以根据控制台输出估计回测速度。在我的(远非最新)笔记本电脑(2012 年款 Macbook Pro,配备四核 Core i7 处理器、SSD 驱动器和 16 GB 内存)上,读取文件数据花费了 12 秒,处理 1 百万条柱状图花费了 93 秒。还不错:我们可以在不到 2 分钟内模拟 32 个月的时间!

那么,从这样一个长期的角度来看,权益曲线又是怎样的呢?这里就是:

图 11.13 – 使用前 1,000,000 个数据点计算得到的样本策略的理论表现(权益曲线)

图 11.13 – 使用前 1,000,000 个数据点计算得到的样本策略的理论表现(权益曲线)

哇!看起来就像是交易的圣杯!这样的原始策略真的能够在如此长的时间内产生如此稳定的回报吗?

一般而言,每次你得到这样乐观的结果时,都要专注于寻找错误。在我们的案例中,我们怀疑我们没有在交易逻辑上犯错误——它太原始了,我们手动测试过。那么,我们在回测中可能遗漏了什么导致了这个不切实际的良好结果?或者,这个结果确实是现实的?

当然,不幸的是,它并不是。

让我们再次回到emulateBrokerExecution函数。我们假设任何订单都是在收盘时执行的——这很好,因为我们没有回测的 tick 数据。但是我们的代码在执行买入和卖出订单时没有区别:它们都以相同的价格执行,在我们的例子中是出价。但是当我们在本章早期测试实时交易应用时,我们发现以实际价格(卖出订单的出价和买入订单的询价)执行订单可能会在损益表(PnL)上产生很大的差异。因此,由于我们的历史数据中没有询价,让我们来模拟它:我们将典型价差加到收盘价上,从而考虑到出价和询价之间的差异:

        if order['Side'] == 'Buy':
            order['Executed Price'] = bar['Close'] + 0.00005

在现实中,EURUSD 的价差可能从 0 低至 0.0010 甚至更高(通常在重要经济新闻发布之前;参见第六章基本面分析的基础及其在 FX 交易中的可能用途),但可以安全地假设 1/2 点足以模拟平均价差。

让我们再次运行回测并查看权益曲线:

图 11.14 – 测试策略更现实的模拟权益曲线

图 11.14 – 测试策略更现实的模拟权益曲线

这是一个多么激进的变化!现在,策略不再是稳步盈利,而是稳步亏损,而且亏损速度非常快:它仅通过交易一个所谓的迷你合约(10,000 基础货币)在不到 3 年的时间里亏损了 100,000 美元。

这是怎么发生的?

尽管策略在纸上没有考虑价差的情况下盈利,但平均而言,它每笔交易产生的纸面金额非常小:它甚至少于价差。一旦我们正确地模拟了以出价和询价执行订单,圣杯就消失得无影无踪,而令人悲伤的真相被揭露。

注意

当你进行任何市场研究和发展任何策略时,这个故事应该始终牢记在心。始终确保尽可能真实地模拟市场条件——以避免得到过于乐观的理论结果和在生产中遭受的痛苦失望。

所有这些之后最伟大的新闻是,你现在有一个可以信赖的工具:我们的回测平台。

总结 – 我们接下来该去哪里?

恭喜你在我们的学习中走这么远!我知道这一章非常长,但希望不会无聊。我们几乎涵盖了开发实时交易应用和回测器的所有方面,所以现在,你拥有了强大的工具,这些工具应该能帮助你开发出优秀的交易策略。

让我们快速总结一下本章学到的内容,并概述一些前景。

我们现在完全理解了任何交易应用的所有四个基本组成部分:接收数据、处理数据、生成订单以及控制它们的执行。

我们还熟悉最典型的技术问题,例如错误地模拟订单执行或处理非市价价格,我们也知道如何解决这些问题。

然后,我们学习了如何通过使用队列和线程事件对象来同步多个线程,我们也知道如何确保交易应用中的每个组件都恰好运行在预期的时刻。

接下来,我们学习了如何手动重建策略性能的部分来检查测试或实时订单生成和执行的准确性。现在,我们可以 100%确信我们可以信赖我们编写的代码。

我们甚至创造了我们交易的第一件圣杯——通过批判性地审查代码,我们立即将其拆解成碎片,从而学到了系统交易的主要教训:任何最微小的细节都不能被忽视,无论是故意还是偶然,在将结果用于生产之前,你应该检查你的结果两次,以避免在用真钱交易时遇到非常不愉快的惊喜。

现在,让我们指出我们可以在我们的开发中更进一步的地方。

首先,目前,我们的平台仅支持市价订单。是的,你们可能还记得,在前一章中,在许多情况下,市价订单是首选的,我们可以用市价订单来模拟所有其他类型的订单。然而,至少从开发目的来看,添加对限价和止损订单的模拟会很好。

在当前形式下,该代码在仓位和策略层面均未实现任何风险管理。添加至少基本的止损订单对于保护交易账户免受意外灾难性场景的影响是至关重要的。

同时,计算策略性能的一些非常基本的统计数据也会很棒:目前,我们只能分析权益时间序列,但我们想了解更多关于平均交易价值、交易次数、盈利交易百分比等信息。

当然,我们还可以通过添加多个策略、将它们分组到投资组合中,并通过添加多个实时数据流来实现平台的进一步复杂化,以达到最高水平的复杂性。尽管这肯定超出了本书的范围,但我鼓励你们大胆好奇,就像任何真正的研究者应该做的那样,玩转现有的代码并尝试改进它——你们会发现,从长远来看,你们的努力将得到高度回报。

本书剩余的章节致力于实现特定类型的交易策略。我们将为本章创建的回测平台添加一些组件,但不会进行重大更改。相反,我们将专注于开发战略逻辑并分析其理论性能。

第四部分:策略、性能分析和展望

在前面的部分中,我们获得了足够的知识,能够开发出一个既适合回测又适合——经过最小修改——实时交易的平台。我们甚至编写了一个模拟策略,并如预期的那样发现,如果没有经过适当的测试,它是无法赚钱的。

第四部分 解释了如何构建一个盈利的交易策略,从寻找交易想法到将其编码实现。我们还将学习如何生成最重要的策略性能数据,并分析它,以最终判断这样的策略是否可以在现实生活中使用。然后,我们将看到如何正确地实施限价和止损订单,并考虑另一种基于完全不同交易想法的交易策略。最后,最后一章提出了一些关于算法交易进一步自我发展的指导方针,并附有链接到有用和有价值的资源。

本部分包括以下章节:

  • 第十二章示例策略——趋势跟踪

  • 第十三章交易还是不交易——性能分析

  • 第十四章现在该去哪里?

第十二章:样本策略 – 趋势跟踪

在上一章中,我们开发了两个交易应用:一个用于生产,另一个用于促进交易策略的测试和研究。更好的说法是,这些都是同一应用的两个版本,具有不同的数据处理模块。我们设计它们,以便在回测应用中开发的交易逻辑可以在生产应用中使用,无需修改(或在复杂情况下只需进行最小修改)。我们还使用一个样本“策略”测试了代码,发现代码运行正确,但“策略”一直在亏损——幸运的是,只是在纸上(这就是为什么我始终用斜体这个词的原因)。

现在,是时候学习研究并开发应用于外汇市场的一种最流行的经典交易策略的过程了。我们已经在书中讨论过它,但只是从定性而不是定量的角度。现在我们将提出一个正式的数学模型,并在代码中实现它。当然,我们还将进行回测,以检查结果是否可用于实际交易。

在本章中,我们将考虑趋势跟踪策略,并学习以下主题:

  • 回顾趋势跟踪 – 交易设置

  • 选择市场和准备数据

  • 趋势跟踪策略 – 实施

回顾趋势跟踪 – 交易设置

在第九章《交易策略及其核心要素》中,我们考虑了趋势跟踪,并得出结论,尽管它是最简单和最直观的交易策略之一,我们仍然需要一套规则来确定以下内容:

  • 市场中是否存在趋势

  • 趋势是向北还是向南(即上升或下降)

  • 当是时候加入趋势时(分别买入或卖出)

  • 当是时候退出现有头寸时(因此我们预计趋势将结束和/或反转)

让我们在接下来的章节中了解更多。

确定趋势,第一部分 – 市场模型

如果我问你户外是晴天还是雨天,我确信在大多数情况下你不会犹豫给出答案。你可以很容易地区分它们,因为你非常熟悉许多帮助你做出决定的属性。确实,区分光明与黑暗、温暖与寒冷、湿润与干燥等都很简单。

现在,假设我问你是否有轻微的雨或雾,并在半夜这么做。我想象你会很难区分它们。很可能会出去,试图感受皮肤上的空气,闻闻空气,最后带着某种像“嗯,看起来像是在下雨。”这样的东西回来。在这种情况下,你必须进行多项测试并使用它们的结果来做出判断。

你为什么能够进行这些测试并使用它们的结果?

因为你在心中有一个模型,一个天气模型。除非你是这个职业,否则你不会去想它。在大多数情况下,你都会直觉和即时地做出决定。然而,如果你处于压力之下,你可以找到一些或多或少正式的属性——如湿度、温度和风——来决定天气,再次在天气模型的框架内。

现在,让我们以天气为例,开发一个市场趋势模型。

首先,最重要的是,我们应该决定趋势是始终存在还是偶尔发生。如果我们将这两个模型(永久趋势和偶尔趋势)与我们的天气例子进行比较,我们会看到以下类比:

市场 天气
市场总是处于上升趋势或下降趋势。趋势可能更明显或不太明显。 风总是吹着,只是有时强有时弱。
市场可以是趋势模式非趋势模式 有时风在吹,有时风根本就不吹。

表 12.1 – 市场趋势与天气之间的类比

尽管看起来相当简单,但这个表格完美地说明了在建模过程(市场或天气)的方法上的基本差异。模型开发者必须决定观察到的过程只能处于一种状态(只有趋势;只有风在吹)或者可以处于多种状态(市场例子中的趋势或非趋势;天气例子中的有风或无风)。

话虽如此,我们得出一个非常重要的结论。

没有这样的东西叫做模型。模型只是试图以一定的精确度解释观察到的现象,并服务于做出决策的目的。

因此,模型帮助做出实际决策的能力是模型有效性的唯一标准。如果你通过看窗外就能知道如何为一天穿衣,那么天气模型就是有效的。如果你通过咨询复杂的设备来决定是否带伞,但仍然经常全身湿透,那么很可能,这个复杂设备中使用的模型是无效的。

在市场中,情况类似。如果我们能提出一个简单的市场模型,尽管如此,它仍然能够持续优于基准(见第九章交易策略及其核心要素),那么这是一个可接受的模型。同时,我们可能有一个极其复杂的模型,它使用人工智能和量子力学,但如果从长远来看它不能打败市场,那么它可能只对学术研究有吸引力。

随着我们迈出算法交易的第一步,让我们从简单的事物开始。如果简单的模型不起作用,我们可以在之后使我们的模型更加复杂。

在我们目前的案例中,我们应该决定如何从趋势的角度来建模市场。

  • 市场总是处于趋势中,无论是上升还是下降;只是这些趋势的持续时间可能长或短

  • 市场处于趋势或非趋势状态,我们在确定上升趋势或下降趋势之前应该区分这两种状态

当然,第一个模型更简单:我们不需要提出一个区分趋势和非趋势的方法,现在我们只关注那些能够区分上升趋势和下降趋势的技术设置。如果这种方法不起作用,我们可以回到这个决策点,改变我们的模型,然后从头开始研究。

因此,我们在我们的清单上确定了第一个要点:我们选择“始终处于趋势”的市场模型进行进一步研究。

既然我们已经确定了市场模型,接下来让我们寻找一个合适的工具来区分上升趋势和下降趋势。同样,我们从这个最简单的解决方案开始:移动平均线。

确定趋势,第二部分 – 移动平均线

经常用于确定趋势的经典技术分析研究之一是移动平均线。在第七章《技术分析及其在 Python 中的应用》中,我们已经考虑了移动平均线,并发现它们作为数字滤波器,消除高频(短期价格波动),并保留低频,我们将这些视为价格运动中的主导长期趋势。让我们通过在价格图表上绘制 20 周期移动平均线(MA20)来快速更新我们的知识。

图 12.1 – EURJPY 1 分钟图表上的 20 周期移动平均。图表由 Multicharts 提供

图 12.1 – EURJPY 1 分钟图表上的 20 周期移动平均。图表由 Multicharts 提供

我们可以看到,有时,柱状图的收盘价倾向于保持在 MA20 之上,而有时则低于它。合理地假设,只要这些收盘价保持在 MA20 之上,市场就处于上升趋势,而如果它们保持在之下,市场就处于下降趋势。

这个技术设置的问题有时价格在移动平均线之上或之下停留的时间太短。我们之前已经同意,在我们的模型中市场总是处于趋势中,我们不区分任何特殊的非趋势条件。然而,可能存在更好的技术设置,某种能够更好地指示上升趋势或下降趋势,而不需要那么多短期趋势,这是我们直觉上甚至不愿意称之为趋势的。

是的,有,这是最古老的技术交易设置之一:我们使用两个移动平均线,一个短期,另一个长期。然后,我们只有在以下两个条件都满足时才认为存在上升趋势:

  • 价格高于短期移动平均线

  • 短期移动平均线本身位于长期移动平均线之上

对于下降趋势来说,情况类似:

  • 价格低于短期移动平均线

  • 短期移动平均本身位于长期移动平均之下

让我们看看它在图表上可能是什么样子。

图 12.2 – 添加长期移动平均有助于排除不希望出现的情况

图 12.2 – 添加长期移动平均有助于排除不希望出现的情况

在前面的图中,我添加了一个 50 期移动平均(MA50)到现有的 MA20,并放大了图形以查看更多细节。由虚线椭圆形包围的区域说明了添加第二个移动平均如何过滤掉某些(但当然不是所有)不希望出现的情况。如果我们仅仅通过收盘价高于或低于 MA20 来决定是上升趋势还是下降趋势,那么在图 12.2中显示的阶段,我们就必须决定是否存在下降趋势。然而,如果我们使用我们之前建议的设置(即价格应该在 MA20 和 MA50 之下),那么我们既不能将其认定为上升趋势也不能认定为下降趋势。因此,在交易逻辑中,我们简单地跳过这个区间——然后我们可以看到随后的价格变动发展成为一个真正的上升趋势。

太好了,现在我们已经覆盖了我们列表中的两个要点——我们知道市场何时有趋势以及其方向。现在我们需要决定何时实际进入和退出市场中的头寸。

进入和退出规则

在确定技术交易设置的各个要素之后,关键问题仍然是何时实际进入和退出头寸。在某些情况下,这可能是一个非平凡的问题,对这个问题的回答可能会严重影响策略的结果性能。然而,在我们的简单模型中,我们可以假设一旦满足趋势条件,我们就可以进入市场。这意味着当以下条件成立时,我们就会开立新的多头头寸:

  • 条的收盘价高于短期移动平均

  • 短期移动平均位于长期移动平均之上

  • 目前,我们在市场上没有多头头寸(因此我们不会增加现有头寸,如果我们已经多头,我们也不会再买入)

至于退出开放的头寸——同样,在我们的市场模型中,实际上没有必要退出头寸,因为该模型假设市场总是处于趋势中。因此,我们只在开立空头头寸时退出多头头寸,反之亦然。

换句话说,我们计划一个始终在市场的策略,并在检测到趋势变化时立即改变交易的方向。

与市场模型一样,如果我们测试当前简单模型的结果不可接受,我们可以稍后改变这种方法。

资金管理

资金管理意味着每次新订单你将交易多少。有许多资金管理理论和技巧,从非常简单到相当复杂。不幸的是,我们无法在一个章节中涵盖所有这些内容——那需要一本书!但既然我们现在保持简单,并且更感兴趣于了解交易逻辑是如何在一般情况下工作的,那么我们也使用最简单的资金管理概念:我们将对所有交易使用相同的恒定交易规模,当我们应该从多头转为空头或从空头转为多头时,我们将将其翻倍。我们在上一章开发回测平台时已经这样做过了。

一个经验法则是:如果你的策略在恒定交易规模下表现良好,那么通过资金管理可以进一步提高其表现。如果策略在恒定交易规模下表现不佳,那么尝试通过使用各种资金管理规则来提高其表现通常会失败。

因此,我们已经成功覆盖了我们在本章开头概述的四个关键点:

  • 我们知道市场何时处于趋势中

  • 我们知道趋势的方向

  • 我们知道何时进入市场

  • 我们知道何时退出市场

此外,我们还知道每次交易我们有多少风险。

太好了,现在我们可以继续选择我们将要使用趋势跟踪进行交易的市场,并准备相关数据。

选择市场和准备数据

关于系统交易有一个非常常见的误解:人们认为技术交易策略应该在任何市场上都有效。我希望前几章已经消除了这个神话。举个例子,让我们回忆一下著名的 EURCHF 市场,当时瑞士国家银行正在将瑞士法郎的汇率钉住欧元(参见第九章交易策略及其核心要素)——如果价格几乎没有任何变动,就去用趋势跟踪策略进行交易吧!

即使我们将这样的极端例子放在一边,选择市场本身也可以是一个非同寻常的任务。大多数时候,即使我们能够对哪种策略应该表现更好有一个明智的猜测,我们也必须尝试许多市场。然而,现在我们将使用一些一般性的指导方针。

首先,由于我们专注于趋势跟踪,我们希望交易一个充满趋势的市场(尽管这听起来可能像是一个不言而喻的真理)。如果我们处于外汇领域,我们可能希望关注两种货币之间利率差异最大的货币对(参见第六章基本分析部分,其中简要讨论了利率和息差交易,以了解利率和息差交易),因为这是非常少数可能导致形成或多或少长期趋势的因素之一。因此,像澳大利亚元或新西兰元与日元或甚至美元(尤其是当美国利率较低时)这样的货币对可能是一个好的开始。

选择澳大利亚元作为趋势跟踪策略交易的原因之一是,澳大利亚经济依赖于黄金生产,就像加拿大经济依赖于石油(尽管可能程度较小)。因此,澳大利亚元的汇率容易受到黄金和其他出口商品价格相应变化的影响。由于商品价格由于生产周期而表现出周期性行为,我们可以看到这一点在 AUDUSD 或 AUDJPY 中有所反映。因此,考虑到这两个因素,选择 AUDUSD 作为尝试趋势跟踪策略的第一个货币对似乎是一个自然的选择。

第二,我们应该决定时间框架或数据分辨率。尽管之前使用移动平均线的样本图表是以 1 分钟分辨率制作的,但日内数据实际上并不适合趋势跟踪策略。原因是日内外汇市场在波动模式上表现出强烈的周期性行为(参见第三章流动性和波动性 – 如何一个转化为另一个部分,从开发者的角度来看,外汇市场概述)。这些市场在白天活跃,在夜间缓慢。这种周期性会产生许多虚假趋势,使确定真实趋势的任务变得显著更加困难。同时,日时间框架没有这种特性,我们可能期望在这个数据分辨率(当然,取决于市场是否倾向于趋势)上有更稳定的趋势行为。因此,当我们有选择通过向交易逻辑中添加一个模块来使我们的模型更加复杂,该模块可以区分真实虚假趋势时,我们宁愿使用更高分辨率的 数据来完全消除这个问题。

因此,我们已经确定了市场(让我们从 AUDUSD 开始)和数据分辨率(每日)。一如既往,让我指出,如果我们得到不满意的结果,我们可以尝试不同的市场和时间框架。

现在我们已经确定了所有先决条件,让我们开始编码。

将数据压缩到日时间框架

让我们从编写一个简单的实用程序开始,该实用程序将市场数据压缩到所需的分辨率,并将其转换为所需的格式,与我们的回测和实时交易代码兼容(见第十一章回测和理论表现)。我们将使用实时版本的getBarRealtime()函数,并稍作修改作为独立的实用程序。

此实用程序应执行以下操作:

  • 读取源数据文件(tick 或 1 分钟条)

  • 将数据聚合到任何更大的时间框架

  • 使用与回测兼容的格式将数据保存到磁盘

像往常一样,我们从导入开始:

import csv
from datetime import datetime

我们添加了一个类似于第六章基本面分析的基础及其在 FX 交易中的可能用途中使用的滑动窗口类,但我们将以不同的方式使用它:在当前条形图和上一个条形图上存储任何参数(价格、时间、成交量或任何其他)的值。因此,我们添加了两个相应的方法来快速检索这些值:

class slidingWindow:
    def __init__(self, len):
        self.data = [0 for i in range(len)]
    def add(self, element):
        self.data.pop(0)
        self.data.append(element)
    def last(self):
        return self.data[-1]
    def previous(self):
        return self.data[-2]

然后,我们指定源文件和目标文件,并读取保存的数据(tick 或 1 分钟数据)——这与我们在上一章中做的大致相同:

source_file = open("LMAX AUD_USD 1 Minute.txt")
dest_file = open("AUDUSD_daily.csv", "w")
csvFile = csv.DictReader(source_file)
all_data = list(csvFile)

我们立即将第一行写入目标文件——这一行将作为进一步处理文件作为 CSV 的标题:

dest_file.write(("Date,Time,Open,High,Low,Close\n"))

然后,我们创建slidingWindow类的实例,并初始化第一个条形图,我们将对其进行聚合并保存到目标文件:

timestamp = slidingWindow(2)
bar = {'Open': 0, 'High': 0, 'Low': 0, 'Close': 0}

现在,我们开始遍历源文件中的所有样本,转换数据,并将时间戳添加到时间滑动窗口中:

for sample in all_data:
    open = float(sample[' <Open>'])
    high = float(sample[' <High>'])
    low = float(sample[' <Low>'])
    close = float(sample[' <Close>'])
    ts = datetime.strptime(sample['<Date>'] + 'T' + sample[' <Time>'] + 'Z', "%m/%d/%YT%H:%M:%SZ")
    timestamp.add(ts)

备注

源文件的标题格式可能不同。在我这本书中使用的文件中,至少有两种不同的格式:普通单词('Open''High'等)和三角括号中的单词(<Open>等)。请小心,不要忘记根据你将要使用的数据源调整这段代码!

如果时间戳的日期与上一个时间戳的日期不相等——这意味着新的一天开始了——我们将保存更新的每日条形图到目标文件,并重新初始化条形图:

    if timestamp.previous() != 0:
        if timestamp.last().date() != timestamp.previous().date():
            if bar['Open'] != 0:
                dest_file.write(','.join(map(str,[*bar.values()])) + "\n")
            bar = {'Date': timestamp.last().date(), 'Time': timestamp.last().time(), 'Open': open, 'High': high, 'Low': low, 'Close': close}

最后,我们更新正在形成的条形图:

    bar['High'] = max([bar['High'], high])
    bar['Low'] = min([bar['Low'], low])
    bar['Close'] = close
    bar['Time'] = timestamp.last().time()

for循环完成后,不要忘记关闭目标文件:

dest_file.close()

如果你使用与我相同的 AUDUSD 1 分钟历史数据运行此代码(你可以在 GitHub 上找到它以及代码),你将得到一个 CSV 文件,其中'Time'列有两个不同的时间:17:0023:59。为什么会这样?

实际上,这是一个非常重要的问题,值得一个深刻的答案。

注意时间!

不,这并不意味着你应该每隔一分钟就看看你的手表。这意味着在处理市场数据时,时间是导致混淆的非常因素,尤其是在处理来自如外汇等去中心化市场的数据时。

使用来自集中交易所的数据相对容易一些:在这种情况下,我们始终知道交易所位于哪个时区以及其工作时间。因此,从这个交易所获取的任何市场数据中,所有时间戳都将与交易所所在的时区相同,并且仅在市场开盘和收盘之间。

在外汇市场中,情况不同。我们知道这个市场没有单一的交易所,并且它几乎每周工作 24 小时,5 天。

请注意,是“几乎”。

首先,我们必须考虑时区。每个外汇数据供应商和每个外汇经纪商都可能以他们认为正确的任何时区提供数据。最常用的是 GMT(UTC)或 BST(UTC+1)用于伦敦,CET(UTC+1)或 CEST(UTC+2)用于法兰克福,以及 EST(UTC-5)或 EDT(UTC-4)用于纽约。在处理时间戳之前,你应该检查数据源使用的时区。

其次,我们必须考虑工作时间。大多数外汇交易场所周日大约在下午 5 点开始交易(纽约时间;纽约银行结算时间;参见第三章从开发者角度的外汇市场概述),但有些可能更晚开始。同样,周末收盘时间:大多数场所周五在纽约的同一时间下午 5 点关闭,但有些甚至提供周末交易。我们无法认真考虑这种周末交易,因为周末交易的成交量可以忽略不计,但你可能会从提供周末交易的场所获取市场数据,这些周末数据将给你的研究过程带来相当多的混乱。如果你计划交易外币,很可能是它们只在各自国家中央银行的工作时间内或稍长一点的时间内进行交易。

除了正常的工作时间外,还有一些例外,大多在节假日。例如,如果你在圣诞节前后看到提前收盘或延迟开盘,不要感到惊讶。

第三,我们必须考虑如何解释 0:00 时间。一些数据提供商将这个时间视为新的一天的开始,而另一些则认为它指的是前一天。此外,一些数据提供商甚至没有这样的时间戳,他们数据中的最后时间是 23:59(对于 1 分钟数据)。

这个 0:00 的时间很令人困惑。当我们处理以条形图压缩的数据时,条形图的时间戳表示条形图关闭的时间。因此,0:00 表示条形图在午夜关闭。但它们仍然代表午夜之前发生的价格变动,所以它们属于刚刚结束的那一天!所以,如果你在处理时间时想绝对精确,你可能需要在你的代码中添加一些额外的检查,以考虑我们刚才讨论的问题。

现在,让我们看看我们例子中使用的数据,看看我们实际上做了什么。这些数据是在纽约时区,所以一周的最后时间是 17:00 – 这就是我们每周五压缩的每日数据中所看到的。这个数据提供商将 0:00 视为新一天的第一时间,因此由于我们按日期划分日期而不考虑时间,一天的最后时间现在是 23:59。

我们可能想要修改条形图工具代码中的新一天条件。一个可能的解决方案可能是这样的条件:

if (timestamp.last().date() != timestamp.previous().date() and str(timestamp.last().time()) != '00:00:00') or (str(timestamp.previous().time()) == '00:00:00'):

如果我们现在运行修改后的代码,我们会得到正确压缩的数据,但请注意,现在 0:00 的时间戳表示一天的结束,而不是开始!

现在我们已经准备好了所有数据,是时候尝试编写我们第一个策略的代码了。

跟随趋势策略 – 实现

由于我们打算使用上一章中开发的回测代码(参见第十一章回测和理论表现)中的带有历史数据馈送回测平台部分),我们只需要添加支持所需对象的小段代码。

注意

不要忘记将数据源更改为我们刚刚创建的包含每日 AUDUSD 数据的文件!

让我们从添加slidingWindow类来实现移动平均开始。显然,我们从上一节中的代码中复制它。代码(像类声明一样)通常放在导入之后和第一个函数声明之前:

class slidingWindow:
    def __init__(self, len):
        self.data = [0 for i in range(len)]
    def add(self, element):
        self.data.pop(0)
        self.data.append(element)

随着你逐渐开发各种策略,迟早你会发现在大多数策略中都会使用到许多类或函数,因此你可以将它们移动到一个单独的模块中,并将该模块导入到任何策略原型中。

我们添加两个滑动窗口来实现移动平均:

data_window_small = slidingWindow(5)
data_window_large = slidingWindow(20)

为什么移动平均周期使用520?嗯,没有特别的原因:当我们处理每日数据时,5通常用来表示一个工作周,而20表示一个工作月。这些值通常可以在技术分析研究中找到。其他流行的周期是50(代表一个季度)和200(代表一年)。无论如何,这只是一个草案,所以我们将在评估策略性能后,能够修改这些值。

然后我们有实际计算移动平均值的函数:

def moving_average(data):
    return sum(data) / len(data)

我们现在需要修改的是tradeLogic()函数的一部分。它是位于trade logic starts heretrade logic ends here注释之间的代码块:

        ####################################
        #     trade logic starts here     #
        ####################################

首先,我们检索一个新的收盘价并将其添加到两个滑动窗口中。然后我们计算移动平均值:

        close = bar['Close']
        data_window_small.add(close)
        data_window_large.add(close)
        ma_small = moving_average(data_window_small.data)
        ma_large = moving_average(data_window_large.data)

现在是主要部分:入场条件。如果收盘价低于短期移动平均线,且短期移动平均线低于长期移动平均线,我们就卖出。别忘了,我们只有在没有已经开放空头头寸的情况下才这样做(参见第十一章中的带有实时数据流的交易应用部分,回测和理论表现):

        if close < ma_small and ma_small < ma_large and System.market_position >= 0:

'Sell'子句中的其余交易逻辑代码保持不变,我将其复制在这里只是为了保持完整性:

            order = {}
            order['Type'] = 'Market'
            order['Price'] = close
            order['Side'] = 'Sell'
            if System.market_position == 0:
                order['Size'] = 10000
            else:
                order['Size'] = 20000
            orders_stream.put(order)

对于买入订单来说是对称的:当收盘价高于短期移动平均线,且短期移动平均线高于长期移动平均线时,我们买入:

        if close > ma_small and ma_small > ma_large and System.market_position <= 0:
            order = {}
            order['Type'] = 'Market'
            order['Price'] = close
            order['Side'] = 'Buy'
            if System.market_position == 0:
                order['Size'] = 10000
            else:
                order['Size'] = 20000
            orders_stream.put(order)
        ####################################
        #      trade logic ends here      #
        ####################################

就这样。不需要对其他任何内容进行修改。

你准备好测试你的第一个交易策略了吗?让我们运行代码并查看权益曲线。

如果你到目前为止一切操作正确,你应该会看到一个类似于图 12.3中所示的权益曲线。

图 12.3 – 使用 AUDUSD 每日数据对趋势跟踪策略进行回测的权益曲线

图 12.3 – 使用 AUDUSD 每日数据对趋势跟踪策略进行回测的权益曲线

还不错!在第十一章回测和理论表现中,我们讨论了权益曲线,并指出交易者(尤其是投资者!)正在寻找能够随着时间的推移展示持续增长的策略。由于我们的权益曲线是我们趋势跟踪策略的 PnL 每日表示,我们可以同意这个策略确实展示了权益的增长。

然而,这个结果引发了一些进一步的问题。x轴和y轴上的数字意味着什么?我们如何从金钱或百分比增长的角度来解释这个结果?我们能否说回测展示的增长是一致的?这些问题以及其他问题将在下一章中进行讨论。

摘要

让我们快速回顾一下本章所学的内容。这确实是所有我们在前几章中获得的知识和技能汇聚并转化为一个工作交易应用的关键点。不仅如此,实际上,我们现在拥有了一个可扩展的交易平台,适用于研究和实时交易。我们提出了一种稳健的平台设计,保持了架构的模块化和可扩展性。我们学习了如何同步线程,以确保平台模块执行的正确顺序,同时保持这些模块的隔离。我们看到了使用各种数据源的实用示例,这些数据源允许平台同时处理实时数据流和历史数据。我们将交易逻辑完全从应用程序的其他部分隔离出来,因此现在我们可以使用回测来开发策略,然后立即将代码复制粘贴到我们平台的版本中。最后,利用前几章中关于外汇市场的知识,我们开发了一个简单的趋势跟踪策略,对其进行了测试,并看到了有希望的结果。

现在是时候分析我们的结果,以全面了解策略的行为和性能了。

第十三章:交易与否——性能分析

在上一章中,我们开发了一个简单的趋势跟踪策略,进行了回测,并生成了我们的第一条权益曲线(策略随时间利润和损失的视觉表示)。直观地,我们已知盈利策略的权益曲线应该随时间增长,增长越陡峭,越好。乍一看,我们的策略似乎符合这一要求,但当然,如果能基于事实而不是情绪来发表意见会更好。因此,我们希望有一个可以用来作为指标的定量指标:如果其值大于某个值,则策略是好的。如果它更小,则策略不好。

如你所猜想的,这样的单一、通用的指标并不存在。交易策略是一个非常复杂的结构,即使其逻辑看似简单,其表现也应该从不同的角度进行分析。我们感兴趣的不只是特定策略可能带来的多少金钱,还包括为了实现这样的收益我们应该投入多少作为风险资本。同样,不仅整体净利润,而且利润(和损失!)随时间分布的情况也很重要。最终,我们想要确保策略确实可交易:在真实市场中进行的交易将与回测期间模拟的交易或多或少相似。

这章远非交易策略性能分析的终极指南:这个主题如此广泛,以至于可能需要一本书,甚至多本书。在这里,我们将考虑最关键的指标,看看它们如何帮助评估策略的表现,并做出我们的主要决定——是否尝试这个策略或将其搁置。

在本章中,你将了解以下主题:

  • 交易分析

  • 平均交易和交易成本

  • 衡量性能——alpha 和 beta 重访

  • 净利润与持有

  • 回撤

  • 杠杆的力量——或者我需要交易多少?

交易分析

如你所记,交易和投资之间的区别,从广义上讲,是交易的数量。如果你在资产中持有头寸 1 年或更长时间,那么它就是投资。如果你在同年内多次买卖同一资产或不同资产,那么它就是交易。

为什么是一年?

根据大多数税法,持有头寸 1 年零 1 天可以将其视为投资,由此产生的任何利润将以折扣税率征税。如果你持有的头寸少于 1 年,则被视为交易,由此产生的利润将按全额收入税率征税。这条规则主要适用于股票交易,但至少使用 1 年作为参考期限是有意义的。

因此,策略在给定期间内积极开仓和平仓。无论整个期间的总净收益(或亏损)如何,首先,最重要的是,我们感兴趣的是每笔交易对整体结果贡献了多少——至少在平均意义上。

平均交易和交易成本

平均交易是衡量指标中最基本且最简单的之一:它只是策略实现的总净收益与总交易次数的比率:

公式

我们为什么需要这个值?

为了回答这个问题,我们应该再次回顾一下理论中的几个要点。

第三章,[从开发者角度的 FX 市场概述]中,我们考虑了市场的组织方式,并了解到我们能够买入和卖出的价格之间总是存在差异——即点差。无论策略中使用的订单类型如何,市场价格至少必须移动点差距离,才能将新开仓位从负区域移动到盈亏平衡点。

此外,别忘了在第十章[Python 中订单类型及其模拟]中考虑的各种流动性和订单执行问题。如果策略使用市价订单,它可能会遇到滑点、部分成交或拒绝(取决于 TIF 规范,参见同一章节中的时间有效:更好地控制执行)。无论如何,这只是交易成本中应被策略覆盖的另一个成本。

最后,别忘了经纪商和交易场所会收取佣金,这笔费用也应由交易策略承担。

因此,平均交易价值应该大于点差、典型滑点以及经纪商和/或交易场所佣金的总额。只有在这种情况下,策略才能在真实市场中盈利。

现在,让我们看看基于我们在上一章中开发的策略表现的实际数据。为了获得必要的值,让我们在策略代码中添加几行。首先,我们将向tradingSystemMetadata类添加一个list_of_orders属性——在这里,我们将保存所有已执行的订单。接下来,在processOrders()函数中,我们将在if order['Status'] == 'Executed':行之后添加以下语句:

System.list_of_orders.append(order)

最后,在回测代码的末尾,紧接在plt.plot(System.equity_timeseries)之后(参见第十一章历史数据回测平台回测和理论表现):

total_trades = len(System.list_of_orders)
print("Total trades:", total_trades)
print("Average trade:", System.equity / total_trades)

如果你现在再次使用相同的数据(澳元/美元每日数据)进行回测,你应该得到 37 笔总交易和平均交易价值约为 79.41。

这好还是不好?

关于交易次数,一般来说,次数越多越好。为什么?因为用来评估策略表现的指标都是基于统计的,而统计在数据集规模较大时效果最佳。我不会深入探讨这个意义上的“显著”的确切含义,因为这是一个过于复杂的话题,但为了简单起见,让我们假设我们需要至少 20-30 个数据点,这样统计指标才开始有意义。从这个角度来看,37 次交易看起来是令人满意的。

现在来看平均交易本身。它的价值是 79.41,但单位是什么?美元?澳大利亚元?也许还有其他?

在这里,我们应该回忆一下即期外汇交易总是以保证金进行(参见[第三章交易机制:再次一些术语部分,从开发者的角度来看的外汇市场概述)以及 1 个点的货币价值取决于交易规模。在第十一章带有实时数据流的交易应用部分,回测和理论表现中,我们已经指出,对于回测,我们可能希望使用一个交易规模,使得 1 个点等于 1 美元。这就是为什么我们选择了 10,000 的交易规模进行回测,现在我们可以将平均交易解释为79.41 个点。然后,我们最终可以决定这是否足够。

让我们总结一下这里提到的所有交易成本:点差、滑点费和佣金。

点差

点差取决于流动性提供者或经纪商(后者通常将他们自己的费用添加到点差中,这种做法称为加价),以及一天中的时间。今天 AUD/USD 的行业平均点差在 0.5 到 2 个点之间,很少增加到 5 个点以上,通常在经济新闻发布前。因此,可以安全地假设我们应该从平均交易价值中减去 1 个点,以考虑到市场在相对流动性较低的时间段的点差(记住,我们的策略在午夜下单,那时流动性比正常工作时间要薄)。

注意

如果你使用未经修改的emulateBrokerExecution函数进行回测代码,那么其中已经包含了 0.5 个点的点差。每次你用新的货币对运行新的策略时,都可以修改它,或者完全删除,并从最终的平均交易中减去点差。

滑点费

滑点主要取决于订单簿中的交易规模和流动性。如果你的订单规模从未超过订单簿顶部的流动性,那么在绝大多数情况下,滑点将为零。为了得到更精确的估计,我们需要分析交易时的订单簿顶部数据。如果你通过零售经纪商交易,那么你可能很难获得订单簿数据——通常,经纪商不会公布它。如果你使用经纪商作为 STP 以直接访问交易场所,那么你将至少获得带有成交量的订单簿顶部数据。让我们使用 LMAX 数据来进行估计。

我们的战略只在每天结束时进行交易,即午夜(纽约时间,EST/EDT)。这不是最流动的时间,但平均来看,在那个时间我们可以看到 50,000 到 200,000 AUD 在订单簿的顶部。因此,如果你的订单规模不超过 50,000 AUD,那么你可以安全地假设滑点可以忽略不计。然而,如果你的订单规模超过 50,000 AUD,那么你很可能会体验到高达 1 个点的滑点——这取决于实际的交易量。如果交易规模超过 500,000 AUD,那么滑点可能增加到 2 个点甚至更多。

我们应该使用哪个估计值?你如何知道你将使用哪个交易规模?

让我们再次回顾一下,外汇市场是以保证金交易。这意味着你不需要在经纪商账户中有全部的资金来开仓:经纪商将根据你的状态(零售或专业)提供的杠杆为你提供信用额度。如果你是零售交易员,那么你很可能会得到 30:1 的杠杆。如果你是专业交易员,那么 100:1 这样的高杠杆是可用的。然而,正如你将在本章后面看到的那样(见杠杆的力量——我需要多少来交易它?部分),你使用超过 10:1 的杠杆的可能性非常高。最有可能的是,它将在 2:1-5:1 的范围内。在我们的例子中,为了以 10:1 的杠杆开仓 200,000 AUD/USD 的头寸,你需要大约以下金额:

公式

0.6918 是过去 52 周(一个工作年)的平均 AUD/USD 汇率。实际上,你可能需要更多一些,因为开仓头寸应该能够承受回撤(见本章以下内容),但本质上,大约 15,000 美元的估计似乎是足够的。没有任何在口袋里数钱的意图,让我假设如果你是一个零售交易员,那么你几乎不会开仓超过 200,000 AUD 的头寸,因此可以安全地假设 1 个点的滑点将为我们的估计提供足够的额外空间。

注意

我们所有估计背后的主要思想是假设一个理论上的最坏情况,这种情况在现实交易中可能不会实现。如果我们已经考虑了所有可能的负面影响后,策略仍然能在纸上赚钱,那么它很可能在现实生活中也能赚钱。

然而,如果你与一家金融机构合作,那么平均订单量很可能会至少增加 10 倍,因此滑点开始发挥相当重要的作用。我建议在订单量中至少减去每 10 万 1 个点来考虑在夜间等流动性较差的时间进行交易时的滑点。

交易规模、滑点和执行

在现实生活中,超过订单簿顶部的订单很少直接作为市价订单发送到市场,以避免在不受欢迎的价格执行。如果你真的需要填补一个大订单,考虑重新编写processOrders()函数,将大订单拆分成几部分,并逐一执行。

佣金

交易成本的最后一部分似乎就是佣金。通常,一个交易场所会收取大约 25-40 美元每百万DPM)。我们如何将其重新计算成我们交易的美元价值?

在回测中使用的交易规模是 10,000 AUD/USD。这意味着我们买入或卖出 10,000 澳大利亚元,或者(使用平均汇率 AUD/USD 约为 0.6920)大约 7000 美元。以 30 DPM 的费率计算,经纪商佣金大约为 0.21 美元。现在,如果我们回想一下在我们的回测中 1 美元等于 1 个点,我们可以这样说,佣金大约是 0.21 个点。

到目前为止,我们已经计算了合理的价差、滑点和经纪商佣金的估计值。这是我们应该考虑的全部吗?

不,还有另一个许多交易者经常忘记的成本:隔夜掉期。

隔夜掉期

第六章“基本面分析基础及其在 FX 交易中的可能应用”部分中,“经济新闻”部分我们已经提到了外汇市场最奇特的特征之一:一对货币之间的利率差异。这种差异可能是正的,也可能是负的:例如,如果英镑的年利率为 5%,而日元的年利率为 0%,那么 GBP/JPY 的差异为正(5%),而 JPY/GBP 的差异为负(-5%)。

一旦你持有隔夜未平仓的头寸(超过纽约银行结算时间),你要么获得利息(如果差异为正),要么支付利息(如果差异为负)。在我们的例子中,如果你买入 GBP/JPY(或卖出 JPY/GBP,两者相同),那么你会获得利息;如果你卖出 GBP/JPY 或买入 JPY/GBP,那么你需要支付利息。

在我们的当前策略中,我们持有 AUD/USD 多日,有时长多,有时短空,所以我们可能会根据头寸方向和当前利率差异获得或支付利息。

我们必须考虑到这些隔夜费用,通常被称为掉期

备注

不要将掉期与金融工具混淆,我们在第三章“外汇工具”部分进行了讨论,“从开发者角度的外汇市场概述”

然而,这竟然是一个相当复杂的任务。这里有两个问题:

  • 首先,这些掉期利率会随时间变化,因为它们是基于中央银行(通常称为基准利率)设定的利率,这些利率也会变化。所以,至少我们需要基准利率的历史数据。

  • 第二,每个经纪商都会在自己的基准利率上添加自己的溢价或加价,而且在大多数情况下,这种溢价是公开未知的。鉴于经纪商不存储他们的隔夜掉期利率的历史数据,可靠地恢复货币对利率差异的历史就变得有困难了。

尽管如此,我们可以尝试一下。首先,我们需要历史利率数据。它比历史市场数据要少一些,但有一些资源可以免费提供——例如,Global-rates.com。访问www.global-rates.com/en/interest-rates/central-banks/central-banks.aspx,在那里您将找到美联储和澳大利亚储备银行利率数据的链接。

如您所见,数据变化相当不频繁,不超过每月一次——因为利率决策是由一个特殊的中央银行委员会做出的,并且通常他们每月都有会议。我们不需要在计算上绝对精确:从长远来看,隔夜掉期将只是几分之一个点,所以同步一个月的数据就足够了。我将数据复制到了 Excel 表格中,这是我的结果:

RBA FED 利率差异
12/06/22 3.10% 12/14/22 4.50% -1.40%
11/01/22 2.85% 11/02/22 4.00% -1.15%
10/04/22 2.60% 3.25% -0.65%
09/06/22 2.35% 09/21/22 3.25% -0.90%
08/02/22 1.85% 07/28/22 2.50% -0.65%
07/06/22 1.35% 06/15/22 1.75% -0.40%
06/07/22 0.85% 05/04/22 1.00% -0.15%
05/03/22 0.35% 03/16/22 0.50% -0.15%
11/03/20 0.10% 03/15/20 0.25% -0.15%
03/19/20 0.25% 03/03/20 1.25% -1.00%

表 13.1 – RBA 和美联储的历史中央银行利率

如果我们从后者减去前者,我们将得到澳元/美元货币对之间的利率差异。我们可以看到,它从低至 0.15%变化到 1.4%。现在的问题是它在金钱上意味着多少。

要计算这个,我们首先应该回忆起一年有 365 天,然后我们应该将表格最后一列的值除以 365。让我们用最新的值来做:

接下来,我们应该将每日利息乘以交易量。在我们的例子中,我们交易 10,000 澳元,所以我们将每日利息乘以 10,000。最后,如果我们想知道以美元计价的掉期价值,而不是澳元,我们应该将结果乘以实际的汇率,即澳元/美元的市场价格。

我想在这里已经很明显了,掉期利率的变化不仅是因为中央银行改变利率,还因为货币对的市场价格变化。为了我们的粗略估计,我们可以使用现货价格的 52 周平均值,因此将最终结果乘以 0.68。因此,在我们的例子中,我们得到:

我们如何解释这个结果?

这个结果意味着,如果我们买入 10,000 澳元/美元并持有这个头寸过夜,那么我们需支付 26 美分。看起来微不足道,但仅仅是因为 10,000 的交易量相当小:别忘了外汇市场是按保证金交易的,即使零售杠杆为 30:1,你只需要账户中有 300 美元就能开一个 10,000 美元的头寸。很多时候,零售交易都是以 100,000 为基础货币进行的,而机构交易通常1,000,000 开始。如果你持有 100,000 的头寸过夜,你需支付 2.6 美元——不多,但如果每晚都这样做,那么它逐渐积累成相当大的金额。如果你作为机构交易员进行交易,那么你每晚开始支付 26 美元,平均每周 130 美元,等等。

好吧,我听到你在说,“很显然,当我们持有澳元/美元多头时,我们需要支付费用,但如果我们是空头,我们每晚应该收到等额的金额,不是吗?

不幸的是,现实生活(就像往常一样)比任何理论都要复杂。问题是每个经纪商都会在自己的隔夜利率上添加自己的加价。这并不令人惊讶:经纪商也必须借款来为你提供信用额度,他们不能直接从中央银行借款,因此对他们来说,利率已经更高了。这意味着负值变得更负,而正值……好吧,在大多数情况下也是负的!我鼓励你检查任何经纪商的隔夜掉期利率,将它们与我们计算出的公允价值进行比较,看看差异。实际上,截至今天(2023 年 1 月初),我能找到的最佳隔夜掉期利率是 100,000 澳元/美元多头头寸为-5.30 美元,空头头寸为-0.30 美元。

此外,请记住,在周三,你将需要额外支付费用,因为我们进行的是现货交易,现货是在 T+2 的基础上交付的(再次提醒,请参阅第三章从开发者角度的外汇市场概述)。

我们可以使用一个估计,在最坏的情况下,我们为持有头寸过夜支付大约 0.5 个点的费用。点——因为,我希望,你记得使用 10,000 澳元/美元的交易规模可以使我们既以美元又以点来解释结果,这对于进一步的计算非常方便。

现在!终于!让我们总结一下到目前为止所讨论的所有成本。

交易成本计算

每笔交易的总交易成本 = 1 个点差 + 1 个点滑点 + 0.21 个点佣金 + 0.5 个点隔夜掉期 * 平均 20 天持有头寸 = 2.71 个点。

这是我们必须从平均交易中扣除的,如果剩余金额仍然是正数,那么我们的策略至少在纸上可以赚钱。

显然,现在我们可以看到,平均 79 个点的交易量是巨大的,因为即使成本过高,也几乎无法达到预期回报的 15%。这是一个真正了不起的结果,而且只有长期交易策略才能实现。

注意

策略在市场中的位置、此类策略的平均交易价值以及其回报的波动性之间存在密切关系:我们待在一个位置的时间越长,我们预期的平均交易量就越大,不幸的是,此类策略的回报波动性也会更大。这一事实甚至可以通过定理从数学上得到证明,但它远远超出了本书的范围,所以我现在建议你把它当作理所当然的事情。

总体而言,交易成本分析显示,我们的策略在这方面非常稳健(并且相信我,你未来自己开发的绝大多数策略甚至无法通过这个第一个测试——但对于研究和开发来说这是正常的)。接下来是什么?

接下来,我们必须重新审视本书中已经提到的一些概念。

衡量性能——重提α和β

在第九章《交易策略及其核心要素》中,我们提到了两个主要用于分析性能的重要概念:α和β。当时,我们从稍微不同的角度看待它们:我们是在寻找在市场上系统获利的机会,并且只从那个角度考虑所有这些指标。然而,不要忘记,它们最初是建议用于评估投资表现的——如果用简单的话说,就是判断投资是否优于或击败市场。

注意

我将故意简化α和β的概念,并避免使用它们的精确数学公式。使用它们需要良好的概率论理论掌握,并且我知道从我的以往经验来看,这正是许多读者感到困惑的数学领域。所以,请原谅我,数学纯粹主义者,但我只是想让每个人都能理解这些相对复杂的话题。

让我们从更常见且简单的指标α开始。

投资和交易中的α

Alpha 代表超额回报;这是最终决定你在给定时间段内是否战胜市场的指标。

通常,在资本管理中,alpha 是根据以下公式计算的:

在这里,R 代表投资组合的回报, 表示所谓的无风险回报率, 代表基准回报,而 代表回报的波动性(见下文)。

听起来很复杂吗?

嗯,它并没有乍一看那么复杂。我们只需要更好地理解这个方程中每个元素的含义。

通常,回报指的是在给定时间段内获得(或损失)的金额。在投资组合投资中,回报通常按月和按年估算,在主动交易中,我们也考虑日回报 – 在某些情况下,甚至更细的时间框架。

对于投资的回报或交易策略的回报,这应该是直观的,但什么是无风险回报率?在交易或投资中,什么可以被认为是无风险的?

通常,无风险回报被理解为当前各自债券收益率与通货膨胀率之间的差额。对于以美元计价的美国投资,我们可以使用国债。对于欧元区的投资,通常认为德国债券是一种无风险投资,等等。

现在我们又看到了一个基准。正如我们在第九章使用货币汇率作为基准部分所讨论的,交易策略及其核心要素,为外汇交易找到一个合适的基准可能相当困难。因此,我们通常使用货币对价格的变化来查看我们的策略是否比市场做得更好。

如果我们想尽可能简单明了地说明这一切,我们可以使用市场增长和回报的线性模型,假设 = 0(没有免费的午餐,对吧?)和 = 1(回报的波动性与市场本身的波动性完全相同)。例如,如果x轴代表时间,比如天数,而y轴代表回报,那么 alpha 恰好是两个函数值之间的差异:

图 13.1 – 返回和 alpha 的简化模型

图 13.1 – 返回和 alpha 的简化模型

要使关于 alpha 的故事完整,我应该指出,有时,特别是在分析主动交易策略的表现而不是投资时,alpha 被认为是从经典的线性方程形式中推导出来的:

如我们之前的例子,这是一个非常、非常简化的模型,展示了回报随时间增长的情况。如果我们假设市场本身以线性增长,其增长率为 1(y = x),那么任何 > 1 就意味着投资的回报增长速度比市场快,并且投资实际上击败了市场。同样,任何 < 1 就意味着投资的回报增长速度比市场本身慢。在这种情况下,即使这项投资给账户带来一些资金,它也没有击败市场:你通过简单地购买市场本身(股票、商品甚至货币可以构成相应的市场基准)可能会赚得更多。这个简单的假设在下面的图中展示:

![图 13.2 – 线性函数,其中 = 1, > 1, 和 < 1]

假设基准(市场)以 = 1(黑色线条)的速度增长。任何 > 1(在我们的例子中 = 2)的函数将增长得更快,而任何 < 1(在我们的例子中 = 0.5)的函数将比基准增长得慢。

备注

这种对 alpha 的解释并不常见,且不能用于经典资本管理理论。我仅提供这个不寻常的观点供您参考。

现在,我希望关于 alpha 的所有方面都更加清晰,但 beta 呢?

贝塔

贝塔是衡量回报波动性的指标。还记得我们在 第九章Beta – earn on volatility 部分讨论的类比吗?不仅我们能从 A 点到达 B 点的事实很重要,而且我们如何到达那里,即路径的直或弯也很重要。如果我们想说明这种比较,我们可以再次使用线性函数作为直线路径的例子,以及添加周期性成分的相同线性函数作为额外波动路径的例子:

图 13.3 – 带有和未添加波动性的线性函数

图 13.3 – 带有和未添加波动性的线性函数

我们可以看到,两个函数都从点 (0,0)(2,2),但线性函数做得更快。现在,让我们想象这两个图表都描绘了某种投资的回报。那么,x-轴代表时间,y-轴代表在一定时期内赚得或损失的资金量。因此,当我们看到曲线在增长时,这意味着资金在账户中积累,而当我们看到它在下降时,这意味着账户在亏损。

现在,让我们对我们的样本投资活动进行定期快照,并测量两个指标:

  • 从上一个快照时间到当前快照时间的价值变化了多少

  • 在给定时间段内的快照期间价值变化了多少

在下面的图中,我放大了并标记了图上的相应值:

图 13.4 – 简化模型中的回报及其波动性

图 13.4 – 简化模型中的回报及其波动性

在这个例子中,我们检查了回报(R)和波动性(V)在每一个整数值(1,2,3 等等)。

让回报的前一个值(在 0 处测量)与当前值(1)之间的差值是ΔR。在我们的例子中,

然后,我们注意到回报首先增长(从 0 到大约 1.3),然后下降到大约-0.65,然后再次增长到 1.6,然后下降到-0.15,最后再次增长到大约 1.4。因此,在从 0 到 1 的快照期间,函数已经经历了几个极值,包括最大值和最小值。设ΔV为局部最大值与紧随其后的下一个局部最小值之间的差值,这个最小值紧随最大值之后(而不是相反!)在我们的例子中,

ΔV 的值被称为回撤,并且是任何投资或交易策略的关键性能指标之一。

那么,我们如何现在估算(非常粗略,但无论如何)回报的 beta 值?

我们对回报与回撤的比率感兴趣:

我们希望这个比率的值尽可能高。如果我们有两个或更多策略可供选择,那么我们更喜欢回报与回撤比率更高的那个。

在我们的例子中,显然的选择是线性策略,因为它只会带来收益而没有损失。这意味着这种策略的价值ΔV = 0,因此所讨论的比率是无限的。当然,在现实中这种情况永远不会发生,但它清楚地说明了任何投资者乌托邦式目标。

第二种策略的特点是回报与回撤的比率约为 0.77。这意味着尽管这种策略能赚钱,但投资者在进入正区之前会有很多绝望的时刻:这种策略的回报波动性大于回报本身!

我们刚才考虑的方法当然是一个非常简化的方法。在实际应用中,贝塔通常被计算为回报的波动性,而不是与相同策略或投资的回报相比,而是与基准相比,就像 alpha 和其他投资绩效指标一样。除此之外,评估整个投资或交易期间的回报与回撤比率,以及首先计算多个较小期间的这些比率,然后平均它们,最后计算这些值的分散度,从而了解这个比率随时间的变化或这个给定策略或投资的回报波动性是否一致,这会更加正确。

然而,我承诺在这本书中保持简单,不要求从概率论的理论中获取具体知识,所以在我们第一次接触绩效指标时,我们不会走得太远。如果你想深入了解这个非常有趣且非常复杂的话题,我建议从 Investopedia 上的框架文章开始(www.investopedia.com/investing/measure-mutual-fund-risk),并跟随那里提供的所有链接:很快,你将发现自己置身于均值、标准差、方差和协方差的热带雨林中,但从长远来看,这将是一次相当有回报的经历。

重要提示

本章前面提到的经典资本管理公式中使用的贝塔估计与这里建议的贝塔不同。这里引入它只是为了解释回报波动性的本质,更好地理解接下来讨论的基本绩效指标。

好的,我们已经刷新了对 alpha 和 beta 的记忆,但你有没有注意到,到目前为止,我们一直在谈论 alpha 作为投资的绩效指标?投资和交易之间有什么区别,我们还能否继续使用 alpha 来评估交易策略的表现?

两者之间的主要区别在于,投资假设购买资产并长期持有,而交易意味着你在同一时间段内积极买卖资产。因此,在投资中,我们可以通过选择表现优于整个市场(通过指数或其他基准)的资产来有可能超越市场,而在主动交易中,我们试图通过持有较短的时间来超越资产本身。

使用 alpha 和 beta 相关指标来分析交易策略的表现的主要问题是缺乏适合此目的的相关基准。我们已经在第九章《交易策略及其核心要素》中提到了这个问题,并指出使用特定的 FX 基准来评估 FX 交易策略的表现会更加合理。

然而,有时真的很难找到一个合适的基准来比较苹果和苹果:例如,如果我们试图评估日内策略的表现,该策略每天开仓和平仓多个头寸,理想情况下,我们应该将其与用作参考的类似日内策略进行比较,但找到这样的参考本身就是一个相当大的挑战!那么,在没有这样的基准的情况下,我们如何才能充分评估交易策略的表现呢?

嗯,这就是为什么大多数用于评估交易策略表现的性能指标只使用相同的表现数据,有时与资产价格数据结合使用。这些指标可以被认为是代理真实 alpha真实 beta,如果结合起来,可以提供相当全面的分析,这有助于回答交易的主要问题:是否用这个策略下注。

让我们从旨在估计交易策略回报 alpha 值的指标开始。

净利润与买入并持有

这是在确认平均交易大于覆盖所有交易成本所需的绝对最低值之后需要检查的第一件事。买入并持有意味着我们购买与回测中使用的相同数量的货币,在整个回测期间持有该头寸,然后查看价格时间序列的最后一条记录上的最终盈亏PnL)。在我们的例子中,我们应该购买10,000 澳元/美元,持有大约 3 年,然后查看这笔单一交易的回报。

我建议比较策略的净收益和买入并持有,以及权益曲线:这将让我们了解在两种情况下权益随时间的变化情况。

让我们快速创建一个买入并持有策略的权益曲线。如您所记,我们将交易逻辑隔离到一个单独的函数中,tradeLogic(),因此我们只需重写交易逻辑开始于此交易逻辑结束于此注释之间的部分,同时保持其余代码不变。首先复制tradeLogic()函数并给它一个合适的名字——例如,buyAndHold(),在修改代码之前是一个好主意:

        if System.market_position == 0:
            close = bar['Close']
            order = {}
            order['Type'] = 'Market'
            order['Price'] = close
            order['Side'] = 'Buy'
            order['Size'] = 10000
            orders_stream.put(order)

如您所见,buyAndHold()tradeLogic()之间的唯一区别是我们移除了所有交易逻辑,并用在第一个条上的单一交易替换了它。在回测的其余部分不再放置其他交易。

如果我们现在运行回测,我们将看到我们假设的买入并持有投资的权益曲线。请注意,x轴和y轴的含义与简化插图中的相同:x轴代表时间——在我们的回测中,天数(因为我们使用的是日数据)——而y轴代表回报。

图 13.5 – 买入并持有回报,澳元/美元

图 13.5 – 买入并持有回报,澳元/美元

并且控制台输出如下:

Total trades: 1
Average trade: -153.9999999999997

这意味着如果我们 3 年前用美元购买了 10,000 澳大利亚元(并且历史价格数据从 2020 年开始),那么我们就会获得丰厚的回报-153 美元(!),相当于 3 年内的-1.53%(!)。而且这还没有提到在我们取得如此惊人的结果之前,我们几乎有 1,500 美元或 15%的回撤。

好吧,我希望你感受到了我的讽刺:现在你可以看到为什么我总是对关于货币市场投资的任何说法都变得非常怀疑:即使在如此短暂的 3 年时间内,也很难找到一个能够提供与股票市场常态(在这里,我指的是牛市,当股票指数每年、每个季度甚至有时每月都在增长。让我们现在先不考虑熊市,以免破坏这样一个理想化的画面)相当的回报。如果不积极管理头寸(买入和卖出),在 FX 市场中实现任何可接受的回报都是不可能的。让我们切换回来——用tradeLogic()替换buyAndHold()函数,并再次运行回测:

图 13.6 – 样本趋势跟踪交易策略的回报,澳元/美元

图 13.6 – 样本趋势跟踪交易策略的回报,澳元/美元

控制台输出现在如下所示:

Total trades: 37
Average trade: 79.40540540540536

这意味着在相同的 3 年测试期内,我们通过交易相同规模的 10,000 澳元/美元,赚取了近 3,000 美元。无需任何评论,就可以明显看出这个结果是如何超越买入并持有策略的:它在 3 年内等于 30%,而买入并持有策略为-1.53%。

让我们计算一下趋势跟踪策略的 alpha 估计值。策略回报为 30%,无风险回报率可以估计为-2.85%(3 年期的国债收益率为 4.25%,减去 2022 年的通货膨胀率 7.1%),基准回报为-1.53%,鉴于贝塔值,或者说回报的波动性,至少不比基准差(仅从这两个图表来看,这显然是正确的),那么我们的策略在 3 年内的 alpha 大约为 31.53%。

因此,我们可以这样说,该策略已经通过了两个检验:它具有平均交易价值,这使得策略能够真正进行交易(而不仅仅是纸上谈兵),它还产生了 alpha(优于基准),并且其回报与传统投资(例如,投资于标普 500 ETF 在同样的 3 年内回报约为 18%)相当。

现在让我们转向评估贝塔值,或者说回报的波动性。正如之前所提到的,我们不会提供在资本管理中使用的经典贝塔定义,因为它需要概率理论方面的特定知识。相反,我们将考虑回撤,这是平均交易和整体回报之后第三重要的指标,并展示为什么它可以作为评估回报波动性的代理

回撤

根据 CFI(最大回撤:corporatefinanceinstitute.com/resources/capital-markets/maximum-drawdown/)上发布的一篇文章,“最大回撤(MDD)衡量投资价值的最大下跌,这是通过最低谷值与之前最高峰值的差来给出的。”我们已经在图 134 中看到了回撤的说明,它相当容易理解。然而,实际的权益曲线永远不会像用于说明目的的理想正弦波那样看起来,因此回撤不是一个恒定值;它会随时间变化,这些变化也可能说很多关于回报波动性的信息。

因此,我们想要根据以下算法计算一个新的时间序列:

  1. 让权益曲线的第一个(最左侧)数据点作为当前历史高点(最大值)。

  2. 将下一个值与当前值进行比较。

  3. 如果下一个值大于当前值,则下一个值成为新的历史高点。

  4. 如果下一个值小于当前值,则将此值与当前历史高点之间的差值存储为回撤。

  5. 当权益曲线达到新的历史高点时完成回撤的更新,并重复整个算法。

如您所见,如果我们处理时间序列或任何其他序列,可迭代对象,数组等,我们可以在仅两步中更有效地实现相同的计算:

  1. 形成一个新序列,其中下一个元素要么等于前一个元素,要么更大。

  2. 从结果序列中减去原始序列。

这将给我们一个回撤序列。

为了更好地理解回撤是什么以及它是如何计算的,让我们考虑一个简单的例子。

让我们创建一个包含一系列非负值的列表:

a = [1,2,4,3,5,8,7,6,2,9,10]

然后,准备一个空列表,我们将在这里存储结果:

m = [] # m stands for 'maximum'

并初始化一个变量,该变量应存储初始列表中的当前高点(最大值):

x = 0

现在,我们遍历列表中的所有元素,a,将它们与最后一个最大值进行比较,选择新的最大值,更新它,并将其追加到结果列表m中:

for el in a:
    if el > x:
        x = el
    m.append(x)

如果你一切操作正确,那么m应包含以下序列:[1, 2, 4, 4, 5, 8, 8, 8, 8, 9, 10]。如果我们现在从m中减去原始序列a,那么我们得到一系列回撤:

[x - y for x, y in zip(a, m)]

这将返回[0, 0, 0, -1, 0, 0, -1, -2, -6, 0, 0]。这是在原始序列的每个数据点计算出的回撤列表。

我们可以通过从左到右扫描原始序列中的数据来验证结果。a中的第二个元素大于第一个,所以下跌为 0,最新高点为 2。第三个元素大于最新高点,所以下跌再次为 0,最新高点更新为 4。a中的第四个元素比最新高点低 1,所以m中的第四个元素为-1,最新高点保持为 4。你可以继续这个计算,并确保所有下跌值都计算正确——并且你现在完全理解了下跌的含义。

使用numpy数组可以更快地完成这个操作:

import numpy as np
a = [1,2,4,3,5,8,7,6,2,9,10]
m = np.maximum.accumulate(a)
dd = a - m

这段简单的代码将生成与之前相同的下跌序列,唯一的区别现在是将使用numpy数组而不是原生的 Python 列表:

>>> dd
array([ 0,  0,  0, -1,  0,  0, -1, -2, -6,  0,  0])

现在我们完全理解了什么是下跌以及它是如何计算的,让我们为我们趋势跟踪策略的权益时间序列进行计算。

首先,最重要的是,我们应该在代码的非常开始处添加import numpy,以及其他导入。

接下来,让我们稍微修改回测代码的末尾,其中调用matplotlibplot()函数:

dd = System.equity_timeseries - np.maximum.accumulate(System.equity_timeseries)
plt.subplot(2,1,1)
plt.plot(System.equity_timeseries)
plt.subplot(2,1,2)
plt.plot(dd)
plt.show()

你可以看到一个新的命令:plt.subplot()。它用于在同一画布上放置多个图表或图形(参见第八章使用 Python 进行外汇交易中的数据可视化,以刷新你对matplotlib绘图对象模型的记忆)。为了节省时间和空间,我建议你浏览一下www.w3schools.com/python/matplotlib_subplot.asp上的教程,以了解subplot()参数的含义。

现在,如果你已经正确地完成了所有操作并再次运行回测(当然,使用相同的源价格数据!),你现在将看到与之前相同的权益曲线,但下面是下跌图表:

图 13.7 – 样本趋势跟踪策略的权益曲线和下跌

图 13.7 – 样本趋势跟踪策略的权益曲线和下跌

现在,我们对我们的策略了解得更多了,特别是关于与之相关的风险。即使没有进行精确的计算(这可能相当复杂)并且只是对生成的图表进行表面观察,我们也可以得出一些重要的结论,这些结论将在下一节中讨论。

风险/回报和下跌回报

首先,我们想了解在回测期间下跌是否分布得更均匀或更不均匀。这非常重要,因为如果存在随机的重大下跌,那么这是一个警告信号:这意味着市场中有一些偶尔的过程没有被交易策略中使用的模型所考虑,迟早,这些过程中的一个可能会完全摧毁策略。

在我们继续之前,我建议你回顾一下均值和标准差的概念,我们之前在第六章“波动性指标”部分讨论过这些概念,基本面分析基础及其在 外汇交易 中的可能用途

我们可以通过在交互式控制台中运行回测并在初始图表显示后输入以下内容来快速分析结果:

mean_dd = dd.mean()

这计算了回撤的均值。以下命令返回回撤的标准差:

std_dd = dd.std()

现在,在图表中添加三条线:均值回撤、均值减去 1 个标准差和均值减去 2 个标准差:

plt.plot(range(len(dd)), [mean_dd] * len(dd))
plt.plot(range(len(dd)), [mean_dd - std_dd] * len(dd))
plt.plot(range(len(dd)), [mean_dd - 2*std_dd] * len(dd))

如果你一切操作正确,你应该在下面的图中看到类似的内容:

图 13.8 – 回撤的均值减去 1 和 2 个标准差

图 13.8 – 抽样均值减去 1 和 2 个标准差的回撤

我们如何解释这个结果?

位于 2 个标准差之外(图表中的底部水平线)的回撤被称为异常值。它们值得特别注意,因为通常它们是由策略逻辑中没有考虑到的因素引起的。在我们的例子中,幸运的是,这些异常值不多,而且至少第一个异常值可以很容易地用 COVID-19 恐慌初期市场的波动性来解释。

均值和 2 个标准差之间的回撤在时间上分布得或多或少均匀。这意味着它们只是由策略中使用的市场模型的不完美性引起的系统性回撤。

大多数回撤都高于均值,并且再次,它们沿着时间轴均匀分布——所以,这让我们有理由相信市场模型对市场是合适的。

在进行初步的定性分析之后,让我们转向数字。我们策略的最大回撤(也可以通过在交互式控制台中输入 min(dd) 或者在代码中添加 print(min(dd)) 来找到)为 646 点。这是大还是小?如果你投资 10,000 美元并暂时损失 646 美元,这意味着你承担了账户大约 6.5% 的风险。这是一个中等到低风险,这在大多数投资者看来是可以接受的。

然而,一个更重要的指标是风险/回报比。这个指标的含义相当直观:比率越低,回报的波动性越小,因此整体投资风险也越小。

让我们为我们的例子做简单的数学计算。我们的策略在 3 年内的总净盈利略高于 3,000 点。即使我们假设它每年都产生相同的回报(这并不成立),那么平均每年大约有 1,500 点的回报。这意味着通过承担 646 美元的风险,我们可能期望获得大约 1,500 美元,因此风险/回报比约为 0.43。

再次提出神圣的问题:这是还是

通常来说,任何小于 1 的风险/回报比都是好的,因为这意味着你冒的风险小于你可能期望赢得的。关于风险/回报比(以及所有其他性能指标)的最优值存在争议,但大多数作者认为任何小于 0.5 的值都为投资金融产品提供了潜在绿灯。

注意

严格来说,关于风险/回报比的问题也与基准问题密切相关。不幸的是,这个主题过于广泛和复杂,无法在本书中仔细考虑,但提供的粗略估计可以作为你研究的起点。

另一个本质上与风险/回报比相反的指标是回撤回报率。唯一的区别是后者通常以百分比计算,而不是以分数值计算。这个指标的含义也非常直观:它表示预期的风险溢价——如果我们冒一定数量的钱,我们期望的回报。

在我们的例子中,回撤回报率为 1,500/646*100 = 232.2%,在 2 年内。换句话说,你可能会期望大约是你能承担分配给这个策略的风险的两倍。

在这一点上,你可能会惊呼,“为什么,但在这章的早期,我们计算出的 alpha 值只有大约 30%,现在我们怎么得到了如此巨大的回报?”

这可能是外汇交易中最令人困惑的部分,我们将在下一节详细讨论。

杠杆的力量——我需要多少来交易?

最后,我们将回答任何投资者面临的主要问题:我需要在交易账户中有多少钱,以及我可以期望从中获得多少?

在我们继续之前,让我提醒你注意关于外汇交易的一个重要事实。

注意

不要将投资回报率作为性能指标与实际投资的实际回报率混淆!

要理解这一点,我们再次回顾杠杆交易的本质以及它与无杠杆的常规投资和交易有何不同。

当你进行无杠杆投资或交易时,你只能购买你账户中金额对应的资产(股票、商品等)。例如,如果你交易股票,股票价格为 100 美元,你账户中有 10,000 美元,那么你最多只能购买 100 股股票(实际上因为交易成本会少一些)。

然而,如果你进行杠杆交易,那么你只需要支付所需金额的一部分,其余部分将由你的经纪人以信用额度提供。在我们的例子中,如果你账户中有 10,000 美元,你的经纪人提供 30:1 的零售杠杆,那么你理论上可以购买多达img/Formula_B19145_13_025.png澳大利亚元。

能够用交易账户中的资金购买比通常可能购买更多的资产,这使得保证金交易对许多新手交易者非常有吸引力。然而,杠杆是一把双刃剑。让我们做一些简单的数学计算,看看为什么它可能非常危险。

你还记得我们一直以点数计算一切,而不是以货币计算吗?现在我们可以从中受益,因为我们只需要根据所选的杠杆调整点数价值,并按比例调整所有性能指标。

所以,当我们交易 10,000 澳元/美元时,净回报大约是 3,000 点,最大回撤是 646 点。如果我们账户中有相同的 10,000 美元,这意味着我们没有使用杠杆进行交易,而对于无杠杆交易,回报确实会达到大约 30%,而回撤将保持在可接受的低水平,大约 6.5%。

现在,我们以 30:1 的杠杆进行交易。这意味着现在一个点的价值等于 30 美元,而不是 1 美元。那么,回报将是$30 * 3000 = $90,000——多么丰厚的利润!如果我们把这个数字作为初始投资的百分比增长来表示,那么在仅仅 3 年内,我们就会有一个令人难以置信的 900%的价值。所以,保证金交易是一个巨大的好处,也是一条可行的道路,不是吗?

但我们忘记了回撤。我们还需要将其放大,因为现在 1 点价值 30 美元。这样,646 点的回撤就转化为$30 * 646 = $19,380。

哎呀。

现在的回撤是账户总初始资本的的两倍!这意味着实际上,我们从未达到预期的回报。此外,我们不仅没有盈利,反而会损失账户中所有的钱,面临一个臭名昭著的称为保证金追缴的灾难:当经纪商要求向账户添加资金时,否则,无法维持开放的头寸。大多数零售经纪商不会等到客户向他们的账户添加资金,而是简单地以损失了结这样的头寸。因此,如果我们以 30:1 的杠杆进行交易,我们会在第一次回撤时损失账户中所有的钱。

那么,我们如何选择可接受的杠杆?

在保证金交易中,我们通常从相反的一端开始。不是首先从策略指标计算预期的回报,而是首先尝试估算一个杠杆,使得我们的头寸在整个交易期间保持开放。

我们知道最大回撤是 646 点,我们在交易账户中有 10,000 美元。因此,可能的最大杠杆应该使点数价值不超过 10,000/646,大约是 15 美元。因此,绝对可能的最大杠杆是 15:1,比经纪商提供的 30:1 低一半。

现在,让我们仔细看看结果。

我们能否以 15:1 的杠杆进行交易?从理论上讲,是的,但实际上不行,因为在这种情况下,回撤将占满整个交易账户。我无法想象任何交易者,更不用说投资者了,能够承受这样的回撤,即使只是心理上。

在现实中,正确的杠杆计算按以下顺序进行:

  1. 定义最大损失水平。这并不是策略的最大回撤,而是投资者可接受的最大回撤。

  2. 计算点值,以便这个可接受的损失水平等于策略的回撤。

  3. 定义杠杆为预期点值与原始点值的比率。

  4. 定义交易规模为原始交易规模乘以杠杆。

让我们假设一个平均投资者可以承受初始投资的 10% 的回撤,来计算我们策略的杠杆。让我们还假设初始投资是 $10,000。因此,账户中的最大损失永远不应超过 $1,000。以 646 点的最大策略回撤计算,1 点的价值可以是 $1,000/646 = $1.54。所以,我们只使用 1.5:1 的杠杆(不是 15:1,不是 30:1,当然也不是许多经纪商提供的 100:1!)现在,我们回想一下,当我们每笔交易 $10,000 时,我们可以获得 $1 的点值。我们将原始交易规模(10,000)乘以杠杆(1.5),我们得到只有 15,000 AUD/USD。

那么,回报如何呢?之前我们计算了两个指标——alpha 和回撤回报——它们给出了两个非常不同的值:前者约为 30%,后者超过 200%。以我们可接受的 10% 损失率和 1.5:1 的杠杆率计算,我们预计在 2 年内获得 3,000 点 * $1.5 = $4,500 的回报。这相当于初始投资 $10,000 的 45%。

现在,让我们总结本章中讨论的所有关键要点。

备注

Alpha 是针对无杠杆投资计算的,并给出了可能的最低回报估计。

回撤回报是根据最大可能的杠杆计算的,并给出了可能的回报上限估计(假设回撤可能达到初始投资的 100%)。

现实回报是基于所选风险水平计算的,并且始终介于 alpha 和回撤回报之间。

摘要

在本章中,我们熟悉了最基本但最重要的性能指标。我们现在明白,有三个主要方面有助于评估策略性能:交易分析、与 alpha 相关的指标,如回报,以及与 beta 相关的指标,如回报的波动性和回撤分析。我们了解了始终对性能产生负面影响的主要因素,例如点差、滑点、佣金和隔夜掉期,并了解了我们如何在评估中现实地考虑它们。

我们已经对资本管理的基础进行了初步了解,并考虑了阿尔法(alpha)和贝塔(beta),以简化的形式,但至少在帮助改善我们对策略的判断方面是足够的。最后,我们仔细考虑了杠杆,看到了它的双刃剑性质,并采用了为保证金交易市场,如外汇市场选择杠杆的正确方式。

当然,本章只是对资本管理和风险评估这个庞大、复杂但有趣的世界的一个简介,正如整本书也只是在一般意义上介绍了系统外汇交易的世界。然而,即使本书中考虑的基本事实和技术也足以开始你自己的作业。希望这项作业能够持续下去——因为任何成功的交易员,从长远来看,都是市场的不懈学习者。

我鼓励你测试你遇到的任何想法——既然你知道如何去做,就使用本章概述的方法,批判性地分析结果——并且继续你的学习,将我的评论“遗憾的是,这超出了本书的范围”作为自己进一步学习的邀请。有一天,你可能会发现你管理着一个在众多市场中运行的策略组合,这既给你带来了从体面的工作中获得的精神满足,也带来了相当可观的经济回报。

第十四章:现在去哪里?

尽管上一章听起来像是书的结尾,但我认为不给你一些关于进一步发展你在外汇市场和创建交易算法(算法)的知识和技能的指导是不公平的。与之前的章节不同,其中每一章都专注于一个大型主题,这一章是关于外汇算法交易不同方面的短篇故事集,旨在为你提供进一步研究的起点。

掌握任何复杂主题都需要努力,而交易可能是耗时和劳动密集型活动,它需要结合科学家和商人的心态的非常特殊的态度。任何成功的交易策略或算法都是许多小时工作的结果,其中只有 10-20%的时间用于实际的编码、调试和重构;大部分时间总是花在研究市场上,寻找交易想法和无数试验和错误的证明概念。本章将给你提示,如何在市场上找到自己的优势,并使你的交易应用更加健壮。

不要忘记,我们都是市场的终身学生,只要你在算法交易这个复杂而激动人心的行业中,你将不断学习。

在本章中,你将学习以下主题:

  • 实施限价单和止损单

  • 计算交易次数的正确方法

  • 从交易想法到实施——使用限价单和止损单的另一个示例策略

  • 资金管理和处理多个入场

  • 重新审视策略表现——更多指标

  • 关于算法交易特定风险的更多内容

  • 经典的技术交易设置

  • 优化——算法交易的祝福与诅咒

实施限价单和止损单

第十章“在 Python 中考虑订单类型及其模拟”中,我们考虑了三种主要的订单类型:市价单、限价单和止损单。然而,到目前为止,我们只在实际代码中使用了市价单。虽然我们指出,实际交易应用可能永远不会使用止损单和限价单,因为它们可以在客户端模拟,并在必要时作为市价单发送到市场,但在回测器中实现这两种类型的订单肯定是有用的,以简化交易策略的开发。

让我们快速回顾一下限价单和止损单的精髓。

限价单总是以等于订单价格或更好的价格执行。这意味着如果市场价格目前是 100,并且发送了一个低于市场的买入限价单,例如,在 99,那么它只有在价格变为 99 或更低时才会被填充。如果发送了一个高于市场的买入限价单,例如,在 101,那么它将立即执行,其价格将作为订单执行期间可能出现的价格上涨的限制。

同样,停损单总是以等于订单价格或更差的价格执行。使用相同的例子,如果发送一个低于市场的买入停损单,那么它将立即执行;如果发送一个高于市场的停损单,那么它将只在市场价格达到订单水平时执行。在执行停损单时,对价格在执行过程中的增长没有限制。

对于卖出订单,情况是对称的。

当我们使用 tick 数据模拟执行限价单和停损单时,我们总能检查某个 tick 是否满足订单条件,然后假设它为执行订单的价格。然而,当我们处理压缩数据时,例如 1 分钟、1 小时、1 天等,我们不知道订单会在哪个 tick 实际执行。相反,我们假设如果 K 线的最高价或最低价穿过订单水平,那么订单应该被成交。表 14.1总结了限价单和停损单所有可能的成交条件:

订单类型 方向 成交条件 成交价格
限价 买入 K 线最低价 < 订单价格 订单价格和 K 线开盘价的最小值
限价 卖出 K 线最高价 > 订单价格 订单价格和 K 线开盘价的最大值
停损 买入 K 线最高价 > 订单价格 订单价格和 K 线开盘价的最大值
停损 卖出 K 线最低价 < 订单价格 订单价格和 K 线开盘价的最小值

表 14.1 – 触发限价单和停损单的条件及其假设执行价格

你可以看到,当使用压缩数据进行回测时,我们假设订单执行的价格并不一定等于订单价格。在某些情况下,它可能是 K 线的开盘价。为什么?

要回答这个问题,我们应该回想一下市场价格不是连续的,有时相邻的 tick 之间价格差异可能相当显著。在 K 线图中,这可以看作是 K 线收盘和下一根 K 线开盘之间的空隙。这些空隙被称为缺口

通常,外汇市场的 K 线图在周五市场收盘和周日晚些时候重新开盘之间会有缺口。通常,这些缺口并不很大,但有时它们可能非常显著,尤其是在周末期间有重要的经济或政治新闻。以下图表展示了 2023 年 2 月 6 日 USD/JPY 的周末缺口:

图 14.1 – USD/JPY 的周末缺口(由 Multicharts 图表)

图 14.1 – USD/JPY 的周末缺口(由 Multicharts 图表)

市场于 2023 年 2 月 3 日星期五收盘价为131.141,并于 2023 年 2 月 6 日星期一开盘价为132.194。这两个价格之间的差异由灰色箭头表示。这个差异超过 100 点,与平均每日价格波动相当。如果在周五收盘时我们发送了一个以 131.50 为买入止损订单,那么实际上,这个订单只会以 132.194 的价格成交(实际上,由于市场开盘后流动性较差的时间段的滑点,可能甚至更差)。如果我们周五收盘时卖出,即使我们用止损单保护我们的头寸——实际上这只是一种止损订单——嗯...我们只能希望头寸大小足够小,不至于因为一次巨大的损失而毁掉交易账户。

不管怎样,我们现在可以理解为什么我们总是检查订单价格是否超过条的开盘价,以及为什么我们建议根据第 14.1 表中的成交价格列计算实际执行价格。

现在我们已经了解了如何现实地模拟限价和止损订单,让我们继续编码。我们将使用我们在第十一章回测和理论表现中开发的回测代码,我们将修改emulateBrokerExecution()函数,因为订单执行被隔离在其中(还记得保持代码逻辑模块化的理念吗?现在,我们开始真正从这种方法中受益)。

目前,emulateBrokerExecution()函数只包含市场订单的实现。让我们直接在其下方添加以下代码块:

    if order['Type'] == 'Limit':
        if order['Status'] == 'Created':
            order['Status'] = 'Submitted'
        if order['Status'] == 'Submitted':
            if order['Side'] == 'Buy' and bar['Low'] <= order['Price']:
                order['Status'] = 'Executed'
                order['Executed Price'] = min(order['Price'], bar['Open'])
            if order['Side'] == 'Sell' and bar['High'] >= order['Price']:
                order['Status'] = 'Executed'
                order['Executed Price'] = max(order['Price'], bar['Open'])

我希望代码足够透明,以便可以看到它只是实现了在第 14.1 表中概述的逻辑。

如果你想模拟限价订单典型的执行问题,那么你可能想在代码中将<=替换为<,将>=替换为>,以检查条的价格是否与订单价格匹配。在这种情况下,你假设限价订单有一个保证的成交(参见第十章Python 中的订单类型及其模拟)。

现在,让我们添加一个非常相似的代码块来模拟止损订单的执行:

    if order['Type'] == 'Stop':
        # print('Begin processing limit order',
                Broker.orders_list)
        if order['Status'] == 'Created':
            # Here we actually send orders to the API
            order['Status'] = 'Submitted'
        if order['Status'] == 'Submitted':
            if order['Side'] == 'Buy' and bar['High'] >= order['Price']:
                order['Status'] = 'Executed'
                order['Executed Price'] = max(order['Price'], bar['Open'])
            if order['Side'] == 'Sell' and bar['Low'] <= order['Price']:
                order['Status'] = 'Executed'
                order['Executed Price'] = min(order['Price'], bar['Open'])

代码与限价订单模拟的代码绝对是对称的。

如果你想要模拟止损订单典型的滑点,那么你可能想在执行价格上添加或减去一小部分金额。通常,如果我们交易流动性较高的市场,如 EUR/USD,并且从早上 7 点交易到 GMT 晚上 9:50,我们可能不需要添加或减去任何东西。如果我们交易流动性较低的货币对,如 AUD/USD,那么 0.00001 到 0.00005 点的滑点看起来是合理的。如果你交易的是更不寻常的货币对,如 TRY/MXN,那么检查订单簿(如果你的经纪商甚至提供这样的对!)。再次参见第十章Python 中的订单类型及其模拟,以获取关于止损订单和滑点的详细信息。

注意我们对待市价、限价或止损订单的方式之间的重要区别。市价订单的状态在订单被emulateBrokerExecution()函数接收后立即设置为已执行,但限价或止损订单的状态首先设置为已提交已提交状态被分配,因为这些订单一旦生成,应该保留在订单队列中,直到它们被执行或取消。

现在我们已经添加了两种新的订单类型,我们应该检查我们是否一切都做得正确。正如我们在第十章中测试市价订单执行时所做的,我们将使用相同的源数据文件,它包含 EUR/USD 的 1 分钟记录。我们只将读取前 20 条记录,并执行一个限价订单和一个止损订单,以确保它们被正确模拟。

在我们进行任何测试之前,让我们在任何文本编辑器中打开源数据文件,复制前 21 行,并将它们粘贴到 Excel 电子表格中。然后,我们将使用金融图表类型构建一个图表:

图 14.2 – 使用 EUR/USD 源数据文件的前 20 条记录制作的测试图表

图 14.2 – 使用 EUR/USD 源数据文件的前 20 条记录制作的测试图表

价格数据从大约1.1295开始,在第四条记录时跌至1.1290。太好了,让我们在1.1290处放置一个买入限价订单。然后,我们可以看到价格开始上涨,但最终跌至1.1285,所以让我们在这个水平放置一个卖出止损订单——从而模拟止损(记住,止损可以保护开放头寸免受过度损失,并且总是放置在头寸相反的一侧,所以在我们这个例子中,它将是一个卖出止损)。这个止损订单应该在第 11 条记录时执行。

现在,让我们开始编码:

  1. 首先,在getBar()函数中,我们设置从文件中读取的最大条数:

     if counter == 20:
    
         break
    
  2. 接下来,我们实现策略逻辑,使其只产生两个订单:

            if close == 1.12949 and System.market_position == 0:
    
                order = {}
    
                order['Type'] = 'Limit'
    
                order['Price'] = 1.1290
    
                order['Side'] = 'Buy'
    
                order['Size'] = 10000
    
                order['Status'] = 'Created'
    
                orders_stream.put(order)
    
                order = {}
    
                order['Type'] = 'Stop'
    
                order['Price'] = 1.1285
    
                order['Side'] = 'Sell'
    
                order['Size'] = 10000
    
                order['Status'] = 'Created'
    
                orders_stream.put(order)
    

如果我们现在尝试运行回测,它将不会产生任何输出,因为我们还没有处理processOrders()函数的订单状态。这个处理逻辑相当简单:如果订单状态是已提交,我们应该将其返回到订单队列。

然而,我们在将订单返回队列时应该非常小心。别忘了processOrders()函数使用了一个内部无限循环,它从队列中检索订单,并且只有当没有订单剩下时才会停止工作。如果我们在这个无限循环中返回提交的订单回队列,我们将永远无法从中退出。

再次,这个问题有几种不同的解决方案,也许你会建议一个更好的方案,但现在让我们使用最直接的方法。让我们添加临时存储,我们将在这里存储已处理但未执行的订单,然后在所有订单处理完毕后将它们放回订单队列。

让我们从在tradingSystemMetadata类的构造函数中添加self.orders_buffer = []临时存储开始。然后,在processOrders()函数中if order['Status'] == 'Executed':逻辑块下方添加以下代码:

if order['Status'] == 'Submitted':
    System.orders_buffer.append(order)

这将把提交的订单添加到缓冲区。最后,我们将重写except:子句如下:

for order in System.orders_buffer:
    orders_stream.put(order)
    break

策略是,如果没有更多的订单在订单队列中,它将引发一个异常,因此我们可以安全地将暂时积累在缓冲区中的所有提交的订单返回到队列中。

现在,让我们运行回测并查看产生的权益曲线:

图 14.3 – 权益曲线

图 14.3 – 权益曲线

我们可以看到,买入限价订单确实在第四个柱上执行(记住我们从零开始计数),然后权益开始增长,然后下降,最后在第十一根柱上执行了卖出止损订单。在控制台中,我们可以检查交易次数和平均交易价值的信息:

Total trades: 2
Average trade: -2.6999999999999247

但等等!现在有些不对劲。根据执行订单的价格水平,入场和出场之间的距离应该是正好 5 个点,但平均交易是-2.7 个点,甚至不是一个整数值。发生了什么?为了回答这个问题,我们应该修改我们计算交易次数的方式。

计算交易次数的正确方法

当我们在第十二章中处理趋势跟踪策略时,示例策略 – 趋势跟踪,我们只打开新的仓位,每次打开都会关闭之前打开的仓位。这对于始终在市场中的策略来说是正常的。在这种情况下,确实,交易次数与执行订单次数相匹配。

在我们的限价和止损订单示例中,我们使用两个订单实际上只执行一笔交易:买入然后以盈利或亏损退出市场。因此,我们应该只使用入场订单的数量来计算平均交易。我们如何区分开仓和平仓订单?

有多种方法可以做到这一点。其中一种可能的选择是在订单顺序中添加另一个键,其值为EntryExit,但我们将采用不同的方法:我们将在tradingSystemMetadata类中添加一个新的属性,该属性将保存实际的交易次数,并且我们只会在订单执行后的市场位置不为零时更新它,也就是说,最后执行的订单不是一个平仓订单:

  1. 首先,让我们在tradingSystemMetadata类的构造函数中添加self.number_of_trades = 0。这是我们保存交易次数的地方。

  2. 接下来,我们需要修改我们计算交易的方式。正确的地方是在processOrders()函数中更新市场头寸时。目前,我们使用只更新市场头寸而不检查在执行最后一个订单后市场头寸是否为零的代码:

    if order['Side'] == 'Buy':
    
        System.market_position = System.market_position + order['Size']
    
    if order['Side'] == 'Sell':
    
        System.market_position = System.market_position – order['Size']
    
  3. 现在,我们将用以下代码替换这段代码:

    if order['Side'] == 'Buy':
    
        System.market_position = System.market_position + order['Size']
    
        if System.market_position != 0:
    
            System.number_of_trades += 1
    
    if order['Side'] == 'Sell':
    
        System.market_position = System.market_position - order['Size']
    
        if System.market_position != 0:
    
            System.number_of_trades += 1
    

你可以看到,这段代码检查交易后市场头寸是否变为非零,并且只有在这种情况下才会增加交易次数。因此,如果最后一个订单只是关闭了头寸而没有打开新的头寸,它将不会被考虑。

  1. 现在唯一要修复的是代码的末尾,在那里我们打印交易次数并计算平均交易:

    print("Total trades:", System.number_of_trades) # introduced number_of_trades
    
    print("Average trade:", System.equity / System.number_of_trades)
    
  2. 如果我们现在重新运行回测,我们将看到正确的输出:

    Total trades: 1
    
    Average trade: -5.399999999999849
    

差不多完成了!交易次数是正确的,但奇怪的是平均交易价值中多出了 0.4 个点——根据我们在策略逻辑中指定的水平,平均交易应该是正好-5 个点,而不是-5.4 个点。差异是从哪里来的?

  1. 要回答这个问题,我们应该再次批判性地审查processOrders()函数。当我们开发它来处理仅在 K 线结束时生成的市价订单时,我们在while True循环之前放置了以下三行代码,该循环处理订单队列:

    System.equity += (bar['Close'] - System.last_price) * System.market_position
    
    System.equity_timeseries.append(System.equity)
    
    System.last_price = bar['Close']
    

因此,在开始处理订单之前,函数首先根据 K 线的收盘价重新计算权益。如果我们只使用市价订单,那就没问题,我们在处理订单时更新System.last_price的值,它将始终与 K 线的收盘价一致。然而,现在订单可以在 K 线的低点和高点之间任何位置执行,所以我们有额外的processOrders()函数步骤如下:

  1. 让我们将更新权益的三行代码块从代码的顶部(在while True循环之前)移动到末尾(在while True循环之后)。参见Stop and limit orders.py代码和processOrders()函数。

  2. if order['Status'] == 'Executed'之后,让我们添加以下行:

    System.equity += (order['Executed Price'] - System.last_price) * System.market_position
    

这样,我们通过将前一个 K 线的收盘价(在执行此行时存储在System.last_price中)与订单执行的价格之间的价格差异乘以市场头寸,即订单执行之前存在的头寸来更新权益值。然后,在processOrders()函数的末尾,权益再次更新,这次是通过计算订单价格与 K 线收盘价之间的差异,乘以新的市场头寸。这为我们提供了策略权益的完美精确计算,从而也提供了平均交易的价值。

注意

我故意没有在这本书中发布修改后的回测代码的完整代码。我的目标现在是训练你在发现新的不足并提出解决方案的心理过程。在你完成所有修改后,我建议你从 GitHub 下载我的代码并与之比较——这将为你提供一个极好的机会来提高你在升级代码逻辑方面的技能,这是算法交易整个开发过程中最重要和最敏感的部分。

  1. 如果我们现在运行最终代码,我们将得到绝对精确的值:

    Total trades: 1
    
    Average trade: -4.999999999999449
    

现在我们已经掌握了限价和止损订单,让我给你一些关于进一步开发回测和实盘交易应用的提示。

从交易想法到实施——使用限价和止损订单的另一个示例策略

让我们考虑一下我们刚刚实施的限价和止损订单的实际应用。我喜欢使用这个例子,因为它说明了在编写代码之前拥有交易想法的重要性,并表明交易想法不必复杂。在实践中,交易想法越简单,它在生产中成功的可能性就越大。

如您从第三章《从开发者角度的 FX 市场概述》中可能记得,大多数外汇市场都经历一个银行结算程序,这个程序在纽约时间下午 5 点进行。结算时的价格非常重要,因为它被用来评估许多其他金融工具,并且用于任何双方之间的所有现金交易的结算。因此,将日内价格与最后结算价格进行比较,可以给我们这个市场的整体情绪提供一个想法:如果它大于最后结算价格,那么情绪是积极的——如果它低于,那么情绪是消极的。

接下来,我们可能想要假设,如果整体市场情绪是积极的,那么我们可以尝试开多仓,如果它是消极的,那么开空仓。

然后,我们必须决定何时进行这样的操作。自然,这应该是下一次结算的时间,因为在一天中,外部因素如新闻的发布可能会使价格上下波动多次。然而,记住,在下午 5 点时发送订单是不可能的,因为那时市场已经关闭,订单将被拒绝。除此之外,在结算前几分钟,流动性变得越来越稀薄,所以在结算前 1 分钟进行交易,即使交易规模很小,也可能有问题。因此,我们将尝试在结算时间前 10 分钟进入市场。

最后,我们必须决定我们在市场中的停留时间,或者在我们以哪个价格平仓头寸。让我们不要贪婪,只以 5 点的微小利润退出。我们将使用限价订单在入场价格的一定距离处退出头寸——这样的订单通常被称为盈利目标。然而,如果市场向相反方向发展,我们将以亏损平仓头寸,因此我们将使用止损订单来完成这个目的。问题是我们在哪个水平放置这个止损订单。

关于这个问题,有很多不同的观点。许多作者认为,止损应该始终小于潜在利润,否则,看起来你冒的风险比你可能赢得的要多。其他人则认为,不仅赢和输的大小很重要,它们的概率也很重要。事实上,如果我们赢的百分比大于 67%,即使平均损失是平均赢利的 2 倍,我们仍然可以赢得比赛。

我们有快速测试这两种方法的奢侈:让我们首先尝试将止损金额设定与盈利目标相同,然后增加它以保持头寸更长时间,希望迟早会达到我们的盈利目标水平。

如您所见,我们再次构建市场模型,就像我们在第十一章中所做的那样,但这次,模型的目标不是解释整个价格时间序列。相反,这个模型只描述了相当短期的市场过程,这种过程可能会在市场上定期发生。一般来说,如果我们找到足够数量的定期出现的市场过程,我们甚至可以用这种方式模拟整个价格序列。

因此,总结一下,交易算法应该是这样的:

  • 在下午 5 点纽约时间,我们将收盘价作为参考。

  • 在第二天下午 4:50 纽约时间,我们将价格与参考价进行比较。如果差异为正,则买入。如果差异为负,则卖出。

  • 我们设定盈利目标为 5 点,止损为 5 点。

  • 我们在市场中的停留时间,直到盈利目标或止损被触及。

就这些了吗?

不。当其中任何一个订单被执行时,我们必须取消盈利目标-止损对中的另一个订单。否则,当市场位置为平仓(零)时,剩余的订单可能会被触发,这将打开一个意外且不希望出现的头寸,我们将无法管理。依赖于其他订单执行的订单被称为权限订单

权限订单

本书开发的后测试和实时交易代码假设任何订单都发送给经纪人,然后永远不会修改。然而,有时你想要修改已经提交但尚未执行的订单中的某些内容。例如,你发送了一个止损订单,但市场条件发生了变化,你现在想要增加或减少订单价格。

我们的代码不支持这项功能,而且并非所有经纪商都支持。如果您想实现它,最简单的方法是为每个订单添加一个唯一标识符,然后在需要修改时在订单队列中引用它。首先,您必须从队列中删除旧订单,然后发出新订单。这将与几乎任何经纪商兼容,因为这是经纪商侧修改订单的首选方式。

我们现在不会实现一个需要引入订单 ID 和适当的订单处理方法的通用订单管理解决方案,因为这超出了本书的范围。我们将在processOrders()函数中添加一小段代码,以便在限价或止损订单执行后立即清除整个订单队列。这样,我们可以有效地实现附带利润目标和止损订单。

重要提示

建议的解决方案仅在我们只有一个附带订单对时有效。如果您想实现一个更复杂的策略,该策略利用多个附带订单,除了添加订单 ID 并实现处理单个订单的例程外,没有其他方法可以做到。

要实现我们的简单解决方案,让我们在processOrders()函数的if order['Status'] == 'Executed':分支末尾添加以下代码,紧接在System.last_price = order['Executed Price']之后:

if order['Type'] == 'Limit' or order['Type'] == 'Stop':
    System.orders_buffer = []
    orders_stream.queue.clear()

这段代码的思路是,如果任何附带订单已被执行(记住,我们将此添加到已执行订单状态的处理中),那么我们将取消所有其他订单。让我再次重复一遍,这个解决方案只在我们只有一个附带订单对在订单队列中,且没有其他订单时才有效。

我们现在需要做的只是添加策略逻辑。一如既往,我们只修改位于trade logic starts heretrade logic ends here注释之间的代码。代码非常简单,实现了刚刚描述的四步逻辑:

        close = bar['Close']
        if bar['Time'] == '23:00:00':
            ref_close = close
        if bar['Time'] == '22:50:00' and System.market_position == 0:
            order = {}
            order['Type'] = 'Market'
            order['Price'] = close
            if close < ref_close:
                order['Side'] = 'Sell'
            if close > ref_close:
                order['Side'] = 'Buy'
            order['Size'] = 10000
            order['Status'] = 'Created'
            orders_stream.put(order)
            order = {}
            order['Type'] = 'Limit'
            if close < ref_close:
                order['Side'] = 'Buy'
                order['Price'] = close - 0.0005
            if close > ref_close:
                order['Side'] = 'Sell'
                order['Price'] = close + 0.0005
            order['Size'] = 10000
            order['Status'] = 'Created'
            orders_stream.put(order)
            order = {}
            order['Type'] = 'Stop'
            if close < ref_close:
                order['Side'] = 'Buy'
                order['Price'] = close + 0.0005
            if close > ref_close:
                order['Side'] = 'Sell'
                order['Price'] = close - 0.0005
            order['Size'] = 10000
            order['Status'] = 'Created'
            orders_stream.put(order)

我们检查价格是高于还是低于参考价格,使用市价单开仓,然后立即发送一个限价单以获取利润和一个止损单以亏损退出。

我不会在这里考虑整个策略代码,因为其中大部分代码与在第第十章中开发的内容相同,而引入限价和止损订单的修改已在本章早期讨论(见实现限价和止损订单部分)。您可以从 GitHub 下载代码,分析所做的更改,并运行它以确保您得到正确的结果。

如果我们使用 EUR/USD 1 分钟 K 线作为源数据运行代码,我们将得到这样的权益曲线:

图 14.4 – 具有紧止损策略的权益曲线

图 14.4 – 具有紧止损策略的权益曲线

我们还将获得以下基本性能指标:1,464 笔总交易,平均交易约为-0.12 点。

显然,这个策略不起作用,我们甚至不需要深入分析其表现。因此,将止损点设置与利润目标相同距离的想法是无效的。如果我们现在将止损点设置为 50 点而不是 5 点,再次运行代码会怎样呢?

在下面的图表中,您可以看到结果现在有了戏剧性的变化:

图 13.5 – 具有更宽止损策略的权益曲线

图 13.5 – 具有更宽止损策略的权益曲线

现在,我们有 1,428 笔交易,平均交易为 1.18 点。

这个策略可交易吗?

首先,我们需要了解平均交易是否可以覆盖所有交易成本。这里交易的工具,欧元兑美元,在 FX 市场中远比其他货币流动性高,因此在入场时,点差大约为 0.00001-0.00002 点,至少有 100,000 到 500,000 个订单簿顶部(取决于交易场所)。因此,如果我们保持合理的交易规模,就不应该遭受滑点,从表面上看,我们实际上可以交易这个策略。

我鼓励您遵循第十三章中考虑的所有步骤,“交易还是不交易 – 性能分析”,并就这个策略做出最终决定。

提示

不要忘记考虑隔夜掉期。看看它们如何影响策略表现。

然而,无论这个策略是否可交易,让我再次强调代码背后的交易理念的重要性。交易理念不是凭空出现的:它们都是基于特定市场的各种特定因素。我甚至可以说,交易理念始终基于将市场从随机过程中区分开来的因素,从数学意义上讲。这就是为什么我们在本书中花费了大量时间研究市场基本面,并希望您能够在我们所考虑的大量事实中找到许多其他交易理念。

到目前为止,我们只使用那些只在市场位置为零或需要反转当前开放头寸时才开新头寸的策略。在这种情况下,策略逻辑的优势在于其正确地把握进出时机。然而,存在一类策略,通过管理同一方向上的多个开放头寸来获得优势。在这种情况下,我们说策略在资金管理方面具有优势。

资金管理和多重入场

为了让您了解资金管理是什么以及它可能如何影响策略表现,让我向您介绍可能是最著名或臭名昭著的资金管理技术,即马丁格尔

马丁格尔的起源在于赌博。想象一下最简单的赌博游戏——抛硬币。你抛硬币,如果正面朝上,你赢;如果反面朝上,你输。我们可以用1表示赢,用-1表示输,投掷序列可以用以下序列表示:

S = {1, -1, -1, 1, -1, 1, 1, 1, -1, -1, 1, -1, ...}

如果你每次投掷硬币时都下注相同数量的钱,我们可以将序列乘以那个数量,并写成如下形式:

S1 = {b, -b, -b, b, -b, b, b, b, -b, -b, b, -b, ...}

在这里,b指的是赌注的大小。显然,你在游戏中的总赢利是整个数列的和。在一个理想化的模型中,每次投掷的结果是相互独立的,硬币正面或反面的概率严格为 50%。因此,从长远来看,数列的和将始终围绕零,没有赢得这个游戏的机会。

这个数列的和确实为零。然而,如果你开始使用马丁格尔形式的资金管理,情况将发生戏剧性的变化。每次新的亏损后,你将你的赌注翻倍,每次新的胜利后,你将赌注大小重置为其初始值。然后,胜负序列将转变为以下形式:

S2 = {b, -b, -2b, 4b, -b, 2b, b, b, -b, -2b, 4b, -b, ...}

很明显,现在序列的和与零相差甚远;在我们的例子中,它是 5b。这意味着通过使用资金管理,有可能以等概率的结果赢得游戏。

初看之下,马丁格尔似乎是一种赢得任何游戏的终极方法,但有两个陷阱:

  • 这种方法可能只适用于结果真正独立的游戏。在概率论中,这类过程被称为随机(参见第六章基本分析及其在 FX 交易中的可能用途)。在学术研究中,金融时间序列通常被认为是随机的,至少在分笔级别上,但我们已经知道,在某些时刻甚至相对较长的时期,这并不成立:例如,在发布重要经济新闻后,很明显,上涨或下跌将占主导地位一段时间,这取决于新闻的共鸣(再次参见第六章基本分析及其在 FX 交易 中的可能用途)。

  • 即使是纯粹的随机游走过程,只有在长期(为了绝对精确,只有在无限长的序列中)才能实现结果的等概率。如果我们分析结果的短期子序列,我们可能会看到一系列相同的结果,而且没有人能保证这样的序列一定会以某个特定的结果数结束——正是因为新结果发生的概率不依赖于先前结果!如果你最初只有 100 美元,每次亏损后都将赌注翻倍,那么在 n 次亏损后,你必须在桌面上放美元。在第三次亏损后,你将不得不赌 800 美元,在第四次亏损后,1600 美元,以此类推,如果你不幸连续遭遇 10 次亏损,那么你应该打电话给你的银行,要求他们提供信用额度,因为现在你不得不赌 102,400 美元!

我希望你能理解为什么马丁格尔策略在说明书中如此受欢迎,但在实际应用中却高度不推荐

在交易中,有一种马丁格尔策略的变体,称为平均下降。假设我们开了一个多头仓位,但市场价格下跌。我们不会平仓这个仓位或只是等待价格回到初始水平,而是开立新的多头仓位,增加交易规模并将平均入场价格向下移动。例如,如果我们最初以 100.00 的价格开了一个 1 手的多头仓位,然后以 90.00 的价格再增加 1 手,那么我们实际上有一个在 95.00 的价格开立的 2 手仓位。

马丁格尔策略和平均策略可能只有有限的用途,通常情况下,使用这些策略的投资者会对持仓的最大规模施加一些限制。

像许多其他作者一样,我使用马丁格尔策略和平均下降策略是为了说明目的:仅仅因为它们能非常清楚地说明资金管理是什么。还有更多保守的资金管理策略,其中一些相当复杂。如果你对学习更多关于资金管理策略感兴趣,并希望找到一个单一的资源,我推荐由资金管理大师范·瑟普撰写的终极指南,《定位大小策略的终极指南》(Definitive Guide to Position Sizing Strategies)。如果你对这一主题有更学术的兴趣,或者更倾向于从各种来源收集信息,我建议从了解一般随机过程(zh.wikipedia.org/wiki/随机过程)和特定随机游走(zh.wikipedia.org/wiki/随机游走)开始,然后参考拉尔夫·文斯所著的书籍,《金钱管理的数学:交易者的风险分析技术》(The Mathematics of Money Management: Risk Analysis Techniques for Traders)。

要使用资金管理策略,你应该非常小心地处理订单大小。目前,我们只与那些以固定大小开仓并在开新仓之前关闭的策略合作。如果你通过同一方向的多笔订单增加仓位,当你尝试平仓时,你应该仔细计算订单大小。如果你出错,策略可能会留下一个或多个未平仓的仓位,这可能会严重影响策略的表现。因此,通常情况下,本章中提到的订单处理方法对于正确实施资金管理策略是必需的。

我很高兴继续我们关于外汇市场、系统交易和算法交易的讨论,但遗憾的是,本书的篇幅有限,所以我只能就一些更重要的主题提供一些一般性指导,供你自己进一步研究。它们在这里作为独立主题呈现,它们之间没有明显的逻辑联系。

策略性能回顾——更多指标

第十三章《交易还是不交易——性能分析》中,我们只考虑了非常基本的性能指标。当然,还有很多其他同样重要的指标。我建议从Quantinsti提供的良好概述开始(blog.quantinsti.com/performance-metrics-risk-metrics-optimization/),在代码中实现每个指标,然后你可以像市场专业人士一样分析你的策略。

更多关于算法交易特定风险的介绍

我们已经考虑了任何交易中的主要风险:操作风险、系统风险和交易风险。让我们强调另一种特定于算法交易的风险。

当你使用压缩数据开发和回测策略,并使用限价或止损订单时,存在一个风险,即这些订单中的多个可能会在同一根 K 线上被模拟。通常情况下,这发生在订单价格彼此过于接近且数据分辨率不够细粒度时。例如,如果你在彼此相距 5 个点的地方放置限价和止损订单,并使用每日数据运行回测,那么在大多数日子里,这两个订单都应该在同一根 K 线上被执行。这是你无论如何都要避免的事情,因为回测器根本不知道价格在这根 K 线内的实际移动情况,因此没有人知道哪两个订单会首先被触发,哪个会随后触发。因此,正确选择数据分辨率对于使回测结果真实至关重要。

选择数据分辨率的经验法则

总是选择一个柱状图平均范围(柱状图的高值和低值之间的差)小于订单价格之间距离的数据分辨率。如果你放置 100+点数的限价或止损订单,那么你可以使用日数据。如果你使用 20+点数的限价或止损订单,那么可能 30 分钟的时间框架会工作。如果你使用像前一个例子中的紧止损或限价,那么 1 分钟分辨率是最佳选择。记住,只有使用 tick 数据进行测试才能给出最终正确的画面,尽管这种回测会花费很多时间。

经典技术交易设置

第七章《技术分析及其在 Python 中的应用》中,我们考虑了多个经典的技术分析指标,例如 RSI、随机振荡器、移动平均线和布林带。我们看到了每个指标都能聚焦于价格时间序列的某个特定属性:例如,布林带是波动性指标,而移动平均线是去除价格数据中高频的数字滤波器。然而,我们没有考虑任何经典交易设置与这些指标的结合。为什么?

这个问题的答案有两个方面。首先,这些设置可以在任何关于技术分析的书或网络出版物中找到。你可以从 Investopedia(www.investopedia.com/terms/t/technicalindicator.asp)上的技术指标概述开始,然后通过链接到特定指标的文章来了解它们应该如何被用来生成交易信号。

我们在这里不深入探讨这些经典设置的第二和更重要的原因是,它们中的任何一个都无法直接创造出盈利的交易策略。你可以尝试通过实施任何经典设置来构建一个简单的策略,然后在多个市场使用不同的数据分辨率运行它,你会发现不幸的是,没有任何组合能够提供令人满意的表现。

为什么会发生这种情况?这难道意味着技术分析指标对实际交易没有用吗?

这是因为所有经典指标都是为突出特定市场在特定时间发生的特定过程而开发的。

例如,RSI 的创造者 Welles Wilder 在 20 世纪 70 年代进行商品期货交易时开发了这一指标。当时,市场相对流动性较差,每天只开放几个小时。只有少数交易者可以访问它,交易要么在交易池中进行,要么通过电话进行——这就是 20 世纪 70 年代的商品期货市场。相反,现在的外汇市场全天 24 小时开放,流动性充足,市场参与者种类繁多,从大型银行到零售交易者。其计算机化订单可以每秒处理数千笔交易。比较这两个市场,你就会明白为什么旧日的指标可能无法指示它们应该指示的内容。

对于第二个问题——是否古典技术指标在当今已经无用——的回答是否定的。如果我们理解它们确切显示的内容,我们仍然可以使用这些指标中的任何一个或全部。这就是为什么我试图关注它们的含义,而不仅仅是列出众所周知的用例,例如移动平均线交叉(见www.investopedia.com/terms/c/crossover.asp)或通过 RSI 或随机振荡器确定的超买/超卖区域(见www.investopedia.com/ask/answers/121214/what-are-best-indicators-identify-overbought-and-oversold-stocks.asp))。

尽管现在的电子外汇市场与过去良好的场内交易期货市场大不相同,但我强烈推荐阅读由技术指标创造者撰写的经典书籍——因为在这本书中,他们解释了为什么建议使用特定的指标,他们试图通过指标识别哪种市场过程,以及我们如何从建议的配置中获利

在彻底阅读上花费几天时间可能比试图将一个特定的知名技术交易设置拟合到一个本质上不适合市场的市场上浪费几周和几个月的时间更有价值。我可以推荐从 Welles Wilder Jr.的经典著作《技术交易系统的新概念》开始,其中他仔细解释了他是如何注意到某些可能有利可图的市场过程的他是如何尝试使它们形式化的他最终形成了一套技术指标,以及他实际上是如何使用它们来获利的。专注于这个心理过程,而不仅仅是数字,你将更好地理解制定交易策略的过程。

请记住,任何公式和任何代码背后都应有一个交易理念,而交易理念只能通过市场分析找到,而不是在数字处理或将不相关的模型拟合到市场上。在下一节中,我们将看到过度拟合不仅可能适得其反,而且可能危险。

优化——算法交易的祝福与诅咒

你还记得我们在这章早期创建的一个简单隔夜策略的表现,当我们将 5 点的紧止损替换为 50 点的宽止损时,其表现是如何彻底改变的吗?

但这个事实又提出了另一个重要问题:为什么是 5 点和 50 点?为什么不是 6 点和 45 点?或者 10 点和 76 点?

任何量化策略都依赖于其参数的值,而寻找最佳参数组合以获得最佳回测结果的过程被称为优化

优化是一个庞大的主题。我甚至可以说它非常庞大且复杂。乍一看,这似乎很简单:让我们找到最佳参数值的组合,然后使用这些值实时运行策略。然而,问题在于我们总是使用过去的数据来测试和优化我们的策略。我希望你已经理解和很好地记住了市场根本不是平稳过程。这意味着价格行为可能会在未来发生变化,并且使用相同参数组合的相同最佳策略将开始亏损。

注意

当一个策略使用不足的数据或不适当的逻辑进行优化,然后在实盘交易中开始亏损的情况被称为过度拟合曲线拟合。这被认为是所有算法交易中的瘟疫,也是为什么许多交易员仍然对其持怀疑态度的原因。

如何减轻这种具体风险?

有各种解决方案,前向测试可能是最受欢迎的。在进行前向测试时,我们首先使用整个过去市场数据的一个子集来优化策略参数,然后为回测生成性能报告,在另一个数据子集上运行。例如,如果我们有 2015 年到 2023 年的数据,那么我们可能希望使用 2015 年到 2017 年的数据来优化策略,然后使用 2018 年到 2023 年的数据来测试。使用第一个子集进行的回测被称为样本内回测,使用第二个子集进行的回测被称为样本外回测。如果样本外策略的表现与样本内策略相当,我们可以估计它将在未来继续有效(尽管在现实中,事情可能要复杂得多)。

另一种方法是仅对相对少量的样本外数据进行前向测试,然后使用新数据重新优化策略,并在另一个新的样本外数据部分上重复前向测试,依此类推。在我们的例子中,我们可以使用 2015 年到 2017 年的数据来优化策略,然后仅对 2018 年进行前向测试,然后使用 2016 年到 2018 年的数据重新优化策略,对 2019 年进行前向测试,依此类推。这种方法被称为前向优化

如你所可能已经意识到的,优化是一个极其耗费资源和时间的流程。确实,我们需要使用一组参数值运行回测,然后保存结果,修改参数,再次运行回测,并重复这个过程。在我们的样本策略中,如果我们想要在 5 到 50 点之间,以 5 点的步长找到最佳止损和盈利目标值,这将需要运行整个回测 100 次,这在大多数计算机上可能需要数小时。这就是为什么优化算法大多数情况下都是使用 Python 的编译版本开发的,例如 Cython 或 Numba。

如果你想要真正理解优化,我建议从 Davide Scassola 在Triality上简洁而优美的介绍开始,阅读《优化算法在交易策略中的应用介绍》,然后阅读 Robert Pardo 的书籍,《交易策略的评估与优化》策略(www.amazon.com/Evaluation-Optimization-Trading-Strategies/dp/0470128011)。

最后的话

好吧,任何故事迟早都会结束,这本书也不例外。即使你一开始对 FX 市场和算法交易一无所知,现在你无疑已经提升到了一个新的水平。你对 FX 市场的了解与初出茅庐的专业交易员相当。你知道如何开发用于实时交易和生成可靠回测的交易应用。你还了解与交易相关的风险,特别是算法交易的风险。你有许多道路可以选择——在资金管理、性能分析和优化方面——但有一件事我真心希望你无论做什么都始终记住:

任何好的交易策略都一定有其背后的交易理念。如果策略只是随机选择的技术分析和参数的组合,那么再复杂的数学、再好的资金管理或再优化的算法都无法帮助。在市场中寻找灵感,并使用数学和编程工具来实施、测试和运行它们——不要害怕或贪婪。

posted @ 2025-09-19 10:35  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报