阅读提示

这篇讲解如何解读 Analyst 生成的绩效报告——从 summary_df(指标表)到交互 HTML(plotly 图表)。学完之后,你能看懂任何一个回测报告里的每个数字是怎么算出来的,以及哪些指标最值得关注。

导言

Analyst 是 Betalens 的绩效评价模块,负责把 BacktestBase 的回测结果(净值、持仓、每日盈亏、调仓日志)转化成一组可读的指标表和交互图表。

核心用法只有两行:

1
2
3
4
from betalens.analyst import Analyst

analyst = Analyst.from_backtest(engine, name="股息率策略", benchmark=hs300_nav)
analyst.report(to_excel="report.xlsx", to_html="report.html")

跑完之后,你会得到一个 Excel 文件(详细数据表)和一个 HTML 文件(交互图表)。这篇讲解这些输出里的每一个数字代表什么。

Analyst 的基本用法

从回测实例创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from betalens.analyst import Analyst

# 最简用法
analyst = Analyst.from_backtest(engine, name="股息率策略")

# 带基准对比
analyst = Analyst.from_backtest(engine, name="股息率策略", benchmark=hs300_bt)
# benchmark 可以是:
# - BacktestBase 实例
# - nav Series(净值曲线)
# - 证券代码字符串(如 "000906.SH")

# 从已有的 Excel 报告重新分析
analyst = Analyst.from_excel("backtest_dump.xlsx", name="历史策略")

生成报告

1
2
3
4
5
6
7
8
9
10
11
# 同时输出 Excel 和 HTML
analyst.report(to_excel="report.xlsx", to_html="report.html")

# 只输出 HTML(用于分享)
analyst.to_html("report.html")

# 只输出 Excel(用于进一步分析)
analyst.to_excel("report.xlsx")

# 在终端打印文本报告
analyst.print_report()

核心指标逐个解读

summary_df:核心指标汇总表

1
2
summary = analyst.summary_df()
print(summary)

典型的 summary 包含以下指标:

收益类

指标 含义 理想值
累计收益率 净值终点 - 1 越高越好
年化收益率 $(1+\text{累计收益率})^{252/\text{交易日}}-1$ 越高越好
超额收益率 策略收益 - 基准收益 越高越好
年化超额收益率 $(1+\text{超额收益率})^{252/\text{交易日}}-1$ 越高越好

风险类

指标 含义 理想值
年化波动率 收益日标准差 × $\sqrt{252}$ 越低越好
最大回撤 从净值高点到低点的最大跌幅 越小越好
回撤持续期 最大回撤从峰值到恢复的时间 越短越好
VaR (95%) 95% 置信度下的最大单日损失 越小越好

风险调整收益类

指标 含义 理想值
夏普比率 $(\text{年化收益} - \text{无风险利率}) / \text{年化波动率}$ > 1 良好,> 2 优秀
卡尔玛比率 $\text{年化收益} / \text{最大回撤}$ > 1 良好
索提诺比率 $(\text{年化收益} - \text{无风险利率}) / \text{下行波动率}$ 越高越好
信息比率 $\text{年化超额收益} / \text{跟踪误差}$ > 0.5 良好

交易类

指标 含义
总交易次数 买入 + 卖出总笔数
平均换手率 每次调仓的权重变化均值
平均持仓天数 策略的平均持仓周期
胜率 正收益调仓期数 / 总调仓期数

top_holdings_df:持仓明细

1
2
holdings = analyst.top_holdings_df()
print(holdings)

显示最新一期(或任意一期)的 Top 10 持仓明细:代码、名称、权重、所属行业。证券名称从 daily_market 表自动查询,降级为原代码。

contribution_df:收益归因

1
2
contrib = analyst.contribution_df()
print(contrib)

把策略收益拆解到行业或因子维度,看哪个行业/因子贡献最大。

trade_pnl_df:交易盈亏明细

1
2
trade_pnl = analyst.trade_pnl_df()
print(trade_pnl)

每次调仓的盈亏明细,包含交易成本。可以用来分析”哪笔交易亏损最多”、”手续费占比多少”。

monthly_table:月度收益热力表

1
2
monthly = analyst.monthly_table()
print(monthly)

每个月的策略收益 vs 基准收益,格式为:

2020-01 2020-02
策略 +2.3% -1.1%
基准 +1.8% +0.3%
超额 +0.5% -1.4%

交互图表

1
2
3
4
# 获取所有交互图表
figs = analyst.interactive_plots()
print(figs.keys())
# dict_keys(['净值曲线', '回撤曲线', '滚动胜率', '滚动夏普', '月度收益', '权重堆积', '收益贡献'])

每个图表都是 plotly 对象,可以在 Jupyter Notebook 里直接 fig.show(),也可以保存为 HTML 嵌入报告。

净值曲线

1
2
fig_nav = figs["净值曲线"]
fig_nav.show()

策略净值 vs 基准净值的叠加图,会标注最大回撤区间。

回撤曲线

1
fig_dd = figs["回撤曲线"]

从净值高点画到低点的阴影区间,颜色越深表示回撤越深。

月度收益

1
fig_monthly = figs["月度收益"]

热力图:横轴是月份,纵轴是策略/基准/超额,单元格颜色深浅代表收益大小。

从 Excel 报告深度分析

1
2
3
4
5
6
7
8
9
10
# 如果你拿到了别人发来的 report.xlsx
analyst = Analyst.from_excel("report.xlsx", name="分析报告")

# 可以继续调用任意分析方法
summary = analyst.summary_df()
monthly = analyst.monthly_table()
figs = analyst.interactive_plots()

# 导出为自己风格的报告
analyst.to_html("my_style_report.html")

开发者侧:指标的计算方式

如果需要自定义指标,可以直接用底层函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from betalens.analyst.metrics import (
sharpe_ratio,
max_drawdown,
calmar_ratio,
information_ratio,
)

nav = engine.nav
returns = nav.pct_change().dropna()

sr = sharpe_ratio(returns, rf=0.03) # rf = 无风险利率
mdd = max_drawdown(nav)
cmr = calmar_ratio(returns, rf=0.03)
ir = information_ratio(returns, benchmark_returns)

print(f"夏普比率:{sr:.3f}")
print(f"最大回撤:{mdd:.2%}")
print(f"卡尔玛比率:{cmr:.3f}")
print(f"信息比率:{ir:.3f}")

底层函数都在 betalens.analyst.metrics 里,是纯函数,没有副作用,适合集成到自己的报告生成流水线里。

常见问题

1. 中文名称显示为证券代码

1
2
3
4
# 原因:daily_market 表里没有这只股票的名字
# 解决:手动补全
analyst.names["000001.SZ"] = "平安银行"
analyst.report(to_excel="report.xlsx", ...)

2. 基准净值曲线缺失

1
2
3
4
5
6
7
# 只传入了策略,没有基准
analyst = Analyst.from_backtest(engine, name="策略")

# 需要对比基准时:
analyst_with_benchmark = Analyst.from_backtest(
engine, name="策略", benchmark="000906.SH" # 中证800
)

3. to_excel 报错文件被占用

1
2
# 先关闭 Excel 文件,再重新生成
analyst.to_excel("report.xlsx", index=False)

延伸阅读