赞
踩
原文:
zh.annas-archive.org/md5/6efde0935976ca50d877b2b5774aeade
译者:飞龙
在本章中,我们通过概述关键示例来介绍 Python 中的四个主要统计库—statsmodels
、pmdarima
、fbprophet
和 scikitlearn
。这些库用于对时间序列建模并提供它们的预测值,以及置信区间。此外,我们演示了如何使用分类模型来预测时间序列的百分比变化。
为此,我们将涵盖以下使用案例:
statsmodels 介绍
使用具有外生因素的季节性自回归综合移动平均(SARIMAX)时间序列模型与 pmdarima
使用 Facebook 的 Prophet 库进行时间序列预测
scikit-learn 回归和分类介绍
本章使用的 Python 代码可在书籍的代码存储库的 Chapter06 文件夹
中找到。
statsmodels 是一个 Python 库,允许我们探索数据、执行统计检验并估计统计模型。
本章重点介绍了 statsmodels 对时间序列的建模、分析和预测。
许多统计学习技术的一个基本假设是观测值/字段是正态分布的。
虽然有许多用于正态分布的健壮统计检验方法,但一种直观的视觉方法被称为分位数-分位数图(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.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 分析将数据分解为三个不同的组件,如下所示:
趋势(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 组件的合成价格的图表
在上一张截图中,我们清楚地看到了季节性组件——从中位数值上下波动。我们还看到了误差噪声,因为波动不是完美的。最后,我们看到数值正在增加——趋势组件。
在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
模型。
将时间序列分解为 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
方法中提取的趋势组件,如下所示:
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 值)非常低时,我们可以得出结论数据集是平稳的。我们将在以下章节中详细介绍详细步骤。
让我们检查平稳性,并通过使用差分方法将我们的数据集转换为一个平稳数据集。我们从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 测试,以检查是否具有平稳性,如下所示:
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 模型由三个组成部分组成,如下所述:
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 模型预测的历史和预测价格
在前面截图中显示的图中,预测价格明显遵循过去价格的趋势。
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 是一个用于预测单变量时间序列的 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
是一个基于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
之间存在强关系。
让我们使用 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 中的金融市场数据访问*
第八章*,Zipline 和 PyFolio 简介*
第九章*,基础算法交易策略*
本章概述了几个关键的市场数据源,从免费到付费的数据源都有涵盖。可从github.com/wilsonfreitas/awesome-quant#data-sources
获得更完整的可用资源列表。
算法交易模型信号的质量基本取决于正在分析的市场数据的质量。市场数据是否已清理出错误记录,并且是否有质量保证流程来在发生错误时更正任何错误?如果市场数据源有问题,那么数据可以多快被纠正?
下述描述的免费数据源适用于学习目的,但不适用于专业交易目的 - 每天的 API 调用次数可能非常有限,API 可能较慢,并且如果数据不正确,则没有支持和更正。此外,在使用任何这些数据提供者时,请注意其使用条款。
在本章中,我们将涵盖以下主要内容:
探索 yahoofinancials Python 库
探索 pandas_datareader Python 库
探索 Quandl 数据源
探索 IEX Cloud 数据源
探索 MarketStack 数据源
本章中使用的 Python 代码可在书籍代码存储库的Chapter07/marketdata.ipynb
笔记本中找到。
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
是用于金融数据的最先进的库之一,提供对多个数据源的访问。
支持的一些数据源如下:
雅虎财经
圣路易斯联邦储备银行的 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
库的输出几乎相同。
可用股票标记列表在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.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 是互联网上最大的经济/金融数据存储库之一。其数据源可以免费访问。它还提供高级数据源,需要付费。
安装很简单:
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 是其中一个商业产品。它为个人提供每月 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 提供跨主要全球股票交易所的实时、盘内和历史市场数据的广泛数据库。它为每月高达 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 的 Python 库,它们抽象出了算法交易策略的回测和性能/风险分析方面的复杂性。它们允许您完全专注于交易逻辑。
为此,我们将涵盖以下主要内容:
简介 Zipline 和 PyFolio
安装 Zipline 和 PyFolio
将市场数据导入 Zipline/PyFolio 回测系统
构建 Zipline/PyFolio 回测模块
查看关键 Zipline API 参考
从命令行运行 Zipline 回测
简介 PyFolio 提供的关键风险管理指标
本章中使用的 Python 代码可在书籍代码库的 Chapter08/risk_management.ipynb
笔记本中找到。
回测是一种计算方法,用于评估如果将交易策略应用于历史数据,该策略将表现如何。理想情况下,这些历史数据应来自于一个具有类似市场条件的时期,例如具有类似于当前和未来的波动性。
回测应包括所有相关因素,如滑点和交易成本。
Zipline 是最先进的开源 Python 库之一,用于算法交易回测引擎。其源代码可在 github.com/quantopian/zipline
找到。Zipline 是一个适用于日常交易的回测库(也可以回测每周、每月等)。它不太适合回测高频交易策略。
PyFolio 是一个开源的 Python 性能和风险分析库,由金融投资组合组成,与 Zipline 紧密集成。您可以在 github.com/quantopian/pyfolio
找到其文档。
使用这两个库来回测您的交易策略可以节省大量时间。
本章的目标是描述这些库的关键功能并建立您的直觉。鼓励您在 PyCharm 或任何其他 Python IDE 中调试代码,并研究每个结果变量的内容以充分利用提供的信息。一旦您熟悉了每个结果对象的内容,简要地研究这些库的源代码以查看其全部功能。
我们建议按照 附录 A 中描述的方式设置开发环境。尽管如此,详细的说明在以下各节中给出。
出于性能原因,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。
您可以通过 pip
安装 pyfolio
包:
pip install pyfolio
正如我们所见,安装 PyFolio 也是一项简单的任务。
回测依赖于我们拥有广泛的市场数据数据库。
Zipline 引入了两个与市场数据相关的术语 - bundle 和 ingest:
Bundle 是从自定义源逐步将市场数据导入到 Zipline 的专有数据库的接口。
Ingest 是将自定义源市场数据逐步导入到 Zipline 的专有数据库的实际过程;数据摄取不会自动更新。每次需要新鲜数据时,您都必须重新进行数据摄取。
默认情况下,Zipline 支持以下 bundle:
历史 Quandl bundle(2018 年之前的美国股票每日免费数据)
.csv
文件 bundle
现在我们将更详细地学习如何导入这两个 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 文件采用 开盘价、最高价、最低价、收盘价、成交量(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 提供每月 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 中,但存在替代数据源。
该项目在 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
仓库文件复制到您的 Zipline bundles
目录;例如,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 回测代码定义了三个函数:
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 参考。
在本节中,我们将概述来自 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
。
对于大型回测任务,最好从命令行运行回测。
以下命令运行在 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 报告如下所示:
图 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 的标准偏差的代码与用于计算价格标准偏差(市场波动率)的代码相同。
让我们计算一个滚动 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 = $5,并且每日 PnL 的标准偏差设置为大约 $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 以及最大回撤
从这张图中,我们可以评估到这个策略的最大回撤为 $43K,从 1996 年的峰值 PnL 约 $28K 至 2001 年的谷底 PnL 约 -$15K。如果我们从 1996 年开始实施这个策略,我们会在账户上经历 $43K 的亏损,我们需要意识到并为未来做好准备。
在开仓交易之前,设置止损线非常重要,止损线被定义为一种策略或投资组合(仅是一系列策略的集合)在被停止之前能够承受的最大损失次数。
可以使用历史最大回撤值来设置止损线。对于我们的假设性策略,我们发现在 20 年的时间内,实现的最大回撤为 $43K。虽然历史结果并不能完全代表未来结果,但您可能希望为这个策略使用 $43K 的止损值,如果未来损失这么多资金,就关闭它。在实践中,设置止损线要比这里描述的复杂得多,但这应该可以帮助您建立一些有关止损线的直觉。
一旦策略停止,我们可以决定永久关闭策略,或仅在一定时间内关闭策略,甚至关闭策略直到某些市场条件发生改变。这个决定取决于策略的行为和其风险容忍度。
在本章中,我们学习了如何安装和设置基于 Zipline 和 PyFolio 的完整回测和风险/绩效分析系统。然后,我们将市场数据导入到 Zipline/PyFolio 回测投资组合中,并对其进行了结构化和审核。接着,我们研究了如何使用 PyFolio 管理风险并构建一个成功的算法交易系统。
在下一章中,我们将充分利用这一设置,并介绍几个关键的交易策略。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。