简介:基于LSTM的财务因子预测选股模型Python源码,是一份答辩评审达98分的高分毕业设计项目,适合计算机、人工智能、量化金融等相关专业学生及从业者用于毕设、课程设计或进阶学习。压缩包共15个文件,整体约1.19MB;其中7个Python脚本构成项目主体,覆盖数据预处理、LSTM/BP模型训练、MACD_RSI指标计算与选股预测流程,并附带训练完成的模型检查点文件(.meta、.index、.data-00000-of-00001)可直接加载推理,另有Git相关配置与JSON设置便于环境复现。代码已经过调试可稳定运行,既能帮助入门者快速理解时序预测选股的完整实现,也为基础较好的读者提供了清晰的扩展接口。目前已有184人学习下载,整体结构清晰,可作财务因子量化选股从研究到落地的参考样例。
1. LSTM财务因子预测选股模型:一份能跑通的量化毕设,而不是一个“选股黑匣子”
拿到这份“基于LSTM的财务因子预测选股模型”源码时,我的第一反应是它和我平时见到的大多数课程设计不一样:压缩包里除了 LSTM 模型,还有一份 BP 神经网络做对照,模型 checkpoint 权重文件也已经训练好,连 MACD、RSI 技术指标代码都配齐了。也就是说,你不是拿到一个只能“看着跑”的 demo,而是一整套从数据到模型再到选股信号的闭环流程。这个项目适合两类人:一类是拿它做毕业设计或课程大作业,需要快速出一个有 LSTM 亮点、有对比实验、能演示的完整系统;另一类是想入门量化选股但不知道从哪下手的 Python 开发者——你能从里面看到财务因子怎么组织、LSTM 训练脚本怎么写、训练好的模型怎么拿去做预测选股。但先说清楚:这个项目的价值在流程完整和工程骨架,不在“预测百分百赚钱”。理解这一点,你后面踩的坑会少一半。
2. 为什么用 LSTM 做财务因子预测:时序结构、门控机制与适用边界
2.1 LSTM 与 BP 在因子预测上的本质区别
这份源码里同时给出了LSTM_class.py和BP_class.py,这个对照设计本身就很有教学价值。传统 BP 神经网络做因子预测,本质上是在做一张静态映射表:输入一批财务指标,输出一个预测值,样本之间彼此独立。它没有“记忆”能力,上一个季度的财报好不好、股价对财报的滞后反应规律是什么,这些信息在 BP 里是丢失的。
LSTM 不同,它在网络结构里加了三个门——遗忘门、输入门、输出门,让信息可以沿着时间步有选择地保留和丢弃。放到财务因子预测的场景里:一家公司连续三个季度的 ROE 下滑,这个“趋势”本身就是强信号,而单看某一个季度的 ROE 绝对值看不出问题。LSTM 能把过去若干期的因子数据作为一个序列输入,捕捉这种跨期的趋势和依赖。
那为什么不是所有选股模型都用 LSTM?因为它的训练成本更高、对数据量和数据质量更敏感。BP 可能用几百条样本就能拟合个大概,LSTM 需要对每个样本构造一个“时间窗口”,样本量直接缩水一个数量级。这也是我在拿到这份代码时最关心的问题——它到底在什么频率的数据上训练。
2.2 财务因子怎么选、怎么构造训练样本
财务因子预测选股,第一步不是调参,是把“预测目标”定义清楚。常见的做法分两种:一种是预测未来一段时间的收益率,另一种是预测某个关键财务因子(比如未来一期的 ROE 或净利润增长率)的取值。前者的好处是直接跟选股目标对齐,后者的可解释性更强。
因子维度一般会从这几类里挑:估值类(PE、PB、PS)、盈利类(ROE、毛利率、净利率)、成长类(营收同比增长率、净利润同比增长率)、运营类(资产负债率、存货周转率、现金流比率)。你的模型里有多少个因子,input_dim就得设多大。我在拆这份代码时看到它把 LSTM 模型类拆成了独立文件,输入参数支持自定义,说明作者当时是把因子选择做成配置项的——这对毕设答辩很实用,你可以直接告诉评委“因子维度可配置,换一组因子就能扩展实验”。
训练样本的构造方式是这类项目最容易翻车的地方。假设你有 5 年的季度财务数据,每个季度末算一批因子值,那么一个样本就是“连续 T 期的因子序列 → 下一期的收益标签”。如果 T 取 4,意味着用过去 4 个季度(一年)的财务因子,预测下个季度初买入并持有 N 天的收益。这里有个财务数据特有的坑:财报数据是滞后披露的,Q1 的财报往往 4 月底才公布,你在 4 月初用 Q1 数据做训练,就是提前拿到了未来数据——量化里管这个叫“未来函数”。后面避坑章节会专门展开。
2.3 模型结构与关键参数设计
从single_model.ckpt这个权重文件和后缀ckpt.index、ckpt.meta能判断出,这份项目的 LSTM 模型是用 TensorFlow 训练的,单模型结构。一个标准的 LSTM 选股模型长这样:
输入层维度:[batch_size, time_step, input_dim],其中time_step是回溯的历史期数,input_dim是因子个数。中间是 1~2 层 LSTM 或堆叠 LSTM 单元,后面接一个 Dropout 层防过拟合,再经过全连接层压缩维度,最后输出一个标量预测值。损失函数用均方误差(MSE),优化器一般选 Adam 或 RMSProp,学习率从 0.001 起步。
单层 LSTM 和双层 LSTM 在这个场景的区别要取舍一下:财务数据本身是低频的(季度或月度),信息量有限,单层 LSTM 往往就够用;双层 LSTM 虽然拟合能力更强,但在小样本数据上更容易过拟合。对于毕业设计来说,不妨做一组对比实验,用同一份数据分别跑单层和双层 LSTM,把 loss 曲线和回测结果放进论文附录,这个对比比调参本身要有说服力得多。
3. 源码包拆解:六个 py 文件,各自管哪一段流程
3.1 文件结构与模块职责
打开压缩包,去掉.DS_Store、.vscode之类的环境和系统文件,核心代码是这几个:
| 文件 | 职责 | 运行环境 |
|---|---|---|
mindgo_lstm.py | 掘金量化平台上的 LSTM 策略主流程,数据获取→训练→预测→选股信号输出 | MindGo 策略运行环境 |
mindgo_LSTM_single_model.py | 单模型版本的 LSTM 选股脚本,逻辑更精简 | MindGo 策略运行环境 |
mindgo_bp.py | BP 神经网络选股脚本,用于与 LSTM 做对照实验 | MindGo 策略运行环境 |
LSTM_class.py | LSTM 模型类定义,独立封装便于复用 | 任意 Python 环境 |
BP_class.py/BP.py | BP 模型类定义与训练脚本 | 任意 Python 环境 |
MACD_RSI.py | 基于 MACD、RSI 技术指标的选股脚本 | 任意 Python 环境 |
这个文件结构的设计思路很清楚:mindgo_前缀的文件是直接跑在量化平台上的,_class.py是模型层封装,MACD_RSI.py是传统技术指标的对照组。你下载到的是“三层分离”的工程结构,不是一坨揉在一起的脚本。这对复现和二次开发都是友好的——你可以不动策略文件,只改模型类,就能换一种网络结构重新跑。
3.2 mindgo_lstm.py 的主流程逻辑
mindgo_lstm.py是最值得先读的文件。它的整体流程可以概括成五个阶段:
阶段一,用平台的财务数据接口获取股票池的因子数据;阶段二,做数据清洗和归一化;阶段三,构造滑动窗口训练集;阶段四,初始化LSTM_class.py里的模型并训练;阶段五,把训练好的模型对新一期因子数据做预测,按预测值排序生成选股列表。
# mindgo_lstm.py 核心流程示意 from LSTM_class import LSTMModel import numpy as np # 1. 获取财务因子数据(以ROE和营收增长率为例) # MindGo平台中get_fundamentals是常用的财务数据接口 fundamentals = get_fundamentals( table='financial_indicator', columns='roe, yoyprofits, or_yoy', date='2020-03-31' ) # 2. 因子预处理:填充缺失值、归一化 data = fundamentals.fillna(0.0).values from sklearn.preprocessing import StandardScaler scaler = StandardScaler() data_scaled = scaler.fit_transform(data) # 3. 构造滑动窗口样本:前time_step期预测下一期 time_step = 4 # 用过去4期预测下一期 X, y = [], [] for i in range(len(data_scaled) - time_step): X.append(data_scaled[i : i + time_step]) y.append(data_scaled[i + time_step, 0]) # 预测目标取第一个因子 X = np.array(X).reshape(-1, time_step, data_scaled.shape[1]) y = np.array(y)这段是这类项目通用的数据管线写法。get_fundamentals拿的是横截面数据——某个时点全部股票在某一个报告期的财务指标;fillna处理的是部分股票缺失因子值的情况,量化里通常不会直接删行,因为不同股票的报表齐全程度不一样,删了会破坏面板数据的完整性;StandardScaler把不同量纲的因子(PE 可能到几百,ROE 只有个位数)压缩到同一尺度。
有个细节值得留意:训练样本构造时我们用了reshape(-1, time_step, data_scaled.shape[1]),这里time_step放在第二维,对应 LSTM 输入要求的形状[batch, time, feature],顺序别搞反,这是新手最常见的维度报错来源。
3.3 LSTM_class.py 的类封装
LSTM_class.py把模型定义、训练、预测封装成了一个类,这是典型的工程化写法。好处是mindgo_lstm.py和mindgo_LSTM_single_model.py可以共用同一个模型类,只用初始化参数做区分。
# LSTM_class.py 核心结构 import tensorflow as tf class LSTMModel: def __init__(self, input_dim, time_step, lstm_units=64, num_layers=1, learning_rate=0.001): self.input_dim = input_dim self.time_step = time_step self.lstm_units = lstm_units self.num_layers = num_layers self.learning_rate = learning_rate self._build_model() def _build_model(self): self.model = tf.keras.Sequential([ tf.keras.layers.LSTM( self.lstm_units, return_sequences=(self.num_layers > 1), input_shape=(self.time_step, self.input_dim) ), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(1) ]) self.model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=self.learning_rate), loss='mse' ) def train(self, X_train, y_train, epochs=50, batch_size=32): # 返回训练过程中的loss历史,便于画图 history = self.model.fit( X_train, y_train, epochs=epochs, batch_size=batch_size, validation_split=0.2, verbose=0 ) return historylstm_units=64是 LSTM 层的隐藏状态维度,不是因子维度,这一点要跟input_dim区分。Dropout(0.2)是训练时随机丢弃 20% 的神经元连接,防止模型死记训练样本。validation_split=0.2表示从训练集尾部切 20% 做验证,用来观察是否过拟合——如果训练 loss 持续下降但验证 loss 开始回升,就是过拟合的信号。
对照组的BP_class.py结构类似,只是把 LSTM 层换成了Dense层。拿 BP 做对照的核心意义在于回答一个问题:把同样的因子数据喂给同样规模的普通神经网络,效果比 LSTM 差多少?如果答辩时评委问“为什么不用随机森林、不用 XGBoost”,这就是你的对照组论据。
4. 从配置环境到跑通训练:一份可复现的完整路径
4.1 环境准备:TensorFlow 版本、Python 环境和导入顺序
项目用到的是 TensorFlow 框架,模型权重文件是ckpt后缀。这里要特别留个心眼:ckpt.index和ckpt.meta是 TensorFlow 1.x 中 Saver 保存的格式风格,如果你是刚装了 TensorFlow 2.x 就跑这份代码,很可能在恢复模型时报错或行为不一致。我建议先确认你本地的 TF 版本:
python -c "import tensorflow as tf; print(tf.__version__)"如果是 2.x 环境,代码里tf.Session()这类写法会直接报错。常见做法是:优先在 1.x 兼容模式下跑通,或者用tf.compat.v1替换旧 API。把tf.Session()全局替换成tf.compat.v1.Session()这类改动,在文件不多的情况下十分钟能搞定。
mindgo_前缀的脚本依赖掘金量化终端的环境,本地直接跑会卡在平台接口调用上。我的建议是分两步:先在本地把LSTM_class.py和BP_class.py用随机的因子数据跑通,验证模型层没问题,再接 MindGo 环境跑完整流程。不要一上来就指望平台环境一步到位。
4.2 数据预处理考核点:对齐、去极值与中性化
比训练更重要的是数据预处理的三板斧:去极值、标准化、中性化。去极值常用分位数缩尾(MAD 法),把偏离均值过多倍标准差的数据拉到边界,防止个别极端值主导 loss;标准化就是把不同量纲压到均值 0、方差 1;中性化一般用的是一元线性回归剔除市值、行业等对因子的干扰。这份代码里在预处理部分做了标准化,你要做二次开发时,可以按下面的逻辑补上去极值和中值化:
# 因子预处理:MAD去极值 + 标准化 from scipy.stats import median_abs_deviation def mad_winsorize(factor, n=5): """MAD(绝对中位差)去极值,n是缩尾倍数""" median = np.median(factor) mad = median_abs_deviation(factor) upper = median + n * mad lower = median - n * mad return np.clip(factor, lower, upper)n取 5 是行业常见配置,表示把超出中位数 5 倍绝对中位差的样本拉回边界。这个函数可以在训练前对每一个因子列调用一遍。注意先做去极值再做标准化,顺序反了的话,极值会被标准化放大,反而更影响训练。
4.3 训练与模型恢复:加载已有权重还是从头训练
项目里已经带了训练好的权重文件single_model.ckpt,你可以选择直接加载它做预测,也可以重新训练。如果是毕设演示,我建议两条路都走:先加载现有权重跑通演示流程;然后清空权重重新训练,把训练曲线放进论文。加载权重的方式参考下面的写法:
from LSTM_class import LSTMModel # 定义与训练时一致的模型结构 model = LSTMModel( input_dim=8, # 因子个数,按你的数据维度调整 time_step=4, # 与训练时保持一致 lstm_units=64, num_layers=1 ) # 加载已有checkpoint权重 model.model.load_weights('single_model.ckpt') # 用最新一期因子数据做预测 latest_factors = scaler.transform(new_fundamentals) # 同一套scaler,别重新fit X_pred = latest_factors.reshape(1, 4, 8) predicted = model.model.predict(X_pred)注意load_weights时要求你构建的模型结构跟原模型结构完全一致,差别在input_dim或time_step上都会报形状不匹配。另外,如果加载时报unexpected key in checkpoint之类的错,大概率是保存时带上优化器状态,而你加载时没有重建优化器。这种小问题常见但容易卡人,可以直接把报错信息复制到搜索引擎找同款。
4.4 数据流检查清单
在本地运行这份模型前,建议先按下面这份清单过一遍数据流,很多问题在这步就能排查掉:
| 数据流节点 | 预期输出形状 | 常见问题 |
|---|---|---|
get_fundamentals返回 | [股票数, 因子数] | 列名与表格字段不一致 |
| 预处理后数据 | 无缺失值、无量纲差异 | 极值未处理导致 loss 异常 |
| 滑动窗口构造 | X: [样本数, time_step, 因子数] | 维度顺序写成[样本数, 因子数, time_step] |
| 模型输入层 | input_shape=(time_step, 因子数) | 与 X 的第二、三维不对齐 |
| 预测输出 | [样本数, 1] | 输出维度大于 1,检查最后一个 Dense 层 |
5. 避坑指南:训练 LSTM 选股模型最常见的五个坑
5.1 checkpoint 文件恢复失败:TensorFlow 版本不兼容
现象:按教程load_weights('single_model.ckpt')恢复权重,报Failed to load the checkpoint或AttributeError: 'CheckpointReader' has no attribute 'get_tensor'。
原因:项目的模型权重是用特定版本的 TensorFlow 保存的,ckpt.meta和ckpt.index的组合形态是 TensorFlow 1.x Saver 风格。本地环境如果是最新的 TensorFlow 2.x,API 变了,恢复机制不兼容。
解决:先pip list | grep tensorflow看版本;如果是 2.x,尝试在代码开头加一行tf.compat.v1.disable_eager_execution(),并把tf.Session换成tf.compat.v1.Session;如果你确实需要用 2.x 的load_weights,那就要重新训练一次,用新格式保存权重。不要强行混用保存格式,这是浪费一个小时的最快方式。
5.2 财务数据滞后导致的“未来函数”
现象:训练时 loss 很低,回测时选出的股票收益也很可观,但拿到实盘模拟盘跑完全不是那么回事。
原因:典型的未来函数问题。财务因子数据本身有披露滞后:A 股上市公司一季报要在 4 月 30 日前披露,你在 4 月初用一季度因子数据,就是在用未来信息做当下决策。训练集里如果直接把当期因子对齐当期收益,模型学到的“规律”里就混入了未来信息。
解决:构造训练样本时,把因子数据统一往后推一期对齐。假设你有 2019Q1 的因子数据,对应收益标签用 2019Q2 之后的收益,中间至少留一个完整季度的披露缓冲期。我在做这类项目时习惯在因子表里加一列announce_date而不是只用报告期,用来做精确的披露日期对齐。这份源码里没有专门处理披露日期,你自己二次开发时要补上。
5.3 归一化泄漏:scaler 不能在全样本上 fit
现象:训练 loss 正常,验证 loss 也正常,但预测阶段结果一塌糊涂,预测值全部被压在一个很小的范围内。
原因:归一化的 fit 过程用到了全部数据(包括未来数据)的均值和方差。这等于把未来的信息泄露给了模型。特别是在按时间序列构造训练集的情况下,用全样本的均值和方差做归一化,在用前半段数据训练、后半段数据测试时,测试数据的信息已经提前进入了参数。
解决:用训练集的统计量来做归一化,代码上的体现是关键是“先分割,再 fit,再 transform”,或者至少是在时间序列场景下,用滚动的方式计算归一化参数。修改也不复杂:
# 先按时间顺序分割 split_idx = int(len(data) * 0.8) train_data = data[:split_idx] test_data = data[split_idx:] # 只用训练集fit scaler scaler = StandardScaler() scaler.fit(train_data) # train和test都transform train_scaled = scaler.transform(train_data) test_scaled = scaler.transform(test_data)5.4 样本量太小导致过拟合
现象:训练集 loss 一路降到接近 0,验证集 loss 从某个 epoch 开始反弹,模型对训练集记忆得好,对没见过的数据没泛化能力。
原因:财务因子数据是季频或月频的,一年最多 4 条季度数据、12 条月度数据,股票池假设 300 只股票,一年下来也才千把条样本,再乘以历史年份,总量不大。LSTM 又是参数密集型模型,一个小 LSTM 层就有几千个参数,样本量远小于参数量的情况非常普遍。
解决:可以从三个方向解决。扩充样本:把股票池放大到 800 只以上,或者把因子数据从季频降成月频能摊薄很多;正则化:把Dropout从默认 0.2 提到 0.4 或 0.5;另外就是早期停止——监控验证 loss,连续多个 epoch 没有变好就提前终止训练。
5.5 MindGo 平台和本地环境跑出来结果不一致
现象:同一份代码在本地能跑通,放到 MindGo 策略运行环境上报错,或者结果对不上。
原因:MindGo 策略运行在它自己的沙箱环境里,Python 版本、第三方库版本、数据接口返回格式都可能与你本地环境有差异。最典型的是get_fundamentals返回的列名和你本地模拟数据里用的不一样。
解决:拿到代码后,先在本地用假数据把模型层跑通。再把假的因子数据替换成 MindGo 返回的数据,逐段打印输出形状和列名,跟模型输入的期望做对比。不要一上来就整体跑,分阶段验证数据流,最后才拼起来。平台的环境变量也可以在配置里指定用的 Python 路径,确认用的不是系统默认环境。
6. 把预测结果变成选股信号:验证方法与两个进阶改法
6.1 选股信号的落地流程与验证方法
模型训练好之后,真正的难点是把预测值转化成可验证的选股信号。先定义一个“打分—排序—分仓位”的流程:模型对全市场股票做收益率预测,预测值排名前 N 的进候选池,按分数线性映射到仓位权重。为了防止模型预测值集中在很小的区间(很多回归模型的通病),可以用一个百分位排名替代原始预测值,这样选股信号对极端预测值不那么敏感:
from scipy.stats import rankdata # pred是模型对所有股票的预测收益率 pred = model.model.predict(X_all).flatten() # 把预测值转成0-100的百分位排名分 scores = rankdata(pred) / len(pred) * 100 # 取得分最高的20只股票等权买入 top_n = 20 selected = np.argsort(scores)[-top_n:]验证这份代码到底行不行,不要只看训练 loss。量化里的标准做法是三层验证:第一层看 IC(信息系数),计算预测值和实际收益率的 Spearman 相关系数,绝对值大于 0.03 算有正向预测力;第二层做分组回测,按分数分成五组,看最高组是否稳定跑赢最低组;第三层做样本外测试,用前 80% 时间训练、后 20% 时间做预测,看曲线是否还能保持正收益。这三层做完,如果结果都能过,你再谈实盘模拟。
6.2 进阶改法一:LSTM 加 BP 做集成预测
项目里同时提供 LSTM 和 BP 模型,你可以直接把两者组合成集成模型。两个模型分别预测,然后加权平均。这个做法在量化比赛中出现频率很高,因为单模型容易过拟合,集成后偏差和方差都能摊薄:
from LSTM_class import LSTMModel from BP_class import BPModel lstm_pred = lstm_model.model.predict(X_all).flatten() bp_pred = bp_model.model.predict(X_all).flatten() # 归一化到同一量纲再加权 from sklearn.preprocessing import MinMaxScaler scaler_lstm = MinMaxScaler() scaler_bp = MinMaxScaler() lstm_scaled = scaler_lstm.fit_transform(lstm_pred.reshape(-1, 1)).flatten() bp_scaled = scaler_bp.fit_transform(bp_pred.reshape(-1, 1)).flatten() # 加权融合,权重可以按验证集IC来定 final_score = 0.6 * lstm_scaled + 0.4 * bp_scaled收益率预测的绝对值本身误差很大,但排序信息是有价值的,所以集成时用MinMaxScaler把两组预测压到可比区间,再按各自的验证集 IC 或准确率来决定0.6和0.4的权重,这比拍脑袋定权重更有说服力。
6.3 进阶改法二:把预测目标从收益率改成超额收益
如果你的回测结果显示模型选的股票和沪深 300 指数走势差不多,那不是模型没用,是预测目标选错了。用绝对收益做标签,模型学到的是全市场的系统性上涨,选股信号区分度不高。把标签改成个股收益率减去同期指数收益率,模型聚焦在“相对跑赢”的分化上,这样选出来的股票才是真正有 alpha 的那部分。改动就是标签替换为:
stock_return = df['close'].pct_change(periods=hold_days).shift(-hold_days) benchmark_return = df_index['close'].pct_change(periods=hold_days).shift(-hold_days) # 超额收益作为新标签 label = stock_return - benchmark_return整个项目拆下来,最值钱的不是那一层 LSTM 网络本身,而是这个把财务数据变成训练样本、把训练结果变成选股信号、把预测效果放在回测框架里验证的完整骨架。这份代码能让你在一个下午内跑通整条链路,也能为论文提供完整的 LSTM 与 BP 对比实验。把那几个坑提前排掉,训练一个属于自己的 LSTM 选股模型,并不需要太多代码功底,但需要足够的耐心把数据流捋顺。从那以后,我每拿到一个量化相关项目,都强制自己先画完“数据→标签→对齐→归一化”这条链路再碰网络结构,而不是急着把模型跑起来。这个习惯帮我省下的调试时间,远比训练模型花的时间多。希望帮到你。
本文还有配套的精品资源,点击获取