Python 金融交易实用指南(三)(4)https://developer.aliyun.com/article/1523775
结构化 Zipline/PyFolio 回测模块
典型的 Zipline 回测代码定义了三个函数:
initialize:此方法在任何模拟交易发生之前调用;它用于使用股票代码和其他关键交易信息丰富上下文对象。它还启用了佣金和滑点考虑。handle_data:此方法下载市场数据,计算交易信号并下单交易。这是您放置实际交易逻辑的地方,用于进入/退出仓位。analyze:调用此方法执行交易分析。在我们的代码中,我们将使用 pyfolio 的标准分析。请注意,pf.utils.extract_rets_pos_txn_from_zipline(perf)函数返回任何返回、持仓和交易以进行自定义分析。
最后,代码定义了 run_algorithm 方法。此方法返回所有交易的综合摘要,以后可以分析。
在 Zipline 代码中,有几种典型的模式,具体取决于使用情况。
交易每天都会发生
让我们直接从 run_algorithm 方法中引用 handle_data 方法:
from zipline import run_algorithm from zipline.api import order_target_percent, symbol from datetime import datetime import pytz import matplotlib.pyplot as plt import pandas as pd import pyfolio as pf from random import random def initialize(context): pass def handle_data(context, data): pass def analyze(context, perf): returns, positions, transactions = \ pf.utils.extract_rets_pos_txn_from_zipline(perf) pf.create_returns_tear_sheet(returns, benchmark_rets = None) start_date = pd.to_datetime('1996-1-1', utc=True) end_date = pd.to_datetime('2020-12-31', utc=True) results = run_algorithm(start = start_date, end = end_date, initialize = initialize, analyze = analyze, handle_data = handle_data, capital_base = 10000, data_frequency = 'daily', bundle ='quandl')
handle_data 方法将在 start_date 和 end_date 之间的每一天调用。
交易发生在自定义的时间表上
我们省略了 run_algorithm 方法中对 handle_data 方法的引用。相反,我们在 initialize 方法中设置调度程序:
from zipline import run_algorithm from zipline.api import order_target_percent, symbol, set_commission, schedule_function, date_rules, time_rules from datetime import datetime import pytz import matplotlib.pyplot as plt import pandas as pd import pyfolio as pf from random import random def initialize(context): # definition of the stocks and the trading parameters, e.g. commission schedule_function(handle_data, date_rules.month_end(), time_rules.market_open(hours=1)) def handle_data(context, data): pass def analyze(context, perf): returns, positions, transactions = \ pf.utils.extract_rets_pos_txn_from_zipline(perf) pf.create_returns_tear_sheet(returns, benchmark_rets = None) start_date = pd.to_datetime('1996-1-1', utc=True) end_date = pd.to_datetime('2020-12-31', utc=True) results = run_algorithm(start = start_date, end = end_date, initialize = initialize, analyze = analyze, capital_base = 10000, data_frequency = 'daily', bundle ='quandl')
handle_data 方法将在每个 month_end 后 1 小时的市场开盘后调用价格。
我们可以指定各种日期规则,如下所示:
图 8.7 – 包含各种日期规则的表格
类似地,我们可以指定时间规则,如下所示:
图 8.8 – 包含各种时间规则的表格
现在我们将学习如何查看关键的 Zipline API 参考。
查看关键的 Zipline API 参考
在本节中,我们将概述来自 www.zipline.io/appendix.html 的主要功能。
对于回测来说,订单类型、佣金模型和滑点模型是最重要的功能。让我们更详细地看看它们。
订单类型
Zipline 支持以下类型的订单:
图 8.9 – 支持的订单类型
下单逻辑通常放置在 handle_data 方法中。
以下是一个示例:
def handle_data(context, data): price_hist = data.history(context.stock, "close", context.rolling_window, "1d") order_target_percent(context.stock, 1.0 if price_hist[-1] > price_hist.mean() else 0.0)
本示例根据最后一个每日价格是否高于收盘价格的平均值来下订单,以便我们拥有该股票的 100%。
佣金模型
佣金是券商为买卖股票而收取的费用。
Zipline 支持各种类型的佣金,如下所示:
图 8.10 – 支持的佣金类型
此逻辑通常放置在 initialize 方法中。
以下是一个示例:
def initialize(context): context.stock = symbol('AAPL') context.rolling_window = 90 set_commission(PerTrade(cost=5))
在本例中,我们定义了每笔交易 5 美元的佣金。
滑点模型
滑点被定义为预期价格和执行价格之间的差异。
Zipline 提供以下滑点模型:
图 8.11 – 支持的滑点模型
滑点模型应放置在 initialize 方法中。
以下是一个示例:
def initialize(context): context.stock = symbol('AAPL') context.rolling_window = 90 set_commission(PerTrade(cost=5)) set_slippage(VolumeShareSlippage(volume_limit=0.025, price_impact=0.05))
在这个示例中,我们选择了VolumeShareSlippage,限制为0.025,价格影响为0.05。
从命令行运行 Zipline 回测
对于大型回测任务,最好从命令行运行回测。
以下命令运行在 job.py Python 脚本中定义的回测策略,并将结果 DataFrame 保存在 job_results.pickle pickle 文件中:
zipline run -f job.py --start 2016-1-1 --end 2021-1-1 -o job_results.pickle --no-benchmark
例如,您可以设置一个批处理,其中包含几十个 Zipline 命令行作业,以便在夜间运行,并且每个都将结果存储在 pickle 文件中以供以后分析。
保持日志和过去的回测 pickle 文件库以便轻松参考是一个好的实践。
用 PyFolio 进行风险管理介绍
拥有风险管理系统是成功运行算法交易系统的基本组成部分。
算法交易涉及各种风险:
- 市场风险:虽然所有策略在其生命周期的某个阶段都会亏钱,但量化风险度量并确保存在风险管理系统可以缓解策略损失。在某些情况下,糟糕的风险管理可能会将交易损失增加到极端,并且甚至会完全关闭成功的交易公司。
- 监管风险:这种风险源于无意或有意违反法规。它旨在执行顺畅和公平的市场功能。一些众所周知的例子包括假单、报价填充和封闭。
- 软件实施风险:软件开发是一个复杂的过程,而复杂的算法交易策略系统尤其复杂。即使是看似微小的软件错误也可能导致算法交易策略失效,并产生灾难性结果。
- 操作风险:这种风险来自于部署和操作这些算法交易系统。操作/交易人员的错误也可能导致灾难性结果。这个类别中最著名的错误也许是“手指失误”,它指的是意外发送大量订单和/或以非预期价格的错误。
PyFolio 库提供了广泛的市场表现和风险报告功能。
典型的 PyFolio 报告如下所示:
图 8.12 - PyFolio 的标准输出显示回测摘要和关键风险统计数据
以下文本旨在解释此报告中的关键统计数据;即年度波动率、夏普比率和回撤。
为了本章的目的,让我们从一个假想的交易策略生成交易和收益。
以下代码块生成了一个具有轻微正偏差的交易策略的假设 PnL,以及没有偏差的假设头寸:
dates = pd.date_range('1992-01-01', '2012-10-22') np.random.seed(1) pnls = np.random.randint(-990, 1000, size=len(dates)) # slight positive bias pnls = pnls.cumsum() positions = np.random.randint(-1, 2, size=len(dates)) positions = positions.cumsum() strategy_performance = \ pd.DataFrame(index=dates, data={'PnL': pnls, 'Position': positions}) strategy_performance PnL Position 1992-01-01 71 0 1992-01-02 -684 0 1992-01-03 258 1 ... ... ... 2012-10-21 32100 -27 2012-10-22 32388 -26 7601 rows × 2 columns
让我们来审查一下 20 年内 PnL 的变化情况:
strategy_performance['PnL'].plot(figsize=(12,6), color='black', legend='PnL')
下面是输出:
图 8.13 - 显示带有轻微正偏差的合成生成的 PnL
这个图表证实了轻微的正偏差导致策略在长期内具有盈利性。
现在,让我们探索一些这个假设策略表现的风险指标。
市场波动性、PnL 方差和 PnL 标准偏差
市场波动性 定义为价格的标准偏差。通常,在更具波动性的市场条件下,交易策略的 PnL 也会经历更大的幅度波动。这是因为相同的持仓容易受到更大的价格波动的影响,这意味着 PnL 变化。
PnL 方差 用于衡量策略表现/回报的波动幅度。
计算 PnL 的标准偏差的代码与用于计算价格标准偏差(市场波动率)的代码相同。
让我们计算一个滚动 20 天期间的 PnL 标准偏差:
strategy_performance['PnLStdev'] = strategy_performance['PnL'].rolling(20).std().fillna(method='backfill') strategy_performance['PnLStdev'].plot(figsize=(12,6), color='black', legend='PnLStdev')
输出如下:
图 8.14 - 显示一个 20 天滚动期间 PnL 标准偏差的图
这个图表证明了,在这种情况下,没有显著的模式 - 这是一个相对随机的策略。
交易级夏普比率
交易级夏普比率将平均 PnL(策略回报)与 PnL 标准偏差(策略波动性)进行比较。与标准夏普比率相比,交易级夏普比率假定无风险利率为 0,因为我们不滚动头寸,所以没有利息费用。这个假设对于日内或日常交易是现实的。
这个指标的优势在于它是一个单一的数字,考虑了所有相关的风险管理因素,因此我们可以轻松比较不同策略的表现。然而,重要的是要意识到夏普比率并不能讲述所有的故事,并且重要的是要与其他风险指标结合使用。
交易级夏普比率的定义如下:
让我们为我们的策略回报生成夏普比率。首先,我们将生成每日 PnL:
daily_pnl_series = strategy_performance['PnL'].shift(-1) - strategy_performance['PnL'] daily_pnl_series.fillna(0, inplace=True) avg_daily_pnl = daily_pnl_series.mean() std_daily_pnl = daily_pnl_series.std() sharpe_ratio = avg_daily_pnl/std_daily_pnl sharpe_ratio 0.007417596376703097
从直觉上讲,这个夏普比率是有意义的,因为假设策略的预期每日平均表现设置为 (1000-990)/2 =
,并且每日

