ARTICLE DETAIL

资讯详情

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

机器学习多因子选股模型:从因子工程到LightGBM回测实战

机器学习多因子选股模型:从因子工程到LightGBM回测实战 简介面向量化投资与金融工程学习者这是一套基于机器学习构建多因子选股模型的完整工程包含源代码与文档说明。资源覆盖单因子测试、因子共线性分析、特征与标签构建、机器学习模型回测等环节给出了从因子筛选、模型对比到交易逻辑与风险控制策略的完整流程。内置SVR、LSTM、XGBoost、随机森林、AdaBoost、MLP等多种模型实现并附回测报告与可视化图表最优随机森林模型累计收益约60%经择时策略风控后最大回撤约9%夏普率约0.9。压缩包共38个文件以15份Python脚本为核心辅以10份研究报告PDF、可视化图片、项目说明文档等总大小14.71MB目录结构清晰。已有464人学习适合计算机、人工智能、金融工程等专业学生作为毕业设计、课程设计或量化研究参考代码经测试可运行实践价值高。1. 从回测曲线到实盘亏损机器学习多因子模型真正要解决的问题深夜盯着回测平台里那条近乎完美的净值曲线你可能会误以为自己找到了圣杯。但换一段样本外数据、换一个时间窗口曲线立刻走形成另一副面孔——这是“基于机器学习方法构建多因子选股模型”这个标题背后最常见的真实遭遇。机器学习模型比传统线性多因子模型更擅长捕捉因子间的非线性交互但也更容易把噪声当成规律。本文要和你一起解决的不是“如何跑通一段源代码”而是如何把因子数据、模型训练、回测验证和风险控制串成一条完整链路从原始行情和财报数据出发构造有效因子用 LightGBM 等树模型完成收益预测再用严谨的回测框架检验模型是否真正具备样本外预测能力。这个方案适合已经能用 pandas 处理数据、了解基础机器学习概念、但尚未构建过完整量化选股流水线的研发工程师也适合想从单因子测试转向机器学习多因子组合的量化研究员。标题里提到的“源代码”和“文档说明”是交付物形态但核心难点在于你需要一份能落地的、考虑了数据穿越和过拟合问题的最小实现而不是一段漂亮的示例代码。2. 多因子选股模型的理论基石与数据准备2.1 多因子模型的本质用一组特征预测横截面收益排序多因子模型的思想起源于 CAPM 和 Fama-French 三因子模型核心假设是股票收益可以被一组共同因子解释。传统做法是线性回归估计因子暴露和因子收益率而机器学习方法改变了这个流程中的关键部分——不再预先假设因子与未来收益之间是线性关系而是让模型从数据中自动学习交互效应和非线性映射。在构建机器学习多因子模型时你需要区分两个容易混淆的概念。传统多因子模型中“因子收益率”是回归系数代表因子值每变化一个单位带来的收益贡献机器学习模型中的“特征重要性”则是基于分裂增益或置换重要性评估的贡献度它不给出方向性信息。实践中我一般会保留两个输出模型的预测值用于排序选股特征重要性列表用于因子有效性的持续监控。另一个需要提前确定的决定是预测目标。常见做法有两种一是预测未来 N 日收益率连续值回归任务二是预测未来 N 日收益率的排名排序任务通常转化为二分类或 LambdaRank。从落地效果看直接预测未来 5 日收益率的回归模型最简单稳定配合截面排序选股能够规避大部分尺度漂移问题。2.2 数据源选择与对齐行情、财报与预期数据的时序对齐陷阱训练数据的第一步是确定股票池和时间范围。建议以中证 800 或沪深 300 成分股为起点剔除 ST、上市不满 60 个交易日的新股。日线行情数据、财报数据、分析师预期数据这三类数据需要严格对齐其中最常见的坑是财务数据的“未来函数”——财报发布日期通常滞后于报告期比如一季报在 4 月底才披露完毕如果你直接使用 report_date 对齐而不用 announce_date就会把未来信息引入模型。import pandas as pd # 财务数据对齐publish_date 为实际披露日report_date 为报告期 # 必须用 publish_date 做时间对齐否则发生前视偏差 financial pd.read_csv(financial.csv, parse_dates[publish_date, report_date]) price pd.read_csv(daily_price.csv, parse_dates[trade_date]) # 每个交易日只取已实际披露的最近一期财报 def align_financial(trade_date, fin_df): disclosed fin_df[fin_df[publish_date] trade_date] if disclosed.empty: return None # 按报告期排序取距离当前交易日最近的一期 return disclosed.sort_values(report_date).iloc[-1] # 示例对 2023-05-10 这一天做财务对齐 aligned align_financial(pd.Timestamp(2023-05-10), financial)这里的关键参数是publish_date和report_date的区分。publish_date是数据实际可获得的日期report_date是财报所属季度。任何基于report_date的切片都会在未来数据上训练模型回测虚高但实盘失效。除此之外涨跌停板数据也需要单独处理——涨停时无法买入、跌停时无法卖出这些交易约束必须在回测中体现否则模型会选出大量实际无法成交的标的。2.3 因子计算与预处理去极值、标准化与中性化因子计算是第二步。基本面因子包括市盈率、市净率、市销率、ROE、毛利率变化率量价因子包括动量、反转、换手率、波动率、资金流另类因子可以纳入北向资金持股变化、大宗交易折价率等。对每个因子都必须在截面维度做预处理去极值MAD 或分位数、标准化z-score、行业与市值中性化。import numpy as np from scipy import stats def winsorize_series(s, n3): MAD 去极值以中位数为中心截断超过 n 倍绝对偏差的值 median s.median() mad (s - median).abs().median() upper median n * 1.4826 * mad lower median - n * 1.4826 * mad return s.clip(lower, upper) def standardize_cross_section(df, factor_col): 截面标准化每个交易日独立操作避免未来数据泄漏 df df.copy() df[factor_col] winsorize_series(df[factor_col]) df[factor_col] (df[factor_col] - df[factor_col].mean()) / df[factor_col].std() return df # 示例对 2023 年全市场每日 PE 因子做截面处理 factor_data load_all_stock_daily() processed factor_data.groupby(trade_date, group_keysFalse).apply( lambda d: standardize_cross_section(d, pe_ttm) )去极值这一步常常被忽视但它直接影响梯度提升树的训练稳定性。极端值会让树模型在分裂时优先拟合少数异常样本消耗大量分裂深度。MAD 方法的参数n决定截断宽度我一般取 3你也可以根据因子分布调整到 4 到 5。标准化不做也可以跑但对梯度提升树来说标准化不影响树的分裂点搜索所以这一步的意义更多在于后续做因子合成和中性化回归时的数值稳定性。中性化是另一个关键步骤——将因子值对行业哑变量和市值取对数做线性回归取残差作为新的因子值。这一步的目的是剔除行业偏好和大小盘风格的影响让因子在不同市场环境下表现更稳定。机器学习模型理论上能自己学到行业信息但显式中性化可以避免模型过度依赖行业标签从而提升样本外泛化能力。3. 从 LightGBM 到模型训练参数、交叉验证与特征重要性3.1 为什么选择 LightGBM非线性交互与缺失值处理的天然优势在多因子选股场景中LightGBM 是当前工程实践里最稳妥的起点。原因有三个第一树模型天然处理因子之间的非线性交互例如“低估值高动量”组合的有效性不需要你手动构造交叉特征第二LightGBM 原生支持缺失值处理财务数据中大量存在的空值不会被强制填充第三训练速度快支持 GPU 加速日频数据的截面回测迭代效率远高于深度学习模型。XGBoost 与 LightGBM 在这个任务上的差异不在精度而在工程便利性。LightGBM 的直方图算法对连续特征做分箱内存占用更低其基于叶子生长的策略更容易过拟合但也更容易在调参后达到更好的训练指标。如果你的数据集较小建议优先尝试 XGBoost 或随机森林避免小样本下的过拟合。深度学习模型如 Transformer 和 LSTM 更适合处理长序列时序依赖而多因子选股本质上是截面排序任务逐日独立预测收益率的树模型已经足够。import lightgbm as lgb # 构造训练集X 为因子值矩阵y 为未来 5 日收益率 train_x feature_matrix.loc[train_dates] train_y forward_returns.loc[train_dates] params { objective: regression, metric: rmse, learning_rate: 0.03, num_leaves: 31, max_depth: 7, min_child_samples: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l1: 0.1, lambda_l2: 1.0, verbose: -1, } model lgb.train( paramsparams, train_setlgb.Dataset(train_x, labeltrain_y), num_boost_round500, valid_sets[lgb.Dataset(val_x, labelval_y)], callbacks[lgb.early_stopping(stopping_rounds50)], )这几组参数需要特别说明。learning_rate设为 0.03 是为了配合 500 棵树的迭代量如果你增大学习率就必须减少树的数量否则必然过拟合。num_leaves控制树的复杂度叶子数越多模型表达能力越强但也越容易记住噪声。min_child_samples设为 50 保证每个叶子节点至少有 50 个样本这一步是防止模型在截面数据上学到极端个股的偶然收益。feature_fraction和bagging_fraction分别是特征采样和数据采样比例两者的随机性能够提升模型的泛化能力在因子数量不多时应适度调高。3.2 时序交叉验证避免随机打乱带来的数据泄漏训练机器学习模型时常规的 K-Fold 交叉验证在这里是错的。股票的日频数据存在强烈的时间自相关性如果随机打乱样本训练集中会混入未来数据回测结果会显著高估。正确的做法是使用 Purged K-Fold 或简单地按时间顺序切分。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(feature_matrix): train_x_fold feature_matrix.iloc[train_idx] val_x_fold feature_matrix.iloc[val_idx] # 每个 fold 重新训练模型记录 validation rmse model_fold lgb.train( params, lgb.Dataset(train_x_fold, labelforward_returns.iloc[train_idx]), num_boost_round500, valid_sets[lgb.Dataset(val_x_fold, labelforward_returns.iloc[val_idx])], callbacks[lgb.early_stopping(stopping_rounds30)], )TimeSeriesSplit 要求数据严格按时间排序它每次使用更早的数据训练、更新的数据验证。这里有一个细节当你的数据包含多个交易日时feature_matrix的行顺序必须按日期升序排列并且每个日期的所有股票行要保持相邻。如果数据打乱了日期分组TimeSeriesSplit 就失去了意义。另外在金融数据中训练集和验证集之间最好设置一个间隔如一周避免因相邻交易日的高度相关性导致验证集偏乐观。3.3 特征重要性与因子有效性评估不只是输出 importanceLightGBM 训练完成后模型的feature_importance能告诉你哪些因子在分裂中被使用最多但它有三个盲区不能反映因子的方向一致性、不能反映因子的独立性、不能反映因子在不同市场状态下的稳定性。因此我一般会同时计算每个因子的 IC信息系数——即因子值与未来收益的截面秩相关系数。def calc_ic(prediction, forward_return): 计算 Rank IC预测值与实际收益的 Spearman 相关系数 return prediction.rank().corr(forward_return.rank()) # 对验证集逐日计算 IC daily_ic val_data.groupby(trade_date).apply( lambda d: calc_ic(d[pred], d[actual_return]) ) mean_ic daily_ic.mean() icir mean_ic / daily_ic.std() # IC 信息比率稳定性的度量Rank IC 是量化模型最核心的评价指标。IC 的绝对值大于 0.03 就说明模型有一定预测能力大于 0.05 属于优秀水平。ICIR 是 IC 均值除以 IC 标准差衡量预测能力的稳定性一般要求大于 0.3 才能支撑实盘应用。这里不要把 LightGBM 的 feature importance 和 IC 混为一谈——importance 高只说明因子在训练集上参与了较多分裂不代表它在样本外有稳定的预测方向。4. 回测验证与陷阱排查从预测值到可交易策略4.1 构建分层回测用十分组检验单调性训练完成的模型给出每只股票的预测值但预测值本身不是交易信号。最常见的验证方式是分层回测每天按预测值从高到低将股票分为 10 组计算每组未来 5 日的等权平均收益。如果模型有效第 1 组到第 10 组的收益应该呈现单调递减趋势且多空组合第 1 组做多、第 10 组做空的累计收益曲线应该平稳上行。def layer_backtest(daily_pred, daily_ret, layers10): 分层回测将每日预测值分为 10 层计算各层未来收益 results [] for date, group in daily_pred.groupby(trade_date): group[rank] group[pred].rank(pctTrue) group[layer] (group[rank] * layers).astype(int) merged group.merge(daily_ret.loc[[date]], onstock_code) layer_ret merged.groupby(layer)[future_ret].mean() results.append(layer_ret) layer_ret_df pd.DataFrame(results).mean() return layer_ret_df如果第 1 层和第 2 层收益差距不大但第 10 层收益特别差说明模型的区分能力集中在空头端。这种情况在实际操作中的意义有限因为 A 股市场做空手段受限融券成本高昂。因此很多模型在实盘中只使用多头端——每天选预测值最高的前 50 到 100 只股票等权买入持有一段时间后换仓。4.2 回测中的关键陷阱前视偏差、幸存者偏差与交易成本回测结果虚高的头号原因是前视偏差。除了前面提到的财务数据对齐问题常见的还有复权价格计算错误导致股息处理失误、停牌股在停牌期间被错误标记为可交易、涨跌停股在无法成交的情况下仍然计入组合收益。处理方式是在回测中维护一个可交易股票列表剔除当日停牌、涨跌停、上市不足 60 日、ST 的股票。幸存者偏差是另一个隐蔽问题——如果当前的股票列表只包含存续公司那么退市股票不会被纳入回测这会让收益虚高。解决方法是使用历史成分股列表或者在样本池中加入历史任意时点存在过的股票即使它后来退市了。交易成本方面模型换手率决定了对佣金和滑点的敏感度。日频调仓的模型单边换手率通常在 20% 到 50%按双边千分之三的费率计算年化成本可能超过 20%这会完全吞掉模型超额收益。因此回测框架必须逐笔扣除佣金、印花税和冲击成本。回测指标含义合格标准年化收益率策略总收益的年化值高于基准 5% 以上夏普比率年化收益 / 年化波动率大于 1.5最大回撤净值峰谷最大跌幅小于 20%卡玛比率年化收益 / 最大回撤大于 1.0换手率单边调仓比例均值低于 30%4.3 滚动训练与模型更新避免模型衰减市场风格漂移是机器学习多因子模型的最大敌人——去年有效的因子今年可能变成负贡献。常见做法是月度滚动重训练每个月用过去 12 个月的数据重新训练模型验证集采用最近 1 个月的数据然后对下一个月生成预测。def rolling_train_predict(feature_matrix, ret_matrix, re_train_period30): 每 30 个交易日重新训练一次滚动生成预测 all_dates sorted(feature_matrix[trade_date].unique()) predictions [] for i in range(re_train_period, len(all_dates), re_train_period): train_dates all_dates[max(0, i - 250):i] # 约 1 年训练窗口 val_dates all_dates[i:i re_train_period] model lgb.train(params, lgb.Dataset( feature_matrix[feature_matrix[trade_date].isin(train_dates)], labelret_matrix[ret_matrix[trade_date].isin(train_dates)] )) pred model.predict(feature_matrix[feature_matrix[trade_date].isin(val_dates)]) predictions.append(pred) return predictions训练窗口长度 250 个交易日约等于一年太短会欠拟合太长会包含陈旧的市场结构。重训练频率设置为每月一次这个频率在预测能力和计算成本之间取得了平衡。注意每次重训练时要使用最新的因子数据而不是沿用旧模型预测新数据。5. 实盘部署侧的验证技巧从回测曲线到可持续的超额收益回测结果理想后直接上实盘仍然会经历一段难熬的失效期。这里分享三个我验证模型真实有效性的技巧它们不需要更复杂的模型只需要更严格的数据审视。第一个技巧是样本外时间分割的“三重检验”。把历史数据按时间顺序切成三段开发段约 60%、验证段约 20%、押注段约 20%。开发段用来做因子探索和模型调参验证段用来评估模型效果并决定是否继续迭代押注段在全部开发工作完成后只跑一次作为最终接受与否的判断。很多人从头到尾只在一个时间段上反复调试这本质上是在同一段数据上做了多次隐式拟合。第二个技巧是监控因子衰减速度。实盘运行时每天计算模型当日的 Rank IC 和累计 IC 曲线。如果 IC 从 0.04 掉到 0 附近并持续一周先不要急着调参检查是否出现了极端市场风格切换。这时候要观察特征重要性变化如果价值类因子的 importance 显著上升、动量类因子下降说明市场风格发生了切换可以通过调整训练数据的时间权重来适应。LightGBM 没有内置时间衰减机制但你可以在训练时提高近期样本的权重也可以直接用最近 6 个月的迷你数据集训练一个对比模型观察它在同一验证集上的表现差异。第三个技巧是换手率约束下的组合构建。每天取预测值最高的前 50 只股票直接等权买入会产生高换手和大量不必要的交易成本。更好的做法是引入个股换手率约束和行业权重约束在优化器里求解组合权重目标函数是最大化预测分值同时最小化换手率。这个优化通常用线性规划求解器实现约束条件包括单股权重上限、行业暴露偏离度、换手率预算。回测中这个步骤能显著降低交易成本对超额收益的侵蚀实盘中更是必不可少的一环。from scipy.optimize import linprog # 简化示例换手率限制下的权重优化 # -pred 为负的预测分最小化向量为目标权重与上一期权重的绝对差 prev_weight np.array([0.02] * 50) # 上一期 50 只股票的权重 pred_score model.predict(new_features) # 约束权重之和为 1换手率不超过 30% A_eq [np.ones(50)] b_eq [1.0] bounds [(0.01, 0.05)] * 50 # 单股权重上下限 result linprog( c-pred_score, A_ubNone, b_ubNone, A_eqA_eq, b_eqb_eq, boundsbounds, methodhighs, )这段代码中的bounds将单股权重限制在 1% 到 5%避免预测头部集中度过高。linprog的methodhighs是目前求解速度和稳定性最好的后端选择。换手率约束通过添加额外的线性约束实现新权重与旧权重的绝对差之和除以 2 不超过 0.3。如果你希望在选股代码中直接加入这一约束需要引入额外的辅助变量表达绝对值之后再用A_ub和b_ub表达这些约束即可。模型在押注段跑出了预期效果后仍然要保留一个独立的数据卫生检查环节核对所有因子值在交易日结束时是否可得。例如如果某个因子依赖收盘价计算而回测中使用了当日收盘后的因子值去预测当日收盘买入的收益这就引入了仅 1 天的极小时间差前视偏差。严谨的做法是T 日收盘后计算因子值T1 日开盘买入组合。这个时间差看似微小在持仓期只有 5 天的模型中足以让夏普比率从 2.0 掉到 1.2。本文还有配套的精品资源点击获取
返回列表