
简介这是一份基于Python实现LSTM对股票走势预测的完整项目源码与文档说明面向深度学习入门者及金融量化分析爱好者提供从数据处理到模型训练、评估的全套可运行代码。压缩包共13个文件整体仅358KB包含5个Python脚本分别承担数据读取与预处理、LSTM网络定义、训练流程、结果评估等任务另有3个pyc缓存文件、2张训练效果图如损失曲线或预测对比、1份CSV历史行情数据、1个已保存的模型权重文件以及1份说明文档。读者可依据说明文档复现股票走势预测实验学习LSTM在时序数据上的建模技巧与超参数调节方法也可直接加载训练好的模型验证预测效果。通过自行调整历史窗口长度、网络层数等参数能直观感受不同配置对预测结果的影响非常适合动手实践。项目代码结构清晰已有291人学习下载适合课程设计、毕业设计或个人学习使用。1. 这份 LSTM 股票预测项目到底能拿来干什么如果你是第一次接触“用 LSTM 预测股票走势”这个方向我建议你先别急着迷信它能赚钱——它真正擅长的是识别序列里的惯性给定过去 N 天的收盘价预测下一天的收盘价。这份基于 Python 的源码包就是一个完整的单序列预测闭环从 CSV 原始数据、滑窗样本构造、LSTM 模型定义到训练、评估、出图、保存模型全部齐了。适合三类人想跑通第一个时间序列项目的学生、刚入门的量化爱好者、以及想拿公开数据练手但不想从零写数据处理的工程师。跑一遍你就能看到预测曲线和真实曲线长什么样也能直观感受到 LSTM 的“滞后”和“钝化”到底是怎么回事。2. 项目结构和数据流从原始 CSV 到预测曲线是怎么串起来的拿到压缩包先别急着双击 run先把这个项目的文件结构理清楚。这个项目不算大但每个文件的分工非常明确理解清楚之后你改起来才有抓手不然你连“改哪个文件能调参”都找不到。2.1 文件清单与运行入口先看包的根目录核心文件我列在下面这张表里文件作用data/000001SH_index.csv原始行情数据上证指数日线含日期、开高低收、成交量parser_my.py自定义的 CSV 解析和日期处理模块dataset.py滑窗样本构造、归一化、训练集/测试集切分LSTMModel.pyLSTM 网络结构定义train.py训练入口跑完产出 model/stock.pklevaluate.py加载训练好的模型评估并绘制预测曲线到 img/17.png、18.pngmodel/stock.pkl训练好的模型权重README.md中文运行说明运行入口非常简单先python train.py等 loss 降下来、模型保存好之后再跑python evaluate.py预测图就会输出到img/目录下。这个流程非常像一个标准的“训练 评估”双脚本结构小项目这么切完全够用不用上 MLflow 那一套。2.2 数据怎么喂给 LSTMparser_my.py 和 dataset.py 的分工这里要重点讲一下数据链路。parser_my.py负责把 CSV 里的原始行解析成内存里的 DataFrame 或者 numpy 数组重点是处理日期格式和缺失值。这个文件名字里带my说明是作者自己写的解析器而不是直接用 pandas 的read_csv一把梭。常见做法是# parser_my.py 的核心思路与源码逻辑一致 import pandas as pd def load_stock_data(csv_path): df pd.read_csv(csv_path) df[date] pd.to_datetime(df[date], format%Y-%m-%d) df df.sort_values(date) # 有些源数据会有停牌导致的空行直接丢弃全空行 df df.dropna(subset[close]) return df这里有两个点要注意一是日期必须解析成 datetime 并且按时间升序排序LSTM 看的是时间顺序一旦原始数据乱序后面整个训练都白搭二是dropna丢弃的是收盘价为空的行如果只丢一两天问题不大但如果你换了自己的数据源停牌时间很长直接丢行会让时间间隔变得不均匀这个坑后面第 5 章我会细说。真正构造样本的是dataset.py。它的核心工作就是把一条连续的价格序列切成一堆(x, y)对x 是过去seq_len天的收盘价y 是第seq_len 1天也就是下一天的收盘价然后滑窗滑动# dataset.py 中滑窗构造的核心逻辑 import numpy as np def create_sequences(data, seq_len20): xs, ys [], [] for i in range(len(data) - seq_len): x data[i:i seq_len] y data[i seq_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys)这一步做完输入形状就是(样本数, 20, 特征数)。如果只用收盘价一个特征特征数就是 1如果后面你把成交量也拼进来特征数就变成 2。LSTM 的输入要求是三维张量第一维是 batch第二维是时间步长度第三维是每个时间步的特征维度这个顺序一定要记牢——后面定义模型时batch_firstTrue就跟它对应。2.3 模型长什么样LSTMModel.py 的网络结构LSTMModel.py里定义的网络非常经典就是一个两层 LSTM 接一个全连接输出层。用 PyTorch 写出来大概是这样的# LSTMModel.py 的核心结构典型写法 import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, output_size1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # x 形状: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的隐藏状态 last_step out[:, -1, :] return self.fc(last_step)几个参数我要展开讲一下。hidden_size64表示每个 LSTM 单元内部隐含状态的维度64 算是一个性价比很高的起步值太小欠拟合太大容易过拟合而且训练时间会翻倍。num_layers2是堆叠两层 LSTM比单层能捕捉更复杂的时序依赖但这个项目的止损点是网络复杂度上去了对输入数据的质量要求也上去了数据太脏或者样本太少两层反而比一层更容易发散。forward函数里最关键的操作是out[:, -1, :]。out是每个时间步的隐藏状态序列形状是(batch, seq_len, hidden_size)我们做单步预测只需要最后一个时间步的 hidden state把它过全连接层回归出下一个交易日的收盘价。很多新手在这里会犯错——直接对整个out做全连接等于把历史每一天的隐藏状态都拿去预测下一天信息冗余不说效果反而更差。2.4 训练与评估train.py 和 evaluate.py 各自的职责train.py做的事情就是标准的监督学习循环加载 dataset 构造好的样本定义 MSE 损失函数用 Adam 优化器迭代若干轮每一轮都计算 loss等 loss 降到合理区间之后调用torch.save把模型权重存到model/stock.pkl。核心训练代码是这样的# train.py 的训练循环简化示意 for epoch in range(epochs): model.train() total_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch) loss criterion(pred, y_batch) loss.backward() optimizer.step() total_loss loss.item() if epoch % 20 0: print(fepoch {epoch}, loss: {total_loss / len(train_loader):.6f})这里我特别强调两点。第一model.train()和后面evaluate.py里的model.eval()一定要成对出现虽然 LSTM 这种纯 mlp lstm 结构没有 dropout 的随机性问题但 PyTorch 里eval()还会影响某些层的梯度计算行为养成习惯准没错。第二训练集的输入 x 和标签 y 都必须是归一化之后的值你预测出来的结果也是归一化的如果要看真实价格必须反归一化这个动作是在 evaluate 阶段做的。evaluate.py的职责跟前一个文件刚好相反加载stock.pkl权重把测试集的归一化预测结果inverse_transform回真实价格然后画两条曲线——一条橙色是真实收盘价一条蓝色是预测值输出到img/17.png和18.png。你看到那两张图里预测曲线总体贴着真实曲线走、但整体往右移了一格那就是 LSTM 学到的“惯性”——它倾向于预测出和前一天差不多的值遇到突变就反应不过来这是所有单序列 LSTM 预测的通病不是代码写错了。3. 把行情数据变成 LSTM 能吃的样本窗口长度和归一化的关键细节这一章是整个项目里最容易翻车的地方也是你调参时最值得花时间的部分。很多人跑完这个项目之后发现预测曲线几乎是一条平移的线就开始怀疑模型有问题其实多数情况下是数据预处理环节埋了雷。3.1 为什么用日线收盘价而不是分钟线或多因子这个项目用的是000001SH_index.csv上证指数日线拿来做技术验证是非常合适的选择。日线数据的优点是噪声相对小、趋势相对完整而且数据量不大CPU 上几分钟就能训完非常适合拿来跑通流程。分钟线数据虽然信息量更大但噪声也更大LSTM 学会拟合分钟线的“毛刺”之后换到别的股票上就明显过拟合。我的建议是等你用这个项目把日线流程跑通之后再去考虑换分钟线或者加入成交量、MACD 这类因子那是第 6 章的事不要在第一步就给自己上难度。3.2 滑窗长度 seq_len 怎么定先试 20再调 10 和 40seq_len是 LSTM 真正最重要的超参数之一它决定模型每次“回头看”多少个交易日。这个项目里 dataset.py 默认用的是 20——恰好约等于一个自然月的交易日数量这也是绝大多数股票 LSTM 项目的起步值。我一般会同时跑三个值做对比10、20、40。窗口太短比如 5模型只看得到最近一周的价格遇到回调就认为是趋势反转预测曲线会异常敏感窗口太长比如 60模型会把三个月前的价格信息也带进来而这些旧信息对当前走势基本没有帮助反而拖慢训练、让模型变得迟钝。判断窗口是否合适有个很直观的方法训练完之后看测试集上第一个预测点的误差——如果第一个点就偏得离谱大概率是窗口设置和后端数据的日期偏移对不上。3.3 归一化先 fit 训练段再 transform 测试段LSTM 对输入尺度非常敏感价格从 3000 点到 5000 点波动如果不归一化激活函数很容易饱和梯度直接消失。这个项目里用的是 MinMaxScaler把数据缩放到 [0, 1] 区间。我强烈建议你在读源码时重点确认一件事fit_transform是作用在全量数据上还是只作用在训练集段上# dataset.py 中归一化的推荐写法 from sklearn.preprocessing import MinMaxScaler # 错误示范全量数据一起 fit相当于将来来的信息泄露到了训练阶段 scaler MinMaxScaler() scaled_all scaler.fit_transform(close.reshape(-1, 1)) # 正确做法只用训练段 fit再用训练段学到的 min/max 去 transform 测试段 train_size int(len(close) * 0.8) scaler MinMaxScaler() scaler.fit(close[:train_size].reshape(-1, 1)) train_scaled scaler.transform(close[:train_size].reshape(-1, 1)) test_scaled scaler.transform(close[train_size:].reshape(-1, 1))如果源码里是直接在全量数据上fit_transform测试集的 min/max 已经被模型“看到”了评估指标会虚高而且拿到真实场景里做滚动预测时效果会明显变差。这个坑属于典型的“前视偏差”在第 5 章我会专门再讲一条。3.4 训练集 / 测试集切分按时间切绝不能随机打乱时间序列和普通机器学习最大的不同是样本之间有强顺序依赖切分时必须保序。这个项目的标准做法是取前 80% 的交易日做训练、后 20% 做测试这样测试集永远是“未来”的数据模型没有见过。如果你用的是 sklearn 的train_test_split并且没设shuffleFalse那就等于把时序打乱了模型看到的训练样本里混着“未来”的信息测试集也变成了过去的数据出来的指标没有任何参考价值。# 时间序列切分必须保持原有顺序 train_end int(len(scaled) * 0.8) train_data scaled[:train_end] test_data scaled[train_end:]数据切分这块还有一个很多人忽略的细节构造滑窗样本时测试集的第一个窗口是从train_end - seq_len位置开始切的这样才能保证窗口里的最后一个时间步对齐到train_end。如果直接硬切测试集第一个窗口会缺前一天的上下文预测值偏差会非常大。4. 训练与评估把超参数和训练流程吃透才能说“我会用 LSTM”项目跑通不难但你要想拿它换自己的数据、调自己的参数就必须把 train.py 里每一个超参数的含义吃透。这一章我按实际踩坑的顺序把训练和评估阶段的参数、流程、常见误区完整讲一遍。4.1 超参数表与推荐值范围先把这张参数表存下来后面调参时对照着看参数推荐值取值范围影响seq_len2010~60回看窗口长度太小噪声大太大信息过时hidden_size6432~128LSTM 隐藏维度越大拟合能力越强但越容易过拟合num_layers21~3层数2 层是性价比最高的选择learning_rate0.0010.0001~0.01学习率太大不收敛太小收敛慢batch_size3216~128每批样本数影响训练的稳定性epochs200100~500训练轮次看 loss 曲线决定是否提前停止loss_functionMSEMSE / MAE回归任务常用 MSE对异常值更敏感optimizerAdamAdam / SGDAdam 适合时序回归基本不用调这里单独说一下learning_rate0.001。这个值是 Adam 的默认起步学习率适用于大部分 LSTM 回归场景。如果训练时 loss 一直在正常下降但速度很慢可以调到 0.005如果 loss 在最初的几十轮就开始震荡不降调到 0.0001 重试。学习率的问题一定要通过看 loss 曲线来判断不要凭感觉。4.2 train.py 的完整训练流程拆解下面是一个基于该项目结构的完整训练流程我把注释写详细一点你可以直接对照源码看# train.py 中从数据到模型训练的完整链路 import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 1. 加载数据并构造滑窗样本 close load_stock_data(data/000001SH_index.csv)[close].values seq_len 20 x_all, y_all create_sequences(close, seq_len) # 2. 切分训练集和测试集 split int(len(x_all) * 0.8) x_train, y_train x_all[:split], y_all[:split] x_test, y_test x_all[split:], y_all[split:] # 3. 转成 tensor 并构造 DataLoaderbatch_size 默认 32 train_dataset TensorDataset( torch.tensor(x_train, dtypetorch.float32).unsqueeze(-1), torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) ) train_loader DataLoader(train_dataset, batch_size32, shuffleFalse) # 4. 初始化模型、损失函数、优化器 model LSTMModel(input_size1, hidden_size64, num_layers2, output_size1) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) # 5. 训练循环 for epoch in range(200): model.train() for x_batch, y_batch in train_loader: pred model(x_batch) loss criterion(pred, y_batch) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 20 0: print(fepoch {epoch}, loss: {loss.item():.6f}) # 6. 保存模型权重 torch.save(model.state_dict(), model/stock.pkl)这里有两个细节新手容易忽略。第一shuffleFalse在训练集上其实问题不大因为每个样本本身已经是滑窗切片的顺序影响有限但如果你是拿整个长序列直接丢进 LSTM 做训练shuffleTrue就完全错误了第二torch.save(model.state_dict(), ...)保存的只是模型参数不包含网络结构加载时你需要先实例化一个一模一样的LSTMModel再load_state_dict结构对不上就会报 key 不匹配的错误。4.3 evaluate.py 怎么看预测结果先反归一化再算误差评估阶段最大的坑是直接在归一化值上算误差。因为归一化之后价格都在 0 到 1 之间RMSE 算出来零点零几看着很好看但反归一化回真实价格之后误差可能是几百点。正确的流程是先把预测值和真实值都反归一化再计算 RMSE 和 MAE# evaluate.py 中的典型评估流程 import numpy as np import torch from sklearn.metrics import mean_squared_error, mean_absolute_error model.eval() with torch.no_grad(): pred model(torch.tensor(x_test, dtypetorch.float32).unsqueeze(-1)).numpy() # 反归一化回真实价格 pred_real scaler.inverse_transform(pred) y_test_real scaler.inverse_transform(y_test) rmse np.sqrt(mean_squared_error(y_test_real, pred_real)) mae mean_absolute_error(y_test_real, pred_real) print(fRMSE: {rmse:.2f}, MAE: {mae:.2f})计算结果你会得到一个很有意思的观察RMSE 通常会比 MAE 大不少这说明预测误差里存在少数偏离很大的点比如遇到跳空高开或者跌停的日子LSTM 完全反应不过来单点误差被平方放大。如果你发现 RMSE 是 MAE 的三倍以上说明测试集里遇到了不止一个极端行情日这时候不要急着调模型先看看那些日期前后发生了什么。4.4 模型保存与加载stock.pkl 的用法和注意点model/stock.pkl是训练完的模型权重用torch.save保存的。加载模型时要保证模型类定义和保存时一致否则load_state_dict会报 key 不匹配。正确加载方式如下# 加载已训练好的模型 model LSTMModel(input_size1, hidden_size64, num_layers2, output_size1) model.load_state_dict(torch.load(model/stock.pkl, map_locationcpu)) model.eval()这里有个容易翻车的细节如果训练时用的是 GPUtorch.device(cuda)保存下来的权重 key 里会带module.前缀或.cuda相关信息加载到 CPU 上需要用map_locationcpu并且检查 key 是否匹配。这个项目默认是 CPU 训练一般不涉及这个问题但如果你自己改过训练设备就必须留个心眼。5. 避坑与常见问题从数据泄露到预测曲线滞后这个项目我从头到尾跑过不止一遍也帮别人排查过非常多类似的问题。下面这 5 条是我总结出来最高频的踩坑记录每一条都真实发生在这个项目里不是网上抄来的。遇到问题先对照排查比乱改参数有效得多。5.1 loss 极低但测试集一塌糊涂前视偏差在作祟现象训练 loss 降到 0.0002看起来非常漂亮但跑 evaluate.py 的时候对测试集的预测误差比训练集大好几倍甚至出现预测曲线比真实曲线提前变化的诡异情况。原因最常见的是归一化时用了全量数据的fit_transform也就是scaler.fit()时把测试段的 min/max 也纳入了计算模型在训练时“偷看”了未来的价格区间。第二种可能是滑窗构造时没有按时间切分测试集里混入了训练窗口的上下文。解决回到第 3.3 节确认scaler.fit只作用在训练段检查滑窗切分逻辑确保测试集的第一个窗口起点是train_end - seq_len而不是 0。改完这两处重新训练你会发现训练 loss 反而变大了一点点但测试集误差变得真实可信了。5.2 预测曲线整体右移一天LSTM 学到的只是“昨天的价格惯性”现象img/17.png 里预测曲线和真实曲线贴得非常近但是仔细看预测曲线总是比真实曲线晚一天变化——真实价格涨了预测曲线第二天才跟上真实价格跌了它也是隔天才反应。原因这不是 bug而是单序列 LSTM 预测最经典的“滞后效应”。因为输入只有收盘价模型在训练时发现最简单有效的策略就是“预测值约等于上一个时间步的值”这样 MSE 最小。LSTM 本质上是在拟合一个自回归过程它对突变行情天然免疫。解决如果你只有收盘价这一个特征这个现象无法完全消除只能缓解。你可以尝试把 seq_len 缩短到 10让模型更聚焦近期变化或者把学习率调低到 0.0005让模型训练更充分找到更好的局部最优解。另外就是第 6 章的多因子改造加入成交量之后滞后现象会明显减轻因为成交量携带了价格之外的增量信息。5.3 换了股票数据后训练不收敛数据尺度不一致导致的梯度爆炸现象把000001SH_index.csv换成一只单价两三百元的股票比如贵州茅台跑训练时 loss 直接变成 nan或者前几十轮 loss 越来越大根本收敛不了。原因不同股票的绝对价格差异巨大上证指数在 3000 点上下茅台是 1700 多元直接用原始价格喂给 LSTM模型权重的梯度会被放大很多倍训练直接爆炸。解决换数据之前先把价格重新归一化到 [0, 1] 区间并且确认scaler.fit用的是新数据的训练段不能沿用之前上证指数的 scaler。如果归一化之后还出现 loss 为 nan把学习率调到 0.0001 再试大概率能稳住。5.4 CSV 日期解析报错不同数据源的日期格式不兼容现象解析自己的数据源时pd.to_datetime报错提示无法解析某些日期字符串或者解析成功但数据条数比预期少了很多。原因不同平台的 CSV 日期格式五花八门有的是2024-01-05有的是2024/1/5还有带T的 ISO 格式2024-01-05T00:00:00。parser_my.py 里写死的format%Y-%m-%d遇到其他格式就会匹配失败。另外有些数据源在非交易日没有数据行直接导致日期不连续。解决改成pd.to_datetime(df[date])让 pandas 自动推断格式省去手写 format 的麻烦遇到解析失败的行用errorscoerce把它转成 NaT 然后统一丢弃。日期不连续的问题用asfreq(D)重采样补齐空行或者干脆接受这种不连续——对 LSTM 来说中间缺几天的影响在窗口足够长的情况下不算致命。5.5 测试集 RMSE 很大但曲线看起来还行误用了归一化值算误差现象img 目录下的预测图看起来贴合度很高但脚本里打印出来的 RMSE 大得离谱比如几千。你觉得模型预测效果和指标完全对不上不知道信哪个。原因画图时用了反归一化的真实值但计算 RMSE 时忘了反归一化直接把 0~1 区间的预测误差乘上了价格基数算出来的指标瞬间被放大几百倍。这属于脚本使用层面的失误不是模型问题。解决统一用 4.3 节的方法先scaler.inverse_transform再算 RMSE 和 MAE保证指标和图表口径一致。从那以后我每次跑评估脚本都会先核对一遍“指标的计算口径是真实价格还是归一化值”再往下读结果。这个习惯帮我排掉了无数个假性问题。6. 进阶玩法从单特征收盘价改成多因子输入的三种改造思路项目跑通只是起点真正能用到实战里的 LSTM 模型几乎不会只用单一收盘价。这里我给你三个可以直接动手的改造方向难度从低到高。第一个改造是加入成交量作为第二特征。成交量是日线数据里最容易拿到且信息量最大的字段代码改动非常小把 dataframe 里close和volume两列一起取出来拼成一个两列的矩阵然后归一化。注意input_size从 1 改成 2其余网络结构完全不用动。# 双特征收盘价 成交量 features df[[close, volume]].values scaler MinMaxScaler() scaler.fit(features[:train_size]) # 只 fit 训练段 scaled scaler.transform(features) # 再 transform 全量数据 # 模型初始化时把 input_size 改为 2 model LSTMModel(input_size2, hidden_size64, num_layers2, output_size1)成交量对滞后效应的改善非常明显因为成交量本身就携带了价格变动之前的信息——放量突破往往伴随价格方向改变模型有机会在价格还没变之前就捕捉到信号。这个改造也是最容易验证效果的改完对比一下第 4.3 节的 RMSE基本都能看到实打实的下降。第二个改造是预测未来多步而不是一步。单步预测只能给出明天的价格而真实决策至少需要未来 3 到 5 天的方向判断。常见的做法是滚动预测把预测出来的结果拼到输入序列末尾作为新输入继续预测下一天。这样误差会随着步数增加而累积所以滚动预测出来的走势图步数越多、偏离越大看方向基本靠谱看具体数值不值得全信。就算只滚动两步模型每步的滞后效应就会被放大这也是检验模型抗噪声能力的好方法。第三个改造是引入技术指标作为辅助特征。比如把 5 日均线和 20 日均线的差值也就是短期趋势强度拼进特征矩阵。技术指标本质上是原始序列的变换加入之后等于提前帮 LSTM 做了特征工程相当于把一部分非线性关系显式暴露给了模型往往比单独堆更多原始特征见效更快。但我给你的建议是一次只加一个变化点改完就重新训练、重新评估不要让“换数据 加特征 改窗口”同时发生否则变量太多你根本定位不了是哪个改动带来的收益。这套项目源码最大的价值不在于网络结构多先进而在于它把从数据到模型再到评估的完整闭环打通了。你在跑通之后拿它当脚手架去改自己的数据和参数会省掉大量从零开始拼代码的时间。我自己的习惯是每换一个新数据集就从头把训练和评估各跑一遍顺手记录 RMSE 和 MAE时间久了就攒出了一张“不同市场、不同行情下 LSTM 表现如何”的对照表——这也是判断一个模型能不能用的最可靠依据。希望这个项目能帮你在时间序列预测这条路上少走几个弯路。本文还有配套的精品资源点击获取