当前位置:   article > 正文

Python 金融交易实用指南(三)

Python 金融交易实用指南(三)

原文:zh.annas-archive.org/md5/6efde0935976ca50d877b2b5774aeade

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:统计估计、推断和预测

在本章中,我们通过概述关键示例来介绍 Python 中的四个主要统计库—statsmodelspmdarimafbprophetscikitlearn。这些库用于对时间序列建模并提供它们的预测值,以及置信区间。此外,我们演示了如何使用分类模型来预测时间序列的百分比变化。

为此,我们将涵盖以下使用案例:

  • 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)
  • 1
  • 2
  • 3
  • 4

结果显示的图示了两个分布之间的非线性关系,这是预期的,因为我们使用了均匀分布:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.1 – 从均匀分布生成的数据集的 Q-Q 图

在下面的代码块中,我们重复测试,但这次使用 numpy.random.exponential(...) 分布作为我们的样本分布:

fig = qqplot(np.random.exponential(size=10000), line='s')
fig.set_size_inches(12, 6)
  • 1
  • 2

结果显示的 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)
  • 1
  • 2

结果是如预期的线性关系的图示,如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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)
  • 1
  • 2

这导致了以下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)]
  • 1
  • 2
  • 3
  • 4
  • 5

dates_from_range(...)方法中,日期的频率可以通过开始日期和一个特殊的格式来指定,其中m1后缀表示第一个月和月频率,q1表示第一个季度和季度频率,如下面的代码片段所示:

sm.tsa.datetools.dates_from_range('2010m1', length=120)
  • 1

这导致了以下月频率的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)]
  • 1
  • 2
  • 3
  • 4
  • 5

现在让我们对一个时间序列执行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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

结果是以下包含 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

让我们可视化我们生成的时间序列数据集,如下所示:

import matplotlib.pyplot as plt
dataset.plot(figsize=(12, 6), color='black')
  • 1
  • 2

生成的时间序列数据集具有明显的线性增长趋势,其中夹杂着季节性组件,如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.4 – 显示具有 ETS 组件的合成价格的图表

在上一张截图中,我们清楚地看到了季节性组件——从中位数值上下波动。我们还看到了误差噪声,因为波动不是完美的。最后,我们看到数值正在增加——趋势组件。

Hodrick-Prescott 滤波器

statsmodels中,这被实现为statsmodels.api.tsa.filters.hpfilter(...)

让我们使用 lamb=129600 平滑参数进行分解(值 129600 是月度数据的推荐值)。我们使用返回的一对系列值生成一个 DataFrame,其中包含 Pricehp_cyclehp_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在下一节中,我们将查看 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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

输出包含模型的详细信息,如下所示的代码块所示:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

我们可以使用 residcycle.smoothedlevel.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
  • 1
  • 2
  • 3
  • 4

decomp DataFrame 现在包含以下新列,其中包含来自 UnobservedComponents 模型的 CycleTrendError 项:

              ...        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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

接下来,我们将查看 statsmodel.tsa.seasonal.seasonal_decompose(…) 方法。

statsmodels.tsa.seasonal.seasonal_decompose(...) 方法

执行 ETS 分解的另一种方法是使用 statsmodels.tsa.seasonal.seasonal_decompose(...) 方法。

下面的代码块使用加法模型,通过指定 model='additive' 参数,并且通过访问 DecomposeResult 对象中的 seasontrendresid 属性,将 SDC_CycleSDC_TrendSDC_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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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
  • 1
  • 2
  • 3
  • 4
  • 5

接下来,我们将绘制前面各节得到的各种结果。

绘制 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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这给我们提供了以下图表,趋势组件与原始价格并排绘制。所有三个模型都很好地识别了总体上升的趋势,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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

以下结果显示,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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

输出如下屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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]))
  • 1
  • 2
  • 3
  • 4
  • 5

