ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qlib量化框架实战:模型训练、回测与结果分析全解

Qlib量化框架实战:模型训练、回测与结果分析全解 Qlib这套框架我从去年开始断断续续用了快一年前两篇笔记把Data Layer和Operator Layer聊得差不多了这篇把剩下的主要组件一次性说清楚。如果你正准备用Qlib搭一套自己的量化研究流程或者已经把数据准备那步跑通、现在卡在“模型怎么接、回测怎么跑、结果怎么分析”这个环节那这篇文章应该能帮你把整条链路的最后几块拼图补上。Part Ⅲ的定位很明确讲透Model、Workflow、Backtest、Analysis这几个核心组件各自负责什么它们之间怎么协作以及我在实际使用中踩过、后来摸清楚的几个坑。官方文档里很多概念散落在不同页面对新手不够友好这篇笔记换个讲法按一个实验从数据到结果的生命周期来走。1. 组件地图先搞清楚谁在谁后面1.1 我习惯的分层方式Qlib官方文档把MAIN COMPONENTS分成了Data Layer、Operator Layer、Model Layer、Workflow Layer、Backtest Layer和Analysis Layer。但对我来说文档里的分层更适合已经熟悉框架的人回顾刚开始上手时按“一个实验跑起来之后发生了什么”来理解会更快。阶段核心组件主要任务数据接入DataServer、DataHandler把原始行情/财务数据按统一格式接入划分日历和标的池特征工程Operator、ExpressionEngine在数据上生成因子比如Alpha158、Alpha360模型构建Model、Trainer定义模型结构完成训练和预测实验编排Task、Workflow把数据集、模型、记录器串成一条可复现的流水线模拟交易Executor、Strategy、Portfolio根据预测信号生成订单模拟成交并更新仓位结果分析Analysis评估预测质量、回测收益、风险指标前两篇笔记已经覆盖了表格前两行这篇从模型构建开始往后走。你会发现后面这些组件几乎都是围绕一个叫Workflow的东西转的而Workflow本身并不高深它其实就是一套约定好的配置结构和调度逻辑。1.2 为什么Part Ⅲ这样切分我见过不少新手卡在一个地方用qrun跑官方example能出结果但一换模型、一改策略就抓瞎。原因在于Qlib把很多步骤都用缺省配置帮你做了你不清楚每一步背后调了谁的类、传了什么参数。所以Part Ⅲ在内容上专门做了两个取舍一是把模型和训练过程挑出来单独讲因为自定义模型是大多数人迈向实战的第一步二是把回测和分析放一起讲因为它们的很多概念是贯通的比如预测信号怎么变成订单、订单又如何影响最终收益归因。这样切还有个现实原因Qlib的Data Layer和Operator Layer相对稳定一旦你理解了字段、日历、因子表达式后面很少改动而Model、Workflow、Backtest这三个环节是你做实验时反复修改的地方值得花更多篇幅去拆解。2. Model组件一套接口适配所有算法2.1 Qlib里的“模型”到底是个什么东西在Qlib中一个模型并不是某个具体的算法而是对外暴露统一接口的类。它必须至少实现两个方法fit用于训练predict用于预测。你用什么算法都行LightGBM、MLP、GRU、Transformer甚至是你自己写的回归模型只要把这两个方法按约定实现就能接入Qlib的整套工作流。这个设计很关键。它意味着你的实验对比脚本可以写得很干净数据集不变只改yaml配置里的model.class其他代码一行不用动就能比较不同模型的IC、Rank IC、回测收益。如果Qlib把每个模型都做成独立的调用方式想拍平对比就得写一堆胶水代码实验效率会低很多。我最早用Qlib时犯过一个错以为只有内置那几种模型能用。后来翻了源码才发现官方在qlib/contrib/model下面提供了线性回归、LightGBM、MLP、LSTM、GRU、Transformer、再加一部分强化学习算法的baseline基本覆盖了主流路子。但更实用的能力是你可以把自己的模型塞进去。2.2 同构训练接口从LightGBM到GRU为什么能无缝切换要理解这个“无缝切换”看一个简化版的自定义模型模板就明白了from qlib.model.base import Model from qlib.data.dataset import DatasetH class ToyModel(Model): def __init__(self, learning_rate0.01): self.learning_rate learning_rate def fit(self, dataset: DatasetH, num_workers0, **kwargs): # 拿到训练集的特征和标签 train_df dataset.prepare( train, col_set[feature, label], data_keyDataHandler.DK_I ) feature train_df[feature] label train_df[label] # 在这里做你的训练逻辑 ... def predict(self, dataset: DatasetH, segmenttest, **kwargs): # 拿到某个时间段的特征 test_df dataset.prepare(segment, col_setfeature, data_keyDataHandler.DK_I) # 在这里生成预测 pred pd.Series(...) return preddataset.prepare解决了数据供给问题fit和predict解决了训练和推理问题。Qlib内部在做实验调度时只认这两个方法名不会管你内部是树模型还是神经网络。这就是为什么在yaml里把model.class从LGBModel换成GRUModel然后把对应参数改一下整个workflow就能跑通。得益于这种接口同构Qlib把“模型选型”这种本该很重的工程问题压缩成了一个配置问题。2.3 模型注册与自定义模型照着BaseModel写就行除了直接在代码里实例化模型Qlib更推荐的方式是把模型注册到框架的模型注册表里这样yaml配置才能按名字引用。注册方式非常简单就是用框架自带的装饰器给模型类打标。from qlib.model.base import Model from qlib.utils import code_to_obj code_to_obj class ToyModel(Model): ...注册之后你在workflow配置里这样写model: class: ToyModel module_path: my_models.toy_model kwargs: learning_rate: 0.001Qlib的小型工具init_instance_by_config会按照module_path定位模块、按class找类名、再把kwargs作为参数实例化。这整套机制学起来有点绕但实际用起来很顺手——它让实验配置可读性大大提高也方便了参数网格搜索。这里提个忠告自定义模型时第一版不要追求花哨。先用最简单的线性回归或均值预测跑通接口确认fit和predict的数据形状、索引都对再往上叠加复杂度。很多人一上来就接Transformer结果数据对齐错了都没发现后面全是白费。3. 工作流组件Task、Trainer、Record如何串起一次实验3.1 qrun与一个典型的workflow配置Qlib里跑实验最常用的入口是qrun命令它接收一个yaml配置文件配置文件描述了一个完整的Task。Task是Qlib对“一次实验”的形式化定义它把数据集、模型、训练参数、记录器打包在一起让实验具备可复现性。我一般在examples/benchmarks/LightGBM这类官方目录的基础上改配置下面是一个去掉次要字段的精简版experiment_name: lgbm_alpha158 model: class: LGBModel module_path: qlib.contrib.model.gbdt kwargs: loss: mse learning_rate: 0.05 num_leaves: 64 early_stopping_rounds: 50 dataset: class: DatasetH module_path: qlib.data.dataset kwargs: handler: class: Alpha158 module_path: qlib.contrib.data.handler kwargs: start_time: 2008-01-01 end_time: 2020-12-31 fit_start_time: 2008-01-01 fit_end_time: 2014-12-31 segments: train: - 2008-01-01 - 2014-12-31 valid: - 2015-01-01 - 2016-12-31 test: - 2017-01-01 - 2020-12-31 task: fit: true predict: true record: - class: SignalRecord module_path: qlib.workflow.record_temp kwargs: model_name: lgbm_alpha158 - class: SigAnaRecord module_path: qlib.workflow.record_temp kwargs: ana_long_short: true ana_ic: true - class: PortAnaRecord module_path: qlib.workflow.record_temp kwargs: config: strategy: class: TopkDropoutStrategy module_path: qlib.contrib.strategy kwargs: topk: 50 n_drop: 5乍一看字段很多但拆开其实就四块model定义算法和超参dataset定义特征处理和切分task定义生命周期动作record定义训练后要记录哪些产物。Qlib拿到这个配置后会按顺序执行初始化模型、初始化数据集然后进入训练器和记录器的流程。3.2 Dataset的处理链与缓存机制很多人把Dataset简单理解成“特征数据”其实它更像一条处理链。Qlib里的Dataset由Handler负责生成原始特征比如Alpha158会把基础行情数据扩展成158维因子然后Dataset再按segments切出train、valid、test三段时间区间。初次运行时Dataset的处理结果会被持久化到缓存文件里。这样第二次跑同样配置就不用重新计算因子了能省下大量时间。但也正是这个缓存坑了不少人。你修改了因子表达式或者换了Handler参数如果不清理缓存跑出来的结果还是旧的。我习惯在改完特征配置后先删掉对应数据集目录下的缓存文件再跑实验。如果用的是notebook环境重启kernel也是个保险操作。等到确认特征逻辑稳定了再依赖缓存提速也不迟。Dataset这块的另一个经验是segments的时间段要和Handler的fit窗口配合好。常见错误是Handler用全区间数据做标准化然后你拿前一段数据做训练这会造成标签泄漏。Qlib为此设计了fit_start_time和fit_end_time专门控制因子处理器的拟合区间正是用来避免这种问题的。3.3 Trainer训练器训练、预测、记录各司其职Trainer是Workflow里很容易被忽略的组件。它本身不实现算法但负责把“训练动作”和“记录动作”编排起来。fit阶段Trainer调用模型的fit方法并传入Datasetpredict阶段Trainer调用模型的predict方法得到预测信号在predict之后它还会触发SignalRecord、SigAnaRecord这些记录器去保存结果。之所以多个Record类是因为一个实验通常需要多个产物预测信号要存下来IC分析要单独算回测结果还要根据组合策略再跑一遍。Qlib把记录器设计成列表允许你同时挂多个跑完一个Task后所有结果都会按实验名归档到mlruns目录下。如果你是自己用脚本写实验循环要注意Trainer默认会把每次实验存成独立运行记录。同一个配置跑多次会自动生成新的run id不会互相覆盖。刚开始可能觉得多此一举但当你做参数对比时就会感谢这个设计因为它保证了每个实验的结果都有迹可循。4. 回测组件从预测信号到真实交易模拟4.1 回测里的几个关键角色预测信号本身不能直接说明策略盈亏必须放进一个模拟交易环境里跑一圈。Qlib的回测组件不像有些框架那样把“买什么、卖什么”写死在代码里而是拆成几个互相配合的角色。Strategy负责根据预测信号和当前持仓决定要交易哪些标的、按什么权重调仓。比如TopkDropoutStrategy会一直持有得分最高的Topk只股票每隔N天调仓把掉出前Topk的剔掉把新进入的加进来。Executor负责把策略生成的order执行掉。它会模拟成交考虑滑点、交易成本、涨跌停约束。Account和Position负责记录现金、持仓、交易流水相当于账本。RiskManager负责组合风控比如控制单一股票的持仓上限。我在实际跑实验时最常用的是TopkDropoutStrategy加默认的SimulatorExecutor。它简单、直观、回测结果容易解释适合做baseline。后续如果要贴近实盘可以自己写Strategy把过滤停牌、处理涨跌停的逻辑加进去。下面是一段常见的回测配置片段可以看到策略和执行器都是可以任意替换的portfolio: class: SignalPortfolio module_path: qlib.workflow.portfolio kwargs: strategy: class: TopkDropoutStrategy module_path: qlib.contrib.strategy kwargs: topk: 20 n_drop: 5 executor: class: SimulatorExecutor module_path: qlib.backtest.executor kwargs: time_per_step: daytime_per_step: day表示按日级调仓。Qlib回测支持day、week、month等不同频率选哪一种要跟你的策略逻辑匹配。如果你做的是中低频价量因子日频够了如果信号本身就来自日线硬去做小时级回测反而会引入大量不真实的噪声。4.2 回测中常见的坑停牌、涨跌停和成本估计回测看起来简单但真实细节非常多。最容易翻车的是标的池里混入了停牌股或一字板股票。默认执行器会基于行情快照模拟成交但如果你没对标的池做可交易性过滤回测结果会很乐观因为你认为能买到的价格实际上排队买不到。我吃过一次不小的亏某次回测年化收益率多出十几个点最后排查发现是一批停牌复牌后一字涨停的股票被我按一字板价格成交了。此后我在回测前都会先过滤掉当天不可交易的标的或者至少在解读结果时标注“未考虑涨跌停成交限制”这个假设。另一个容易被忽略的问题是交易成本。Qlib默认支持在Executor里配置交易成本如果你用的是官方示例配置成本参数往往很保守。做策略对比时成本参数最好固定否则你很难区分收益差异是策略带来的还是成本假设不同带来的。回测还有个细节是换手率。组合调仓卖出旧票买入新票会产生成本换手率越高成本拖累越明显。所以分析回测结果时我会同时看两个东西总收益和换手率。一个策略收益高但换手率奇高实盘跑起来很可能被成本吃光收益。5. 分析组件用IC和收益曲线判断模型好坏5.1 结果分析器都有哪些花样训练完模型、跑完回测Qlib会把预测信号、组合权重、账户净值都存下来。接下来就要靠分析组件把这些原始产物翻译成人类能看懂的指标。其中SignalRecord保存的预测值对应两类最基础的衡量指标IC和Rank IC。IC是预测值与未来收益的相关系数Rank IC则是用排名计算的相关系数。IC越高说明预测因子的线性预测能力越强。Rank IC不受异常值影响更稳健。正常有效的日频价量因子Rank IC大概在0.03到0.06之间超过0.1已经算很优秀了。SigAnaRecord会额外输出ICIRIC的均值除以标准差这个指标衡量因子预测能力的稳定性。两个因子IC均值相同ICIR更高的那个更可信因为它的预测效果更稳定。PortAnaRecord则输出回测层面的指标累计收益、年化收益、最大回撤、夏普比率、换手率等。这些指标在回测报告里都有但其中最大回撤最值得关注。很多模型在测试集上平均表现不错但遇到极端行情回撤会异常大说明模型的风控能力不足。5.2 从回测报告反推模型选型我一般在一次实验结束后会先看三个维度IC和Rank IC判断预测质量累计收益和回撤判断策略表现换手率判断可交易性。如果三者里有明显短板再决定是调模型还是调策略。如果IC不错、但回测收益差问题很可能出在策略层。比如调仓频率不合适、Topk设置太大、交易成本假设不合理。如果IC和收益都不行问题往往在特征层或模型层优先检查因子逻辑是否正确、训练数据是否泄漏。Qlib的分析组件还支持做多组实验的横向对比。我在做模型选型时会在同一份数据集上分别跑LightGBM、GRU、Transformer然后用同一个TopkDropout策略回测最后把所有指标拉到一张表里看。这个流程很机械但正是因为Workflow把实验步骤标准化了才能这样批量对比。指标用处主观参考IC判断预测与未来收益的相关性绝对值长期看是否稳定大于0.02Rank IC剔除异常值影响后的信号强度比IC更稳波动更小ICIR信号预测能力的稳定性越大越好通常大于0.3年化收益直观的投资回报需要结合回撤看最大回撤极端风险水平越小越稳换手率成本敏感度越高实盘越难兑现6. Part Ⅲ踩坑随笔几个容易被文档绕晕的细节6.1 自定义模型注册后不生效先想想是不是缓存没清有一次我改完模型结构重新跑实验结果输出的指标跟之前一模一样怎么自查都没发现代码问题。后来才反应过来模型实例被Qlib在进程内缓存了notebook环境里没重启kernel旧的模型对象一直在被复用。解决办法不复杂写完模型代码需要重新加载的模块最好手动reload或者干脆重启kernel让它重新走一遍模块导入流程。如果你在脚本环境里跑那就重新启动进程别省那几秒。这件事给我的教训是Qlib的注册机制虽然方便但它会掩盖“代码更新没生效”的问题。我后来自定义模型时都会先在fit方法里打一条日志确认跑的是新代码。6.2 qrun跑workflow时路径和日志信息一定确认好qrun在哪个目录下执行决定了相对路径的基准。如果配置文件里用了相对路径而你又恰好换了执行目录数据集索引和缓存路径就会对不上各种找不到文件的报错接踵而来。我现在的习惯是每个实验项目开一个独立目录yaml配置里尽量用绝对路径或者统一在项目根目录下执行qrun。日志方面Qlib会把实验细节打到mlruns对应run的目录下跑完先看日志尾部基本能定位90%的问题。另外一点不要忽略experiment_name和run_id。很多人实验跑多了以后翻回去找不到哪次是哪次。我给实验命名时会带上日期和模型简称方便后续回溯。6.3 训练段和回测段时间切分要严格对齐预测和回测的实验设计里最容易出错的是时间切分。训练集用的是2008到2014验证集是2015到2016回测你当然会觉得应该跑三年测试集但实际一写配置可能segment写错、或者Handler的start_time包含了未来数据。为了避免这种问题我会在跑回测前先单独打印一次数据集的segments确认范围确认测试集结束日期和回测日期一致再动手。这种“先看一眼再跑”的习惯帮我挡下了不止一次数据泄漏事故。Qlib的灵活性很高但这份灵活性也需要你对自己的实验设计有清晰边界。每次跑实验前我都会在心里过一遍模型看到的数据、策略交易的标的、回测统计的时间段这三者之间有没有不该存在的重叠。按我个人的使用体验来说Qlib的学习曲线不算平缓但一旦你把Model、Workflow、Backtest、Analysis这几个组件的协作关系捋顺了后面做实验的效率会成倍提升。这篇文章提到的坑基本都是文档里不太会写、但实际操作中大概率会遇到的细节希望能给你省下一些排查时间。
返回列表