首页
/ Qlib 回测结果可视化分析:Analysis 图形化报告与模型评估实战

Qlib 回测结果可视化分析:Analysis 图形化报告与模型评估实战

2026-09-05 09:22:23作者:翟萌耘Ralph

在量化投研流程中,回测产出的不只是几条数字指标——累积收益、最大回撤、IC 序列等,更需要一套图形化手段来直观检验策略与模型的合理性。Qlib 的 Analysis 模块(qlib.contrib.report)正是为此设计:它面向 Intraday Trading 场景,为投资组合评估和模型打分能力评估提供了一组开箱即用的图形化报告。读完本篇,你将掌握 Qlib 全部 6 类图形报告(analysis_position 下的 5 类与 analysis_model 下的 1 类)的调用方式、输入数据要求、每张图每个子图的业务含义,以及其背后的源码实现逻辑,从而能够独立完成一次完整的回测结果与模型表现诊断。

Qlib 投资组合图形化报告示例

一、Analysis 模块总览:支持的图形报告清单

Analysis 模块覆盖两大类报告:

  • analysis_position(组合层面,依赖真实回测结果)
    • report_graph
    • score_ic_graph
    • cumulative_return_graph
    • risk_analysis_graph
    • rank_label_graph
  • analysis_model(模型层面,依赖预测分数与 label)
    • model_performance_graph

所有报告函数都注册在包级常量 GRAPH_NAME_LIST 中,可以直接通过 import qlib.contrib.report as qcr 后打印确认:

>>> import qlib.contrib.report as qcr
>>> print(qcr.GRAPH_NAME_LIST)
['analysis_position.report_graph', 'analysis_position.score_ic_graph',
 'analysis_position.cumulative_return_graph', 'analysis_position.risk_analysis_graph',
 'analysis_position.rank_label_graph', 'analysis_model.model_performance_graph']

该常量定义在 qlib/contrib/report/init.py,与上述清单完全一致。官方文档也提示:每个函数的完整参数说明可直接通过 help(qcr.analysis_position.report_graph) 等查看(源码中每个函数都附带了可运行的 docstring 示例)。

一个重要的计算约定:累积指标用“加和”而非“复利”

Qlib 中所有累积型利润指标(return、max drawdown 等)都是通过 summation(加和) 计算的,而不是逐日相乘的几何累积。源码注释明确解释了这一设计动机:

Qlib tries to cumulate returns by summation instead of product to avoid the cumulated curve being skewed exponentially.

这一原则体现在 qlib/contrib/evaluate.pyrisk_analysis 函数中:mode="sum"(默认)时,annualized_return = mean * Nmax_drawdown = (r.cumsum() - r.cumsum().cummax()).min(),即基于简单收益率序列的累加曲线计算回撤;mode="product" 则提供几何累积口径作为对照。因此阅读报告图形时,纵轴的累积收益应当理解为“线性累加收益”,而非复利净值曲线。

二、analysis_position.report_graph:组合绩效总览图

report_graph 是最核心的组合级报告,输入是回测得到的 report_normal_df

API 与数据要求

函数签名与输入约束(源码 docstring 明确要求):

  • report_df.index.name 必须是 date
  • df.columns 必须包含 return、turnover、cost、bench 四列。
                    return      cost        bench       turnover
date
2017-01-04  0.003421    0.000864    0.011693    0.576325
2017-01-05  0.000508    0.000447    0.000721    0.227882
2017-01-06 -0.003321   0.000212   -0.004322   0.102765
2017-01-09  0.006753    0.000212    0.006874   0.105864
2017-01-10 -0.000416   0.000440   -0.003350   0.208396

完整调用示例(源自函数 docstring)

典型流程是:初始化 qlib → 构造 TopkDropoutStrategySimulatorExecutor → 执行 backtest → 取出 portfolio_metric_dict 中对应频率的 report_normal_df → 绘图:

import qlib
import pandas as pd
from qlib.utils.time import Freq
from qlib.utils import flatten_dict
from qlib.backtest import backtest, executor
from qlib.contrib.evaluate import risk_analysis
from qlib.contrib.strategy import TopkDropoutStrategy

# init qlib
qlib.init(provider_uri=<qlib data dir>)

CSI300_BENCH = "SH000300"
FREQ = "day"
STRATEGY_CONFIG = {
    "topk": 50,
    "n_drop": 5,
    # pred_score, pd.Series
    "signal": pred_score,
}

EXECUTOR_CONFIG = {
    "time_per_step": "day",
    "generate_portfolio_metrics": True,
}

backtest_config = {
    "start_time": "2017-01-01",
    "end_time": "2020-08-01",
    "account": 100000000,
    "benchmark": CSI300_BENCH,
    "exchange_kwargs": {
        "freq": FREQ,
        "limit_threshold": 0.095,
        "deal_price": "close",
        "open_cost": 0.0005,
        "close_cost": 0.0015,
        "min_cost": 5,
    },
}

# strategy object
strategy_obj = TopkDropoutStrategy(**STRATEGY_CONFIG)
# executor object
executor_obj = executor.SimulatorExecutor(**EXECUTOR_CONFIG)
# backtest
portfolio_metric_dict, indicator_dict = backtest(executor=executor_obj, strategy=strategy_obj, **backtest_config)
analysis_freq = "{0}{1}".format(*Freq.parse(FREQ))
# backtest info
report_normal_df, positions_normal = portfolio_metric_dict.get(analysis_freq)

qcr.analysis_position.report_graph(report_normal_df)

其中 exchange_kwargs 直接控制图中“含成本”与“不含成本”两条曲线的差异来源:open_cost=0.0005close_cost=0.0015min_cost=5 决定了 cost 列的取值。

图形各子轴含义

横轴为交易日(Trading day),7 个纵排子图依次为:

子图 含义
cum bench 基准累计收益序列
cum return wo cost 组合累计收益(不含成本)
cum return w cost 组合累计收益(含成本)
return wo mdd 不含成本累计收益的最大回撤序列
return w cost mdd 含成本累计收益的最大回撤序列
cum ex return wo cost 相对基准的超额累计收益 CAR(不含成本)
cum ex return w cost 相对基准的超额累计收益 CAR(含成本)
turnover 换手率序列
cum ex return wo cost mdd CAR(不含成本)的回撤序列
cum ex return w cost mdd CAR(含成本)的回撤序列

图中还有两处阴影矩形:上半部分阴影标出 cum return wo cost 对应的最大回撤区间,下半部分阴影标出 cum ex return wo cost 对应的最大回撤区间。

源码实现要点

qlib/contrib/report/analysis_position/report.py 中:

  1. _calculate_report_data 对输入的 4 列原始数据做 cumsum() 得到全部累积序列,例如 cum_return_w_cost = (df["return"] - df["cost"]).cumsum()cum_ex_return_wo_cost = (df["return"] - df["bench"]).cumsum(),与上文“加和口径”完全对应;
  2. 回撤由 _calculate_mdd(series) 实现,即 series - series.cummax(),因此回撤序列恒为负值或零;
  3. _calculate_maximum 通过 idxmin() 找到回撤最低点作为区间终点,再在其之前找 cumsum 峰值作为起点,生成两处阴影矩形的 x0/x1
  4. 最终通过 SubplotsGraph(定义于 qlib/contrib/report/graph.py)以 7 行 1 列、shared_xaxes=True、行宽 row_width=[1, 1, 1, 3, 1, 1, 3] 的 plotly 子图布局输出;show_notebook=True 时直接在 notebook 内渲染,为 False 时返回 plotly.graph_objs.Figure 列表,便于二次保存或嵌入报告。

三、analysis_position.score_ic_graph:预测分数与实际收益的每日相关性