的标准偏差设置为大约5,并且每日PnL的标准偏差设置为大约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约43K,从 1996 年的峰值 PnL 约 28K 至 2001 年的谷底 PnL 约 -

。如果我们从


年开始实施这个策略,我们会在账户上经历15K。如果我们从1996年开始实施这个策略,我们会在账户上经历15K。如果我们从 1996 年开始实施这个策略,我们会在账户上经历 43K 的亏损,我们需要意识到并为未来做好准备。
策略停止规则 – 止损线/最大损失
在开仓交易之前,设置止损线非常重要,止损线被定义为一种策略或投资组合(仅是一系列策略的集合)在被停止之前能够承受的最大损失次数。
可以使用历史最大回撤值来设置止损线。对于我们的假设性策略,我们发现在 20 年的时间内,实现的最大回撤为 

。虽然历史结果并不能完全代表未来结果,但您可能希望为这个策略使用43K。虽然历史结果并不能完全代表未来结果,但您可能希望为这个策略使用43K。虽然历史结果并不能完全代表未来结果,但您可能希望为这个策略使用 43K 的止损值,如果未来损失这么多资金,就关闭它。在实践中,设置止损线要比这里描述的复杂得多,但这应该可以帮助您建立一些有关止损线的直觉。
一旦策略停止,我们可以决定永久关闭策略,或仅在一定时间内关闭策略,甚至关闭策略直到某些市场条件发生改变。这个决定取决于策略的行为和其风险容忍度。
总结
在本章中,我们学习了如何安装和设置基于 Zipline 和 PyFolio 的完整回测和风险/绩效分析系统。然后,我们将市场数据导入到 Zipline/PyFolio 回测投资组合中,并对其进行了结构化和审核。接着,我们研究了如何使用 PyFolio 管理风险并构建一个成功的算法交易系统。
在下一章中,我们将充分利用这一设置,并介绍几个关键的交易策略。