Python-金融交易实用指南-全-
Python 金融交易实用指南(全)
原文:
zh.annas-archive.org/md5/6efde0935976ca50d877b2b5774aeade
译者:飞龙
前言
算法交易帮助你通过量化分析制定策略,从而在市场上保持领先,获取利润并减少损失。本书将帮助你理解金融理论,并自信地执行一系列算法交易策略。
本书首先介绍算法交易、pyfinance 生态系统和 Quantopian。然后你将学习使用 Python 进行算法交易和量化分析,并学习如何在 Quantopian 上构建算法交易策略。随着你的进步,你将深入了解用于分析金融数据集的 Python 库,如 NumPy 和 pandas,并探索 matplotlib、statsmodels 和 scikit-learn 等库进行高级分析。接下来,你将探索有用的金融概念和理论,如金融统计学、杠杆和套期保值以及卖空,这将帮助你了解金融市场的运作方式。最后,你将发现用于分析和理解金融时间序列数据的数学模型和方法。
通过本交易书的学习,你将能够构建预测性交易信号,采用基本和高级算法交易策略,并在 Quantopian 平台上进行组合优化。
本书面向对象
本书适用于想要使用 Python 核心库探索算法交易的数据分析师和金融交易员。如果你正在寻找一本实用指南来执行各种算法交易策略,那么本书适合你。具备 Python 编程和统计学的基本工作知识将会有所帮助。
本书内容概要
第一章,算法交易和 Python 入门,介绍了关键的金融交易概念,并解释了为什么 Python 最适合算法交易。
第二章,Python 中的探索性数据分析,提供了处理任何数据集的第一步骤,即探索性数据分析的概述。
第三章,使用 NumPy 进行高速科学计算,详细介绍了 NumPy,这是一个用于快速和可扩展结构化数组和矢量化计算的库。
第四章,使用 pandas 进行数据操作和分析,介绍了建立在 NumPy 之上的 pandas 库,该库提供了用于结构化 DataFrame 的数据操作和分析方法。
第五章,使用 Matplotlib 进行数据可视化,聚焦于 Python 中的主要可视化库之一,Matplotlib。
第六章,统计估计、推断和预测,讨论了 statsmodels 和 scikit-learn 库,用于高级统计分析技术,时间序列分析技术,以及训练和验证机器学习模型。
第七章,Python 中的金融市场数据访问,描述了 Python 中检索市场数据的替代方法。
第八章,Zipline 和 PyFolio 介绍,涵盖了 Zipline 和 PyFolio,这是 Python 库,它们摆脱了算法交易策略的实际回测和性能/风险分析的复杂性。它们允许您完全专注于交易逻辑。
第九章,基本算法交易策略,介绍了算法策略的概念,以及八种不同的交易算法代表了最常用的算法。
要充分利用本书
按照附录部分的说明,使用存储在书籍 GitHub 存储库中的environment.yml
文件重新创建conda
虚拟环境。一个命令即可还原整个环境。
如果您使用本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库(链接在下一节中提供)访问代码。这样做将帮助您避免与复制和粘贴代码相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件,网址为github.com/PacktPublishing/Hands-On-Financial-Trading-with-Python
。如果代码有更新,将在现有 GitHub 存储库上进行更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/
获取。快去看看吧!
下载彩色图片
我们还提供了一份 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781838982881_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
文本中的代码
:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个示例:“让我们使用 Python 3.6 创建一个zipline_env
虚拟环境。”
代码块设置如下:
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol
from datetime import datetime
import pytz
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
from . import quandl # noqa
from . import csvdir # noqa
from . import quandl_eod # noqa
粗体:表示一个新术语、一个重要单词或在屏幕上看到的词。例如,菜单或对话框中的单词会出现在文本中,如下所示。这里是一个示例:“然后,在环境变量...对话框中指定变量。”
提示或重要说明
看起来像这样。
第一部分:算法交易简介
本节将向您介绍算法交易和 Python 中的重要概念。
本节包括以下章节:
- 第一章**,算法交易和 Python 简介
第一章:算法交易简介
在本章中,我们将带您走过交易的简要历史,并解释在哪些情况下手动交易和算法交易各自有意义。此外,我们将讨论金融资产类别,这是对不同类型金融资产的分类。您将了解现代电子交易所的组成部分,最后,我们将概述算法交易系统的关键组成部分。
在本章中,我们将涵盖以下主题:
-
穿越算法交易的演变
-
了解金融资产类别
-
穿越现代电子交易所
-
了解算法交易系统的组成部分
穿越算法交易的演变
从交换一种财产换另一种财产的概念自古以来就存在。在其最早期形式中,交易对于交换较不理想的财产以换取较理想的财产很有用。随着时间的流逝,交易演变为参与者试图找到一种以低于公平价值的价格购买和持有交易工具(即产品)的方式,希望能够在未来以高于购买价格的价格出售它们。这个低买高卖的原则成为迄今所有盈利交易的基础;当然,如何实现这一点就是复杂性和竞争的关键所在。
市场由供求基本经济力量驱动。当需求增加而供应没有相应增加,或者供应减少而需求没有减少时,商品变得稀缺并增值(即,其市场价格)。相反,如果需求下降而供应没有减少,或者供应增加而需求没有增加,商品变得更容易获得并且价值更低(市场价格更低)。因此,商品的市场价格应该反映基于现有供应(卖方)和现有需求(买方)的平衡价格。
手动交易方法有许多缺点,如下所示:
-
人类交易员天生处理新市场信息的速度较慢,使他们很可能错过信息或在解释更新的市场数据时出错。这导致了糟糕的交易决策。
-
人类总体上也容易受到分心和偏见的影响,从而降低利润和/或产生损失。例如,对于失去金钱的恐惧和赚钱的喜悦也会导致我们偏离理论上理解但在实践中无法执行的最佳系统化交易方法。此外,人们也天生且非均匀地偏向于盈利交易与亏损交易;例如,人类交易员往往会在盈利交易后迅速增加风险金额,并在亏损交易后减缓降低风险金额的速度。
-
人类交易员通过经历市场条件来学习,例如通过参与和交易实时市场。因此,他们无法从历史市场数据条件中学习和进行回测——这是自动化策略的一个重要优势,我们稍后将会看到。
随着技术的发展,交易已经从通过大声呼喊和示意购买和出售订单进行的交易演变为使用复杂、高效和快速的计算机硬件和软件执行交易,通常几乎没有人为干预。复杂的算法交易软件系统已经取代了人类交易员和工程师,而构建、运行和改进这些系统的数学家,即量化交易员,已经崛起。
特别是,自动化、计算机驱动的系统化/算法交易方法的主要优势如下:
-
计算机非常擅长执行明确定义和重复的基于规则的任务。它们可以非常快速地执行这些任务,并且可以处理大规模的吞吐量。
-
此外,计算机不会分心、疲倦或犯错误(除非存在软件错误,从技术上讲,这算是软件开发者的错误)。
-
算法交易策略在交易过程中也没有情绪上的损失或利润,因此它们可以坚持系统性的交易计划。
所有这些优势使得系统性算法交易成为建立低延迟、高吞吐量、可扩展和稳健交易业务的完美选择。
然而,算法交易并不总是比手动交易更好:
-
手动交易更擅长处理极其复杂的思想和现实交易运营的复杂性,有时很难将其表达为自动化软件解决方案。
-
自动交易系统需要大量的时间和研发成本投入,而手动交易策略通常能更快地进入市场。
-
算法交易策略也容易受到软件开发/操作错误的影响,这可能会对交易业务产生重大影响。整个自动交易操作在几分钟内被清除并不罕见。
-
通常,自动量化交易系统不擅长处理被称为黑天鹅事件的极不可能发生的事件,例如 LTCM 崩盘、2010 年闪崩、Knight Capital 崩盘等。
在本节中,我们了解了交易历史以及何时自动化/算法交易优于手动交易。现在,让我们继续前往下一节,在那里我们将了解被分类为金融资产类别的实际交易主题。
了解金融资产类别
算法交易涉及金融资产的交易。金融资产是一种价值来源于合同协议的非实物资产。
主要的金融资产类别如下:
-
股票(股票):这允许市场参与者直接投资于公司并成为公司的所有者。
-
固定收益(债券):这些代表投资者向借款人(例如政府或公司)提供的贷款。每张债券都有其到期日,到期日时贷款本金应偿还,并且通常由借款人在债券寿命期间支付固定或可变的利息。
-
房地产投资信托(REITs):这些是拥有、经营或融资产生收入的房地产的上市公司。这些可以被用作直接投资于房地产市场的代理,比如通过购买一处房产。
-
大宗商品:例如金属(银、金、铜等)和农产品(小麦、玉米、牛奶等)。它们是跟踪基础大宗商品价格的金融资产。
-
交易所交易基金(ETFs):ETF 是一个在交易所上市的安全性,跟踪其他证券的集合。ETFs,例如 SPY、DIA 和 QQQ,持有股票来跟踪更大型的著名标准普尔 500、道琼斯工业平均指数和纳斯达克股票指数。ETFs,如美国石油基金(USO),通过投资于短期 WTI 原油期货来跟踪油价。ETFs 是投资者以相对较低成本投资于广泛资产类别的便利投资工具。
-
外汇(FX)在不同货币对之间交易,主要货币包括美元(USD)、欧元(EUR)、英镑(GBP)、日元(JPY)、澳大利亚元(AUD)、新西兰元(NZD)、加拿大元(CAD)、瑞士法郎(CHF)、挪威克朗(NOK)和瑞典克朗(SEK)。它们通常被称为 G10 货币。
-
主要的金融衍生品包括期权和期货——这些是复杂的杠杆衍生产品,可以放大风险和回报:
a) 期货是金融合同,以预定的未来日期和价格购买或出售资产。
b) 期权是金融合同,赋予其所有者权利,但不是义务,以在规定的价格(行权价)之前或之后的特定日期买入或卖出基础资产。
在本节中,我们了解了金融资产类别及其独特属性。现在,让我们讨论现代电子交易交易所的订单类型和交易匹配算法。
通过现代电子交易交易所进行交易
第一个交易所是阿姆斯特丹证券交易所,始于 1602 年。在这里,交易是面对面进行的。将技术应用于交易的方式包括使用信鸽、电报系统、莫尔斯电码、电话、计算机终端,以及如今的高速计算机网络和先进的计算机。随着时间的推移,交易微观结构已经发展成为我们今天所熟悉的订单类型和匹配算法。
对于算法策略的设计,现代电子交易所微观结构的了解至关重要。
订单类型
金融交易策略采用各种不同的订单类型,其中一些最常见的包括市价订单、带价格保护的市价订单、立即取消(IOC)订单、填写和取消(FAK)订单、有效至当天(GTD)订单、长效(GTC)订单、止损订单和冰山订单。
对于我们将在本书中探讨的策略,我们将专注于市价订单、IOC 和 GTC。
市价订单
市价订单是需要立即以当前市场价格执行的买入或卖出订单,当执行的即时性优于执行价格时使用。
这些订单将以订单价格执行对立方的所有可用订单,直到要求的所有数量被执行。如果没有可用的流动性可以匹配,它可以被配置为停留在订单簿中或到期。停留在订单簿中意味着订单变为待定订单,被添加到订单簿中供其他参与者进行交易。到期意味着剩余订单数量被取消,而不是被添加到订单簿中,因此新订单无法与剩余数量匹配。
因此,例如,买入市价订单将与订单簿中从最佳价格到最差价格的所有卖出订单匹配,直到整个市价订单被执行。
这些订单可能会遭受极端的滑点,滑点被定义为已执行订单价格与发送订单时市场价格之间的差异。
IOC 订单
IOC 订单无法以比发送价格更差的价格执行,这意味着买入订单无法以高于订单价格的价格执行,卖出订单也无法以低于订单价格的价格执行。这个概念被称为限价,因为价格受限于订单可以执行的最差价格。
IOC 订单将继续与订单方的订单进行匹配,直到出现以下情况之一:
-
IOC 订单的全部数量被执行。
-
对方的被动订单价格比 IOC 订单的价格差。
-
IOC 订单部分执行,剩余数量到期。
如果 IOC 订单的价格优于另一方的最佳可用订单(即,买单低于最佳卖出价,或卖单高于最佳买入价),则根本不会执行,而只会过期。
GTC 订单
GTC 订单可以无限期存在,并需要特定的取消订单。
限价订单簿
交易所接受来自所有市场参与者的订单请求,并将其保存在限价订单簿中。限价订单簿是交易所在任何时间点上所有可见订单的视图。
买单(或竞价)按照从最高价格(即,最佳价格)到最低价格(即,最差价格)的顺序排列,而卖单(即卖出或报价)则按照从最低价格(即,最佳价格)到最高价格(即,最低价格)的顺序排列。
最高竞价价格被认为是最佳竞价价格,因为具有最高买价的买单首先被匹配,而对于卖价,情况相反,即具有最低卖价的卖单首先匹配。
相同方向、相同价格水平的订单按照先进先出(FIFO)的顺序排列,也被称为优先顺序 - 优先级更高的订单排在优先级较低的订单前面,因为优先级更高的订单比其他订单先到达了交易所。其他条件相同(即,订单方向、价格和数量相同)的情况下,优先级更高的订单将在优先级较低的订单之前执行。
交易所撮合引擎
电子交易所的撮合引擎使用交易所撮合算法执行订单的撮合。撮合过程包括检查市场参与者输入的所有活跃订单,并将价格交叉的订单进行匹配,直到没有可以匹配的未匹配订单为止 - 因此,价格在或高于其他卖单的买单与之匹配,反之亦然,即价格在或低于其他买单的卖单与之匹配。剩余订单保留在交易所撮合簿中,直到新的订单流入,如果可能的话,进行新的匹配。
在 FIFO 匹配算法中,订单首先按照价格从最佳价格到最差价格进行匹配。因此,来自最佳价格的买单会尝试与摆放在最低价格到最高价格的卖单(即要价/出价)匹配,而来自最高价格的卖单会尝试与摆放在最高价格到最低价格的买单匹配。新到达的订单将根据特定的规则进行匹配。对于具有更好价格的主动订单(价格优于另一侧的最佳价格水平的订单),它们将按照先到先服务的原则进行匹配,即首先出现的订单会提取流动性,因此首先匹配。对于坐在订单簿中的被动挂单,因为它们不会立即执行,它们将根据先到先服务的优先级进行分配。这意味着同一方和相同价格的订单将根据它们到达匹配引擎的时间进行排列;时间较早的订单将获得更好的优先级,因此有资格首先匹配。
在本节中,我们了解了现代电子交易所的订单类型和交易匹配引擎。现在,让我们继续前往下一节,我们将了解算法交易系统的组件。
了解算法交易系统的组件
客户端算法交易基础设施大致可以分为两类:核心基础设施和量化基础设施。
算法交易系统的核心基础设施
核心基础设施负责使用市场数据和订单输入协议与交易所进行通信。它负责在交易所和算法交易策略之间传递信息。
它的组件还负责捕获、时间戳和记录历史市场数据,这是算法交易策略研究和开发的重中之重。
核心基础设施还包括一层风险管理组件,以防止交易系统受到错误或失控的交易策略的影响,以防止灾难性结果发生。
最后,算法交易业务中涉及的一些不太光彩的任务,如后勤协调任务、合规性等,也由核心基础设施处理。
交易服务器
交易服务器涉及一个或多个计算机接收和处理市场和其他相关数据,并处理交易所信息(例如订单簿),并发出交易订单。
从限价订单簿中,交易所匹配簿的更新通过市场数据协议传播给所有市场参与者。
市场参与者拥有接收这些市场数据更新的交易服务器。尽管技术上,这些交易服务器可以位于世界任何地方,但现代算法交易参与者将其交易服务器放置在离交易所匹配引擎非常近的数据中心。这称为共同定位或直接市场访问(DMA)设置,这保证参与者尽可能快地收到市场数据更新,因为它们尽可能接近匹配引擎。
一旦市场数据更新通过交易所提供的市场数据协议通信到每个市场参与者,它们就使用称为市场数据接收处理程序的软件应用程序解码市场数据更新并将其馈送到客户端上的算法交易策略。
一旦算法交易策略消化了市场数据更新,根据策略中开发的智能,它生成外向订单流。这可以是在特定价格和数量上添加、修改或取消订单。
订单请求通常由一个名为订单录入网关的单独客户端组件接收。订单录入网关组件使用订单录入协议与交易所通信,将策略对交易所的请求进行转换。电子交易所对这些订单请求的响应通知被发送回订单录入网关。再次,针对特定市场参与者的订单流动,匹配引擎生成市场数据更新,因此回到此信息流循环的开头。
算法交易系统的量化基础设施
量化基础设施构建在核心基础设施提供的平台之上,并尝试在其上构建组件,以研究、开发和有效利用平台以产生收入。
研究框架包括回测、交易后分析(PTA)和信号研究组件等组件。
其他在研究中使用并部署到实时市场的组件包括限价订单簿、预测信号和信号聚合器,将单个信号组合成复合信号。
执行逻辑组件使用交易信号并完成管理活动,管理各种策略和交易工具之间的活动订单、持仓和损益(PnL)。
最后,交易策略本身有一个风险管理组件,用于管理和减轻不同策略和工具之间的风险。
交易策略
有利可图的交易理念始终是由人类直觉驱动的,这种直觉是从观察市场条件的模式和不同市场条件下各种策略的结果中发展起来的。
例如,历史上观察到,大规模的市场上涨会增强投资者信心,导致更多的市场参与者进入市场购买更多;因此,反复造成更大规模的上涨。相反,市场价格大幅下跌会吓跑投资于交易工具的参与者,导致他们抛售持有的资产并加剧价格下跌。市场观察到的这些直观观念导致了趋势跟随策略的想法。
还观察到,短期内价格的波动往往倾向于恢复到其之前的市场价格,导致了均值回归为基础的投机者和交易策略。同样,历史观察到类似产品价格的移动会相互影响,这也是直觉的合理性所在,这导致了相关性和共线性为基础的交易策略的产生,如统计套利和配对交易策略。
由于每个市场参与者使用不同的交易策略,最终的市场价格反映了大多数市场参与者的观点。与大多数市场参与者观点一致的交易策略在这些条件下是有利可图的。单一的交易策略通常不可能 100%的盈利,所以复杂的参与者有一个交易策略组合。
交易信号
交易信号也被称为特征、计算器、指标、预测器或阿尔法。
交易信号是驱动算法交易策略决策的因素。信号是从市场数据、另类数据(如新闻、社交媒体动态等)甚至我们自己的订单流中获得的明确的情报,旨在预测未来某些市场条件。
信号几乎总是源自对某些市场条件和/或策略表现的直觉观察。通常,大多数量化开发人员花费大部分时间研究和开发新的交易信号,以改善在不同市场条件下的盈利能力,并全面提高算法交易策略。
交易信号研究框架
大量的人力投入到研究和发现新信号以改善交易表现。为了以系统化、高效、可扩展和科学的方式做到这一点,通常第一步是建立一个良好的信号研究框架。
这个框架有以下子组件:
-
数据生成是基于我们试图构建的信号和我们试图捕捉/预测的市场条件/目标。在大多数现实世界的算法交易中,我们使用 tick 数据,这是代表市场上每个事件的数据。正如你可以想象的那样,每天都会有大量的事件发生,这导致了大量的数据,因此您还需要考虑对接收到的数据进行子抽样。子抽样有几个优点,例如减少数据规模,消除噪音/虚假数据片段,并突出显示有趣/重要的数据。
-
对与其尝试捕捉/预测的市场目标相关的特征的预测能力或有用性进行评估。
-
在不同市场条件下维护信号的历史结果,并调整现有信号以适应不断变化的市场条件。
信号聚合器
信号聚合器是可选组件,它们从各个信号中获取输入,并以不同的方式对其进行聚合,以生成新的复合信号。
一个非常简单的聚合方法是取所有输入信号的平均值,并将平均值作为复合信号值输出。
熟悉统计学习概念的读者 - bagging 和 boosting 的集成学习 - 可能能够发现这些学习模型与信号聚合器之间的相似之处。通常,信号聚合器只是统计模型(回归/分类),其中输入信号只是用于预测相同最终市场目标的特征。
策略执行
策略执行涉及根据交易信号的输出有效地管理和执行订单,以最小化交易费用和滑点。
滑点是市场价格和执行价格之间的差异,由于订单经历了延迟才能到达市场,价格在变化之前发生了变化,以及订单的大小在达到市场后引起价格变化所致。
在算法交易策略中使用的执行策略的质量可以显著改善/降低有利交易信号的表现。
限价订单簿
限价订单簿既在交易所撮合引擎中构建,也在算法交易策略期间构建,尽管并不一定所有算法交易信号/策略都需要整个限价订单簿。
复杂的算法交易策略可以将更多的智能集成到其限价订单簿中。我们可以在限价订单簿中检测和跟踪自己的订单,并了解根据我们的优先级,我们的订单被执行的概率是多少。我们还可以利用这些信息在交易所的订单录入网关收到执行通知之前甚至执行我们自己的订单,并利用这种能力为我们谋利。通过限价订单簿和许多电子交易所的市场数据更新,还可以实现更复杂的微观结构特征,例如检测冰山订单、检测止损订单、检测大量买入/卖出订单流入或流出等。
头寸和损益管理
让我们探讨交易策略通过执行交易开仓和平仓多头和空头头寸时,头寸和损益如何演变。
当策略没有市场头寸时,即价格变动不影响交易账户价值时,这被称为持平头寸。
从持平头寸开始,如果执行买单,则被称为持有多头头寸。如果策略持有多头头寸且价格上涨,则头寸从价格上涨中获利。在这种情况下,损益也增加,即利润增加(或亏损减少)。相反,如果策略持有多头头寸且价格下跌,则头寸从价格下跌中损失。在这种情况下,损益减少,例如,利润减少(或亏损增加)。
从持平头寸开始,如果执行卖单,则被称为持有空头头寸。如果策略持有空头头寸且价格下跌,则头寸从价格下跌中获利。在这种情况下,损益增加。相反,如果策略持有空头头寸且价格上涨,则损益减少。仍然未平仓头寸的损益被称为未实现损益(unrealized PnL),因为只要头寸保持未平仓状态,损益就会随着价格变动而变化。
通过卖出等量的工具来关闭多头头寸。这被称为平仓,此时损益被称为实现损益(realized PnL),因为价格变动不再影响损益,因为头寸已关闭。
类似地,空头头寸通过买入与头寸规模相同的数量来关闭。
在任何时刻,总损益(total PnL)是所有已平仓头寸的实现损益和所有未平仓头寸的未实现损益的总和。
当多头或空头头寸由以不同价格和不同大小进行多次买入或卖出时,则通过计算成交量加权平均价格(Volume Weighted Average Price,VWAP)来计算头寸的平均价格,即根据每个价格上执行的数量加权平均。按市价计价是指获取头寸的 VWAP,并将其与当前市场价格进行比较,以了解某个多头/空头头寸的盈利或亏损情况。
回测
回测器使用历史记录的市场数据和模拟组件来模拟算法交易策略的行为和性能,就好像它在过去被部署到实时市场中一样。直到策略的表现符合预期,才会开发和优化算法交易策略。
回测器是需要模拟市场数据流、客户端和交易所端延迟的复杂组件在软件和网络组件中、准确的 FIFO 优先级、滑点、费用和市场影响来自策略订单流(即其他市场参与者将如何对策略的订单流作出反应添加到市场数据流)以生成准确的策略和投资组合绩效统计数据。
PTA
PTA 是在模拟或实时市场运行的算法交易策略生成的交易上执行的。
PTA 系统用于从历史回测策略生成性能统计,目的是了解历史策略性能期望。
当应用于由实时交易策略生成的交易时,PTA 可用于了解实时市场中的策略性能,并比较和确认实时交易性能是否符合模拟策略性能期望。
风险管理
良好的风险管理原则确保策略以最佳 PnL 表现运行,并采取措施防止失控 / 错误策略。
不良的风险管理不仅可以将有利可图的交易策略变成无利可图的策略,而且还可能由于无法控制的策略损失、失灵的策略和可能的监管后果而使投资者的整个资本面临风险。
概要
在本章中,我们学习了什么时候算法交易比手动交易更具优势,金融资产类别是什么,最常用的订单类型是什么,限价订单簿是什么,以及订单是如何由金融交易所匹配的。
我们还讨论了算法交易系统的关键组成部分 - 核心基础设施和量化基础设施,包括交易策略、执行、限价订单簿、持仓、PnL 管理、回测、交易后分析和风险管理。
在下一章中,我们将讨论 Python 在算法交易中的价值。
第二部分:深入了解用于金融数据集分析的 Python 库
本节将深入介绍核心 Python 库 NumPy 和 pandas,这些库用于分析和操作大型数据框。我们还将涵盖与 pandas 密切相关的可视化库 Matplotlib。最后,我们将介绍 statsmodels 和 scikit-learn 库,这些库允许更高级的金融数据集分析。
本节包括以下章节:
-
第二章**, Python 中的探索性数据分析
-
第三章**, 使用 NumPy 进行高速科学计算
-
第四章**, 使用 Pandas 进行数据操作和分析
-
第五章**, 使用 Matplotlib 进行数据可视化
-
第六章**, 统计估计、推断和预测
第二章:Python 中的探索性数据分析
本章重点介绍探索性数据分析(EDA),这是处理任何数据集的第一步。EDA 的目标是将数据加载到最适合进一步分析的数据结构中,以识别和纠正任何错误/坏数据,并获得对数据的基本见解——字段的类型有哪些;它们是否是分类的;有多少缺失值;字段之间的关系等等。
这些是本章讨论的主要话题:
-
EDA 介绍
-
用于 EDA 的特殊 Python 库
技术要求
本章中使用的 Python 代码可以在书的代码库中的Chapter02/eda.ipynb
笔记本中找到。
EDA 介绍
EDA 是从感兴趣的结构化/非结构化数据中获取、理解和得出有意义的统计见解的过程。这是在对数据进行更复杂的分析之前的第一步,例如从数据中预测未来的期望。在金融数据的情况下,EDA 有助于获得后续用于构建盈利交易信号和策略的见解。
EDA 指导后续决策,包括使用或避免哪些特征/信号,使用或避免哪些预测模型,并验证和引入关于变量性质和它们之间关系的正确假设,同时否定不正确的假设。
EDA 也很重要,可以理解样本(完整数据集的代表性较小数据集)统计数据与总体(完整数据集或终极真相)统计数据之间的差异,并在绘制关于总体的结论时记住这一点,基于样本观察。因此,EDA 有助于减少后续可能的搜索空间;否则,我们将浪费更多的时间后来构建不正确/不重要的模型或策略。
必须以科学的心态来对待 EDA。有时,我们可能会基于轶事证据而不是统计证据得出不充分验证的结论。
基于轶事证据的假设受到以下问题的影响:
-
不具有统计学意义——观测数量太少。
-
选择偏见——假设只是因为它首先被观察到而产生的。
-
确认偏见——我们对假设的内在信念会偏向于我们的结果。
-
观察中的不准确性。
让我们探索 EDA 涉及的不同步骤和技术,使用真实数据集。
EDA 的步骤
以下是 EDA 涉及的步骤列表(我们将在接下来的子章节中逐个进行讨论):
-
加载必要的库并进行设置
-
数据收集
-
数据整理/整理
-
数据清洗
-
获得描述性统计
-
数据的可视化检查
-
数据清洗
-
高级可视化技术
加载必要的库并进行设置
我们将使用numpy
、pandas
和matplotlib
,这些库可以通过以下代码加载:
%matplotlib inline
import numpy as np
import pandas as pd
from scipy import stats
import seaborn as sn
import matplotlib.pyplot as plt
import mpld3
mpld3.enable_notebook()
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_rows', 2)
我们使用mpld3
库来启用 Jupyter 的matplotlib
图表内的缩放。 前面代码块的最后一行指定了应显示pandas
DataFrame 的最大行数为两行。
数据收集
数据收集通常是 EDA 的第一步。 数据可能来自许多不同的来源(逗号分隔值(CSV)文件、Excel 文件、网页抓取、二进制文件等),通常需要正确标准化和首先正确格式化在一起。
对于这个练习,我们将使用存储在.csv
格式中的 5 年期间的三种不同交易工具的数据。 这些工具的身份故意没有透露,因为这可能泄露它们的预期行为/关系,但我们将在练习结束时透露它们的身份,以直观地评估我们对它们进行的 EDA 的表现如何。
让我们从加载我们可用的数据集开始,将其加载到三个 DataFrame(A
,B
和C
)中,如下所示:
A = pd.read_csv('A.csv', parse_dates=True, index_col=0);
A
DataFrame A
的结构如下所示:
图 2.1 – 从 A.csv 文件构造的 DataFrame
类似地,让我们加载 DataFrame B
,如下所示:
B = pd.read_csv('B.csv', parse_dates=True, index_col=0);
B
DataFrame B
的结构如下所示:
图 2.2 – 从 B.csv 文件构造的 DataFrame
最后,让我们将C
数据加载到一个 DataFrame 中,如下所示:
C = pd.read_csv('C.csv', parse_dates=True, index_col=0);
C
我们看到C
有以下字段:
图 2.3 – 从 C.csv 文件构造的 DataFrame
如我们所见,所有三个数据源的格式都是2015-05-15
和2020-05-14
。
数据整理/处理
数据很少是以可直接使用的格式提供的。 数据整理/处理指的是从初始原始来源操纵和转换数据的过程,使其成为结构化的、格式化的和易于使用的数据集。
让我们使用pandas.DataFrame.join(...)
来合并这些 DataFrame,并对齐它们以具有相同的DateTimeIndex
格式。 使用lsuffix=
和rsuffix=
参数,我们将_A
,_B
和_C
后缀分配给来自三个 DataFrame 的列,如下所示:
merged_df = A.join(B, how='outer', lsuffix='_A', sort=True).join(C, how='outer', lsuffix='_B', rsuffix='_C', sort=True)
merged_df
我们将检查我们刚刚创建的merged_df
DataFrame,并确保它具有我们从所有三个 DataFrame 中预期的所有字段(仅显示前七列)。 DataFrame 可以在这里看到:
图 2.4 – 通过合并 DataFrame A、B 和 C 构造的 DataFrame
请注意,原始三个数据框(A
、B
和 C
)分别有 1,211、1,209 和 1,206 行,但合并后的数据框有 1,259 行。这是因为我们使用了外部连接,它使用了所有三个数据框的日期的并集。当它在特定日期的特定数据框中找不到值时,它会将该数据框的字段的那个位置放置一个 NaN
值。
数据清洗
数据清洗是指处理来自缺失数据、不正确数据值和异常值的数据错误的过程。
在我们的示例中,merged_df
的许多字段都缺失原始数据集和不同日期数据框合并而来的字段。
让我们首先检查是否存在所有值都缺失(NaN
)的行,如下所示:
merged_df[merged_df.isnull().all(axis=1)]
结果表明,我们没有任何所有字段都缺失的行,如我们所见:
图 2.5 – DataFrame 表明没有所有字段都缺失的行。
现在,让我们找出有多少行存在至少一个字段缺失/NaN
的,如下所示:
merged_df[['Close_A', 'Close_B', 'Close_C']].isnull().any(axis=1).sum()
因此,结果显示我们的 1,259 行中有 148 行具有一个或多个字段缺失值,如下所示:
148
对于我们的进一步分析,我们需要有效的 Close
价格。因此,我们可以通过运行以下代码删除所有三个工具的任何 Close
价格缺失的行:
valid_close_df = merged_df.dropna(subset=['Close_A', 'Close_B', 'Close_C'], how='any')
删除缺失的 Close
价格后,我们不应该再有缺失的 Close
价格字段,如下代码段所示:
valid_close_df[['Close_A', 'Close_B', 'Close_C']].isnull().any(axis=1).sum()
结果证实,不再存在任何 Close_A
、Close_B
或 Close_C
字段为 NaN
值的行,如我们所见:
0
让我们检查新的 DataFrame,如下所示:
valid_close_df
这是结果(仅显示前七列):
图 2.6 – 没有任何收盘价缺失/NaN 值的结果 DataFrame
如预期的那样,我们删除了具有任何收盘价缺失/NaN
值的 148 行。
接下来,让我们处理任何其他字段具有 NaN
值的行,首先了解有多少这样的行。我们可以通过运行以下代码来做到这一点:
valid_close_df.isnull().any(axis=1).sum()
这是该查询的输出:
165
因此,存在 165 行至少有一些字段缺失值。
让我们快速检查一下至少有一些字段缺失值的几行,如下所示:
valid_close_df[valid_close_df.isnull().any(axis=1)]
以下显示了一些具有一些缺失值的行(仅显示前七列),如下所示:
图 2.7 – DataFrame 表明仍然有一些行存在一些缺失值
因此,我们可以看到 2015-05-18
(在前述截屏中不可见)的 Low_C
字段和 2020-05-01
的 Open_B
字段有 NaN
值(当然还有其他 163 个)。
让我们使用 pandas.DataFrame.fillna(...)
方法与一种称为 backfill
的方法 —— 这使用缺失值后的下一个有效值来填充缺失值。代码如下所示:
valid_close_complete = valid_close_df.fillna(method='backfill')
让我们看看 backfilling 的影响,如下所示:
valid_close_complete.isnull().any(axis=1).sum()
现在,这是查询的输出:
0
正如我们所看到的,在进行 backfill
操作之后,任何行的任何字段都不再有缺失或 NaN
值。
获取描述性统计数据
下一步是生成数据的关键基本统计信息,以便熟悉每个字段,使用 DataFrame.describe(...)
方法。代码如下所示:
pd.set_option('display.max_rows', None)
valid_close_complete.describe()
请注意,我们已经增加了要显示的 pandas
DataFrame 的行数。
这是运行 pandas.DataFrame.describe(…)
后的输出,仅显示了前七列:
图 2.8 – 有效关闭完整 DataFrame 的描述统计
前面的输出为我们的 DataFrame 中的每个字段提供了快速摘要统计信息。
从 图 2.8 的关键观察点可以总结如下:
-
Volume_C
的所有统计值都为0
,这意味着每一行的Volume_C
值都设置为0
。因此,我们需要移除此列。 -
Open_C
的最小值为-400
,这不太可能是真实的,原因如下:a) 其他价格字段 ——
High_C
、Low_C
、Close_C
和Adj Close_C
—— 的所有最小值都约为9
,因此Open_C
具有-400
的最小值是没有意义的。b) 考虑到
Open_C
的第 25 个百分位数为12.4
,其最小值不太可能远低于此。c) 资产的价格应为非负数。
-
Low_C
的最大值为330
,这同样不太可能,原因如下:a) 出于先前所述的相同原因,
Open_C
是不正确的。b) 此外,考虑到
Low_C
应始终低于High_C
,根据定义,一天中的最低价格必须低于当天的最高价格。
让我们将所有 pandas
DataFrame 的输出恢复为只有两行,如下所示:
pd.set_option('display.max_rows', 2)
现在,让我们移除所有三个工具的 Volume
字段,使用以下代码:
prices_only = valid_close_complete.drop(['Volume_A', 'Volume_B', 'Volume_C'], axis=1)
prices_only
而 prices_only
DataFrame 具有以下数据(仅显示前七列):
图 2.9 – 仅价格的 DataFrame
预期之中的是,我们移除了三个工具的交易量列之后,将 DataFrame 维度减少到 1111 × 15
—— 这些以前是 1111 × 18
。
数据的视觉检查
似乎没有任何明显的错误或不一致之处,所以让我们快速可视化价格,看看这是否符合我们从描述性统计中学到的内容。
首先,我们将从A
的价格开始,因为根据描述性统计摘要,我们期望这些是正确的。代码如下所示:
valid_close_complete['Open_A'].plot(figsize=(12,6), linestyle='--', color='black', legend='Open_A')
valid_close_complete['Close_A'].plot(figsize=(12,6), linestyle='-', color='grey', legend='Close_A')
valid_close_complete['Low_A'].plot(figsize=(12,6), linestyle=':', color='black', legend='Low_A')
valid_close_complete['High_A'].plot(figsize=(12,6), linestyle='-.', color='grey', legend='High_A')
输出与我们的期望一致,我们可以根据统计数据和下面截图中显示的图表得出A
的价格是有效的结论:
图 2.10 – 展示了交易工具 A 的开盘价、收盘价、最高价和最低价在 5 年内的价格
现在,让我们绘制 C 的价格图,看看图表是否提供了关于我们怀疑某些价格不正确的进一步证据。代码如下所示:
valid_close_complete['Open_C'].plot(figsize=(12,6), linestyle='--', color='black', legend='Open_C')
valid_close_complete['Close_C'].plot(figsize=(12,6), linestyle='-', color='grey', legend='Close_C')
valid_close_complete['Low_C'].plot(figsize=(12,6), linestyle=':', color='black', legend='Low_C')
valid_close_complete['High_C'].plot(figsize=(12,6), linestyle='-.', color='grey', legend='High_C')
输出证实了Open_C
和Low_C
具有一些与其他值极端相距甚远的错误值—这些是异常值。下面的截图显示了说明这一点的图表:
图 2.11 – 展示了 C 价格中正负两个方向的大异常值的图表
我们需要进行一些进一步的数据清理,以消除这些异常值,以便我们不从数据中得出不正确的统计见解。
检测和移除异常值最常用的两种方法是四分位数范围(IQR)和 Z 分数。
IQR
IQR 方法使用整个数据集上的百分位数/分位数值范围来识别和移除异常值。
在应用 IQR 方法时,我们通常使用极端百分位数值,例如 5% 到 95%,以最小化移除正确数据点的风险。
在我们的Open_C
示例中,让我们使用第 25 百分位数和第 75 百分位数,并移除所有数值超出该范围的数据点。第 25 到 75 百分位数范围是(12.4, 17.68
),因此我们将移除异常值-400
。
Z 分数
Z 分数(或标准分数)是通过从数据集中减去每个数据点的均值,并通过数据集的标准偏差进行归一化得到的。
换句话说,数据点的 Z 分数表示数据点与所有数据点的均值之间的标准偏差距离。
对于正态分布(适用于足够大的数据集),有一个68-95-99的分布规则,总结如下:
-
所有数据的 68% 将落在距离均值一个标准差的范围内。
-
所有数据的 95% 将落在距离均值两个标准差的范围内。
-
所有数据的 99% 将落在距离均值三个标准差的范围内。
因此,在计算了数据集中所有数据点的 Z 得分(足够大的数据集)之后,存在着大约 1% 的数据点具有 Z 得分大于或等于3
的概率。
因此,我们可以利用这些信息筛选出所有 Z 得分为3
或更高的观察结果以检测并移除异常值。
在我们的示例中,我们将删除所有 Z 得分小于-6
或大于6
的值的行—即,与平均值相差六个标准偏差。
首先,我们使用scipy.stats.zscore(...)
计算prices_only
DataFrame 中每列的 Z 得分,然后我们使用numpy.abs(...)
获取 Z 得分的大小。最后,我们选择所有字段的 Z 得分低于 6 的行,并将其保存在no_outlier_prices
DataFrame 中。代码如下所示:
no_outlier_prices = prices_only[(np.abs(stats.zscore(prices_only)) < 6).all(axis=1)]
让我们看看这个 Z 得分异常值移除代码对仪器C
的价格字段产生了什么影响,通过重新绘制其价格并与之前的图进行比较,如下所示:
no_outlier_prices['Open_C'].plot(figsize=(12,6), linestyle='--', color='black', legend='Open_C')
no_outlier_prices['Close_C'].plot(figsize=(12,6), linestyle='-', color='grey', legend='Close_C')
no_outlier_prices['Low_C'].plot(figsize=(12,6), linestyle=':', color='black', legend='Low_C')
no_outlier_prices['High_C'].plot(figsize=(12,6), linestyle='-.', color='grey', legend='High_C')
这是输出:
图 2.12 – 应用数据清理移除异常值后显示 C 的价格的绘图
绘图清楚地显示了Open_C
和Low_C
的早期极端值观察已被丢弃;不再有-400
的低谷。
请注意,虽然我们移除了极端异常值,但我们仍然能够保留 2015 年、2018 年和 2020 年价格的剧烈波动,因此并没有导致大量数据损失。
我们还要通过重新检查描述性统计数据来检查我们的异常值移除工作的影响,如下所示:
pd.set_option('display.max_rows', None)
no_outlier_prices[['Open_C', 'Close_C', 'Low_C', 'High_C']].describe()
这些统计数据看起来明显更好—正如我们在以下截图中看到的,所有价格的min
和max
值现在看起来符合预期,并且没有极端值,所以我们在数据清理任务中取得了成功:
图 2.13 – 选择的列的无异常价格的描述性统计
让我们将要显示的pandas
DataFrame 的行数重置回来,如下所示:
pd.set_option('display.max_rows', 5)
高级可视化技术
在本节中,我们将探讨一元和多元统计可视化技术。
首先,让我们收集三个工具的收盘价格,如下所
close_prices = no_outlier_prices[['Close_A', 'Close_B', 'Close_C']]
接下来,让我们计算每日收盘价格变动,以评估三个工具之间的每日价格变动是否存在关系。
每日收盘价格变动
我们将使用 pandas.DataFrame.shift(...)
方法将原始 DataFrame 向前移动一个周期,以便我们可以计算价格变动。这里的 pandas.DataFrame.fillna(...)
方法修复了由于 shift
操作而在第一行生成的一个缺失值。最后,我们将列重命名为 Delta_Close_A
、Delta_Close_B
和 Delta_Close_C
,以反映这些值是价格差异而不是实际价格。以下是代码示例:
delta_close_prices = (close_prices.shift(-1) - close_prices).fillna(0)
delta_close_prices.columns = ['Delta_Close_A', 'Delta_Close_B', 'Delta_Close_C']
delta_close_prices
新生成的 delta_close_prices
DataFrame 的内容如下截图所示:
图 2.14 – delta_close_prices DataFrame
从前几个实际价格和计算出的价格差异来看,这些值看起来是正确的。
现在,让我们快速检查这个新 DataFrame 的摘要统计信息,以了解价格差值的分布情况,如下所示:
pd.set_option('display.max_rows', None)
delta_close_prices.describe()
这个 DataFrame 的描述性统计如下所示截图所示:
图 2.15 – delta_close_prices DataFrame 的描述性统计
我们可以从这些统计数据中观察到,所有三个 delta 值的均值都接近于 0,仪器 A
经历了大幅价格波动,而仪器 C
则经历了明显较小的价格波动(来自 std
字段)。
直方图
让我们观察 Delta_Close_A
的分布,以更加熟悉它,使用直方图绘制。以下是代码示例:
delta_close_prices['Delta_Close_A'].plot(kind='hist', bins=100, figsize=(12,6), color='black', grid=True)
在下面的截图中,我们可以看到分布大致呈正态分布:
图 2.16 – Delta_Close_A 值的直方图大致呈正态分布,围绕着 0 值
箱线图
让我们绘制一个箱线图,这也有助于评估值的分布。以下是相应代码的示例:
delta_close_prices['Delta_Close_B'].plot(kind='box', figsize=(12,6), color='black', grid=True)
输出结果如下截图所示:
图 2.17 – 箱线图显示均值、中位数、四分位距(25th 到 75th 百分位数)和异常值
相关性图表
多元数据统计的第一步是评估 Delta_Close_A
、Delta_Close_B
和 Delta_Close_C
之间的相关性。
最方便的方法是绘制一个相关性散点矩阵,显示三个变量之间的成对关系,以及每个单独变量的分布。
在我们的示例中,我们演示了使用核密度估计(KDE)的选项,这与直方图密切相关,但在对角线上的图中提供了更平滑的分布表面。其代码如下所示:
pd.plotting.scatter_matrix(delta_close_prices, figsize=(10,10), color='black', alpha=0.75, diagonal='kde', grid=True)
这个图表表明,Delta_Close_A
和 Delta_Close_B
之间存在强烈的正相关性,以及 Delta_Close_C
与另外两个变量之间存在强烈的负相关性。对角线也显示了每个单独变量的分布,使用了 KDE。
下面是字段的散点图:
图 2.18 – Delta_Close 字段的散点图,对角线上是 KDE 直方图
接下来,让我们看一些提供变量之间关系的统计数据。DataFrame.corr(...)
为我们完成了这项工作,并且还显示了线性相关性。这可以在以下代码片段中看到:
delta_close_prices.corr()
相关矩阵证实了 Delta_Close_A
和 Delta_Close_B
之间存在强烈的正相关性(非常接近 1.0,这是最大值),这符合我们根据散点图的预期。此外,Delta_Close_C
与其他两个变量呈负相关(接近 -1.0 而不是 0.0)。
您可以在以下屏幕截图中看到相关矩阵:
图 2.19 – Delta_Close_A、Delta_Close_B 和 Delta_Close_C 的相关矩阵
成对相关热图
一种称为 seaborn.heatmap(...)
的替代可视化技术,如下面的代码片段所示:
plt.figure(figsize=(6,6))
sn.heatmap(delta_close_prices.corr(), annot=True, square=True, linewidths=2)
在下面的屏幕截图中显示的图中,最右侧的刻度显示了一个图例,其中最暗的值代表最强的负相关,最浅的值代表最强的正相关:
图 2.20 – Seaborn 热图可视化 Delta_Close 字段之间的成对相关性
热图在图表中以图形方式显示了前一节中的表格信息 —— Delta_Close_A
和 Delta_Close_B
之间存在非常高的相关性,而 Delta_Close_A
和 Delta_Close_C
之间存在非常高的负相关性。Delta_Close_B
和 Delta_Close_C
之间也存在非常高的负相关性。
A、B 和 C 的身份揭示以及 EDA 的结论
A
仪器是 B
仪器是 C
仪器是芝加哥期权交易所(CBOE)波动率指数(VIX),基本上跟踪市场在任何给定时间内的波动性(基本上,是股票指数价格波动的函数)。
从我们对神秘仪器的 EDA 中,我们得出了以下结论:
-
C
(VIX)的价格不能为负值,也不能超过 90,这在历史上一直成立。 -
A
(DJIA)和B
(SPY)在 2008 年和 2020 年都有巨大的跌幅,分别对应股市崩盘和 COVID-19 大流行。同时,C
(VIX)的价格也在同一时间上升,表明市场动荡加剧。 -
A
(DJIA)的每日价格波动最大,其次是B
(SPY),最后是C
(VIX),其每日价格波动非常小。考虑到它们所隐藏的基础工具,这些观察也是正确的。
A
(DJIA)和 B
(SPY)具有非常强的正相关性,这是有道理的,因为它们都是大型市值股票指数。C
(VIX)与 A
(DJIA)和 B
(SPY)都有很强的负相关性,这也是有道理的,因为在繁荣时期,波动性保持低位,市场上涨,在危机期间,波动性激增,市场下跌。
在下一节中,我们介绍了一个特殊的 Python 库,它可以自动生成最常见的 EDA 图表和表格。
用于 EDA 的特殊 Python 库
有多个 Python 库可以提供单行代码的 EDA。其中最先进的之一是 dtale
,如下面的代码片段所示:
import dtale
dtale.show(valid_close_df)
前面的命令生成了一个包含所有数据的表格(仅显示前七列),如下所示:
图 2.21 – dtale 组件显示对 valid_close_df DataFrame 的类似电子表格的控制
点击顶部的箭头会显示一个带有所有功能的菜单,如下面的截图所示:
图 2.22 – dtale 全局菜单显示其功能
点击列标题会显示每个特征的单独命令,如下面的截图所示:
图 2.23 – dtale 列菜单显示列功能
交互式 EDA,而不是命令驱动的 EDA,具有其优势——它直观、促进视觉创造力,并且速度更快。
摘要
EDA 的目标是了解我们处理的数据集,并纠正基本数据错误,如不太可能的异常值。我们已经描述了通过运行单独的 Python 命令构建的 EDA,以及使用特殊的 Python EDA 库进行自动化的 EDA。
下一章介绍了我们其中一个最重要的 Python 库:numpy
。
第三章:使用 NumPy 进行高速科学计算
本章介绍了 NumPy,一个用于矩阵计算的高速 Python 库。大多数数据科学/算法交易库都是基于 NumPy 的功能和约定构建的。
在本章中,我们将讨论以下关键主题:
-
NumPy 简介
-
创建 NumPy n 维数组(ndarrays)
-
与 NumPy 数组一起使用的数据类型
-
ndarray 的索引
-
基本 ndarray 操作
-
对 ndarray 进行文件操作
技术要求
本章中使用的 Python 代码在书籍代码仓库的 Chapter03/numpy.ipynb
笔记本中可用。
NumPy 简介
在 Python 中,可以使用列表表示多维异构数组。列表是一个一维数组,列表的列表是一个二维数组,列表的列表的列表是一个三维数组,依此类推。然而,这种解决方案很复杂,难以使用,并且非常慢。
NumPy Python 库的主要设计目标之一是引入高性能和可扩展的结构化数组和矢量化计算。
大多数 NumPy 中的数据结构和操作都是用 C/C++ 实现的,这保证了它们具有优越的速度。
创建 NumPy ndarrays
ndarray 是一个极其高性能和空间有效的多维数组数据结构。
首先,我们需要导入 NumPy 库,如下所示:
import numpy as np
接下来,我们将开始创建一个一维 ndarray。
创建一维 ndarray
以下代码行创建一个一维 ndarray:
arr1D = np.array([1.1, 2.2, 3.3, 4.4, 5.5]);
arr1D
这将产生以下输出:
array([1.1, 2.2, 3.3, 4.4, 5.5])
让我们用以下代码检查数组的类型:
type(arr1D)
这表明数组是一个 NumPy ndarray,如下所示:
numpy.ndarray
我们可以轻松地创建两个或更多维的 ndarray。
创建二维 ndarray
要创建一个二维 ndarray,请使用以下代码:
arr2D = np.array([[1, 2], [3, 4]]);
arr2D
结果有两行,每行有两个值,所以它是一个 2 x 2 的 ndarray,如下代码片段所示:
array([[1, 2],
[3, 4]])
创建任意维度的 ndarray
ndarray 可以构造具有任意维度的数组。以下代码创建了一个 2 x 2 x 2 x 2 维的 ndarray:
arr4D = np.array(range(16)).reshape((2, 2, 2, 2));
arr4D
数组的表示如下所示:
array([[[[ 0, 1],
[ 2, 3]],
[[ 4, 5],
[ 6, 7]]],
[[[ 8, 9],
[10, 11]],
[[12, 13],
[14, 15]]]])
NumPy ndarrays 具有描述 ndarray 维度的 shape
属性,如下代码片段所示:
arr1D.shape
以下片段显示 arr1D
是一个包含五个元素的一维数组:
(5,)
我们可以使用以下代码检查 arr2D
上的 shape
属性:
arr2D.shape
如预期的那样,输出描述它为一个 2 x 2 的 ndarray,如下所示:
(2, 2)
在实践中,有一些矩阵经常被使用,比如零矩阵、全一矩阵、单位矩阵、包含一系列数字的矩阵或随机矩阵。NumPy 提供了支持用一个命令生成这些常用 ndarray 的功能。
使用 np.zeros(...) 创建 ndarray
np.zeros(...)
方法创建一个填充有全 0 的 ndarray,如下代码片段所示:
np.zeros(shape=(2,5))
输出是全 0,维度为 2 x 5,如下代码片段所示:
array([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
使用 np.ones(...) 创建一个 ndarray
np.ones(...)
类似,但每个值都被赋予值 1,而不是 0。该方法如下所示:
np.ones(shape=(2,2))
结果是一个 2 x 2 的 ndarray,每个值都设置为 1,如下面的代码片段所示:
array([[1., 1.],
[1., 1.]])
使用 np.identity(...) 创建一个 ndarray
在矩阵运算中,我们经常需要创建一个单位矩阵,可以使用 np.identity(...)
方法,如下面的代码片段所示:
np.identity(3)
这将创建一个 3 x 3 的单位矩阵,对角线上为 1,其他位置为 0,如下面的代码片段所示:
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
使用 np.arange(...) 创建一个 ndarray
np.arange(...)
是 Python range(...)
方法的 NumPy 等价物。这生成具有起始值、结束值和增量的值,但是返回的是 NumPy ndarrays,如下所示:
np.arange(5)
返回的 ndarray 如下所示:
array([0, 1, 2, 3, 4])
默认情况下,值从 0 开始,递增为 1。
使用 np.random.randn(...) 创建一个 ndarray
np.random.randn(...)
生成一个指定维度的 ndarray,每个元素从标准正态分布中随机抽取的随机值(mean=0
,std=1
),如下所示:
np.random.randn(2,2)
输出是一个 2 x 2 的 ndarray,其值为随机值,如下面的代码片段所示:
array([[ 0.57370365, -1.22229931],
[-1.25539335, 1.11372387]])
使用 NumPy ndarrays 的数据类型
NumPy ndarrays 是同质的——即,ndarray 中的每个元素具有相同的数据类型。这与 Python 列表不同,Python 列表可以包含不同数据类型的元素(异质的)。
np.array(...)
方法接受一个显式的 dtype=
参数,允许我们指定 ndarray 应该使用的数据类型。常用的数据类型包括 np.int32
、np.float64
、np.float128
和 np.bool
。请注意,np.float128
在 Windows 上不受支持。
你应该注意各种数值类型对 ndarrays 的内存使用,主要原因是——数据类型提供的精度越高,其内存需求就越大。对于某些操作,较小的数据类型可能已经足够了。
创建一个 numpy.float64 数组
要创建一个 128 位浮点值数组,请使用以下代码:
np.array([-1, 0, 1], dtype=np.float64)
输出如下所示:
array([-1., 0., 1.], dtype=float64)
创建一个 numpy.bool 数组
我们可以通过将指定的值转换为目标类型来创建一个 ndarray。在下面的代码示例中,尽管提供了整数数据值,但由于指定了数据类型为 np.bool
,所以生成的 ndarray 的 dtype
为 bool
:
np.array([-1, 0, 1], dtype=np.bool)
值如下所示:
array([ True, False, True])
我们观察到整数值 (-1, 0, 1
) 被转换为布尔值 (True, False, True
)。0
被转换为 False
,所有其他值被转换为 True
。
ndarrays 的 dtype 属性
ndarrays 有一个 dtype
属性用于检查数据类型,如下所示:
arr1D.dtype
输出是一个 NumPy dtype
对象,其值为 float64
,如下所示:
dtype('float64')
使用 numpy.ndarrays.astype(...) 转换 ndarray 的底层数据类型
我们可以使用 numpy.ndarrays.astype(...)
方法轻松地将 ndarray 的基础数据类型转换为任何其他兼容的数据类型。例如,要将arr1D
从np.float64
转换为np.int64
,我们使用以下代码:
arr1D.astype(np.int64).dtype
这反映了新的数据类型,如下所示:
dtype('int64')
当 numpy.ndarray.astype(...)
转换为较窄的数据类型时,它将截断值,如下所示:
arr1D.astype(np.int64)
这将arr1D
转换为以下整数值 ndarray:
array([1, 2, 3, 4, 5])
原始的浮点值(1.1, 2.2, …)被转换为它们截断的整数值(1, 2, …)。
ndarray 的索引
数组索引是指访问特定数组元素或元素的方式。在 NumPy 中,所有 ndarray 索引都是从零开始的,即数组的第一个项目索引为0
。负索引被理解为从数组的末尾开始计数。
直接访问 ndarray 的元素
直接访问单个 ndarray 元素是最常用的访问形式之一。
以下代码构建了一个 3 x 3 的随机值 ndarray 供我们使用:
arr = np.random.randn(3,3);
arr
arr
ndarray 具有以下元素:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 1.65004669, -0.09589629, 0.15586867],
[ 0.39533427, 1.47193681, 0.32148741]])
我们可以使用整数索引 0
索引第一个元素,如下所示:
arr[0]
这给我们了arr
ndarray 的第一行,如下所示:
array([-0.04113926, -0.273338 , -1.05294723])
我们可以通过以下代码访问第一行的第二列元素:
arr[0][1]
结果如下所示:
-0.2733379996693689
ndarray 也支持执行相同操作的替代表示法,如下所示:
arr[0, 1]
它访问了与之前相同的元素,如下所示:
-0.2733379996693689
当访问具有非常大维度的 ndarray 时,numpy.ndarray[index_0, index_1, … index_n]
表示法尤其更简洁和有用。
负索引从 ndarray 的末尾开始,如下所示:
arr[-1]
这将返回 ndarray 的最后一行,如下所示:
array([0.39533427, 1.47193681, 0.32148741])
ndarray 切片
虽然单个 ndarray 访问很有用,但是对于批量处理,我们需要一次访问数组的多个元素(例如,如果 ndarray 包含某个资产的所有每日价格,我们可能只想处理所有星期一的价格)。
切片允许一次访问多个 ndarray 记录。ndarray 的切片工作方式与 Python 列表的切片类似。
基本切片语法是 i:j:k,其中 i 是我们想要包括的第一个记录的索引,j 是停止索引,k 是步长。
访问第一个元素之后的所有 ndarray 元素
要访问第一个元素之后的所有元素,我们可以使用以下代码:
arr[1:]
这返回了第一个元素之后的所有行,如下代码片段所示:
array([[ 1.65004669, -0.09589629, 0.15586867],
[ 0.39533427, 1.47193681, 0.32148741]])
获取所有行,从第二行开始,列为第一列和第二列
类似地,要获取从第二行开始的所有行,并且列不包括第三列,运行以下代码:
arr[1:, :2]
这是一个 2 x 2 的 ndarray,正如预期的那样,可以在这里看到:
array([[ 1.65004669, -0.09589629],
[ 0.39533427, 1.47193681]])
使用负索引进行切片
更复杂的切片表示法也是可能的,包括正负索引范围的混合,如下所示:
arr[1:2, -2:-1]
这是一种不太直观的方法,用于查找位于第二行和第二列的元素的切片,如下所示:
array([[-0.09589629]])
没有索引的切片
没有索引的切片将产生整个行/列。下面的代码生成包含第三行所有元素的切片:
arr[:][2]
输出如下所示:
array([0.39533427, 1.47193681, 0.32148741])
下面的代码生成原始arr
ndarray 的切片:
arr[:][:]
输出如下所示:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 1.65004669, -0.09589629, 0.15586867],
[ 0.39533427, 1.47193681, 0.32148741]])
将切片的值设置为 0
经常,我们需要将 ndarray 的某些值设置为给定值。
让我们生成一个包含arr
的第二行并将其分配给一个新变量arr1
的切片,如下所示:
arr1 = arr[1:2];
arr1
arr1
现在包含了最后一行,如下代码片段所示:
array([[ 1.65004669, -0.09589629, 0.15586867]])
现在,让我们将arr1
的每个元素设置为值0
,如下所示:
arr1[:] = 0;
arr1
如预期的那样,arr1
现在包含了全部为 0 的值,如下所示:
array([[0., 0., 0.]])
现在,让我们重新检查我们的原始arr
ndarray,如下所示:
arr
输出如下所示:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 0. , 0. , 0. ],
[ 0.39533427, 1.47193681, 0.32148741]])
我们看到我们对arr1
切片的操作也改变了原始的arr
ndarray。这带我们来到最重要的一点:ndarray 切片是原始 ndarrays 的视图,而不是副本。
在使用 ndarrays 时记住这一点很重要,这样我们就不会无意中改变我们不想改变的东西。这个设计纯粹是为了效率原因,因为复制大型 ndarrays 会产生巨大的开销。
要创建一个 ndarray 的副本,我们需要显式调用numpy.ndarray.copy(...)
方法,如下所示:
arr_copy = arr.copy()
现在,让我们更改arr_copy
ndarray 中的一些值,如下所示:
arr_copy[1:2] = 1;
arr_copy
我们可以在下面的代码片段中看到arr_copy
的变化:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 1. , 1. , 1. ],
[ 0.39533427, 1.47193681, 0.32148741]])
让我们也来检查一下原始的arr
ndarray,如下所示:
arr
输出如下所示:
array([[-0.04113926, -0.273338 , -1.05294723],
[ 0. , 0. , 0. ],
[ 0.39533427, 1.47193681, 0.32148741]])
我们看到原始 ndarray 没有更改,因为arr_copy
是arr
的副本而不是引用/视图。
布尔索引
NumPy 提供了多种索引 ndarray 的方法。NumPy 数组可以通过使用求值为True
或False
的条件进行索引。让我们从重新生成一个arr
ndarray 开始,如下所示:
arr = np.random.randn(3,3);
arr
这是一个 3 x 3 的 ndarray,具有随机值,如下代码片段所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
让我们重新审视一下运行以下代码的输出,实际上只是调用了np.less(...)
np.less(arr, 0)
方法:
arr < 0
这生成另一个包含True
和False
值的 ndarray,其中True
表示arr
中的相应元素为负数,而False
表示arr
中的相应元素不是负数,如下代码片段所示:
array([[ True, True, False],
[False, True, False],
[False, False, True]])
我们可以使用该数组作为索引到arr
来找到实际的负元素,如下所示:
arr[(arr < 0)]
如预期的那样,这会获取以下负值:
array([-0.50566069, -0.52115534, -0.99280199, -0.02532004])
我们可以使用&
(和)和|
(或)运算符组合多个条件。Python 的&
和|
布尔运算符不适用于 ndarrays,因为它们适用于标量。这里是&
运算符的一个示例:
(arr > -1) & (arr < 1)
这将生成一个值为True
的 ndarray,其中元素在-1
和1
之间,否则为False
,如下代码片段所示:
array([[ True, True, True],
[False, True, True],
[ True, True, True]])
正如我们之前看到的,我们可以使用布尔数组索引 arr
并找到实际的元素,如下所示:
arr[((arr > -1) & (arr < 1))]
以下输出是满足条件的元素数组:
array([-0.50566069, -0.52115534, 0.0757591 , -0.99280199, 0.80878346,
0.56937775, 0.36614928, -0.02532004])
使用数组进行索引
ndarray 的索引还允许我们直接传递感兴趣的索引列表。让我们首先生成一个随机值的 ndarray,如下所示:
arr
输出如下所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
我们可以使用以下代码选择第一行和第三行:
arr[[0, 2]]
输出是一个包含两行的 2 x 3 ndarray,如下所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 0.56937775, 0.36614928, -0.02532004]])
我们可以结合使用数组进行行和列索引,如下所示:
arr[[0, 2], [1]]
上述代码给出了第一和第三行的第二列,如下所示:
array([-0.52115534, 0.36614928])
我们还可以改变传递的索引的顺序,这在输出中有所体现。下面的代码按照指定的顺序挑选出第三行和第一行:
arr[[2, 0]]
输出反映了我们期望的两行的顺序(先第三行;然后第一行),如下代码片段所示:
array([[ 0.56937775, 0.36614928, -0.02532004],
[-0.50566069, -0.52115534, 0.0757591 ]])
现在我们已经学会了如何创建 ndarrays 以及各种检索其元素值的方法,让我们讨论最常见的 ndarray 操作。
基本的 ndarray 操作
在接下来的示例中,我们将使用一个 arr2D
ndarray,如下所示:
arr2D
这是一个 1
到 4
的值的 2 x 2 ndarray,如下所示:
array([[1, 2],
[3, 4]])
与 ndarray 的标量乘法
与 ndarray 的标量乘法会使 ndarray 的每个元素相乘,如下所示:
arr2D * 4
输出如下所示:
array([[ 4, 8],
[12, 16]])
ndarray 的线性组合
以下操作是标量和 ndarray 操作以及 ndarray 之间的操作的组合:
2*arr2D + 3*arr2D
输出是我们预期的,如下所示:
array([[ 5, 10],
[15, 20]])
ndarray 的指数运算
我们可以将 ndarray 的每个元素提升到某个幂,如下所示:
arr2D ** 2
输出如下所示:
array([[ 1, 4],
[ 9, 16]])
将 ndarray 与标量相加
将 ndarray 与标量相加的结果类似,如下所示:
arr2D + 10
输出如下所示:
array([[11, 12],
[13, 14]])
转置矩阵
找到矩阵的转置,这是一个常见的操作,在 NumPy 中可以使用 numpy.ndarray.transpose(...)
方法实现,如下代码片段所示:
arr2D.transpose()
这转置了 ndarray 并输出它,如下所示:
array([[1, 3],
[2, 4]])
改变 ndarray 的布局
np.ndarray.reshape(...)
方法允许我们更改 ndarray 的布局(形状),而不改变其数据为兼容的形状。
例如,要将 arr2D
从 2 x 2 重塑为 4 x 1,我们使用以下代码:
arr2D.reshape((4, 1))
新的重塑后的 4 x 1 ndarray 如下所示:
array([[1],
[2],
[3],
[4]])
以下代码示例结合了 np.random.randn(...)
和 np.ndarray.reshape(...)
来创建一个 3 x 3 的随机值 ndarray:
arr = np.random.randn(9).reshape((3,3));
arr
生成的 3 x 3 ndarray 如下所示:
array([[ 0.24344963, -0.53183761, 1.08906941],
[-1.71144547, -0.03195253, 0.82675183],
[-2.24987291, 2.60439882, -0.09449784]])
查找 ndarray 中的最小值
要查找 ndarray 中的最小值,我们使用以下命令:
np.min(arr)
结果如下所示:
-2.249872908111852
计算绝对值
所示的np.abs(...)
方法计算 ndarray 的绝对值:
np.abs(arr)
输出 ndarray 如下所示:
array([[0.24344963, 0.53183761, 1.08906941],
[1.71144547, 0.03195253, 0.82675183],
[2.24987291, 2.60439882, 0.09449784]])
计算 ndarray 的均值
np.mean(...)
方法,如下所示,计算 ndarray 中所有元素的均值:
np.mean(arr)
这里显示了arr
元素的均值:
0.01600703714906236
我们可以通过指定axis=
参数来沿列找到均值,如下所示:
np.mean(arr, axis=0)
返回以下数组,其中包含每列的均值:
array([-1.23928958, 0.68020289, 0.6071078 ])
类似地,我们可以通过运行以下代码来沿行找到均值:
np.mean(arr, axis=1)
返回以下数组,包含每行的均值:
array([ 0.26689381, -0.30554872, 0.08667602])
查找 ndarray 中最大值的索引
通常,我们有兴趣找出数组中最大值的位置。np.argmax(...)
方法可以找到 ndarray 中最大值的位置,如下所示:
np.argmax(arr)
这返回以下值,表示最大值的位置(2.60439882
):
7
np.argmax(...)
方法还接受axis=
参数,以按行或按列执行操作,如此处所示:
np.argmax(arr, axis=1)
这将找到每行中最大值的位置,如下所示:
array([2, 2, 1], dtype=int64)
计算 ndarray 元素的累积和
要计算累积总和,NumPy 提供了np.cumsum(...)
方法。np.cumsum(...)
方法如下所示,找到 ndarray 中元素的累积总和:
np.cumsum(arr)
输出提供了每个附加元素后的累积和,如下所示:
array([ 0.24344963, -0.28838798, 0.80068144, -0.91076403, -0.94271656,
-0.11596474, -2.36583764, 0.23856117, 0.14406333])
注意累积和和求和之间的差异。累积和是一个累加的数组,而求和是一个单个数字。
将axis=
参数应用于cumsum
方法的效果类似,如以下代码片段所示:
np.cumsum(arr, axis=1)
这将按行进行,并生成以下数组输出:
array([[ 0.24344963, -0.28838798, 0.80068144],
[-1.71144547, -1.743398 , -0.91664617],
[-2.24987291, 0.35452591, 0.26002807]])
查找 ndarray 中的 NaN 值
在 NumPy 中,缺失或未知值通常使用Not a Number (NaN)值表示。对于许多数值方法,必须将这些值删除或替换为插值。
首先,让我们将第二行设置为np.nan
,如下所示:
arr[1, :] = np.nan;
arr
新的 ndarray 具有 NaN 值,如以下代码片段所示:
array([[ 0.64296696, -1.35386668, -0.63063743],
[ nan, nan, nan],
[-0.19093967, -0.93260398, -1.58520989]])
np.isnan(...)
ufunc 找到 ndarray 中的值是否为 NaN,如下所示:
np.isnan(arr)
输出是一个 ndarray,其中存在 NaN 的地方为True
值,不存在 NaN 的地方为False
值,如下所示的代码片段所示:
array([[False, False, False],
[ True, True, True],
[False, False, False]])
查找两个 ndarray 的 x1>x2 的真值
布尔 ndarray 是获取感兴趣的值的索引的有效方式。使用布尔 ndarray 比逐个遍历矩阵元素要高效得多。
让我们按照以下方式构建另一个具有随机值的arr1
ndarray:
arr1 = np.random.randn(9).reshape((3,3));
arr1
结果是一个 3 x 3 的 ndarray,如下所示的代码片段中所示:
array([[ 0.32102068, -0.51877544, -1.28267292],
[-1.34842617, 0.61170993, -0.5561239 ],
[ 1.41138027, -2.4951374 , 1.30766648]])
类似地,让我们构建另一个arr2
ndarray,如下所示:
arr2 = np.random.randn(9).reshape((3,3));
arr2
输出如下所示:
array([[ 0.33189432, 0.82416396, -0.17453351],
[-1.59689203, -0.42352094, 0.22643589],
[-1.80766151, 0.26201455, -0.08469759]])
np.greater(...)
函数是一个二进制 ufunc,当 ndarray 中的左值大于 ndarray 中的右值时生成True
值。该函数如下所示:
np.greater(arr1, arr2)
输出是如前所述的True
和False
值的 ndarray,如我们在这里所见:
array([[False, False, False],
[ True, True, False],
[ True, False, True]])
>
中缀操作符,如下段代码片段所示,是numpy.greater(...)
的简写:
arr1 > arr2
输出相同,如我们在这里所见:
array([[False, False, False],
[ True, True, False],
[ True, False, True]])
对 ndarray 进行任何和所有的布尔运算
除了关系运算符外,NumPy 还支持其他方法来测试矩阵值上的条件。
以下代码生成一个 ndarray,对满足条件的元素返回True
,否则返回False
:
arr_bool = (arr > -0.5) & (arr < 0.5);
arr_bool
输出如下所示:
array([[False, False, True],
[False, False, False],
[False, True, True]])
以下numpy.ndarray.any(...)
方法在任何元素为True
时返回True
,否则返回False
:
arr_bool.any()
在这里,我们至少有一个元素为True
,因此输出为True
,如下所示:
True
再次,它接受常见的axis=
参数并且表现如预期,如我们在这里所见:
arr_bool.any(axis=1)
并且按行执行的操作生成如下所示:
array([True, False, True])
以下numpy.ndarray.all(...)
方法在所有元素都为True
时返回True
,否则返回False
:
arr_bool.all()
这返回了以下内容,因为并非所有元素都为True
:
False
它还接受axis=
参数,如下所示:
arr_bool.all(axis=1)
再次,每行至少有一个False
值,因此输出为False
,如下所示:
array([False, False, False])
对 ndarray 进行排序
在排序的 ndarray 中查找元素比处理 ndarray 的所有元素更快。
让我们生成一个 1D 随机数组,如下所示:
arr1D = np.random.randn(10);
arr1D
ndarray 包含以下数据:
array([ 1.14322028, 1.61792721, -1.01446969, 1.26988026, -0.20110113,
-0.28283051, 0.73009565, -0.68766388, 0.27276319, -0.7135162 ])
np.sort(...)
方法非常简单,如下所示:
np.sort(arr1D)
输出如下所示:
array([-1.01446969, -0.7135162 , -0.68766388, -0.28283051, -0.20110113,
0.27276319, 0.73009565, 1.14322028, 1.26988026, 1.61792721])
让我们检查原始 ndarray,看看它是否被numpy.sort(...)
操作修改了,如下所示:
arr1D
以下输出显示原始数组未改变:
array([ 1.14322028, 1.61792721, -1.01446969, 1.26988026, -0.20110113,
-0.28283051, 0.73009565, -0.68766388, 0.27276319, -0.7135162 ])
以下np.argsort(...)
方法创建一个表示每个元素在排序数组中位置的索引数组:
np.argsort(arr1D)
此操作的输出生成以下数组:
array([2, 9, 7, 5, 4, 8, 6, 0, 3, 1])
NumPy ndarray 还具有numpy.ndarray.sort(...)
方法,该方法可以就地对数组进行排序。该方法在下面的代码片段中说明:
arr1D.sort()
np.argsort(arr1D)
调用sort()
后,我们调用numpy.argsort(...)
来确保数组已排序,这将生成以下数组,确认了该行为:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
在 ndarray 中搜索
在 ndarray 上满足某个条件的元素的索引是一种基本操作。
首先,我们从一个具有连续值的 ndarray 开始,如下所示:
arr1 = np.array(range(1, 11));
arr1
这将创建以下 ndarray:
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
我们根据第一个 ndarray 创建了第二个 ndarray,不过这次第二个 ndarray 中的值乘以了1000
,如下面的代码片段所示:
arr2 = arr1 * 1000;
arr2
然后,我们知道arr2
包含以下数据:
array([ 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000,
10000])
我们定义另一个 ndarray,其中包含 10 个随机的True
和False
值,如下所示:
cond = np.random.randn(10) > 0;
cond
cond
ndarray 中的值显示如下:
array([False, False, True, False, False, True, True, True, False, True])
np.where(...)
方法允许我们根据条件是 True
还是 False
从一个 ndarray 或另一个中选择值。以下代码将生成一个 ndarray,当 cond
数组中对应的元素为 True
时,从 arr1
中选择值;否则,从 arr2
中选择值:
np.where(cond, arr1, arr2)
返回的数组如下所示:
array([1000, 2000, 3, 4000, 5000, 6, 7, 8, 9000, 10])
ndarray 的文件操作
大多数 NumPy 数组都是从文件中读取的,在处理后,再写回文件。
文本文件的文件操作
文本文件的主要优点是它们可读性强,并且与任何自定义软件兼容。
让我们从以下随机数组开始:
arr
此数组包含以下数据
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
numpy.savetxt(...)
方法以文本格式将 ndarray 保存到磁盘。
以下示例使用了 fmt='%0.2lf'
格式字符串并指定了逗号分隔符:
np.savetxt('arr.csv', arr, fmt='%0.2lf', delimiter=',')
让我们检查当前目录中写入磁盘的 arr.csv
文件,如下所示:
!cat arr.csv
逗号分隔值 (CSV) 文件包含以下数据:
-0.51,-0.52,0.08
1.68,-0.99,0.81
0.57,0.37,-0.03
numpy.loadtxt(...)
方法从文本文件加载 ndarray 到内存中。在这里,我们显式指定了 delimiter=','
参数,如下所示:
arr_new = np.loadtxt('arr.csv', delimiter=',');
arr_new
从文本文件中读入的 ndarray 包含以下数据:
array([[-0.51, -0.52, 0.08],
[ 1.68, -0.99, 0.81],
[ 0.57, 0.37, -0.03]])
二进制文件的文件操作
二进制文件对于计算机处理来说效率更高——它们保存和加载更快,比文本文件更小。但是,它们的格式可能不受其他软件支持。
numpy.save(...)
方法将 ndarray 存储为二进制格式,如下代码片段所示:
np.save('arr', arr)
!cat arr.npy
arr.npy
文件的输出如下:
numpy.save(...)
方法会自动为其创建的二进制文件分配 .npy
扩展名。
numpy.load(...)
方法,如下代码片段所示,用于读取二进制文件:
arr_new = np.load('arr.npy');
arr_new
新读入的 ndarray 如下所示:
array([[-0.50566069, -0.52115534, 0.0757591 ],
[ 1.67500165, -0.99280199, 0.80878346],
[ 0.56937775, 0.36614928, -0.02532004]])
二进制文件格式的另一个优点是,数据可以以极高的精度存储,特别是在处理浮点值时,这在某些情况下在文本文件中并不总是可能的,因为在某些情况下会有一些精度损失。
让我们通过运行以下代码检查旧的 arr
ndarray 和新读入的 arr_new
数组是否完全匹配:
arr == arr_new
这将生成以下数组,如果元素相等则包含 True
,否则包含 False
:
array([[ True, True, True],
[ True, True, True],
[ True, True, True]])
因此,我们看到每个元素都完全匹配。
概要
在本章中,我们学习了如何在 Python 中创建任意维度的矩阵,如何访问矩阵的元素,如何对矩阵进行基本的线性代数运算,以及如何保存和加载矩阵。
使用 NumPy 矩阵是任何数据分析的主要操作,因为向量运算经过机器优化,因此比 Python 列表上的操作要快得多——通常快 5 到 100 倍。回测任何算法策略通常包括处理庞大的矩阵,而速度差异可以转化为节省的小时或天数时间。
在下一章中,我们将介绍第二重要的用于数据分析的库:Pandas,它是建立在 NumPy 基础上的。NumPy 提供了对基于数据框架的数据操作的支持(数据框架是 Excel 工作表的 Python 版本——即,一个二维数据结构,其中每列都有自己的类型)。
第四章:使用 pandas 进行数据操作和分析
在本章中,您将学习基于 NumPy 构建的 Python pandas
库,该库为结构化数据框提供了数据操作和分析方法。根据维基百科对 pandas 的页面,pandas 这个名字是从 panel data 派生而来,它是一个描述多维结构化数据集的计量经济学术语。
pandas
库包含两种基本数据结构来表示和操作带有各种索引选项的结构化矩形数据集:Series 和 DataFrames。两者都使用索引数据结构。
Python 中处理金融数据的大多数操作都是基于 DataFrames 的。DataFrame 就像一个 Excel 工作表 - 一个可能包含多个时间序列的二维表格,存储在列中。因此,我们建议您在您的环境中执行本章中的所有示例,以熟悉语法并更好地了解可能的操作。
在本章中,我们将涵盖以下主题:
-
介绍 pandas Series、pandas DataFrames 和 pandas Indexes
-
学习 pandas DataFrames 上的基本操作
-
使用 pandas DataFrames 探索文件操作
技术要求
本章中使用的 Python 代码在书籍代码存储库中的Chapter04/pandas.ipynb
笔记本中可用。
介绍 pandas Series、pandas DataFrames 和 pandas Indexes
pandas Series、pandas DataFrames 和 pandas Indexes 是 pandas 的基本数据结构。
pandas.Series
pandas.Series
数据结构表示同质值(整数值、字符串值、双精度值等)的一维系列。 Series 是一种列表类型,只能包含带索引的单个列表。另一方面,Data Frame 是一个包含一个或多个 series 的集合。
让我们创建一个pandas.Series
数据结构:
import pandas as pd
ser1 = pd.Series(range(1, 6));
ser1
该系列包含在第一列中的索引,第二列中的索引对应的值:
0 1
1 2
2 3
3 4
4 5
dtype: int64
我们可以通过指定index
参数来指定自定义索引名称:
ser2 = pd.Series(range(1, 6),
index=['a', 'b', 'c', 'd', 'e']);
ser2
输出将如下所示:
a 1
b 2
c 3
d 4
e 5
dtype: int64
我们还可以通过字典指定index -> value
映射来创建一个系列:
ser3 = pd.Series({ 'a': 1.0, 'b': 2.0, 'c': 3.0,
'd': 4.0, 'e': 5.0 });
ser3
输出如下所示:
a 1.0
b 2.0
c 3.0
d 4.0
e 5.0
dtype: float64
pandas.Series.index
属性允许我们访问索引:
ser3.index
索引的类型是pandas.Index
:
Index(['a', 'b', 'c', 'd', 'e'], dtype='object')
可以使用pandas.Series.values
属性访问系列的值:
ser3.values
值如下:
array([ 1., 2., 3., 4., 5.])
我们可以通过修改pandas.Series.name
属性为系列指定一个名称:
ser3.name = 'Alphanumeric'; ser3
输出如下所示:
a 1.0
b 2.0
c 3.0
d 4.0
e 5.0
Name: Alphanumeric, dtype: float64
上述示例演示了构建 pandas Series 的多种方式。让我们了解一下 DataFrame,这是一种可能包含多个 Series 的数据结构。
pandas.DataFrame
pandas.DataFrame
数据结构是多个可能不同类型的pandas.Series
对象的集合,由相同的公共 Index 对象索引。
所有统计时间序列操作的大部分都是在数据框上执行的,pandas.DataFrame
针对数据框的并行超快处理进行了优化,比在单独系列上进行处理快得多。
我们可以从字典创建一个数据框,其中键是列名,该键的值包含相应系列/列的数据:
df1 = pd.DataFrame({'A': range(1,5,1),
'B': range(10,50,10),
'C': range(100, 500, 100)});
df1
输出如下所示:
A B C
0 1 10 100
1 2 20 200
2 3 30 300
3 4 40 400
我们也可以在这里传递index=
参数来标记索引:
df2 = pd.DataFrame({'A': range(1,5,1),
'B': range(10,50,10),
'C': range(100, 500, 100)},
index=['a', 'b', 'c', 'd']);
df2
这构建了以下数据框:
A B C
a 1 10 100
b 2 20 200
c 3 30 300
d 4 40 400
pandas.DataFrame.columns
属性返回不同列的名称:
df2.columns
结果是一个Index
对象:
Index(['A', 'B', 'C'], dtype='object')
索引可以从pandas.DataFrame.index
属性中访问:
df2.index
这给了我们这个:
Index(['a', 'b', 'c', 'd'], dtype='object')
数据框还包含pandas.DataFrame.values
属性,该属性返回列中包含的值:
df2.values
结果是以下 2D 数组:
array([[ 1, 10, 100],
[ 2, 20, 200],
[ 3, 30, 300],
[ 4, 40, 400]])
我们可以通过以下方式向数据框添加具有指定值和相同索引的新列:
df2['D'] = range(1000,5000,1000);
df2
更新后的数据框如下:
A B C D
a 1 10 100 1000
b 2 20 200 2000
c 3 30 300 3000
d 4 40 400 4000
我们可以为数据框的索引和列指定名称。
我们可以通过修改pandas.DataFrame.index.name
属性来命名索引:
df2.index.name = 'lowercase'; df2
这导致以下更新后的数据框:
A B C D
lowercase
a 1 10 100 1000
b 2 20 200 2000
c 3 30 300 3000
d 4 40 400 4000
可以使用pandas.DataFrame.columns.name
属性重命名列:
df2.columns.name = 'uppercase'; df2
新数据框如下所示:
uppercase A B C D
lowercase
a 1 10 100 1000
b 2 20 200 2000
c 3 30 300 3000
d 4 40 400 4000
前面的例子演示了如何构造数据框。
pandas.Index
pandas.Series
和pandas.DataFrame
数据结构都利用pandas.Index
数据结构。
有许多特殊类型的Index
对象:
-
Int64Index
:Int64Index
包含整数索引值。 -
MultiIndex
:MultiIndex
包含用于分层索引的元组索引,我们将在本章中探讨。 -
DatetimeIndex
:DatetimeIndex
,我们之前已经见过,包含时间序列数据集的日期时间索引值。
我们可以通过以下方式创建一个pandas.Index
对象:
ind2 = pd.Index(list(range(5))); ind2
结果是这样的:
Int64Index([0, 1, 2, 3, 4], dtype='int64')
注意
Index
对象是不可变的,因此无法就地修改。
让我们看看如果我们尝试修改Index
对象中的元素会发生什么:
ind2[0] = -1
我们得到以下输出:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-34-20c233f961b2> in <module>()
----> 1 ind2[0] = -1
...
TypeError: Index does not support mutable operations
Python 警告我们无法手动修改索引对象。
我们现在已经学会了如何构建系列和数据框。让我们探索对数据框进行的基本操作。
学习重要的 pandas.DataFrame 操作
本节描述了对数据框进行的基本操作。了解它们的存在以及如何使用它们将为您节省大量时间。
数据框的索引、选择和过滤
pandas 数据结构通过特殊的Index
对象进行索引(而numpy.ndarrays
和 Python 列表对象仅可通过整数索引)。本课程的步骤如下:
-
让我们检查在本章前面创建的
df2
数据框的内容:df2
输出如下所示:
uppercase A B C D lowercase a 1 10 100 1000 b 2 20 200 2000 c 3 30 300 3000 d 4 40 400 4000
-
我们可以通过执行以下操作选择列
B
中的值序列:df2['B']
这产生了以下序列:
lowercase a 10 b 20 c 30 d 40 Name: B, dtype: int64
-
我们可以通过传递列名列表来选择多个列(与我们在
numpy.ndarrays
中看到的有些相似):df2[['A', 'C']]
这产生了以下具有两列的 DataFrame:
uppercase A C lowercase a 1 100 b 2 200 c 3 300 d 4 400
-
我们可以通过以下方式使用 DataFrame 进行布尔选择:
df2[(df2['D'] > 1000) & (df2['D'] <= 3000)]
这选择了满足提供条件的以下行:
uppercase A B C D lowercase b 2 20 200 2000 c 3 30 300 3000
-
pandas.DataFrame.loc[...]
属性允许我们索引行而不是列。以下选择了两行c
和d
:df2.loc[['c', 'd']]
这产生了以下子集 DataFrame:
uppercase A B C D lowercase c 3 30 300 3000 d 4 40 400 4000
-
pandas DataFrame 仍然支持通过
pandas.DataFrame.iloc[...]
属性进行标准整数索引。我们可以通过这样做来选择第一行:df2.iloc[[0]]
这选择了以下单行 DataFrame:
uppercase A B C D lowercase a 1 10 100 1000
我们可以通过类似这样的操作修改 DataFrame:
df2[df2['D'] == 2000] = 0; df2
这将 DataFrame 更新为这个新 DataFrame:
uppercase A B C D lowercase a 1 10 100 1000 b 0 0 0 0 c 3 30 300 3000 d 4 40 400 4000
在本节中,我们学习了如何索引、选择和过滤 DataFrame。在下一节中,我们将学习如何删除行和列。
从 DataFrame 中删除行和列
从 DataFrame 中删除行和列是一个关键操作——它不仅有助于节省计算机的内存,还确保 DataFrame 只包含逻辑上需要的信息。步骤如下:
-
让我们显示当前 DataFrame:
df2
此 DataFrame 包含以下内容:
uppercase A B C D lowercase a 1 10 100 1000 b 0 0 0 0 c 3 30 300 3000 d 4 40 400 4000
-
要删除索引为
b
的行,我们使用pandas.DataFrame.drop(...)
方法:df2.drop('b')
这产生了一个没有索引为
b
的行的新 DataFrame:uppercase A B C D lowercase a 1 10 100 1000 c 3 30 300 3000 d 4 40 400 4000
让我们检查原始 DataFrame 是否已更改:
df2
输出显示没有,也就是说,默认情况下
pandas.DataFrame.drop(...)
不是原位的:uppercase A B C D lowercase a 1 10 100 1000 b 0 0 0 0 c 3 30 300 3000 d 4 40 400 4000
-
要修改原始 DataFrame,我们使用
inplace=
参数:df2.drop('b', inplace=True); df2
新的原地修改的 DataFrame 如下所示:
uppercase A B C D lowercase a 1 10 100 1000 c 3 30 300 3000 d 4 40 400 4000
-
我们也可以删除多个行:
df2.drop(['a', 'd'])
这返回了以下新 DataFrame:
uppercase A B C D lowercase c 3 30 300 3000
-
要删除列而不是行,我们指定额外的
axis=
参数:df2.drop(['A', 'B'], axis=1)
这给了我们具有两个删除列的新 DataFrame:
uppercase C D lowercase a 100 1000 c 300 3000 d 400 4000
我们在本节中学习了如何删除行和列。在下一节中,我们将学习如何对值进行排序和 rand。
对 DataFrame 进行排序值和排列值顺序
首先,让我们创建一个具有整数行索引、整数列名和随机值的 DataFrame:
import numpy as np
df = pd.DataFrame(np.random.randn(5,5),
index=np.random.randint(0, 100, size=5),
columns=np.random.randint(0,100,size=5));
df
DataFrame 包含以下数据:
87 79 74 3 61
7 0.355482 -0.246812 -1.147618 -0.293973 -0.560168
52 1.748274 0.304760 -1.346894 -0.548461 0.457927
80 -0.043787 -0.680384 1.918261 1.080733 1.346146
29 0.237049 0.020492 1.212589 -0.462218 1.284134
0 -0.153209 0.995779 0.100585 -0.350576 0.776116
pandas.DataFrame.sort_index(...)
按索引值对 DataFrame 进行排序:
df.sort_index()
结果如下:
87 79 74 3 61
0 -0.153209 0.995779 0.100585 -0.350576 0.776116
7 0.355482 -0.246812 -1.147618 -0.293973 -0.560168
29 0.237049 0.020492 1.212589 -0.462218 1.284134
52 1.748274 0.304760 -1.346894 -0.548461 0.457927
80 -0.043787 -0.680384 1.918261 1.080733 1.346146
我们也可以通过指定 axis
参数按列名值进行排序:
df.sort_index(axis=1)
这产生了以下按顺序排列的 DataFrame:
3 61 74 79 87
7 -0.293973 -0.560168 -1.147618 -0.246812 0.355482
52 -0.548461 0.457927 -1.346894 0.304760 1.748274
80 1.080733 1.346146 1.918261 -0.680384 -0.043787
29 -0.462218 1.284134 1.212589 0.020492 0.237049
0 -0.350576 0.776116 0.100585 0.995779 -0.153209
要对 DataFrame 中的值进行排序,我们使用 pandas.DataFrame.sort_values(...)
方法,该方法采用 by=
参数指定要按其排序的列:
df.sort_values(by=df.columns[0])
这产生了以下按第一列值排序的 DataFrame:
87 79 74 3 61
0 -0.153209 0.995779 0.100585 -0.350576 0.776116
80 -0.043787 -0.680384 1.918261 1.080733 1.346146
29 0.237049 0.020492 1.212589 -0.462218 1.284134
7 0.355482 -0.246812 -1.147618 -0.293973 -0.560168
52 1.748274 0.304760 -1.346894 -0.548461 0.457927
pandas.DataFrame.rank(...)
方法产生一个包含每列值的排名/顺序的 DataFrame:
df.rank()
输出包含值的排名(按升序):
87 79 74 3 61
7 4.0 2.0 2.0 4.0 1.0
52 5.0 4.0 1.0 1.0 2.0
80 2.0 1.0 5.0 5.0 5.0
29 3.0 3.0 4.0 2.0 4.0
0 1.0 5.0 3.0 3.0 3.0
本课程完成后,在下一节中,我们将对 DataFrame 执行算术运算。
DataFrame 上的算术操作
首先,让我们为我们的示例创建两个 DataFrames:
df1 = pd.DataFrame(np.random.randn(3,2),
index=['A', 'C', 'E'],
columns=['colA', 'colB']);
df1
df1
DataFrame 包含以下内容:
colA colB
A 0.519105 -0.127284
C -0.840984 -0.495306
E -0.137020 0.987424
现在我们创建df2
DataFrame:
df2 = pd.DataFrame(np.random.randn(4,3),
index=['A', 'B', 'C', 'D'],
columns=['colA', 'colB', 'colC']);
df2
这包含以下内容:
colA colB colC
A -0.718550 1.938035 0.220391
B -0.475095 0.238654 0.405642
C 0.299659 0.691165 -1.905837
D 0.282044 -2.287640 -0.551474
我们可以将两个 DataFrame 相加。请注意它们具有不同的索引值以及不同的列:
df1 + df2
输出是元素的总和,如果索引和列存在于两个 DataFrame 中,则为 NaN:
colA colB colC
A -0.199445 1.810751 NaN
B NaN NaN NaN
C -0.541325 0.195859 NaN
D NaN NaN NaN
E NaN NaN NaN
我们可以使用pandas.DataFrame.add(...)
方法并带有fill_value=
参数指定一个值来替代NaN
(在这种情况下是0
):
df1.add(df2, fill_value=0)
输出如下所示:
colA colB colC
A -0.199445 1.810751 0.220391
B -0.475095 0.238654 0.405642
C -0.541325 0.195859 -1.905837
D 0.282044 -2.287640 -0.551474
E -0.137020 0.987424 NaN
我们还可以在 DataFrame 和 Series 之间执行算术操作:
df1 - df2[['colB']]
这个操作的输出如下(因为右侧只有colB
):
colA colB
A NaN -2.065319
B NaN NaN
C NaN -1.186471
D NaN NaN
E NaN NaN
现在让我们学习如何将多个 DataFrame 合并和组合成一个单独的 DataFrame。
将多个 DataFrame 合并和组合成一个 DataFrame
让我们首先创建两个 DataFrame,df1
和 df2
:
df1.index.name = 'Index'; df1.columns.name = 'Columns'; df1
df1
DataFrame 包含以下数据:
Columns colA colB
Index
A 0.519105 -0.127284
C -0.840984 -0.495306
E -0.137020 0.987424
现在我们创建df2
:
df2.index.name = 'Index'; df2.columns.name = 'Columns'; df2
df2
DataFrame 包含以下数据:
Columns colA colB colC
Index
A -0.718550 1.938035 0.220391
B -0.475095 0.238654 0.405642
C 0.299659 0.691165 -1.905837
D 0.282044 -2.287640 -0.551474
pandas.merge(...)
方法连接/合并两个 DataFrames。left_index=
和right_index=
参数指示合并应该在两个 DataFrames 的索引值上执行:
pd.merge(df1, df2, left_index=True, right_index=True)
这产生了以下合并后的 DataFrame。_x
和 _y
后缀用于区分左右两个 DataFrame 中具有相同名称的列:
Columns colA_x colB_x colA_y colB_y colC
Index
A 0.519105 -0.127284 -0.718550 1.938035 0.220391
C -0.840984 -0.495306 0.299659 0.691165 -1.905837
我们可以使用suffixes=
参数指定自定义后缀:
pd.merge(df1, df2, left_index=True, right_index=True,
suffixes=('_1', '_2'))
结果是带有我们提供的后缀的以下 DataFrame:
Columns colA_1 colB_1 colA_2 colB_2 colC
Index
A 0.519105 -0.127284 -0.718550 1.938035 0.220391
C -0.840984 -0.495306 0.299659 0.691165 -1.905837
我们可以使用how=
参数指定连接的行为(外部、内部、左连接或右连接):
pd.merge(df1, df2, left_index=True, right_index=True,
suffixes=('_1', '_2'), how='outer')
这会产生以下带有NaNs
的 DataFrame,用于缺失值:
Columns colA_1 colB_1 colA_2 colB_2 colC
Index
A 0.519105 -0.127284 -0.718550 1.938035 0.220391
B NaN NaN -0.475095 0.238654 0.405642
C -0.840984 -0.495306 0.299659 0.691165 -1.905837
D NaN NaN 0.282044 -2.287640 -0.551474
E -0.137020 0.987424 NaN NaN NaN
pandas DataFrame 本身具有pandas.DataFrame.merge(...)
方法,其行为方式相同:
df1.merge(df2, left_index=True, right_index=True,
suffixes=('_1', '_2'), how='outer')
这会产生以下结果:
Columns colA_1 colB_1 colA_2 colB_2 colC
Index
A 0.519105 -0.127284 -0.718550 1.938035 0.220391
B NaN NaN -0.475095 0.238654 0.405642
C -0.840984 -0.495306 0.299659 0.691165 -1.905837
D NaN NaN 0.282044 -2.287640 -0.551474
E -0.137020 0.987424 NaN NaN NaN
另一种选择是pandas.DataFrame.join(...)
方法:
df1.join(df2, lsuffix='_1', rsuffix='_2')
并且连接的输出(默认为左连接)如下所示:
Columns colA_1 colB_1 colA_2 colB_2 colC
Index
A 0.519105 -0.127284 -0.718550 1.938035 0.220391
C -0.840984 -0.495306 0.299659 0.691165 -1.905837
E -0.137020 0.987424 NaN NaN NaN
pandas.concat(...)
方法通过将行连接在一起来组合 DataFrame:
pd.concat([df1, df2])
这会产生以下带有NaNs
的连接 DataFrame:
colA colB colC
Index
A 0.519105 -0.127284 NaN
C -0.840984 -0.495306 NaN
E -0.137020 0.987424 NaN
A -0.718550 1.938035 0.220391
B -0.475095 0.238654 0.405642
C 0.299659 0.691165 -1.905837
D 0.282044 -2.287640 -0.551474
我们可以通过指定axis=
参数在列之间进行连接:
pd.concat([df1, df2], axis=1)
这会产生以下带有来自df2
的额外列的 DataFrame:
Columns colA colB colA colB colC
A 0.519105 -0.127284 -0.718550 1.938035 0.220391
B NaN NaN -0.475095 0.238654 0.405642
C -0.840984 -0.495306 0.299659 0.691165 -1.905837
D NaN NaN 0.282044 -2.287640 -0.551474
E -0.137020 0.987424 NaN NaN NaN
现在我们将学习分层索引。
分层索引
到目前为止,我们一直在处理的索引对象都是一个简单的单个值。分层索引使用MultiIndex
对象,它是每个索引的多个值的元组。这使我们能够在单个 DataFrame 内创建子 DataFrame。
让我们创建一个MultiIndex
DataFrame:
df = pd.DataFrame(np.random.randn(10, 2),
index=[list('aaabbbccdd'),
[1, 2, 3, 1, 2, 3, 1, 2, 1, 2]],
columns=['A', 'B']);
df
这是使用分层索引的MultiIndex
DataFrame 的布局:
A B
a 1 0.289379 -0.157919
2 -0.409463 -1.103412
3 0.812444 -1.950786
b 1 -1.549981 0.947575
2 0.344725 -0.709320
3 1.384979 -0.716733
c 1 -0.319983 0.887631
2 -1.763973 1.601361
d 1 0.171177 -1.285323
2 -0.143279 0.020981
我们可以使用pandas.MultiIndex.names
属性为MultiIndex
对象分配名称 - 它需要一个名称列表,其维度与MultiIndex
DataFrame 的维度相同(在本例中为两个元素):
df.index.names = ['alpha', 'numeric']; df
这会得到以下结果:
A B
alpha numeric
a 1 0.289379 -0.157919
2 -0.409463 -1.103412
3 0.812444 -1.950786
...
pandas.DataFrame.reset_index(...)
方法默认情况下从MultiIndex
DataFrame 中移除所有索引级别,但可以用于移除一个或多个级别:
df.reset_index()
这导致以下整数索引 DataFrame 以及MultiIndex
值被添加为此 DataFrame 的列:
alpha numeric A B
0 a 1 0.289379 -0.157919
1 a 2 -0.409463 -1.103412
2 a 3 0.812444 -1.950786
...
pandas.DataFrame.unstack(...)
方法的行为类似,并将内部索引的级别旋转并将其转换为列:
df.unstack()
让我们检查新的 DataFrame,其中最内层的索引级别[1, 2, 3]
变为列:
A B
numeric 1 2 3 1 2 3
alpha
a 0.289379 -0.409463 0.812444 -0.157919 -1.103412 -1.950786
b -1.549981 0.344725 1.384979 0.947575 -0.709320 -0.716733
c -0.319983 -1.763973 NaN 0.887631 1.601361 NaN
d 0.171177 -0.143279 NaN -1.285323 0.020981 NaN
pandas.DataFrame.stack(...)
方法的作用与unstack(...)
相反:
df.stack()
输出 DataFrame 是具有分层索引的原始 DataFrame:
alpha numeric
a 1 A 0.289379
B -0.157919
2 A -0.409463
B -1.103412
3 A 0.812444
B -1.950786
...
dtype: float64
让我们检查MultiIndex
DataFrame 的结构。请注意,我们首先调用pandas.DataFrame.stack(...)
将列[A, B]
转换为MultiIndex
DataFrame 中的第三个索引级别:
df.stack().index
这给我们一个具有三个索引级别的MultiIndex
对象:
MultiIndex(levels=[['a', 'b', 'c', 'd'],
[1, 2, 3], ['A', 'B']],
labels=[[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], [0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 0, 0, 1, 1], [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]],
names=['alpha', 'numeric', None])
现在我们将学习如何在 DataFrames 中进行分组操作。
DataFrames 中的分组操作
pandas 中的分组操作通常遵循操作的分割-应用-组合过程:
-
首先,数据根据一个或多个键分成组。
-
然后,我们对这些组应用必要的函数来计算所需的结果。
-
最后,我们将它们组合起来构建转换后的数据集。
因此,对单索引 DataFrame 进行分组会构建一个分层 DataFrame。步骤如下:
-
让我们使用
pandas.DataFrame.reset_index(…)
方法从先前的df
DataFrame 中移除所有分层索引:df = df.reset_index(); df
这返回了以下带有整数索引的 DataFrame:
alpha numeric A B 0 a 1 -0.807285 0.170242 1 a 2 0.704596 1.568901 2 a 3 -1.417366 0.573896 3 b 1 1.110121 0.366712 ...
-
让我们使用
pandas.DataFrame.groupby(...)
方法来按alpha
列对A
和B
列进行分组:grouped = df[['A','B']].groupby(df['alpha']); grouped
这产生了以下的
DataFrameGroupBy
对象,随后我们可以对其进行操作:<pandas.core.groupby.DataFrameGroupBy object at 0x7fd21f24cc18>
-
我们可以使用
DataFrameGroupBy.describe(...)
方法来收集摘要描述性统计信息:grouped.describe()
这产生了以下输出,其中生成了
A
和B
的统计信息,但是按alpha
列分组:A B alpha a count 3.000000 3.000000 mean -0.506685 0.771013 std 1.092452 0.719863 min -1.417366 0.170242 25% -1.112325 0.372069 50% -0.807285 0.573896 75% -0.051344 1.071398 max 0.704596 1.568901 ...
-
我们可以使用
DataFrameGroupBy.apply(...)
方法应用pandas.DataFrame.unstack(...)
方法,该方法接受不同的函数并将它们应用于grouped
对象的每个组:grouped.apply(pd.DataFrame.unstack)
这产生了以下分层 DataFrame:
alpha a A 0 -0.807285 1 0.704596 2 -1.417366 B 0 0.170242 1 1.568901 2 0.573896 ... dtype: float64
-
还存在
DataFrameGroupBy.agg(...)
方法,它接受函数并使用该方法为每个组的每个列聚合该方法。下一个示例使用mean
方法进行聚合:grouped[['A', 'B']].agg('mean')
输出包含了按
alpha
值分组的A
和B
列的均值:A B alpha a -0.506685 0.771013 b 0.670435 0.868550 c 0.455688 -0.497468 d -0.786246 0.107246
-
类似的方法是
DataFrameGroupBy.transform(...)
方法,唯一的区别在于 transform 一次只对一列起作用,并返回与系列长度相同的值序列,而 apply 可以返回任何类型的结果:from scipy import stats grouped[['A', 'B']].transform(stats.zscore)
这会为列
A
和B
生成 Z 得分,我们在第二章中解释了这个探索性数据分析:A B 0 -0.337002 -1.022126 1 1.357964 1.357493 2 -1.020962 -0.335367 3 0.610613 -0.567813 4 -1.410007 1.405598 5 0.799394 -0.837785 6 -1.000000 1.000000 7 1.000000 -1.000000 8 -1.000000 -1.000000 9 1.000000 1.000000
我们现在将学习如何转换 DataFrame 轴索引中的值。
转换 DataFrame 轴索引中的值
让我们首先重新检查我们将在这些示例中使用的 df2
DataFrame:
df2
这包含以下数据:
Columns colA colB colC
Index
A -2.071652 0.742857 0.632307
B 0.113046 -0.384360 0.414585
C 0.690674 1.511816 2.220732
D 0.184174 -1.069291 -0.994885
我们可以使用 pandas.DataFrame.index
属性重命名索引标签,就像我们之前看到的那样:
df2.index = ['Alpha', 'Beta', 'Gamma', 'Delta'];
df2
这会生成以下转换后的 DataFrame:
Columns colA colB colC
Alpha -2.071652 0.742857 0.632307
Beta 0.113046 -0.384360 0.414585
Gamma 0.690674 1.511816 2.220732
Delta 0.184174 -1.069291 -0.994885
pandas.Index.map(...)
方法应用于转换索引的函数。
在以下示例中,map
函数取名称的前三个字符并将其设置为新名称:
df2.index = df2.index.map(lambda x : x[:3]); df2
输出如下:
Columns colA colB colC
Alp -2.071652 0.742857 0.632307
Bet 0.113046 -0.384360 0.414585
Gam 0.690674 1.511816 2.220732
Del 0.184174 -1.069291 -0.994885
pandas.DataFrame.rename(...)
方法允许我们转换索引名称和列名称,并接受从旧名称到新名称的字典映射:
df2.rename(index={'Alp': 0, 'Bet': 1, 'Gam': 2, 'Del': 3},
columns={'colA': 'A', 'colB': 'B', 'colC': 'C'})
结果 DataFrame 在两个轴上都有新标签:
Columns A B C
0 -2.071652 0.742857 0.632307
1 0.113046 -0.384360 0.414585
2 0.690674 1.511816 2.220732
3 0.184174 -1.069291 -0.994885
通过学习这个课程,我们将学习如何处理 DataFrame 中的缺失数据。
处理 DataFrame 中的缺失数据
缺失数据是数据科学中常见的现象,可能由多种原因导致 - 例如,技术错误,人为错误,市场假期。
过滤掉缺失数据
在处理缺失数据时,第一个选择是删除具有任何缺失数据的所有观察。
此代码块使用 pandas.DataFrame.at[...]
属性修改了 df2
DataFrame,并将一些值设置为 NaN
:
for row, col in [('Bet', 'colA'), ('Bet', 'colB'),
('Bet', 'colC'), ('Del', 'colB'), ('Gam', 'colC')]:
df2.at[row, col] = np.NaN
df2
修改后的 DataFrame 如下:
Columns colA colB colC
Alp -1.721523 -0.425150 1.425227
Bet NaN NaN NaN
Gam -0.408566 -1.121813 NaN
Del 0.361053 NaN 0.580435
pandas.DataFrame.isnull(...)
方法在 DataFrame 中查找缺失值:
df2.isnull()
结果是一个 DataFrame,其中缺失值为 True
,否则为 False
:
Columns colA colB colC
Alp False False False
Bet True True True
Gam False False True
Del False True False
pandas.DataFrame.notnull(...)
方法执行相反操作(检测到非缺失值):
df2.notnull()
输出是以下 DataFrame:
Columns colA colB colC
Alp True True True
Bet False False False
Gam True True False
Del True False True
pandas.DataFrame.dropna(...)
方法允许我们删除具有缺失值的行。 附加的 how=
参数控制哪些行被删除。 要删除所有字段都为 NaN
的行,我们执行以下操作:
df2.dropna(how='all')
结果是以下修改后的 DataFrame,其中 Bet
行被移除,因为那是唯一一个所有值都为 NaN
的行:
Columns colA colB colC
Alp -1.721523 -0.425150 1.425227
Gam -0.408566 -1.121813 NaN
Del 0.361053 NaN 0.580435
将 how=
设置为 any
会删除具有任何 NaN 值的行:
df2.dropna(how='any')
这给我们以下包含所有非 NaN 值的 DataFrame:
Columns colA colB colC
Alp -1.721523 -0.42515 1.425227
现在我们将看看如何填充缺失数据。
填充缺失数据
处理缺失数据的第二个选择是使用我们选择的值或使用同一列中的其他有效值来填充缺失值以复制/推断缺失值。
让我们首先重新检查一下 df2
DataFrame:
df2
这产生以下带有一些缺失值的 DataFrame:
Columns colA colB colC
Alp -1.721523 -0.425150 1.425227
Bet NaN NaN NaN
Gam -0.408566 -1.121813 NaN
Del 0.361053 NaN 0.580435
现在,让我们使用 pandas.DataFrame.fillna(...)
方法,使用 method='backfill'
和 inplace=True
参数来使用 backfill
方法从其他值向后填充缺失值并就地更改 DataFrame:
df2.fillna(method='backfill', inplace=True);
df2
新的 DataFrame 包含以下内容:
Columns colA colB colC
Alp -1.721523 -0.425150 1.425227
Bet -0.408566 -1.121813 0.580435
Gam -0.408566 -1.121813 0.580435
Del 0.361053 NaN 0.580435
(Del,colB)
处的 NaN
值是因为该行后没有观察到值,因此无法执行向后填充。 这可以使用向前填充来修复。
使用函数和映射来转换 DataFrame
pandas DataFrame 的值也可以通过传递函数和字典映射来修改,这些函数和映射作用于一个或多个数据值,并生成新的转换值。
让我们通过添加一个新列 Category
来修改 df2
DataFrame,其中包含离散文本数据:
df2['Category'] = ['HIGH', 'LOW', 'LOW', 'HIGH']; df2
新的 DataFrame 包含以下内容:
Columns colA colB colC Category
Alp 1.017961 1.450681 -0.328989 HIGH
Bet -0.079838 -0.519025 1.460911 LOW
Gam -0.079838 -0.519025 1.460911 LOW
Del 0.359516 NaN 1.460911 HIGH
pandas.Series.map(...)
方法接受包含从旧值到新值的映射的字典,并对值进行转换。以下代码片段将 Category
中的文本值更改为单个字符:
df2['Category'] = df2['Category'].map({'HIGH': 'H',
'LOW': 'L'});
df2
更新后的 DataFrame 如下所示:
Columns colA colB colC Category
Alp 1.017961 1.450681 -0.328989 H
Bet -0.079838 -0.519025 1.460911 L
Gam -0.079838 -0.519025 1.460911 L
Del 0.359516 NaN 1.460911 H
pandas.DataFrame.applymap(...)
方法允许我们在 DataFrame 中对数据值应用函数。
以下代码应用了 numpy.exp(...)
方法,计算指数:
df2.drop('Category', axis=1).applymap(np.exp)
结果是一个包含原始 DataFrame 值的指数值的 DataFrame(除了 NaN
值):
Columns colA colB colC
Alp 2.767545 4.266020 0.719651
Bet 0.923266 0.595101 4.309883
Gam 0.923266 0.595101 4.309883
Del 1.432636 NaN 4.309883
现在我们已经学会了如何转换 DataFrame,我们将看到如何对 DataFrame 中的值进行离散化和分桶。
DataFrame 值的离散化/分桶
实现离散化的最简单方法是创建数值范围,并为落入某个区间的所有值分配一个单独的离散标签。
首先,让我们为我们的使用生成一个随机值 ndarray:
arr = np.random.randn(10);
arr
这包括以下内容:
array([ 1.88087339e-01, 7.94570445e-01, -5.97384701e-01,
-3.01897668e+00, -5.42185315e-01, 1.10094663e+00,
1.16002554e+00, 1.51491444e-03, -2.21981570e+00,
1.11903929e+00])
pandas.cut(...)
方法可用于离散化这些数值。以下代码使用 bins=
和 labels=[...]
参数将值分为五个离散值,并提供标签:
cat = pd.cut(arr, bins=5, labels=['Very Low', 'Low', 'Med',
'High', 'Very High']);
cat
在转换后,我们得到了离散值:
[High, Very High, Med, Very Low, Med, Very High, Very High, High, Very Low, Very High]
Categories (5, object): [Very Low < Low < Med < High < Very High]
pandas.qcut(...)
方法类似,但使用四分位数将连续值划分为离散值,以便每个类别具有相同数量的观测值。
以下使用 q=
参数构建了五个离散区间:
qcat = pd.qcut(arr, q=5, labels=['Very Low', 'Low', 'Med',
'High', 'Very High']);
qcat
四分位数离散化产生以下类别:
[Med, High, Low, Very Low, Low, High, Very High, Med, Very Low, Very High]
Categories (5, object): [Very Low < Low < Med < High < Very High]
以下代码块构建了一个包含原始连续值以及由 cut
和 qcut
生成的类别的 pandas DataFrame:
pd.DataFrame({'Value': arr, 'Category': cat,
'Quartile Category': qcat})
此 DataFrame 允许并列比较:
Category Quartile Category Value
0 High Med 0.188087
1 Very High High 0.794570
2 Med Low -0.597385
3 Very Low Very Low -3.018977
4 Med Low -0.542185
5 Very High High 1.100947
6 Very High Very High 1.160026
7 High Med 0.001515
8 Very Low Very Low -2.219816
9 Very High Very High 1.119039
pandas.Categorical.categories
属性为我们提供了区间范围:
pd.cut(arr, bins=5).categories
在这种情况下,区间/数值范围如下:
Index(['(-3.0232, -2.183]', '(-2.183, -1.347]',
'(-1.347, -0.512]', '(-0.512, 0.324]',
'(0.324, 1.16]'],
dtype='object')
我们也可以检查 qcut
的区间:
pd.qcut(arr, q=5).categories
它们与先前的区间略有不同,并显示如下:
Index(['[-3.019, -0.922]', '(-0.922, -0.216]',
'(-0.216, 0.431]', '(0.431, 1.105]',
'(1.105, 1.16]'],
dtype='object')
现在我们将看到如何对 DataFrame 值进行排列和抽样以生成新的 DataFrame。
对 DataFrame 值进行排列和抽样以生成新的 DataFrame
对可用数据集进行排列以生成新数据集,以及对数据集进行抽样以进行子抽样(减少观测数量)或超抽样(增加观测数量)是统计分析中常见的操作。
首先,让我们生成一个随机值 DataFrame 进行操作:
df = pd.DataFrame(np.random.randn(10,5),
index=np.sort(np.random.randint(0, 100,
size=10)),
columns=list('ABCDE'));
df
结果如下:
A B C D E
0 -0.564568 -0.188190 -1.678637 -0.128102 -1.880633
0 -0.465880 0.266342 0.950357 -0.867568 1.504719
29 0.589315 -0.968324 -0.432725 0.856653 -0.683398
...
当应用于 DataFrame 时,numpy.random.permutation(...)
方法会沿着索引轴随机洗牌,并且可以用于对数据集的行进行置换:
df.loc[np.random.permutation(df.index)]
这产生了以下随机打乱行的 DataFrame:
A B C D E
42 0.214554 1.108811 1.352568 0.238083 -1.090455
0 -0.564568 -0.188190 -1.678637 -0.128102 -1.880633
0 -0.465880 0.266342 0.950357 -0.867568 1.504719
62 -0.266102 0.831051 -0.164629 0.349047 1.874955
...
我们可以使用 numpy.random.randint(...)
方法在一定范围内生成随机整数,然后使用 pandas.DataFrame.iloc[...]
属性从我们的 DataFrame 中进行随机替换采样(同一观察结果可能会被多次选择
以下代码块随机选择了五行,并进行了替换采样:
df.iloc[np.random.randint(0, len(df), size=5)]
这导致了以下随机子采样的 DataFrame:
A B C D E
54 0.692757 -0.584690 -0.176656 0.728395 -0.434987
98 -0.517141 0.109758 -0.132029 0.614610 -0.235801
29 0.589315 -0.968324 -0.432725 0.856653 -0.683398
35 0.520140 0.143652 0.973510 0.440253 1.307126
62 -0.266102 0.831051 -0.164629 0.349047 1.874955
在接下来的章节中,我们将探索使用 pandas.DataFrames
进行文件操作。
使用 pandas.DataFrames 探索文件操作
pandas 支持将 DataFrames 持久化到纯文本和二进制格式中。常见的文本格式是 CSV 和 JSON 文件,最常用的二进制格式是 Excel XLSX、HDF5 和 pickle。
在本书中,我们专注于纯文本持久化。
CSV 文件
CSV 文件(逗号分隔值 文件)是数据交换标准文件。
写入 CSV 文件
使用 pandas.DataFrame.to_csv(...)
方法可以轻松将 pandas DataFrame 写入 CSV 文件。header=
参数控制是否将标题写入文件顶部,而 index=
参数控制是否将索引轴值写入文件:
df.to_csv('df.csv', sep=',', header=True, index=True)
我们可以使用以下 Linux 命令检查写入磁盘的文件。!
字符指示笔记本运行一个 shell 命令:
!head -n 4 df.csv
文件包含以下行:
,A,B,C,D,E
4,-0.6329164608486778,0.3733235944037599,0.8225354680198685,-0.5171618315489593,0.5492241692404063
17,0.7664860447792711,0.8427366352142621,0.9621402130525599,-0.41134468872009666,-0.9704305306626816
24,-0.22976016405853183,0.38081314413811984,-1.526376189972014,0.07229102135441286,-0.3297356221604555
读取 CSV 文件
使用 pandas.read_csv(...)
方法可以读取 CSV 文件并构建一个 pandas DataFrame。在这里,我们将指定字符(虽然这是 read_csv
的默认值),index_col=
参数来指定哪一列作为 DataFrame 的索引,以及 nrows=
参数来指定要读取的行数:
pd.read_csv('df.csv', sep=',', index_col=0, nrows=5)
这构建了以下 DataFrame,该 DataFrame 与写入磁盘的相同:
A B C D E
4 -0.632916 0.373324 0.822535 -0.517162 0.549224
17 0.766486 0.842737 0.962140 -0.411345 -0.970431
24 -0.229760 0.380813 -1.526376 0.072291 -0.329736
33 0.662259 -1.457732 -2.268573 0.332456 0.496143
33 0.335710 0.452842 -0.977736 0.677470 1.164602
我们还可以指定 chunksize=
参数,该参数一次读取指定数量的行,这在探索非常大的文件中包含的非常大的数据集时会有所帮助:
pd.read_csv('df.csv', sep=',', index_col=0, chunksize=2)
这将返回一个 pandas TextFileReader
生成器,我们可以根据需要迭代它,而不是一次加载整个文件:
<pandas.io.parsers.TextFileReader at 0x7fb4e9933a90>
我们可以通过将生成器包装在列表中来强制生成器完成评估,并观察按两行一组加载的整个 DataFrame:
list(pd.read_csv('df.csv', sep=',', index_col=0,
chunksize=2))
这给我们带来了以下两行块的列表:
[ A B C D E
4 -0.632916 0.373324 0.822535 -0.517162 0.549224
17 0.766486 0.842737 0.962140 -0.411345 -0.970431,
A B C D E
24 -0.229760 0.380813 -1.526376 0.072291 -0.329736
33 0.662259 -1.457732 -2.268573 0.332456 0.496143,
...
现在我们将看看如何探索 JSON 文件中的文件操作。
JSON 文件
JSON 文件基于与 Python 字典相同的数据结构。这使得 JSON 文件非常方便,可用于许多目的,包括表示 DataFrames 和表示配置文件。
pandas.DataFrame.to_json(...)
方法方便地将 DataFrame 写入磁盘上的 JSON 文件。在这里,我们只写入了前四行:
df.iloc[:4].to_json('df.json')
让我们来看看写入磁盘的 JSON 文件:
!cat df.json
这样我们就得到了以下写入磁盘的字典样式 JSON 文件:
{"A":{"4":-0.6329164608,"17":0.7664860448,"24":-0.2297601641,"33":0.6622594878},"B":{"4":0.3733235944,"17":0.8427366352,"24":0.3808131441,"33":-1.4577321521},"C":{"4":0.822535468,"17":0.9621402131,"24":-1.52637619,"33":-2.2685732447},"D":{"4":-0.5171618315,"17":-0.4113446887 ,"24":0.0722910214,"33":0.3324557226},"E":{"4":0.5492241692 ,"17":-0.9704305307,"24":-0.3297356222,"33":0.4961425281}}
使用pandas.read_json(...)
方法将 JSON 文件读回到 Pandas DataFrames 中同样很容易:
pd.read_json('df.json')
这样我们就能得到原始的写入磁盘的四行 DataFrame:
A B C D E
4 -0.632916 0.373324 0.822535 -0.517162 0.549224
17 0.766486 0.842737 0.962140 -0.411345 -0.970431
24 -0.229760 0.380813 -1.526376 0.072291 -0.329736
33 0.662259 -1.457732 -2.268573 0.332456 0.496143
恭喜成功完成本课程!
总结
本章介绍了 pandas 库,几乎所有 Python 中的时间序列操作都是基于它完成的。我们已经学会了如何创建 DataFrame,如何修改它以及如何持久化它。
Pandas DataFrames 主要用于高性能的大规模数据操作、选择和重塑数据。它们是 Python 版本的 Excel 工作表。
在下一章中,我们将使用 Matplotlib 在 Python 中进行可视化探索。
第五章:使用 Matplotlib 进行数据可视化
数据可视化比阅读纯数字表格更容易理解数值数据。即时获取数据洞察力和识别模式、趋势和异常值是绘图库的主要用途。
在决定哪些股票可能适合哪些算法交易策略时,创建股票价格图是第一步——某些策略仅适用于趋势股票,某些策略适用于均值回归股票等等。虽然数值统计数据至关重要,但设计良好的图表是无法替代的。
本章向我们介绍了 Matplotlib,这是一个静态、动画和交互式的 Python 可视化库,扩展了 NumPy 的功能。pandas
库允许直接使用 Matplotlib 绘制 DataFrame 图表。
本章涵盖了以下主要主题:
-
创建图和子图
-
使用颜色、标记和线型丰富图表
-
通过刻度、标签和图例丰富坐标轴
-
使用注释丰富数据点
-
将图保存到文件中
-
使用 Matplotlib 绘制
pandas
DataFrame
技术要求
本章中使用的 Python 代码可在该书代码库中的 Chapter05/matplotlib.ipynb
笔记本中找到。
创建图和子图
Matplotlib 支持在单个图中绘制多个图表(子图),这是 Matplotlib 对绘图画布的术语。
定义图的子图
要创建一个 matplotlib.pyplot.figure
对象,请使用以下方法:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(12, 6), dpi=200)
这会生成一个空的图形对象(0 Axes
):
<Figure size 2400x1200 with 0 Axes>
在我们在这个图上绘制任何东西之前,我们需要添加子图以为它们创建空间。matplotlib.pyplot.figure.add_subplot(...)
方法让我们通过指定子图的大小和位置来做到这一点。
下面的代码块在左侧添加了一个大小为 1x2 的子图,然后在右上方添加了一个 2x2 的子图,最后在右下方添加了一个 2x2 的子图:
ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(2, 2, 2)
ax3 = fig.add_subplot(2, 2, 4)
fig
结果是以下图形对象,其中包含我们刚刚添加的子图:
图 5.1 – 包含三个空子图的图
现在,一旦我们创建了图表的空间("plots"
/"subplots"
),我们就可以用可视化数据填充它们。在所有报告中,页面上的物理空间都非常昂贵,因此创建像前面的图表是最佳实践。
在子图中绘图
让我们使用 numpy.linspace(...)
在 x 轴上生成均匀间隔的值,然后使用 numpy.square(...)
、numpy.sin(...)
和 numpy.cos(...)
方法在 y 轴上生成相应的值。
我们将使用从添加子图中获得的 ax1
、ax2
和 ax3
轴变量来绘制这些函数:
import numpy as np
x = np.linspace(0, 1, num=20)
y1 = np.square(x)
ax1.plot(x, y1, color='black', linestyle='--')
y2 = np.sin(x)
ax2.plot(x, y2, color='black', linestyle=':')
y3 = np.cos(x)
ax3.plot(x, y3, color='black', linestyle='-.')
fig
现在,以下图包含了我们刚刚绘制的值:
图 5.2 - 包含三个子图绘制平方、正弦和余弦函数的图
创建子图时,可以传递sharex=
参数,以指定所有子图应共享相同的x轴。
让我们演示这个功能并绘制平方,然后使用numpy.power(...)
方法将x
提高到 10 次方,并使用相同的x轴绘制它们:
fig, (ax1, ax2) = plt.subplots(2, figsize=(12, 6),
sharex=True)
ax1.plot(x, y1, color='black', linestyle='--')
y2 = np.power(x, 10)
ax2.plot(x, y2, color='black', linestyle='-.')
结果是以下带有共享x轴和不同函数绘制在每个图表上的图:
图 5.3 - 共享 x 轴的子图,包含平方和取 10 次方函数
我们生成的图表还不够自解释 - 尚不清楚x轴和y轴上的单位是什么,以及每个图表代表什么。为了改进图表,我们需要用颜色、标记和线型来丰富它们,用刻度、图例和标签来丰富轴,并提供选定数据点的注释。
用颜色、标记和线型丰富图表
颜色、标记和线型使图表更易于理解。
接下来的代码块绘制了四个不同的函数,并使用以下参数修改外观:
-
color=
参数用于分配颜色。 -
linewidth=
参数用于更改线的宽度/粗细。 -
marker=
参数分配不同形状来标记数据点。 -
markersize=
参数改变了这些标记的大小。 -
alpha=
参数用于修改透明度。 -
drawstyle=
参数将默认线连接方式更改为一种绘制之间的阶梯连接方式。
代码如下:
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4,
figsize=(12, 12),
sharex=True)
x = np.linspace(0, 10, num=20)
y1 = np.exp(x)
y2 = x ** 3
y3 = np.sin(y2)
y4 = np.random.randn(20)
ax1.plot(x, y1, color='black', linestyle='--', linewidth=5,
marker='x', markersize=15)
ax2.plot(x, y2, color='green', linestyle='-.', linewidth=2,
marker='^', markersize=10, alpha=0.9)
ax3.plot(x, y3, color='red', linestyle=':', marker='*',
markersize=15, drawstyle='steps')
ax4.plot(x, y4, color='green', linestyle='-', marker='s',
markersize=15)
输出显示了四个具有不同属性的函数:
图 5.4 - 演示不同颜色、线型、标记样式、透明度和大小选项的图
使用不同的颜色、线型、标记样式、透明度和大小选项,可以生成具有易于识别的多个时间序列的丰富图表。选择颜色时要明智,因为它们在某些笔记本电脑屏幕上或在打印时可能不会很好地呈现。
丰富轴是制作出色图表的下一步。
用刻度、标签和图例丰富轴
通过自定义轴来进一步改进图表,通过刻度、限制和标签:
matplotlib.pyplot.xlim(...)
方法设置x轴上的值的范围。
matplotlib.pyplot.xticks(...)
方法指定x轴上刻度显示的位置:
plt.xlim([8, 10.5])
plt.xticks([8, 8.42, 8.94, 9.47, 10, 10.5])
plt.plot(x, y1, color='black', linestyle='--', marker='o')
这将修改x轴以在指定的限制内,并在明确指定的值处显示刻度:
图 5.5 – 具有显式 x 轴限制和刻度的图表
我们还可以使用 matplotlib.Axes.set_yscale(...)
方法将其中一个轴的比例改变为非线性。
matplotlib.Axes.set_xticklabels(...)
方法更改x轴上的标签:
fig, ax = plt.subplots(1, figsize=(12, 6))
ax.set_yscale('log')
ax.set_xticks(x)
ax.set_xticklabels(list('ABCDEFGHIJKLMNOPQRSTUV'))
ax.plot(x, y1, color='black', linestyle='--', marker='o',
label='y=exp(x)')
那个代码块的输出显示了y轴比例尺的差异,现在是对数,以及x轴刻度具有特定的刻度标签:
图 5.6 – 具有对数 y 轴比例尺和自定义 x 轴刻度标签的图表
图表中的对数比例尺在数据集涵盖大范围的值和/或我们想要传达百分比变化或乘法因子时非常有用。
matplotlib.Axes.set_title(...)
方法用于向绘图添加标题,matplotlib.Axes.set_xlabel(...)
和 matplotlib.Axes.set_ylabel(...)
方法用于设置x和y轴的标签。
matplotlib.Axes.legend(...)
方法添加图例,使图表更易于解释。loc=
参数指定图例在图表上的位置,loc='best'
表示 Matplotlib 自动选择最佳位置:
ax.set_title('xtickslabel example')
ax.set_xlabel('x labels')
ax.set_ylabel('log scale y values')
ax.legend(loc='best')
fig
以下图表显示了标题、x 和 y 轴标签以及图例:
图 5.7 – 展示了标题、x 轴和 y 轴标签以及图例的图表
每个时间序列的不同渲染图表以及解释单位和轴标签足以理解图表。然而,总会有一些特殊的数据点,值得指出。
用注释丰富数据点
matplotlib.Axes.text(...)
方法向我们的图表添加文本框:
ax.text(1, 10000, 'Generated using numpy and matplotlib')
fig
输出如下:
图 5.8 – 显示 Matplotlib 文本注释的图表
matplotlib.Axes.annotate(...)
方法提供了对注释的更多控制。
接下来的代码块使用以下参数控制注释:
-
xy=
参数指定数据点的位置。 -
xytext=
参数指定文本框的位置。 -
arrowprops=
参数接受一个字典,用于指定控制从文本框到数据点的箭头的参数。 -
facecolor=
参数指定颜色,shrink=
参数指定箭头的大小。 -
horizontalalignment=
和verticalalignment=
参数指定文本框相对于数据点的方向。
代码如下:
for i in [5, 10, 15]:
s = '(x=' + str(x[i]) + ',y=' + str(y1[i]) + ')'
ax.annotate(s, xy=(x[i], y1[i]), xytext=(x[i]+1,
y1[i]-5),
arrowprops=dict(facecolor='black',
shrink=0.05), horizontalalignment='left',
verticalalignment='top')
fig
结果如下:
图 5.9 – 具有数据点的文本和箭头注释的图表
引导读者注意关键数据点有助于读者专注于图表的信息。
可以使用 matplotlib.Axes.add_patch(...)
方法添加不同形状的注释。
接下来的代码块添加了一个 matplotlib.pyplot.Circle
对象,它接受以下内容:
-
使用
xy=
参数来指定位置 -
使用
radius=
参数来指定圆的半径 -
使用
color=
参数来指定圆的颜色
代码如下:
fig, ax = plt.subplots(1, figsize=(12, 6))
ax.plot(x, x, linestyle='--', color='black', marker='*',
markersize=15)
for val in x:
ax.add_patch(plt.Circle(xy=(val, val), radius=0.3,
color='darkgray'))
这生成了以下图表,其中数据点周围有圆圈:
图 5.10 – 包含围绕数据点添加补丁生成的圆形注释的图表
现在我们已经生成了漂亮、专业的图表,需要学习如何分享这些图片。
将图表保存到文件
matplotlib.pyplot.figure
对象使我们能够以不同的文件格式保存图表到磁盘,具有许多尺寸和分辨率说明符,例如 dpi=
参数:
fig.savefig('fig.png', dpi=200)
这将以下图表写入 fig.png
文件:
图 5.11 – 写入磁盘上的 Matplotlib 图表并在外部查看器中打开
导出的交易策略表现图像经常用于 HTML 或电子邮件报告。对于打印,请选择打印机的 DPI 作为图表的 DPI。
使用 Matplotlib 绘制 pandas DataFrame 图表
pandas
库提供了使用 Matplotlib 对 Series 和 DataFrame 对象进行绘图的功能。
让我们创建一个包含连续值的 Cont
值的 pandas
DataFrame,模拟价格,以及模拟价格变化的 Delta1
和 Delta2
值。 Cat
值包含五种可能的分类数据:
import pandas as pd
df = pd.DataFrame(index=range(1000),
columns=['Cont value', 'Delta1 value',
'Delta2 value', 'Cat value'])
df['Cont value'] = np.random.randn(1000).cumsum()
df['Delta1 value'] = np.random.randn(1000)
df['Delta2 value'] = np.random.randn(1000)
df['Cat value'] = np.random.permutation(['Very high',
'High', 'Medium',
'Low',
'Very Low']*200)
df['Delta1 discrete'] = pd.cut(df['Delta1 value'],
labels=[-2, -1, 0, 1, 2],
bins=5).astype(np.int64)
df['Delta2 discrete'] = pd.cut(df['Delta2 value'],
labels=[-2, -1, 0, 1, 2],
bins=5).astype(np.int64)
df
这生成了以下 DataFrame:
Cont value Delta1 val Delta2 val Cat value Delta1 discrete Delta2 discrete
0 -1.429618 0.595897 -0.552871 Very high 1 0
1 -0.710593 1.626343 1.123142 Medium 1 1
... ... ... ... ... ... ...
998 -4.928133 -0.426593 -0.141742 Very high 0 0
999 -5.947680 -0.183414 -0.358367 Medium 0 0
1000 rows × 6 columns
让我们探索不同的方式,展现这个 DataFrame。
创建 DataFrame 列的线图
我们可以使用 pandas.DataFrame.plot(...)
方法的 kind=
参数在线图中绘制 'Cont value'
:
df.plot(y='Cont value', kind='line', color='black',
linestyle='-', figsize=(12, 6))
此命令生成以下图表:
图 5.12 – 使用 pandas.DataFrame.plot(…) 方法生成的线图
线图通常用于显示时间序列。
创建 DataFrame 列的条形图
可以使用 kind='bar'
参数与 pandas.DataFrame.plot(...)
方法构建条形图。
让我们首先按 'Cat value'
值对 DataFrame 进行分组,然后绘制 Delta1 discrete
值的条形图:
df.groupby('Cat value')['Delta1 discrete']\
.value_counts().plot(kind='bar', color='darkgray',
title='Occurrence by (Cat,Delta1)',
figsize=(12, 6))
这生成了以下图表,显示了 (Cat value,Delta1 discrete)
值对的频率:
图 5.13 – 显示(Cat value,Delta1 discrete)值对频率的垂直条形图
kind='barh'
参数创建了一个水平条形图而不是垂直条形图:
df.groupby('Delta2 discrete')['Cat value'].value_counts()\
.plot(kind='barh', color='darkgray',
title='Occurrence by (Delta2,Cat)',
figsize=(12, 12))
输出如下:
图 5.14 – 水平条形图显示(Delta2 离散,Cat 值)对的频率
条形图最适合比较分类值的大小。
创建 DataFrame 列的直方图和密度图
pandas.DataFrame.plot(…)
方法中的 kind='hist'
参数创建了一个直方图。
让我们创建一个 Delta1 离散
值的直方图:
df['Delta1 discrete'].plot(kind='hist', color='darkgray',
figsize=(12, 6), label='Delta1')
plt.legend()
生成的直方图如下所示:
图 5.15 – Delta1 离散频率的直方图
我们可以使用 kind='kde'
参数,根据 Delta2 离散
值生成一个概率密度函数(PDF):
df['Delta2 discrete'].plot(kind='kde', color='black',
figsize=(12, 6),
label='Delta2 kde')
plt.legend()
输出如下:
图 5.16 – 显示 Delta2 离散值的概率密度函数(PDF)的 KDE 图
直方图和概率密度函数(PDF)/核密度估计(KDE)用于评估一些随机变量的概率分布。
创建两个 DataFrame 列的散点图
pandas.DataFrame.plot(...)
方法生成的散点图使用 kind='scatter'
参数。
以下代码块绘制了 Delta1
和 Delta2
值之间的散点图:
df.plot(kind='scatter', x='Delta1 value', y='Delta2 value',
alpha=0.5, color='black', figsize=(8, 8))
输出如下:
图 5.17 – Delta1 值和 Delta2 值字段的散点图
pandas.plotting.scatter_matrix(...)
方法在矩阵的非对角线条目上构建散点图矩阵,并在矩阵的对角线条目上构建直方图/核密度估计(KDE)图,这些图显示了 Delta1
和 Delta2
值之间的关系:
pd.plotting.scatter_matrix(df[['Delta1 value',
'Delta2 value']],
diagonal='kde', color='black',
figsize=(8, 8))
输出如下:
图 5.18 – Delta1 值和 Delta2 值字段的散点矩阵图
散点图/散点矩阵用于观察两个变量之间的关系。
绘制时间序列数据
以下代码块创建了一个包含两个虚构交易工具 A
和 B
的价格的 pandas
DataFrame。 DataFrame 由表示从 1992
年到 2012
年的每日日期的 DateTimeIndex
对象索引:
dates = pd.date_range('1992-01-01', '2012-10-22')
time_series = pd.DataFrame(index=dates, columns=['A', 'B'])
time_series['A'] = \
np.random.randint(low=-100, high=101,
size=len(dates)).cumsum() + 5000
time_series['B'] = \
np.random.randint(low=-75, high=76,
size=len(dates)).cumsum() + 5000
time_series
结果 DataFrame 如下:
A B
1992-01-01 5079 5042
1992-01-02 5088 5047
... ... ...
2012-10-21 6585 7209
2012-10-22 6634 7247
7601 rows × 2 columns
让我们使用这个时间序列代表性类型的图。
绘制线性图中的价格
首先,让我们用线性图绘制 A
和 B
的每日价格 20 年:
time_series['A'].plot(kind='line', linestyle='—',
color='black', figsize=(12, 6),
label='A')
time_series['B'].plot(kind='line', linestyle='-.',
color='darkgray', figsize=(12, 6),
label='B')
plt.legend()
输出如下:
图 5.19 – 20 年期间虚构仪器 A 和 B 的价格显示图
大多数时间序列图表都是线性图,而其他图表类型提供了额外的见解。
绘制价格变化直方图
金融时间序列分析中通常的下一步是检查某段时间内的价格变化。
下面的代码块使用pandas.DataFrame.shift(...)
和pandas.DataFrame.fillna(...)
方法生成表示 1 天、5 天和 20 天内价格变化的六个新字段。由于移位而导致数据缺失,我们还删除了具有缺失数据的行,并将最终的 DataFrame 保存在 time_series_delta
DataFrame 中:
time_series['A_1_delta'] = \
time_series['A'].shift(-1) – time_series['A'].fillna(0)
time_series['B_1_delta'] = \
time_series['B'].shift(-1) – time_series['B'].fillna(0)
time_series['A_5_delta'] = \
time_series['A'].shift(-5) – time_series['A'].fillna(0)
time_series['B_5_delta'] = \
time_series['B'].shift(-5) – time_series['B'].fillna(0)
time_series['A_20_delta'] = \
time_series['A'].shift(-20) – time_series['A'].fillna(0)
time_series['B_20_delta'] = \
time_series['B'].shift(-20) – time_series['B'].fillna(0)
time_series_deltas = time_series[['A_1_delta', 'B_1_delta',
'A_5_delta', 'B_5_delta',
'A_20_delta',
'B_20_delta']].dropna()
time_series_deltas
DataFrame 包含以下内容:
A_1_delta B_1_delta A_5_delta B_5_delta A_20_delta B_20_delta
1992-01-01 9.0 5.0 -49.0 118.0 -249.0 -56.0
1992-01-02 -91.0 69.0 -84.0 123.0 -296.0 -92.0
... ... ... ... ... ... ...
2012-10-01 88.0 41.0 -40.0 -126.0 -148.0 -84.0
2012-10-02 -10.0 -44.0 -71.0 -172.0 -187.0 -87.0
7581 rows × 6 columns
我们可以使用下面的代码块根据本章所学绘制A
的价格变化直方图:
time_series_delt's['A_20_de'ta'].plot(ki'd='h'st',
col'r='bl'ck',
alpha=0.5,
lab'l='A_20_de'ta',
figsize=(8,8))
time_series_delt's['A_5_de'ta'].plot(ki'd='h'st',
col'r='darkg'ay',
alpha=0.5,
lab'l='A_5_de'ta',
figsize=(8,8))
time_series_delt's['A_1_de'ta'].plot(ki'd='h'st',
col'r='lightg'ay',
alpha=0.5,
lab'l='A_1_de'ta',
figsize=(8,8))
plt.legend()
输出如下:
图 5.20 – A_1、A_5 和 A_20 三个值的直方图
直方图用于评估底层数据的概率分布。这个特定的直方图表明,A_20
delta 具有最大的变异性,这是合理的,因为底层数据呈现出强烈的趋势。
创建价格变化密度图
我们还可以使用 KDE PDF 绘制价格变化的密度。
下面的代码块绘制了B
的价格变化密度函数:
time_series_deltas['B_20_delta'].plot(kind='kde',
linestyle='-',
linewidth=2,
color='black',
label='B_20_delta',
figsize=(8,8))
time_series_deltas['B_5_delta'].plot(kind='kde',
linestyle=':',
linewidth=2,
color='black',
label='B_5_delta',
figsize=(8,8))
time_series_deltas['B_1_delta'].plot(kind='kde',
linestyle='--',
linewidth=2,
color='black',
label='B_1_delta',
figsize=(8,8))
plt.legend()
输出如下:
图 5.21 – B 在 1、5 和 20 天内价格变化的 KDE 密度图
KDE 密度图与直方图非常相似。与由离散箱子组成的直方图不同,KDE 是连续的线条。
创建区间的箱线图
我们可以按年度、季度、月度或周度等不同间隔分组每日价格,并使用箱线图显示这些价格的分布。
下面的代码首先使用pandas.Grouper
对象指定年度周期性,然后将结果应用于pandas.DataFrame.groupby(…)
方法以构建pandas.DataFrameGroupBy
对象。最后,我们调用pandas.DataFrameGroupBy.boxplot(...)
方法生成箱线图。我们指定rot=90
参数以将 x 轴刻度标签旋转,使其更易读:
group_A = time_series[['A']].groupby(pd.Grouper(freq='A'))
group_A.boxplot(color='black', subplots=False, rot=90,
figsize=(12,12))
输出如下:
图 5.22 – 按年份分组的 A 的价格箱线图分布的图表
带有须的箱线图用于通过相应的四分位数可视化数值数据组:
-
箱子的下界对应于下四分位数,而箱子的上界表示组的上四分位数。
-
箱子内的线显示间隔的中值。
-
箱子下方的线结束于最低观测值。
-
箱子上方的线结束于最高观测值。
创建滞后散点图
我们可以使用pandas.plotting.scatter_matrix(…)
方法可视化不同价格变化变量之间的关系:
pd.plotting.scatter_matrix(time_series[['A_1_delta',
'A_5_delta',
'A_20_delta',
'B_1_delta',
'B_5_delta',
'B_20_delta']],
diagonal='kde', color='black',
alpha=0.25, figsize=(12, 12))
结果显示(A_5_Delta 和 A_1_Delta)
、(A_5_Delta 和 A_20_Delta)
、(B_1_Delta 和 B_5_Delta)
以及(B_5_Delta 和 B_20_Delta)
变量对之间存在一些线性关系:
图 5.23 – A 和 B 价格变量的散点矩阵图
我们还可以使用pandas.plotting.lag_plot(...)
方法,并使用不同的lag=
值来指定不同水平的滞后,以生成A
的价格与滞后价格之间的散点图:
fig, (ax1, ax2, ax3) = plt.subplots(3, figsize=(12, 12))
pd.plotting.lag_plot(time_series['A'], ax=ax1, lag=1,
c='black', alpha=0.2)
pd.plotting.lag_plot(time_series['A'], ax=ax2, lag=7,
c='black', alpha=0.2)
pd.plotting.lag_plot(time_series['A'], ax=ax3, lag=20,
c='black', alpha=0.2)
这生成了滞后值为 1、7 和 20 天的三个图:
图 5.24 – A 的价格滞后 1、7 和 20 天的滞后图,显示马丁盖尔性质
对数图检查时间序列是否是没有任何趋势的随机的。对于随机时间序列,其滞后图显示没有结构。前述图表显示了明显的线性趋势;也就是说,我们可能成功地用自回归模型进行建模。
创建自相关图
自相关图可视化了特定时间点的价格与滞后若干期价格之间的关系。
我们可以使用pandas.plotting.autocorrelation_plot(...)
方法将滞后值绘制在x轴上,将价格与指定值滞后的价格之间的相关性绘制在y轴上:
fig, ax = plt.subplots(1, figsize=(12, 6))
pd.plotting.autocorrelation_plot(time_series['A'], ax=ax)
我们可以看到随着滞后值的增加,自相关逐渐恶化:
图 5.25 – 绘制滞后值与价格和指定值滞后的自相关之间关系的图表
自相关图总结了时间序列的随机性。对于随机时间序列,所有滞后相关性都会接近于 0。对于非随机时间序列,至少一个滞后相关性会显著非零。
摘要
在本章中,我们学习了如何使用 Matplotlib 创建 pandas DataFrame 的视觉上吸引人的图表。虽然我们可以计算许多数值统计量,但图表通常更快地提供更深入的洞察力。您应该尽可能绘制尽可能多的不同图表,因为每个图表都提供了数据的不同视角。
在下一章中,我们将学习如何在 Python 中执行统计测试并估计统计模型。
第六章:统计估计、推断和预测
在本章中,我们通过概述关键示例来介绍 Python 中的四个主要统计库—statsmodels
、pmdarima
、fbprophet
和 scikitlearn
。这些库用于对时间序列建模并提供它们的预测值,以及置信区间。此外,我们演示了如何使用分类模型来预测时间序列的百分比变化。
为此,我们将涵盖以下使用案例:
-
statsmodels 介绍
-
使用具有外生因素的季节性自回归综合移动平均(SARIMAX)时间序列模型与 pmdarima
-
使用 Facebook 的 Prophet 库进行时间序列预测
-
scikit-learn 回归和分类介绍
技术要求
本章使用的 Python 代码可在书籍的代码存储库的 Chapter06 文件夹
中找到。
statsmodels 介绍
statsmodels 是一个 Python 库,允许我们探索数据、执行统计检验并估计统计模型。
本章重点介绍了 statsmodels 对时间序列的建模、分析和预测。
使用 Q-Q 图进行正态分布检验
许多统计学习技术的一个基本假设是观测值/字段是正态分布的。
虽然有许多用于正态分布的健壮统计检验方法,但一种直观的视觉方法被称为分位数-分位数图(Q-Q 图)。如果一个样本是正态分布的,它的 Q-Q 图是一条直线。
在下面的代码块中,使用 statsmodels.graphics.api.qqplot(...)
方法来检查 numpy.random.uniform(...)
分布是否是正态分布:
from statsmodels.graphics.api import qqplot
import numpy as np
fig = qqplot(np.random.uniform(size=10000), line='s')
fig.set_size_inches(12, 6)
结果显示的图示了两个分布之间的非线性关系,这是预期的,因为我们使用了均匀分布:
图 6.1 – 从均匀分布生成的数据集的 Q-Q 图
在下面的代码块中,我们重复测试,但这次使用 numpy.random.exponential(...)
分布作为我们的样本分布:
fig = qqplot(np.random.exponential(size=10000), line='s')
fig.set_size_inches(12, 6)
结果显示的 Q-Q 图再次证实了两个分布之间的非正态关系,如下截图所示:
图 6.2 – 从指数分布生成的数据集的 Q-Q 图
最后,我们将从正态分布中挑选出 10,000 个样本,使用 numpy.random.normal(...)
方法,并使用 qqplot(...)
进行观察,如下代码片段所示:
fig = qqplot(np.random.normal(size=10000), line='s')
fig.set_size_inches(12, 6)
结果是如预期的线性关系的图示,如下截图所示:
图 6.3 – 从标准正态分布中采样的 10,000 个样本的 Q-Q 图
Q-Q 图用于比较两个概率分布——其中一个最常见的是正态分布——通过将它们的分位数相互绘制来绘制它们之间的比较。前面的例子演示了通过视觉测试正态分布是多么容易。
使用 statsmodels 进行时间序列建模
时间序列是按时间顺序排列的一系列数值数据点。
处理时间序列数据的一个关键部分涉及处理日期和时间。
statsmodels.api.tsa.datetools
模块提供了一些基本方法来生成和解析日期和日期范围,例如dates_from_range(...)
。
在以下代码片段中,我们使用length=12
参数从2010
年开始以年度频率生成了 12 个datetime.datetime
对象:
import statsmodels.api as sm
sm.tsa.datetools.dates_from_range('2010', length=12)
这导致了以下datetime
对象列表:
[datetime.datetime(2010, 12, 31, 0, 0),
datetime.datetime(2011, 12, 31, 0, 0),
...
datetime.datetime(2020, 12, 31, 0, 0),
datetime.datetime(2021, 12, 31, 0, 0)]
在dates_from_range(...)
方法中,日期的频率可以通过开始日期和一个特殊的格式来指定,其中m1
后缀表示第一个月和月频率,q1
表示第一个季度和季度频率,如下面的代码片段所示:
sm.tsa.datetools.dates_from_range('2010m1', length=120)
这导致了以下月频率的datetime
对象列表:
[datetime.datetime(2010, 1, 31, 0, 0),
datetime.datetime(2010, 2, 28, 0, 0),
...
datetime.datetime(2019, 11, 30, 0, 0),
datetime.datetime(2019, 12, 31, 0, 0)]
现在让我们对一个时间序列执行Error, Trend, Seasonality (ETS)分析。
时间序列的 ETS 分析
时间序列的 ETS 分析将数据分解为三个不同的组件,如下所示:
-
趋势(trend)组件捕获了时间序列的总体趋势。
-
季节性(seasonality)组件捕获了周期性/季节性变化。
-
误差(error)组件捕获了数据中无法用其他两个组件捕获的噪声。
让我们使用datetools.dates_from_range(...)
方法生成 20 年的月度日期作为 Pandas DataFrame 数据集的索引,如下所示:
import pandas as pd
n_obs = 12 * 20
linear_trend = np.linspace(100, 200, num=n_obs)
cycle = np.sin(linear_trend) * 10
error_noise = np.random.randn(n_obs)
dataset = \
pd.DataFrame(
linear_trend + cycle + error_noise,
index=sm.tsa.datetools.dates_from_range('2000m1',
length=n_obs),
columns=['Price'])
dataset
结果是以下包含 ETS 组件的Price
字段的 DataFrame:
Price
2000-01-31 96.392059
2000-02-29 99.659426
... ...
2019-11-30 190.067039
2019-12-31 190.676568
240 rows × 1 columns
让我们可视化我们生成的时间序列数据集,如下所示:
import matplotlib.pyplot as plt
dataset.plot(figsize=(12, 6), color='black')
生成的时间序列数据集具有明显的线性增长趋势,其中夹杂着季节性组件,如下截图所示:
图 6.4 – 显示具有 ETS 组件的合成价格的图表
在上一张截图中,我们清楚地看到了季节性组件——从中位数值上下波动。我们还看到了误差噪声,因为波动不是完美的。最后,我们看到数值正在增加——趋势组件。
Hodrick-Prescott 滤波器
在statsmodels
中,这被实现为statsmodels.api.tsa.filters.hpfilter(...)
。
让我们使用 lamb=129600
平滑参数进行分解(值 129600
是月度数据的推荐值)。我们使用返回的一对系列值生成一个 DataFrame,其中包含 Price
、hp_cycle
和 hp_trend
字段,以表示价格、季节性组件和趋势组件,如下面的代码片段所示:
hp_cycle, hp_trend = \
sm.tsa.filters.hpfilter(dataset['Price'], lamb=129600)
decomp = dataset[['Price']]
decomp['HP_Cycle'] = hp_cycle
decomp['HP_Trend'] = hp_trend
decomp
decomp
DataFrame 包含以下数据:
Price HP_Cycle HP_Trend
2000-01-31 96.392059 -4.731153 101.123212
2000-02-29 99.659426 -1.839262 101.498688
... ... ... ...
2019-11-30 190.067039 -8.350371 198.417410
2019-12-31 190.676568 -8.107701 198.784269
240 rows × 3 columns
在下一节中,我们将查看 UnobservedComponents
模型。
UnobservedComponents 模型
将时间序列分解为 ETS 组件的另一种方法是使用 statsmodels.api.tsa.UnobservedComponents
对象。
UnobservedComponentsResults.summary(...)
方法生成模型的统计信息,如下所示:
uc = sm.tsa.UnobservedComponents(dataset['Price'],
level='lltrend',
cycle=True,
stochastic_cycle=True)
res_uc = uc.fit(method='powell', disp=True)
res_uc.summary()
输出包含模型的详细信息,如下所示的代码块所示:
Optimization terminated successfully.
Current function value: 2.014160
Iterations: 6
Function evaluations: 491
Unobserved Components Results
Dep. Variable: Price No. Observations: 240
Model: local linear trend Log Likelihood -483.399
+ stochastic cycle AIC 976.797
Date: Fri, 12 Jun 2020 BIC 994.116
Time: 08:09:46 HQIC 983.779
Sample: 01-31-2000
- 12-31-2019
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
sigma2.irregular 0.4962 0.214 2.315 0.021 0.076 0.916
sigma2.level 6.954e-17 0.123 5.63e-16 1.000 -0.242 0.242
sigma2.trend 2.009e-22 4.03e-05 4.98e-18 1.000 -7.91e-05 7.91e-05
sigma2.cycle 1.5485 0.503 3.077 0.002 0.562 2.535
frequency.cycle 0.3491 0.013 27.768 0.000 0.324 0.374
Ljung-Box (Q): 347.56 Jarque-Bera (JB): 0.42
Prob(Q): 0.00 Prob(JB): 0.81
Heteroskedasticity (H): 0.93 Skew: -0.09
Prob(H) (two-sided): 0.73 Kurtosis: 2.91
我们可以使用 resid
、cycle.smoothed
和 level.smoothed
属性访问 ETS/周期性组件,并将它们添加到 decomp
DataFrame 中,如下所示:
decomp['UC_Cycle'] = res_uc.cycle.smoothed
decomp['UC_Trend'] = res_uc.level.smoothed
decomp['UC_Error'] = res_uc.resid
decomp
decomp
DataFrame 现在包含以下新列,其中包含来自 UnobservedComponents
模型的 Cycle
、Trend
和 Error
项:
... UC_Cycle UC_Trend UC_Error
2000-01-31 ... -3.358954 99.743814 96.392059
2000-02-29 ... -0.389834 100.163434 6.173967
... ... ... ... ...
2019-11-30 ... -9.725420 199.613395 1.461497
2019-12-31 ... -9.403885 200.033015 0.306881
240 rows × 6 columns
接下来,我们将查看 statsmodel.tsa.seasonal.seasonal_decompose(…)
方法。
statsmodels.tsa.seasonal.seasonal_decompose(...)
方法
执行 ETS 分解的另一种方法是使用 statsmodels.tsa.seasonal.seasonal_decompose(...)
方法。
下面的代码块使用加法模型,通过指定 model='additive'
参数,并且通过访问 DecomposeResult
对象中的 season
、trend
和 resid
属性,将 SDC_Cycle
、SDC_Trend
和 SDC_Error
列添加到 decomp
DataFrame 中:
from statsmodels.tsa.seasonal import seasonal_decompose
s_dc = seasonal_decompose(dataset['Price'],
model='additive')
decomp['SDC_Cycle'] = s_dc.seasonal
decomp['SDC_Trend'] = s_dc.trend
decomp['SDC_Error'] = s_dc.resid
decomp[118:122]
decomp
DataFrame 现在有了三个附加字段及其值,如下面的代码块所示:
... SDC_Cycle SDC_Trend SDC_Error
2009-11-30 ... 0.438633 146.387392 -8.620342
2009-12-31 ... 0.315642 147.240112 -6.298764
2010-01-31 ... 0.228229 148.384061 -3.538544
2010-02-28 ... 0.005062 149.912202 -0.902362
接下来,我们将绘制前面各节得到的各种结果。
绘制 HP 过滤器结果、UnobservedComponents 模型和 seasonal_decompose 方法的结果
让我们绘制从 HP
过滤器、UnobservedComponents
模型和 seasonal_decompose
方法中提取的趋势组件,如下所示:
plt.title('Trend components')
decomp['Price'].plot(figsize=(12, 6), color='black',
linestyle='-', legend='Price')
decomp['HP_Trend'].plot(figsize=(12, 6), color='darkgray',
linestyle='--', lw=2,
legend='HP_Trend')
decomp['UC_Trend'].plot(figsize=(12, 6), color='black',
linestyle=':', lw=2,
legend='UC_Trend')
decomp['SDC_Trend'].plot(figsize=(12, 6), color='black',
linestyle='-.', lw=2,
legend='SDC_Trend')
这给我们提供了以下图表,趋势组件与原始价格并排绘制。所有三个模型都很好地识别了总体上升的趋势,seasonal_decompose(...)
方法捕捉到了一些非线性/周期性的趋势组件,除了总体上线性增长的趋势之外:
图 6.5 - 显示从不同 ETS 分解方法中提取的趋势组件
下面的代码块绘制了从三个模型中获取的循环/季节性组件:
plt.title('Cycle/Seasonal components')
decomp['HP_Cycle'].plot(figsize=(12, 6), color='darkgray',
linestyle='--', lw=2,
legend='HP_Cycle')
decomp['UC_Cycle'].plot(figsize=(12, 6), color='black',
linestyle=':', lw=2,
legend='UC_Cycle')
decomp['SDC_Cycle'].plot(figsize=(12, 6), color='black',
linestyle='-.', lw=2,
legend='SDC_Cycle')
以下结果显示,seasonal_decompose(...)
方法生成了具有非常小波动的季节性组件,这是因为一些季节性组件的部分已经内置到我们之前看到的趋势图中:
图 6.6 - 显示通过不同 ETS 分解方法提取的周期/季节性组件的图表
最后,我们将通过使用差分方法将我们的数据集可视化为一个平稳数据集,如下所示:
plt.title('Error components')
plt.ylim((-20, 20))
decomp['UC_Error'].plot(figsize=(12, 6), color='black',
linestyle=':', lw=2,
legend='UC_Error')
decomp['SDC_Error'].plot(figsize=(12, 6), color='black',
linestyle='-.', lw=2,
legend='SDC_Error')
输出如下屏幕截图所示:
图 6.7 - 显示来自不同 ETS 分解模型的误差项的图表
前面屏幕截图中显示的图表显示了误差项围绕0
振荡,并且它们没有明显的趋势。
时间序列的平稳的增广迪基-富勒测试
平稳时间序列是指其统计属性,如均值、方差和自相关在时间上保持恒定。许多统计预测模型假设时间序列数据集可以通过一些数学操作(如差分)转换为平稳数据集。
增广迪基-富勒(ADF)测试用于检查数据集是否平稳 - 它计算数据集不平稳的可能性,当该概率(p 值)非常低时,我们可以得出结论数据集是平稳的。我们将在以下章节中详细介绍详细步骤。
第一步 - 对价格进行 ADF 测试
让我们检查平稳性,并通过使用差分方法将我们的数据集转换为一个平稳数据集。我们从statsmodels.tsa.stattools.adfuller(...)
方法开始,如以下代码片段所示:
from statsmodels.tsa.stattools import adfuller
result = adfuller(dataset['Price'])
print('Test Stat: {}\np value: {}\nLags: {}\nNum \
observations: {}'.format(result[0], result[1],
result[2], result[3]))
将其应用于Price
字段时,输出如下值。 Test
统计量为正值,p 值为 98%,这意味着有强有力的证据表明Price
字段不是平稳的。我们知道这是预期的,因为Price
字段中有强趋势和季节性组件:
Test Stat: 0.47882793726850786
p value: 0.9842151821849324
Lags: 14
Num observations: 225
第二步 - 对价格进行第一阶差分
接下来,我们应用第一阶差分转换;这从一个观测到下一个观测中找到第一个差异。如果我们再次对差分数据集进行差分,则会产生第二阶差分,依此类推。
我们将一阶差分的pandas.Series
数据集存储在price_diff
变量中,如下面的代码块所示:
price_diff = \
(dataset['Price'].shift(-1) - dataset['Price']).fillna(0)
price_diff
该数据集包含以下数值:
2000-01-31 4.951062
2000-02-29 5.686832
...
2019-11-30 3.350694
2019-12-31 0.000000
Name: Price, Length: 240, dtype: float64
第三步 - 对价格进行差分的 ADF 测试
现在,我们对转换后的数据集重新运行 ADF 测试,以检查是否具有平稳性,如下所示:
result = adfuller(price_diff)
print('Test Stat: {}\np value: {}\nLags: {}\nNum \
observations: {}'.format(result[0], result[1],
result[2], result[3]))
现在,测试统计量具有较大的负值(值小于-4 的值具有非常高的平稳性可能性)。现在不平稳的概率现在降低到极低的值,表明转换后的数据集是平稳的,如以下代码片段所示:
Test Stat: -7.295184662866956
p value: 1.3839111942229784e-10
Lags: 15
Num observations: 224
时间序列的自相关和偏自相关
自相关或串行相关是观察值与延迟副本自身的关联性。它衡量当前观察值是否与未来/过去的值有关系。
在我们具有明显线性趋势和一些季节性组分的数据集中,随着滞后数的增加,自相关缓慢减小,但对于较小的滞后值,由于总体线性趋势较大,数据集具有较高的自相关值。statsmodels.graphics.tsaplots.plot_acf(...)
方法绘制了Price
字段与滞后值从0
到100
的自相关,如下代码片段所示:
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig = plot_acf(dataset['Price'], lags=100)
fig.set_size_inches(12, 6)
结果表明,自相关在约 36 的滞后值附近仍然相对较强,在这里它低于 0.5。如下截图所示:
图 6.8 – 自相关图显示自相关与不同滞后值的关系
statsmodels.graphics.tsaplots.plot_pacf(…)
方法让我们可以绘制偏自相关值与不同滞后值之间的关系图。自相关和偏自相关的区别在于,偏自相关只使用与前一个滞后期观测值的相关性,并且消除了低滞后值的相关性效应。该方法在以下代码片段中显示:
fig = plot_pacf(dataset['Price'], lags=100)
fig.set_size_inches(12, 6)
输出结果如下截图所示:
图 6.9 – 偏自相关图显示偏自相关与滞后值的关系
在前面的截图中显示的图形在前两个滞后项之后急剧下降,并且在每 10 个滞后项之后季节性地从正值变为负值。
ARIMA 时间序列模型
自回归积分滑动平均(ARIMA)模型是最知名的时间序列建模和预测模型之一。它用于预测具有相关数据点的时间序列数据。
ARIMA 模型由三个组成部分组成,如下所述:
-
p
参数,指定要使用的滞后数。根据自相关图,当对Price
系列进行 ARIMA 建模时,我们将指定p=36
。 -
d
参数,指定要执行的差分阶数,在我们的情况下将是d=1
。正如我们在时间序列的增广迪基-富勒检验稳定性部分所看到的,一阶差分导致了一个稳定的数据集。 -
q
,即 MA 窗口的大小。在我们的情况下,我们将根据偏自相关图设置此参数,并使用q=2
的值,因为在滞后值为1
之后,偏自相关急剧下降。
在 statsmodels 中,statsmodels.tsa.arima.model.ARIMA
模型将时间序列构建为 ARIMA 模型。使用order=(36, 1, 2)
参数,我们指定了p=36
,d=1
和q=2
。然后,我们调用ARIMA.fit(...)
方法将模型拟合到我们的Price
系列,并调用ARIMA.summary(...)
方法输出有关拟合的 ARIMA 模型的信息。
一些其他的包——例如,pmdarima
——提供了auto_arima
方法,可以自动找到 ARIMA 模型,如下面的代码片段所示:
from statsmodels.tsa.arima.model import ARIMA
arima = ARIMA(dataset['Price'], order=(36,1,2))
res_ar = arima.fit()
res_ar.summary()
以下输出描述了拟合参数:
SARIMAX Results
Dep. Variable: Price No. Observations: 240
Model: ARIMA(36, 1, 2) Log Likelihood -360.195
Date: Sat, 13 Jun 2020 AIC 798.391
Time: 09:18:46 BIC 933.973
Sample: 01-31-2000 HQIC 853.027
- 12-31-2019
Covariance Type: opg
coef std err z P>|z| [0.025 0.975]
ar.L1 -0.8184 0.821 -0.997 0.319 -2.428 0.791
ar.L2 -0.6716 0.495 -1.358 0.175 -1.641 0.298
...
ar.L35 0.3125 0.206 1.514 0.130 -0.092 0.717
ar.L36 0.1370 0.161 0.851 0.395 -0.178 0.452
ma.L1 -0.0244 0.819 -0.030 0.976 -1.630 1.581
ma.L2 0.1694 0.454 0.373 0.709 -0.721 1.060
sigma2 1.0911 0.144 7.586 0.000 0.809 1.373
Ljung-Box (Q): 13.99 Jarque-Bera (JB): 1.31
Prob(Q): 1.00 Prob(JB): 0.52
Heteroskedasticity (H): 1.15 Skew: 0.09
Prob(H) (two-sided): 0.54 Kurtosis: 2.69
使用statsmodels.tsa.arima.ARIMAResults.predict(...)
方法,我们可以使用拟合的模型预测指定起始和结束日期指数(在本例中是整个数据集)上的值。我们将预测的价格保存在PredPrice
字段中,以便稍后进行比较。代码如下所示:
dataset['PredPrice'] = res_ar.predict(dataset.index[0],
dataset.index[-1])
dataset
结果将添加新列并显示预测价格,如下所示:
Price PredPrice
2000-01-31 95.317833 0.000000
2000-02-29 100.268895 95.317901
... ... ...
2019-11-30 188.524009 188.944216
2019-12-31 191.874704 190.614641
240 rows × 2 columns
现在,我们将在下面的代码块中绘制原始Price
和PredPrice
字段,以便进行视觉比较:
plt.ylim(70, 250)
dataset['Price'].plot(figsize=(12, 6), color='darkgray',
linestyle='-', lw=4, legend='Price')
dataset['PredPrice'].plot(figsize=(12, 6), color='black',
linestyle='-.',
legend='PredPrice')
预测价格相当准确,这是因为指定的参数(p
, d
, q
)是精确的。结果可以在下面的截图中看到:
图 6.10 – 比较原始价格和 ARIMA(36, 1, 2)模型预测价格的绘图
让我们使用这个拟合的模型来预测未来日期的值。首先,我们使用datetools.dates_from_range(...)
方法和pandas.DataFrame.append(...)
方法构建一个包含另外 4 年的日期索引且没有数据(将使用NaN
值填充)的extended_dataset
DataFrame,如下所示:
extended_dataset = pd.DataFrame(index=sm.tsa.datetools.dates_from_range('2020m1', length=48))
extended_dataset = dataset.append(extended_dataset)
extended_dataset
Price PredPrice
2000-01-31 95.317833 0.000000
2000-02-29 100.268895 95.317901
... ... ...
2023-11-30 NaN NaN
2023-12-31 NaN NaN
288 rows × 2 columns
接着,我们可以再次调用ARIMAResults.predict(...)
方法,为整个时间序列生成预测价格,从而对我们添加的新日期进行预测,如下所示:
extended_dataset['PredPrice'] = \
res_ar.predict(extended_dataset.index[0],
extended_dataset.index[-1])
extended_dataset
Price PredPrice
2000-01-31 95.317833 0.000000
2000-02-29 100.268895 95.317901
... ... ...
2023-11-30 NaN 215.441777
2023-12-31 NaN 220.337355
288 rows × 2 columns
以下代码块绘制了extended_dataset
DataFrame 中的最后 100 个观测值:
extended_dataset['Price'].iloc[-100:].plot(figsize=(12, 6),
color='darkgray',
linestyle='-',
lw=4,
legend='Price')
extended_dataset['PredPrice'].iloc[-100:].plot(figsize=(12, 6),
color='black',
linestyle='-.',
legend='PredPrice')
这样就得到了一个包含预测的PredPrice
值的绘图,如下面的截图所示:
图 6.11 – ARIMA 模型预测的历史和预测价格
在前面截图中显示的图中,预测价格明显遵循过去价格的趋势。
使用 pmdarima 的 SARIMAX 时间序列模型
SARIMA是 ARIMA 模型的扩展,用于具有季节性成分的单变量时间序列。
SARIMAX,是模型的名称,同时支持外生变量。
这些是三个 ARIMA 参数:
-
p
= 趋势自回归阶数 -
d
= 趋势差分阶数 -
q
= 趋势移动平均阶数
除了前面的参数之外,SARIMA 还引入了另外四个参数,如下所示:
-
P
= 季节性自回归阶数 -
D
= 季节性差分阶数。 -
Q
= 季节性 MA 阶数。 -
m
= 单个季节周期的长度,以时间步数表示。
手动查找这些参数可能会耗费时间,使用自动 ARIMA 模型可能更有优势。
在 Python 中,auto-ARIMA 建模由 pmdarima
库提供。其文档可在 alkaline-ml.com/pmdarima/index.html
上找到。
安装很简单,如下所示:
pip install pmdarima
自动 ARIMA 模型试图通过进行各种统计测试来自动发现 SARIMAX 参数,如下所示:
图 6.12 – 各种统计检验的表格。
一旦找到最佳的 d
值,auto-ARIMA 模型将在由 start_p
、max_p
、start_q
和 max_q
定义的范围内搜索最适合的模型。如果启用了 seasonal
参数,则一旦确定最佳的 D
值,我们就会使用类似的程序来找到 P
和 Q
。
最佳模型通过最小化信息准则的值确定(阿卡奇信息准则 (AIC), 校正 AIC, 贝叶斯信息准则 (BIC), Hannan-Quinn 信息准则 (HQC), 或 袋外 (OOB)—用于验证评分—分别)。
如果未找到合适的模型,auto-ARIMA 将返回 ValueError
输出。
让我们使用前面的数据集进行自动 ARIMA。时间序列具有明显的季节性分量,周期为 12。
请注意下面的代码块中,我们为预测值生成了 95%的置信区间,这对于交易规则非常有用,例如,如果价格高于上限置信区间值,则卖出:
import pmdarima as pm
model = pm.auto_arima(dataset['Price'], seasonal=True,
stepwise=True, m=12)
print(model.summary())
extended_dataset = \
pd.DataFrame(
index=sm.tsa.datetools.dates_from_range('2020m1',
length=48))
extended_dataset['PredPrice'], conf_int = \
model.predict(48, return_conf_int=True, alpha=0.05)
plt.plot(dataset['Price'], c='blue')
plt.plot(extended_dataset['PredPrice'], c='green')
plt.show()
print(extended_dataset)
print(conf_int)
输出如下所示:
图 6.13 – SARIMAX 结果来自自动 ARIMA 的统计数据。
图中显示如下的截图:
图 6.14 – 自动 ARIMA 模型预测的历史和预测价格预测。
输出还包括预测价格,如下所示:
PredPrice
2020-01-31 194.939195
... ...
2023-12-31 222.660698
[48 rows x 1 columns]
另外,输出提供了每个预测价格的置信区间,如下所示:
[[192.39868933 197.4797007 ]
[196.80033117 202.32443987]
[201.6275806 207.60042584]
...
[212.45091331 225.44676173]
[216.11548707 229.20590827]]
现在我们将看到使用 Facebook 的 Prophet 库进行时间序列预测。
使用 Facebook 的 Prophet 库进行时间序列预测。
Facebook Prophet 是一个用于预测单变量时间序列的 Python 库,对季节性和节假日效应提供了强大的支持。它特别适用于具有频繁变化趋势的时间序列,并且足够强大以处理异常值。
更具体地说,Prophet
模型是一个具有以下属性的加法回归模型:
-
分段线性或逻辑增长趋势。
-
年度季节性分量采用傅里叶级数模拟。
-
用虚拟变量建模的每周季节性分量。
-
用户提供的节假日列表
Prophet
的安装更加复杂,因为它需要编译器。 安装它的最简单方法是使用 Anaconda,如下所示:
conda install -c conda-forge fbprophet
附带的 Git 存储库包含了带有 Prophet
的 conda
环境设置。
Prophet
库要求输入的 DataFrame 包含两列—ds
代表日期,y
代表值。
让我们将 Prophet
模型拟合到以前的数据集中。请注意在以下代码片段中,我们明确告诉 Prophet
我们希望获得每月的预测值 (freq='M'
):
from fbprophet import Prophet
prophet_dataset = \
dataset.rename(columns={'Price' : 'y'}).rename_axis('ds')\
.drop('PredPrice', 1).reset_index()
print(prophet_dataset)
model = Prophet()
model.fit(prophet_dataset)
df_forecast = model.make_future_dataframe(periods=48,
freq='M')
df_forecast = model.predict(df_forecast)
print(df_forecast[['ds', 'yhat', 'yhat_lower',
'yhat_upper']].tail())
model.plot(df_forecast, xlabel='Date', ylabel='Value')
model.plot_components(df_forecast)
预测值与 SARIMAX 模型非常相似,可以在此处看到:
图 6.15 – Prophet 库的输出包括预测值,以及模型组件的值
预测值存储在 yhat
列中,其中包含了 yhat_lower
和 yhat_upper
置信区间。
Prophet
确实生成了 Prophet 组件的图表,这对于理解模型的预测能力非常有用。 一个趋势组件图表可以在这里看到:
图 6.16 – Prophet 模型的趋势组件图表
以下截图显示了年度季节性的输出:
图 6.17 – Prophet 模型的年度季节性组件图表
这是预测图表的输出:
图 6.18 – Prophet 模型的预测图表及置信区间
每个时间序列模型都略有不同,并且最适合不同类别的时间序列。 但总的来说,Prophet
模型非常稳健,并且在大多数情况下最容易使用。
scikit-learn 回归和分类简介
scikit-learn
是一个基于numpy
和scipy
库构建的 Python 监督 和 无监督 机器学习库。
让我们演示如何使用 scikit-learn
中的 RidgeCV
回归和分类来预测价格变化。
生成数据集
让我们从生成以下示例所需的数据集开始—一个包含了 20 年每日数据的 Pandas DataFrame,其中包含了BookPressure
、TradePressure
、RelativeValue
和Microstructure
字段来表示一些基于该数据集构建的合成交易信号(也被称为PriceChange
字段代表我们试图预测的价格每日变化(也被称为PriceChange
字段一个线性函数,并带有一些随机权重和一些随机噪声。Price
字段代表使用pandas.Series.cumsum(...)
方法生成的工具的实际价格。 以下代码段中可以看到代码:
import numpy as np
import pandas as pd
df = pd.DataFrame(index=pd.date_range('2000', '2020'))
df['BookPressure'] = np.random.randn(len(df)) * 2
df['TradePressure'] = np.random.randn(len(df)) * 100
df['RelativeValue'] = np.random.randn(len(df)) * 50
df['Microstructure'] = np.random.randn(len(df)) * 10
true_coefficients = np.random.randint(low=-100, high=101,
size=4) / 10
df['PriceChange'] = ((df['BookPressure'] * true_coefficients[0])
+ (df['TradePressure'] * true_coefficients[1])
+ (df['RelativeValue'] * true_coefficients[2])
+ (df['Microstructure'] * true_coefficients[3])
+ (np.random.randn(len(df)) * 200))
df['Price'] = df['PriceChange'].cumsum(0) + 100000
让我们快速检查分配给我们四个特征的真实权重,如下所示:
true_coefficients
array([10\. , 6.2, -0.9, 5\. ])
让我们还检查包含所有数据的 DataFrame,如下所示:
Df
BookPressure TradePressure RelativeValue Microstructure PriceChange Price
2000-01-01 4.545869 -2.335894 5.953205 -15.025576 -263.749500 99736.250500
2000-01-02 -0.302344 -186.764283 9.150213 13.795346 -758.298833 98977.951667
... ... ... ... ... ... ...
2019-12-31 -1.890265 -113.704752 60.258456 12.229772 -295.295108 182827.332185
2020-01-01 1.657811 -77.354049 -39.090108 -3.294086 -204.576735 182622.755450
7306 rows × 6 columns
让我们视觉检查Price
字段,如下所示:
df['Price'].plot(figsize=(12, 6), color='black',
legend='Price')
图中显示了 20 年来以下逼真的价格演变:
图 6.19 – 合成数据集的价格图
让我们显示除Price
列之外的所有列的散点矩阵,如下所示:
pd.plotting.scatter_matrix(df.drop('Price', axis=1),
color='black', alpha=0.2,
grid=True, diagonal='kde',
figsize=(10, 10))
输出如下所示:
图 6.20 – 合成数据集的散点矩阵
散点矩阵显示PriceChange
与TradePressure
之间存在强关系。
在数据集上运行 RidgeCV 回归
让我们使用 scikit-learn 回归方法将线性回归模型拟合到我们的数据集。我们将使用四个特征尝试拟合和预测PriceChange
字段。
首先,我们将特征和目标收集到一个 DataFrame 和一个 Series 中,如下所示:
features = df[['BookPressure', 'TradePressure',
'RelativeValue', 'Microstructure']]
target = df['PriceChange']
我们将使用sklearn.linear_model.RidgeCV
,一个带有 L2 正则化的线性回归模型(使用 L2 范数惩罚因子以避免过拟合),该模型使用交叉验证学习最佳系数。我们将使用sklearn.linear_model.RidgeCV.fit(...)
方法使用特征拟合目标值。代码如下所示:
from sklearn.linear_model import RidgeCV
ridge = RidgeCV()
ridge.fit(features, target)
结果是一个RidgeCV
对象,如下所示:
RidgeCV(alphas=array([ 0.1, 1\. , 10\. ]), cv=None,
fit_intercept=True, gcv_mode=None,
normalize=False, scoring=None,
store_cv_values=False)
我们可以使用RidgeCV.coef_
属性访问Ridge
模型学到的权重/系数,并将其与实际系数进行比较,如下所示:
true_coefficients, ridge.coef_
模型学到的系数似乎非常接近真实权重,每个系数都有一些误差,如下所示:
(array([10\. , 6.2, -0.9, 5\. ]),
array([11.21856334, 6.20641632, -0.93444009, 4.94581522]))
RidgeCV.score(...)
方法返回 R2 分数,表示拟合模型的准确性,如下所示:
ridge.score(features, target)
这返回以下 R2 分数,最大值为 1,因此该模型相当适合数据:
0.9076861352499385
RidgeCV.predict(...)
方法输出预测的价格变化值,我们将其与pandas.Series.cumsum(...)
方法相结合,生成预测的价格系列,然后将其保存在PredPrice
字段中,如下所示:
df['PredPrice'] = \
ridge.predict(features).cumsum(0) + 100000; df
这将在我们的 DataFrame 中添加一个新列,如下所示:
... Price PredPrice
2000-01-01 ... 99736.250500 99961.011495
2000-01-02 ... 98977.951667 98862.549185
... ... ... ...
2019-12-31 ... 182827.332185 183059.625653
2020-01-01 ... 182622.755450 182622.755450
7306 rows × 7 columns
在以下代码块中,将真实的Price
字段与预测的PredPrice
字段一起绘制:
df['Price'].plot(figsize=(12, 6), color='gray',
linestyle='--', legend='Price')
df['PredPrice'].plot(figsize=(12, 6), color='black',
linestyle='-.', legend='PredPrice')
生成的图表,如下截图所示,显示PredPrice
大部分时间都跟踪Price
,但在某些时间段会出现预测误差:
图 6.21 – 原始价格与 Ridge 回归模型预测价格的比较图
我们可以缩小到 2010 年第一季度,检查预测误差,如下所示:
df['Price'].loc['2010-01-01':'2010-03-31']\
.plot(figsize=(12, 6), color='darkgray', linestyle='-',
legend='Price')
df['PredPrice'].loc['2010-01-01':'2010-03-31']\
.plot(figsize=(12, 6), color='black', linestyle='-.',
legend='PredPrice')
这产生了下面的图表,显示了那段时间内 Price
和 PredPrice
之间的差异:
图 6.22 – 比较 2010 年第一季度岭回归模型的原始价格和预测价格的图表
我们可以计算预测误差并使用密度图绘制它们,如下代码片段所示:
df['Errors'] = df['Price'] - df['PredPrice']
df['Errors'].plot(figsize=(12, 6), kind='kde',
color='black', legend='Errors')
这生成了下面截图中显示的图表,展示了错误的分布:
图 6.23 – 显示岭回归模型预测误差分布的图表
前面截图显示的错误图表表明错误没有明显的偏差。
在数据集上运行分类方法
让我们演示 scikit-learn 的分类方法。
首先,我们需要为分类模型创建离散分类目标标签以进行预测。我们分别给这些条件分配 -2
、-1
、0
、1
和 2
数值标签,并将离散目标标签保存在 target_discrete pandas.Series
对象中,如下所示:
target_discrete = pd.cut(target, bins=5,
labels = \
[-2, -1, 0, 1, 2]).astype(int);
target_discrete
结果显示如下:
2000-01-01 0
2000-01-02 -1
...
2019-12-28 -1
2019-12-29 0
2019-12-30 0
2019-12-31 0
2020-01-01 0
Freq: D, Name: PriceChange, Length: 7306, dtype: int64
我们可以使用以下代码可视化五个标签的分布:
target_discrete.plot(figsize=(12, 6), kind='hist',
color='black')
结果是一个频率图,如下截图所示,显示了五个标签的出现频率:
图 6.24 – 我们的离散目标-价格变化标签值 [-2, -1, 0, 1, 2] 的频率分布
对于分类,我们使用 sklearn.ensemble.RandomForestClassifier
提供的决策树分类器集合。随机森林是一种使用装袋集成方法的分类器,并通过对从原始数据集中进行带替换的随机抽样生成的数据集训练每棵树来构建决策树森林。使用 max_depth=5
参数,我们限制了每棵树的高度以减少过拟合,然后调用 RandomForestClassifier.fit(...)
方法来拟合模型,如下所示:
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(max_depth=5)
rf.fit(features, target_discrete)
这构建了以下 RandomForestClassifier
拟合模型:
RandomForestClassifier(
bootstrap=True, ccp_alpha=0.0, class_weight=None,
criterion='gini', max_depth=5, max_features='auto',
max_leaf_nodes=None, max_samples=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=100,
n_jobs=None, oob_score=False, random_state=None,
verbose=0, warm_start=False)
RandomForestClassifier.score(...)
方法返回预测与True
标签的平均准确度,如下所示:
rf.score(features, target_discrete)
正如我们在这里看到的,准确度分数为 83.5%,非常好:
0.835340815767862
我们向 DataFrame 添加 DiscretePriceChange
和 PredDiscretePriceChange
字段,以保存使用 RandomForestClassifier.predict(...)
方法的真实标签和预测标签,如下所示:
df['DiscretePriceChange'] = target_discrete
df['PredDiscretePriceChange'] = rf.predict(features)
df
结果如下 DataFrame,带有两个额外的字段:
... DiscretePriceChange PredDiscretePriceChange
2000-01-01 ... 0 0
2000-01-02 ... -1 -1
... ... ... ...
2019-12-31 ... 0 -1
2020-01-01 ... 0 -1
7306 rows × 10 columns
在下面的代码块中,我们绘制了 2010 年第一季度的两个字段:
df['DiscretePriceChange'].loc['2010-01-01':'2010-03-31'].plot(figsize=(12, 6), color='darkgray', linestyle='-', legend='DiscretePriceChange')
df['PredDiscretePriceChange'].loc['2010-01-01':'2010-03-31'].plot(figsize=(12, 6), color='black', linestyle='-.', legend='PredDiscretePriceChange')
这产生了一个图表,如下截图所示,其中True
和预测标签之间存在一些错位:
图 6.25 – 2010 年 Q1 的 RandomForest 分类模型原始和预测离散价格变动标签的比较
我们可以使用以下代码计算和绘制ClassificationErrors
DataFrame 的分布:
df['ClassificationErrors'] = \
df['DiscretePriceChange'] - df['PredDiscretePriceChange']
df['ClassificationErrors'].plot(figsize=(12, 6),
kind='kde', color='black',
legend='ClassificationErrors')
这产生了以下误差分布:
图 6.26 – RandomForest 分类器模型分类错误分布图
分类错误再次没有偏差,可以忽略不计。
摘要
所有先进的交易算法都使用统计模型,无论是用于直接交易规则还是只是决定何时进入/离开交易。在本章中,我们涵盖了 Python 的四个关键统计库——statsmodels
、pmdarima
、fbprophet
和 scikitlearn
。
在下一章中,我们将讨论如何将关键的金融和经济数据导入到 Python 中。
第三部分:Python 中的算法交易
本节教你如何在 Python 中获取市场数据,如何运行基本的算法交易回测,并详细描述了关键的算法交易算法。
本节包括以下章节:
-
第七章**,Python 中的金融市场数据访问
-
第八章**,Zipline 和 PyFolio 简介
-
第九章**,基础算法交易策略
第七章:Python 中的金融市场数据访问
本章概述了几个关键的市场数据源,从免费到付费的数据源都有涵盖。可从github.com/wilsonfreitas/awesome-quant#data-sources
获得更完整的可用资源列表。
算法交易模型信号的质量基本取决于正在分析的市场数据的质量。市场数据是否已清理出错误记录,并且是否有质量保证流程来在发生错误时更正任何错误?如果市场数据源有问题,那么数据可以多快被纠正?
下述描述的免费数据源适用于学习目的,但不适用于专业交易目的 - 每天的 API 调用次数可能非常有限,API 可能较慢,并且如果数据不正确,则没有支持和更正。此外,在使用任何这些数据提供者时,请注意其使用条款。
在本章中,我们将涵盖以下主要内容:
-
探索 yahoofinancials Python 库
-
探索 pandas_datareader Python 库
-
探索 Quandl 数据源
-
探索 IEX Cloud 数据源
-
探索 MarketStack 数据源
技术要求
本章中使用的 Python 代码可在书籍代码存储库的Chapter07/marketdata.ipynb
笔记本中找到。
探索 yahoofinancials Python 库
yahoofinancials Python 库提供了对雅虎财经市场数据的免费访问,其提供商是 ICE Data Services。库存储库位于github.com/JECSand/yahoofinancials
。
它提供以下资产的历史和大多数资产的实时定价数据访问:
-
货币
-
索引
-
股票
-
商品
-
ETF
-
共同基金
-
美国国债
-
加密货币
要找到正确的股票代码,请使用finance.yahoo.com/
上的查找功能。
每个 IP 地址每小时的调用次数有严格的限制(每小时每个 IP 地址约为 1,000-2,000 次请求),一旦达到限制,您的 IP 地址将被阻止一段时间。此外,提供的功能不断变化。
库的安装是标准的:
pip install yahoofinancials
访问数据非常简单,如下所示:
from yahoofinancials import YahooFinancials
该库支持单一股票检索和多个股票检索。
单一股票检索
单一股票检索的步骤如下:
-
首先,我们定义
AAPL
的股票对象:aapl = yf.Ticker("AAPL")
-
然后,还有历史数据检索的问题。让我们打印出 2020 年的所有历史每日价格数据:
hist = aapl.get_historical_price_data('2020-01-01', '2020-12-31', 'daily') print(hist)
输出以以下内容开始:
{'AAPL': {'eventsData': {'dividends': {'2020-02-07': {'amount': 0.1925, 'date': 1581085800, 'formatted_date': '2020-02-07'}, '2020-05-08': {'amount': 0.205, 'date': 1588944600, 'formatted_date': '2020-05-08'}, '2020-08-07': {'amount': 0.205, 'date': 1596807000, 'formatted_date': '2020-08-07'}, '2020-11-06': {'amount': 0.205, 'date': 1604673000, 'formatted_date': '2020-11-06'}}, 'splits': {'2020-08-31': {'date': 1598880600, 'numerator': 4, 'denominator': 1, 'splitRatio': '4:1', 'formatted_date': '2020-08-31'}}}, 'firstTradeDate': {'formatted_date': '1980-12-12', 'date': 345479400}, 'currency': 'USD', 'instrumentType': 'EQUITY', 'timeZone': {'gmtOffset': -18000}, 'prices': [{'date': 1577975400, 'high': 75.1500015258789, 'low': 73.79750061035156, 'open': 74.05999755859375, 'close': 75.0875015258789, 'volume': 135480400, 'adjclose': 74.4446029663086, 'formatted_date': '2020-01-02'}, {'date': 1578061800, 'high': 75.1449966430664, 'low': 74.125, 'open': 74.2874984741211, 'close': 74.35749816894531, 'volume': 146322800, 'adjclose': 73.72084045410156, 'formatted_date': '2020-01-03'}, {'date': 1578321000, 'high': 74.98999786376953, 'low': 73.1875, 'open': 73.44750213623047, 'close': 74.94999694824219, 'volume': 118387200, 'adjclose': 74.30826568603516, 'formatted_date': '2020-01-06'}, {'date': 1578407400, 'high': 75.2249984741211, 'low': 74.37000274658203, 'open': 74.95999908447266, 'close': 74.59750366210938, 'volume': 108872000, 'adjclose': 73.95879364013672, 'formatted_date': '2020-01-07'}, {'date': 1578493800, 'high': 76.11000061035156, 'low': 74.29000091552734, 'open': 74.29000091552734, 'close': 75.79750061035156, 'volume': 132079200, 'adjclose': 75.14852142333984, 'formatted_date': '2020-01-08'}, {'date': 1578580200, 'high': 77.60749816894531, 'low': 76.55000305175781, 'open': 76.80999755859375, 'close': 77.40750122070312, 'volume': 170108400, 'adjclose': 76.7447280883789, 'formatted_date': '2020-01-09'}, {'date': 1578666600, 'high': 78.1675033569336, 'low': 77.0625, 'open': 77.6500015258789, 'close': 77.5824966430664, 'volume': 140644800, 'adjclose': 76.91822052001953, 'formatted_date': '2020-01-10'}, {'date': 1578925800, 'high': 79.26750183105469, 'low': 77.7874984741211, 'open': 77.91000366210938, 'close': 79.23999786376953, 'volume': 121532000, 'adjclose': 78.56153106689453, 'formatted_date': '2020-01-13'}, {'date': 1579012200, 'high': 79.39250183105469, 'low': 78.0425033569336, 'open': 79.17500305175781, 'close': 78.16999816894531, 'volume': 161954400, 'adjclose': 77.50070190429688, 'formatted_date': '2020-01-14'}, {'date': 1579098600, 'high': 78.875, 'low': 77.38749694824219, 'open': 77.9625015258789, 'close': 77.83499908447266, 'volume': 121923600, 'adjclose': 77.16856384277344, 'formatted_date': '2020-01-15'}, {'date': 1579185000, 'high': 78.92500305175781, 'low': 78.02249908447266, 'open': 78.39749908447266, 'close': 78.80999755859375, 'volume': 108829200, 'adjclose': 78.13522338867188, 'formatted_date': '2020-01-16'}, {'date': 1579271400, 'high': 79.68499755859375, 'low': 78.75, 'open': 79.06749725341797, 'close': 79.68250274658203, 'volume': 137816400, 'adjclose': 79.000244140625, 'formatted_date': '2020-01-17'}, {'date': 1579617000, 'high': 79.75499725341797, 'low': 79.0, 'open': 79.29750061035156, 'close': 79.14250183105469, 'volume': 110843200, 'adjclose': 78.46488189697266, 'formatted_date': '2020-01-21'}, {'date': 1579703400, 'high': 79.99749755859375, 'low': 79.32749938964844, 'open': 79.6449966430664, 'close': 79.42500305175781, 'volume': 101832400, 'adjclose': 78.74495697021484, 'formatted_date': '2020-01-22'}, ...
注意
您可以将频率从
'daily'
更改为'weekly'
或'monthly'
。 -
现在,让我们查看每周数据结果:
hist = aapl.get_historical_price_data('2020-01-01', '2020-12-31', 'weekly') print(hist)
输出如下:
{'AAPL': {'eventsData': {'dividends': {'2020-02-05': {'amount': 0.1925, 'date': 1581085800, 'formatted_date': '2020-02-07'}, '2020-05-06': {'amount': 0.205, 'date': 1588944600, 'formatted_date': '2020-05-08'}, '2020-08-05': {'amount': 0.205, 'date': 1596807000, 'formatted_date': '2020-08-07'}, '2020-11-04': {'amount': 0.205, 'date': 1604673000, 'formatted_date': '2020-11-06'}}, 'splits': {'2020-08-26': {'date': 1598880600, 'numerator': 4, 'denominator': 1, 'splitRatio': '4:1', 'formatted_date': '2020-08-31'}}}, 'firstTradeDate': {'formatted_date': '1980-12-12', 'date': 345479400}, 'currency': 'USD', 'instrumentType': 'EQUITY', 'timeZone': {'gmtOffset': -18000}, 'prices': [{'date': 1577854800, 'high': 75.2249984741211, 'low': 73.1875, 'open': 74.05999755859375, 'close': 74.59750366210938, 'volume': 509062400, 'adjclose': 73.95879364013672, 'formatted_date': '2020-01-01'}, {'date': 1578459600, 'high': 79.39250183105469, 'low': 74.29000091552734, 'open': 74.29000091552734, 'close': 78.16999816894531, 'volume': 726318800, 'adjclose': 77.50070190429688, 'formatted_date': '2020-01-08'}, {'date': 1579064400, 'high': 79.75499725341797, 'low': 77.38749694824219, 'open': 77.9625015258789, 'close': 79.14250183105469, 'volume': 479412400, 'adjclose': 78.46488189697266, 'formatted_date': '2020-01-15'}, {'date': 1579669200, 'high': 80.8324966430664, 'low': 76.22000122070312, 'open': 79.6449966430664, 'close': 79.42250061035156, 'volume': 677016000, 'adjclose': 78.74247741699219, 'formatted_date': '2020-01-22'}, {'date': 1580274000, 'high': 81.9625015258789, 'low': 75.55500030517578, 'open': 81.11250305175781, 'close': 79.7125015258789, 'volume': 853162800, 'adjclose': 79.02999877929688, 'formatted_date': '2020-01-29'}, {'date': 1580878800, 'high': 81.30500030517578, 'low': 78.4625015258789, 'open': 80.87999725341797, 'close': 79.90249633789062, 'volume': 545608400, 'adjclose': 79.21836853027344, 'formatted_date': '2020-02-05'}, {'date': 1581483600, 'high': 81.80500030517578, 'low': 78.65249633789062, 'open': 80.36750030517578, 'close': 79.75, 'volume': 441122800, 'adjclose': 79.25482177734375, 'formatted_date': '2020-02-12'}, {'date': 1582088400, 'high': 81.1624984741211, 'low': 71.53250122070312, 'open': 80.0, 'close': 72.0199966430664, 'volume': 776972800, 'adjclose': 71.57282257080078, 'formatted_date': '2020-02-19'}, {'date': 1582693200, 'high': 76.0, 'low': 64.09249877929688, 'open': 71.63249969482422, 'close': 72.33000183105469, 'volume': 1606418000, 'adjclose': 71.88089752197266, 'formatted_date': '2020-02-26'}, {'date': 1583298000, 'high': 75.8499984741211, 'low': 65.75, 'open': 74.11000061035156, 'close': 71.33499908447266, 'volume': 1204962800, 'adjclose': 70.89207458496094, 'formatted_date': '2020-03-04'}, {'date': 1583899200, 'high': 70.3050003051757 ...
-
然后,我们检查月度数据结果:
hist = aapl.get_historical_price_data('2020-01-01', '2020-12-31', 'monthly') print(hist)
输出如下:
{'AAPL': {'eventsData': {'dividends': {'2020-05-01': {'amount': 0.205, 'date': 1588944600, 'formatted_date': '2020-05-08'}, '2020-08-01': {'amount': 0.205, 'date': 1596807000, 'formatted_date': '2020-08-07'}, '2020-02-01': {'amount': 0.1925, 'date': 1581085800, 'formatted_date': '2020-02-07'}, '2020-11-01': {'amount': 0.205, 'date': 1604673000, 'formatted_date': '2020-11-06'}}, 'splits': {'2020-08-01': {'date': 1598880600, 'numerator': 4, 'denominator': 1, 'splitRatio': '4:1', 'formatted_date': '2020-08-31'}}}, 'firstTradeDate': {'formatted_date': '1980-12-12', 'date': 345479400}, 'currency': 'USD', 'instrumentType': 'EQUITY', 'timeZone': {'gmtOffset': -18000}, 'prices': [{'date': 1577854800, 'high': 81.9625015258789, 'low': 73.1875, 'open': 74.05999755859375, 'close': 77.37750244140625, 'volume': 2934370400, 'adjclose': 76.7149887084961, 'formatted_date': '2020-01-01'}, {'date': 1580533200, 'high': 81.80500030517578, 'low': 64.09249877929688, 'open': 76.07499694824219, 'close': 68.33999633789062, 'volume': 3019851200, 'adjclose': 67.75486755371094, 'formatted_date': '2020-02-01'}, {'date': 1583038800, 'high': 76.0, 'low': 53.15250015258789, 'open': 70.56999969482422, 'close': 63 ...
-
嵌套的 JSON 可轻松转换为 pandas 的 DataFrame:
import pandas as pd hist_df = \ pd.DataFrame(hist['AAPL']['prices']).drop('date', axis=1).set_index('formatted_date') print(hist_df)
输出如下:
图 7.1 - 嵌套 JSON 转换为 pandas 的 DataFrame
注意两列 - adjclose
和close
。调整后的收盘价是根据股利、股票拆分和其他公司事件调整的收盘价。
实时数据检索
要获取实时股票价格数据,请使用get_stock_price_data()
函数:
print(aapl.get_stock_price_data())
输出如下:
{'AAPL': {'quoteSourceName': 'Nasdaq Real Time Price', 'regularMarketOpen': 137.35, 'averageDailyVolume3Month': 107768827, 'exchange': 'NMS', 'regularMarketTime': '2021-02-06 03:00:02 UTC+0000', 'volume24Hr': None, 'regularMarketDayHigh': 137.41, 'shortName': 'Apple Inc.', 'averageDailyVolume10Day': 115373562, 'longName': 'Apple Inc.', 'regularMarketChange': -0.42500305, 'currencySymbol': '$', 'regularMarketPreviousClose': 137.185, 'postMarketTime': '2021-02-06 06:59:58 UTC+0000', 'preMarketPrice': None, 'exchangeDataDelayedBy': 0, 'toCurrency': None, 'postMarketChange': -0.0800018, 'postMarketPrice': 136.68, 'exchangeName': 'NasdaqGS', 'preMarketChange': None, 'circulatingSupply': None, 'regularMarketDayLow': 135.86, 'priceHint': 2, 'currency': 'USD', 'regularMarketPrice': 136.76, 'regularMarketVolume': 72317009, 'lastMarket': None, 'regularMarketSource': 'FREE_REALTIME', 'openInterest': None, 'marketState': 'CLOSED', 'underlyingSymbol': None, 'marketCap': 2295940513792, 'quoteType': 'EQUITY', 'volumeAllCurrencies': None, 'postMarketSource': 'FREE_REALTIME', 'strikePrice': None, 'symbol': 'AAPL', 'postMarketChangePercent': -0.00058498, 'preMarketSource': 'FREE_REALTIME', 'maxAge': 1, 'fromCurrency': None, 'regularMarketChangePercent': -0.0030980287}}
免费数据源的实时数据通常延迟 10 到 30 分钟。
至于获取财务报表,让我们获取苹果股票的财务报表 - 损益表、现金流量表和资产负债表:
statements = aapl.get_financial_stmts('quarterly',
['income', 'cash',
'balance'])
print(statements)
输出如下:
{'incomeStatementHistoryQuarterly': {'AAPL': [{'2020-12-26': {'researchDevelopment': 5163000000, 'effectOfAccountingCharges': None, 'incomeBeforeTax': 33579000000, 'minorityInterest': None, 'netIncome': 28755000000, 'sellingGeneralAdministrative': 5631000000, 'grossProfit': 44328000000, 'ebit': 33534000000, 'operatingIncome': 33534000000, 'otherOperatingExpenses': None, 'interestExpense': -638000000, 'extraordinaryItems': None, 'nonRecurring': None, 'otherItems': None, 'incomeTaxExpense': 4824000000, 'totalRevenue': 111439000000, 'totalOperatingExpenses': 77905000000, 'costOfRevenue': 67111000000, 'totalOtherIncomeExpenseNet': 45000000, 'discontinuedOperations': None, 'netIncomeFromContinuingOps': 28755000000, 'netIncomeApplicableToCommonShares': 28755000000}}, {'2020-09-26': {'researchDevelopment': 4978000000, 'effectOfAccountingCharges': None, 'incomeBeforeTax': 14901000000, 'minorityInterest': None, 'netIncome': 12673000000, 'sellingGeneralAdministrative': 4936000000, 'grossProfit': ...
金融报表数据在算法交易中有多种用途。首先,它可用于确定要交易的股票的总体情况。其次,从非价格数据创建算法交易信号会增加额外的价值。
摘要数据检索
摘要数据可通过get_summary_data
方法获取:
print(aapl.get_summary_data())
输出如下:
{'AAPL': {'previousClose': 137.185, 'regularMarketOpen': 137.35, 'twoHundredDayAverage': 119.50164, 'trailingAnnualDividendYield': 0.0058825673, 'payoutRatio': 0.2177, 'volume24Hr': None, 'regularMarketDayHigh': 137.41, 'navPrice': None, 'averageDailyVolume10Day': 115373562, 'totalAssets': None, 'regularMarketPreviousClose': 137.185, 'fiftyDayAverage': 132.86455, 'trailingAnnualDividendRate': 0.807, 'open': 137.35, 'toCurrency': None, 'averageVolume10days': 115373562, 'expireDate': '-', 'yield': None, 'algorithm': None, 'dividendRate': 0.82, 'exDividendDate': '2021-02-05', 'beta': 1.267876, 'circulatingSupply': None, 'startDate': '-', 'regularMarketDayLow': 135.86, 'priceHint': 2, 'currency': 'USD', 'trailingPE': 37.092484, 'regularMarketVolume': 72317009, 'lastMarket': None, 'maxSupply': None, 'openInterest': None, 'marketCap': 2295940513792, 'volumeAllCurrencies': None, 'strikePrice': None, 'averageVolume': 107768827, 'priceToSalesTrailing12Months': 7.805737, 'dayLow': 135.86, 'ask': 136.7, 'ytdReturn': None, 'askSize': 1100, 'volume': 72317009, 'fiftyTwoWeekHigh': 145.09, 'forwardPE': 29.410751, 'maxAge': 1, 'fromCurrency': None, 'fiveYearAvgDividendYield': 1.44, 'fiftyTwoWeekLow': 53.1525, 'bid': 136.42, 'tradeable': False, 'dividendYield': 0.0061000003, 'bidSize': 2900, 'dayHigh': 137.41}}
使用此函数检索的摘要数据是财务报表函数和实时数据函数的摘要。
多股票检索
多股票检索,也称为批量检索,比单股票检索更高效快速,因为每个下载请求关联的大部分时间都用于建立和关闭网络连接。
历史数据检索
让我们获取这些外汇对的历史价格:EURCHF
、USDEUR
和GBPUSD
:
currencies = YahooFinancials(['EURCHF=X', 'USDEUR=X',
'GBPUSD=x'])
print(currencies.get_historical_price_data('2020-01-01',
'2020-12-31',
'weekly'))
输出如下:
{'EURCHF=X': {'eventsData': {}, 'firstTradeDate': {'formatted_date': '2003-01-23', 'date': 1043280000}, 'currency': 'CHF', 'instrumentType': 'CURRENCY', 'timeZone': {'gmtOffset': 0}, 'prices': [{'date': 1577836800, 'high': 1.0877000093460083, 'low': 1.0818699598312378, 'open': 1.0872000455856323, 'close': 1.084280014038086, 'volume': 0, 'adjclose': 1.084280014038086, 'formatted_date': '2020-01-01'}, {'date': 1578441600, 'high': 1.083299994468689, 'low': 1.0758999586105347, 'open': 1.080530047416687, 'close': 1.0809999704360962, 'volume': 0, 'adjclose': 1.0809999704360962, 'formatted_date': '2020-01-08'}, {'date': 1579046400, 'high': 1.0774999856948853, 'low': 1.0729299783706665, 'open': 1.076300024986267, 'close': 1.0744800567626953, 'volume': 0, 'adjclose': 1.0744800567626953, 'formatted_date': '2020-01-15'}, {'date': 1579651200, 'high': 1.0786099433898926, 'low': 1.0664700269699097, 'open': 1.0739500522613525, 'close': 1.068600058555603, 'volume': 0, 'adjclose': 1.068600058555603, 'formatted_date': '2020-01-22'}, {'date': 1580256000, 'high': 1.0736199617385864, 'low': 1.0663000345230103, 'open': 1.0723999738693237, 'close': 1.0683200359344482, 'volume': 0, 'adjclose': 1.068320035 ...
我们发现历史数据不包含任何财务报表数据。
写作本书时库支持的全部方法如下:
-
get_200day_moving_avg()
-
get_50day_moving_avg()
-
get_annual_avg_div_rate()
-
get_annual_avg_div_yield()
-
get_beta()
-
get_book_value()
-
get_cost_of_revenue()
-
get_currency()
-
get_current_change()
-
get_current_percent_change()
-
get_current_price()
-
get_current_volume()
-
get_daily_dividend_data(start_date, end_date)
-
get_daily_high()
-
get_daily_low()
-
get_dividend_rate()
-
get_dividend_yield()
-
get_earnings_per_share()
-
get_ebit()
-
get_exdividend_date()
-
get_financial_stmts(frequency, statement_type, reformat=True)
-
get_five_yr_avg_div_yield()
-
get_gross_profit()
-
get_historical_price_data(start_date, end_date, time_interval)
-
get_income_before_tax()
-
get_income_tax_expense()
-
get_interest_expense()
-
get_key_statistics_data()
-
get_market_cap()
-
get_net_income()
-
get_net_income_from_continuing_ops()
-
get_num_shares_outstanding(price_type='current')
-
get_open_price()
-
get_operating_income()
-
get_payout_ratio()
-
get_pe_ratio()
-
get_prev_close_price()
-
get_price_to_sales()
-
get_research_and_development()
-
get_stock_earnings_data(reformat=True)
-
get_stock_exchange()
-
get_stock_price_data(reformat=True)
-
get_stock_quote_type_data()
-
get_summary_data(reformat=True)
-
get_ten_day_avg_daily_volume()
-
get_three_month_avg_daily_volume()
-
get_total_operating_expense()
-
get_total_revenue()
-
get_yearly_high()
-
get_yearly_low()
我们将在下一部分中探索 pandas_datareader
库。
探索 pandas_datareader Python 库
pandas_datareader
是用于金融数据的最先进的库之一,提供对多个数据源的访问。
支持的一些数据源如下:
-
雅虎财经
-
圣路易斯联邦储备银行的 FRED
-
IEX
-
Quandl
-
Kenneth French 的数据库
-
世界银行
-
经济合作与发展组织
-
Eurostat
-
Econdb
-
纳斯达克交易员符号定义
参考pandas-datareader.readthedocs.io/en/latest/remote_data.html
以获取完整列表。
安装很简单:
pip install pandas-datareader
现在,让我们设置基本的数据检索参数:
from pandas_datareader import data
start_date = '2010-01-01'
end_date = '2020-12-31'
下载数据的一般访问方法是 data.DataReader(ticker, data_source, start_date, end_date)
。
访问雅虎财经
让我们下载 Apple 过去 10 年的股票价格:
aapl = data.DataReader('AAPL', 'yahoo', start_date,
end_date)
aapl
High Low Open Close Volume Adj Close
Date
2010-01-04 7.660714 7.585000 7.622500 7.643214 493729600.0 6.593426
2010-01-05 7.699643 7.616071 7.664286 7.656428 601904800.0 6.604825
2010-01-06 7.686786 7.526786 7.656428 7.534643 552160000.0 6.499768
2010-01-07 7.571429 7.466072 7.562500 7.520714 477131200.0 6.487752
2010-01-08 7.571429 7.466429 7.510714 7.570714 447610800.0 6.530883
... ... ... ... ... ... ...
2020-12 -21 128.309998 123.449997 125.019997 128.229996 121251600.0 128.229996
2020-12-22 134.410004 129.649994 131.610001 131.880005 168904800.0 131.880005
2020-12-23 132.429993 130.779999 132.160004 130.960007 88223700.0 130.960007
2020-12-24 133.460007 131.100006 131.320007 131.970001 54930100.0 131.970001
2020-12-28 137.339996 133.509995 133.990005 136.690002 124182900.0 136.690002
输出与前一部分中的 yahoofinancials
库的输出几乎相同。
访问 EconDB
可用股票标记列表在www.econdb.com/main-indicators
上可用。
让我们下载美国过去 10 年的月度石油产量时间序列:
oilprodus = data.DataReader('ticker=OILPRODUS', 'econdb',
start_date, end_date)
oilprodus
Reference Area United States of America
Energy product Crude oil
Flow breakdown Production
Unit of measure Thousand Barrels per day (kb/d)
TIME_PERIOD
2010-01-01 5390
2010-02-01 5548
2010-03-01 5506
2010-04-01 5383
2010-05-01 5391
... ...
2020-04-01 11990
2020-05-01 10001
2020-06-01 10436
2020-07-01 10984
2020-08-01 10406
每个数据源都有不同的输出列。
访问圣路易斯联邦储备银行的 FRED
可以在fred.stlouisfed.org/
检查可用数据列表和标记。
让我们下载美国过去 10 年的实际国内生产总值:
import pandas as pd
pd.set_option('display.max_rows', 2)
gdp = data.DataReader('GDP', 'fred', start_date, end_date)
gdp
我们将输出限制为只有两行:
GDP
DATE
2010-01-01 14721.350
... ...
2020-07-01 21170.252
43 rows × 1 columns
现在,让我们研究美国政府债券 20 年期恒久收益率的 5 年数据:
gs10 = data.get_data_fred('GS20')
gs10
GS20
DATE
2016-01-01 2.49
... ...
2020-11-01 1.40
59 rows × 1 columns
圣路易斯联邦储备银行的 FRED 数据是可用的最清洁的数据源之一,提供免费支持。
缓存查询
该库的一个关键优势是实现了查询结果的缓存,从而节省带宽,加快代码执行速度,并防止因 API 过度使用而禁止 IP。
举例来说,让我们下载 Apple 股票的全部历史数据:
import datetime
import requests_cache
session = \
requests_cache.CachedSession(cache_name='cache',
backend='sqlite',
expire_after = \
datetime.timedelta(days=7))
aapl_full_history = \
data.DataReader("AAPL",'yahoo',datetime.datetime(1980,1,1),
datetime.datetime(2020, 12, 31),
session=session)
aapl_full_history
High Low Open Close Volume Adj Close
Date
1980-12-12 0.128906 0.128348 0.128348 0.128348 469033600.0 0.101087
... ... ... ... ... ... ...
2020-12-28 137.339996 133.509995 133.990005 136.690002 124182900.0 136.690002
现在,让我们只访问一个数据点:
aapl_full_history.loc['2013-01-07']
High 18.903572
...
Adj Close 16.284145
Name: 2013-01-07 00:00:00, Length: 6, dtype: float64
缓存也可以为所有以前的示例启用。
探索 Quandl 数据源
Quandl 是互联网上最大的经济/金融数据存储库之一。其数据源可以免费访问。它还提供高级数据源,需要付费。
安装很简单:
pip install quandl
要访问数据,您必须提供访问密钥(在quandl.com
申请):
import quandl
quandl.ApiConfig.api_key = 'XXXXXXX'
要查找股票和数据源,请使用www.quandl.com/search
。
现在让我们下载法国大都市地区每月平均消费价格 - 苹果(1 公斤);欧元
数据:
papple = quandl.get('ODA/PAPPLE_USD')
papple
Value
Date
1998-01-31 1.735999
... ...
2020-11-30 3.350000
275 rows × 1 columns
现在让我们下载苹果公司的基本数据:
aapl_fundamental_data = quandl.get_table('ZACKS/FC',
ticker='AAPL')
m_ticker ticker comp_name comp_name_2 exchange currency_code per_end_date per_type per_code per_fisc_year ... stock_based_compsn_qd cash_flow_oper_activity_qd net_change_prop_plant_equip_qd comm_stock_div_paid_qd pref_stock_div_paid_qd tot_comm_pref_stock_div_qd wavg_shares_out wavg_shares_out_diluted eps_basic_net eps_diluted_net
None
0 AAPL AAPL APPLE INC Apple Inc. NSDQ USD 2018-09-30 A None 2018 ... NaN NaN NaN NaN None NaN 19821.51 20000.44 3.000 2.980
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
4 AAPL AAPL APPLE INC Apple Inc. NSDQ USD 2018-12-31 Q None 2019 ... 1559.0 26690.0 -3355.0 -3568.0 None -3568.0 18943.28 19093.01 1.055 1.045
5 rows × 249 columns
Yahoo 和 Quandl 数据之间的区别在于,Quandl 数据更可靠、更完整。
探索 IEX Cloud 数据源
IEX Cloud 是其中一个商业产品。它为个人提供每月 9 美元的计划。它还提供一个免费计划,每月限制为 50,000 次 API 调用。
Python 库的安装是标准的:
pip install iexfinance
完整的库文档可在addisonlynch.github.io/iexfinance/stable/index.html
上找到。
以下代码旨在检索所有符号:
from iexfinance.refdata import get_symbols
get_symbols(output_format='pandas', token="XXXXXX")
symbol exchange exchangeSuffix exchangeName name date type iexId region currency isEnabled figi cik lei
0 A NYS UN NEW YORK STOCK EXCHANGE, INC. Agilent Technologies Inc. 2020-12-29 cs IEX_46574843354B2D52 US USD True BBG000C2V3D6 0001090872 QUIX8Y7A2WP0XRMW7G29
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
9360 ZYXI NAS NASDAQ CAPITAL MARKET Zynex Inc 2020-12-29 cs IEX_4E464C4C4A462D52 US USD True BBG000BJBXZ2 0000846475 None
9361 rows × 14 columns
以下代码旨在获取苹果公司的资产负债表(免费账户不可用):
from iexfinance.stocks import Stock
aapl = Stock("aapl", token="XXXXXX")
aapl.get_balance_sheet()
以下代码旨在获取当前价格(免费账户不可用):
aapl.get_price()
以下代码旨在获取部门绩效报告(免费账户不可用):
from iexfinance.stocks import get_sector_performance
get_sector_performance(output_format='pandas',
token =token)
以下代码旨在获取苹果公司的历史市场数据:
from iexfinance.stocks import get_historical_data
get_historical_data("AAPL", start="20190101",
end="20200101",
output_format='pandas', token=token)
close high low open symbol volume id key subkey updated ... uLow uVolume fOpen fClose fHigh fLow fVolume label change changePercent
2019-01-02 39.48 39.7125 38.5575 38.7225 AAPL 148158948 HISTORICAL_PRICES AAPL 1606830572000 ... 154.23 37039737 37.8227 38.5626 38.7897 37.6615 148158948 Jan 2, 19 0.045 0.0011
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2019-12-31 73.4125 73.42 72.38 72.4825 AAPL 100990500 HISTORICAL_PRICES AAPL 1606830572000 ... 289.52 25247625 71.8619 72.7839 72.7914 71.7603 100990500 Dec 31, 19 0.5325 0.0073
252 rows × 25 columns
我们可以看到每个数据源提供了略有不同的输出列。
探索 MarketStack 数据源
MarketStack 提供跨主要全球股票交易所的实时、盘内和历史市场数据的广泛数据库。它为每月高达 1,000 次的 API 请求提供免费访问。
虽然没有官方的 MarketStack Python 库,但 REST JSON API 在 Python 中提供了对其所有数据的舒适访问。
让我们下载苹果公司的调整后收盘数据:
import requests
params = {
'access_key': 'XXXXX'
}
api_result = \
requests.get('http://api.marketstack.com/v1/tickers/aapl/eod', params)
api_response = api_result.json()
print(f"Symbol = {api_response['data']['symbol']}")
for eod in api_response['data']['eod']:
print(f"{eod['date']}: {eod['adj_close']}")
Symbol = AAPL
2020-12-28T00:00:00+0000: 136.69
2020-12-24T00:00:00+0000: 131.97
2020-12-23T00:00:00+0000: 130.96
2020-12-22T00:00:00+0000: 131.88
2020-12-21T00:00:00+0000: 128.23
2020-12-18T00:00:00+0000: 126.655
2020-12-17T00:00:00+0000: 128.7
2020-12-16T00:00:00+0000: 127.81
2020-12-15T00:00:00+0000: 127.88
2020-12-14T00:00:00+0000: 121.78
2020-12-11T00:00:00+0000: 122.41
2020-12-10T00:00:00+0000: 123.24
2020-12-09T00:00:00+0000: 121.78
2020-12-08T00:00:00+0000: 124.38
2020-12-07T00:00:00+0000: 123.75
2020-12-04T00:00:00+0000: 122.25
现在让我们下载纳斯达克证券交易所的所有股票代码:
api_result = \
requests.get('http://api.marketstack.com/v1/exchanges/XNAS/tickers', params)
api_response = api_result.json()
print(f"Exchange Name = {api_response['data']['name']}")
for ticker in api_response['data']['tickers']:
print(f"{ticker['name']}: {ticker['symbol']}")
Exchange Name = NASDAQ Stock Exchange
Microsoft Corp: MSFT
Apple Inc: AAPL
Amazoncom Inc: AMZN
Alphabet Inc Class C: GOOG
Alphabet Inc Class A: GOOGL
Facebook Inc: FB
Vodafone Group Public Limited Company: VOD
Intel Corp: INTC
Comcast Corp: CMCSA
PepsiCo Inc: PEP
Adobe Systems Inc: ADBE
Cisco Systems Inc: CSCO
NVIDIA Corp: NVDA
Netflix Inc: NFLX
MarketStack 的票务宇宙检索功能是最有价值的功能之一。所有回测的第一步之一是确定股票交易的宇宙(即完整列表)。然后,您可以通过仅交易具有某些趋势或某些交易量的股票等方式将自己限制在该列表的子集中。
概要
在本章中,我们概述了在 Python 中获取金融和经济数据的不同方法。在实践中,您通常同时使用多个数据源。我们探索了yahoofinancials
Python 库,并看到了单个和多个股票检索。然后,我们探索了pandas_datareader
Python 库,以访问 Yahoo Finance、EconDB 和 Fed 的 Fred 数据,并缓存查询。然后我们探索了 Quandl、IEX Cloud 和 MarketStack 数据源。
在下一章中,我们将介绍回测库 Zipline,以及交易组合绩效和风险分析库 PyFolio。
第八章:Zipline 和 PyFolio 简介
在本章中,您将了解到被称为 Zipline 和 PyFolio 的 Python 库,它们抽象出了算法交易策略的回测和性能/风险分析方面的复杂性。它们允许您完全专注于交易逻辑。
为此,我们将涵盖以下主要内容:
-
简介 Zipline 和 PyFolio
-
安装 Zipline 和 PyFolio
-
将市场数据导入 Zipline/PyFolio 回测系统
-
构建 Zipline/PyFolio 回测模块
-
查看关键 Zipline API 参考
-
从命令行运行 Zipline 回测
-
简介 PyFolio 提供的关键风险管理指标
技术要求
本章中使用的 Python 代码可在书籍代码库的 Chapter08/risk_management.ipynb
笔记本中找到。
简介 Zipline 和 PyFolio
回测是一种计算方法,用于评估如果将交易策略应用于历史数据,该策略将表现如何。理想情况下,这些历史数据应来自于一个具有类似市场条件的时期,例如具有类似于当前和未来的波动性。
回测应包括所有相关因素,如滑点和交易成本。
Zipline 是最先进的开源 Python 库之一,用于算法交易回测引擎。其源代码可在 github.com/quantopian/zipline
找到。Zipline 是一个适用于日常交易的回测库(也可以回测每周、每月等)。它不太适合回测高频交易策略。
PyFolio 是一个开源的 Python 性能和风险分析库,由金融投资组合组成,与 Zipline 紧密集成。您可以在 github.com/quantopian/pyfolio
找到其文档。
使用这两个库来回测您的交易策略可以节省大量时间。
本章的目标是描述这些库的关键功能并建立您的直觉。鼓励您在 PyCharm 或任何其他 Python IDE 中调试代码,并研究每个结果变量的内容以充分利用提供的信息。一旦您熟悉了每个结果对象的内容,简要地研究这些库的源代码以查看其全部功能。
安装 Zipline 和 PyFolio
我们建议按照 附录 A 中描述的方式设置开发环境。尽管如此,详细的说明在以下各节中给出。
安装 Zipline
出于性能原因,Zipline 严重依赖于特定版本的 Python 及其相关库。因此,最好的安装方式是在 conda
虚拟环境中创建并在那里安装 Zipline。我们建议使用 Anaconda Python 进行此操作。
让我们创建一个名为 zipline_env
的虚拟环境,使用 Python 3.6,并安装 zipline
包:
conda create -n zipline_env python=3.6
conda activate zipline_env
conda install -c conda-forge zipline
现在我们将安装 PyFolio。
安装 PyFolio
您可以通过 pip
安装 pyfolio
包:
pip install pyfolio
正如我们所见,安装 PyFolio 也是一项简单的任务。
将市场数据导入到 Zipline/PyFolio 回测系统中
回测依赖于我们拥有广泛的市场数据数据库。
Zipline 引入了两个与市场数据相关的术语 - bundle 和 ingest:
-
Bundle 是从自定义源逐步将市场数据导入到 Zipline 的专有数据库的接口。
-
Ingest 是将自定义源市场数据逐步导入到 Zipline 的专有数据库的实际过程;数据摄取不会自动更新。每次需要新鲜数据时,您都必须重新进行数据摄取。
默认情况下,Zipline 支持以下 bundle:
-
历史 Quandl bundle(2018 年之前的美国股票每日免费数据)
-
.csv
文件 bundle
现在我们将更详细地学习如何导入这两个 bundle。
从历史 Quandl bundle 导入数据
首先,在激活的 zipline_env
环境中,将 QUANDL_API_KEY
环境变量设置为您的免费(或付费)Quandl API 密钥。然后,进行 quandl
数据摄取。
对于 Windows,请使用以下代码:
SET QUANDL_API_KEY=XXXXXXXX
zipline ingest -b quandl
对于 Mac/Linux,请使用以下代码:
export QUANDL_API_KEY=XXXXXXXX
zipline ingest -b quandl
注意
Quandl 在 2018 年停止更新免费 bundle,但对于最初的几个算法交易步骤仍然非常有用。
最好在 Windows 的系统属性中设置 QUANDL_API_KEY
(按下 Windows 图标并键入 Environment Variables
):
图 8.1 – 在 Windows 上定位“编辑系统环境变量”对话框
然后,选择 编辑系统环境变量。
图 8.2 – 在 Windows 的系统属性中定位“环境变量...”对话框
然后,在环境变量...对话框中指定变量。
对于 Mac/Linux,将以下命令添加到 ~/.bash_profile
以进行基于用户的操作,或添加到 ~/.bashrc
以进行非登录交互式 shell:
export QUANDL_API_KEY=xxxx
现在,让我们学习如何从 CSV 文件 bundle 导入数据。
从 CSV 文件 bundle 导入数据
默认的 CSV bundle 要求 CSV 文件采用 开盘价、最高价、最低价、收盘价、成交量(OHLCV)格式,并带有日期、红利和拆分:
date,open,high,low,close,volume,dividend,split
本书的 GitHub 存储库包含一个示例输入 CSV 文件。其前几行如下所示:
date,open,high,low,close,volume,dividend,split
2015-05-15,18251.9707,18272.7207,18215.07031,18272.56055,108220000,0,0
2015-05-18,18267.25,18325.53906,18244.25977,18298.88086,79080000,0,0
2015-05-19,18300.48047,18351.35938,18261.34961,18312.39063,87200000,0,0
2015-05-20,18315.06055,18350.13086,18272.56055,18285.40039,80190000,0,0
2015-05-21,18285.86914,18314.89063,18249.90039,18285.74023,84270000,0,0
2015-05-22,18286.86914,18286.86914,18217.14063,18232.01953,78890000,0,0
2015-05-26,18229.75,18229.75,17990.01953,18041.53906,109440000,0,0
要使用自定义 CSV 文件 bundle,请按照以下步骤操作:
-
为 CSV 文件创建一个目录,例如
C:\MarketData
,其中包含一个名为Daily
的子目录。 -
将 CSV 文件复制到创建的目录中(例如
C:\MarketData\Daily
)。 -
在 Windows 的
C:\Users\<username>\.zipline\extension.py
目录或 Mac/Linux 的~/.zipline/extension.py
中编辑.py
文件扩展名,如下所示:import pandas as pd from zipline.data.bundles import register from zipline.data.bundles.csvdir import csvdir_equities register( 'packt-csvdir-bundle', csvdir_equities( ['daily'], 'c:/MarketData/', ), calendar_name='NYSE', start_session=pd.Timestamp('2015-5-15', tz='utc'), end_session=pd.Timestamp('2020-05-14', tz='utc') )
请注意,我们将市场数据与交易日历相关联。在这种情况下,我们使用的是
NYSE
,对应美国股票。 -
摄入捆绑包,如下所示:
zipline ingest -b packt-csvdir-bundle
输出如下:
图 8.3 – zipline 摄入 packt-csvdir-bundle 的输出
这已经创建了一个具有A
票据的资产。
从自定义捆绑包导入数据
历史 Quandl 捆绑包最适合学习如何设计和回测算法策略。CSV 文件捆绑包最适合导入没有公开价格的资产的价格。但是,对于其他用途,您应该购买市场数据订阅。
从 Quandl 的 EOD 美国股票价格数据导入数据
Quandl 提供每月 49 美元的 End of Day 美国股票价格数据库订阅服务(www.quandl.com/data/EOD-End-of-Day-US-Stock-Prices
),季度或年度付款可享受折扣。
与其他服务相比,该服务的优点如下:
-
Quandl 已深度集成到 Zipline 中,您可以使用一个命令下载所有股票的历史记录。
-
与其他提供商不同,每月您可以进行的 API 调用数量没有硬限制。
安装自定义捆绑包很简单:
-
使用以下命令找到
bundles
目录的位置:python -c "import zipline.data.bundles as bdl; print(bdl.__path__)"
在我的计算机上,这导致以下输出:
['d:\\Anaconda3\\envs\\zipline_env\\lib\\site-packages\\zipline\\data\\bundles']
-
将
quandl_eod.py
文件从本书的 GitHub 存储库复制到该目录中。该文件是对 Zipline 的 GitHub 上代码的轻微修改。 -
在相同的目录中,修改
__init__.py
文件(在那里添加这行):from . import quandl_eod # noqa
完整的__init__.py
文件示例如下:
# These imports are necessary to force module-scope register calls to happen.
from . import quandl # noqa
from . import csvdir # noqa
from . import quandl_eod # noqa
from .core import (
UnknownBundle,
bundles,
clean,
from_bundle_ingest_dirname,
ingest,
ingestions_for_bundle,
load,
register,
to_bundle_ingest_dirname,
unregister,
)
__all__ = [
'UnknownBundle',
'bundles',
'clean',
'from_bundle_ingest_dirname',
'ingest',
'ingestions_for_bundle',
'load',
'register',
'to_bundle_ingest_dirname',
'unregister',
]
安装完成后,请确保您已将QUANDL_API_KEY
环境变量设置为您的 API 密钥,并运行ingest
命令:
zipline ingest -b quandl_eod
输出如下:
图 8.4 – 摄入 quandl_eod 捆绑包的输出
quandl_eod.py
的实际源代码是不言自明的。带有@bundles.register("quandl_eod")
注解的quandl_eod_bundle
函数定义了下载过程:
@bundles.register("quandl_eod")
def quandl_eod_bundle(environ,
asset_db_writer,
minute_bar_writer,
daily_bar_writer,
adjustment_writer,
calendar,
start_session,
end_session,
cache,
show_progress,
output_dir):
"""
quandl_bundle builds a daily dataset using Quandl's WIKI Prices dataset.
For more information on Quandl's API and how to obtain an API key,
please visit https://docs.quandl.com/docs#section-authentication
"""
api_key = environ.get("QUANDL_API_KEY")
if api_key is None:
raise ValueError(
"Please set your QUANDL_API_KEY environment variable and retry."
)
raw_data = fetch_data_table(
api_key, show_progress,
environ.get("QUANDL_DOWNLOAD_ATTEMPTS", 5)
)
asset_metadata = \
gen_asset_metadata(raw_data[["symbol", "date"]],
show_progress)
asset_db_writer.write(asset_metadata)
symbol_map = asset_metadata.symbol
sessions = calendar.sessions_in_range(start_session,
end_session)
raw_data.set_index(["date", "symbol"], inplace=True)
daily_bar_writer.write(
parse_pricing_and_vol(raw_data, sessions,
symbol_map),
show_progress=show_progress,
)
raw_data.reset_index(inplace=True)
raw_data["symbol"] = \
raw_data["symbol"].astype("category")
raw_data["sid"] = raw_data.symbol.cat.codes
adjustment_writer.write(
splits=parse_splits(
raw_data[["sid", "date", "split_ratio"]].loc[raw_data.split_ratio != 1],
show_progress=show_progress,
),
dividends=parse_dividends(
raw_data[["sid", "date", "ex_dividend"]].loc[raw_data.ex_dividend != 0],
show_progress=show_progress,
),
)
参与此过程的步骤如下:
-
下载所有 EOD 数据。
-
生成元数据。
-
应用交易日历。
-
应用企业事件。
虽然 Quandl 的商业数据源已深度集成到 Zipline 中,但存在替代数据源。
从雅虎财经和 IEX 付费数据导入数据
该项目在 github.com/hhatefi/zipline_bundles
提供了一个用于 Yahoo Finance 和 IEX 的 Zipline bundle。该软件包支持从 Yahoo Finance 的 .csv
文件、直接从 Yahoo Finance 和从 IEX 导入 Zipline。本书仅专注于从 Yahoo Finance 和 IEX 直接导入。
虽然该软件包允许自动安装,但我不建议这样做,因为它要求在 Windows 的 C:\Users\<username>\.zipline\extension.py
目录或 Mac/Linux 的 ~/.zipline/extension.py
目录中有一个空的 extension.py
文件。
安装步骤如下:
-
从
github.com/hhatefi/zipline_bundles
下载该仓库。 -
将仓库的
\zipline_bundles-master\lib\extension.py
文件与 Windows 的C:\Users\<username>\.zipline\extension.py
或 Mac/Linux 的~/.zipline/extension.py
合并。如果后者文件不存在,只需复制并粘贴该文件。 -
在以下代码中编辑起始日期和结束日期:
register('yahoo_direct', # bundle's name direct_ingester('YAHOO', every_min_bar=False, symbol_list_env='YAHOO_SYM_LST', # the environment variable holding the comma separated list of assert names downloader=yahoo.get_downloader(start_date='2010-01-01', end_date='2020-01-01' ), ), calendar_name='NYSE', )
在以下代码中执行相同操作:
register('iex', # bundle's name direct_ingester('IEX Cloud', every_min_bar=False, symbol_list_env='IEX_SYM_LST', # the environemnt variable holding the comma separated list of assert names downloader=iex.get_downloader(start_date='2020-01-01', end_date='2020-01-05' ), filter_cb=lambda df: df[[cal.is_session(dt) for dt in df.index]] ), calendar_name='NYSE', )
完整文件应如下所示:
#!/usr/bin/env python # -*- coding: utf-8 -*- from pathlib import Path from zipline.data.bundles import register from zipline.data.bundles.ingester import csv_ingester # ingester.py need to be placed in zipline.data.bundles _DEFAULT_PATH = str(Path.home()/'.zipline/csv/yahoo') register( 'yahoo_csv', csv_ingester('YAHOO', every_min_bar=False, # the price is daily csvdir_env='YAHOO_CSVDIR', csvdir=_DEFAULT_PATH, index_column='Date', column_mapper={'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close', 'Volume': 'volume', 'Adj Close': 'price', }, ), calendar_name='NYSE', ) from zipline.data.bundles.ingester import direct_ingester from zipline.data.bundles import yahoo register('yahoo_direct', # bundle's name direct_ingester('YAHOO', every_min_bar=False, symbol_list_env='YAHOO_SYM_LST', # the environemnt variable holding the comma separated list of assert names downloader=yahoo.get_downloader(start_date='2010-01-01', end_date='2020-01-01' ), ), calendar_name='NYSE', ) from zipline.data.bundles import iex import trading_calendars as tc cal=tc.get_calendar('NYSE') register('iex', # bundle's name direct_ingester('IEX Cloud', every_min_bar=False, symbol_list_env='IEX_SYM_LST', # the environemnt variable holding the comma separated list of assert names downloader=iex.get_downloader(start_date='2020-01-01', end_date='2020-01-05' ), filter_cb=lambda df: df[[cal.is_session(dt) for dt in df.index]] ), calendar_name='NYSE', )
-
使用以下命令查找
bundles
目录的位置:python -c "import zipline.data.bundles as bdl; print(bdl.__path__)"
这将在我的计算机上产生以下输出:
['d:\\Anaconda3\\envs\\zipline_env\\lib\\site-packages\\zipline\\data\\bundles']
-
将
Copy \zipline_bundles-master\lib\iex.py
、\zipline_bundles-master\lib\ingester.py
和\zipline_bundles-master\lib\yahoo.py
仓库文件复制到您的 Ziplinebundles
目录;例如,d:\\Anaconda3\\envs\\zipline_env\\lib\\site-packages\\zipline\\data\\bundles\
。 -
将感兴趣的股票代码设置为环境变量。例如,在 Windows 上,使用以下代码:
set YAHOO_SYM_LST=GOOG,AAPL,GE,MSFT set IEX_SYM_LST=GOOG,AAPL,GE,MSFT
在 Mac/Linux 上,请使用以下代码:
export YAHOO_SYM_LST=GOOG,AAPL,GE,MSFT export IEX_SYM_LST=GOOG,AAPL,GE,MSFT
-
如果有可用的 IEX 令牌(以
sk_
开头),请在 Windows 上像这样设置:set IEX_TOKEN=xxx
对于 Mac/Linux,请执行以下操作:
export IEX_TOKEN=xxx
-
导入数据:
zipline ingest -b yahoo_direct zipline ingest -b iex
这将导致关于
yahoo_direct
bundle 的以下输出:
图 8.5 – 导入 yahoo_direct bundle 的输出
这也会导致以下输出,这是关于 iex
bundle 的:
图 8.6 – 导入 iex bundle 的输出
与其他数据源集成,例如本地 MySQL 数据库,类似于 github.com/hhatefi/zipline_bundles
中的代码。某些这样的 bundle 可在 github.com 上获得。
结构化 Zipline/PyFolio 回测模块
典型的 Zipline 回测代码定义了三个函数:
-
initialize
:此方法在任何模拟交易发生之前调用;它用于使用股票代码和其他关键交易信息丰富上下文对象。它还启用了佣金和滑点考虑。 -
handle_data
:此方法下载市场数据,计算交易信号并下单交易。这是您放置实际交易逻辑的地方,用于进入/退出仓位。 -
analyze
:调用此方法执行交易分析。在我们的代码中,我们将使用 pyfolio 的标准分析。请注意,pf.utils.extract_rets_pos_txn_from_zipline(perf)
函数返回任何返回、持仓和交易以进行自定义分析。
最后,代码定义了 run_algorithm
方法。此方法返回所有交易的综合摘要,以后可以分析。
在 Zipline 代码中,有几种典型的模式,具体取决于使用情况。
交易每天都会发生
让我们直接从 run_algorithm
方法中引用 handle_data
方法:
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol
from datetime import datetime
import pytz
import matplotlib.pyplot as plt
import pandas as pd
import pyfolio as pf
from random import random
def initialize(context):
pass
def handle_data(context, data):
pass
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('1996-1-1', utc=True)
end_date = pd.to_datetime('2020-12-31', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
handle_data
方法将在 start_date
和 end_date
之间的每一天调用。
交易发生在自定义的时间表上
我们省略了 run_algorithm
方法中对 handle_data
方法的引用。相反,我们在 initialize
方法中设置调度程序:
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission, schedule_function, date_rules, time_rules from datetime import datetime
import pytz
import matplotlib.pyplot as plt
import pandas as pd
import pyfolio as pf
from random import random
def initialize(context):
# definition of the stocks and the trading parameters, e.g. commission
schedule_function(handle_data, date_rules.month_end(),
time_rules.market_open(hours=1))
def handle_data(context, data):
pass
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('1996-1-1', utc=True)
end_date = pd.to_datetime('2020-12-31', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
handle_data
方法将在每个 month_end
后 1 小时的市场开盘后调用价格。
我们可以指定各种日期规则,如下所示:
图 8.7 – 包含各种日期规则的表格
类似地,我们可以指定时间规则,如下所示:
图 8.8 – 包含各种时间规则的表格
现在我们将学习如何查看关键的 Zipline API 参考。
查看关键的 Zipline API 参考
在本节中,我们将概述来自 www.zipline.io/appendix.html
的主要功能。
对于回测来说,订单类型、佣金模型和滑点模型是最重要的功能。让我们更详细地看看它们。
订单类型
Zipline 支持以下类型的订单:
图 8.9 – 支持的订单类型
下单逻辑通常放置在 handle_data
方法中。
以下是一个示例:
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
order_target_percent(context.stock, 1.0 if price_hist[-1] > price_hist.mean() else 0.0)
本示例根据最后一个每日价格是否高于收盘价格的平均值来下订单,以便我们拥有该股票的 100%。
佣金模型
佣金是券商为买卖股票而收取的费用。
Zipline 支持各种类型的佣金,如下所示:
图 8.10 – 支持的佣金类型
此逻辑通常放置在 initialize
方法中。
以下是一个示例:
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
在本例中,我们定义了每笔交易 5 美元的佣金。
滑点模型
滑点被定义为预期价格和执行价格之间的差异。
Zipline 提供以下滑点模型:
图 8.11 – 支持的滑点模型
滑点模型应放置在 initialize
方法中。
以下是一个示例:
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
set_slippage(VolumeShareSlippage(volume_limit=0.025,
price_impact=0.05))
在这个示例中,我们选择了VolumeShareSlippage
,限制为0.025
,价格影响为0.05
。
从命令行运行 Zipline 回测
对于大型回测任务,最好从命令行运行回测。
以下命令运行在 job.py
Python 脚本中定义的回测策略,并将结果 DataFrame 保存在 job_results.pickle
pickle 文件中:
zipline run -f job.py --start 2016-1-1 --end 2021-1-1 -o job_results.pickle --no-benchmark
例如,您可以设置一个批处理,其中包含几十个 Zipline 命令行作业,以便在夜间运行,并且每个都将结果存储在 pickle 文件中以供以后分析。
保持日志和过去的回测 pickle 文件库以便轻松参考是一个好的实践。
用 PyFolio 进行风险管理介绍
拥有风险管理系统是成功运行算法交易系统的基本组成部分。
算法交易涉及各种风险:
-
市场风险:虽然所有策略在其生命周期的某个阶段都会亏钱,但量化风险度量并确保存在风险管理系统可以缓解策略损失。在某些情况下,糟糕的风险管理可能会将交易损失增加到极端,并且甚至会完全关闭成功的交易公司。
-
监管风险:这种风险源于无意或有意违反法规。它旨在执行顺畅和公平的市场功能。一些众所周知的例子包括假单、报价填充和封闭。
-
软件实施风险:软件开发是一个复杂的过程,而复杂的算法交易策略系统尤其复杂。即使是看似微小的软件错误也可能导致算法交易策略失效,并产生灾难性结果。
-
操作风险:这种风险来自于部署和操作这些算法交易系统。操作/交易人员的错误也可能导致灾难性结果。这个类别中最著名的错误也许是“手指失误”,它指的是意外发送大量订单和/或以非预期价格的错误。
PyFolio 库提供了广泛的市场表现和风险报告功能。
典型的 PyFolio 报告如下所示:
图 8.12 - PyFolio 的标准输出显示回测摘要和关键风险统计数据
以下文本旨在解释此报告中的关键统计数据;即年度波动率、夏普比率和回撤。
为了本章的目的,让我们从一个假想的交易策略生成交易和收益。
以下代码块生成了一个具有轻微正偏差的交易策略的假设 PnL,以及没有偏差的假设头寸:
dates = pd.date_range('1992-01-01', '2012-10-22')
np.random.seed(1)
pnls = np.random.randint(-990, 1000, size=len(dates))
# slight positive bias
pnls = pnls.cumsum()
positions = np.random.randint(-1, 2, size=len(dates))
positions = positions.cumsum()
strategy_performance = \
pd.DataFrame(index=dates,
data={'PnL': pnls, 'Position': positions})
strategy_performance
PnL Position
1992-01-01 71 0
1992-01-02 -684 0
1992-01-03 258 1
... ... ...
2012-10-21 32100 -27
2012-10-22 32388 -26
7601 rows × 2 columns
让我们来审查一下 20 年内 PnL 的变化情况:
strategy_performance['PnL'].plot(figsize=(12,6), color='black', legend='PnL')
下面是输出:
图 8.13 - 显示带有轻微正偏差的合成生成的 PnL
这个图表证实了轻微的正偏差导致策略在长期内具有盈利性。
现在,让我们探索一些这个假设策略表现的风险指标。
市场波动性、PnL 方差和 PnL 标准偏差
市场波动性 定义为价格的标准偏差。通常,在更具波动性的市场条件下,交易策略的 PnL 也会经历更大的幅度波动。这是因为相同的持仓容易受到更大的价格波动的影响,这意味着 PnL 变化。
PnL 方差 用于衡量策略表现/回报的波动幅度。
计算 PnL 的标准偏差的代码与用于计算价格标准偏差(市场波动率)的代码相同。
让我们计算一个滚动 20 天期间的 PnL 标准偏差:
strategy_performance['PnLStdev'] = strategy_performance['PnL'].rolling(20).std().fillna(method='backfill')
strategy_performance['PnLStdev'].plot(figsize=(12,6),
color='black',
legend='PnLStdev')
输出如下:
图 8.14 - 显示一个 20 天滚动期间 PnL 标准偏差的图
这个图表证明了,在这种情况下,没有显著的模式 - 这是一个相对随机的策略。
交易级夏普比率
交易级夏普比率将平均 PnL(策略回报)与 PnL 标准偏差(策略波动性)进行比较。与标准夏普比率相比,交易级夏普比率假定无风险利率为 0,因为我们不滚动头寸,所以没有利息费用。这个假设对于日内或日常交易是现实的。
这个指标的优势在于它是一个单一的数字,考虑了所有相关的风险管理因素,因此我们可以轻松比较不同策略的表现。然而,重要的是要意识到夏普比率并不能讲述所有的故事,并且重要的是要与其他风险指标结合使用。
交易级夏普比率的定义如下:
让我们为我们的策略回报生成夏普比率。首先,我们将生成每日 PnL:
daily_pnl_series = strategy_performance['PnL'].shift(-1) - strategy_performance['PnL']
daily_pnl_series.fillna(0, inplace=True)
avg_daily_pnl = daily_pnl_series.mean()
std_daily_pnl = daily_pnl_series.std()
sharpe_ratio = avg_daily_pnl/std_daily_pnl
sharpe_ratio
0.007417596376703097
从直觉上讲,这个夏普比率是有意义的,因为假设策略的预期每日平均表现设置为 (1000-990)/2 = 1,000,根据这条线:
pnls = np.random.randint(-990, 1000, size=len(dates))
# slight positive bias
在实践中,夏普比率通常是年化的,以便我们可以更公平地比较不同期限的策略。要将计算出的每日收益的夏普比率年化,我们必须将其乘以 252 的平方根(一年中的交易日期数):
其代码如下:
annualized_sharpe_ratio = sharpe_ratio * np.sqrt(252)
annualized_sharpe_ratio
0.11775069203166105
现在,让我们解释夏普比率:
-
比率达到 3.0 或更高是极好的。
-
比率 > 1.5 非常好。
-
比率 > 1.0 是可以接受的。
-
比率 < 1.0 被认为是次优的。
现在我们将看看最大回撤。
最大回撤
最大回撤是一个交易策略在一段时间内累计 PnL 的峰值到谷底的下降。换句话说,它是与上一次已知的最大累计 PnL 相比损失资金的最长连续期。
这个指标量化了基于历史结果的交易账户价值的最坏情况下的下降。
让我们直观地找到假设策略表现中的最大回撤:
strategy_performance['PnL'].plot(figsize=(12,6),
color='black',
legend='PnL')
plt.axhline(y=28000, color='darkgrey', linestyle='--',
label='PeakPnLBeforeDrawdown')
plt.axhline(y=-15000, color='darkgrey', linestyle=':',
label='TroughPnLAfterDrawdown')
plt.vlines(x='2000', ymin=-15000, ymax=28000,
label='MaxDrawdown', color='black', linestyle='-.')
plt.legend()
这是输出结果:
图 8.15 – 显示峰值和谷底 PnL 以及最大回撤
从这张图中,我们可以评估到这个策略的最大回撤为 28K 至 2001 年的谷底 PnL 约 -43K 的亏损,我们需要意识到并为未来做好准备。
策略停止规则 – 止损线/最大损失
在开仓交易之前,设置止损线非常重要,止损线被定义为一种策略或投资组合(仅是一系列策略的集合)在被停止之前能够承受的最大损失次数。
可以使用历史最大回撤值来设置止损线。对于我们的假设性策略,我们发现在 20 年的时间内,实现的最大回撤为 43K 的止损值,如果未来损失这么多资金,就关闭它。在实践中,设置止损线要比这里描述的复杂得多,但这应该可以帮助您建立一些有关止损线的直觉。
一旦策略停止,我们可以决定永久关闭策略,或仅在一定时间内关闭策略,甚至关闭策略直到某些市场条件发生改变。这个决定取决于策略的行为和其风险容忍度。
总结
在本章中,我们学习了如何安装和设置基于 Zipline 和 PyFolio 的完整回测和风险/绩效分析系统。然后,我们将市场数据导入到 Zipline/PyFolio 回测投资组合中,并对其进行了结构化和审核。接着,我们研究了如何使用 PyFolio 管理风险并构建一个成功的算法交易系统。
在下一章中,我们将充分利用这一设置,并介绍几个关键的交易策略。
第九章:基础算法交易策略
这一章概述了几种算法,根据给定的股票、时间窗口和特定参数,旨在帮助您构思如何制定自己的交易策略。
在本章中,我们将讨论以下主题:
-
什么是算法交易策略?
-
学习基于动量的/趋势跟随策略
-
学习均值回归策略
-
学习基于数学模型的策略
-
学习基于时间序列预测的策略
技术要求
本章中使用的 Python 代码可在书籍代码存储库中的Chapter09/signals_and_strategies.ipynb
笔记本中找到。
什么是算法交易策略?
任何算法交易策略都应包含以下内容:
-
它应该是基于潜在市场理论的模型,因为只有这样才能发现其预测能力。将模型拟合到具有出色回测结果的数据中是简单的,但通常不能提供可靠的预测。
-
应尽可能简单 - 策略越复杂,长期表现越差(过度拟合)。
-
应将策略限制为一组明确定义的金融资产(交易宇宙),基于以下内容:
a) 他们的收益概况。
b) 他们的收益不相关。
c) 他们的交易模式 - 您不希望交易流动性不足的资产;您限制自己只交易交易活跃的资产。
-
应该定义相关的金融数据:
a) 频率:每日、每月、日内等等
b) 数据来源
-
应该定义模型的参数。
-
应定义它们的定时、入场、退出规则和头寸规模策略 - 例如,我们不能交易超过平均每日交易量的 10%;通常,进入/退出决策由几个指标的组合做出。
-
应该定义风险水平 - 单个资产能承受多大风险。
-
应该定义用于比较绩效的基准。
-
应该定义其再平衡政策 - 随着市场的发展,头寸大小和风险水平将偏离其目标水平,因此需要调整投资组合。
通常,您会拥有大量的算法交易策略库,回测将建议这些策略中的哪些策略,在哪些资产上以及何时可能获利。您应该保持一份回测日志,以跟踪哪些策略有效,哪些策略无效,以及在哪些股票和时间段内。
如何寻找要考虑交易的股票组合?选项如下:
-
使用 ETF/指数成分 - 例如,道琼斯工业平均指数的成员。
-
使用所有上市股票,然后将列表限制为以下内容:
a) 那些交易最多的股票
b) 只是非相关的股票
c) 使用收益模型(例如法玛-法 rench 三因子模型)表现不佳或表现良好的股票。
-
您应该将每支股票尽可能多地分类:
a) 价值/成长股
b) 按行业分类
每个交易策略都取决于许多参数。您如何找到每个参数的最佳值?可能的方法如下:
-
通过尝试每个参数的可能值范围内的每个可能值来进行参数扫描,但这将需要大量的计算资源。
-
很多时候,通过从可能值范围内测试许多随机样本,而不是所有值,进行参数扫描可以提供合理的近似。
要建立一个庞大的算法交易策略库,您应该执行以下操作:
-
订阅金融交易博客。
-
阅读金融交易书籍。
关键的算法交易策略可归类如下:
-
基于动量/趋势跟踪的策略
-
均值回归策略
-
基于数学模型的策略
-
套利策略
-
做市商策略
-
指数基金再平衡策略
-
交易时机优化策略(VWAP、TWAP、POV 等)
此外,您自己应根据最适合其工作环境的环境对所有交易策略进行分类 - 一些策略在波动较大且趋势明显的市场中表现良好,而另一些则不然。
以下算法使用免费获取的 Quandl 数据包;因此,最后的交易日期为 2018 年 1 月 1 日。
您应该积累许多不同的交易算法,列出可能的参数数量,并在股票宇宙中对许多参数进行回测(例如,那些平均交易量至少为X的股票)以查看哪些可能是有利可图的。回测应该在时间窗口内进行,例如,波动率制度。
阅读以下策略的最佳方式如下:
-
确定策略的信号公式,并考虑将其用于您自己的策略的入场/出场规则或与其他策略的组合 - 一些最赚钱的策略是现有策略的组合。
-
考虑交易频率 - 日常交易可能不适用于所有策略,因为交易成本较高。
-
每个策略适用于不同类型的股票及其市场 - 一些只适用于趋势股票,一些只适用于高波动性股票,等等。
学习基于动量/趋势跟踪的策略
基于动量/趋势跟踪的策略是技术分析策略的一种。它们假设近期的未来价格将遵循上升或下降趋势。
滚动窗口均值策略
此策略是如果最新的股票价格高于过去X天的平均价格,则拥有金融资产的最佳方法。
在以下示例中,它对苹果股票和 90 天的时间段效果良好:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
order_target_percent(context.stock, 1.0 if price_hist[-1] > price_hist.mean() else 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2000-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.1 - 滚动窗口平均策略;摘要回报和风险统计
在评估交易策略时,上述统计数据是第一步。每个都提供了策略表现的不同视角:
-
夏普比率:这是超额收益与超额收益标准差的比率。比率越高,算法在风险调整基础上表现越好。
-
Calmar 比率:这是平均复合年收益率与其最大回撤的比率。比率越高,算法在风险调整基础上表现越好。
-
稳定性:这是对累积对数收益的线性拟合的 R 平方值的定义。数字越高,累积收益的趋势就越高。
-
Omega 比率:这被定义为收益与损失的概率加权比率。这是夏普比率的一般化,考虑了分布的所有时刻。比率越高,算法在风险调整基础上表现越好。
-
Sortino 比率:这是夏普比率的一种变体 - 它仅使用负投资组合收益(下行风险)的标准偏差。比率越高,算法在风险调整基础上表现越好。
-
尾部比率:这被定义为右尾 95%与左尾 5%之间的比率。例如,1/3 的比率意味着损失是利润的三倍。数字越高,越好。
在这个例子中,我们看到策略在交易窗口上具有很高的稳定性(.92),这在一定程度上抵消了较高的最大回撤(-59.4%)。尾部比率最有利:
图 9.2 - 滚动窗口平均策略;最差的五次回撤期间
虽然 59.37%的最大回撤确实不好,但如果我们调整了入市/退出策略规则,我们很可能会避免它。请注意回撤期的持续时间 - 最大回撤期超过 3 年。
图 9.3 - 滚动窗口平均策略;投资视角下的累积回报
正如稳定性指标所证实的那样,我们在交易周期内看到累积收益呈正趋势。
图 9.4 - 滚动窗口平均策略;投资视角下的回报
图表证实了收益在零点周围波动很大。
图 9.5 - 滚动窗口平均策略;投资视角下的 6 个月滚动波动率
这张图表说明了策略的回报波动率在时间范围内在减少。
图 9.6 – 滚动窗口均值策略;投资视角下的 6 个月滚动夏普比率
我们看到该策略的最大夏普比率高达 4 以上,最小值低于 -2。如果我们审查进出场规则,应该能够提高策略的表现。
图 9.7 – 滚动窗口均值策略;投资视角下的前五个最差回撤期
最大回撤的图形表示表明,最大回撤期过长。
图 9.8 – 滚动窗口均值策略;投资视角下的月度收益、年度收益和月度收益分布
月度收益图表显示我们在大多数月份进行了交易。年度收益柱状图显示收益绝大部分为正值,而月度收益分布图显示右偏正态。
滚动窗口均值策略是最简单的策略之一,对于某些股票组合和时间范围仍然非常有利可图。请注意,该策略的最大回撤较大,如果我们添加了更高级的进出场规则,可能会改善。
简单移动平均线策略
该策略遵循一个简单的规则:如果短期移动平均线升破长期移动平均线,则买入股票:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
rolling_mean_short_term = \
price_hist.rolling(window=45, center=False).mean()
rolling_mean_long_term = \
price_hist.rolling(window=90, center=False).mean()
if rolling_mean_short_term[-1] > rolling_mean_long_term[-1]:
order_target_percent(context.stock, 1.0)
elif rolling_mean_short_term[-1] < rolling_mean_long_term[-1]:
order_target_percent(context.stock, 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2000-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.9 – 简单移动平均线策略;摘要收益和风险统计
统计数据显示,该策略在长期内非常有利可图(高稳定性和尾部比率),而最大回撤可能相当大。
图 9.10 – 简单移动平均线策略;前五个最差回撤期
最差的回撤期相当长 – 超过 335 天,甚至在最糟糕的情况下可能超过 3 年。
图 9.11 – 简单移动平均线策略;投资视角下的累积收益
然而,这张图表确实确认了这个长期策略是有利可图的 – 我们看到累积收益在第一次回撤后持续增长。
图表 9.12 – 简单移动平均线策略;投资期内回报
图表说明,在交易窗口的开头就发生了一次重大的负回报事件,然后回报围绕零波动。
图表 9.13 – 简单移动平均线策略;投资期内 6 个月滚动波动率
滚动波动率图表显示,滚动波动率随时间递减。
图表 9.14 – 简单移动平均线策略;投资期内 6 个月滚动夏普比率
虽然最大夏普比率超过了 4,最小值低于 -4,但平均夏普比率为 0.68。
图表 9.15 – 简单移动平均线策略;投资期内前五个最大回撤期
该图表证实了最大回撤期间非常长。
图表 9.16 – 简单移动平均线策略;月度回报、年度回报和投资期内月度回报分布
月度回报表显示,很多月份都没有交易。年度回报大部分为正数。月度回报分布图表证实了偏斜是负向的。
简单移动平均线策略的盈利能力较低,并且最大回撤比滚动窗口均值策略更大。一个可能的原因是移动平均的滚动窗口太大了。
指数加权移动平均线策略
该策略与之前的策略类似,唯一的区别是使用不同的滚动窗口和指数加权移动平均线。结果略优于之前策略下取得的结果。
一些其他移动平均算法在决策规则中同时使用简单移动平均线和指数加权移动平均线;例如,如果简单移动平均线大于指数加权移动平均线,则采取行动:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
rolling_mean_short_term = \
price_hist.ewm(span=5, adjust=True,
ignore_na=True).mean()
rolling_mean_long_term = \
price_hist.ewm(span=30, adjust=True,
ignore_na=True).mean()
if rolling_mean_short_term[-1] > rolling_mean_long_term[-1]:
order_target_percent(context.stock, 1.0)
elif rolling_mean_short_term[-1] < rolling_mean_long_term[-1]:
order_target_percent(context.stock, 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2000-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图表 9.17 – 指数加权移动平均策略;摘要回报和风险统计数据
结果显示,最大回撤水平从之前的策略中下降了,同时仍然保持非常强的稳定性和尾部比率。
图 9.18 – 指数加权移动平均策略;最糟糕的五个回撤期
最糟糕的回撤幅度以及其最长持续时间的大小,比前两种策略都要好得多。
图 9.19 – 指数加权移动平均策略;投资周期内的累计回报
如稳定性指标所示,我们看到持续的正累计回报。
图 9.20 – 指数加权移动平均策略;投资周期内的回报
回报在零附近波动,更多是正的而不是负的。
投资周期内的波动性](https://gitee.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/hsn-fin-trd-py/img/Figure_9.21_B15029.jpg)
图 9.21 – 指数加权移动平均策略;投资周期内的 6 个月滚动波动率
滚动波动率随着时间的推移而下降。
夏普比率投资周期内](https://gitee.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/hsn-fin-trd-py/img/Figure_9.22_B15029.jpg)
图 9.22 – 指数加权移动平均策略;投资周期内的 6 个月滚动夏普比率
我们看到,最大夏普比率几乎达到 5,而最小夏普比率略低于 -2,这再次比前两种算法要好。
图 9.23 – 指数加权移动平均策略;投资周期内的前五个回撤期
注意,最后三种算法的最糟糕回撤期不相同。
图 9.24 – 指数加权移动平均策略;投资周期内的月度回报、年度回报和月度回报分布
月度回报表显示,我们在大多数月份进行了交易。年度回报图表证实了大多数回报都是正的。月度回报分布图呈现正偏态,这是一个好的迹象。
在给定的时间范围内,指数加权移动平均策略对苹果股票表现更佳。然而,总的来说,最适合的平均数策略取决于股票和时间范围。
RSI 策略
该策略依赖于stockstats
包。阅读源代码非常有益,地址为github.com/intrad/stockstats/blob/master/stockstats.py
。
要安装它,请使用以下命令:
pip install stockstats
RSI 指标测量价格波动的速度和幅度,并在金融资产超买或超卖时提供指标。它是一个领先指标。
它的取值范围为 0 到 100,值超过 70 表示超买,值低于 30 表示超卖:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from stockstats import StockDataFrame as sdf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 20
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock,
["open", "high",
"low","close"],
context.rolling_window, "1d")
stock=sdf.retype(price_hist)
rsi = stock.get('rsi_12')
if rsi[-1] > 90:
order_target_percent(context.stock, 0.0)
elif rsi[-1] < 10:
order_target_percent(context.stock, 1.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2015-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.25 – RSI 策略;总结的收益和风险统计数据
对该策略的初步研究显示出优秀的夏普比率,最大回撤非常低,并且尾部比率有利。
图 9.26 – RSI 策略;最差的五个回撤期
最糟糕的回撤期非常短暂 – 不到 2 个月 – 且不重大 – 最大回撤仅为-10.55%。
图 9.27 – RSI 策略;投资期限内的累积收益
累积收益图表显示,我们在大部分交易期间都没有进行交易,而当我们进行交易时,累积收益呈正趋势。
图 9.28 – RSI 策略;投资期限内的收益
我们可以看到,在进行交易时,收益更可能为正而不是负。
图 9.29 – RSI 策略;投资期限内的 6 个月滚动波动率
注意到最大滚动波动率为 0.2,远低于先前策略。
图 9.30 – RSI 策略;投资期限内的 6 个月滚动夏普比率
我们可以看到夏普比率一直稳定在 1 以上,最大值超过 3,最小值低于-1。
图 9.31 – RSI 策略;投资期限内的前五个回撤期
图表显示了短暂且不显著的回撤期。
图 9.32 – RSI 策略;月度收益、年度收益以及投资期限内月度收益的分布
月度收益 表格显示,大多数月份我们都没有交易。但是,根据 年度收益 图表,在我们交易时,我们的利润非常巨大。 月度收益分布 图表证实了偏斜非常正向,峰度很大。
RSI 策略在给定时间范围内的苹果股票表现非常出色,夏普比率为 1.11。然而,请注意,该策略的成功很大程度上取决于非常严格的进出场规则,这意味着我们根本不会在某些月份进行交易。
MACD 交叉策略
移动平均线收敛背离(MACD)是一种滞后的、追踪趋势的动量指标,反映了股价两个移动平均线之间的关系。
该策略依赖于两个统计数据,即 MACD 和 MACD 信号线:
-
MACD 被定义为 12 天指数移动平均线和 26 天指数移动平均线之间的差异。
-
然后将 MACD 信号线定义为 MACD 的 9 天指数移动平均线。
MACD 交叉策略定义如下:
-
当 MACD 线向上转向并超过 MACD 信号线时,发生了牛市交叉。
-
当 MACD 线向下转向并穿过 MACD 信号线时,发生了空头交叉。
因此,这种策略最适合波动大、交易活跃的市场:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from stockstats import StockDataFrame as sdf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 20
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock,
["open","high",
"low","close"],
context.rolling_window, "1d")
stock=sdf.retype(price_hist)
signal = stock['macds']
macd = stock['macd']
if macd[-1] > signal[-1] and macd[-2] <= signal[-2]:
order_target_percent(context.stock, 1.0)
elif macd[-1] < signal[-1] and macd[-2] >= signal[-2]:
order_target_percent(context.stock, 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2015-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.33 – MACD 交叉策略;汇总收益和风险统计数据
尾部比率表明,最大收益和最大损失大致相当。非常低的稳定性表明,累计收益没有强劲的趋势。
图 9.34 – MACD 交叉策略;最糟糕的五个回撤期
除了最糟糕的回撤期外,其他时期都少于 6 个月,并且净回撤低于 10%。
图 9.35 – MACD 交叉策略;投资期限内的累计收益
累计收益 图表证实了低稳定性指标值。
以下是 Returns 图表:
图 9.36 – MACD 交叉策略;投资期限内的收益
Returns 图表显示,收益在零点周围波动幅度很大,有一些异常值。
以下是 滚动波动率 图表:
图 9.37 – MACD 交叉策略;投资期限内的 6 个月滚动波动率
滚动波动率一直在 0.15 左右波动。
以下是滚动夏普比率图表:
图 9.38 – MACD 交叉策略;投资周期内 6 个月滚动夏普比率
大约为 4 的最大滚动夏普比率,最小比率为 -2,大部分是有利的。
以下是前五个回撤期图表:
图 9.39 – MACD 交叉策略;投资周期内前五个最差的回撤期
我们看到最糟糕的两个回撤期相当长。
图 9.40 – MACD 交叉策略;月度收益、年度收益和投资周期内月度收益的分布
月度收益 表确认我们几乎在每个月都进行了交易。年度收益 图表表明最赚钱的一年是 2017 年。月度收益分布 图表显示了轻微的负偏斜和较大的峰度。
MACD 交叉策略在趋势市场中是一种有效的策略,可以通过提高入场/出场规则来显著改进。
RSI 和 MACD 策略
在这个策略中,我们将 RSI 和 MACD 策略结合起来,如果 RSI 和 MACD 标准都给出买入信号,就持有该股票。
使用多个标准可以更全面地了解市场(请注意,我们将 RSI 阈值通用化为 50):
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from stockstats import StockDataFrame as sdf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('MSFT')
context.rolling_window = 20
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock,
["open", "high",
"low","close"],
context.rolling_window, "1d")
stock=sdf.retype(price_hist)
rsi = stock.get('rsi_12')
signal = stock['macds']
macd = stock['macd']
if rsi[-1] < 50 and macd[-1] > signal[-1] and macd[-2] <= signal[-2]:
order_target_percent(context.stock, 1.0)
elif rsi[-1] > 50 and macd[-1] < signal[-1] and macd[-2] >= signal[-2]:
order_target_percent(context.stock, 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2015-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.41 – RSI 和 MACD 策略;摘要收益和风险统计
高稳定性值,高尾比率和优秀的夏普比率,以及低最大回撤,表明该策略是优秀的。
以下是最差的五个回撤期图表:
图 9.42 – RSI 和 MACD 策略;最差的五个回撤期
我们看到最差的回撤期很短 – 少于 4 个月 – 最差的净回撤为 -10.36%。
以下是累积收益 图表:
图 9.43 – RSI 和 MACD 策略;投资周期内的累积收益
高稳定性值是有利的。注意图表中的水平线;这些表示我们没有进行交易。
以下是收益 图表:
图 9.44 – RSI 和 MACD 策略;投资周期内的收益
收益图表显示,当我们进行交易时,正收益超过负收益。
以下是滚动波动率图表:
图 9.45 – RSI 和 MACD 策略;投资期内 6 个月滚动波动率
滚动波动率随时间递减,且相对较低。
以下是滚动夏普比率图表:
图 9.46 – RSI 和 MACD 策略;投资期内 6 个月滚动夏普比率
最大滚动夏普比率超过 3,最小值低于 -2,平均值超过 1.0,表明结果非常好。
以下是前五个最差回撤期图表:
图 9.47 – RSI 和 MACD 策略;投资期内前五个回撤期
我们可以看到回撤期较短且不显著。
以下是月度收益率、年度收益率和月度收益分布图表:
图 9.48 – RSI 和 MACD 策略;月度收益率、年度收益率和月度收益分布;投资期内
月度收益率表格证实我们大多数月份没有进行交易。然而,根据年度收益率图表,在我们进行交易时,利润非常可观。月度收益分布图表呈正态分布,峰度较高。
RSI 和 MACD 策略作为两种策略的组合,表现出优异的性能,夏普比率为 1.27,最大回撤为 -10.4%。需要注意的是,它在一些月份内没有触发任何交易。
三重指数平均线策略
三重指数平均线(TRIX)指标是围绕零线振荡的振荡器。正值表示市场超买,而负值表明市场超卖:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from stockstats import StockDataFrame as sdf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('MSFT')
context.rolling_window = 20
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock,
["open","high",
"low","close"],
context.rolling_window, "1d")
stock=sdf.retype(price_hist)
trix = stock.get('trix')
if trix[-1] > 0 and trix[-2] < 0:
order_target_percent(context.stock, 0.0)
elif trix[-1] < 0 and trix[-2] > 0:
order_target_percent(context.stock, 1.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2015-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.49 – TRIX 策略;摘要收益和风险统计
高尾部比率和高于平均水平的稳定性表明,总体上是一种盈利策略。
以下是最差的五个回撤期图表:
图 9.50 – TRIX 策略;前五个最差回撤期
第二最差的回撤期长达一年。最差净回撤为 -15.57%。
以下是累积收益率图表:
图 9.51 – TRIX 策略;投资周期内的累积回报率
累积回报率图表表明我们在许多月份没有进行交易(水平线),并且存在长期正向趋势,高稳定性值证实了这一点。
以下是回报图表:
图 9.52 – TRIX 策略;投资周期内的回报
此图表表明我们进行交易时更可能获得正回报。
以下是滚动波动率图表:
图 9.53 – TRIX 策略;投资周期内的 6 个月滚动波动率
滚动波动率图表显示,随着时间的推移,滚动波动率逐渐减小,尽管最大波动率相当高。
以下是滚动夏普比率图表:
图 9.54 – TRIX 策略;投资周期内的 6 个月滚动夏普比率
滚动夏普比率更可能为正值而不是负值,其最大值在 3 左右,最小值略低于 -1。
以下是前五个回撤期图表:
图 9.55 – TRIX 策略;投资周期内前五个回撤期
前五个回撤期证实了最糟糕的回撤期很长。
以下是月度回报,年度回报和月度回报分布图表:
图 9.56 – TRIX 策略;月度回报、年度回报和月度回报分布
月度回报表格证实我们在许多月份没有进行交易。年度回报图表显示,最大回报发生在 2015 年。月度回报分布图表显示略微正偏态和较大的峰度。
对于某些股票,如苹果,TRIX 策略在给定的时间范围内表现非常糟糕。对于其他股票,如在前述报告中包括的微软,某些年份的表现非常出色。
威廉斯 R% 策略
此策略由 Larry Williams 开发,William R% 在 0 到 -100 之间波动。stockstats
库已实现了从 0 到 +100 的值。
-20 以上的值表示证券被超买,而-80 以下的值表示证券被超卖。
对于微软的股票来说,这个策略非常成功,但对苹果的股票来说不太成功:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from stockstats import StockDataFrame as sdf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('MSFT')
context.rolling_window = 20
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock,
["open", "high",
"low","close"],
context.rolling_window, "1d")
stock=sdf.retype(price_hist)
wr = stock.get('wr_6')
if wr[-1] < 10:
order_target_percent(context.stock, 0.0)
elif wr[-1] > 90:
order_target_percent(context.stock, 1.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2015-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.57 – 威廉斯 R% 策略;摘要回报和风险统计
摘要统计显示出一个出色的策略 – 高稳定性证实了回报的一致性,具有很大的尾部比率,非常低的最大回撤和坚实的夏普比率。
以下是最差的五个回撤期的图表:
图 9.58 – 威廉斯 R% 策略;最差的五个回撤期
除了持续约 3 个月且净回撤为-10%的最糟糕的回撤期外,其他期间在持续时间和幅度上都不重要。
以下是累计回报图表:
图 9.59 – 威廉斯 R% 策略;投资期限内的累计回报率
该图表确认了策略的高稳定性价值 – 累计回报率以稳定的速度增长。
以下是回报图表:
图 9.60 – 威廉斯 R% 策略;投资期限内的回报率
回报图表表明,无论何时进行交易,盈利都比亏损多。
以下是滚动波动率图表:
图 9.61 – 威廉斯 R% 策略;投资期限内的 6 个月滚动波动率
滚动波动率图表显示,随着时间的推移,滚动波动率的值在减小。
以下是滚动夏普比率图表:
图 9.62 – 威廉斯 R% 策略;投资期限内的 6 个月滚动夏普比率
滚动夏普比率图表确认,夏普比率在交易期间始终为正值,最大值为 3.0。
以下是前五个回撤期的图表:
图 9.63 – 威廉斯 R% 策略;投资期限内的前五个回撤期
前 5 个回撤期图表显示,除了一个期间外,其他最糟糕的回撤期都不重要。
以下是月度回报、年度回报和月度回报分布图表:
图 9.64 – 威廉姆斯 R%策略;月回报、年回报以及投资期内月回报的分布
月回报表格表明,虽然我们并没有在每个月都交易,但每次交易时基本上都是盈利的。年回报图表证实了这一点。月回报的分布图表证实了一个具有大峰度的正偏斜。
威廉姆斯 R%策略是一种高性能策略,适用于微软股票,在给定的时间范围内夏普比率为 1.53,最大回撤仅为-10%。
学习均值回归策略
均值回归策略基于某些统计数据会回归到其长期均值的假设。
布林带策略
布林带策略基于识别短期波动期。
它依赖于三条线:
-
中间带线是简单移动平均线,通常是 20-50 天。
-
上轨是中间基准线的两个标准差以上。
-
下轨是中间基准线的两个标准差以下。
从布林带中创建交易信号的一种方法是定义超买和超卖的市场状态:
-
当金融资产的价格升破上轨时,市场处于超买状态,因此应该回调。
-
当金融资产的价格跌破下轨时,市场处于超卖状态,因此应该反弹。
这是一种均值回归策略,意味着长期来看,价格应该保持在下轨和上轨之间。对于低波动性股票来说,效果最佳。
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('DG')
context.rolling_window = 20
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
middle_base_line = price_hist.mean()
std_line = price_hist.std()
lower_band = middle_base_line - 2 * std_line
upper_band = middle_base_line + 2 * std_line
if price_hist[-1] < lower_band:
order_target_percent(context.stock, 1.0)
elif price_hist[-1] > upper_band:
order_target_percent(context.stock, 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2000-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.65 – 布林带策略;摘要回报和风险统计
摘要统计数据确实显示稳定性很好,尾部比率也很有利。然而,最大回撤是相当大的,为-27.3%。
以下是最差的五个回撤期图表:
图 9.66 – 布林带策略;最差的五个回撤期
最糟糕的回撤期持续时间相当长。也许我们应该调整入市/出市规则,以避免在这些时期进入交易。
以下是累积回报图表:
图 9.67 – 布林带策略;投资期内累积回报
累积回报图表显示我们已经有 10 年没有交易,然后我们经历了累积回报持续向上的一致趋势。
以下是回报图表:
图 9.68 – 布林带策略;投资视角下回报率
回报 图表显示正回报超过了负回报。
以下是 滚动波动率 图表:
图 9.69 – 布林带策略;投资视角下 6 个月滚动波动率
滚动波动率 图表表明该策略具有相当大的波动性。
以下是 滚动夏普比率 图表:
图 9.70 – 布林带策略;投资视角下 6 个月滚动夏普比率
滚动夏普比率 图表显示,滚动夏普比率波动范围很大,最大值接近 4,最小值低于 -2,但平均值为正。
以下是 前五次最大回撤期间 图表:
图 9.71 – 布林带策略;投资视角下前五次最大回撤期间
前五次最大回撤期间 图表证实回撤期间的持续时间相当长。
以下是 月度回报、年度回报 和 月度回报分布 图表:
图 9.72 – 布林带策略;投资视角下月度回报、年度回报和月度回报分布
月度回报 表明,由于我们的进出规则,从 2000 年到 2010 年没有进行任何交易。然而,年度回报 图表显示,每次交易发生时都是盈利的。月度回报分布 图表显示轻微的负偏态和巨大的峰态。
布林带策略是适用于波动较大的股票的策略。在这里,我们将其应用于Dollar General(DG)公司的股票。
对冲交易策略
这种策略在一段时间前变得非常流行,从那时起,就被过度使用,因此现在几乎没有盈利。
该策略涉及找到移动密切的股票对,或者高度协整的股票对。然后,同时为一只股票下达买入
订单,为另一只股票下达卖出
订单,假设它们之间的关系将恢复。在算法的实施方面,有各种各样的调整方法 - 价格是否是对数价格?只有关系非常紧密时我们才交易吗?
为了简单起见,我们选择了百事可乐(PEP)和可口可乐(KO)股票。另一个选择可以是花旗银行(C)和高盛(GS)。我们有两个条件:首先,协整的 p 值必须非常强大,然后 z 得分必须非常强大:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock_x = symbol('PEP')
context.stock_y = symbol('KO')
context.rolling_window = 500
set_commission(PerTrade(cost=5))
context.i = 0
def handle_data(context, data):
context.i += 1
if context.i < context.rolling_window:
return
try:
x_price = data.history(context.stock_x, "close",
context.rolling_window,"1d")
x = np.log(x_price)
y_price = data.history(context.stock_y, "close",
context.rolling_window,"1d")
y = np.log(y_price)
_, p_value, _ = coint(x, y)
if p_value < .9:
return
slope, intercept = sm.OLS(y, sm.add_constant(x, prepend=True)).fit().params
spread = y - (slope * x + intercept)
zscore = (\
spread[-1] - spread.mean()) / spread.std()
if -1 < zscore < 1:
return
side = np.copysign(0.5, zscore)
order_target_percent(context.stock_y,
-side * 100 / y_price[-1])
order_target_percent(context.stock_x,
side * slope*100/x_price[-1])
except:
pass
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2015-1-1', utc=True)
end_date = pd.to_datetime('2018-01-01', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.73 – 对冲交易策略;摘要回报和风险统计
虽然夏普比率非常低,但最大回撤也非常低。稳定性为中等。
以下是最差的五个回撤期图:
图 9.74 – 对冲交易策略;最差的五个回撤期
最差的五个回撤期表格显示,最大回撤微不足道且非常短暂。
以下是累积回报图:
图 9.75 – 对冲交易策略;投资期间内的累积回报
累积回报图表明,我们已经没有交易了两年,然后在最后一个期间获利颇丰。
以下是回报图:
图 9.76 – 对冲交易策略;投资期间内的回报
回报图显示,除了最后一个期间外,交易期间的回报都是正的。
以下是滚动波动率图:
图 9.77 – 对冲交易策略;投资期间内 6 个月滚动波动率
滚动波动率图显示,虽然波动率的幅度不显著,但波动率仍在不断增加。
以下是滚动夏普比率图:
图 9.78 – 对冲交易策略;投资期间内 6 个月滚动夏普比率
滚动夏普比率图显示,如果我们改进我们的退出规则并提前退出,我们的夏普比率将高于 1。
以下是前 5 个回撤期图:
图 9.79 – 对冲交易策略;投资期间内前五个回撤期
前 5 个回撤期图告诉我们同样的故事 – 最后一个期间是为什么这次回测结果并不像它本来可能那样成功的原因。
以下是月度回报,年度回报和月度回报分布图:
图 9.80 – 成对交易策略;月收益、年收益和投资周期内月收益分布
月收益 表格证实我们直到 2017 年才开始交易。年收益 图表显示了 2017 年的交易是成功的,而月收益分布 图表显示了一个略微负偏斜的图表,具有小的峰度。
在过去的十年中,成对交易策略已经被过度使用,因此利润较少。识别成对的一种简单方法是寻找竞争对手 —— 在这个例子中,是百事可乐公司和可口可乐公司。
学习基于数学模型的策略
我们现在将在以下部分中看各种基于数学模型的策略。
每月交易的组合波动率最小化策略
该策略的目标是最小化组合波动率。它受到了github.com/letianzj/QuantResearch/tree/master/backtest
的启发。
在以下示例中,投资组合包括 道琼斯工业平均指数 中的所有股票。
该策略的关键成功因素如下:
-
股票范围 —— 或许全球指数 ETF 组合会更好。
-
滚动窗口 —— 我们回溯 200 天。
-
交易频率 —— 以下算法使用每月交易 —— 注意构造。
代码如下:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission, schedule_function, date_rules, time_rules
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from scipy.optimize import minimize
import numpy as np
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stocks = [symbol('DIS'), symbol('WMT'),
symbol('DOW'), symbol('CRM'),
symbol('NKE'), symbol('HD'),
symbol('V'), symbol('MSFT'),
symbol('MMM'), symbol('CSCO'),
symbol('KO'), symbol('AAPL'),
symbol('HON'), symbol('JNJ'),
symbol('TRV'), symbol('PG'),
symbol('CVX'), symbol('VZ'),
symbol('CAT'), symbol('BA'),
symbol('AMGN'), symbol('IBM'),
symbol('AXP'), symbol('JPM'),
symbol('WBA'), symbol('MCD'),
symbol('MRK'), symbol('GS'),
symbol('UNH'), symbol('INTC')]
context.rolling_window = 200
set_commission(PerTrade(cost=5))
schedule_function(handle_data,
date_rules.month_end(),
time_rules.market_open(hours=1))
def minimum_vol_obj(wo, cov):
w = wo.reshape(-1, 1)
sig_p = np.sqrt(np.matmul(w.T,
np.matmul(cov, w)))[0, 0]
return sig_p
def handle_data(context, data):
n_stocks = len(context.stocks)
prices = None
for i in range(n_stocks):
price_history = \
data.history(context.stocks[i], "close",
context.rolling_window, "1d")
price = np.array(price_history)
if prices is None:
prices = price
else:
prices = np.c_[prices, price]
rets = prices[1:,:]/prices[0:-1, :]-1.0
mu = np.mean(rets, axis=0)
cov = np.cov(rets.T)
w0 = np.ones(n_stocks) / n_stocks
cons = ({'type': 'eq',
'fun': lambda w: np.sum(w) - 1.0},
{'type': 'ineq', 'fun': lambda w: w})
TOL = 1e-12
res = minimize(minimum_vol_obj, w0, args=cov,
method='SLSQP', constraints=cons,
tol=TOL, options={'disp': False})
if not res.success:
return;
w = res.x
for i in range(n_stocks):
order_target_percent(context.stocks[i], w[i])
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2010-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
capital_base = 10000,
data_frequency = 'daily'
bundle ='quandl')
输出如下:
图 9.81 – 组合波动率最小化策略;摘要回报和风险统计
结果是积极的 —— 见到强稳定性为 0.91
,而尾比率仅略高于 1。
注意结果包括交易成本,如果我们每日交易,结果会更糟。始终尝试最佳交易频率。
以下是最差的五个回撤期图表:
图 9.82 – 组合波动率最小化策略;最差的五个回撤期
最差的回撤期持续了一年,净回撤为 -18.22%。其他最差期间的净回撤幅度低于 -10%。
以下是 累积收益 图表:
图 9.83 – 组合波动率最小化策略;投资周期内的累积收益
我们看到累积收益持续增长,这是预期的,鉴于稳定性为 0.91。
以下是回报图表:
图 9.84 – 投资周期内投资组合波动率最小化策略; 回报
-0.3
至0.04
。
以下是滚动波动率图表:
图 9.85 – 投资周期内投资组合波动率最小化策略; 6 个月滚动波动率
0.18
以及滚动波动率约为0.1
。
以下是滚动夏普比率图表:
图 9.86 – 投资周期内投资组合波动率最小化策略; 6 个月滚动夏普比率
最小值为5.0
,最小值略高于-3.0
。
以下是前五次回撤期图表:
图 9.87 – 投资周期内投资组合波动率最小化策略; 前五次回撤期
前五次回撤期图表证实,如果我们通过更智能的进出规则避开最糟糕的回撤期,将极大地改善该策略的表现。
以下是月度回报、年度回报和月度回报分布图表:
图 9.88 – 投资周期内投资组合波动率最小化策略; 月度回报、年度回报和月度回报分布
月度回报表显示我们在 2010 年的前几个月没有交易。年度回报图表显示该策略每年都有盈利,但 2015 年除外。月度回报分布图表绘制了一个略微负偏态、小峰度的策略。
投资组合波动率最小化策略通常只对非日常交易有利。在这个例子中,我们采用了月度交易,实现了 0.93 的夏普比率,最大回撤为-18.2%。
月度交易的最大夏普比率策略
该策略基于哈利·马克维茨 1952 年的论文《投资组合选择》中的思想。简而言之,最佳投资组合位于有效边界上 - 一组在每个风险水平下具有最高预期投资组合回报的投资组合。
在该策略中,对于给定的股票,我们选择它们的权重,使其最大化投资组合的预期夏普比率 - 这样的投资组合位于有效边界上。
我们使用 PyPortfolioOpt
Python 库。要安装它,请使用本书提供的 conda
环境或以下命令:
pip install PyPortfolioOpt
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbols, set_commission, schedule_function, date_rules, time_rules
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import numpy as np
from pypfopt.efficient_frontier import EfficientFrontier
from pypfopt import risk_models
from pypfopt import expected_returns
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stocks = \
symbols('DIS','WMT','DOW','CRM','NKE','HD','V','MSFT',
'MMM','CSCO','KO','AAPL','HON','JNJ','TRV',
'PG','CVX','VZ','CAT','BA','AMGN','IBM','AXP',
'JPM','WBA','MCD','MRK','GS','UNH','INTC')
context.rolling_window = 252
set_commission(PerTrade(cost=5))
schedule_function(handle_data, date_rules.month_end(),
time_rules.market_open(hours=1))
def handle_data(context, data):
prices_history = data.history(context.stocks, "close",
context.rolling_window,
"1d")
avg_returns = \
expected_returns.mean_historical_return(prices_history)
cov_mat = risk_models.sample_cov(prices_history)
efficient_frontier = EfficientFrontier(avg_returns,
cov_mat)
weights = efficient_frontier.max_sharpe()
cleaned_weights = efficient_frontier.clean_weights()
for stock in context.stocks:
order_target_percent(stock, cleaned_weights[stock])
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2010-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出结果如下:
图 9.89 – 最大夏普比率策略;汇总收益和风险统计
该策略表现出稳定的稳定性,为 0.76
,尾部比率接近 1 (1.01
)。然而,该策略的年波动率非常高 (17.0%
)。
下面是最差五个回撤期图表:
图 9.90 – 最大夏普比率策略;最差五个回撤期
最差回撤期持续时间超过 2 年,净回撤幅度为 -21.14%。如果我们调整入场/出场规则以避免这个回撤期,结果将会大大改善。
下面是累积收益图表:
图 9.91 – 最大夏普比率策略;投资期内累积收益
累积收益图表显示了积极的稳定性。
下面是收益图表:
图 9.92 – 最大夏普比率策略;投资期内收益
收益图表显示该策略在投资期初非常成功。
下面是滚动波动率图表:
图 9.93 – 最大夏普比率策略;投资期内 6 个月滚动波动率
滚动波动率图表显示随着时间的推移,滚动波动率有所下降。
下面是滚动夏普比率图表:
图 9.94 – 最大夏普比率策略;投资期内 6 个月滚动夏普比率
5.0
,而其最小值高于 -3.0
。
下面是前五个回撤期图表:
图 9.95 – 最大夏普比率策略;投资期内前五个回撤期
前五个回撤期图表显示最大回撤期间很长。
下面是月度收益、年度收益和月度收益分布图表:
图 9.96 – 最大夏普比率策略;月度收益、年度收益以及投资期内月度收益的分布
月度收益表格证明我们几乎每个月都进行了交易。年度收益图表显示,除了 2016 年外,每年的年度收益都为正。月度收益分布图呈正偏态,具有轻微的峰度。
最大夏普比率策略通常只对非日常交易有利。
基于时间序列预测的策略学习
基于时间序列预测的策略取决于在未来某个时间点准确估计股票价格以及其相应置信区间。通常,估计的计算非常耗时。
简单交易规则则包括最后已知价格与未来价格或其下限/上限置信区间值之间的关系。
更复杂的交易规则包括基于趋势分量和季节性分量的决策。
SARIMAX 策略
该策略基于最基本的规则:如果当前价格低于预测的 7 天后价格,则持有股票:
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
import pmdarima as pm
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
try:
model = pm.auto_arima(price_hist, seasonal=True)
forecasts = model.predict(7)
order_target_percent(context.stock, 1.0 if price_hist[-1] < forecasts[-1] else 0.0)
except:
pass
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2017-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.97 – SARIMAX 策略;摘要收益和风险统计
在交易期内,该策略表现出很高的尾部比率1.95
,但稳定性很低,为0.25
。最大回撤率为-7.7%
,表现优异。
以下是最差五个回撤期的图表:
图 9.98 – SARIMAX 策略;最差五个回撤期
最差的回撤期显示了净回撤量低于-10%
的幅度。
以下是累积收益图表:
图 9.99 – SARIMAX 策略;投资期内累积收益
累积收益图表证明我们只在交易期的前半段进行了交易。
以下是收益图表:
图 9.100 – SARIMAX 策略;投资期内收益
收益图表显示,收益幅度的波动比其他策略大。
以下是滚动波动率图表:
图 9.101 – SARIMAX 策略;投资期内 6 个月滚动波动率
滚动波动率 图表显示,随着时间的推移,滚动波动率已经减少。
以下是 滚动夏普比率 图表:
图 9.102 - SARIMAX 策略;投资视角下的 6 个月滚动夏普比率
滚动夏普比率 图表显示,交易视角下前半段的夏普比率非常好,然后开始下降。
以下是 前 5 个回撤期间 图表:
图 9.103 - SARIMAX 策略;投资视角下前五个最糟糕的回撤期间
前 5 个回撤期间 图表显示,最糟糕的回撤期是整个交易窗口的后半段。
以下是 月度回报、年度回报 和 月度回报分布 图表:
图 9.104 - 月度回报、年度回报以及投资视角下月度回报的分布
月度回报 表格证实,我们在 2017 年下半年没有进行交易。年度回报 图表显示 2017 年的回报为正,并且 月度回报分布 图表呈现负偏态和大峰度。
在测试的时间范围内,SARIMAX 策略的进入规则并没有经常被触发。但是,它产生了夏普比率为 1.01,最大回撤为 -7.7%。
Prophet 策略
此策略基于预测置信区间,因此比以前的策略更加健壮。此外,Prophet 预测比 SARIMAX 更能应对频繁变化。回测结果完全相同,但是预测算法显著更好。
只有当最后价格低于置信区间的下限值时(我们预计股价将上涨)才购买股票,并且只有当最后价格高于预测置信区间的上限值时才卖出股票(我们预计股价将下跌):
%matplotlib inline
from zipline import run_algorithm
from zipline.api import order_target_percent, symbol, set_commission
from zipline.finance.commission import PerTrade
import pandas as pd
import pyfolio as pf
from fbprophet import Prophet
import logging
logging.getLogger('fbprophet').setLevel(logging.WARNING)
import warnings
warnings.filterwarnings('ignore')
def initialize(context):
context.stock = symbol('AAPL')
context.rolling_window = 90
set_commission(PerTrade(cost=5))
def handle_data(context, data):
price_hist = data.history(context.stock, "close",
context.rolling_window, "1d")
price_df = pd.DataFrame({'y' : price_hist}).rename_axis('ds').reset_index()
price_df['ds'] = price_df['ds'].dt.tz_convert(None)
model = Prophet()
model.fit(price_df)
df_forecast = model.make_future_dataframe(periods=7,
freq='D')
df_forecast = model.predict(df_forecast)
last_price=price_hist[-1]
forecast_lower=df_forecast['yhat_lower'].iloc[-1]
forecast_upper=df_forecast['yhat_upper'].iloc[-1]
if last_price < forecast_lower:
order_target_percent(context.stock, 1.0)
elif last_price > forecast_upper:
order_target_percent(context.stock, 0.0)
def analyze(context, perf):
returns, positions, transactions = \
pf.utils.extract_rets_pos_txn_from_zipline(perf)
pf.create_returns_tear_sheet(returns,
benchmark_rets = None)
start_date = pd.to_datetime('2017-1-1', utc=True)
end_date = pd.to_datetime('2018-1-1', utc=True)
results = run_algorithm(start = start_date, end = end_date,
initialize = initialize,
analyze = analyze,
handle_data = handle_data,
capital_base = 10000,
data_frequency = 'daily',
bundle ='quandl')
输出如下:
图 9.105 - Prophet 策略;摘要回报和风险统计
与 SARIMAX 策略相比,Prophet 策略显示出更好的结果 - 尾部比率为 1.37
,夏普比率为 1.22
,最大回撤为 -8.7%
。
以下是前五个最糟糕的回撤期间图表:
图 9.106 - Prophet 策略;前五个最糟糕的回撤期间
前五个最糟糕的回撤期间证实,最糟糕的净回撤幅度低于 10%。
以下是累积回报图表:
图 9.107 – 先知策略;投资周期内的累积回报
累积回报 图表显示,虽然我们在某些时间段没有进行交易,但入场/出场规则比 SARIMAX 策略更为稳健 – 对比两个累积回报图表。
以下是回报图表:
图 9.108 – 先知策略;投资周期内的回报
回报图表表明正回报超过了负回报。
以下是滚动波动率图表:
图 9.109 – 先知策略;投资周期内的 6 个月滚动波动率
滚动波动率 图表显示几乎恒定的滚动波动率 – 这是先知策略的特点。
以下是滚动夏普比率图表:
图 9.110 – 先知策略;投资周期内的 6 个月滚动夏普比率
-.50
和 1.5
。
以下是前 5 个回撤期图表:
图 9.111 – 先知策略;投资周期内前五个回撤期
前 5 个回撤期图表显示,尽管回撤期相当严重,但算法能够很好地处理它们。
以下是月度回报、年度回报和月度回报分布图表:
图 9.112 – 先知策略;月度回报、年度回报和月度回报分布
月度回报表格确认我们每个月都进行了交易,年度回报良好,如年度回报图表所示。月度回报分布图表呈正偏态,峰度较小。
先知策略是最稳健的策略之一,能够迅速适应市场变化。在给定的时间段内,它产生了 1.22 的夏普比率,最大回撤为 -8.7。
概要
在本章中,我们了解到,算法交易策略由模型、入场/离场规则、头寸限制以及其他关键属性定义。我们展示了在 Zipline 和 PyFolio 中设置完整的回测和风险分析/头寸分析系统是多么容易,这样你就可以专注于策略的开发,而不是浪费时间在基础设施上。
尽管前述策略已广为人知,但通过明智地组合它们,以及智能地选择入场和退出规则,你可以构建高度盈利的策略。
一帆风顺!
附录 A:如何设置 Python 环境
本书的 GitHub 存储库(github.com/PacktPublishing/Hands-On-Financial-Trading-with-Python/
)包含 Jupyter 笔记本,将帮助你复制此处显示的输出。
该环境是通过手动选择所有包含的软件包的兼容版本创建的。
第十章:技术要求
本书中的代码可以在 Windows、Mac 或 Linux 操作系统上运行。
初始设置
要设置 Python 环境,请按照以下步骤进行操作:
-
如果尚未安装 Anaconda Python,请从
www.anaconda.com/products/individual
下载并安装。 -
git clone
存储库:git clone XXXXX
-
将当前目录更改为克隆的 GitHub 存储库。
-
运行以下代码:
conda env create -f handson-algorithmic-trading-with-python\environment.yml -n handson-algorithmic-trading-with-python
-
更改活动环境:
conda activate handson-algorithmic-trading-with-python
-
设置市场访问的全局环境变量:
图 1 – 各种变量名及其免费令牌获取位置的表
-
使用 Windows 的控制面板,设置系统环境:
Export QUANDL_API_KEY=xxxx
关闭命令提示符,以激活全局环境变量。
-
继续执行
environment.yml
文件,该文件是在修复了一个软件包的元文件中的拼写错误后,使用conda env export > environmenmt.yml
命令生成的。
下载免费的 Quandl 数据包
步骤如下:
-
更改活动环境:
conda activate handson-algorithmic-trading-with-python
-
如果您尚未通过 Window 的控制面板或使用
.bash_profile
或.bashrc
设置过QUANDL_API_KEY
值,请设置它。对于 Windows,请使用以下命令:
SET QUANDL_API_KEY=XXXXXXXX
对于 Mac/Linux,请使用以下命令:
export QUANDL_API_KEY=XXXXXXXX
-
摄取数据:
zipline ingest -b quandl
注意
你不需要重复下载这个包。数据已经不再更新。
安装好环境后,请按照以下步骤进行操作:
-
将当前目录更改为克隆的 GitHub 存储库。
-
更改活动环境:
conda activate handson-algorithmic-trading-with-python
-
启动 Jupyter Lab,如下所示:
jupyter lab
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
2020-05-09 iBooker AI+财务提升星球 2020.4 热门讨论