将其应用于Price字段时,输出如下值。 Test统计量为正值,p 值为 98%,这意味着有强有力的证据表明Price字段不是平稳的。我们知道这是预期的,因为Price字段中有强趋势和季节性组件:

Test Stat: 0.47882793726850786
p value: 0.9842151821849324
Lags: 14
Num observations: 225
  • 1
  • 2
  • 3
  • 4

第二步 - 对价格进行第一阶差分

接下来,我们应用第一阶差分转换;这从一个观测到下一个观测中找到第一个差异。如果我们再次对差分数据集进行差分,则会产生第二阶差分,依此类推。

我们将一阶差分的pandas.Series数据集存储在price_diff变量中,如下面的代码块所示:

price_diff = \
(dataset['Price'].shift(-1) - dataset['Price']).fillna(0)
price_diff
  • 1
  • 2
  • 3

该数据集包含以下数值:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

第三步 - 对价格进行差分的 ADF 测试

现在,我们对转换后的数据集重新运行 ADF 测试,以检查是否具有平稳性,如下所示:

result = adfuller(price_diff)
print('Test Stat: {}\np value: {}\nLags: {}\nNum \
      observations: {}'.format(result[0], result[1], 
                               result[2], result[3]))
  • 1
  • 2
  • 3
  • 4

现在,测试统计量具有较大的负值(值小于-4 的值具有非常高的平稳性可能性)。现在不平稳的概率现在降低到极低的值,表明转换后的数据集是平稳的,如以下代码片段所示:

Test Stat: -7.295184662866956
p value: 1.3839111942229784e-10
Lags: 15
Num observations: 224
  • 1
  • 2
  • 3
  • 4

时间序列的自相关和偏自相关

自相关或串行相关是观察值与延迟副本自身的关联性。它衡量当前观察值是否与未来/过去的值有关系。

在我们具有明显线性趋势和一些季节性组分的数据集中,随着滞后数的增加,自相关缓慢减小,但对于较小的滞后值,由于总体线性趋势较大,数据集具有较高的自相关值。statsmodels.graphics.tsaplots.plot_acf(...) 方法绘制了Price字段与滞后值从0100的自相关,如下代码片段所示:

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig = plot_acf(dataset['Price'], lags=100)
fig.set_size_inches(12, 6)
  • 1
  • 2
  • 3

结果表明,自相关在约 36 的滞后值附近仍然相对较强,在这里它低于 0.5。如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.8 – 自相关图显示自相关与不同滞后值的关系

statsmodels.graphics.tsaplots.plot_pacf(…) 方法让我们可以绘制偏自相关值与不同滞后值之间的关系图。自相关和偏自相关的区别在于,偏自相关只使用与前一个滞后期观测值的相关性,并且消除了低滞后值的相关性效应。该方法在以下代码片段中显示:

fig = plot_pacf(dataset['Price'], lags=100)
fig.set_size_inches(12, 6)
  • 1
  • 2

输出结果如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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=36d=1q=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()
  • 1
  • 2
  • 3
  • 4

以下输出描述了拟合参数:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

使用statsmodels.tsa.arima.ARIMAResults.predict(...)方法,我们可以使用拟合的模型预测指定起始和结束日期指数(在本例中是整个数据集)上的值。我们将预测的价格保存在PredPrice字段中,以便稍后进行比较。代码如下所示:

dataset['PredPrice'] = res_ar.predict(dataset.index[0], 
                                      dataset.index[-1])
dataset
  • 1
  • 2
  • 3

结果将添加新列并显示预测价格,如下所示:

                  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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

现在,我们将在下面的代码块中绘制原始PricePredPrice字段,以便进行视觉比较:

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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

