
简介这是一份面向高校计算机、人工智能及相关专业学生的毕业设计级源码包聚焦LSTM财务因子预测选股模型的完整实现。项目包含7个Python脚本覆盖LSTM分类模型、BP分类模型、MACD/RSI技术指标计算、MindGo平台模型接入与单模型预测等核心模块同时附有checkpoint模型权重、meta/index/data文件与JSON配置便于直接加载训练结果继续调试。压缩包共14个文件包含py源码、模型文件、md/txt说明文档等类型整体仅1.19MB轻量易复现。目前已有40人学习下载。借助运行说明和项目介绍读者可快速理解数据流向与训练流程在此基础上修改因子组合、调整网络结构或接入自有数据即可适配毕业设计、课程设计及科研演示初学者也可通过源码重点掌握LSTM因子建模、BP对比模型、技术指标合成与模型持久化的典型实践。1. LSTM财务因子预测选股模型源码Python实现拆开这个高分项目前你需要知道的事如果你想找一份能直接跑、能写进毕业论文的量化选股源码这个标题确实是常见检索里命中率很高的一个方向。它的核心不是“预测股价”而是用财务因子PE、ROE、营收增速这类基本面数据构建特征再用LSTM捕捉因子在时间序列上的变化规律最终输出一只股票的“未来收益预测值”按预测值排序选股。整个过程从数据拉取、因子计算、样本构造、模型训练到回测对比都是一条完整可复现的技术路线。这套东西能解决什么问题最直接的是给毕业设计提供一个“深度学习金融数据”的落地框架不用玄学调参不用手工打分模型自己从因子历史里学非线性关系。它适合已经会Python基础语法、懂一点pandas和机器学习、但没系统做过时序预测项目的本科生或硕士生。源码包拿到手后你的任务不是改几行代码就交差而是搞清楚每一层数据是怎么流动的否则答辩时一句“标准化为什么在截面上做”就能让你翻车。2. 财务因子选股为什么需要LSTM从打分逻辑到序列预测2.1 财务因子到底指什么四个维度与计算口径财务因子不是随便选几个指标堆在一起。常见做法是把基本面数据分成四类盈利能力ROE、毛利率、净利率、成长能力营收同比增速、净利润同比增速、偿债能力资产负债率、流动比率和营运能力总资产周转率、存货周转天数。每一类选两到三个指标总共十个左右构成因子池。这里有一个容易忽略的口径问题财务数据是季度披露的而且年报、季报的发布时间有滞后。用Tushare或AkShare拉数据时接口返回的end_date是报告期截止日不是数据真正可用的日期。比如2022年年报的end_date是2022-12-31但公告日ann_date可能是2023年4月底。如果直接用报告期做特征等于让模型在2022年底就“知道”了2023年4月才公布的数据这就是典型的前视偏差后文会专门讲。import pandas as pd # 假设已经从数据接口拉到了财务指标宽表 fin_df # 字段包括ts_code(股票代码), end_date(报告期), ann_date(公告日期), roe, gross_profit_margin, revenue_yoy fin_df pd.read_csv(financial_factors.csv, parse_dates[end_date, ann_date]) # 关键一步用公告日期而不是报告期日期对齐到行情 fin_df[factor_date] fin_df[ann_date] fin_df fin_df.sort_values([ts_code, factor_date]).drop_duplicates( subset[ts_code, end_date], keeplast )这段代码的核心逻辑是建立“因子可用时间”的概念。字段用ann_date是因为只有在公告发布之后这个因子值才真实可用drop_duplicates的目的是防止同一报告期被重复计算。如果你在原始项目里看到直接用end_date做对齐建议改成公告日期这个改动能让整个实验的可信度上一个台阶。2.2 量化选股里如何构建“未来收益标签”有监督学习必须有标签。在选股模型里标签通常不是“涨/跌”这种分类而是未来N个交易日的收益率。N取多少取决于策略调仓频率做月度调仓就用未来20个交易日做季度调仓就按交易日换算成大约60个交易日。构造标签时有一个细节决定模型成败标签必须用“未来收盘价 / 当前收盘价 - 1”如果当前特征里包含了当天收盘价那模型其实在拿当天的信息预测当天的涨跌这在回测里会表现为结果特别好实盘却一塌糊涂。# 行情数据每只股票的日线 # 字段ts_code, trade_date, close price_df pd.read_csv(daily_price.csv, parse_dates[trade_date]) price_df price_df.sort_values([ts_code, trade_date]) # 未来20个交易日收益月度调仓标签 price_df[ret_future] ( price_df.groupby(ts_code)[close].shift(-20) / price_df[close] - 1 ) # 合并因子与标签时按股票代码和时间对齐 # 注意合并必须在“因子日期”之后、且未到“未来收益截止日”之前完成shift(-20)表示把未来第20天的收盘价搬到今天这一行这是构造未来收益最直接的方式。做回归任务时标签就是连续收益率如果想做成三分类涨、平、跌把收益率映射到区间即可。但实践中回归任务更常用因为模型输出的连续值可以直接用于排序选股而不是硬切分类边界。2.3 从因子池初筛到相关性去重因子不是越多越好。财务因子之间高度相关比如ROE和ROA都反映盈利能力一起放进去会让LSTM学到重复模式还会让模型对冗余特征过拟合。常见做法是先做相关性矩阵把相关系数超过0.7的因子只留一个同时剔除缺失率超过30%的因子。# 因子初筛计算相关系数矩阵并剔除高相关因子 corr_matrix factor_df.corr().abs() upper_tri corr_matrix.where( np.triu(np.ones(corr_matrix.shape), k1).astype(bool) ) # 找出相关系数大于0.7的因子对保留每个因子对里缺失率更低的一个 high_corr_pairs [ (col, row) for row in upper_tri.index for col in upper_tri.columns if upper_tri.loc[row, col] 0.7 ] drop_cols [] for col, row in high_corr_pairs: if row not in drop_cols: drop_cols.append(col) factor_df factor_df.drop(columnsdrop_cols)这个筛选逻辑虽然用列表推导式写的但思路很清晰适用于任何因子池。筛选后还需要做截面标准化和极值处理每个交易日按所有股票的因子值做Z-score然后用分位数截断法把极端值拉回来。不做这一步的话市值大的银行股会因为绝对数值大而长期霸榜模型学到的不是选股逻辑而是市值偏好。交叉验证的切分最重要否则“验证集loss低、测试集loss高”这种现象会折磨你好几个晚上。# 按时间切分而不是随机切分 train_end 2022-12-31 val_end 2023-12-31 train_mask df[date] train_end val_mask (df[date] train_end) (df[date] val_end) test_mask df[date] val_end # 标准化参数只在训练集上拟合 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(df.loc[train_mask, feature_cols]) X_val_scaled scaler.transform(df.loc[val_mask, feature_cols]) X_test_scaled scaler.transform(df.loc[test_mask, feature_cols])这段逻辑的任何一步都不能错。fit_transform用在训练集transform用在验证集和测试集这是一个“时序预测里最常见的错误”如果你在项目源码里看到fit_transform对全量数据执行建议立刻改掉。随机打乱数据会让模型记住时间顺序里的偶然模式而真实场景里你永远在预测未来。3.2 滑动窗口样本是“截面的”不是“整段的”很多初学LSTM的人会把选股数据做成类似股价预测那种单条长序列一只股票连续600天的行情前500天训练后100天验证。这在选股场景里不对。选股模型需要处理的是“当前时刻所有股票的截面”然后从中挑出未来表现最好的那一批。正确的样本构造方式每个样本代表“某只股票在某个时间点过去K期的因子序列”形状是(seq_len, num_features)。如果按月调仓seq_len取12含义是过去12个月的月度因子值。把每只股票的样本按时间排列然后所有股票在同一个时间段内的样本合并成一个“截面样本集”。class FactorSequenceDataset(Dataset): def __init__(self, X, y): self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] # 构造三维数组样本数 × 时间步 × 特征数 # df 已按 ss_code、date 排序feature_cols 是因子列名 def build_sequences(df, feature_cols, seq_len12): X, y [], [] for code, group in df.groupby(ts_code): group group.sort_values(date) values group[feature_cols].values targets group[ret_future].values for i in range(len(group) - seq_len): X.append(values[i : i seq_len]) y.append(targets[i seq_len - 1]) return np.array(X), np.array(y)这里seq_len取12代表用12个月的因子序列预测下月收益。每个样本的标签是序列末期的未来收益而不是序列第一天的。遍历顺序是先按股票分组再按时间排列构建后每个样本仍然是“一只股票的一段历史”但训练时所有股票的所有片段合并在一起参与梯度计算。3.3 缺失值和退市股票怎么处理两个影响结果的细节财务因子天然有缺失次新股没有足够历史季报部分公司会延迟披露。处理缺失值不能直接dropna()因为这会砍掉大量样本而且被留下的都是老牌大公司样本选择偏差很严重。常见做法是前向填充用上一期值补再用行业均值兜底。退市股票的问题更隐蔽。如果你只拿当前还在交易的股票列表去拉历史数据模型只见过“活下来”的公司这在金融领域叫幸存者偏差。体现在实验里就是回测很好、实盘一买就跌。处理办法是尽量使用“上市至今全部股票列表”作为股票池即使某只股票后来退市了也要把它退市前的数据保留在训练集里。在答辩时主动说清楚这一点会是个明显的加分项。4. LSTM模型搭建用PyTorch实现最小可跑的选股模型4.1 模型结构两层LSTM加回归头为什么不在LSTM后接Softmax选股模型的输出层应该是一个连续数值代表预测的未来收益而不是一个分类概率。所以这里用的是回归头损失函数是MSE或者Huber Loss。如果用Softmax输出“涨/跌”概率等于把问题简化成分类会丢失收益幅度信息排序选股时不灵敏。import torch import torch.nn as nn class LSTMValueModel(nn.Module): def __init__(self, n_features, hidden_size32, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.head nn.Sequential( nn.Linear(hidden_size, 16), nn.ReLU(), nn.Linear(16, 1), ) def forward(self, x): # x shape: [batch, seq_len, n_features] out, _ self.lstm(x) # 取最后一个时间步的隐藏状态 last_out out[:, -1, :] return self.head(last_out).squeeze(-1)batch_firstTrue这个参数经常被忽略它决定输入张量的维度顺序是(batch, seq_len, features)而不是(seq_len, batch, features)。如果不设这个参数后续所有数据构造都要反过来非常容易出错。out[:, -1, :]取的是序列最后一步的隐藏状态因为LSTM的循环结构已经把整个序列的信息压缩到了最后一步。4.2 训练配置学习率、早停与正则化LSTM训练最常见的坑是梯度爆炸尤其当seq_len较长、网络层数较多时。解决方案是梯度裁剪gradient clipping把梯度的L2范数截断到某个阈值内。还有学习率不能太大1e-3起步比较安全训练到loss平台期可以降到1e-4。import torch.optim as optim from torch.utils.data import DataLoader model LSTMValueModel(n_featuresX_train.shape[-1]) optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() train_loader DataLoader(train_dataset, batch_size256, shuffleTrue) epochs 80 for epoch in range(epochs): model.train() epoch_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb.view(-1)) loss.backward() # 梯度裁剪防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() if (epoch 1) % 10 0: print(fEpoch {epoch1}/{epochs}, Loss: {epoch_loss / len(train_loader):.4f})训练时loss不下降先别急着加层数或调学习率检查数据是否标准化、标签是否有极端值。一个比调参更常见的问题因子数据里如果混入了一个单位是“亿”和一个单位是“元”的字段LSTM会优先去拟合数值大的那个维度。截面标准化能解决这个但很多人只对训练集做了标准化验证集却忘了。4.3 参数边界hidden_size、num_layers、seq_len怎么定LSTM参数不是越大越好。财务因子数据通常样本量只有几千条模型容量过大会直接记忆训练集验证集表现反而更差。我给一个自己常用的起点hidden_size32num_layers2dropout0.2seq_len12。这个组合参数量不大在小样本上不容易过拟合跑一轮训练也很快。如果验证集loss一直降不下来再按顺序调整先加大hidden_size到64看看不行再加到128层数加到3层通常已经是上限4层的LSTM在财务数据上几乎必然过拟合dropout加到0.5可以缓解过拟合但太高会让模型欠拟合表现为loss在训练集上也不下降。seq_len的取值跟调仓周期强相关如果按月调仓12个月的因子序列已经足够没必要用60个月去追求“历史更长”序列过长模型会学到很多久远且可能失效的模式。# 一个稳定的小模型配置通常不需要再往上加复杂度 model LSTMValueModel( n_featureslen(feature_cols), hidden_size32, num_layers2, dropout0.2, )批大小256在全连接网络里不算大但LSTM的隐层状态需要跨时间步传递batch太大反而会让收敛变慢。如果显存或内存紧张降到128如果样本量很大也可以试着提到512。这些参数之间没有绝对最优重要的是固定其他变量、一次只改一个然后记录验证集表现。5. 回测口径、数据泄漏排查与提分方向5.1 未来函数是高分项目的头号杀手现象回测曲线漂亮得惊人年化收益超过100%最大回撤不到5%但你知道真实世界里这几乎不可能。原因代码里某个环节用了未来数据。最常见的是因子对齐用了报告期而不是公告日期或者回测买入价用的是当天收盘价而不是次日开盘价。解决逐行检查三个关键点——因子是否在公告日后才可用、标签是否真的对应未来、回测撮合时是否用了当日无法获取的价格。提示回测时买入价用“次日开盘价”是最安全的假设。用当日收盘价成交等于让模型知道了当天全部信息这在实盘里做不到。5.2 选股回测和组合回测是两回事很多人把“模型预测Top20股票”直接当成回测结果展示这不够。选股模型回测应该是一个滚动过程在每个月末用截至当时可用的数据预测下个月所有股票的收益取预测值最高的前20只等权买入持有一个月后卖出计算这一期的组合收益。把每个月组合收益连起来才是策略净值曲线。# 滚动回测的核心逻辑伪代码 monthly_dates sorted(df[date].unique()) results [] for i in range(len(monthly_dates) - 1): current_date monthly_dates[i] # 训练数据只用到 current_date 之前且因子已公告 model.fit(X_train, y_train) # 对当前截面所有股票预测 preds model.predict(X_current) # 取预测值最高的20只按等权持有到下个月 top20 get_top_n(preds, n20) next_month_return compute_return(top20, current_date, monthly_dates[i 1]) results.append(next_month_return)这段伪代码强调了一个思想模型必须是“滚动重新训练的”而不是训练一次然后直接用在所有未来月份上。财务因子的分布会随时间变化固定模型在一个月后可能已经失效。滚动训练会慢很多但它反映真实使用场景。5.3 数据泄漏排查标准化、标签、删除缺失值三个方向第一个排查点是标准化。整个数据集统一fit_transform会让验证集包含训练集的统计信息这种泄漏很难察觉但影响很大。正确做法只有训练集fit验证集和测试集transform。第二个点是标签构造。shift(-20)之后最后一行的标签是NaN如果用dropna()清除要注意别把该保留的特征一起删掉更隐蔽的是未来收益的计算跨越了停牌期除权除息导致的跳空也会扭曲收益率需要用复权价计算。第三个点是缺失值填充顺序。如果先填充缺失值再做时间切分填充时会用到未来信息比如用未来某期的值前向填充到前面。正确做法是先按时间切分再在训练集内部做填充验证集用训练集学到的填充值去补。5.4 提升质量指数分层回测代替单一TopK单一TopK只能说明模型选的股票不错不能证明模型具备稳定排序能力。更具说服力的做法是把全部股票按预测值从高到低分成5层Q1到Q5每层作为一个等权组合分别计算下月平均收益。如果预测真有效Q1到Q5的收益应该单调递减或递增。df[pred_rank] df.groupby(date)[pred].rank(pctTrue) df[quantile] pd.qcut(df[pred_rank], 5, labels[Q1, Q2, Q3, Q4, Q5]) # 每层月度收益 monthly_layer_return ( df.groupby([date, quantile])[ret_future].mean().unstack() ) # 观察每层累计净值曲线判断是否有单调性这个检验方法的优势在于不需要换模型只把输出从“选20只”改成“分5层”论文里呈现出来的效果直接提升一个量级。如果分层结果完全没有单调性说明模型学到的东西和未来的收益关系不大需要回到因子或标签上找原因。5.5 把“预测收益率”改成“预测排名”更稳的优化目标回归模型直接拟合收益率会被极端值带偏方向。某些股票因为突发利好单月上涨50%MSE会把大量梯度分配给这些样本导致其他股票预测失真。一个常见替代方案是转化成排序问题不预测具体收益率而是预测“这只股票下个月在所有股票中的排名分位”。这个方向有专门对应的损失函数。不展开数学细节的话最简单的做法是把标签从收益率变换成截面排名百分位0到1然后依然用回归头去拟合。这样模型学到的就不是“上涨多少”而是“相对其他股票谁更可能涨”更贴合选股的本质目标。# 把标签从收益率改成截面排名分位 df[ret_future_rank] ( df.groupby(date)[ret_future].rank(pctTrue) )这个改动很小但训练结果的稳定性能明显提升尤其是当你的财务因子数据中包含少数异常暴涨暴跌的股票时。答辩时如果被问到“为什么用排名不用收益率”这是一个很好的讨论切入点收益率的绝对值受市场整体行情影响大而排名是截面相对概念天然剥离了市场Beta。5.6 高分项目的最后一公里写清楚边界拿到任何一套源码之后复现只是开始。建议按这个顺序验证项目完整性先跑通原始代码记录训练loss和回测指标再独立写一个最小验证脚本只保留10个因子和1层LSTM确认核心链路没问题然后逐步把代码恢复到完整状态同时记下每一步的影响。我的习惯是每跑一个版本就记实验表格因子数量、seq_len、hidden_size、层数、验证集MSE、分层单调性、Top20年化、最大回撤。这组数据既是论文的实验章节素材也能帮自己发现哪个环节最敏感。最后再针对复现过程中发现的问题比如未来函数或幸存者偏差特意做一次“修正前后对比”把对比内容写进论文的讨论部分这就是高分项目区别于普通课程设计的核心差距。说到底LSTM选股模型的分数不在模型结构多复杂而在数据流水线是否干净、回测口径是否可信、边界条件是否说清楚。把这几件事做实了哪怕模型本身是两层LSTM加一个线性头也足够撑起一篇优秀毕业设计。如果你能在学习率、seq_len和分层回测这几个细节上多花两天时间反复验证收获会比单纯换一个更大规模的模型多得多。希望帮到你。本文还有配套的精品资源点击获取