Qlib 回测结果可视化分析:Analysis 图形化报告与模型评估实战
在量化投研流程中,回测产出的不只是几条数字指标——累积收益、最大回撤、IC 序列等,更需要一套图形化手段来直观检验策略与模型的合理性。Qlib 的 Analysis 模块(qlib.contrib.report)正是为此设计:它面向 Intraday Trading 场景,为投资组合评估和模型打分能力评估提供了一组开箱即用的图形化报告。读完本篇,你将掌握 Qlib 全部 6 类图形报告(analysis_position 下的 5 类与 analysis_model 下的 1 类)的调用方式、输入数据要求、每张图每个子图的业务含义,以及其背后的源码实现逻辑,从而能够独立完成一次完整的回测结果与模型表现诊断。
一、Analysis 模块总览:支持的图形报告清单
Analysis 模块覆盖两大类报告:
- analysis_position(组合层面,依赖真实回测结果)
report_graphscore_ic_graphcumulative_return_graphrisk_analysis_graphrank_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.py 的 risk_analysis 函数中:mode="sum"(默认)时,annualized_return = mean * N、max_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 → 构造 TopkDropoutStrategy 与 SimulatorExecutor → 执行 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.0005、close_cost=0.0015、min_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 中:
_calculate_report_data对输入的 4 列原始数据做cumsum()得到全部累积序列,例如cum_return_w_cost = (df["return"] - df["cost"]).cumsum()、cum_ex_return_wo_cost = (df["return"] - df["bench"]).cumsum(),与上文“加和口径”完全对应;- 回撤由
_calculate_mdd(series)实现,即series - series.cummax(),因此回撤序列恒为负值或零; _calculate_maximum通过idxmin()找到回撤最低点作为区间终点,再在其之前找cumsum峰值作为起点,生成两处阴影矩形的x0/x1;- 最终通过
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)
图形含义与实现
图形有两条曲线:
- ic:
label与score的逐日 Pearson 相关系数 序列; - rank_ic:两者的逐日 Spearman 秩相关系数 序列。
源码 qlib/contrib/report/analysis_position/score_ic.py 中 _get_score_ic 先 dropna(how="any") 剔除缺失,再 groupby(level="datetime") 逐日计算 x["label"].corr(x["score"]) 与 method="spearman" 的秩相关,最终用 ScatterGraph(lines+markers 模式)绘制,并通过 guess_plotly_rangebreaks 隐藏周末与节假日造成的横轴空隙。
四、analysis_position.cumulative_return_graph:买入/卖出/持有分拆的收益分析
cumulative_return_graph 从“交易行为”角度拆解组合收益:把每天的持仓按 持有(hold)、买入(buy)、卖出(sell) 三类分拆,分别统计加权平均 label 并逐日累加,帮助判断收益究竟来自新买入的票、还是继续持有的票,卖出(回避)行为是否有效。
API 与数据要求
position:qlib.backtest.backtest返回的 positions 字典;report_normal:含 bench 列的 report_normal 数据(用于逐日对齐基准,计算超额);label_data:D.features的结果,index 为 [instrument, datetime],列名为 label。T 日的 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 的核心逻辑:
- 调用 qlib/contrib/report/analysis_position/parse_position.py 的
get_position_data,将 position 字典解析为长表(含 amount/cash/count/price/status/weight 列),并与 label 对齐(注意_add_bench_to_position中bench.shift(-1):买卖当日看的是次日的涨跌,因为执行上 T 日交易、T+1 日才产生收益); _calculate_label_rank中status的语义是 0-hold、1-buy、-1-sell:买入/卖出通过对比 T 日与 T-1 日持仓集合的差集推导(T 存在且 T-1 不存在 → buy,反之 → sell);- 逐日对三类股票分别计算
sum(label * weight) / sum(weight)得到加权平均收益,再对 buy/sell/hold/buy_minus_sell 四列分别cumsum(),最终以 2×2 子图(上方曲线 + 下方权重 + 右侧直方图,红线为均值)输出 4 张图。
五、analysis_position.risk_analysis_graph:风险指标总览与月度分解
risk_analysis_graph 把 qlib/contrib/evaluate.py 中 risk_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_cost 与 excess_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_return、max_drawdown、information_ratio、std四个特征逐一用_get_monthly_analysis_with_feature透视出“日期 × 系列”矩阵并绘制折线; risk_analysis的年化缩放系数按频率确定:日频 238、周频 50、月频 12、分钟频 240×238(见 qlib/contrib/evaluate.py 的cal_risk_analysis_scaler),N与freq至少提供一个,N存在时freq被忽略。
六、analysis_position.rank_label_graph:买/卖/持股票的 label 排名分析
rank_label_graph 回答一个问题:策略当天买入、卖出、继续持有的股票,在全体股票 label 排名中处于什么位置?
API 与数据要求
position:qlib.backtest.backtest返回的 positions 数据;label_data:D.features结果,index 为 [instrument, datetime],列名为 label;label 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-short 与 long-average 的逐日值绘制直方图(DistplotGraph,bin 宽自动取极差/20),观察多空收益的逐日分布。
图形 2:IC 系列(pred_ic)
- IC 柱状图:逐日
label与score的 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 函数一一对应。
八、实战建议与使用边界
- 输入数据的三个约定是全部报告的通用前提:
analysis_position类报告依赖backtest(..., generate_portfolio_metrics=True)产生的portfolio_metric_dict(按"{count}{freq}"组合的 key 取对应频率的(report_normal_df, positions));analysis_model与score_ic依赖[instrument, datetime]双级索引的pred_label矩阵;label 一律使用“T 到 T+1 收益”口径(如Ref($close, -1)/$close - 1),与回测成交在次日的设定保持一致。 show_notebook参数让所有函数兼具交互展示与程序化产出两种形态:返回的plotly.graph_objs.Figure列表可直接fig.write_html(...)落盘,适合嵌入投研流水线。- 口径一致性:报告中所有累积曲线、回撤、年化收益均为加和口径(
risk_analysis默认mode="sum"),横向对比外部复利口径指标时需先换算;如需几何口径,可在调用risk_analysis时显式传mode="product"。 - 相关示例代码可在 examples/nested_decision_execution/workflow.py 中找到(其中包含
analysis_position.report_graph(report_normal_df)的调用示意),配合本文各函数的 docstring 示例即可快速复现整套图形化评估流程。
通过上述 6 类报告,Qlib 把“组合层面(收益/回撤/成本/换手/排名)”与“模型层面(分层收益/IC/自相关)”的诊断完整图形化,覆盖从回测结果解读到模型打分能力验证的关键环节,是 Qlib 工作流中从 backtest/模型预测走向可交付投研结论的重要一环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