score_ic_graph 用于检验模型预测分数(prediction score)与真实收益(label)之间的逐日相关性,是判断“打分能力是否有效”的第一道图形化关卡。

API 与数据要求

输入 pred_label 要求:

  • index 为 pd.MultiIndex,index 名为 [instrument, datetime]
  • 列名为 [score, label]
instrument  datetime        score         label
SH600004  2017-12-11     -0.013502       -0.013502
            2017-12-12   -0.072367       -0.072367
            2017-12-13   -0.068605       -0.068605
            2017-12-14    0.012440        0.012440
            2017-12-15   -0.102778       -0.102778

调用示例(源自 docstring):先从 Qlib 数据层取 label(文档示例中的 label 公式为 Ref($close, -2)/Ref($close, -1)-1,即 T 收盘到 T+1 收盘的收益,特征公式可参考 docs/component/data.rst 的 Feature 一节),再与模型预测拼接后绘图:

from qlib.data import D
from qlib.contrib.report import analysis_position

pred_df_dates = pred_df.index.get_level_values(level='datetime')
features_df = D.features(D.instruments('csi500'), ['Ref($close, -2)/Ref($close, -1)-1'], pred_df_dates.min(), pred_df_dates.max())
features_df.columns = ['label']
pred_label = pd.concat([features_df, pred], axis=1, sort=True).reindex(features_df.index)
analysis_position.score_ic_graph(pred_label)

图形含义与实现

图形有两条曲线:

  • iclabelscore 的逐日 Pearson 相关系数 序列;
  • rank_ic:两者的逐日 Spearman 秩相关系数 序列。

源码 qlib/contrib/report/analysis_position/score_ic.py_get_score_icdropna(how="any") 剔除缺失,再 groupby(level="datetime") 逐日计算 x["label"].corr(x["score"])method="spearman" 的秩相关,最终用 ScatterGraph(lines+markers 模式)绘制,并通过 guess_plotly_rangebreaks 隐藏周末与节假日造成的横轴空隙。

Score IC 图形示例

四、analysis_position.cumulative_return_graph:买入/卖出/持有分拆的收益分析

cumulative_return_graph 从“交易行为”角度拆解组合收益:把每天的持仓按 持有(hold)、买入(buy)、卖出(sell) 三类分拆,分别统计加权平均 label 并逐日累加,帮助判断收益究竟来自新买入的票、还是继续持有的票,卖出(回避)行为是否有效。

API 与数据要求

  • positionqlib.backtest.backtest 返回的 positions 字典;
  • report_normal:含 bench 列的 report_normal 数据(用于逐日对齐基准,计算超额);
  • label_dataD.features 的结果,index 为 [instrument, datetime],列名为 labelT 日的 label 必须是 T 到 T+1 的收益变化,推荐用收盘价构造,如 D.features(D.instruments('csi500'), ['Ref($close, -1)/$close-1'])
  • start_date / end_date:可选的时间切片;show_notebook 控制显示或返回 figure 列表。

图形含义

  • 横轴:交易日;
  • 上方子图 Y 轴:(((Ref($close, -1)/$close - 1) * weight).sum() / weight.sum()).cumsum(),即按权重加权平均的 label 累加曲线;
  • 下方子图 Y 轴:当日该类操作的权重合计;
  • sell 图中,y < 0 表示卖出行为带来了正贡献(少赚了被卖掉的跌票 / 规避了下跌);其他图中 y > 0 表示正贡献;
  • buy_minus_sell 图中,底部 weight 子图的 Y 值为 buy_weight + sell_weight
  • 每张图右侧直方图中,红色竖线表示该序列的均值。

源码实现要点