预测价格相当准确,这是因为指定的参数(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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

接着,我们可以再次调用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

以下代码块绘制了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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这样就得到了一个包含预测的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
  • 1

自动 ARIMA 模型试图通过进行各种统计测试来自动发现 SARIMAX 参数,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.12 – 各种统计检验的表格。

一旦找到最佳的 d 值,auto-ARIMA 模型将在由 start_pmax_pstart_qmax_q 定义的范围内搜索最适合的模型。如果启用了 seasonal 参数,则一旦确定最佳的 D 值,我们就会使用类似的程序来找到 PQ

最佳模型通过最小化信息准则的值确定(阿卡奇信息准则 (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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

输出如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.13 – SARIMAX 结果来自自动 ARIMA 的统计数据。

图中显示如下的截图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.14 – 自动 ARIMA 模型预测的历史和预测价格预测。

输出还包括预测价格,如下所示:

             PredPrice
2020-01-31  194.939195
       ...         ...
2023-12-31  222.660698
[48 rows x 1 columns]
  • 1
  • 2
  • 3
  • 4
  • 5

另外,输出提供了每个预测价格的置信区间,如下所示:

[[192.39868933 197.4797007 ]
 [196.80033117 202.32443987]
 [201.6275806  207.60042584]
...
 [212.45091331 225.44676173]
 [216.11548707 229.20590827]]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在我们将看到使用 Facebook 的 Prophet 库进行时间序列预测。

使用 Facebook 的 Prophet 库进行时间序列预测。

Facebook Prophet 是一个用于预测单变量时间序列的 Python 库,对季节性和节假日效应提供了强大的支持。它特别适用于具有频繁变化趋势的时间序列,并且足够强大以处理异常值。

更具体地说,Prophet 模型是一个具有以下属性的加法回归模型:

  • 分段线性或逻辑增长趋势。

  • 年度季节性分量采用傅里叶级数模拟。

  • 用虚拟变量建模的每周季节性分量。

  • 用户提供的节假日列表

Prophet 的安装更加复杂,因为它需要编译器。 安装它的最简单方法是使用 Anaconda,如下所示:

conda install -c conda-forge fbprophet
  • 1

附带的 Git 存储库包含了带有 Prophetconda 环境设置。

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

预测值与 SARIMAX 模型非常相似,可以在此处看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.15 – Prophet 库的输出包括预测值,以及模型组件的值

预测值存储在 yhat 列中,其中包含了 yhat_loweryhat_upper 置信区间。

Prophet 确实生成了 Prophet 组件的图表,这对于理解模型的预测能力非常有用。 一个趋势组件图表可以在这里看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.16 – Prophet 模型的趋势组件图表

以下截图显示了年度季节性的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.17 – Prophet 模型的年度季节性组件图表

这是预测图表的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.18 – Prophet 模型的预测图表及置信区间

每个时间序列模型都略有不同,并且最适合不同类别的时间序列。 但总的来说,Prophet 模型非常稳健,并且在大多数情况下最容易使用。

scikit-learn 回归和分类简介

scikit-learn 是一个基于numpyscipy库构建的 Python 监督无监督 机器学习库。

让我们演示如何使用 scikit-learn 中的 RidgeCV 回归和分类来预测价格变化。

生成数据集

让我们从生成以下示例所需的数据集开始—一个包含了 20 年每日数据的 Pandas DataFrame,其中包含了BookPressureTradePressureRelativeValueMicrostructure字段来表示一些基于该数据集构建的合成交易信号(也被称为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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

让我们快速检查分配给我们四个特征的真实权重,如下所示:

true_coefficients
array([10\. ,  6.2, -0.9,  5\. ])
  • 1
  • 2

让我们还检查包含所有数据的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

让我们视觉检查Price字段,如下所示:

df['Price'].plot(figsize=(12, 6), color='black',
                 legend='Price')
  • 1
  • 2

图中显示了 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))
  • 1
  • 2
  • 3
  • 4

输出如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.20 – 合成数据集的散点矩阵

散点矩阵显示PriceChangeTradePressure之间存在强关系。

在数据集上运行 RidgeCV 回归

让我们使用 scikit-learn 回归方法将线性回归模型拟合到我们的数据集。我们将使用四个特征尝试拟合和预测PriceChange字段。

首先,我们将特征和目标收集到一个 DataFrame 和一个 Series 中,如下所示:

features = df[['BookPressure', 'TradePressure', 
               'RelativeValue', 'Microstructure']]
target = df['PriceChange']
  • 1
  • 2
  • 3

我们将使用sklearn.linear_model.RidgeCV,一个带有 L2 正则化的线性回归模型(使用 L2 范数惩罚因子以避免过拟合),该模型使用交叉验证学习最佳系数。我们将使用sklearn.linear_model.RidgeCV.fit(...)方法使用特征拟合目标值。代码如下所示:

from sklearn.linear_model import RidgeCV
ridge = RidgeCV()
ridge.fit(features, target)
  • 1
  • 2
  • 3

结果是一个RidgeCV对象,如下所示:

RidgeCV(alphas=array([ 0.1,  1\. , 10\. ]), cv=None,
                     fit_intercept=True, gcv_mode=None, 
                     normalize=False, scoring=None, 
                     store_cv_values=False)
  • 1
  • 2
  • 3
  • 4

我们可以使用RidgeCV.coef_属性访问Ridge模型学到的权重/系数,并将其与实际系数进行比较,如下所示:

true_coefficients, ridge.coef_
  • 1

模型学到的系数似乎非常接近真实权重,每个系数都有一些误差,如下所示:

(array([10\. ,  6.2, -0.9,  5\. ]),
 array([11.21856334, 6.20641632, -0.93444009, 4.94581522]))
  • 1
  • 2

RidgeCV.score(...)方法返回 R2 分数,表示拟合模型的准确性,如下所示:

ridge.score(features, target)
  • 1

这返回以下 R2 分数,最大值为 1,因此该模型相当适合数据:

0.9076861352499385
  • 1

RidgeCV.predict(...)方法输出预测的价格变化值,我们将其与pandas.Series.cumsum(...)方法相结合,生成预测的价格系列,然后将其保存在PredPrice字段中,如下所示:

df['PredPrice'] = \
ridge.predict(features).cumsum(0) + 100000; df
  • 1
  • 2

这将在我们的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在以下代码块中,将真实的Price字段与预测的PredPrice字段一起绘制:

df['Price'].plot(figsize=(12, 6), color='gray', 
                 linestyle='--', legend='Price')
df['PredPrice'].plot(figsize=(12, 6), color='black', 
                     linestyle='-.', legend='PredPrice')
  • 1
  • 2
  • 3
  • 4

生成的图表,如下截图所示,显示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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这产生了下面的图表,显示了那段时间内 PricePredPrice 之间的差异:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.22 – 比较 2010 年第一季度岭回归模型的原始价格和预测价格的图表

我们可以计算预测误差并使用密度图绘制它们,如下代码片段所示:

df['Errors'] = df['Price'] - df['PredPrice']
df['Errors'].plot(figsize=(12, 6), kind='kde', 
                  color='black', legend='Errors')
  • 1
  • 2
  • 3

这生成了下面截图中显示的图表,展示了错误的分布:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.23 – 显示岭回归模型预测误差分布的图表

前面截图显示的错误图表表明错误没有明显的偏差。

在数据集上运行分类方法

让我们演示 scikit-learn 的分类方法。

首先,我们需要为分类模型创建离散分类目标标签以进行预测。我们分别给这些条件分配 -2-1012 数值标签,并将离散目标标签保存在 target_discrete pandas.Series 对象中,如下所示:

target_discrete = pd.cut(target, bins=5, 
                         labels = \
                         [-2, -1, 0, 1, 2]).astype(int);
target_discrete
  • 1
  • 2
  • 3
  • 4

结果显示如下:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们可以使用以下代码可视化五个标签的分布:

target_discrete.plot(figsize=(12, 6), kind='hist', 
                     color='black')
  • 1
  • 2

结果是一个频率图,如下截图所示,显示了五个标签的出现频率:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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)
  • 1
  • 2
  • 3

这构建了以下 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

RandomForestClassifier.score(...) 方法返回预测与True标签的平均准确度,如下所示:

rf.score(features, target_discrete)
  • 1

正如我们在这里看到的,准确度分数为 83.5%,非常好:

0.835340815767862
  • 1

我们向 DataFrame 添加 DiscretePriceChangePredDiscretePriceChange 字段,以保存使用 RandomForestClassifier.predict(...) 方法的真实标签和预测标签,如下所示:

df['DiscretePriceChange'] = target_discrete
df['PredDiscretePriceChange'] = rf.predict(features)
df
  • 1
  • 2
  • 3

结果如下 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在下面的代码块中,我们绘制了 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')
  • 1
  • 2

这产生了一个图表,如下截图所示,其中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')
  • 1
  • 2
  • 3
  • 4
  • 5

这产生了以下误差分布:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6.26 – RandomForest 分类器模型分类错误分布图

分类错误再次没有偏差,可以忽略不计。

摘要

所有先进的交易算法都使用统计模型,无论是用于直接交易规则还是只是决定何时进入/离开交易。在本章中,我们涵盖了 Python 的四个关键统计库——statsmodelspmdarimafbprophetscikitlearn

在下一章中,我们将讨论如何将关键的金融和经济数据导入到 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
  • 1

访问数据非常简单,如下所示:

from yahoofinancials import YahooFinancials
  • 1

该库支持单一股票检索和多个股票检索。

单一股票检索

单一股票检索的步骤如下:

  1. 首先,我们定义AAPL的股票对象:

    aapl = yf.Ticker("AAPL")
    
    • 1
  2. 然后,还有历史数据检索的问题。让我们打印出 2020 年的所有历史每日价格数据:

    hist = aapl.get_historical_price_data('2020-01-01', 
                                          '2020-12-31', 
                                          'daily')
    print(hist)
    
    • 1
    • 2
    • 3
    • 4

    输出以以下内容开始:

    {'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'}, ... 
    
    • 1

    注意

    您可以将频率从'daily'更改为'weekly''monthly'

  3. 现在,让我们查看每周数据结果:

    hist = aapl.get_historical_price_data('2020-01-01', 
                                          '2020-12-31', 
                                          'weekly')
    print(hist)
    
    • 1
    • 2
    • 3
    • 4

    输出如下:

    {'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 ...
    
    • 1
  4. 然后,我们检查月度数据结果:

    hist = aapl.get_historical_price_data('2020-01-01', 
                                          '2020-12-31', 
                                          'monthly')
    print(hist)
    
    • 1
    • 2
    • 3
    • 4

    输出如下:

    {'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 ...
    
    • 1
  5. 嵌套的 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)
    
    • 1
    • 2
    • 3
    • 4

    输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7.1 - 嵌套 JSON 转换为 pandas 的 DataFrame

注意两列 - adjcloseclose。调整后的收盘价是根据股利、股票拆分和其他公司事件调整的收盘价。

实时数据检索

要获取实时股票价格数据,请使用get_stock_price_data()函数:

print(aapl.get_stock_price_data())
  • 1

输出如下:

{'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}}
  • 1

免费数据源的实时数据通常延迟 10 到 30 分钟。

至于获取财务报表,让我们获取苹果股票的财务报表 - 损益表、现金流量表和资产负债表:

statements = aapl.get_financial_stmts('quarterly', 
                                      ['income', 'cash', 
                                       'balance'])
print(statements)
  • 1
  • 2
  • 3
  • 4

输出如下:

{'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': ...
  • 1

金融报表数据在算法交易中有多种用途。首先,它可用于确定要交易的股票的总体情况。其次,从非价格数据创建算法交易信号会增加额外的价值。

摘要数据检索

摘要数据可通过get_summary_data方法获取:

print(aapl.get_summary_data())
  • 1

输出如下:

{'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}}
  • 1

使用此函数检索的摘要数据是财务报表函数和实时数据函数的摘要。

多股票检索

多股票检索,也称为批量检索,比单股票检索更高效快速,因为每个下载请求关联的大部分时间都用于建立和关闭网络连接。

历史数据检索

让我们获取这些外汇对的历史价格:EURCHFUSDEURGBPUSD

currencies = YahooFinancials(['EURCHF=X', 'USDEUR=X', 
                              'GBPUSD=x'])
print(currencies.get_historical_price_data('2020-01-01', 
                                           '2020-12-31', 
                                           'weekly'))
  • 1
  • 2
  • 3
  • 4
  • 5

输出如下:

{'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 ...
  • 1

我们发现历史数据不包含任何财务报表数据。

写作本书时库支持的全部方法如下:

  • 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
  • 1

现在,让我们设置基本的数据检索参数:

from pandas_datareader import data
start_date = '2010-01-01'
end_date = '2020-12-31'
  • 1
  • 2
  • 3

下载数据的一般访问方法是 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

输出与前一部分中的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

每个数据源都有不同的输出列。

访问圣路易斯联邦储备银行的 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
  • 1
  • 2
  • 3
  • 4

我们将输出限制为只有两行:

                  GDP
      DATE  
2010-01-01  14721.350
       ...        ...
2020-07-01  21170.252
43 rows × 1 columns
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在,让我们研究美国政府债券 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

圣路易斯联邦储备银行的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

现在,让我们只访问一个数据点:

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
  • 1
  • 2
  • 3
  • 4
  • 5

缓存也可以为所有以前的示例启用。

探索 Quandl 数据源

Quandl 是互联网上最大的经济/金融数据存储库之一。其数据源可以免费访问。它还提供高级数据源,需要付费。

安装很简单:

pip install quandl
  • 1

要访问数据,您必须提供访问密钥(在quandl.com申请):

import quandl
quandl.ApiConfig.api_key = 'XXXXXXX'
  • 1
  • 2

要查找股票和数据源,请使用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

现在让我们下载苹果公司的基本数据:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Yahoo 和 Quandl 数据之间的区别在于,Quandl 数据更可靠、更完整。

探索 IEX Cloud 数据源

IEX Cloud 是其中一个商业产品。它为个人提供每月 9 美元的计划。它还提供一个免费计划,每月限制为 50,000 次 API 调用。

Python 库的安装是标准的:

pip install iexfinance
  • 1

完整的库文档可在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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

以下代码旨在获取苹果公司的资产负债表(免费账户不可用):

from iexfinance.stocks import Stock
aapl = Stock("aapl", token="XXXXXX")
aapl.get_balance_sheet()
  • 1
  • 2
  • 3

以下代码旨在获取当前价格(免费账户不可用):

aapl.get_price()
  • 1

以下代码旨在获取部门绩效报告(免费账户不可用):

from iexfinance.stocks import get_sector_performance
get_sector_performance(output_format='pandas', 
                       token =token)
  • 1
  • 2
  • 3

以下代码旨在获取苹果公司的历史市场数据:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们可以看到每个数据源提供了略有不同的输出列。

探索 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

现在让我们下载纳斯达克证券交易所的所有股票代码:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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
  • 1
  • 2
  • 3

现在我们将安装 PyFolio。

安装 PyFolio

您可以通过 pip 安装 pyfolio 包:

pip install pyfolio
  • 1

正如我们所见,安装 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
  • 1
  • 2

对于 Mac/Linux,请使用以下代码:

export QUANDL_API_KEY=XXXXXXXX
zipline ingest -b quandl
  • 1
  • 2

注意

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
  • 1

现在,让我们学习如何从 CSV 文件 bundle 导入数据。

从 CSV 文件 bundle 导入数据

默认的 CSV bundle 要求 CSV 文件采用 开盘价、最高价、最低价、收盘价、成交量OHLCV)格式,并带有日期、红利和拆分:

date,open,high,low,close,volume,dividend,split
  • 1

本书的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

要使用自定义 CSV 文件 bundle,请按照以下步骤操作:

  1. 为 CSV 文件创建一个目录,例如C:\MarketData,其中包含一个名为Daily的子目录。

  2. 将 CSV 文件复制到创建的目录中(例如C:\MarketData\Daily)。

  3. 在 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')
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    请注意,我们将市场数据与交易日历相关联。在这种情况下,我们使用的是NYSE,对应美国股票。

  4. 摄入捆绑包,如下所示:

    zipline ingest -b packt-csvdir-bundle
    
    • 1

    输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 调用数量没有硬限制。

安装自定义捆绑包很简单:

  1. 使用以下命令找到bundles目录的位置:

    python -c "import zipline.data.bundles as bdl; print(bdl.__path__)"
    
    • 1

    在我的计算机上,这导致以下输出:

    ['d:\\Anaconda3\\envs\\zipline_env\\lib\\site-packages\\zipline\\data\\bundles']
    
    • 1
  2. quandl_eod.py文件从本书的 GitHub 存储库复制到该目录中。该文件是对 Zipline 的 GitHub 上代码的轻微修改。

  3. 在相同的目录中,修改__init__.py文件(在那里添加这行):

    from . import quandl_eod  # noqa
    
    • 1

完整的__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',
] 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

安装完成后,请确保您已将QUANDL_API_KEY环境变量设置为您的 API 密钥,并运行ingest命令:

zipline ingest -b quandl_eod
  • 1

输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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,
        ),
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

参与此过程的步骤如下:

  1. 下载所有 EOD 数据。

  2. 生成元数据。

  3. 应用交易日历。

  4. 应用企业事件。

虽然 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 文件。

安装步骤如下:

  1. github.com/hhatefi/zipline_bundles 下载该仓库。

  2. 将仓库的 \zipline_bundles-master\lib\extension.py 文件与 Windows 的 C:\Users\<username>\.zipline\extension.py 或 Mac/Linux 的 ~/.zipline/extension.py 合并。如果后者文件不存在,只需复制并粘贴该文件。

  3. 在以下代码中编辑起始日期和结束日期:

    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',
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在以下代码中执行相同操作:

    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',
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    完整文件应如下所示:

    #!/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',
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
  4. 使用以下命令查找 bundles 目录的位置:

    python -c "import zipline.data.bundles as bdl; print(bdl.__path__)"
    
    • 1

    这将在我的计算机上产生以下输出:

    ['d:\\Anaconda3\\envs\\zipline_env\\lib\\site-packages\\zipline\\data\\bundles']
    
    • 1
  5. Copy \zipline_bundles-master\lib\iex.py\zipline_bundles-master\lib\ingester.py\zipline_bundles-master\lib\yahoo.py 仓库文件复制到您的 Zipline bundles 目录;例如,d:\\Anaconda3\\envs\\zipline_env\\lib\\site-packages\\zipline\\data\\bundles\

  6. 将感兴趣的股票代码设置为环境变量。例如,在 Windows 上,使用以下代码:

        set YAHOO_SYM_LST=GOOG,AAPL,GE,MSFT
        set IEX_SYM_LST=GOOG,AAPL,GE,MSFT
    
    • 1
    • 2

    在 Mac/Linux 上,请使用以下代码:

        export YAHOO_SYM_LST=GOOG,AAPL,GE,MSFT
        export IEX_SYM_LST=GOOG,AAPL,GE,MSFT
    
    • 1
    • 2
  7. 如果有可用的 IEX 令牌(以 sk_ 开头),请在 Windows 上像这样设置:

    set IEX_TOKEN=xxx
    
    • 1

    对于 Mac/Linux,请执行以下操作:

    export IEX_TOKEN=xxx
    
    • 1
  8. 导入数据:

    zipline ingest -b yahoo_direct
    zipline ingest -b iex
    
    • 1
    • 2

    这将导致关于 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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

handle_data 方法将在 start_dateend_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')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

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) 
  • 1
  • 2
  • 3
  • 4

本示例根据最后一个每日价格是否高于收盘价格的平均值来下订单,以便我们拥有该股票的 100%。

佣金模型

佣金是券商为买卖股票而收取的费用。

Zipline 支持各种类型的佣金,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8.10 – 支持的佣金类型

此逻辑通常放置在 initialize 方法中。

以下是一个示例:

def initialize(context): 
    context.stock = symbol('AAPL')
    context.rolling_window = 90
    set_commission(PerTrade(cost=5)) 
  • 1
  • 2
  • 3
  • 4

在本例中,我们定义了每笔交易 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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这个示例中,我们选择了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
  • 1

例如,您可以设置一个批处理,其中包含几十个 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

让我们来审查一下 20 年内 PnL 的变化情况:

strategy_performance['PnL'].plot(figsize=(12,6), color='black', legend='PnL')
  • 1

下面是输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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')
  • 1
  • 2
  • 3
  • 4

输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

从直觉上讲,这个夏普比率是有意义的,因为假设策略的预期每日平均表现设置为 (1000-990)/2 = $5,并且每日 PnL 的标准偏差设置为大约 $1,000,根据这条线:

pnls = np.random.randint(-990, 1000, size=len(dates)) 
# slight positive bias
  • 1
  • 2

在实践中,夏普比率通常是年化的,以便我们可以更公平地比较不同期限的策略。要将计算出的每日收益的夏普比率年化,我们必须将其乘以 252 的平方根(一年中的交易日期数):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其代码如下:

annualized_sharpe_ratio = sharpe_ratio * np.sqrt(252)
annualized_sharpe_ratio
0.11775069203166105
  • 1
  • 2
  • 3

现在,让我们解释夏普比率:

  • 比率达到 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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这是输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8.15 – 显示峰值和谷底 PnL 以及最大回撤

从这张图中,我们可以评估到这个策略的最大回撤为 $43K,从 1996 年的峰值 PnL 约 $28K 至 2001 年的谷底 PnL 约 -$15K。如果我们从 1996 年开始实施这个策略,我们会在账户上经历 $43K 的亏损,我们需要意识到并为未来做好准备。

策略停止规则 – 止损线/最大损失

在开仓交易之前,设置止损线非常重要,止损线被定义为一种策略或投资组合(仅是一系列策略的集合)在被停止之前能够承受的最大损失次数。

可以使用历史最大回撤值来设置止损线。对于我们的假设性策略,我们发现在 20 年的时间内,实现的最大回撤为 $43K。虽然历史结果并不能完全代表未来结果,但您可能希望为这个策略使用 $43K 的止损值,如果未来损失这么多资金,就关闭它。在实践中,设置止损线要比这里描述的复杂得多,但这应该可以帮助您建立一些有关止损线的直觉。

一旦策略停止,我们可以决定永久关闭策略,或仅在一定时间内关闭策略,甚至关闭策略直到某些市场条件发生改变。这个决定取决于策略的行为和其风险容忍度。

总结

在本章中,我们学习了如何安装和设置基于 Zipline 和 PyFolio 的完整回测和风险/绩效分析系统。然后,我们将市场数据导入到 Zipline/PyFolio 回测投资组合中,并对其进行了结构化和审核。接着,我们研究了如何使用 PyFolio 管理风险并构建一个成功的算法交易系统。

在下一章中,我们将充分利用这一设置,并介绍几个关键的交易策略。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/579722
推荐阅读
相关标签
  

闽ICP备14008679号