资讯动态

LightGBM+BiLSTM股票量化源码解析:从因子筛选到A股回测实战

发布时间:2026/9/28 16:04:59 来源:尧图企业网站定制
简介这是一份面向毕业设计场景的 Python 股票量化系统完整源码包适合金融、计算机相关专业学生以及对 A 股量化投资策略与 Python 建模感兴趣的开发者。项目基于 A 股全市场股票数据先由 LightGBM 对 50 个价量因子完成重要度筛选保留 Top10 因子再接入 BiLSTM 模型进行因子组合与策略构建并经实证回测验证策略优于市场基准指数从数据处理、因子筛选、模型训练到回测评估完整展示了机器学习与深度学习方法在量化投研中的应用链路。压缩包共 1408 个文件大小约 14.12MB以 py/pyc 源码及编译文件为主同时包含可执行程序、配置脚本、说明文档、模型文件与少量图片表格便于直接运行实验、对比分析并继续二次开发。目前已有 640 人学习浏览对准备量化相关课题或毕业设计的学生有较高参考价值能帮助理解因子挖掘、时序预测与策略回测的完整落地方式。1. 股票量化不是玄学这套 LightGBMBiLSTM 源码在解决什么问题A股量化在很多人眼里是黑匣子拿到一份源码也不敢跑怕跑起来全是坑。这套 python股票量化系统源码 走的是一条很清晰的工程路线先用 LightGBM 在 50 个价量因子里筛出最重要的 10 个再用 BiLSTM 把这 10 个因子组合成预测模型最后落到回测上验证策略收益能不能跑赢沪深300。它适合两类人一类是毕业设计需要完整技术链的学生另一类是已经会写 Python、但没时间从零搭建因子选股框架的从业者。整份代码不是纯理论演示它自带 venv 虚拟环境、训练输出和回测依赖库解压之后按顺序激活环境就能开始干活。2. 先把环境跑起来venv 配置、DLL 依赖与 A 股数据预处理拿到 zip 先别急着看模型代码量化项目翻车有一半是环境问题另一半是数据问题。这套源码解压后你会看到一堆以 activate、pyvenv.cfg 结尾的 venv 虚拟环境文件以及 y_hat.csv、y_hat2.csv、fitness_dll.dll、oputils.dll 这样看起来不像 Python 工程的文件。先把它们搞清楚后面才不会跑一步报一个错。2.1 源码文件清单这些文件分别是干什么的我拆包的习惯是先列文件清单再按依赖关系决定启动顺序。这份资源里最容易被忽略的是两个 DLL 文件它们是 C 编译出的本地优化库LightGBM 训练和回测阶段会调用。如果它们缺失程序不会在 import 时报错而是跑到回测那一环突然崩溃或者直接闪退。文件类型作用activate / activate.bat / deactivate.bat环境脚本进入/退出 venv 虚拟环境pyvenv.cfg / sysconfig.cfg / setup.cfg环境配置记录 Python 版本与 pip 配置y_hat.csv / y_hat2.csv预测结果模型对验证集样本的预测输出用于回测fitness_dll.dll / oputils.dll本地库C 优化工具供因子筛选和回测调用从文件结构能推断出这套源码是用 venv 而不是 conda 管理的pyvenv.cfg 里写的 Python 版本就是你复现时的基准版本。如果解压后直接双击 activate.bat 提示版本不符最常见的坑是创建虚拟环境用的 Python 版本和你本机当前 Python 版本不一致。我一般会先看 pyvenv.cfg 里的 version 字段再用对应版本重建环境。2.2 启动虚拟环境Windows 与 macOS 的差异Windows 下的启动命令比较简单直接在当前目录执行 activate.bat。macOS 或 Linux 环境则要改成 source 语法。# Windows activate.bat # macOS / Linux source activate激活之后命令行前缀会变成(venv)这时候再执行python -V确认解释器路径指向当前目录。如果你用的是 VSCode还要手动把解释器切到.venv目录下否则编辑器右下角会沿用全局 Python。这一步不做后面装包全都装进了全局环境虚拟环境等于白建这是新手最容易踩的误区。2.3 A股全市场数据怎么准备行情清洗与复权处理这套系统的核心输入是 A 股全市场的日线行情。数据源可以自己找常见做法是拉取全市场 daily 表包含日期、股票代码、开高低收价、成交量、成交额。拿到原始数据后第一件事是复权因为前复权和后复权算出来的动量因子差别很大。import pandas as pd # 以股票代码和日期为索引读取原始日线数据 df pd.read_csv(daily.csv, parse_dates[trade_date]) df.sort_values([ts_code, trade_date], inplaceTrue) # 简化版前复权用复权因子把历史价格调整到当前口径 factor_col adj_factor df[adj_close] df[close] * df[factor_col] / df.groupby(ts_code)[factor_col].transform(last) # 过滤上市未满60天的次新股避免因子计算样本不足 df df[df[trade_date] df.groupby(ts_code)[trade_date].transform(min) pd.Timedelta(days60)]这段代码里有两个关键点。第一个是sort_values必须先按股票代码再按日期排序因子计算依赖时间顺序顺序乱了滚动窗口就全错。第二个是复权因子的用法A 股除权除息后不复权价格会出现跳空直接拿原始 close 算收益率会把分红造成的下跌误判成风险信号。数据量大的时候建议按 ts_code 分组循环处理不要一次性concat全市场数据内存容易撑爆。2.4 50个价量因子怎么算用滚动窗口生成特征矩阵因子是这套系统最底层的原料。所谓价量因子就是基于价格和成交量构造的技术指标比如过去 5 日动量、20 日波动率、成交量乖离率。50 个因子在代码里一般以排行榜或列表形式集中管理我拿到手后会先跑一遍全量因子列表确认没有用到未来数据。# 以动量类因子为例遍历股票代码滚动计算 def calc_momentum_feature(df, window[5, 10, 20]): df_feat pd.DataFrame(indexdf.index) for w in window: # close.diff(w) / close.shift(w) 表示过去 w 日的收益率避免用未来数据 df_feat[fmom_{w}] df[adj_close].diff(w) / df[adj_close].shift(w) return df_feat # 对全市场股票循环计算然后纵向拼接 all_feat [] for code, sub_df in df.groupby(ts_code): sub_df sub_df.sort_values(trade_date) all_feat.append(calc_momentum_feature(sub_df)) feature_matrix pd.concat(all_feat)因子计算的边界坑通常在窗口对齐上。shift(w)把当期因子值滞后了 w 天确保 t 时刻的因子只包含 t 之前的信息这是整个因子筛选阶段不能破的底线。如果你在源码里看到某个因子用的是close.pct_change().rolling(w).mean()这种未来窗口要立刻改成 shift 滞后处理。3. LightGBM 因子筛选50 个价量因子收敛到 10 个的做法这套系统最聪明的设计是先让 LightGBM 做减法而不是直接把 50 个因子塞进神经网络。原因很简单BiLSTM 对输入维度和噪声非常敏感原始因子里高度相关的技术指标会在神经网络里制造冗余计算还会放大过拟合。先用树模型筛一遍因子相当于给后面搭好了骨架。3.1 为什么用 LightGBM 而不是直接上 BiLSTMLightGBM 虽然是树模型但它有两个天然优势非常适合因子筛选。第一是它对特征尺度不敏感原始因子无论量纲差异多大树分裂只看阈值不需要先做标准化省掉不少预处理麻烦。第二是特征重要性输出非常直观一次训练就能给 50 个因子按重要性排出序方便人工校验是否符合正常逻辑。对比之下直接用神经网络筛选因子的做法在实践中很难落地。神经网络的特征重要性依赖梯度传播和置换检验计算成本高而且结果不稳定换一个随机种子排序就变了。树模型这一层筛选稳定很多并且训练速度以秒级计算200 棵树的模型在全市场数据上跑完也就几分钟。3.2 标签构造与训练集划分防止后视偏差用 LightGBM 筛因子之前得先定义什么叫“有效因子”。这套源码的做法是构造一个二分类标签未来 5 日收益率为正记 1否则记 0。因子是否重要看它对判断未来涨跌的贡献度。import lightgbm as lgb import numpy as np import pandas as pd # 特征矩阵与标签对齐 X feature_matrix # 50列因子 y (df[adj_close].shift(-5) / df[adj_close] - 1 0).astype(int) # 按时间切分前80%训练后20%验证严禁随机打乱 split_date X[trade_date].quantile(0.8) train_idx X[trade_date] split_date valid_idx X[trade_date] split_date dtrain lgb.Dataset(X[train_idx], labely[train_idx]) dvalid lgb.Dataset(X[valid_idx], labely[valid_idx]) params { objective: binary, metric: auc, learning_rate: 0.05, num_leaves: 31, max_depth: 6, feature_fraction: 0.8, verbosity: -1, } model lgb.train( params, dtrain, num_boost_round300, valid_sets[dvalid], callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)] )shift(-5)构造的标签用到了 t5 时刻的收盘价意味着预测目标是未来 5 日收益方向这在因子上是合法的前视标签。但如果把shift(-5)写错成shift(5)标签就会与历史数据对齐LightGBM 在验证集上的 AUC 会异常高后面回测收益率也跟着虚高这是拿到源码后必须第一个核对的地方。3.3 特征重要性的计算口径用 gain 而不是 splitLightGBM 的feature_importance()有两种口径split 表示特征被用于分裂的次数gain 表示特征分裂带来的平均增益之和。源码里应该用的是 gain 口径因为分裂次数多不代表因子有效可能一个噪声因子分裂了很多次但每次收益都很小。importance pd.Series( model.feature_importance(importance_typegain), indexX.columns ).sort_values(ascendingFalse) # 取前10个因子 top10 importance.head(10).index.tolist() print(top10)这里有一个值得注意的经验如果筛出来的前 10 因子全是动量类或者全是波动率类说明因子池本身的多样性不够。正常情况下价格动量、成交额、波动率、换手率这几类因子都应该有代表进入前十。用 LightGBM 做筛选的另一个好处是它能间接捕捉因子与因子之间的交互效应某个单独看不重要的因子在树模型里与其他因子组合后增益会明显上升。3.4 提高筛选稳定性的两个参数习惯为了筛选结果不随随机种子剧烈波动我跑这类任务时会固定三件事random_state、feature_fraction不要设成 1.0以及提前设置bagging_fraction。如果两次运行选出的 top10 因子重合度低于 80%说明这个因子池稳定性不够需要回去补充因子构造逻辑而不是盲目调参。params.update({ bagging_fraction: 0.8, bagging_freq: 1, seed: 42, feature_fraction_seed: 42, })4. BiLSTM 因子组合建模网络结构、训练参数与预测输出解析LightGBM 筛出的 10 个因子只是“原料清单”真正把原料组合起来的是 BiLSTM。它的作用不是预测股价本身而是预测未来收益率的排序。这套源码把预测结果写在 y_hat.csv 和 y_hat2.csv 里跑回测之前先要把这两份文件的含义搞清楚。4.1 BiLSTM 在因子组合上到底起了什么作用LSTM 家族的模型擅长抓住时间序列上的状态依赖股价和价量因子恰恰带有明显的时序特征。一个因子在 t 时刻的取值往往受过去一小段时间的走势影响而 BiLSTM 的特殊之处在于它同时用前向和后向两个方向编码序列。但这里有一个需要敲黑板的地方双向结构如果直接套用在预测任务上后向编码会把未来信息带进当前时刻的判断这在金融数据上就是前端看未来。常规做法是在训练阶段使用双向结构学习因子之间的时间关联预测时仍只用截止到当前时刻的输入窗口。拿到源码后先检查forward()函数里有没有对输入序列做截断处理这是判断代码专业度的一个指标。4.2 从因子到样本序列窗口的切分方法BiLSTM 的输入不能是单行因子值而是一个时间窗口内的连续序列。每只股票的因子矩阵需要切成形状为股票数 × 时间窗口数, 序列长度, 因子维度的样本。import numpy as np import torch SEQ_LEN 20 # 每个样本回看20个交易日 N_FEATURES 10 # LightGBM筛出的10个因子 BATCH_SIZE 256 # 按股票代码分组把连续因子序列切成seq_len, n_features的窗口 def build_sequences(feature_array, label_array, seq_len20): xs, ys [], [] for stock in range(feature_array.shape[0]): for i in range(seq_len, feature_array.shape[1]): xs.append(feature_array[stock, i-seq_len:i, :]) ys.append(label_array[stock, i]) return np.array(xs), np.array(ys) # 划分训练集与验证集按时间顺序 X_train, y_train build_sequences(train_feat, train_label) X_valid, y_valid build_sequences(valid_feat, valid_label) train_loader torch.utils.data.DataLoader( torch.tensor(X_train, dtypetorch.float32), batch_sizeBATCH_SIZE, shuffleTrue )这里seq_len20是最需要调超参数的环节。20 天大约对应一个月的交易窗口如果把它改成 60模型能看见更多历史信息但训练数据量会缩小到原来的三分之一过拟合风险随之升高。源码提供的默认值通常是反复试出来的不要一上来就大改。4.3 BiLSTM 网络结构与训练逻辑模型主体就是标准的嵌入层加双向 LSTM 层加全连接输出层。PyTorch 实现很简洁关键在双向层的输出如何合并。import torch.nn as nn class FactorBiLSTM(nn.Module): def __init__(self, input_size10, hidden_size64, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout, ) self.fc nn.Linear(hidden_size * 2, output_size) # 双向拼接维度翻倍 def forward(self, x): # x形状: (batch, seq_len, input_size) out, _ self.lstm(x) # out: (batch, seq_len, hidden*2) last out[:, -1, :] # 取最后一个时间步 return self.fc(last).squeeze(-1) model FactorBiLSTM() criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3)全连接层的输入是hidden_size * 2因为双向 LSTM 会把前向和后向的输出拼接在一起这一步忘了乘 2模型运行时就会报维度不匹配的错误。训练目标用的是回归任务直接预测未来 5 日累计收益率这样做的好处是回测阶段可以直接把预测值当因子排序不需要再做分类阈值转换。训练循环中推荐在验证集上做早停常见做法是连续 10 个 epoch 验证损失不下降就提前终止同时把最优权重保存下来避免后几个 epoch 过拟合导致预测值分布被拉偏。best_loss float(inf) for epoch in range(100): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() pred model(batch_x) loss criterion(pred, batch_y) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_loss criterion(model(X_valid), y_valid) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), bilstm_best.pt)4.4 y_hat.csv 与 y_hat2.csv 的差别在哪源码里输出两个预测结果文件通常会让人疑惑。按文件命名习惯推断y_hat.csv 可能是 LightGBM 筛选后 10 因子直接作为 BiLSTM 输入的验证集预测y_hat2.csv 可能是换了序列长度或换了随机种子之后重训模型的预测结果。跑通之前先用 Pandas 读一下两个文件的列数和行数。y1 pd.read_csv(y_hat.csv) y2 pd.read_csv(y_hat2.csv) print(y1.shape, y2.shape) print(y1.head())如果两份文件的行数不一致先检查索引是否与验证集日期对齐最常见的问题是索引错位导致回测代码合并预测值和行情数据时出现大量 NaN。如果只是预测值大小不同那是正常现象可以分别跑一遍回测看看哪组结果更稳定。正规的验证逻辑是选择在验证集上 IC信息系数更高、换手率更温和的那组输出而不是单看收益哪个高。5. 回测与排查为什么同样的代码跑出来的结果不如注释里的数字回测阶段是整个项目最容易让人怀疑人生的地方。源码注释里写着年化收益跑赢沪深300你自己跑出来却发现曲线在基准下方趴着。出现这种落差通常不是模型失效而是回测口径里的手续费、滑点、调仓时点和你默认的设置不一样。5.1 回测框架必须设置的四个参数A股回测不能像美股那样忽略交易成本。源码里应该内置了一个包含手续费、印花税、滑点和调仓频率的参数区这是回测诚实程度的底线。下面是行业里比较常见的默认取值可以直接对照源码里的参数检查参数常见取值说明佣金费率万2.5双边收取最低5元印花税千1卖出单边收取滑点0.1%按成交价偏差估算调仓频率每5个交易日过高会吃掉收益过低会钝化如果源码注释里写的是“单边万3佣金忽略滑点”那跑出来的收益会偏高但不算失真如果写的是“零手续费零滑点”那回测曲线再漂亮也没有实际参考价值。5.2 回测策略构建预测值如何转成持仓BiLSTM 预测出的是每只股票未来 5 日的预期收益率。策略层需要把这组连续值转成持仓权重。最稳妥的做法是排序分组而不是硬性阈值。# 按预测收益从高到低排序取前10%作为持仓 daily_pred pd.DataFrame({code: codes, pred: pred_values}) daily_pred[rank] daily_pred[pred].rank(ascendingFalse, pctTrue) selected daily_pred[daily_pred[rank] 0.1][code].tolist() # 模拟按开盘价调仓记录成交价和交易成本 def execute_trade(selected_stocks, cash, fees): position_size cash / len(selected_stocks) * (1 - fees) return {code: position_size for code in selected_stocks}这里最容易出问题的是调仓时点。如果源码是在第 t 天收盘后拿到预测值、第 t 天收盘价成交那就用到了当天收盘价信息属于轻微未来函数。标准做法是第 t 天收盘后产生信号第 t1 天开盘按开盘价调仓。这个细节直接决定回测曲线每年差出几个百分点。5.3 三条高频踩坑记录现象一运行回测脚本时预测值和行情数据合并后行数骤减。 原因y_hat.csv 的日期索引和行情表索引没有对齐merge时默认做内连接把缺失日期的行直接丢掉了。 解决合并前先reset_index()查看两个 DataFrame 的日期范围是否一致再用outer连接保留全部交易日最后dropna()处理无法预测的日子。现象二LightGBM 训练时 AUC 高达 0.98但回测收益为负。 原因标签构造用了未来 5 日收益但特征矩阵里混入了当日收盘后才知道的成交量数据形成了信息泄露。A 股的成交量是当日收盘后更新如果你在 t 日开盘就用当日的成交量因子等于提前看了收盘答案。 解决所有当日价量因子必须前移一天用shift(1)把因子值压到上一交易日的时点上。现象三验证集结果很好一上全市场测试收益就垮掉。 原因验证集是单一时段可能恰好处于普涨行情模型学到的因子组合规律只在那一刻有效。 解决按时间滚动切分出三段区间分别做训练和测试观察三段结果是否一致。只在一段时间有效的话说明因子选出的 10 个指标本身的生命周期太短需要回因子筛选阶段调整。5.4 拿到源码后的自查路径拿到这套源码我建议按固定顺序检查五个位置环境是否激活、数据是否复权、标签是否用shift(-5)构造、特征是否用shift(1)滞后、回测调仓是否在次日开盘执行。这五个点全部核对无误跑出来的结果和源码注释之间通常不会差太多如果还差再考虑是不是手续费参数的问题。6. 把策略搬到自己的机器上从预测结果到模拟交易的三步验证整套源码跑通只是第一步把模型输出的预测结果变成能信任的投资信号还需要过三道关。我的习惯是先验验证集上的 IC再验滚动窗口下的稳定性最后用纯模拟方式跑一个月的实盘节奏。第一步是算信息系数 IC。IC 的意思是模型预测值与真实收益之间的秩相关系数计算公式是 Spearman 相关系数。Rank IC 大于 0.03 说明预测有微弱的信息量大于 0.06 已经是很不错的结果。from scipy.stats import spearmanr # 合并模型预测值和未来真实收益 valid_df pd.DataFrame({ y_pred: y_hat, y_real: future_return }) valid_df valid_df.dropna() ic, p_value spearmanr(valid_df[y_pred], valid_df[y_real]) print(fRank IC: {ic:.4f}) # 按月份分组计算月度IC并统计IC胜率 valid_df[month] pd.to_datetime(valid_df.index).to_period(M) monthly_ic valid_df.groupby(month).apply( lambda g: spearmanr(g[y_pred], g[y_real])[0] ) print(fIC胜率: {(monthly_ic 0).mean():.2%})从这份资源的使用场景出发我不建议只跑一次验证集就收工。量化系统的核心价值不在某一个模型的精度而在训练流程是否可重复。从那以后我每次拿到新的量化源码都强制走一遍滚动窗口重训——把训练区间每次后移 3 个月重新训练模型并记录 IC连续三次 IC 都稳定为正我才会认真对待这组预测结果。第三步是用模拟盘程序跑一个月的每日节奏把模型预测值按天落库记录每天的调仓信号和实际成交价差。这一步能很直观地暴露滑点设置是否过于乐观也会让你看清哪些预测值在实盘中根本成交不了。量化系统的价值终究要在时间上验证希望这份源码能帮你把第一版策略稳稳跑起来。经过以上整理资源逻辑已基本梳理清楚你可以直接按结构使用或复现。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