qlib/contrib/report/analysis_position/cumulative_return.py 的核心逻辑:

  1. 调用 qlib/contrib/report/analysis_position/parse_position.pyget_position_data,将 position 字典解析为长表(含 amount/cash/count/price/status/weight 列),并与 label 对齐(注意 _add_bench_to_positionbench.shift(-1):买卖当日看的是次日的涨跌,因为执行上 T 日交易、T+1 日才产生收益);
  2. _calculate_label_rankstatus 的语义是 0-hold、1-buy、-1-sell:买入/卖出通过对比 T 日与 T-1 日持仓集合的差集推导(T 存在且 T-1 不存在 → buy,反之 → sell);
  3. 逐日对三类股票分别计算 sum(label * weight) / sum(weight) 得到加权平均收益,再对 buy/sell/hold/buy_minus_sell 四列分别 cumsum(),最终以 2×2 子图(上方曲线 + 下方权重 + 右侧直方图,红线为均值)输出 4 张图。

五、analysis_position.risk_analysis_graph:风险指标总览与月度分解

risk_analysis_graphqlib/contrib/evaluate.pyrisk_analysis 计算的统计指标图形化,分为“总体柱状图 + 4 张月度时序图”。

API 与数据要求

  • analysis_df:分析数据,index 为 pd.MultiIndex,列名为 risk,典型结构(源自 docstring):
                                                                  risk
    excess_return_without_cost mean               0.000692
                                   std                0.005374
                                   annualized_return  0.174495
                                   information_ratio  2.045576
                                   max_drawdown      -0.079103
    excess_return_with_cost    mean               0.000499
                                   std                0.005372
                                   annualized_return  0.125625
                                   information_ratio  1.473152
                                   max_drawdown      -0.088263
  • report_normal_df:与 report_graph 相同的数据结构(index.name 为 date,列含 return/turnover/cost/bench);
  • report_long_short_df:可选,列含 long/short/long_short(当前实现中该分支已被注释停用,实际只使用 report_normal_df);
  • show_notebook:默认 True,在 notebook 中显示;否则返回 figure 列表。

调用示例(源自 docstring,注意 risk_analysis 需要传入 freq=analysis_freq):

analysis = dict()
analysis["excess_return_without_cost"] = risk_analysis(
    report_normal_df["return"] - report_normal_df["bench"], freq=analysis_freq
)
analysis["excess_return_with_cost"] = risk_analysis(
    report_normal_df["return"] - report_normal_df["bench"] - report_normal_df["cost"], freq=analysis_freq
)

analysis_df = pd.concat(analysis)  # type: pd.DataFrame
analysis_position.risk_analysis_graph(analysis_df, report_normal_df)

图形含义

总体柱状图(4 列 × 2 系列:excess_return_without_costexcess_return_with_cost):

  • std:CAR(超额累计收益)的标准差;
  • annualized_return:CAR 的年化收益率;
  • information_ratio:信息比率(IR),衡量单位主动风险换取的超额收益;
  • max_drawdown:CAR 的最大回撤。

月度时序图(横轴为按月分组的交易日,每月两条曲线分别为含/不含成本):

  • annualized_return 图:月度 CAR 的年化收益序列;
  • max_drawdown 图:月度 CAR 的最大回撤序列;
  • information_ratio 图:月度 IR 序列;
  • std 图:月度 CAR 的标准差序列。

源码实现要点

qlib/contrib/report/analysis_position/risk_analysis.py 中:

  • 总体图由 _get_risk_analysis_figure 生成,1 行 4 列的 BarGraph 子图,两个系列(无成本/含成本)并排比较,可直观看到交易成本对年化收益与 IR 的侵蚀幅度;
  • 月度图由 _get_monthly_risk_analysis_figure 生成:先 groupby([year, month]) 按月聚合,当月交易日少于 3 天会被跳过(源码注释说明这是为了避免图中出现断点),再对 annualized_returnmax_drawdowninformation_ratiostd 四个特征逐一用 _get_monthly_analysis_with_feature 透视出“日期 × 系列”矩阵并绘制折线;
  • risk_analysis 的年化缩放系数按频率确定:日频 238、周频 50、月频 12、分钟频 240×238(见 qlib/contrib/evaluate.pycal_risk_analysis_scaler),Nfreq 至少提供一个,N 存在时 freq 被忽略。

