2. Qlib基础回顾
在进入多模型集成与信号融合之前,有必要把 Qlib 的整体骨架重新梳理一遍。很多实战问题——例如“为什么模型训练完了却回测不出信号”“为什么数据更新后因子值对不上”——根源往往不在模型本身,而在于对 Qlib 分层职责与工作流机制理解不清。本节围绕架构分层、数据层、模型层、策略层与工作流五个部分展开,帮助你建立一张可复用的“Qlib 心智地图”。
一、核心原理:Qlib 的分层架构
Qlib 的设计遵循“数据—模型—策略—执行”的解耦思想。它把量化研究拆成若干职责单一的层,每层通过标准接口通信,从而让研究者可以独立替换某一层而不影响其他层。整体上可划分为四层加一条工作流主线:
- 数据层(Data Layer):负责原始行情、财务、因子等数据的存储、表达式计算与高效读取,核心是
DataHandler与表达式引擎。 - 模型层(Model Layer):负责从特征到预测信号的映射,核心是
Model基类及其派生模型(如 LightGBM、LSTM、Transformer)。 - 策略层(Strategy Layer):负责把预测分数转换为具体交易决策,核心是
Strategy基类,如 TopkDropoutStrategy。 - 执行与回测层(Execution & Backtest):负责撮合、成本计算、持仓管理与绩效评估,核心是
Executor与Backtest。 - 工作流(Workflow):以
qrun与 YAML 配置为入口,把上述各层串联成可复现的流水线。
理解这张分层图的关键在于:每一层只关心自己的输入输出契约。数据层输出的是带时间索引的特征矩阵,模型层输出的是预测分数,策略层输出的是目标持仓,执行层输出的是成交与净值曲线。多模型集成本质上是在“模型层”与“策略层”之间插入一个信号融合环节,因此必须先看清这两层的接口形态。
二、数据层:表达式引擎与 DataHandler
Qlib 数据层最容易被低估的能力是表达式引擎。你可以在配置中直接写 Ref($close, 1)、Mean($volume, 5) 这类表达式,Qlib 会将其解析为算子并高效计算,而无需手动写 pandas 循环。数据以二进制格式(.bin)按列存储,配合内存缓存,读取速度远快于逐行 CSV。
数据层最常用的组件是 DataHandler,它决定了“模型看到什么”。典型配置如下:
data_handler_config:
start_time: 2018-01-01
end_time: 2022-12-31
fit_start_time: 2018-01-01
fit_end_time: 2020-12-31
instruments: csi300
infer_processors:
- class: RobustZScoreNorm
kwargs:
fields_group: feature
clip_outlier: true
- class: Fillna
kwargs:
fields_group: feature
learn_processors:
- class: DropnaLabel
- class: CSRankNorm
kwargs:
fields_group: label
这里有两个高频误区。第一,把 fit_start_time 与 start_time 混为一谈:前者用于拟合归一化参数,必须严格早于或等于训练区间,否则会造成未来信息泄露。第二,忘记 learn_processors 中的标签处理:标签若未做截面排序或去极值,模型学到的分布与回测评估口径会不一致。
三、模型层与策略层:接口与职责边界
模型层的统一接口是 fit(dataset) 与 predict(dataset)。无论底层是树模型还是深度网络,只要实现这两个方法,就能被工作流调度。模型输出的 pred 通常是一个 DataFrame,索引为 (datetime, instrument),值为预测分数。
策略层接收这个分数,并决定买卖。以 TopkDropoutStrategy 为例,它按分数排序取前 K 只,同时按 dropout 规则替换部分持仓,以控制换手。其核心参数包括 topk、n_drop、signal 等。策略层不关心分数是怎么来的,只关心分数的高低排序是否稳定。
| 层级 | 核心组件 | 输入 | 输出 | 常见误区 |
|---|---|---|---|---|
| 数据层 | DataHandler / 表达式引擎 | 原始行情、财务数据 | 特征矩阵 + 标签 | 归一化拟合区间泄露未来 |
| 模型层 | Model 基类及派生模型 | 特征矩阵 | 预测分数 pred | 训练/验证切分未按时间 |
| 策略层 | Strategy 基类 | 预测分数 | 目标持仓 | 忽略交易成本与换手约束 |
| 执行层 | Executor / Backtest | 目标持仓 | 成交、净值曲线 | 成本参数与实盘不符 |
| 工作流 | qrun + YAML | 配置文件 | 可复现实验 | 配置散落、无法版本管理 |
四、工作流:qrun 与 YAML 配置
Qlib 用一份 YAML 描述整个实验:数据、模型、策略、回测全部声明式配置,然后通过 qrun 一键执行。这带来的最大好处是可复现——同一份配置在任何机器上都能跑出相同结果(在数据一致的前提下)。
qrun workflow_config_lightgbm.yaml
一个典型工作流配置的结构如下:
qlib_init:
provider_uri: "~/.qlib/qlib_data/cn_data"
region: cn
market: &market csi300
benchmark: &benchmark SH000300
data_handler_config: *data_handler_config
task:
model:
class: LGBModel
module_path: qlib.contrib.model.gbdt
kwargs:
loss: mse
learning_rate: 0.05
dataset:
class: DatasetH
kwargs:
handler:
class: Alpha158
kwargs: *data_handler_config
segments:
train: [2018-01-01, 2020-12-31]
valid: [2021-01-01, 2021-12-31]
test: [2022-01-01, 2022-12-31]
record:
- class: SignalRecord
- class: PortAnaRecord
注意 segments 的时间切分必须严格按时间顺序,且 train 与 valid 不能重叠。很多“回测虚高”的案例,都是因为验证集混入了训练区间,导致模型选择阶段就已经过拟合。
record 决定了你最终能看到什么。若只配置 SignalRecord,你只能拿到预测分数;要看到净值曲线与回撤,必须加上 PortAnaRecord 并配置回测参数。多模型集成时,建议先统一记录格式,再做信号对齐。五、案例复盘:一次“信号对不上”的排查
某次实验中,研究者训练了 LightGBM 与 LSTM 两个模型,想把两者分数平均后送入策略,却发现融合后的回测反而变差。排查过程如下:
- 检查两个模型的
pred索引是否一致——发现 LSTM 的预测日期比 LightGBM 少了一天,原因是 LSTM 的step_len导致首日无法预测。 - 检查分数分布——LightGBM 输出的是回归值,LSTM 输出的是经过 tanh 的值,量纲差异巨大,直接平均等于让 LSTM 主导。
- 检查策略层——
TopkDropoutStrategy只关心排序,但量纲差异会改变排序结果,尤其在分数接近时。
最终解决方案是:先对两个模型的分数做截面排序归一化,再对齐日期索引,最后融合。这个案例说明,多模型集成的第一步不是“融合算法”,而是把各层接口对齐。数据层、模型层、策略层的契约一旦清晰,融合才有稳定的基础。