
简介基于LSTM的股票数据预测实战项目面向希望入门深度学习与时间序列分析的Python开发者覆盖从历史行情数据清洗、特征构建到LSTM模型训练、预测与评估的完整流程。资源包共18个文件包含5个Python脚本分别基于TensorFlow、Keras和PyTorch搭建模型并提供股票CSV原始数据、requirements依赖清单、注意事项说明以及8张预测结果对比图整体仅540KB结构清晰便于直接运行或二次修改。项目涉及数据归一化、滑动窗口序列生成、多层LSTM与Dropout防过拟合、MSE/RMSE评估等关键环节可帮助读者理解股票价格趋势预测的实现思路。由于同一数据在三种框架下均有实现还能对照不同工具库的建模差异与预测效果。已有1077人学习使用适合课程设计、毕业设计或LSTM入门练手。1. 基于LSTM的股票数据预测先泼一盆冷水再动手如果你搜“基于LSTM的股票数据预测Python完整源码和数据”大概率是想把K线塞进神经网络让模型告诉你明天是涨是跌。我的建议是这件事能跑通但跑通和赚钱是两回事。LSTM在序列建模上确实比普通全连接网络强它能记住几十步之前的量价形态这是做股票数据预测最常用的理由。但股票数据信噪比极低你花两周调出来的模型回测曲线可能比真实净值还漂亮一上实盘就翻车。这篇文章面向的是已经会写Python、想用LSTM做一次完整的时间序列预测实验的从业者。我会给出一套能直接复现的代码路径数据从哪来、窗口怎么切、归一化怎么做、模型参数怎么定以及我在实际跑这类任务时踩过的坑。目标不是让你一夜暴富而是让你在最短时间内跑通一条完整的实验链路并知道每个环节的边界在哪。2. 为什么是LSTM从RNN的梯度问题到记忆门控2.1 RNN的短期记忆瓶颈和LSTM的出现股票预测本质上是时间序列预测用历史序列预测未来序列。最朴素的想法是用循环神经网络让每个时刻的隐状态携带之前的信息。但RNN在反向传播时梯度要沿着时间步连乘序列一长梯度要么爆炸要么消失。爆炸还好办梯度裁剪能压住消失才要命网络根本学不到几十步之前发生了什么。LSTM长短期记忆网络在1997年由Hochreiter和Schmidhuber提出核心改动是引入了一条“细胞状态”的传送带。这条传送带上的信息更新由三个门控制遗忘门决定丢掉多少旧记忆输入门决定新信息写入多少输出门决定放出多少给当前隐状态。因为细胞状态的梯度传播路径是线性的加和而非连乘梯度消失被大幅缓解模型才能学到“三根大阳线之后往往还有惯性”这类长程形态。2.2 股票数据里到底哪些东西值得被LSTM记住做股票数据预测LSTM学的不是“明天涨还是跌”这个离散标签而是“给定过去N天的量价序列未来1天的收盘价相对今天的变化”。比如你今天输入过去20天的开高低收、成交量模型输出的是明天收盘价的具体数值。这个数值再和今天的收盘价比得到涨跌幅再按阈值转成信号。我见过不少第一次做这个项目的人直接把原始收盘价丢进LSTM结果Loss降得很快但预测曲线是整体平移的——模型学到了“明天的价格约等于今天的价格”这在平稳段确实误差小一到拐点就露馅。所以我们要做的不是教LSTM背价格而是教它学变化用涨跌幅、收益率、量价关系这些相对量作为特征比用绝对价格稳定得多。3. 数据准备与样本构造最小可用代码3.1 数据获取与字段清洗这个项目最常见的数据来源是tushare、akshare这类开源金融数据接口下载日线行情字段包括date、open、high、low、close、volume。注意先按日期排序否则后续滑窗切片全乱套。我一般会把停牌日、全零成交量的行直接剔除避免模型把无量涨停当成正常样本。import pandas as pd # 假设已经通过 tushare/akshare 拉到原始日线数据 df pd.read_csv(stock_daily.csv, parse_dates[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) # 剔除成交量为0的行避免停牌或无量日干扰 df df[df[volume] 0].reset_index(dropTrue) # 构造相对量特征涨跌幅、振幅、量比 df[ret] df[close].pct_change() df[amplitude] (df[high] - df[low]) / df[close] df[volume_ma5] df[volume].rolling(5).mean() df[vol_ratio] df[volume] / df[volume_ma5] # 删除因滚动计算产生的NaN行 df df.dropna().reset_index(dropTrue)这段代码的逻辑很简单把原始行情转成模型真正需要的特征列。pct_change计算相对昨收的涨跌幅这是最核心的预测目标之一amplitude用当日振幅相对收盘价的比值刻画波动强度vol_ratio衡量当天成交量是否异动。这些相对特征在数值尺度上远小于绝对价格训练时更容易收敛。参数说明rolling(5)的窗口按你自己的交易周期改做短线用3做波段用10。3.2 滑窗切分与归一化顺序不能乱LSTM的输入是三维张量样本数时间步长特征数。时间步长seq_len决定模型看多长的历史我常用的默认值是20个交易日约等于一个自然月。切窗时要用滑窗法而不是随机打乱——金融时间序列一旦打乱未来信息就泄漏到过去了。import numpy as np def build_sequences(features, seq_len20): X, y [], [] for i in range(seq_len, len(features)): X.append(features[i - seq_len:i]) # 预测目标未来1天的收盘价涨跌幅与特征中的ret对齐 y.append(features[i, 0]) # 假设第一列是ret return np.array(X), np.array(y) # 特征列只保留相对量第一列必须是ret便于上面取y feature_cols [ret, amplitude, vol_ratio, close_ma5] # close_ma5 需要在上一步构造这里省略 features df[feature_cols].values X, y build_sequences(features, seq_len20) # 按时间顺序切分前70%训练后30%测试绝不随机抽样 split int(len(X) * 0.7) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] # 归一化只用训练集的统计量测试集不参与拟合 from sklearn.preprocessing import StandardScaler scaler_x StandardScaler() X_train_flat X_train.reshape(-1, X_train.shape[-1]) scaler_x.fit(X_train_flat) X_train_norm scaler_x.transform(X_train_flat).reshape(X_train.shape) X_test_norm scaler_x.transform(X_test.reshape(-1, X_test.shape[-1])).reshape(X_test.shape) # 归一化目标值也单独做一个scaler scaler_y StandardScaler() y_train_norm scaler_y.fit_transform(y_train.reshape(-1, 1)).ravel() y_test_norm scaler_y.transform(y_test.reshape(-1, 1)).ravel()这里最容易被忽略的是归一化的时机。StandardScaler的fit只能跑在训练集上测试集只做transform。如果你对全量数据fit测试集的均值和方差已经被模型“偷看”了回测指标会虚高。另一个细节是目标值y也做了标准化预测完再inverse_transform回来否则Loss数值会很奇怪。滑窗的seq_len20不是死的后面会讲怎么调。4. 建模、训练与预测回放参数照抄可以理解再改4.1 用PyTorch搭一个两层的LSTM模型完整源码里最核心的就是模型定义。我用PyTorch而不是Keras因为中间层拿隐状态做特征分析更方便。模型结构一层LSTM加一层全连接。输入维度是特征数隐层维度常见取32或64层数建议不超过2层——股票数据本身噪声极大层数一深就过拟合训练集Loss漂亮测试集直接崩。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.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) # 取最后一个时间步的隐状态做预测 out out[:, -1, :] return self.fc(out).squeeze(-1)逻辑说明batch_firstTrue让输入张量直接是(batch, seq_len, features)的格式省去转置步骤。out[:, -1, :]取的是序列最后一个时间步的输出这符合“用过去20天预测第21天”的设定。dropout0.2放在两层LSTM之间防止隐层维度到64后过拟合。参数说明hidden_size64在数据量只有几千条时已经够用如果训练集超过5万条可以提到128但训练时间会翻倍。4.2 训练循环Loss选MSE还是Huber训练时损失函数我用Huber Loss而不是纯MSE。股票数据里有极端行情一根涨跌幅超过5%的大阳线如果用MSE它的平方误差会把Loss顶得极高模型被迫去拟合这根异常K线。Huber Loss在误差小于阈值时退化为MSE大于阈值时变成线性对大 outlier 不那么敏感。import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader X_train_t torch.tensor(X_train_norm, dtypetorch.float32) y_train_t torch.tensor(y_train_norm, dtypetorch.float32) X_test_t torch.tensor(X_test_norm, dtypetorch.float32) y_test_t torch.tensor(y_test_norm, dtypetorch.float32) train_dataset TensorDataset(X_train_t, y_train_t) train_loader DataLoader(train_dataset, batch_size64, shuffleFalse) model LSTMPredictor(input_sizeX_train_norm.shape[-1]) optimizer optim.Adam(model.parameters(), lr0.001) loss_fn nn.HuberLoss(delta1.0) epochs 50 for epoch in range(epochs): model.train() total_loss 0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() # 梯度裁剪防止个别样本把梯度带飞 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: print(fEpoch {epoch1}/{epochs}, Loss: {total_loss/len(train_loader):.4f})这里有两个细节值得解释。shuffleFalse必须保留虽然每个样本是独立滑窗但相邻样本共享大量时间步打乱后训练分布和真实推理场景不一致。clip_grad_norm_是梯度裁剪我在股票预测里从不省略——某些批量样本里出现极端涨跌时梯度范数会突然暴涨裁到1.0能保证训练稳定。lr0.001是Adam的常用默认值如果你发现Loss震荡不降优先降到0.0003而不是动网络结构。4.3 测试集预测与逆归一化model.eval() with torch.no_grad(): y_pred_norm model(X_test_t).numpy() # 逆归一化还原成真实涨跌幅 y_pred scaler_y.inverse_transform(y_pred_norm.reshape(-1, 1)).ravel() y_true scaler_y.inverse_transform(y_test_norm.reshape(-1, 1)).ravel() # 方向准确率预测涨跌方向和实际是否一致 direction_acc np.mean(np.sign(y_pred) np.sign(y_true)) print(fDirection Accuracy: {direction_acc:.2%})预测回放时我最先看的是方向准确率而不是MSE。MSE低可能只是模型学会了“预测值贴近0”因为大多数交易日涨跌幅本来就接近0蒙一个0也能拿到很低的MSE。方向准确率若在52%以上说明模型确实学到了微弱的市场惯性低于50%说明这个时间段预测无意义可能换一段历史重新测。5. 避坑手册股票数据预测最容易翻车的5个问题5.1 预测曲线整体平移模型在偷懒现象测试集的预测值曲线和真实曲线几乎平行只是滞后一天。原因LSTM学到的是“明天的价格约等于今天的价格”这在平稳行情里Loss很低但一到拐点就完全失效。解决把预测目标从绝对价格改成相对涨跌幅或者在特征中去掉绝对价格列只保留变化量。我遇到这个现象时第一反应是检查训练集里是不是绝对价格特征占主导。5.2 归一化时不小心用了全量数据现象回测结果好得离谱训练集和测试集都接近90%准确率。原因StandardScaler在全量数据上fit测试集的均值方差被模型提前看到产生了信息泄漏。解决严格只用训练集fit测试集只transform。我把这段代码写在所有脚本的固定位置当作检查点——每次实验跑之前先看这个逻辑有没有被改坏。5.3 滑窗样本之间重复度过高验证集指标虚高现象测试集和训练集的预测效果差不多但实盘表现非常差。原因滑窗生成的相邻样本共享绝大部分时间步测试集和训练集在时间上有重叠相当于模型“见过”测试集附近的行情。解决按时间分段验证例如训练集用前5年验证集用接下来1年切分处留出至少seq_len的间隔保证验证集样本完全没参与训练。5.4 Loss降到很低但方向准确率不到50%现象训练收敛MSE只有0.0001但方向准确率在50%附近徘徊。原因模型学会了预测“涨跌幅接近0”这对MSE来说是最优解因为大部分日子确实变化不大。但方向准确率看的是正负号一个接近0的预测符号随机准确率自然上不去。解决把Loss函数换成交叉熵预测目标改成三分类涨/平/跌或者直接以方向准确率作为早停指标。这个坑我栽过很多次现在每次训完都会同时打印MSE和方向准确率对照着看。5.5 随机种子没固定同一份代码跑两次结果完全不同现象昨天跑的方向准确率58%今天重跑变成了49%模型结构完全没动。原因PyTorch的权重初始化和DataLoader的线程调度默认带随机性训练结果不完全可复现。解决在脚本最前面固定随机种子。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False我习惯把这段放在所有导入之后的第一行。股票预测实验需要反复对比参数没有固定随机种子你根本分不清效果提升来自参数改动还是运气。6. 从“能预测”到“能决策”滚动更新与信号验证单次训练完模型只能说明历史拟合能力真正要贴近实战得做“滚动时间序列验证”每训练一次预测未来N天然后窗口向后平移用新数据重新训练。这个过程的代码量不大但它是从“跑通demo”到“能讲清楚模型价值”的关键一步。# 滚动验证伪代码每次训练集推进30天预测未来5天 lookback_train 1200 # 用过去1200个交易日训练 gap 30 # 每30天重训一次 pred_days 5 # 预测未来5天 for end in range(lookback_train, len(features), gap): train_part features[end - lookback_train:end] test_part features[end:end pred_days] # 训练并预测记录每天的预测值和真实值 # 注意每次都要重新fit scaler只用train_part的统计量滚动验证之外我建议把所有预测结果和真实涨跌幅存下来画一张散点图横轴是预测值纵轴是真实值。如果散点图是一条45度角的直线说明模型拟合得很好如果是一团云说明预测值基本没有信息量。另一个我常用的验证指标是IC信息系数即预测值和真实值的秩相关系数。IC均值超过0.03在量化领域已经算有价值的信号低于0.01说明这个模型只能当研究样本不能进决策流程。我做股票预测项目的最大教训是LSTM很容易做出“看起来对”的曲线但很难做出“真正可用于决策”的信号。如果你跑完这套代码发现方向准确率只有52%不要急着加网络层数先检查是不是数据泄漏再考虑是不是该把预测目标从回归换成分类。这套实验链路能帮你把“好奇心”转化成“对数据的理解力”这比一个好看的回测曲线值钱得多。希望帮到你。本文还有配套的精品资源点击获取