六、analysis_position.rank_label_graph:买/卖/持股票的 label 排名分析

rank_label_graph 回答一个问题:策略当天买入、卖出、继续持有的股票,在全体股票 label 排名中处于什么位置?

API 与数据要求

  • positionqlib.backtest.backtest 返回的 positions 数据;
  • label_dataD.features 结果,index 为 [instrument, datetime],列名为 labellabel T 为 T 到 T+1 的变化,推荐 D.features(D.instruments('csi500'), ['Ref($close, -1)/$close-1'])
  • start_date/end_date:可选时间范围;show_notebook:显示或返回 figure 列表。

调用示例(源自 docstring):

from qlib.data import D
from qlib.contrib.evaluate import backtest
from qlib.contrib.strategy import TopkDropoutStrategy

# backtest parameters
bparas = {}
bparas['limit_threshold'] = 0.095
bparas['account'] = 1000000000

sparas = {}
sparas['topk'] = 50
sparas['n_drop'] = 5
strategy = TopkDropoutStrategy(**sparas)

_, positions = backtest(pred_df, strategy, **bparas)

pred_df_dates = pred_df.index.get_level_values(level='datetime')
features_df = D.features(D.instruments('csi500'), ['Ref($close, -1)/$close-1'], pred_df_dates.min(), pred_df_dates.max())
features_df.columns = ['label']

qcr.analysis_position.rank_label_graph(positions, features_df, pred_df_dates.min(), pred_df_dates.max())

图形含义

输出 3 张图(Hold / Buy / Sell):横轴为交易日,纵轴为当日该类操作股票的 label 平均排名比率(rank ratio)。官方文档给出的公式为:

ranking ratio = Ascending Ranking of label / Number of Stocks in the Portfolio

从源码结构看,qlib/contrib/report/analysis_position/parse_position.py_calculate_label_rank 的实际实现是:

g_df["rank_ratio"] = g_df["label"].rank(ascending=False) / len(g_df) * 100

即对当日全市场股票按 label 排名后归一化到 0–100 的百分比,再对当日 Buy(status=1)、Hold(status=0)、Sell(status=-1)三类股票分别取均值(rank_label_mean)。例如 Buy 曲线长期处于低比值区,说明策略持续买入 label 靠前的股票,验证了打分信号与真实收益的一致性。

七、analysis_model.model_performance_graph:模型打分能力全景诊断

model_performance_graph 面向未执行回测的阶段,仅凭 pred_label(预测分数 + 真实 label)即可诊断模型的排序能力、收益分层效果与预测稳定性,是模型迭代中最常用的评估图形。

API 与参数

  • pred_label:index 为 [instrument, datetime] 的 MultiIndex,列名为 [score, label]label 通常与训练 label 一致(如 Ref($close, -2)/Ref($close, -1) - 1);
  • lag(默认 1):自相关计算中的滞后天数,仅用于 auto-correlation;
  • N(默认 5):分层分组数;
  • reverse(默认 False):为 True 时 score *= -1,用于分数方向与实际效果相反的情形;
  • graph_names(默认 ["group_return", "pred_ic", "pred_autocorr"]):控制生成哪几组图;
  • show_notebook:True 时在 notebook 渲染,否则返回 plotly.graph_objs.Figure 列表;
  • show_nature_day:是否展示非交易日的横轴刻度;
  • **kwargs:透传给 plotly 的样式参数,当前支持 rangebreaks(用于隐藏周末/节假日空隙)。

图形 1:分组累积收益(group_return)

