第 5 章 · 03 pyfolio 绩效分析 本节摘要:本节讲策略评估的「标准化报告」——pyfolio。无论你用向量化还是 backtrader 跑出策略收益,都需要一套统一的指标来横向比较。pyfolio 是 Quantopian 开源的绩效分析套件,输入只要收益率序列(可选持仓和交易),就能一键生成 tear sheet:年度/累计收益、波动率、夏普、索提诺、最大回撤、Calmar、Omega、Tail ratio、日 VaR、Alpha/Beta,加上滚动夏普、滚动 Beta、回撤期分析、极端事件对比。本节讲清这些指标的口径与解读,以及如何划分样本内/外做 walk-forward 验证。 内容来源:原项目 ,汉化并套用体系化模板。
本节摘要:本节讲策略评估的「标准化报告」——pyfolio。无论你用向量化还是 backtrader 跑出策略收益,都需要一套统一的指标来横向比较。pyfolio 是 Quantopian 开源的绩效分析套件,输入只要收益率序列(可选持仓和交易),就能一键生成 tear sheet:年度/累计收益、波动率、夏普、索提诺、最大回撤、Calmar、Omega、Tail ratio、日 VaR、Alpha/Beta,加上滚动夏普、滚动 Beta、回撤期分析、极端事件对比。本节讲清这些指标的口径与解读,以及如何划分样本内/外做 walk-forward 验证。
内容来源:原项目
ch05/03_pyfolio_demo.ipynb,汉化并套用体系化模板。
⚠️ 风险提示:pyfolio 的指标都是基于历史收益算的,样本外表现可能显著弱于样本内。夏普高不等于未来可盈利,警惕过拟合。
阅读完本节,你应当能够:
不同策略、不同回测引擎产出的收益序列格式各异:zipline 输出 backtest DataFrame,backtrader 输出 analyzer 产物,自研引擎输出 CSV。如果每次都手写 mean()/std()*sqrt(252) 算夏普,口径不统一、容易出错。
pyfolio 解决的就是这个标准化问题:它定义了一套统一的输入契约(returns / positions / transactions),并提供一组口径经过反复校验的指标和可视化,称为 tear sheet(撕下来的「一页报告」)。任何引擎只要把产物转成 pyfolio 格式,就能横向比较。
pyfolio 提供 extract_rets_pos_txn_from_zipline,一行从 zipline 的 backtest DataFrame 提取三个核心序列:
from pyfolio.utils import extract_rets_pos_txn_from_zipline returns, positions, transactions = extract_rets_pos_txn_from_zipline(backtest)
backtrader 则用 PyFolio analyzer 的 get_pf_items() 拿到同样的格式。统一格式是 pyfolio 的核心价值。
perf_stats 是一维摘要,show_perf_stats 是带样本内/外拆分的表格。最关键的几个指标:
| 指标 | 含义 | 解读 |
|---|---|---|
| Annual return | 年化收益 | 复合年化,不是简单平均 |
| Cumulative return | 累计收益 | 整段时间总回报 |
| Annual volatility | 年化波动 | 日波动 × √252 |
| Sharpe ratio | 年化超额收益 / 年化波动 | 风险调整收益,>1 不错,>2 优秀 |
| Calmar ratio | 年化收益 / 最大回撤 | 回撤调整收益,衡量「痛苦比」 |
| Omega ratio | 概率加权的盈亏比 | 相对阈值,>1 偏多盈利 |
| Sortino ratio | 超额收益 / 下行标准差 | 只惩罚下行波动,比夏普更宽容上涨 |
| Max drawdown | 历史最高峰谷回撤 | 最痛苦的账面损失幅度 |
| Tail ratio | 右尾(95%分位)绝对值 / 左尾(5%分位) | 大涨 vs 大跌的对称性 |
| Daily VaR | 均值 - 2×标准差 | 单日 95% 置信下的最大损失 |
| Alpha | 不能被基准解释的超额 | 正且显著才有真 Alpha |
| Beta | 对基准的暴露 | >1 高弹性,<1 防守型 |
from pyfolio.timeseries import perf_stats perf_stats(returns=returns, factor_returns=benchmark_rets)
💡 夏普 vs 索提诺:夏普用总波动(包括上涨)做分母,索提诺只用下行波动。对「涨得多跌得少」的策略,索提诺比夏普更公平;对「暴涨暴跌对称」的策略,两者接近。
show_perf_stats 支持 live_start_date,把回测切成样本内(IS)和样本外(OOS)两段分别报告。这是检测过拟合的关键:如果样本外夏普远低于样本内,说明策略是拟合历史的产物。
from pyfolio.plotting import show_perf_stats oos_date = '2016-01-01' show_perf_stats(returns=returns, factor_returns=benchmark_rets, positions=positions, transactions=transactions, live_start_date=oos_date)
plot_rolling_returns 还会画一条「cone」(锥形置信带),显示在随机游走假设下样本外收益的合理范围;如果实际曲线跌破 cone 下沿,说明样本外表现异常差。
单点指标掩盖了策略的时变性。pyfolio 的滚动图能看出策略是否「稳定」:
plot_rolling_sharpe:滚动 6 个月夏普。理想策略夏普长期 >0 且波动小;如果剧烈摆动甚至长期为负,说明策略在某些环境下失效。plot_rolling_beta:滚动对基准的 Beta。如果 Beta 在 1 附近震荡,说明策略主要靠市场暴露赚钱,没多少 Alpha。from pyfolio.plotting import plot_rolling_sharpe, plot_rolling_beta plot_rolling_sharpe(returns=returns) plot_rolling_beta(returns=returns, factor_returns=benchmark_rets)
plot_drawdown_periods 标出历史上最严重的几段回撤(默认 Top 5),plot_drawdown_underwater 画「水下曲线」(累计回撤幅度)。这两张图能回答:「这个策略最坏会亏多少、亏多久?」
from pyfolio.plotting import plot_drawdown_periods, plot_drawdown_underwater fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10)) axes = ax.flatten() plot_drawdown_periods(returns=returns, ax=axes[0]) plot_drawdown_beta(returns=returns, factor_returns=benchmark_rets, ax=axes[1]) plot_drawdown_underwater(returns=returns, ax=axes[2]) plot_rolling_sharpe(returns=returns)
回撤期长度(underwater period)比回撤幅度更折磨人:回撤 20% 但 1 个月恢复 vs 回撤 15% 但 2 年才恢复,后者更难拿。
extract_interesting_date_ranges 内置了一组「压力事件」(如 2008 金融危机、2015 秋季下跌、Brexit),可以把策略在这些时段的表现和基准对比:
from pyfolio.timeseries import extract_interesting_date_ranges interesting = extract_interesting_date_ranges(returns=returns) (interesting['Fall2015'] .to_frame('strategy').join(benchmark_rets) .add(1).cumprod().sub(1).plot(lw=2))
这能回答:「这个策略在极端行情下是放大风险还是分散风险?」对风险预算和实盘承受力至关重要。
live_start_date + cone 锥形带是检测过拟合的标配。下一节,我们看马科维茨均值方差优化——给定一组资产的预期收益和协方差,如何配出风险最小或夏普最高的组合。