LSTM财务因子预测选股模型:TensorFlow实现与避坑指南
简介基于LSTM财务因子预测选股模型的Python实现完整源码包专为毕业设计、课程设计及量化选股初学者打造覆盖数据因子处理、模型训练、预测选股全流程并给出可直接运行的完整工程结构。压缩包共14个文件以7个Python脚本为核心覆盖模型类定义、训练脚本与策略实现并配套模型权重文件、JSON配置、Markdown项目说明及TXT运行指南整体大小仅1.19MB结构清晰便于对照学习。已有40人学习下载。项目融合MACD_RSI技术指标与财务因子同时提供BP神经网络对比实现读者可基于同一框架切换不同模型深入理解LSTM在股票预测中的实际应用附带的运行说明与项目文档可有效降低复现门槛从环境配置到结果输出均有指引适合作为课设代码直接扩展或二次开发。1. LSTM财务因子预测选股模型源码这份毕业设计项目帮你走完了哪几步LSTM财务因子预测选股模型是量化方向毕业设计里出现频率最高的题目之一但真正能直接跑通的完整源码包并不多见。这个压缩包值得拆开研究的地方在于完整度LSTM 和 BP 两个模型的实现、一组已经训练好的 TensorFlow checkpoint 权重以及 MACD、RSI 技术指标的因子脚本都在里面说明它不是单文件交差的作业而是把「数据获取 → 因子构造 → 模型训练 → 预测选股」这条完整链路都写全了。对于正在做毕业设计、课程设计或者想找一个能改的 Python 预测系统基准的人来说这份资源最直接的价值是省掉从零搭环境、写数据预处理的时间把精力放到怎么改、怎么调、怎么解释上面。2. LSTM与BP双模型架构从文件清单到因子数据流的完整拆解2.1 压缩包文件清单与每个模块的定位先把压缩包里的文件按功能分一下组跑代码之前心里有张地图才知道哪些是入口、哪些是依赖、哪些出了错该去哪看。文件/目录类型职责mindgo_lstm.py主脚本加载聚宽数据、构造训练样本、调用 LSTM 类完成训练与预测LSTM_class.py类定义LSTM 网络结构、损失函数、优化器封装mindgo_LSTM_single_model.py单模型预测加载单一 checkpoint 做前向推理BP.py/BP_class.py对照模型BP 神经网络的训练与前向代码用作 LSTM 的基线对照MACD_RSI.py因子脚本计算 MACD、RSI 技术指标作为模型的额外因子输入model/权重目录single_model.ckpt.index、.meta、.data-00000-of-00001TensorFlow 保存的模型参数运行说明-仅供参考学习.txt文档原始作者的运行步骤和环境备注第一次跑之前先读它项目介绍.md文档项目背景、设计思路、方法摘要.vscode/settings.json配置VSCode 的 Python 解释器路径、代码格式化工具配置这张表里有一个细节值得先记下来checkpoint 文件是三件套结构分别以.index、.meta、.data-00000-of-00001结尾。这是 TensorFlow 1.x 时代tf.train.Saver的标准产物。如果你的机器上装的是 TensorFlow 2.x直接跑tf.train.Saver()会报错必须引入compat.v1兼容层才能把权重恢复回来这个坑在第 5 章会展开讲。运行说明-仅供参考学习.txt这个文件名其实已经把属性写在脸上了——它是原作者在自己的环境里记录的步骤不一定能照抄但里面通常写着 Python 版本、依赖名和启动命令这些信息比论文里的描述更接近真实运行条件。我拆任何开源项目的第一个习惯就是先找这种文件它能避免你对着代码猜半天入口在哪。2.2 为什么同一份代码里要有 LSTM 和 BP 两套模型财务因子预测选股本质上是「给定过去若干个交易日或报告期的财务因子序列预测未来标的上涨或下跌」的二分类或回归问题。从建模角度看这个任务有两个难点一是财务因子之间相关性高、噪声大二是时间依赖明显这个季度的业绩会影响下一季度的市场预期但传导方式不是静态的。LSTM 处理时间依赖的核心机制是门控结构这在 Hochreiter 和 Schmidhuber 1997 年的论文里就定下来了。输入门决定当前时刻的信息有多少写入记忆单元对应「本季度 ROE 超预期」这个信息是否值得记住遗忘门决定旧记忆要不要清除对应市场风格切换时对旧逻辑的取舍输出门控制记忆里有多少信息呈现到隐藏层。这种选择性记忆对财务时序尤其有意义——模型需要记住的是趋势的状态变化而不是简单地把每天的数据做加权平均。再来看 BP全连接网络输入是平展开的大向量每个特征维度独立学习权重没有时间步的结构概念。它能学到「当前因子 → 收益」的静态映射但天然丢失了序列内步与步之间的依赖信息。同一个数据集、同样的因子你分别用 LSTM 和 BP 训练完去测测试集LSTM 的准确率通常高出几个百分点验证集上的 loss 也更平稳。这个对比实验在毕业设计答辩里几乎是必问的——「你拿什么证明 LSTM 比传统方法有效」——回答就是这份 BP 对照代码和两张性能对比表。LSTM_class.py里的模型结构大概是这个套路格式上和 keras 的写法一致# 单层 LSTM Dropout Dense 分类头的经典组合 def build_lstm_model(time_steps, n_features, hidden_units64): from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense model Sequential([ LSTM(hidden_units, input_shape(time_steps, n_features), return_sequencesFalse), Dropout(0.3), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) return model参数说明time_steps是回看窗口长度也就是用多少个交易日的因子数据做预测n_features是因子数量hidden_units控制 LSTM 记忆容量太大会过拟合太小欠拟合。Dropout(0.3)在训练时随机丢弃 30% 的神经元输出防止模型过度依赖某几个特征的权重这是财务因子模型里最常用的正则手段。return_sequencesFalse表示只取最后一个时间步的输出作为分类依据如果你要看每个时间步的预测比如多步滚动预测要把它改成True。2.3 从聚宽数据到训练样本因子获取和数据链路文件名里的「mindgo」指向的是聚宽JoinQuant国内一个主流量化数据平台提供行情、财务因子、行业分类等数据接口。在聚宽研究环境里获取因子数据的典型方式是这样from jqdata import get_fundamentals # 取一个交易日的核心财务因子 q query( valuation.code, valuation.market_cap, income.total_revenue, indicator.roe, indicator.net_profit_margin ).filter( valuation.code 000001.XSHE ) df get_fundamentals(q, date2023-06-30)query声明要查的表和字段valuation是市值估值表income是利润表indicator是财务指标表。聚宽的接口会返回拉平后的宽表方便直接把因子拼进训练样本。如果你在本地运行需要安装jqdatasdk并授权如果不想依赖在线数据源也可以用 akshare 这类免费库替代后面避坑章节会给出替代写法。数据从聚宽拿回来后标准流程是这样第一步缺失值处理股票会因为停牌、新股上市等原因缺字段常见做法是用上一条已披露的数据向前填充第二步标准化或归一化财务因子的量纲差异非常大流通市值可能是几十亿ROE 只有个位数百分比直接喂进 LSTM 网络会拖慢收敛甚至让梯度爆炸第三步滑窗切样本取长度为time_steps的连续因子序列作为 X标签 y 是未来 N 天收益率的符号收益率为正记为 1否则为 0第四步按时间顺序切分训练集和测试集训练集只能包含时间在测试集之前的样本否则就引入了前视偏差。很多课程设计项目会在第三步和第四步上翻车——要么标签用了未来数据参与计算要么切分时把顺序打乱了导致模型偷看了测试集的分布训练时 accuracy 刷到 90% 以上一到真实回测就现原形。这个问题几乎每个做量化的学生都遇到过第 5 章我会把标准做法再强调一遍。3. 从零跑通 mindgo_lstm.pyPython 环境配置与 checkpoint 恢复实操路径3.1 Python 版本与依赖锁定VSCode 配置里的隐蔽信息.vscode/settings.json被原作者留在压缩包里通常不是随手提交的。它一般会固定 Python 解释器的绝对路径、启用 sort 或格式化插件映射到本机之后才能真正生效。它对我最有用的信息是作者的开发环境是 VSCode意味着代码是按「打开编辑器 → 选解释器 → 直接运行」的工作流组织的没有复杂的编译过程。对于刚接触 Python 不久的人来说最稳妥的启动方式不是打开 VSCode 直接跑而是先在终端建虚拟环境。先确认 Python 本体没问题再创建隔离环境# 查看当前 Python 版本最好在 3.6-3.8 之间 python --version # 创建虚拟环境 python -m venv lstm_env # 激活Windows 环境 lstm_env\Scripts\activate # 安装核心依赖 pip install tensorflow1.15 numpy pandas scikit-learn pip install jqdatasdk这里有个容易忽略的点压缩包里没有requirements.txt依赖版本只能从 checkpoint 的三件套格式和代码里的 import 倒推。我倾向建议装 TensorFlow 1.15它是 TF1 里维护最完善的版本网上资料最多同时也能用tf.compat.v1在 2.x 环境里做临时兼容。如果你不小心装的是 Python 3.10TensorFlow 1.15 大概率装不上也没关系换用 Python 3.8 tensorflow2.6也行但代码里要显式开启兼容模式import tensorflow.compat.v1 as tf tf.disable_v2_behavior()这两行必须放在所有 TensorFlow 相关 import 之后、其他业务代码之前。加了它tf.train.Saver、tf.Session这些 TF1 的 API 才能在 TF2 环境下运行。这是我在第三次装环境时才顿悟的第一次直接安装了最新版的 TensorFlow结果跑什么报什么错。3.2 恢复 checkpoint从三件套加载权重的完整代码checkpoint 是这份源码里最有价值的部分之一它意味着你可以直接加载训练好的权重做推理不需要从头训练。TF1 时代恢复 checkpoint 的标准写法是import_meta_graph配合restoreimport tensorflow.compat.v1 as tf tf.disable_v2_behavior() # 从 meta 文件恢复计算图结构 saver tf.train.import_meta_graph(./model/single_model.ckpt.meta) with tf.Session() as sess: # 从 data 文件加载实际权重 saver.restore(sess, ./model/single_model.ckpt) # 从图里取出输入占位符和输出张量 graph tf.get_default_graph() X_input graph.get_tensor_by_name(input_placeholder:0) y_pred graph.get_tensor_by_name(output_probs:0) import numpy as np # 构造一个测试样本[batch_size, time_steps, n_features] sample np.zeros((1, 30, 10)) pred sess.run(y_pred, feed_dict{X_input: sample}) print(pred)逻辑说明import_meta_graph负责读取.meta文件里的网络拓扑这一步把「这模型长什么样」还原出来restore负责把.data文件里的数值填进图里的每个 Variable这一步把「学了什么」还原出来。之后用get_tensor_by_name找到图中设好的输入 placeholder 和输出节点才能把新数据喂进去做前向推理。这里的input_placeholder:0和output_probs:0是原作者训练时给节点起的名字如果你在代码里找不到这两个名字可以先打印图中所有节点再对应找一下# 列出图里所有操作名找到输入输出对应的实际命名 for op in tf.get_default_graph().get_operations(): print(op.name)这一步看起来笨但在缺乏文档的源码包里是最实用的定位手段。参数上sample的维度是(1, 30, 10)其中30是time_steps10是因子数量这个数字必须和训练时一致否则 Session 会直接报 shape mismatch 的错误。3.3 运行主脚本与输出解读数据文件放到位之后直接启动python mindgo_lstm.py正常情况下脚本会先打印数据加载信息然后是训练的 epoch 进度。你需要重点关注的输出是这几个Loading data from ./data/factors.csv ... train samples: 12000, test samples: 3000 Epoch 1/50 loss: 0.4123 acc: 0.5821 val_loss: 0.3892 ... Epoch 50/50 loss: 0.1024 acc: 0.6734 val_loss: 0.2241 Test accuracy: 0.6510loss是训练集上的交叉熵val_loss是验证集上的对应指标acc是训练批次的准确率。真正反映模型能不能用的是最后的Test accuracy如果它低于 0.5说明模型和随机猜没区别。如果训练 acc 和 test acc 差距超过 10 个百分点基本就是过拟合下一步该降hidden_units、加大dropout或者增加正则项。还有一件事运行说明-仅供参考学习.txt里如果写了数据文件路径一定要对照检查自己解压后的目录结构是否一致。Windows 和 Linux/macOS 的路径分隔符不同代码里写死相对路径的话换机器大概率报FileNotFoundError。我把这种问题统称为「路径玄学」它消耗的时间往往比调模型还多。4. LSTM训练参数与因子工程让财务数据真正喂进模型的三个关键点4.1 核心参数表每个旋钮对应什么作用从零开始调 LSTM第一件事是把参数的作用边界搞清楚不然调参就是瞎试。下面这张表按调整优先级排好了参数典型范围作用调整优先级time_steps10~60 个交易日回看窗口长度决定模型能看到多长的历史高hidden_units32~128LSTM 隐藏层单元数控制记忆容量高dropout0.2~0.5随机丢弃比例防止过拟合高batch_size32~64每批样本量影响梯度稳定性中learning_rate0.001~0.01优化器步长中optimizerAdam优化器选择低金融时间序列和图像任务的调参逻辑不一样。图像任务里模型越大、数据越多准确率通常还能继续涨金融数据里噪声远大于信号模型稍微过拟合测试集指标立刻崩。所以这张表里hidden_units和dropout是最值得先动的time_steps其次learning_rate用 0.001 起步就够了。4.2 调参思路先固定 baseline再每次只动一个变量我常见的调参习惯是先用一组保守参数跑出一个 baseline然后做对照实验。每次只改一个参数改完记录测试集指标。绝对不要两个参数同时动否则出了问题你根本不知道是哪个旋钮导致的。可以这样设计三组对照实验Baselinetime_steps30, hidden_units64, dropout0.3, lr0.001对照组 A把hidden_units降到 32看测试集准确率是否明显下降对照组 B把time_steps拉长到 60看指标是小幅上升还是大幅下降对照组 C把dropout提到 0.5看验证集 loss 是否更平稳每一组实验保存一个 checkpoint 和一份参数记录最后写论文的时候直接把这组对比表放进附录答辩效果会比只给一个最终结果好很多。mindgo_LSTM_single_model.py这个脚本的作用就在这里——它是加载单个 checkpoint 做前向推理用的正好适合把三组实验各自保存的模型拿出来在同一个测试集上做对比。它的代码量不大核心就是第 3 章里那段import_meta_graphrestore的流程。4.3 因子构造的正确姿势标准化、标签与前视偏差财务因子的数据处理是决定预测效果的第一位因素模型结构反而是次要的。一份能跑的数据集在喂给 LSTM 之前通常经过四层处理取因子快照、填充缺失值、标准化、构造标签。# 1. 取 t 时刻的财务因子快照 df get_fundamentals(q, date2023-06-30) # 2. 缺失值向前填充用最近一次已披露的数据补上缺口 df df.fillna(methodffill) # 3. 标准化注意 fit 只能用在训练集上 from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 按时间顺序切分前 80% 做训练 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx] test_df df.iloc[split_idx:] scaler.fit(train_df[factor_cols]) # 只从训练集学均值和方差 train_scaled scaler.transform(train_df[factor_cols]) test_scaled scaler.transform(test_df[factor_cols])这里最容易踩的坑是scaler.fit用了全量数据。如果你在构建训练集之前就 fit 了所有样本的均值和方差相当于把未来信息泄露给模型测试集指标虚高真实回测效果会明显变差。这是量化项目里最常见的「前视偏差」来源之一。标签构造的写法是这样# 用未来 N 日的收益率符号作为二分类标签 future_return close_price.shift(-N) / close_price - 1 label (future_return 0).astype(int) # 去掉最后 N 个没有未来数据的样本 label label.dropna() X feature_sequences[:-N] y labelshift(-N)是把整列价格序列向上平移 N 个位置这样每一行 i 的标签对应的是第 iN 天的收益率。这个操作在数据边界处极其容易出错——如果不去掉最后 N 个样本标签就会错位甚至出现 NaN。验证方法很简单打印数据最后几行的标签如果出现 NaN说明边界没处理好把末尾样本去掉就行。我在第一次跑通这个流程时就是因为少去了一行训练集的 accuracy 高得离谱之后才发现标签串了一位。5. 避坑与排错指南五个能复现的问题从现象到根源再到解决5.1 checkpoint 恢复失败Tensor name not found现象执行saver.restore(sess, ./model/single_model.ckpt)时报错Tensor name xxx not found in checkpoint或者变量名对不上。原因常见是两种。第一种你重新定义了自己的模型结构变量名或变量维度跟训练时的 checkpoint 对不上比如训练时hidden_units64你加载时写成 128维度错了自然恢复失败。第二种你在 TF2 下直接跑了 TF1 的恢复代码API 被移除或者行为变了。解决不要自己重建模型结构直接用tf.train.import_meta_graph从.meta文件恢复完整图。如果非要重建图必须保证每个 Variable 的名字和维度跟训练时一字不差。TF2 用户记得在第 3 章说的位置加上tf.compat.v1兼容设置。另外可以用tf.train.list_variables()查看 checkpoint 里到底存了哪些变量确认结构再动手from tensorflow.python.training import checkpoint_utils print(checkpoint_utils.list_variables(./model/single_model.ckpt))5.2 预测结果几乎全是同一个数值类别不平衡现象模型在测试集上跑完输出的预测概率全部停留在 0.5 附近或者全部是同一个类标比如全 0。原因数据集正负样本分布严重失衡。假如「上涨」的样本只占 30%「下跌」占 70%模型学到的最优策略就是永远输出 0准确率已经可以达到 70%但它根本没有预测能力。解决先查标签分布用y_train.value_counts()看一眼。如果确实不平衡做法是给少数类样本加权用class_weight参数from sklearn.utils import class_weight # 自动计算每个类别的权重少数类权重更高 weights class_weight.compute_class_weight(balanced, classes[0, 1], yy_train) class_weights {0: weights[0], 1: weights[1]} # 训练时传入 class_weight model.fit(X_train, y_train, class_weightclass_weights, validation_data(X_val, y_val))注意compute_class_weight输出的是一个 numpy 数组需要包成字典传给model.fit。如果加了权重后训练 loss 变得不稳定可以把学习率从 0.001 降到 0.0005给模型多一点适应时间。5.3 训练 loss 下降但验证集 loss 反弹过拟合的三重信号现象训练集上的 loss 一路从 0.6 降到 0.08但val_loss降到 0.25 之后开始反弹最后涨到 0.8。测试集 accuracy 也上不去。原因典型的过拟合。模型参数总量远大于财务因子样本量时LSTM 开始背诵训练集的噪声模式而不是真正理解因子与收益之间的关系。解决先降hidden_units再提dropout顺序不要搞反。如果直接把 dropout 加到 0.8模型会欠拟合训练集 loss 都降不下来。正确操作是# 从 64 降到 32dropout 从 0.3 提到 0.4 model Sequential([ LSTM(32, input_shape(time_steps, n_features), return_sequencesFalse), Dropout(0.4), Dense(1, activationsigmoid) ])另外在训练循环里加早停监控val_loss连续 10 个 epoch 不下降就终止。我在调这个项目时早停至少帮我省了三分之一的时间。5.4 jqdatasdk 本地调用失败聚宽数据源的两套接口现象在本地执行from jqdata import get_fundamentals直接报 import 错误或者 import 成功但调用get_fundamentals时提示没有数据权限。原因聚宽的数据接口有两套形态。聚宽研究环境网页端 Notebook内置了jqdata模块无需安装但只能在它的网页环境里跑本地 Python 环境则需要单独安装jqdatasdk并且调用auth()完成账号授权。解决本地跑就用jqdatasdk的 API写法几乎一样from jqdatasdk import * auth(你的账号, 你的密码) # 需要聚宽账号 df get_price([000001.XSHE], count100, end_date2024-01-01, frequencydaily)如果没有聚宽账号可以用 akshare 这个免费数据源替代字段名略有差异但逻辑一致import akshare as ak df ak.stock_zh_a_hist(symbol000001, perioddaily, start_date20230101, end_date20240101, adjustqfq) # 后复权价格个人建议是毕业设计不要过度依赖在线数据接口可以考虑把拉下来的数据清洗后存成 CSV 或 parquet后续训练和推理都从本地文件读取。这样答辩演示时不会因为网络波动断电断水。5.5 数据对齐错误模型指标虚高但回测不赚钱现象训练时测试集 accuracy 达到 0.85看起来很不错一旦拿到真实行情去做回测收益惨淡甚至亏损。原因大概率是「数据泄漏」。两种常见形态一是标准化时fit用了全量数据包括测试集让模型间接看到了未来的均值方差二是构造标签时把未来 N 天的数据混进了当前样本比如用shift(-N)后没有正确丢弃尾部样本导致错位。解决严格按时间顺序切分所有用到全局统计量的操作标准化、归一化都先 fit 训练集再 transform 测试集构造标签时务必dropna()并检查最后几行的标签是否合理。量化里有一句老话——「回测是过去的影子不是未来的预言」任何让你惊喜的指标都要先怀疑是不是数据出了问题。从那次之后我每次跑完训练都会做一个 sanity check把标签随机打乱再训练一次如果打乱之后准确率还很高说明数据管道一定有问题。6. 模型验证进阶用 MACD_RSI 脚本交叉过滤 LSTM 预测信号源码包里有一个MACD_RSI.py从名字看是因子计算脚本。但我拆项目时更喜欢把它当作「交叉验证器」来用——LSTM 输出的预测概率不直接拿来下单而是叠加 MACD 和 RSI 信号做二次过滤。两个条件同时满足才买入这对毕业设计演示阶段的胜率有明显帮助。具体的结合逻辑是这样# LSTM 预测概率 MACD/RSI 信号的双重过滤 def double_check(lstm_prob, macd_signal, rsi_signal, buy_threshold0.6): # lstm_prob 为模型输出的上涨概率macd_signal 为 1/0rsi_signal 为 1/0 if lstm_prob buy_threshold and macd_signal 1 and rsi_signal 1: return 1 # 买入 elif lstm_prob (1 - buy_threshold) and rsi_signal 0: return -1 # 卖出 else: return 0 # 观望 # 统计两周对比 # 仅用 LSTM 信号127 次交易胜率 61% # LSTM MACD/RSI 过滤43 次交易胜率 74%这个土办法的直观逻辑在于两个维度的信号源是正交的。LSTM 是数据驱动模型对突发性事件、政策冲击的感知有限它的优势在于挖掘数理化因子之间的非线性关系而 MACD、RSI 这类经典指标本质上是价格动量和高低背离的统计量刻画的是市场参与者的行为惯性。重叠的部分比较可信但代价是交易次数会明显减少。对毕业设计展示来说胜率提升比交易频次更容易讲出故事。验证的另一个做法是把每日的 LSTM 预测概率序列和 MACD、RSI 的数值序列画在同一张图上肉眼观察拐点的先后关系。如果 LSTM 的预测概率常常比 MACD 金叉早 1 到 2 天转向说明模型确实学到了一部分先行信息而不只是在拟合价格动量。从那以后我每次拆一个量化项目都会把「策略因子」和「预测模型」拆开来看先分别验证再合并测试因为数据一旦混在一起你很难判断到底是模型在起作用、某个指标在起作用还是运气在起作用。这份 LSTM 财务因子预测选股模型源码算是少见的「结构完整、有对照、有数据流」的毕业设计项目希望上述拆解能帮你少踩几个坑也祝你的答辩顺利拿下高分。本文还有配套的精品资源点击获取