基于Python的LSTM股票走势预测:从数据到模型实战指南

📅 发布时间:2026/9/28 8:59:40
基于Python的LSTM股票走势预测:从数据到模型实战指南
简介这份资源面向计算机、金融科技等专业的学生与开发者提供一套基于Python与LSTM神经网络预测股票走势的完整项目可直接用于毕业设计、期末大作业或课程设计。压缩包共89个文件约8.88MB以24个py源码文件为核心辅以pyc编译文件、txt说明、jpg图表、css与js前端资源、xml配置、html页面及xls/xlsx数据集和sqlite3数据库覆盖数据读取、模型训练、结果可视化等环节。项目内含代码、数据集与已训练模型结构清晰、注释充分新手也能按目录逐步理解LSTM时序建模流程并在此基础上替换数据或调整参数完成二次开发。目前已有140人学习下载适合希望快速获得可运行、可展示的股票预测实战案例的读者参考使用。1. 用 Python 把 LSTM 套在股票走势上先搞清楚它能干什么、不能干什么很多人第一次听到「基于Python实现LSTM对股票走势的预测」脑子里浮现的画面是喂进去几年K线跑一晚上第二天模型告诉你明天涨还是跌然后财富自由。我当年也这么想过直到自己拿真实数据跑了一遍才发现事情完全不是这个走向。LSTM长短期记忆网络确实擅长处理时间序列股票收盘价、成交量、换手率这些按时间排列的数据天然适合它但「擅长拟合历史」和「能预测未来」之间隔着一条很深的沟。这篇文章要讲的是用 Python 和 LSTM 搭出一套完整的股票走势预测流程从数据获取、特征工程、窗口切分到模型搭建、训练、评估再到实盘前必须做的几个验证动作。适合已经会一点 Python、想动手跑通第一个金融时序预测项目的读者也适合跑过 demo 但发现结果「每次都不一样」、想搞清楚问题出在哪的人。我不会给你一个能赚钱的策略但会让你清楚这套东西的边界在哪、参数怎么调、哪些坑一定会踩。2. 数据准备与特征工程LSTM 吃进去的到底是什么2.1 为什么股票数据不能直接丢给 LSTMLSTM 的输入是一个三维张量形状是(样本数, 时间步长, 特征数)。原始股票数据是一张二维表每行一个交易日每列一个字段开盘、最高、最低、收盘、成交量。直接把这表丢进去模型根本不知道哪些行属于同一个「时间窗口」。所以第一步必须做滑动窗口切分用前 N 天的数据预测第 N1 天的值。这个 N 就是时间步长timestep是最关键的参数之一。另一个问题是量纲。收盘价可能是 3000 点成交量可能是几亿手换手率是 0.5%。如果不做归一化成交量这个特征在梯度里会完全压过其他特征模型等于只学了成交量。常见做法是用 MinMaxScaler 把每个特征缩放到 [0,1]或者用收益率代替绝对价格——后者在金融时序里更稳因为价格是非平稳的收益率近似平稳。还有一个容易被忽略的点归一化必须只用训练集拟合 scaler再 transform 验证集和测试集。如果拿全量数据拟合测试集的信息就泄漏进训练过程了评估结果会虚高实盘一跑就翻车。这是血泪经验我见过太多人在这里栽跟头。2.2 用 akshare 拉数据并做窗口切分数据源我用 akshare免费、接口稳定、不需要 token。下面这段代码拉取一只股票近几年的日线做归一化和窗口切分。import akshare as ak import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 1. 拉取日线数据adjustqfq 表示前复权 df ak.stock_zh_a_hist(symbol600519, perioddaily, start_date20190101, end_date20241231, adjustqfq) df df[[日期, 开盘, 最高, 最低, 收盘, 成交量]].copy() df.columns [date, open, high, low, close, volume] df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 2. 构造额外特征日收益率、振幅 df[ret] df[close].pct_change() df[amplitude] (df[high] - df[low]) / df[close].shift(1) df df.dropna().reset_index(dropTrue) feature_cols [open, high, low, close, volume, ret, amplitude] data df[feature_cols].values # 3. 按 7:1.5:1.5 切分训练/验证/测试先切再归一化 n len(data) train_end int(n * 0.7) val_end int(n * 0.85) scaler MinMaxScaler() train_scaled scaler.fit_transform(data[:train_end]) # 只 fit 训练集 val_scaled scaler.transform(data[train_end:val_end]) test_scaled scaler.transform(data[val_end:]) # 4. 滑动窗口切分 def make_windows(arr, timestep): X, y [], [] for i in range(len(arr) - timestep): X.append(arr[i:i timestep]) y.append(arr[i timestep, 3]) # 索引 3 是 close return np.array(X), np.array(y) TIMESTEP 30 X_train, y_train make_windows(train_scaled, TIMESTEP) X_val, y_val make_windows(val_scaled, TIMESTEP) X_test, y_test make_windows(test_scaled, TIMESTEP) print(X_train.shape, y_train.shape) # (样本数, 30, 7)这段代码有几个关键决策。adjustqfq用前复权价避免除权除息造成的价格跳空被模型当成真实波动。ret和amplitude是我加的两个衍生特征收益率帮模型捕捉动量振幅帮它感知波动率。TIMESTEP30是经验值对应大约一个半月的交易日太短模型看不到趋势太长梯度容易衰减、训练也慢。切分比例 7:1.5:1.5 是常见做法验证集用来调参和早停测试集只在最后评估一次绝不能拿来调参。注意make_windows里 y 取的是arr[itimestep, 3]也就是窗口后一天的收盘价。如果你想预测的是「涨还是跌」这里要改成二分类标签模型最后一层也要相应换成 sigmoid。2.3 特征选择的取舍不是特征越多越好。我试过把 MACD、RSI、布林带全塞进去结果验证集 loss 反而更大——特征之间高度相关模型在训练集上记住了噪声。常见做法是先用 5 到 8 个核心特征跑通再逐个加看验证集指标有没有提升。如果加了某个特征验证集变差果断删掉。另外成交量这类量纲差异大的特征归一化后如果分布极度偏斜比如大部分日子成交量很小、偶尔爆量可以考虑先取对数再归一化。3. 搭 LSTM 模型层数、隐藏单元、Dropout 怎么定3.1 PyTorch 版 LSTM 的最小实现框架我选 PyTorch因为它的 LSTM 层参数直观、调试方便。下面是一个两层 LSTM 加全连接输出的模型。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class StockLSTM(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, # 输入形状 (batch, seq, feature) dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # out: (batch, seq, hidden)hn/cn 是最后时刻的隐状态 out, (hn, cn) self.lstm(x) last out[:, -1, :] # 只取最后一个时间步的输出 return self.fc(last).squeeze(-1) device torch.device(cuda if torch.cuda.is_available() else cpu) model StockLSTM(input_sizeX_train.shape[2]).to(device) # 转成 tensor 并建 DataLoader def to_loader(X, y, batch_size64, shuffleFalse): ds TensorDataset(torch.tensor(X, dtypetorch.float32), torch.tensor(y, dtypetorch.float32)) return DataLoader(ds, batch_sizebatch_size, shuffleshuffle) train_loader to_loader(X_train, y_train, shuffleTrue) val_loader to_loader(X_val, y_val) test_loader to_loader(X_test, y_test)batch_firstTrue让输入形状变成(batch, seq, feature)符合直觉。num_layers2是常见起点一层往往欠拟合三层以上在小数据集上容易过拟合。dropout0.2只在多层时生效作用是随机丢弃部分神经元输出抑制过拟合。forward里只取out[:, -1, :]也就是最后一个时间步的隐状态因为我们要预测的是窗口结束后的那一天前面的时间步信息已经被 LSTM 逐步编码进最后的状态里了。3.2 训练循环与早停训练部分要盯住验证集 loss一旦连续几轮不下降就停这就是早停。criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) best_val float(inf) patience, wait 10, 0 EPOCHS 200 for epoch in range(EPOCHS): model.train() train_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * xb.size(0) train_loss / len(train_loader.dataset) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) val_loss criterion(model(xb), yb).item() * xb.size(0) val_loss / len(val_loader.dataset) if val_loss best_val: best_val val_loss wait 0 torch.save(model.state_dict(), best_lstm.pt) else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break if epoch % 10 0: print(fEpoch {epoch}: train{train_loss:.6f}, val{val_loss:.6f})clip_grad_norm_是 LSTM 训练的标配因为反向传播穿过很多时间步梯度容易爆炸裁剪到 max_norm1.0 能稳住训练。weight_decay1e-5是 L2 正则进一步压过拟合。早停的 patience 设 10意思是验证集连续 10 轮没改善就停避免白跑。保存best_lstm.pt是为了最后用验证集上最好的那个模型去测测试集而不是用最后一轮的模型。3.3 关键参数怎么调参数常用范围调大后果调小后果timestep20~60训练慢、梯度衰减看不到趋势hidden_size32~128过拟合、显存涨欠拟合num_layers1~3过拟合、难训练表达能力不足dropout0.1~0.4欠拟合过拟合lr1e-4~1e-2震荡不收敛收敛太慢我一般先固定 timestep30、hidden_size64、num_layers2、dropout0.2、lr1e-3 跑一版看验证集 loss 曲线。如果训练 loss 一直降、验证 loss 早早抬头就是过拟合加大 dropout 或减 hidden_size。如果两个 loss 都降不下去就是欠拟合加 hidden_size 或层数。这个顺序能省很多试错时间。4. 评估与避坑为什么你的预测结果每次都不一样4.1 反归一化后才能算真实误差模型输出的是归一化后的值必须用 scaler 反变换回真实价格才能算 MAE、RMSE 这些指标。model.load_state_dict(torch.load(best_lstm.pt)) model.eval() preds, trues [], [] with torch.no_grad(): for xb, yb in test_loader: xb xb.to(device) preds.append(model(xb).cpu().numpy()) trues.append(yb.numpy()) preds np.concatenate(preds) trues np.concatenate(trues) # 反归一化close 在第 3 列构造同形状的占位矩阵 def inverse_close(scaled_1d): dummy np.zeros((len(scaled_1d), len(feature_cols))) dummy[:, 3] scaled_1d return scaler.inverse_transform(dummy)[:, 3] pred_price inverse_close(preds) true_price inverse_close(trues) mae np.mean(np.abs(pred_price - true_price)) rmse np.sqrt(np.mean((pred_price - true_price) ** 2)) print(fMAE{mae:.2f}, RMSE{rmse:.2f})反归一化时不能只把预测值单独变换因为 scaler 是按所有特征拟合的必须构造一个和原始特征数相同的矩阵把 close 那一列填进去其余列填 0再inverse_transform最后取第 3 列。这个细节很多人写错导致误差算出来完全不对。4.2 避坑清单五个我踩过的坑现象一每次重新训练测试集误差都不一样差很多。原因PyTorch 默认随机初始化权重DataLoader 的 shuffle 也带随机性加上 dropout 在训练时随机丢弃三次随机源叠加。 解决固定随机种子。在代码开头加torch.manual_seed(42)、np.random.seed(42)DataLoader 的 shuffle 用固定 generator。这样至少同一份代码结果可复现。现象二测试集 MAE 很小实盘一跑完全对不上。原因数据泄漏。最常见的是归一化时用了全量数据拟合 scaler或者滑动窗口切分时训练集和测试集有重叠。另一个隐蔽原因是用了未来数据构造特征比如用当天的最高价算当天的振幅再预测当天收盘。 解决严格按时间顺序切分scaler 只在训练集 fit所有特征只能用当前时刻及之前的数据计算。检查一遍特征工程里有没有shift(-1)这种前视操作。现象三模型预测的曲线比真实曲线滞后一天形状几乎一样。原因LSTM 学成了「昨天收盘价约等于今天收盘价」这个平凡解。股票价格自相关性极强模型发现直接输出上一个值就能把 loss 降得很低于是不再学任何有用模式。 解决改成预测收益率而不是绝对价格或者预测涨跌方向。收益率去掉了价格的自相关模型必须真正学模式才能降 loss。另一个办法是在 loss 里对「预测值等于上一时刻值」这种情况加惩罚。现象四训练 loss 降到很低验证 loss 一直很高。原因过拟合。数据量小、模型容量大、特征多模型把训练集的噪声也记住了。 解决加 dropout、加 weight_decay、减 hidden_size 或层数、减特征数。如果数据实在少可以用 walk-forward 验证代替单次切分让模型在多个时间段上滚动训练和验证。现象五预测结果全是接近的一条直线。原因归一化后目标值集中在很窄的区间或者学习率太小、训练轮数不够模型还没学出变化。 解决检查归一化后 y 的分布如果方差极小考虑改用收益率。调大学习率或增加训练轮数观察 loss 是否还在下降。也可能是模型容量太小加 hidden_size 试试。提示这五个坑里数据泄漏和平凡解是最致命的前者让评估结果虚假后者让模型看起来能跑但毫无价值。跑通流程后先花时间排查这两个再谈调参。5. 进阶技巧用 walk-forward 验证逼近真实表现单次 7:1.5:1.5 切分只能给你一个时间点上的评估股票市场风格会变一个模型在 2020 年表现好不代表 2023 年也好。更接近实盘的做法是 walk-forward 验证用前一段训练、紧接着的一段验证然后窗口整体向前滚动重复多次看模型在不同市场阶段的稳定性。具体做法是设一个训练窗口长度比如 500 个交易日和一个验证窗口长度比如 60 个交易日从数据开头开始每次训练完在验证窗口上评估然后把窗口向前滑动 60 天重新训练。这样你会得到一串评估指标看它们的均值和方差。如果方差很大说明模型对市场阶段很敏感实盘风险高。def walk_forward(data, train_len500, val_len60, step60, timestep30): results [] start 0 while start train_len val_len len(data): train_slice data[start:start train_len] val_slice data[start train_len:start train_len val_len] scaler MinMaxScaler().fit(train_slice) tr scaler.transform(train_slice) va scaler.transform(val_slice) X_tr, y_tr make_windows(tr, timestep) X_va, y_va make_windows(va, timestep) if len(X_tr) 0 or len(X_va) 0: start step continue # 这里复用前面的模型和训练逻辑训练后在验证集上算 MAE # ...训练代码略与第 3 章一致 # mae evaluate(model, X_va, y_va, scaler) # results.append(mae) start step return results这个循环里每次都用当前窗口重新 fit scaler保证没有跨窗口的信息泄漏。跑完你会得到一组 MAE如果它们的波动范围在可接受区间内说明模型相对稳健如果某几个窗口 MAE 突然飙高去看看那段时间市场是不是发生了结构性变化比如政策突变、极端行情这种阶段模型失效是正常的关键是要知道它什么时候会失效。另一个实用技巧是集成多个随机种子的模型。同一份数据用 5 个不同种子各训一个 LSTM预测时取平均。这样能平滑掉单次训练的随机性预测曲线更稳。代价是训练时间乘以 5但在股票这种低信噪比场景里这点代价换来的稳定性是值得的。我自己的习惯是任何时序预测项目跑通单次流程后第一件事就是搭 walk-forward第二件事就是多种子集成。这两步做完你才对模型真实水平有个靠谱的判断。单次切分的漂亮指标看看就好别当真。希望帮到你。本文还有配套的精品资源点击获取