从去年开始,我把研究重心从传统多因子模型转向了机器学习量化方向。最开始的路子很原始:pandas算因子、sklearn训模型、回测脚本自己写,每个环节单独跑都挺顺,连在一起就各种对不齐。后面在GitHub上翻到微软开源的Qlib(微矿),发现它把AI量化投资的整条链路——底层行情存储、特征工程、模型训练、回测分析——全部统一到了一个框架里,正好补上我一直缺的"拼装缝"。
这篇笔记是我从零开始学习与使用Qlib的实践记录,主要内容包括选型理由、环境搭建、数据准备、核心概念拆解、跑通LightGBM全流程的实操过程,以及几个差点让我放弃的坑。后续如果继续深入,会随缘更新。先给结论:如果你有Python基础、懂一点机器学习常用模型,但不想在数据基建上花大量时间,Qlib是目前开源生态里最值得下功夫研究的AI量化平台。
1. 为什么最终选了Qlib:横向对比后的结论
1.1 之前自建方案的痛点
在遇到Qlib之前,我维护过一套完全自己搭的量化研究流程。每天的流程是:收盘后手动拉行情数据,写脚本计算各类技术指标,把结果拼成一张大宽表,再训练模型,最后写一个简单回测来验证效果。听起来结构清晰,但实际操作中每个环节都有"隐藏税"。
就拿复权处理来说,分红送转会导致价格跳空,不复权因子没法用,前复权后复权在不同场景下又不一样。停牌股要不要剔除、涨跌停的成交假设怎么设定、交易日历怎么对齐,这些看起来基础的问题,每一个都要自己写代码处理,而且写完之后还得反复验证正确性。等到真正想优化模型的时候,精力已经被这些基建耗掉一大半。
1.2 为什么Qlib和它们不一样
市面上常见的量化开源工具各有侧重,我试用过后做了一个粗略对比:
| 工具/方案 | 定位 | 对AI量化支持 |
|---|---|---|
| 自建pandas流程 | 完全自定义 | 全部自己造轮子,灵活但费时 |
| backtrader | 回测与实盘交易框架 | 偏向CTA/规则策略,机器学习支持弱 |
| zipline | 事件驱动回测引擎 | 支持自定义因子,但AI流程不完整 |
| TA-Lib | 技术指标计算库 | 只负责指标,上下游不管 |
| Qlib | AI量化全流程平台 | 数据、特征、模型、回测一体化 |
一个典型的机器学习选股流程包括:行情数据管理、因子计算、样本标注、数据预处理、训练集/验证集切分、模型训练、预测、回测、绩效归因。大部分工具只覆盖其中一到两个环节,而Qlib从设计之初就是奔着覆盖整个流程去的。它内置的Alpha158、Alpha360因子集,统一的数据接口,以及可复现的工作流机制,让我终于可以把注意力放回模型本身,而不是整天和底层数据较劲。
1.3 什么情况下适合选Qlib
用Qlib顺手的场景,我总结是这几类:
- 以日频或更低频的选股策略为主,不是日内高频交易
- 想快速验证"某个特征+某个模型"到底有没有效果
- 需要一个可复现的实验框架,方便记录和回溯每次改动
- 希望在模型部分自由发挥,但又不想从零写数据层
反过来,如果你的核心需求是分钟级以内的交易执行模拟,或者复杂的组合优化与风控系统,Qlib就显得不是那么对口,它最舒服的定位还是在日频选股研究。
2. 环境搭建与数据准备:这里其实藏着不少坑
2.1 环境配置的两种方式
我前后装过两次Qlib。第一次图省事,直接在原有Python环境里pip install pyqlib,结果和项目里已有的numpy、pandas版本发生冲突,后面为了兼容来回折腾了很久。第二次我学乖了,用conda建了一个独立虚拟环境,一次通过。
conda create -n qlib python=3.8 -y conda activate qlib pip install pyqlib对大多数使用场景,我建议直接安装PyPI上的发布版本,稳定且省事。如果是打算研究源码、甚至自己改框架内部逻辑做二次开发,再考虑源码安装:
git clone https://github.com/microsoft/qlib.git cd qlib pip install -e .源码安装会把仓库以开发模式链接进Python环境,改代码之后不需要重新安装就能生效,调试比较方便,但日常使用没有必要。
2.2 数据下载:一次需要耐心的初始化
Qlib官方为不同市场提供了公开数据集,中国市场数据可以通过命令行直接下载:
python -m qlib.run.get_data qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn这里一定要有心理准备:下载的数据量很大,整个文件包含全市场股票的行情,不是几分钟能搞定的。我第一次执行时,输出日志半天没动静,一度以为程序卡死了,后来才发现是在持续下载。建议动手之前先确认磁盘空间至少剩余20GB,避免下载到一半空间不足,前功尽弃。
如果网络条件一般,下载过程可能会中断。好在Qlib的下载脚本支持断点续传,重新执行一次同样的命令会继续未完成的部分,不需要删掉重来。
2.3 数据目录的底层结构
下载完成后,Qlib数据目录下有三个关键子目录:
calendars/:交易日历文件,标记了每个市场的有效交易日features/:每只股票的量价特征文件,按股票代码存放,使用自研的.bin二进制格式instruments/:股票池定义文件,比如all全市场、csi300、csi500等
这套结构是Qlib数据层的地基。所有后续查询都围绕这三类文件展开,理解它们之后,再看官方文档会顺畅很多。
2.4 如何验证环境和数据就绪
环境装好、数据下完后,建议先跑一段初始化验证代码:
import qlib from qlib.config import REG_CN qlib.init(provider_uri="~/.qlib/qlib_data/cn_data", region=REG_CN) # 获取交易日历 cal = qlib.data.D.calendar(start_time="2022-01-01", end_time="2022-12-31") print(len(cal)) # 获取股票池 instruments = qlib.data.D.instruments(market="csi500") # 获取个股行情 df = qlib.data.D.features( instruments=["SH600000"], fields=["$open", "$high", "$low", "$close", "$volume"], start_time="2022-01-01", end_time="2022-12-31" ) print(df.head())如果这段代码能正常打印DataFrame,说明安装和数据都正常。这里我强烈建议先用少量个股做验证,不要一上来就跑全市场,否则一旦出错很难定位是数据问题还是逻辑问题。
3. 数据流水线拆解:从K线到特征矩阵
3.1 DataHandler与Dataset的区别
这是初学Qlib最容易混淆的一对概念。DataHandler负责从底层读取原始行情数据,并按照你配置的规则生成特征和标签;Dataset则是在DataHandler之上进一步封装,给模型训练提供按时间段切分好的数据对象。
可以这样理解:DataHandler是"数据加工厂",把原始面粉(行情)加工成面团(含特征和标签的宽表);Dataset是"分餐员",把面团切成训练期、验证期、测试期对应的份量,方便模型在严格隔离的数据段上训练和评估。
3.2 Processor:数据预处理的执行者
DataHandler里最核心的配置就是一组processor。每个processor负责一种特定的数据变换:
DropnaLabel:删除标签列存在缺失的样本Fillna:用指定方法填充缺失值MinMaxNorm:做最小最大值归一化CSZScoreNorm:按股票维度做z-score标准化
这里有一个必须严格遵守的原则:数据预处理的统计参数,比如均值、标准差、最大最小值,只能在训练集上计算,然后把这个变换原样应用到验证集和测试集上。如果对全体数据混在一起做标准化,等于是让模型在训练时偷看了测试集的分布信息。
Qlib用两个配置字段来区分这两种场景:learn_processors用于训练集拟合,infer_processors用于验证集和测试集推理。第一次使用时不注意区分,很容易在回测里得到虚高结果,这个问题我后面还会单独展开。
3.3 Alpha158与Alpha360:两套内置因子集
Qlib最省心的地方,是内置了Alpha158和Alpha360两套预置特征集。
Alpha158基于量价数据自动生成158个特征,覆盖移动平均、标准差、分位数、动量、斜率等常用维度。它不依赖任何外部数据,仅凭开高低收和成交量就能构建出相当丰富的基础特征。Alpha360则是把过去360个交易日的数据按时间窗口排列,适合用LSTM、GRU、Transformer这类序列模型进行学习。
对于刚开始上手的人,我的建议非常明确:先上Alpha158加LightGBM。原因是Alpha158已经涵盖了传统量价因子的主要形态,而LightGBM对缺失值和量纲不敏感,不需要额外做大量清洗,训练速度快,效果也足够扎实。等完全跑通这条基线,再去尝试Alpha360加深度学习模型,才不会在复杂模型和长训练时间面前手足无措。
3.4 标签设计:预测目标如何设定
Qlib中模型训练的目标通常是未来N个交易日的收益率。Alpha158配置里默认使用的LabelStock类,会按你设定的窗口长度计算未来的平均收益或累计收益作为标签。
这里有个关键的设计决策:标签窗口长度要和策略调仓频率匹配。做周频调仓,标签长度配置为5个交易日比较合理;做月频调仓,则配置为20个交易日左右。窗口越长,信号越平滑,换手率通常越低,但对市场变化的响应也会变慢;窗口太短,信号衰减快,交易成本对净收益的侵蚀会更加明显。这个平衡需要在实盘环境中反复验证,不是越大或越小就一定更好。
3.5 一份典型的Dataset配置
实际使用中,Dataset配置一般写在YAML文件里,用YAML声明结构比写代码更直观:
dataset: class: DatasetH module_path: qlib.data.dataset kwargs: handler: class: Alpha158 module_path: qlib.contrib.data.handler kwargs: start_time: 2018-01-01 end_time: 2022-12-31 fit_start_time: 2018-01-01 fit_end_time: 2021-12-31 instruments: csi300 segments: train: [2018-01-01, 2020-12-31] valid: [2021-01-01, 2021-12-31] test: [2022-01-01, 2022-12-31]注意segments里的时间区间定义了训练、验证、测试三个阶段,而fit_start_time和fit_end_time需要和训练区间保持一致。这个细节非常重要,它直接决定了标准化参数的拟合范围,一定不能随手写成整个数据集的时间范围。
4. 第一次跑通完整流程:LightGBM加Alpha158的实战
4.1 用qrun一键跑通
Qlib把训练回测的完整流程封装成了可复现的工作流,直接用官方示例命令行就能跑起来:
python qlib/workflow/cli.py --conf qlib/examples/benchmarks/LightGBM/workflow_config_lightgbm_Alpha158.yaml这条命令背后发生的事情比你想象的多:读取配置、构建Alpha158特征、切分数据段、训练LightGBM模型、生成全样本预测信号、执行回测、输出各项绩效指标。第一次看到日志一行行输出时的感觉,确实比自己拼装的那套流程省心太多。
4.2 配置文件的核心构成
这个YAML配置包含三大部分:模型、策略、回测参数。先看模型部分:
model: class: LGBModel module_path: qlib.contrib.model.gbdt kwargs: loss: mse colsample_bytree: 0.8879 learning_rate: 0.0421 subsample: 0.8789 lambda_l1: 205.6999 lambda_l2: 580.9768 max_depth: 8 num_leaves: 210 num_threads: 20这些超参数是官方示例中调好的LightGBM参数,直接使用就能获得不错的基线结果。想换成其他模型也很简单,比如GRU、LSTM等,都有对应的配置模板可以参考,不需要改框架本身。
回测策略部分使用了Qlib自带的TopkDropoutStrategy:
port_analysis_config: strategy: class: TopkDropoutStrategy module_path: qlib.contrib.strategy.signal_strategy kwargs: signal: model topk: 50 n_drop: 5这个策略的核心逻辑是:根据模型预测收益率对全市场股票排序,买入预测最高的50只组合,每次调仓只替换掉5只。这样既能跟随信号变化,又避免了频繁换仓带来的高额交易成本。
回测执行参数则定义了交易规则:
backtest: start_time: 2022-01-01 end_time: 2022-12-31 account: 100000000 benchmark: SH000300 exchange_kwargs: limit_threshold: 0.095 deal_price: close open_cost: 0.0005 close_cost: 0.0015 min_cost: 5这里指定了回测区间、初始资金、比较基准,以及涨跌停限制和交易费用。limit_threshold: 0.095表示涨跌停按9.5%计算,比A股的实际限制稍微保守;deal_price: close表示以收盘价成交;手续费率则分为买入和卖出两档。
4.3 怎么看懂回测报告
跑完流程后,Qlib会在mlruns目录下生成完整的实验记录,包含预测信号、回测净值曲线、风险指标报告。核心指标我习惯重点关注这几个:
| 指标 | 含义 | 参考判断标准 |
|---|---|---|
| IC | 预测值与未来收益的相关系数 | 日频0.03以上说明有一定预测力 |
| Rank IC | 用排名计算的IC,降低极端值影响 | 比IC更稳健 |
| 年化收益 | 扣费后的年度化收益 | 结合回撤一起看 |
| 夏普比率 | 每单位风险的超额回报 | 1.0以上可接受 |
| 最大回撤 | 组合净值最高点到最低点的跌幅 | 越小越稳 |
4.4 从基线结果开始迭代
我第一次跑通回测时,看到的年化收益相当可观,一度觉得自己要发财了。冷静下来仔细检查后才发现,这里面有预处理设置不当带来的虚高成分,这在下一节的"踩坑"部分会详细展开。我的建议是:拿到一份回测结果,不要急着开心,先确认配置里有没有信息泄露,再谈优化。
5. 实战中踩过的坑与调优经验
5.1 信息泄露:最隐蔽的收益造假源头
这是我在初学阶段踩过最大的坑。当时为了图方便,我对整个数据集先做了z-score标准化,然后再切分训练集和测试集。结果测试集上的IC和收益表现异常得好,好到我开始怀疑自己的量化天赋。
实际情况是,标准化时计算均值和标准差用到了测试集的数据,相当于模型在训练阶段就"偷看"了未来的分布信息。表现在结果上就是回测曲线特别平滑、收益特别高,但一到实盘就会原形毕露。
Qlib本身提供了learn_processors和infer_processors的设计来规避这个问题,关键在配置时要理解:训练阶段的预处理统计量只从训练集计算,然后用同样的变换处理测试集。我后来养成一个习惯:每次拿到回测结果,先随机抽几个样本检查测试集特征的均值和方差是否与训练集明显不同。如果几乎一模一样,大概率就是出问题了。
5.2 时间重叠:标签窗口带来的隐性依赖
另一个容易被忽略的问题是训练集和验证集在时间上的重叠。假设标签是未来20日收益,那么第N个交易日的样本特质,其实和它前后20个交易日的样本高度相关。换句话说,如果训练集结束于1月31日,验证集开始于2月1日,那么验证集开头这些样本的标签,在很大程度上有信息是训练集末尾样本标签所蕴含的。
解决办法是在训练和验证之间留出足够的"断带"。我个人做法是,使用20日标签时,训练集结束到验证集开始至少间隔一个月的交易日。这个间隔虽然让可训练样本变少,但能保证验证结果不被时间重叠污染,真实性优先。
5.3 交易成本和涨跌停限制的敏感度
回测中的交易参数对最终结果影响巨大。我把涨跌停限制从默认的9.5%调低到7%做过对比,结果年化收益明显下降。原因很好理解:当模型预测某只股票即将涨停时,实际交易中很可能一字板买不进去,但回测却默认按收盘价成交,导致收益被高估。
因此我建议在跑回测时,至少要检查两档参数:一档是乐观假设,一档是悲观假设。看看策略表现在不同假设下的差异。如果差异超过预期,说明策略对交易成本敏感,实盘时要更加小心。
5.4 自定义模型的接入方法
用了一段时间LightGBM之后,自然会想尝试自己的模型。Qlib的自定义模型接口设计得相当简洁:继承qlib.model.base.Model,实现fit()和predict()两个方法即可。
from qlib.model.base import Model class MyModel(Model): def __init__(self, **kwargs): super().__init__(**kwargs) # 初始化自己的模型结构 def fit(self, dataset): # 在这里实现训练逻辑 # dataset对象可以直接取出训练用的特征矩阵和标签 pass def predict(self, dataset): # 在这里实现推理逻辑 # 返回一个以日期为索引、股票代码为列的概率或收益预测值 return pd.Series(...)写好后,在YAML配置中指定类名和模块路径,Qlib就能像调用内置模型一样调用你的自定义模型。整个流程不需要改动框架源码,非常干净。
5.5 资源占用与训练速度
深度学习模型对硬件的要求明显高于LightGBM。我目前的工作站配置是64GB内存加一块RTX 3080显卡,训练Alpha360特征的GRU模型时显存比较紧张。如果你也遇到类似问题,优先尝试调小batch size,或者干脆换回Alpha158特征集做降级方案。
内存方面,全市场数据的特征矩阵非常占空间。一个有用的技巧是,在DataHandlerLP配置里使用分段处理或按股票池分批加载,避免一次性把整张特征宽表驻留在内存里。这个优化在大规模回测时尤其管用。
5.6 复现性管理
量化实验最怕的是"这次效果好,但下次复现不出来"。Qlib的配置驱动设计天然适合做实验管理。我现在每次跑实验,都会把YAML配置文件、数据版本信息、Qlib commit号一并记入交付清单。这样一来,任何一个实验结果都能回溯到源码、数据和参数的具体组合,排查问题的时候方便得多。
6. 当前进度与后续更新计划
写到这里,Qlib最核心的日常使用链路已经覆盖得比较完整了:环境、数据、流水线、训练、回测、踩坑经验。从这大半年的实际使用来看,Qlib帮我省掉的基建时间非常可观,让我能把精力集中在真正有意义的研究问题上——因子构造、模型迭代、交易成本控制。
我现在正在推进两件事:一是尝试把另类数据源接入Qlib,把舆情因子和新闻情绪量化后加入Alpha158特征集,看看能否给模型带来增量信息;二是研究Qlib内置的强化学习相关模块,探索用强化学习做动态仓位管理的可行性。这两块如果做出阶段性成果,会以随缘更新的方式补充到这篇笔记里来。
最近养成的一个习惯是:每次跑完实验,都会顺手把配置文件、生成的报告和分析结果放进同一个Git仓库,打好标签。因为Qlib的工作流是可复现的,文件配置、数据版本、模型输出这三者的组合决定了实验的唯一性。这个习惯坚持下来之后,不管是回头复盘几个月前的想法,还是对比不同参数组合的效果,都省力很多。
Qlib这套框架还在持续迭代,社区也陆续有人在分享基于它的研究和生产实践。如果你对数