
简介机器学习正在重塑量化投资的研究范式其中梯度提升树因对表格数据的高效拟合能力而备受青睐。LightGBM作为GBDT框架的高性能实现通过直方图算法与叶子优先生长策略在保障精度的同时显著提升训练效率天然适配中低频选股场景。与传统多因子模型相比它无需预设因子权重能从量价数据中自动挖掘非线性组合规律。本文围绕LightGBM量化选股全流程展开涵盖特征工程中的截面标准化、标签定义中的防前视偏差以及基于backtrader的多股组合回测实现并讨论交易成本、过拟合控制等工程细节帮助投资者构建可验证的选股策略。1. 为什么用LightGBM做量化选股方案选型背后的逻辑1.1 机器学习选股与传统多因子模型的差异传统多因子模型大家都很熟先确定一组因子比如市盈率、市净率、ROE、动量、波动率然后给每个因子一个固定权重用加权打分的方式对全市场股票排序选排名靠前的买入。这套逻辑的问题是权重是静态的因子和未来收益之间的关系也基本被假设成线性。但市场哪有那么配合动量因子在趋势市里好用震荡市里可能就变成反转因子一个固定权重的模型很难自适应这种切换。机器学习选股换了个思路我不预设因子和收益之间的具体关系让模型自己去学。输入的还是那些特征输出是股票未来N日收益的概率或排序分模型自己决定特征怎么组合、用什么样的非线性关系。这就把“因子怎么配权重”这种主观决策变成了一个数据驱动的拟合问题。实际做下来LightGBM这类梯度提升树模型在A股中低频选股场景里确实能找到一些传统线性模型找不到的组合规律。1.2 为什么是LightGBM而不是深度学习很多朋友一上来就问现在深度学习这么火是不是直接用LSTM或者Transformer会更好我的回答一直是先看数据量和场景。日线级别的选股样本量通常就几千只股票乘以几千个交易日清洗完有效的训练样本也就几十万条还是带噪声的金融时序。这个数据量下深度模型很容易过拟合而且训练成本高、调参难度大收益不一定比树模型高。LightGBM在量化场景里有几个实打实的优势。一是训练速度快GBDT框架配合直方图算法同样一批数据XGBoost可能要跑几分钟LightGBM几十秒就能跑完一轮这对频繁做滚动训练、调参验证非常友好。二是对特征尺度不敏感不需要像神经网络那样做严格的归一化。三是自带正则化机制配合早停和交叉验证能比较稳地把过拟合控制住。四是可以直接输出排序分数而选股本质上是个排序问题这个特性非常契合。我自己的经验是如果样本量在百万级以内、特征以表格型量价数据为主LightGBM基本是性价比最高的选择之一。等你的策略容量和特征体系都成熟了再考虑往深度模型迁移也不迟。1.3 整体策略框架从数据到信号的完整链路这套策略的完整链路大概是这样的先确定股票池比如沪深300成分股或中证500成分股然后拉取历史日线数据包含开高低收、成交量、成交额、换手率这些原始字段在原始数据基础上做特征工程构造动量、量价、波动率、技术指标等因子再定义标签也就是未来5日或10日的收益并根据排序分成好股票和差股票用历史数据训练LightGBM模型对全市场股票打分最后在backtrader里做多股组合回测模拟真实交易环境。一句话总结就是特征工程喂给模型模型产出排序分排序分驱动交易策略回测框架验证盈利能力。这篇文章会按这个顺序一步步展开重点讲清楚每一步的关键细节和容易踩的坑。2. 特征工程与样本构建决定模型上限的那一半工作2.1 原始数据准备选池子、拉数据、做对齐很多人一上来就全市场选股觉得样本越多模型越强。但从实践来看全市场的股票质量参差不齐ST股、次新股、长期停牌股都会带来一堆噪声模型很容易学到一些假规律。我的习惯是先限定一个相对干净的池子比如沪深300或中证500成分股后续再根据模型表现逐步扩展。数据源方面个人研究阶段可以用tushare、akshare、baostock这类免费接口实盘前再接入Wind或聚宽这类商业数据。要注意两个问题第一是复权前复权和后复权在计算收益率时差别很大建议统一用后复权价格做计算回测时再配合真实价格处理第二是停牌很多接口对停牌日期的处理不一样缺失值如果处理不好后面特征计算和回测数据接入都会出问题。数据字段至少要有date、code、open、high、low、close、volume、amount、turnover_rate这几项。其中amount和turnover_rate对构造量价类特征非常重要很多新手只拉价格数据结果特征池子少了一半。2.2 特征怎么造量价因子、技术因子与截面处理特征工程是这套策略里最花时间的部分也是决定模型上限的关键。我常用的特征分为几大类。第一类是动量反转类包括过去5日、10日、20日、60日的收益率以及动量因子过去20日收益减去过去5日收益。第二类是波动率类包括过去20日收益率标准差、ATR平均真实波幅、振幅。第三类是量价类包括成交额变化率、换手率、量比当前成交量与过去5日平均成交量的比值。第四类是技术指标类包括RSI、MACD、布林带位置当前价格在布林带中的相对位置。光有原始特征还不够A股市场有个典型特点同一个因子在不同股票上的绝对值差异非常大。比如茅台和一只小盘股的换手率完全不在一个量级直接把原始值喂给模型模型可能会把“股票本身是谁”学进去而不是学“换手率变化意味着什么”。所以需要在每个横截面时间点上做处理一般是先去极值再标准化。去极值我常用MAD法也就是中位数绝对偏差法。代码不复杂def mad_winsorize(series, n5): median series.median() mad (series - median).abs().median() upper median n * mad lower median - n * mad return series.clip(lower, upper)标准化建议用z-score但注意一定要在截面维度做也就是同一天所有股票的特征值一起标准化而不是对单只股票的时间序列标准化。截面标准化的目的是让模型学会比较“这只股票今天的量能在全市场里处于什么水平”这是选股任务的核心逻辑。2.3 标签定义与训练集构造别让未来数据混进来标签是整个流程里最需要谨慎的地方定义得好不好直接决定模型学到的目标是否正确。我的做法是先计算未来N日的收益率比如未来5日的收益也就是从第t日收盘到第t5日收盘的涨幅然后按照这个收益率在截面上排序前30%标记为正样本后30%标记为负样本中间40%丢弃。这样做比直接回归预测收益率更稳定因为股票收益噪声很大精确预测数值很难但分清楚相对好和相对差相对可行。这里有一个极其重要但常被忽视的点构造标签时第t个样本的标签用的是第t5日的数据所以在划分训练集和测试集时必须保证训练集的标签时间窗口和测试集的特征时间窗口不重叠。简单说如果你用2022年12月30日的特征做预测它的标签会落到2023年1月6日那训练集就不能用到2023年1月6日之后的任何信息否则就是前视偏差。我习惯用滚动训练的方式比如用2018年到2022年的数据训练2023年上半年做验证调参再用2023年下半年做样本外测试。每个季度重新训练一次模型保证模型参数能跟上市场风格变化。3. LightGBM模型训练与参数调优从还不错的基线到稳定输出3.1 核心参数怎么理解学习率、叶子数与正则化LightGBM参数看起来多但真正影响选股效果的其实就是那七八个。我把它们分成三组。结构参数组num_leaves是最核心的它控制模型的复杂度经验上取值16到64之间比较合适不是越大越好叶子多了模型就容易记住噪声。max_depth一般配合num_leaves一起用建议限制在8到12以内防止模型过深。训练策略参数组learning_rate就是学习率建议设0.05或者0.01配合较大的n_estimators。别用0.3这种默认值在量化场景里数据噪声大学习率太高模型很快就把噪声学进去了。n_estimators一般要让早停机制来决定手工指定容易欠拟合或过拟合。正则化参数组min_data_in_leaf控制叶子节点最少样本数值越大模型越保守这个参数在股票数据上作用非常明显建议从100开始调。feature_fraction是建树时随机抽样的特征比例设为0.7左右能有效降低方差。lambda_l1和lambda_l2是正则项可以适当加大比如lambda_l2设成1到5之间能压住一些无意义的特征权重。3.2 训练流程与早停机制我训练的代码框架大概是这样的import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit params { objective: binary, boosting_type: gbdt, learning_rate: 0.05, num_leaves: 32, max_depth: 8, min_data_in_leaf: 100, feature_fraction: 0.7, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 2.0, verbose: -1 } dtrain lgb.Dataset(X_train, labely_train) dvalid lgb.Dataset(X_valid, labely_valid) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dvalid], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] )这里用early_stopping也就是验证集指标连续100轮不提升就停止训练最终模型的树数量会自动确定。不要等训练完再手动挑轮数效率太低。验证集的划分要特别注意我用的是TimeSeriesSplit也就是按时间顺序切分打乱的KFold在时序数据里会导致验证集信息泄漏到训练集里验证分数会虚高样本外表现会差很多。3.3 特征重要性与模型迭代训练完第一步是看特征重要性。LightGBM有feature_importance函数可以基于split或者gain来评估。我一般用gain重要性因为它是按特征对loss下降的贡献来排的比单纯统计分裂次数更有意义。看完重要性之后要做减法。第一次训练往往有几十个特征里面有一半是锦上添花甚至添乱的。我会把重要性排在后30%的特征逐步去掉重新训练对比验证集表现。很多时候去掉一部分冗余特征后样本外表现反而更好原因很简单特征少了噪声路径少了过拟合风险降低。另外如果发现某个领域的重要特征太少比如全是动量类特征量价类几乎没有我会回头去补特征而不是强行调参。模型效果不好60%的锅在特征工程30%在标签定义只有10%在调参。4. 基于backtrader的多股组合回测把策略真正跑起来4.1 为什么回测要从单股走向多股组合很多刚接触回测的朋友第一步喜欢拿一只股票测试策略比如“平安银行历史上用这个策略能赚多少”。这种单股回测有一个致命问题幸存者偏差和路径依赖。你选的那只股票可能就是过去十年涨得最好的之一换成别的股票结果可能天差地别。量化策略本质上赚的是横截面选股的钱所以要测的是“整个股票池里模型选出来的股票组合是否跑赢基准”。多股组合回测还能更真实地模拟交易分散持仓降低个股黑天鹅的影响更贴近实盘的冲击成本和资金容量。这就是为什么我会把backtrader的多股回测作为整个策略验证的核心环节而不是用单股样本自欺欺人。4.2 多股数据接入格式、对齐与停牌处理backtrader接入多股数据有几个关键点。首先是数据格式backtrader能接受pandas DataFrame但列名有固定要求datetime、open、high、low、close、volume、openinterest这几个字段索引或者列名要对上。import pandas as pd import backtrader as bt data_dict {} for code, df in stock_data.items(): df df[[date, open, high, low, close, volume]].copy() df[datetime] pd.to_datetime(df[date]) df.set_index(datetime, inplaceTrue) df[openinterest] 0 data bt.feeds.PandasData(datanamedf) data_dict[code] data然后是数据对齐问题。不同股票的交易日可能不一样比如某一天某个股票停牌了它的DataFrame里就没有这一天的记录。backtrader支持不同时间周期的数据共存但策略计算排名时需要对齐我的做法是统一用全市场的交易日历做基准股票停牌日用前收盘价填充。还有复权问题回测数据不能用原始价格尤其是分红除权频繁的股票价格会出现跳空导致收益计算错误。我在回测前统一把数据换成后复权价格这样计算出来的收益率才是真实的。4.3 策略逻辑实现调仓信号、仓位与交易成本多股组合策略在backtrader里的实现思路是这样的策略维护一个预测分数表每个调仓日读取当天的模型分数选出排名靠前的N只股票并持有到下一个调仓日。class LightGBMStrategy(bt.Strategy): params ( (rebalance_days, 10), (top_n, 10), (score_table, None), ) def __init__(self): self.rebalance_days self.p.rebalance_days self.top_n self.p.top_n self.score_table self.p.score_table self.day_counter 0 self.current_date None def next(self): self.current_date self.datas[0].datetime.date(0) self.day_counter 1 if self.day_counter % self.p.rebalance_days ! 0: return scores [] for data in self.datas: code data._name if self.current_date in self.score_table and code in self.score_table[self.current_date]: score self.score_table[self.current_date][code] prices data.close[0] if prices and not math.isnan(prices): scores.append((code, score, data)) scores.sort(keylambda x: x[1], reverseTrue) selected scores[:self.top_n] for data in self.datas: if data in [s[2] for s in selected]: target 1.0 / self.top_n self.order_target_percent(data, targettarget) else: self.order_target_percent(data, target0.0)调仓周期我默认设成10个交易日也就是约两周调一次。太频繁交易成本会吃掉收益太迟钝又会错过机会。A股市场T1制度下模型分位数的变化节奏一般就是周度到半月度的尺度。交易成本这个环节必须较真。印花税、佣金、滑点每一项都直接影响策略是否可实盘。backtrader里佣金可以直接设置import backtrader as bt cerebro bt.Cerebro() cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission0.001)但这里有个细节setcommission默认是百分比还是固定值要看参数建议明确写成commission0.001表示千分之一这在A股双边交易中已经算比较保守的估计了。另外加一个固定滑点cerebro.broker.set_slippage_perc(perc0.001)也就是每次成交都要比目标价格高0.1%买入低0.1%卖出。这样做出来的回测结果才具备参考价值。4.4 回测结果评估必须关注的几个指标回测跑完不是看一眼总收益就完事了。我一般会看几个核心指标年化收益率、最大回撤、夏普比率、胜率和盈亏比。cerebro.addanalyzer(bt.analyzers.Returns, _namereturns) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedrawdown) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe, riskfreerate0.02) cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _nametrades) results cerebro.run() strat results[0]夏普比率超过1可以算及格超过2就很优秀了如果低于0.5基本可以判定策略没有超额收益能力。最大回撤这个指标特别重要很多策略年化收益不错但回撤达到50%这种策略实盘根本拿不住因为中间早就被震下车了。我个人能接受的最大回撤是20%以内。还有一点要对比基准。一个策略年化收益20%看起来不错但同期沪深300涨了18%那这个策略的超额收益只有2%几乎没有意义。所以回测时务必引入一个基准指数做对比比如中证500或沪深300。5. 常见问题与踩坑实录量化回测里的那些“隐藏炸弹”5.1 前视偏差与幸存者偏差这两个是量化回测里最隐蔽也最致命的问题。前视偏差我在前面已经提到过特征工程和标签构造的时间窗口错位会直接导致回测结果虚高。我排查这个问题的办法很简单在训练样本里找到某一天的特征确认它的所有输入信息都在当天收盘前可得标签所在的时间窗口完全在特征日之后。幸存者偏差则是股票池选取的问题尤其用免费数据源时很容易拿当前时点还存活的成分股做回测那些中途退市或者被剔除的股票完全没进入股票池。这相当于你用“知道谁会活下来”的视角看历史回测收益自然偏高。修正方法是用历史时点的成分股快照或者至少把退市股数据也纳入股票池。5.2 回测参数过拟合调参需有节制LightGBM本身参数多再加上回测框架里调仓周期、Top N、手续费设置参数组合空间非常大。最大的风险就是你反复调整参数让回测曲线变得很好看但这个参数组合只适配了历史行情换个时间段就失效了。我控制过拟合的方法有三条底线。第一模型层用滚动训练绝对不能在同一段数据上反复调试参数再测试同一段时间。第二参数网格要小每个超参数设两三个典型值就够不要搞几十组组合去刷最佳收益。第三样本外测试必须坚持“一次定稿”也就是用训练集和验证集完成所有调参后只用一次测试集做最终评估结果好坏都接受绝不回头调整再测。5.3 数据质量问题与对齐陷阱多股回测常见的技术坑是交易日期对齐。比如2022年某只股票停牌三个月它的DataFrame里没有这期间的任何记录backtrader在计算组合收益时如果没处理好会把它的仓位算成0或者出现错位。我的处理方式是统一用交易日历做reindex缺失的价格数据用前向填充。还有复权不一致的问题。比如股票池里一部分数据手动做了后复权另一部分用的是原始价格计算组合收益时就会混入大量虚假的收益波动。这种错误很难肉眼发现需要在接入回测前写一个校验函数随机抽几只股票手工计算一段时间内的收益率和回测输出的收益率对比。5.4 实盘与回测的差距交易成本永远比你想的高很多人回测跑出年化30%以上的收益一到实盘就缩水到10%不到主要原因就是交易成本考虑不足。A股佣金虽然可以谈到万1.5但印花税卖出一方收千分之0.5再加上滑点冲击成本一次换仓的实际成本在千分之二到千分之三之间如果调仓频繁一年下来成本累积非常惊人。我的做法是回测时故意把成本调高比如手续费按千分之二计算再额外加千分之一滑点。这样跑出来的结果如果还有正超额收益实盘才有底气。也正因为这个原因我在选股策略中会更偏好中低频调仓比如两周甚至月度调仓收益下降不会太明显但成本节省是实实在在的。6. 写在最后这套流程我跑了半年之后的几点体会整个流程做下来我最大的感受是想靠机器学习在量化里稳定赚钱核心不在模型本身而在于对数据、成本和风险的控制。模型选LightGBM还是XGBoost参数是32叶子还是48叶子对最终收益的影响远不如你如何处理停牌股、如何设置交易成本、如何避免过拟合这些细节的影响大。如果你正准备起步我建议按这个顺序推进先不要急着训练模型用最简单的一两个因子比如20日动量和换手率先把backtrader多股回测框架跑通确认收益曲线、回撤、基准对比都能正常输出然后再加入LightGBM打分逐步丰富特征体系最后再考虑调参优化。这样每一步都有可验证的输出出了问题也知道该查哪一环。量化这条路上坑很多但每踩过一个坑下一次回测结果的可信度就高一点。希望这篇内容能帮你少走一些弯路把更多时间花在真正能产生收益的事情上。本文还有配套的精品资源点击获取