按 score 降序排列后逐日分成 N(默认 5)组,计算各组 label 均值并累加:

  • Group1:label 排名 ratio ≤ 20% 的股票组累积收益;
  • Group2:20% < ratio ≤ 40%;Group3:40% < ratio ≤ 60%;Group4:60% < ratio ≤ 80%;Group5:ratio > 80%;
  • long-short:Group1 与 Group5 累积收益之差;
  • long-average:Group1 与全市场平均累积收益之差。

qlib/contrib/report/analysis_model/analysis_model_performance.py_group_return 的实现细节:先 sort_values("score", ascending=False),再按 len(x) // N 切片取每组 label 均值;除累积曲线外,还会对 long-shortlong-average 的逐日值绘制直方图(DistplotGraph,bin 宽自动取极差/20),观察多空收益的逐日分布。

模型分组累积收益图形示例

图形 2:IC 系列(pred_ic)

  • IC 柱状图:逐日 labelscore 的 Pearson 相关系数,可用于评估预测分数的有效性;
  • Monthly IC 热力图:IC 的月度均值(源码中对缺失月份做了 reindex 填充,保证热力图月轴连续);
  • IC 直方图 + Q-Q 图:IC 的分布形态及与正态分布的 Quantile-Quantile 对比,用于判断 IC 是否稳定、是否存在异常尾部。

实现上 _pred_ic 支持 methods=("IC", "Rank IC")(分别对应 pearson/spearman),Monthly IC、直方图与 Q-Q 图基于第一种 IC 绘制。

图形 3:自相关(pred_autocorr)

逐日计算最新预测分数与 lag 天前预测分数的秩相关(源码先 groupby(level="instrument") 对 score 做 shift(lag),再逐日对两组分数的 rank(pct=True) 求 Pearson 相关)。该序列反映打分信号的稳定性:自相关越高,意味着每日持仓调整越小,可据此估算策略的换手率水平。

扩展能力

从源码结构看,model_performance_graph 还内置了 _pred_turnover(按当日 score 最大/最小 len(x)//N 集合的重叠程度计算 Top/Bottom 换手率),可通过 graph_names=["group_return", "pred_ic", "pred_autocorr", "pred_turnover"] 之类的方式组合启用。函数内部通过 eval(f"_{graph_name}") 动态分发到对应私有函数,因此 graph_names 中每一项都必须与模块内 _xxx 函数一一对应。

八、实战建议与使用边界

  1. 输入数据的三个约定是全部报告的通用前提:analysis_position 类报告依赖 backtest(..., generate_portfolio_metrics=True) 产生的 portfolio_metric_dict(按 "{count}{freq}" 组合的 key 取对应频率的 (report_normal_df, positions));analysis_modelscore_ic 依赖 [instrument, datetime] 双级索引的 pred_label 矩阵;label 一律使用“T 到 T+1 收益”口径(如 Ref($close, -1)/$close - 1),与回测成交在次日的设定保持一致。
  2. show_notebook 参数让所有函数兼具交互展示与程序化产出两种形态:返回的 plotly.graph_objs.Figure 列表可直接 fig.write_html(...) 落盘,适合嵌入投研流水线。
  3. 口径一致性:报告中所有累积曲线、回撤、年化收益均为加和口径(risk_analysis 默认 mode="sum"),横向对比外部复利口径指标时需先换算;如需几何口径,可在调用 risk_analysis 时显式传 mode="product"
  4. 相关示例代码可在 examples/nested_decision_execution/workflow.py 中找到(其中包含 analysis_position.report_graph(report_normal_df) 的调用示意),配合本文各函数的 docstring 示例即可快速复现整套图形化评估流程。

通过上述 6 类报告,Qlib 把“组合层面(收益/回撤/成本/换手/排名)”与“模型层面(分层收益/IC/自相关)”的诊断完整图形化,覆盖从回测结果解读到模型打分能力验证的关键环节,是 Qlib 工作流中从 backtest/模型预测走向可交付投研结论的重要一环。

登录后查看全文
热门项目推荐
相关项目推荐