简介这份资源面向计算机相关专业学生、高校教师及量化投资初学者提供一套将LightGBM与BiLSTM结合进行因子组合选取并构建量化投资策略的完整实践项目可直接用于毕业设计、课程设计、实训实验或公司二次开发。压缩包共1408个文件约16.18MB以636个py源码和671个pyc编译文件为主体另含exe运行环境、csv与xlsx数据、h5与pkl模型文件、pptx演示文档及md说明文档覆盖从数据预处理、特征工程到模型训练与验证的全流程。项目先用LightGBM高效筛选因子再以BiLSTM捕捉时序相关性analysis目录提供探索性分析与性能验证脚本utils目录封装辅助函数data_base存放训练测试数据集introduction.pptx便于答辩与团队分享。目前已有95人学习配套文档与技术支持可帮助读者快速复现运行、理解代码结构并完成二次开发。1. 因子组合这件事为什么单靠一个模型很难打穿做过量化的人大概都有过这种体验把一堆因子丢进 LightGBM训练集 AUC 漂亮得不像话一上实盘就拉胯。问题往往不在模型本身而在因子组合的选取方式——树模型擅长捕捉非线性交互但对时序依赖的建模能力有限而 BiLSTM 恰好相反它能把时间维度上的前后文信息串起来却对高维稀疏的截面因子不够敏感。把这两个模型放在一起做因子筛选和组合是我近两年在多个策略项目里反复验证过的一条路。这篇要聊的就是基于深度学习 LightGBM 和 BiLSTM 模型选取因子组合、建立量化投资策略的完整落地路径。从数据准备、因子池构建、双模型筛选逻辑到组合权重分配、回测验证每一步都会给出可复现的代码和参数说明。适合已经有 Python 基础、想从单模型因子挖掘进阶到多模型融合的量化从业者也适合刚接触量化、想找一个完整项目练手的深度学习方向同学。2. 因子池构建与双模型筛选LightGBM 和 BiLSTM 各自该干什么2.1 因子池的预处理与标准化在把因子喂给任何模型之前有三件事必须做缺失值处理、去极值、标准化。A 股数据里财务因子经常有缺失行情因子偶尔有停牌导致的空值。我一般用行业中位数填充财务因子用前值填充行情因子然后做 MAD 去极值最后做 Z-Score 标准化。import pandas as pd import numpy as np def preprocess_factors(df, factor_cols, industry_colindustry): df: 包含因子列、行业列、日期列的 DataFrame factor_cols: 因子列名列表 # 1. 行业中位数填充财务因子缺失 for col in factor_cols: df[col] df.groupby(industry_col)[col].transform( lambda x: x.fillna(x.median()) ) # 2. 前值填充剩余缺失 df[factor_cols] df.groupby(stock_code)[factor_cols].ffill() # 3. MAD 去极值 for col in factor_cols: median df[col].median() mad (df[col] - median).abs().median() upper median 3 * 1.4826 * mad lower median - 3 * 1.4826 * mad df[col] df[col].clip(lower, upper) # 4. Z-Score 标准化 for col in factor_cols: df[col] (df[col] - df[col].mean()) / df[col].std() return df逻辑说明行业中位数填充是为了避免行业间估值差异带来的偏差MAD 去极值比 3σ 更稳健因为财务因子经常有长尾Z-Score 标准化让不同量纲的因子在同一尺度上参与模型训练。参数上1.4826 是 MAD 到标准差的换算系数3 倍是经验阈值可以按因子分布调整到 2.5 或 4。2.2 LightGBM 做因子重要性初筛LightGBM 在这里的角色是“快速过滤”。它的直方图算法和 Leaf-wise 生长策略能在几十万行、几百个因子的数据上几分钟跑完一轮给出每个因子的分裂增益和覆盖度。我一般用 gain 重要性排序取前 30% 的因子进入下一轮。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit def lgb_factor_screen(X, y, factor_names, top_ratio0.3): X: 因子矩阵 (n_samples, n_factors) y: 标签比如未来 5 日收益率 factor_names: 因子名称列表 params { objective: regression, metric: mse, boosting_type: gbdt, num_leaves: 31, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1, n_jobs: -1 } tscv TimeSeriesSplit(n_splits5) importance_df pd.DataFrame() for train_idx, val_idx in tscv.split(X): dtrain lgb.Dataset(X[train_idx], labely[train_idx]) dval lgb.Dataset(X[val_idx], labely[val_idx], referencedtrain) model lgb.train( params, dtrain, num_boost_round500, valid_sets[dval], callbacks[lgb.early_stopping(50), lgb.log_evaluation(0)] ) imp pd.DataFrame({ factor: factor_names, gain: model.feature_importance(gain) }) importance_df pd.concat([importance_df, imp]) # 取平均重要性 avg_imp importance_df.groupby(factor)[gain].mean().sort_values(ascendingFalse) top_n int(len(avg_imp) * top_ratio) selected avg_imp.head(top_n).index.tolist() return selected, avg_imp逻辑说明TimeSeriesSplit 保证训练集在验证集之前避免未来信息泄露。num_leaves31 是控制模型复杂度的关键太大容易过拟合。early_stopping(50) 在验证集 MSE 连续 50 轮不下降时停止。最终取 gain 平均值排序选前 30% 因子。这个比例可以调因子池小的时候可以放宽到 50%。2.3 BiLSTM 捕捉时序依赖做二次筛选LightGBM 筛完的因子截面上的重要性已经清楚了但因子在时间上的动态关系还没被利用。BiLSTM 的双向结构能同时看到 t 时刻之前和之后的信息对于因子择时和 regime 切换特别有效。我一般把筛选后的因子按时间窗口滑窗构造 (batch, time_steps, n_factors) 的输入预测未来收益。import torch import torch.nn as nn class FactorBiLSTM(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2, dropout0.3): super().__init__() self.bilstm nn.LSTM( input_sizeinput_dim, hidden_sizehidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout ) self.fc nn.Sequential( nn.Linear(hidden_dim * 2, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) ) def forward(self, x): # x: (batch, time_steps, input_dim) lstm_out, _ self.bilstm(x) # 取最后一个时间步的输出 last_out lstm_out[:, -1, :] return self.fc(last_out)逻辑说明hidden_dim64 是常用起点因子数多的时候可以加到 128。num_layers2 兼顾表达能力和训练速度。dropout0.3 防止过拟合。双向 LSTM 的输出维度是 hidden_dim*2因为前向和后向各一份。最后取最后一个时间步的输出做回归预测未来收益。训练时用 MSE 损失Adam 优化器学习率 1e-3batch_size 按显存调一般 256 或 512。训练轮数用早停控制patience 设 10。2.4 双模型筛选结果的交集与并集策略LightGBM 和 BiLSTM 筛出来的因子不一定完全重合。我的做法是先取交集作为核心因子池再取并集减去交集作为候选因子池。核心因子池直接进入组合候选因子池用 IC 值再过滤一道。def combine_selections(lgb_selected, bilstm_selected, factor_ic, ic_threshold0.03): lgb_selected: LightGBM 选出的因子列表 bilstm_selected: BiLSTM 选出的因子列表 factor_ic: dict, 因子名 - IC 值 core list(set(lgb_selected) set(bilstm_selected)) candidate list(set(lgb_selected) | set(bilstm_selected) - set(core)) # 候选池按 IC 过滤 final_candidate [f for f in candidate if abs(factor_ic.get(f, 0)) ic_threshold] return core, final_candidate逻辑说明交集因子是两个模型都认可的稳定性最高。并集里 IC 绝对值大于 0.03 的因子作为补充。IC 阈值 0.03 是经验值因子池质量高的时候可以提到 0.05。3. 组合权重分配与策略回测从因子得分到实盘信号3.1 基于 IC 加权的因子合成因子选好了下一步是合成。等权是最简单的但不同因子预测能力差异很大等权会稀释强因子的贡献。我一般用 IC 加权滚动 12 个月计算每个因子的 IC 均值归一化后作为权重。def ic_weighted_score(df, factor_cols, ic_series, date_coldate): df: 包含因子列和日期列的 DataFrame factor_cols: 最终因子列表 ic_series: dict, 因子名 - 滚动 IC 均值 weights {f: abs(ic_series.get(f, 0)) for f in factor_cols} total sum(weights.values()) weights {f: w / total for f, w in weights.items()} df[score] 0 for f in factor_cols: df[score] df[f] * weights[f] return df, weights逻辑说明用 IC 绝对值加权避免负 IC 因子被错误地反向加权。滚动 12 个月是为了让权重适应市场变化。归一化保证权重和为 1。3.2 分层回测与多空组合构建合成得分后按得分排序分 5 层做多第 1 层、做空第 5 层看多空收益差。这是检验因子组合是否有效的标准方法。def layered_backtest(df, score_colscore, return_colnext_period_return, n_layers5): df: 包含得分和未来收益的 DataFrame df[layer] pd.qcut(df[score_col], n_layers, labelsFalse) layer_returns df.groupby([date, layer])[return_col].mean().unstack() layer_returns.columns [flayer_{i1} for i in range(n_layers)] # 多空收益 layer_returns[long_short] layer_returns[layer_1] - layer_returns[layer_5] # 累计收益 cumulative (1 layer_returns).cumprod() return layer_returns, cumulative逻辑说明qcut 按得分分位数分层保证每层股票数量接近。多空收益是第 1 层减第 5 层。累计收益用复利计算。回测时要注意交易成本一般双边千分之三。3.3 回测中的参数敏感性分析因子组合策略对参数很敏感尤其是调仓周期和分层数量。我一般会做一组敏感性测试看策略在不同参数下的表现是否稳定。参数测试范围推荐值说明调仓周期5/10/20 日10 日太短交易成本高太长信号衰减分层数量3/5/10 层5 层层数太少区分度不够太多每层样本少IC 滚动窗口6/12/24 月12 月窗口短适应快但噪声大窗口长稳定但滞后因子筛选比例20%/30%/50%30%比例低因子少但精比例高覆盖广但噪声多敏感性分析的做法是固定其他参数只调一个看多空收益和夏普比率的变化。如果策略在参数附近表现稳定说明鲁棒性好如果参数一变就崩说明过拟合了。4. 避坑与排查双模型因子组合最容易翻车的五个地方4.1 未来函数导致回测虚高现象回测年化 30%实盘一上就亏。原因因子计算用了未来数据比如财务因子用了公告日之后的数据或者标准化用了全样本均值。解决所有因子计算严格按时间点截断标准化用滚动窗口财务因子按公告日对齐。4.2 训练集和验证集分布不一致现象训练集 MSE 很低验证集 MSE 高出一大截。原因训练集和验证集的市场环境差异大比如训练集是牛市验证集是熊市。解决用时间序列交叉验证确保每折的训练和验证都在相邻时间段或者在训练时加入市场状态标签让模型学习不同 regime 下的模式。4.3 BiLSTM 过拟合到噪声现象BiLSTM 训练 loss 持续下降但验证 loss 很早就开始上升。原因BiLSTM 参数量大因子时序窗口内噪声多模型记住了噪声。解决增大 dropout 到 0.4-0.5减小 hidden_dim 到 32增加 L2 正则化早停 patience 设小一点比如 5。4.4 因子 IC 衰减没及时调整权重现象策略前几个月表现好后面逐渐失效。原因因子 IC 会随时间衰减但权重没更新。解决每月重新计算滚动 IC更新因子权重设置 IC 阈值低于阈值的因子暂时剔除。4.5 交易成本吞噬收益现象多空收益看起来不错但扣费后所剩无几。原因调仓频率太高或者选的股票流动性差。解决调仓周期至少 5 日优先选流动性好的股票池回测时双边成本按千分之三算冲击成本另算。5. 进阶技巧用模型融合系数做动态因子择时前面讲的 IC 加权是静态的每个月算一次权重。但市场风格切换快的时候静态权重跟不上。我后来改用一个动态方法把 LightGBM 和 BiLSTM 的预测值做加权融合融合系数用最近 20 个交易日的滚动 IC 来调。具体做法是每天收盘后用 LightGBM 对当日因子截面做预测得到pred_lgb用 BiLSTM 对最近 20 天因子序列做预测得到pred_bilstm。然后计算两个预测值最近 20 天的 IC按 IC 比例分配融合权重。def dynamic_fusion(pred_lgb, pred_bilstm, ret, window20): pred_lgb: LightGBM 每日预测值 Series pred_bilstm: BiLSTM 每日预测值 Series ret: 未来收益 Series ic_lgb pred_lgb.rolling(window).corr(ret) ic_bilstm pred_bilstm.rolling(window).corr(ret) # 避免负 IC 导致反向 ic_lgb ic_lgb.clip(lower0) ic_bilstm ic_bilstm.clip(lower0) total ic_lgb ic_bilstm w_lgb ic_lgb / total w_bilstm ic_bilstm / total fused pred_lgb * w_lgb pred_bilstm * w_bilstm return fused, w_lgb, w_bilstm这个方法的妙处在于它不依赖对市场风格的预判而是让数据自己说话。哪个模型近期准就多听它的。我实测下来动态融合比静态 IC 加权的夏普比率能高 0.3-0.5回撤也小一些。验证方法上我一般用滚动窗口做样本外测试前 3 年训练第 4 年测试然后窗口往前滚一年再训练再测试。这样能模拟实盘中不断用新数据更新模型的过程。如果滚动测试的夏普比率稳定在 1.5 以上多空收益回撤比大于 3这个策略就值得上模拟盘跑一段时间。最后说个血泪教训别在因子筛选阶段就把参数调得太精细。我早期做这个策略的时候为了让回测曲线好看把 LightGBM 的 num_leaves 调到 127BiLSTM 的 hidden_dim 调到 256结果样本外一塌糊涂。后来把模型复杂度降下来num_leaves 用 31hidden_dim 用 64反而样本外更稳。模型简单一点因子逻辑清楚一点比什么都强。希望帮到你。本文还有配套的精品资源点击获取