简介一套基于Python与LSTM神经网络模型实现的股票指数预测项目源码源自作者大三期末大作业经导师指导并认可评审高达99分适合计算机相关专业学生用做期末大作业、课程设计或毕业设计参考也适合具备Python基础的学习者进行项目实战训练。压缩包共10个文件、1.65MB包含3个ipynb预测脚本分别演示上证综指、道琼斯等指数的建模与预测、2个md说明文档、1个训练好的best_model.pth权重文件及环境依赖yml配置结构清晰便于按需查阅。已有163人学习下载。通过该项目可以完整掌握LSTM时间序列预测流程包括数据读取预处理、网络模型搭建、训练调优、指标可视化以及模型保存与加载自带最优权重可直接运行体验预测效果还可替换股票数据迁移到其他指数或行情序列是一份可复用性很强的实战范例。代码中附有详细注释从数据清洗、特征构建到模型评估均给出完整思路适合初学者循序渐进地完成从理论到实践的跨越也能为课程答辩提供有力支撑。1. LSTM模型做股票指数预测这个项目到底能预测什么你见过大量“Python LSTM 预测股票”的博客配一张近乎完美的拟合图然后评论区一堆人问能不能用来炒股。我的判断是这个方向能做但价值不在“预测明天的涨跌”而是用神经网络算法把市场里的时序结构拆出来——用 Python 写一个完整的 LSTM 股票指数预测项目本质上是在训练一个“走势形态识别器”。它适合三类人想入门深度时序建模的工程师、做量化研究需要基准模型的开发者、以及想验证“AI 能不能解释行情”的研究者。不适合期待短期暴富的散户。指数比个股更适合 LSTM因为它噪音更小、流动性稳定、无突发的单票黑天鹅。项目源码本身不难难在数据切分、归一化时机和损失函数的选择上这三个点决定了模型到底是学到了规律还是背下了噪音。2. 建模逻辑与损失函数设计先想清楚再写代码2.1 问题定义回归任务还是分类任务预测几步才合理LSTM 预测股票指数第一步要明确输入和输出是什么。很多项目直接把“过去 N 天的收盘价”喂进去输出“明天的收盘价”这属于最朴素的回归定义。但这样做有三个隐患价格序列是非平稳的指数的绝对数值会随时间漂移收盘价的一阶自相关极强模型学到一个“复制昨天”的映射就能拿到很低的 loss训练集和测试集如果分布差异大模型在测试集上几乎必然失效。我常用的定义是用过去 60 个交易日的特征序列预测未来 5 个交易日的累计收益率。换成具体形式就是X 的形状是(batch, 60, feature_dim)y 是一个标量“未来五天收益”回归。为什么不预测未来一天一天窗口的噪音太大开盘跳空和尾盘波动会淹没一切信号而五天的窗口更能体现 LSTM 对短期趋势结构的捕捉能力。多步预测也可以做——输出(batch, 5)的向量直接预测未来 5 天每一天的收益率——但训练难度明显增加误差会随时间步长累积。还有一种做法是把问题改成二分类预测未来 5 天收益率大于 0 还是小于 0输出层用 sigmoid损失函数用 BCEWithLogitsLoss。分类的好处是评估直观方向准确率比连续值的 MAE 更容易解读坏处是损失函数不再惩罚“预测得太夸张”这类错误模型的概率输出也未必校准。我一般先用回归跑通基线再看方向准确率能不能过 50%如果连方向都判断不了意味着序列里的可学信号根本不足以支撑策略。2.2 特征选择和归一化为什么只喂收盘价不够只看收盘价做 LSTM 预测相当于让一个人蒙住眼睛只听音量猜市场情绪——能听到一些信息但丢掉了太多。我建议特征至少包含五个维度开盘价、最高价、最低价、收盘价、成交量。如果还想加技术指标相对强弱指标 RSI 和 MACD 的差分值可以各加一列但不要贪多。特征数量从 5 加到 20模型能力不上升反而下降这是小样本时序任务的常见现象——LSTM 的参数量随输入维度线性增长数据量撑不住。归一化在时序预测里比图像识别更敏感。图像归一化用全体数据的均值和标准差没太大问题因为像素分布相对稳定但股票行情完全不一样。训练集是 2015 年到 2020 年测试集是 2021 年你在归一化的时候用到了 2021 年的最高价和最低价模型在训练阶段就“见过”未来的取值范围这叫数据泄露。正确的顺序是先按时间切分然后只在训练集上拟合 MinMaxScaler再分别 transform 验证集和测试集。代码长这样from sklearn.preprocessing import MinMaxScaler # 假设已经按时间排序train_df / val_df / test_df 是三个互不重叠的时间段 scaler MinMaxScaler(feature_range(0, 1)) # 只对训练集的数值列拟合 train_scaled scaler.fit_transform(train_df[feature_cols]) # 验证集和测试集只能 transform不能再次 fit val_scaled scaler.transform(val_df[feature_cols]) test_scaled scaler.transform(test_df[feature_cols])这段代码里scaler.fit_transform和scaler.transform的顺序是生死线。验证集和测试集一旦调用了fit_transform等于让模型在训练时看到了未来的数据范围测试指标会虚高。另外注意MinMaxScaler对异常值敏感如果指数在某一天出现极端跳空比如千股跌停最大最小值会被拉到一个离谱的位置其他所有数据压缩到零点几的区间学出来的特征区分度很差。遇到这种情况用 RobustScaler 替代它用中位数和四分位数做归一化抗异常值能力更强。2.3 损失函数的选择MSE、Huber 还是方向性损失损失函数直接决定模型关注什么。MSE 是时序回归的默认配置它惩罚大误差远重于小误差所以模型会尽可能避免预测出极端值——这在股票预测里不一定对因为市场常常用极端走势来转折。Huber Loss 在误差小于阈值时是平方损失大于阈值时变成线性损失兼顾了平滑和对异常值的容忍度。预测股票指数这种本身就是尖峰厚尾分布的数据Huber 通常比纯 MSE 更稳健。torch.nn.SmoothL1Loss就是 Huber 的一种实现参数控制阈值。我把损失函数从 MSE 换到 Huber 的经验是训练初期的 loss 下降速度会慢一点但验证集上的局部波动明显减少预测曲线不会跟着单日暴涨暴跌剧烈摆动。如果你建的是方向分类模型那就用BCEWithLogitsLoss它把 sigmoid 和交叉熵合并在一起数值更稳定。选择损失函数的核心原则是先去定义什么叫“预测得好”。如果你希望预测曲线贴近真实曲线MSE 和 Huber 都可以如果你希望捕捉拐点那么单纯用回归损失还不够需要在损失里加一个方向惩罚项——比如当预测收益与真实收益符号相反时额外翻倍惩罚。这个自定义损失不复杂但实际收益很明显尤其是用模型信号做多空判断的时候。3. 数据准备清洗行情、计算收益率和切分工序3.1 行情数据清洗日期、缺失值和列顺序数据源我常用本地 CSV格式一般是date, open, high, low, close, volume。第一步一定是排序去重指数数据有时会在同一日期出现两行记录可能是数据源推送重叠导致第二步是缺失值处理停牌或者数据源漏推会产生 NaN 行。直接 dropna 会损失连续的历史上下文用ffill()前向填充更符合行情逻辑——缺失那一天的数值维持前值对 LSTM 的序列输入更友好。import pandas as pd df pd.read_csv(index_daily.csv, parse_dates[date]) # 排序、去重、按日期升序 df df.sort_values(date).drop_duplicates(subsetdate, keeplast).reset_index(dropTrue) # 填充缺失值前向填充 df[[open, high, low, close, volume]] df[[open, high, low, close, volume]].ffill() # 删除仍然缺失的行序列最前面的空窗期 df df.dropna().reset_index(dropTrue)这里有个细节drop_duplicates时keeplast保留同一天的最后一笔记录因为同一交易日的盘中快照可能有多个版本收盘后的最新修订值才是最终行情。ffill()之后一定要检查序列开头的缺失段如果 2010 年最初几周的数据全是 NaNffill会一直空着最后用dropna干净地切掉。3.2 构造收益率序列和滑窗样本接下来把行情转换为模型真正要学的东西。我的惯例是创建目标列future_5d_return代表未来 5 个交易日的累计收益率。用pct_change(5)计算但注意 Pandas 的pct_change默认是当前值相对 5 天前的值我们要的是“未来”所以要对齐方向。最简单的做法是先用pct_change(5).shift(-5)把结果移动到当前时刻位置最后一行的未来 5 天数据不存在会自动变成 NaN最后删除。滑窗生成用 Python 循环或 numpy 的 stride-tricks 都可以。对中等规模数据3000 行左右来说普通循环已经够快不需要上复杂技巧。代码import numpy as np lookback 60 target_lookahead 5 feature_cols [open, high, low, close, volume, ma5, ma20] scaler MinMaxScaler() scaled scaler.fit_transform(df[feature_cols]) X, y [], [] for i in range(lookback, len(df) - target_lookahead): X.append(scaled[i - lookback:i]) # 未来5天累计收益率用原始序列计算 future_return df[close].iloc[i target_lookahead] / df[close].iloc[i] - 1 y.append(future_return) X np.array(X, dtypenp.float32) y np.array(y, dtypenp.float32).reshape(-1, 1)这段代码的三个关键点range起点是lookback保证第一个样本至少有 60 天历史终点是len(df) - target_lookahead保证目标值不越界X的形状是(样本数, 60, 特征数)这就是 LSTM 要求的(seq_len, input_size)在单样本层面的排列。注意特征列里加了ma5和ma20这是移动平均线用来给模型提供趋势上下文实际加不加可以自己试我加的原因是股价单日噪音太大均线能帮 LSTM 稳定输入分布。3.3 时间序列切分为什么不能随机打乱这是新手最容易翻车的地方。图像分类里我们习惯train_test_split(shuffleTrue)随机切分但时序任务一旦随机打乱训练集里混着未来的数据LSTM 学到的是“看到后面猜前面”的虚假映射测试时拿到的完全是无序的历史窗口模型直接崩掉。正确做法是严格按时间顺序切分。n len(X) train_end int(n * 0.7) val_end int(n * 0.85) X_train, y_train X[:train_end], y[:train_end] X_val, y_val X[train_end:val_end], y[train_end:val_end] X_test, y_test X[val_end:], y[val_end:]切分比例 70% / 15% / 15% 是时序任务常用的默认配置。这里还有一层更微妙的问题相邻样本的输入窗口有 59 天是重叠的因此训练集里最后一批样本和验证集第一批样本之间的自相关性很高。严格做法是切分后再按gap lookback丢弃边界上的样本但实践中影响不大大多数人不会这么做。如果你追求严谨在切分后X_val前 60 条删掉就是。另外要检查切分后各集合的收益率均值是否接近如果测试集恰好是一波大牛市起点那模型测试分数会虚高评估结论也会有偏差。4. 基于 PyTorch 构建 LSTM 模型并完成训练4.1 模型结构从输入维度到输出层的完整设计LSTM 模型的主体是两层堆叠的 LSTM 单元再接一个全连接输出层。输入维度是特征数量隐藏单元数我用 64 起步数据集大的话可以上到 128。层数超过两层收益就很小了反而增加训练难度和过拟合概率。dropout 放在 LSTM 层之间数值取 0.2 到 0.3。全连接输出层把最后一个时间步的隐藏状态映射到标量收益。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1), ) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) last_out out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.regressor(last_out)代码里的batch_firstTrue意味着输入形状是(batch, seq_len, input_size)而不是 PyTorch 默认的(seq_len, batch, input_size)。忘记设这个参数会导致维度对不上报错信息很抽象。out[:, -1, :]取出序列最后一个时间步的隐藏状态如果用out[-1]拿到的会是最后一个 LSTM 层的输出语义完全不同——前者是时间维度的最后一步后者是层维度的最后一层。第一次写 LSTM 的人大概率在这两个索引上踩过坑。4.2 训练循环梯度裁剪、学习率衰减和早停训练脚本需要包含四样东西数据加载、损失函数、优化器、训练循环。数据加载用TensorDataset配合DataLoaderbatch_size建议 32 或 64。num_workers在 Windows 环境下容易出问题如果跑不起来就设 0。from torch.utils.data import TensorDataset, DataLoader dataset TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) loader DataLoader(dataset, batch_size64, shuffleFalse) model LSTMPredictor(input_sizeX_train.shape[2], hidden_size64, num_layers2) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.SmoothL1Loss() scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5, factor0.5) clip_value 1.0 for epoch in range(100): model.train() epoch_loss 0 for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), clip_value) optimizer.step() epoch_loss loss.item() * len(xb) # 验证阶段 model.eval() with torch.no_grad(): val_pred model(torch.tensor(X_val)) val_loss criterion(val_pred, torch.tensor(y_val)) scheduler.step(val_loss) if (epoch 1) % 10 0: print(fEpoch {epoch1}, Train Loss: {epoch_loss/len(X_train):.5f}, Val Loss: {val_loss:.5f})几个关键参数说明。shuffleFalse在训练中可能让你犹豫——图像训练都开 shuffle这里为什么不开因为相邻交易日样本高度相关shuffle 会让模型在同一个 batch 里同时看到 1 月和 7 月的数据短期规律会被打散。我实践下来开 shuffle 训练收敛更快但要配合dropout防过拟合不开则更保守验证集波动更平滑。clip_grad_norm_是 LSTM 训练的标准操作梯度范数超过 1.0 时会被缩放防止时间步长过长导致梯度爆炸。ReduceLROnPlateau在验证集 loss 连续 5 个 epoch 不降时把学习率减半这是最实用的调参工具。4.3 超参数实验隐藏层大小、学习率和序列长度的合理范围跑实验时我建议每次只动一个变量。先把hidden_size固定在 64lookback固定在 60lr从 1e-3 开始。如果训练 loss 震荡严重说明学习率太高降到 3e-4如果 loss 下降极慢先检查数据归一化是不是出了问题——特征值范围不一致会导致梯度更新被大数值特征主导。lookback这个参数代表模型“回忆”多少天的行情20 天适合捕捉短期动量60 天在指数上表现更稳120 天数据量不够时容易过拟合。批大小 64 比较中庸显存小的机器用 32。总训练轮数控制在 50 到 100配合早停验证多跑不会带来明显收益——股票数据本身信噪比低LSTM 在高 epoch 数下很快就开始记忆训练集噪音。5. 避坑清单数据泄露、未来函数与 LSTM 的翻车真相5.1 数据泄露用了全体数据的归一化参数现象训练时 loss 正常验证集 loss 也能接受一上测试集分数吓人的好。原因验证集或测试集数据参与了 MinMaxScaler 的拟合等于模型在训练阶段就知道了未来的取值范围测试失真。解决严格按代码执行两步走scaler.fit(train_scaled)之后验证集和测试集只能调用transform。5.2 未来函数特征里混入了当天收盘价去预测当天现象回测曲线惊艳实盘模拟却一塌糊涂每日收益 alpha 几乎为零。原因构造特征时用了今天的收盘价和成交量去预测明天的收益但今天收盘后你实际能拿到的数据只有日终快照高频信号在次日开盘就无法复现。解决检查样本构造循环X 窗口结束的那一天应当对应 y 标签的未来起点。严格形式是X取i - lookback到i含y对应i 1到i target_lookahead的收益不能把i当天同时放进 X 和 y 的计算基准里。用 Pandas 的shift(1)把所有特征整体滞后一天彻底削掉同周期信息。5.3 滞后假象模型在“抄昨天的作业”现象把 predict 值和真实值画在一起曲线几乎重合MAE 低到 0.01但把预测序列往前挪一天看完全对不上。原因收盘价自相关极高直接回归收盘价时 LSTM 学到的最优策略是输出“昨天的收盘价”因为这在 MSE 上的误差极小。解决改用收益率作为目标收益率序列是近似平稳的模型无法用复制来刷低 loss同时计算方向准确率正确率若低于 52%预测收益曲线的“高精度”就是视觉幻觉。5.4 预测值钝化滚动预测的误差累积现象单步测试正常改成多步预测后曲线变成一条趋于水平的线。原因LSTM 每次用预测值作为下一步输入时误差逐步累积最终预测值收敛到一个常数——这不算 bug是所有递归预测的通病。解决部署时避免盲目递归多步改用“训练时直接用真实值做教师强制teacher forcing推理时只外推 5 步就停然后重新取真实行情刷新窗口”。不要做 60 步连续递归预测那既不符合实盘场景也没有实用意义。5.5 过拟合训练集 loss 0.003换行情年份就崩现象训练集和验证集 loss 都低但把模型换到没见过的市场环境比如从牛市段到熊市段预测完全失效。原因模型记住了训练期的特定波动形态。解决增加 dropout、提高学习率衰减的 patience、设置早停更本质的办法是缩短训练区间用最近的 1 年数据训练用随后 3 个月做验证指数行情风格漂移的速度比一般人想得快得多5 年前的数据对今天的预测基本没有价值。6. 进阶方向从预测模型到可验证的量化策略模型跑出结果之后下一步是能不能用。我习惯把预测信号转化为策略过滤器模型预测未来 5 天收益率为负时不开多仓为正时允许执行原有的趋势策略开仓信号。这样做不是拿 LSTM 做主信号而是把它当成风控层过滤掉模型认为风险最高的时段。要验证 LSTM 是否真的有用需要一个对照实验把训练集的 y 标签随机打乱重新训练一个模型对比它与原模型在测试集上的方向准确率。如果两个模型的准确率差距不超过 2%说明原模型学到的全是噪声模式这个数据上就做不出可用的预测信号。这个对照实验我每次都会跑它是最便宜的“后悔药”。工程部署上模型权重会过期建议每 5 个交易日重新训练一次输入窗口用滚动更新不要用固定离线文件。文件组织可以分成data/、scripts/、models/三个目录训练脚本导出标准化的预测结果 CSV评估脚本单独跑。生产环境用 CPU 推理 LSTM 就够了一个指数预测模型只有两层 LSTM单条样本推理不到 2 毫秒不需要 GPU。如果你要往深了做可以尝试把 LSTM 的输出接一个轻量注意力模块或在特征里加入跨资产数据比如同时输入上证指数和沪深 300 的行情做多任务预测这些都比堆模型层数有效。我在自己的项目里多次遇到看似提升实则过拟合的现象一个朴素的经验是先把基线模型稳定跑起来用现金流模拟回测 3 个月再考虑换更复杂的结构。希望帮到你。本文还有配套的精品资源点击获取