基于 Datawhale 开源量化投资学习指南10基于 LightGBM 的量化选股说实话做量化选股走到 LightGBM 这一步算是从人肉挑因子过渡到了让模型自己找规律的关键节点。我自己刚接触量化的时候第一反应跟大多数人一样拿通达信写公式把 MACD、KDJ、RSI 这些指标堆一起然后回测一下看看收益曲线。运气好的时候能跑赢指数运气不好就是过拟合到亲妈都不认识。后来看了 Datawhale 开源量化投资学习指南的系列内容才逐步把重心转移到梯度提升树这类模型上尤其是 LightGBM它几乎成了我日常选股实验的默认工具。这篇内容对应学习指南第 10 期的主题核心就一件事怎么用 LightGBM 做量化选股。我会把从数据准备、特征工程、模型训练到回测评估的完整链路捋一遍重点讲那些课程里不一定会细说、但实操中一定绕不开的细节。比如 lightgbm 的树个数到底怎么定、回归和排序目标函数有什么区别、以及 xendcg 这种听起来很唬人的东西到底在选股场景里有什么用。适合刚完成因子分析、准备上模型的读者也适合已经在用 XGBoost、想看看 LightGBM 有什么不同的朋友。1. 为什么是 LightGBM量化选股里的梯度提升到底在解决什么问题1.1 选股问题如何被翻译成机器学习任务先把底层逻辑捋清楚。选股本质上是从几千只股票里挑出未来表现相对更好的那一批而机器学习做的事情就是学习一个从特征到未来收益的映射函数。标准的建模方式是在每个调仓日比如每月最后一个交易日取每只股票当前的因子数据作为特征用未来一段时间的收益作为标签然后训练模型。预测时模型对所有股票打分排序取分数最高的 N 只构成组合。整个过程听起来不复杂但真正的工程量藏在特征怎么构建和标签怎么定义上。LightGBM 在这个流程里扮演的是监督学习器。它不需要你假设因子和收益之间是线性关系也不需要处理复杂的特征交互——树模型天然就能捕捉非线性关系。比如市盈率和未来收益的关系并不是单调的低估值未必涨高估值未必跌这种 U 型或者倒 U 型的关系在决策树分裂时会被逐步切出来。1.2 决策树模型在金融数据上的天然优势与局限金融数据是出了名的信号弱、噪音大。你花半天构造一个因子单因子的 IC 普遍在 0.02 到 0.05 就不错了换算成相关系数低得让人怀疑人生。线性回归在这种场景下很容易被极端值带着跑而树模型对异常值没那么敏感因为分裂时只看排序关系而不是具体数值大小这算是一个天然的优势。另一个优势是特征尺度无关。做多因子分析时有的因子是估值类数值范围从几到几千有的是动量类可能是百分比还有的是换手率这种小数。如果是神经网络或者逻辑回归你得分箱、标准化、处理共线性一堆前置工作。LightGBM 只需要把缺失值处理好剩下的它自己会通过直方图算法找到合适的分裂点省了很多事。但树模型也有明显的短板它很难外推。树模型的预测值是训练样本叶节点的均值或中位数意味着它永远无法预测出超出历史范围的极端收益。好在选股任务并不需要预测极端值我们只关心相对排序所以这个短板影响不大。数据量方面A 股市场每月一次调仓10 年历史也就 120 个月每期 3000 到 5000 只股票加起来也就几十万条样本。这个量级对 LightGBM 来说是小意思几分钟就能训完一轮这也是它非常适合量化选股实验的原因之一——迭代成本低可以频繁试错。2. 数据基建没有干净的数据模型再强也是白搭2.1 因子数据的对齐与清洗很多人第一次跑 LightGBM 选股模型代码写得没问题但结果一塌糊涂最后发现是数据对齐出了问题。量化里最典型的错误是未来函数你用了 t 日的收盘价算因子但 t1 日开盘才交易这中间的信息差处理不好回测曲线就会异常漂亮实盘立刻现原形。我建议所有因子都基于截面数据统一对齐。具体操作上用交易日历作为索引每只股票的因子值必须用当日收盘后可得的数据标签用未来 N 日的收益。清洗环节至少要处理三件事停牌股要剔除涨跌停无法成交的要剔除上市不足 60 日的次新股建议剔除——它们的波动特征和存量股票差异太大容易干扰模型。极端值处理也很有讲究。金融数据长尾严重直接 z-score 处理不够我习惯先做 winsorize把超过 3 倍中位数绝对偏差的数值压缩到边界再算 z-score。这一步对树模型虽然影响不大但对后续因子加权、组合构建环节很重要。2.2 标签Label怎么定义未来 N 日收益与行业中性化标签的选择直接决定了模型在学什么。最常见的做法是使用未来 5 日或 20 日收益作为回归标签。但有个问题不同时期市场整体涨跌差异很大牛市里所有股票都在涨熊市里都在跌模型学到的可能是大盘beta而不是选股 alpha。解决办法有几个。一是把标签做截面标准化在每个调仓日将未来收益减去当日全市场均值再除以截面标准差相当于构建一个相对收益。二是按行业分组做中性化把未来收益减去行业内均值让模型专注于行业内选股而不是捕捉行业风格。我个人更倾向后者因为 A 股的行业轮动特征太明显行业中性化之后组合的行业暴露更可控。另外要提醒一点不要用未来一段时间的最高收益或最低收益做标签那等于告诉模型未来的极端值在哪属于典型的前视偏差。就用简单直接的未来 N 日收盘到收盘收益率拒绝了任何投机取巧。2.3 训练集/验证集/测试集的切分逻辑时间序列不能乱来这可能是新手最容易踩的坑。常规机器学习里随机打乱数据做交叉验证没问题但金融时序数据里存在明显的时间自相关今天的数据和明天高度相关。如果随机切分训练集和测试集会互相泄露信息评分虚高。正确的做法是严格按时间顺序切分。比如用 2015 到 2019 年做训练2020 年做验证2021 年做测试或者采用滚动窗口walk-forward的方式。我常用 6 年训练、1 年验证、最近 1 年作为样本外测试验证集主要用来定早停轮数和调参测试集只在最终评估时碰一次避免在测试集上反复调参导致隐式过拟合。3. LightGBM 参数实测从默认参数到能用的参数3.1 我踩过的参数坑LightGBM 的默认参数在很多通用数据集上表现不错但直接搬到选股场景上效果往往不太行。核心原因是金融数据噪音大默认参数下模型会较快过拟合。我最早跑的一版训练集上排序 IC 到了 0.15验证集只有 0.03这就是过拟合的典型信号。后来我总结出了选股场景下的一套基准参数。关键在以下几个num_leaves不能太大默认的 31 对于选股任务偏高了我一般设在 8 到 16 之间min_data_in_leaf要调大默认 20 对于几十万条样本来说太少我会设为 100 到 500learning_rate调小到 0.02 左右配合更多的树来弥补学习率降低带来的精度损失。3.2 树个数num_boost_round与早停树个数大概是群里被问得最多的问题之一。很多人直接把num_boost_round设成 1000但到底多少合适其实取决于学习率和数据复杂度。我在实践中几乎不用固定值而是通过验证集早停early stopping来决定。LightGBM 的 Python 包支持callbacks.early_stopping设置比如early_stopping_rounds100模型会在验证集指标连续 100 轮不再提升时停止训练然后自动返回最优迭代次数。这里有个经验学习率和树个数是配套的。学习率 0.1 时可能 200 棵树就过拟合了验证集指标快速掉头向下学习率 0.01 时可能需要 2000 棵树才能收敛训练时间长很多但精度通常更好过拟合风险更低。我测试下来0.02 到 0.05 之间的学习率在选股场景里性价比最高树个数在 500 到 1500 区间。3.3 正则化参数与过拟合LightGBM 的正则化主要体现在三个地方lambda_l1、lambda_l2和max_delta_step。金融数据噪音大我一般会把lambda_l2设到 1 到 5 之间能有效压制叶子节点权重过大导致的过拟合。max_delta_step这个参数很隐蔽它限制每棵树输出的最大梯度步长。对于标签分布极不均匀的数据这个参数能防止单棵树输出过大的预测值。如果你发现模型的预测值在某些截面上异常集中或异常分散可以尝试把max_delta_step从默认的 0 改成 1 或 2。还有一个关键参数是feature_fraction即每棵树随机采样的特征比例。我习惯设为 0.6 到 0.8让每棵树只看部分特征增加多样性类似随机森林的思路能显著提升模型稳定性。参数配置有多重要我放一张自己试过的对照表供参考参数默认值选股场景建议说明num_leaves318-16降低模型复杂度learning_rate0.10.02-0.05更平滑的收敛min_data_in_leaf20100-500防止叶子过少导致过拟合feature_fraction1.00.6-0.8增加特征随机性lambda_l201-5权重衰减max_delta_step01-2限制单步输出这些值不是拍脑袋来的适用于日频截面数据、样本量在十万到百万级、特征在 20 到 100 维的场景。如果你的数据和我不一样还是要以验证集指标为准。4. 回归、分类还是排序LightGBM 在选股场景下的目标函数选择4.1 回归预测收益率 vs 分类预测涨跌选股任务可以建模为回归问题也可以建模为分类问题还可以建模为排序问题。三种方式各有适用场景很多人在这一步纠结。回归问题的目标函数是regression_l2直接预测未来 N 日收益的数值。优点是信息利用充分输出值连续缺点是在市场风格突变时预测值可能与实际收益的量级偏离较大毕竟金融收益的分布是重尾的L2 损失对极端值敏感。分类问题的做法是把标签离散化比如按截面收益排名前 30% 标记为 1后 30% 标记为 0中间忽略然后用binary目标函数训练。这种做法更稳健因为模型只需要学习相对好坏的排序不需要精确预测收益的大小。但缺点是没有利用中间层的信息而且分类阈值的选取有一定主观性。4.2 Lambdarank 与 XE_NDCG 排序目标在选股中的应用如果你的最终目标是选出一个 TOP 组合那么把问题建模为排序任务更契合业务需求。LightGBM 提供了lambdarank目标函数它接受一组组query的数据在组内对样本按相关性排序。在选股场景里一个调仓日就是一个 query该日所有股票都是这组内的候选样本标签就是未来收益的排名或分组。XE_NDCG 是另一类排序目标全称是 cross-entropy NDCG它是 LambdaMART 的变体直接优化 NDCG归一化折损累计增益指标。NDCG 有一个特点对排在前面的文档/股票的错误排序惩罚更重越靠前的位置权重越大。用在选股上就等于让模型优先保证头部选得准而不是均匀地对全市场排序。这两种排序目标和直接回归的差别在于损失函数对相对顺序的敏感度。回归损失关注预测值与真实值的绝对误差而排序损失关注的是排序关系的正确性。对于 Top-K 选股排序目标通常比回归目标带来更稳定的组合表现因为组合选择的正确性完全取决于相对顺序。4.3 实操中我推荐的目标函数从我个人的大量对比测试来看没有绝对的最优目标函数但有相对稳定的选择。如果以月频调仓、选前 20% 股票为目标我推荐优先尝试lambdarank组query设置为调仓日期。具体参数上lambdarank需要指定label_gain默认是 0 到 3 的等级。我通常把标签按未来收益排名等分为 5 档对应 gain 为 0 到 4然后设置lambdarank_truncation_level50表示只看每组前 50 名的 NDCG。XE_NDCG 在 LightGBM 4.x 版本里已经可以直接用objectivexendcg它在小数据集上收敛更快对噪声的容忍度也更高。但有一个注意点xe_ndcg 对组内样本量的均匀性有一定要求如果某些调仓日股票数量特别少比如市场极端行情下大面积停牌训练可能不稳定需要过滤掉样本量过小的日期。回归目标也不是不能用如果你在构建多因子模型时已经做了严格的行业中性化和截面标准化回归目标配合排名选股同样能出不错的结果。最关键的是评估指标要一致无论用什么目标函数最终都回到预测排序的 IC 上来验证不要因为训练损失下降就开心选股只认排序正确率。5. 特征工程与因子处理从原始数据到模型输入5.1 因子标准化与缺失值处理LightGBM 虽然对特征尺度不敏感但这不代表不需要做标准化。标准化在树模型里最大的意义不是提升精度而是保证特征重要性的可解释性。如果你把估值因子单位是倍和换手率因子单位是百分比直接混在一起特征重要性的数值不直观不利于后续因子迭代。我推荐的做法是所有连续因子先做 winsorize再算截面 z-score。注意是截面标准化每个调仓日单独算均值和标准差而不是时间序列标准化。原因很简单截面标准化的含义是这只股票在这个因子上的相对位置这正好是选股需要的信息。缺失值处理方面树模型有自己的缺失值处理方法LightGBM 默认把缺失值分配到增益最大的一侧这个机制很好用。但在金融场景里缺失值往往本身携带信息——比如某只股票刚上市、某类分析师覆盖不足、某项财务指标没有公布。所以我会额外构造一个缺失标记特征把缺失与否本身作为一个二值变量喂给模型让模型自己学习缺失的含义。5.2 行业哑变量与时间截面特征行业信息在选股里非常重要。经验上加入行业哑变量后模型的组合在行业上的暴露更均衡回撤也更可控。常见的做法是采用申万一级行业分类构造 28 到 31 个哑变量。但直接用 LightGBM 的时候行业特征太多反而会增加噪音我更喜欢用行业本身作为一个类别特征categorical_feature让树模型自己切分。时间截面特征的做法也值得分享。除了纯粹的行情因子我还会加入当日横截面排名、过去 60 日波动率、年内动量、以及与大盘的相对强弱。这些特征有一个共同点——它们描述的是这家公司当前处于什么状态而不是简单的时间序列数值。加入这些截面特征后模型的 IC 通常会有明显提升。5.3 特征重要性的解读与迭代LightGBM 训练完成后feature_importance能告诉我们哪些特征被用来分割最多的节点、贡献了最大的信息增益。但这里有个容易误读的地方特征重要性高不代表这个因子在经济学意义上有效它只代表模型在这个数据集上依赖该特征做决策。我的习惯是每迭代一版特征就把特征重要性排名打印出来和因子分析阶段的 IC 排名做对比。如果某个因子在单因子测试里 IC 不错但模型特征重要性极低那说明它的信息被其他特征覆盖了反过来如果某个新特征一加入就冲到重要性前十那说明它带来了增量信息值得重点保留。特征数量上我建议大家克制一点。见过不少爱好者一上来就构造 200 多个因子LightGBM 确实能跑但过拟合风险指数级上升。我通常限制在 30 到 80 个特征之间这个数量既能覆盖多维度信息又不至于让模型过于自由。6. 模型训练与验证的完整流程附伪代码6.1 Walk-forward 滚动训练前面的章节把各个模块讲得比较散这里把完整的训练流程串起来。为了更贴近工程实践我提供一段基于 LightGBM Python API 的伪代码。实际跑通这段代码你需要准备一个面板数据 DataFrame索引是日期每一行是一只股票在某一天的截面数据。import lightgbm as lgb import pandas as pd import numpy as np from scipy.stats import spearmanr # 假设 df 包含以下列 # date: 调仓日期 # code: 股票代码 # feature_1 ... feature_n: 因子特征 # label_5d: 未来5日收益率行业中性化后的相对收益 # industry: 行业分类 df[query] df[date].astype(category).cat.codes features [c for c in df.columns if c.startswith(feature_)] train_df df[df[date] 2020-01-01] valid_df df[(df[date] 2020-01-01) (df[date] 2021-01-01)] test_df df[df[date] 2021-01-01] lgb_train lgb.Dataset( train_df[features], labeltrain_df[label_5d], grouptrain_df.groupby(date).size().tolist(), categorical_feature[industry], ) lgb_valid lgb.Dataset( valid_df[features], labelvalid_df[label_5d], groupvalid_df.groupby(date).size().tolist(), referencelgb_train, ) params { objective: lambdarank, metric: ndcg, learning_rate: 0.03, num_leaves: 16, min_data_in_leaf: 200, feature_fraction: 0.7, lambda_l2: 2.0, max_delta_step: 1.0, verbosity: -1, lambdarank_truncation_level: 50, } model lgb.train( params, lgb_train, num_boost_round2000, valid_sets[lgb_valid], callbacks[lgb.early_stopping(stopping_rounds100)], ) pred model.predict(test_df[features], num_iterationmodel.best_iteration) test_df test_df.copy() test_df[pred] pred # 按调仓日计算 Rank IC def rank_ic(df): ics [] for _, sub in df.groupby(date): if sub[label_5d].nunique() 5: continue ic, _ spearmanr(sub[pred], sub[label_5d]) ics.append(ic) return np.mean(ics), np.std(ics) / np.sqrt(len(ics)) mean_ic, ic_std rank_ic(test_df) print(f样本外 RankIC 均值: {mean_ic:.4f}, IR: {mean_ic / ic_std:.4f})这段代码有几个关键点。group参数是排序目标的核心每一组的长度必须与该调仓日的股票数量完全一致否则会报错或者静默出错。categorical_feature里指定行业不用手动做哑变量。6.2 IC / Rank IC 评估ICInformation Coefficient是选股模型最核心的评估指标计算的是预测值和实际收益之间的相关系数。Rank IC 用 Spearman 秩相关比 Pearson 相关更稳健是行业通用标准。Rank IC 的绝对值在 0.03 以下模型基本没有选股能力0.03 到 0.05 算不错0.05 以上在月频选股里已经算很优秀了。但要注意选股 IC 和经济意义不一定直接挂钩还要看 IC 的稳定性即 IC 的均值除以标准差也就是 IRInformation RatioIR 大于 0.3 才算模型有持续性的信号。此外我还会看 IC 的时间序列图。如果 IC 在某些年份持续为正某些年份突然衰退那说明模型可能已经在市场风格切换中失效需要及时重新训练或调整特征。6.3 分位数组合回测评估模型的最终方式是构建组合回测。简单有效的方法是分层测试每次调仓把所有股票按预测值从高到低分成 10 组分别计算每组在未来 N 日的等权收益率。如果第一组明显跑赢最后一组且各组收益率呈现单调性说明模型确实在捕捉选股信息。单调性很重要。如果第一组和最后一组的差异很大但中间组乱序说明模型可能只在极端位置有效需要谨慎。如果单调性良好再构建 TOP 20% 的组合加入交易成本和换手率限制进一步看净收益。分层回测还有一个用途观察模型在不同市场环境下的表现。牛市里模型可能偏向高 beta 股票熊市里偏向防御股这些动态变化可以通过分层分组在不同时期的收益来诊断。7. 真实场景中的坑与经验7.1 前视偏差回测与实盘差距的第一凶手前视偏差是个老生常谈但永远有人踩的问题。除了前面说的标签定义还有个隐蔽的地方因子计算时使用了未来信息。比如你在交易日 t 日计算当日开盘涨幅因子用的开盘价是 t 日开盘但你在 t 日开盘前就已经做出了买卖决策这就矛盾了。处理原则只有一条确保所有用于生成预测的数据在实盘下单时刻是已经公开可得的。如果你在收盘后跑模型、次日开盘下单那因子只能用当日收盘前可得的数据比如收盘价、成交量但不能用次日的任何数据。回测中用的集合竞价数据、盘后公布的龙虎榜数据都要考虑实际可获取时点。一旦在回测里出现了用未来数据预测过去的情况回测收益会虚高得离谱而且这种虚高没有规律可循极难识别。7.2 涨跌停与停牌处理A 股和美股最大的区别之一就是涨跌停制度。模型预测某只股票明天会涨但今天它已经涨停了你根本买不进去。如果不处理回测里你的组合就会买进大量实际无法成交的股票收益虚高。我的处理方式是在生成每期持仓时剔除当期处于涨停、停牌状态的股票如果股票在买入后遇到连续的涨跌停也要在模型中设置卖出约束。另一种做法是提前一天的成交额做过滤流动性太差的剔除减少冲击成本。7.3 换手率与交易成本很多初学者只关注模型的 IC忽略了换手率和交易成本对实盘收益的侵蚀。LightGBM 做的是截面选股每期预测排序都在变化组合换手率很容易达到 50% 以上这在月频调仓里意味着每个月有一半仓位在变动。手续费、印花税、滑点加在一起单边成本在 A 股大约千分之二到千分之五。如果组合月度单边换手率 50%一年的成本大概是 12% 到 30%基本吃掉大部分超额收益。因此在模型训练时就要有意识地控制换手比如在组合构建时加入惩罚项或者对预测值做平滑处理——用过去三期预测值的加权平均替代单期预测值能有效降低换手。成本敏感性测试是判断策略容量的重要手段。我习惯在回测时分别设 0、千分之一、千分之三三档成本观察收益衰减速度。如果千分之三成本下策略转负那说明模型信号强度不足需要回到因子或模型层面改进而不是在交易参数上硬撑。8. 从模型到实盘LightGBM 选股策略的工程化落地8.1 与通达信指标公式选股的差别聊到量化选股绕不开通达信这类传统软件里的指标公式。它们本质上是一系列规则组合比如市盈率小于 20 且 ROE 大于 15%属于基于人工经验的显式规则。优点是逻辑透明容易理解和调试缺点是规则之间无法自动组合无法挖掘高阶非线性关系。LightGBM 的做法是用数据驱动的方式自动学习这些规则。它会把市盈率小于 20 且 ROE 大于 15%这种规则推广到更复杂的层次可能是市盈率小于 18 且动量因子大于 0.5 且行业为医药时有效这种组合模式用人眼是挖不出来的。但这里我要说句公道话不要认为模型一定能碾压规则公式。当样本外表现不好时传统的通达信公式因为逻辑简单反而更容易定位到失效原因比如某个因子逻辑被市场风格变化打断而模型的黑盒特性让归因变得困难。所以我的建议是二者互补用传统公式做风控过滤和底线保障用 LightGBM 做收益增强。8.2 与 XGBoost 的对比为什么我最终选 LightGBMXGBoost 在很长一段时间里是梯度提升树的事实标准我自己也用过相当长时间。两者的核心差异可以归纳为两点生长策略和分箱算法。XGBoost 默认使用 level-wise按层生长的方式每一层所有节点一起分裂这样容易找到全局较优的切分但计算开销大。LightGBM 使用 leaf-wise按叶子生长的方式每次只在收益最大的叶子节点上分裂可以在相同迭代次数下获得更低的训练损失但更容易过拟合所以必须配合max_depth和min_data_in_leaf来控制复杂度。分箱算法方面LightGBM 用直方图算法把连续特征离散成有限个区间内存占用小、训练速度快在处理大规模高维特征时优势明显。在选股场景里几十万样本量下两者训练速度差距可能不大但当你把特征扩展到上百维、做滚动训练时LightGBM 的速度优势会体现得很充分。还有一个实用层面的选择理由LightGBM 原生支持类别特征不需要手动做哑变量编码这一点在处理行业特征时特别方便。XGBoost 虽然新版也做了支持但生态和文档成熟度不如 LightGBM。8.3 上线后的持续监控与模型更新策略上线不是终点而是持续维护的起点。A 股市场的风格切换很快一个模型半年不更新就可能失效。我的更新节奏是每月调仓时滚动重训一次用最近 36 个月的数据训练、最近 12 个月做验证。同时每次重训后对比新旧模型在最近 3 个月的 Rank IC如果新模型明显更差就暂时沿用旧模型等下一个周期再说。监控层面我会记录每天的预测值分布、组合的行业暴露、换手率变化。一旦发现预测值分布偏离训练期分布过大大概率是市场环境发生了结构性变化要立刻检查是哪些特征驱动了这种变化以及是否需要调整因子池。另一个容易被忽略的点是模型版本管理。LightGBM 模型文件本身要纳入版本管理配合每次训练用的数据版本和特征版本做到完全可复现。不然三个月后回看某个策略结果根本想不起来当时是哪些样本、哪些特征、哪些参数跑出来的这对策略迭代是致命的。最后说一个我在实际中反复验证的心得LightGBM 的默认参数给你的是起点不是终点量化选股的胜负手永远在数据质量和特征语义上。同一份数据、同一个模型框架不同人做出来的效果能差出一倍以上差的往往不是模型复杂度而是对数据细节的敬畏。建议大家从 Datawhale 指南里的数据集开始先把完整流程跑通再逐步加入自己的因子和想法。模型只是工具真正值钱的是你对市场的理解和对数据的处理。