基于Python的LSTM股票预测:从数据准备到防泄漏实战

发布时间:2026/10/12 12:56:05
基于Python的LSTM股票预测:从数据准备到防泄漏实战
简介基于Python的LSTM股票走势预测项目面向Python课程期末大作业、毕业设计及课程设计场景适合初学到中级开发者参考。资源包含完整可运行代码、训练好的模型与数据集覆盖数据预处理、LSTM建模、训练评估及结果可视化等环节可直接部署演示或二次开发。包内含89个文件以py源码、pyc编译文件、txt说明、css/js/html前端页面、xls/xlsx数据文件及sqlite3数据库为主压缩包整体8.88MB目录结构清晰。已有141人学习浏览项目经严格调试可稳定运行。下载后能获得完整项目源码、模型权重、样本数据及界面文件便于理解LSTM在金融时序预测中的实际应用也可作为高分结题展示的重要支撑。1. 基于Python实现LSTM对股票走势的预测为什么九成教程跑通后仍然亏钱用Python实现LSTM对股票走势的预测这个标题在技术社区里能搜到几百篇教程但真正能把预测结果落到实盘验证的十不存一。LSTM确实擅长抓取序列中的时间依赖股票价格天然是带时间戳的序列所以这个组合看起来顺理成章可一旦进入实操数据切分、归一化时机、滞后预测、过拟合这些细节每一个都能让结果从“概念验证”变成“自我欺骗”。这篇文章按我自己实际做过项目的顺序来写从Python环境与行情数据集准备讲起到模型搭建、训练验证最后把踩过的坑一条条拆开。适合刚入门Python、会装numpy但没正经跑过LSTM的读者也适合已经跑通教程、却觉得预测曲线只是把真实曲线复制了一遍的熟手。2. 准备数据从行情接口拉到可喂给LSTM的滑动窗口序列2.1 选数据源免费接口与字段取舍做股票预测第一步不是写模型而是搞定一套干净、连续、可复现的历史行情。常见的免费数据源有三个yfinance 无需注册直接拉美股日线akshare 覆盖A股但接口偶尔调整tushare 数据质量高但部分接口需要积分。我的建议是新手先用 yfinance 跑通全流程因为它返回的 DataFrame 字段规整不需要处理 token 和积分把精力集中在模型本身。pip install yfinance pandas numpy scikit-learn matplotlib tensorflow这行命令把数据获取、数据处理、机器学习、画图和深度学习框架一次装齐。如果你在安装时遇到 numpy 编译报错通常是 Python 版本和包版本不匹配换成 64 位的 Python 3.9 之后再装会省很多事。import yfinance as yf import pandas as pd import numpy as np df yf.download(AAPL, start2015-01-01, end2023-12-31, auto_adjustTrue) df df[[Close]].dropna() close df[Close].values.reshape(-1, 1) print(close.shape, close[:3])参数说明start和end决定回测区间8 年左右的美股日线大约有 2000 个交易日足够训练一个中小型 LSTMauto_adjustTrue拿到的是复权后的价格分红拆股不会在序列里制造假跳水只保留Close是为了让第一条基线模型足够简单后面想加成交量、RSI 这些特征时再扩充。reshape(-1, 1)把一维价格数组变成 LSTM 需要的二维列向量这里漏掉 reshape 是新手最常见的报错来源。如果下载失败先检查本机网络和日期区间实在不行换 akshare 的stock_zh_a_hist接口字段对齐后逻辑不变。2.2 归一化先切分再缩放避免未来信息偷跑LSTM 内部用的是 tanh 激活函数输入会被压到 (-1,1) 区间所以原始价格必须做归一化。但这里有个绝大多数教程都没讲透的坑必须在切分训练集和测试集之后再对训练集做fit_transform测试集只做transform。如果把整个序列一起fit缩放器就提前看到了测试段的最高价和最低价等于考试前偷看了试卷。from sklearn.preprocessing import MinMaxScaler test_ratio 0.2 split_index int(len(close) * (1 - test_ratio)) train_data close[:split_index] test_data close[split_index:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_data) test_scaled scaler.transform(test_data)逻辑说明fit_transform内部先计算训练段的最小值和最大值再把数据映射到 0 到 1 之间测试段只调用transform用的是训练段保存下来的那组 min/max。这样保证了模型在验证时接触到的数值变换规则全部来自历史信息。代价是如果测试段价格超过了训练段的历史范围缩放后会被截断在 0 或 1这是标准化策略的一个边界问题后面避坑章会展开讲。后续反归一化时也要用同一个scaler的inverse_transform千万不能新建一个。2.3 构造滑动窗口lookback 是超参数而不是常数LSTM 不能一次性吞下整段历史它靠“滑动窗口”把价格切成一个个等长的样本。每个样本由连续的lookback个交易日价格组成预测目标是窗口后一天的收盘价。lookback这个值的选取关系到模型能记忆多长的依赖关系60 是个常见的经验起点但一定要当作超参来调后面会讲。def make_sequences(data, lookback60): X, y [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i]) y.append(data[i]) return np.array(X), np.array(y) X_train, y_train make_sequences(train_scaled, lookback60) X_test, y_test make_sequences(test_scaled, lookback60) print(X_train.shape, y_train.shape)make_sequences的逻辑是从第lookback个位置开始遍历取它前面 60 个点作为输入 X当前点作为标签 y。切片data[i - 60:i]是左闭右开恰好包含 60 个元素。最终X_train的形状是(样本数, 60, 1)这个三维张量就是 LSTM 的标准输入格式第一维是样本数第二维是时间步第三维是特征数。这里用列表收集再统一转np.array比预分配数组再填充更不容易出错而且np.array会自动推断 dtype如果你后续要喂给 TensorFlow建议再显式转一下X_train.astype(np.float32)可以避免部分 GPU 环境下的数据类型报错。3. 搭建LSTM模型Keras中把价格序列变成预测结果的网络结构3.1 网络结构两层LSTM加Dropout的理由模型骨架我用 Keras Sequential 搭它对这个任务足够直观。两层 LSTM 加一层 Dropout、再接一个全连接输出层是时间序列预测里最不容易翻车的组合。单层 LSTM 表达力不够三层以上在股票这种低信噪比数据上几乎必然过拟合两层是多数场景的甜点区。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense from tensorflow.keras.optimizers import Adam model Sequential() model.add(LSTM(50, return_sequencesTrue, input_shape(lookback, 1))) model.add(Dropout(0.2)) model.add(LSTM(50, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizerAdam(learning_rate0.001), lossmean_squared_error, metrics[mae])参数说明第一个LSTM(50, return_sequencesTrue)表示这一层有 50 个记忆单元并返回每个时间步的隐藏状态这样才能喂给第二层 LSTM第二个LSTM(50, return_sequencesFalse)只返回最后一个时间步的输出直接对接Dense(1)输出单值预测。input_shape(lookback, 1)里的 1 是特征数如果之后加了成交量、开盘价等多特征这里要改成对应的数字。Dropout(0.2)在每层 LSTM 之后随机丢弃 20% 的神经元输出是抑制过拟合的主要手段比例设在 0.2 到 0.3 之间比较稳妥太高会让训练收敛变得很慢。3.2 损失函数为什么回归用MSE而不是准确率你可能会想预测涨跌为什么不直接用准确率当损失函数。原因是准确率是离散的、不可导的梯度下降根本没法优化它而 MSE 是连续可导的计算每个样本预测值与真实值的平方差越接近真实值误差越小。实际操作里我会把loss设为mean_squared_error同时用metrics[mae]观察平均绝对误差两个指标一起看能发现模型是不是在个别极端值上崩掉。还有个细节股票预测里 MSE 会放大那些大涨大跌日的误差因为价格波动大平方项非常敏感。这会引导模型更加保守宁愿预测得平庸也不要猜错大方向。如果你希望模型对极端行情更有反应可以换mean_absolute_error做损失或者用 Huber loss 在两者之间折中。首次跑通建议保持默认 MSE后面调优时再替换。3.3 训练管线早停、模型保存与关闭shuffle数据准备好了模型也搭好了还不能直接model.fit。时间序列训练有三件事必须做早停防止过拟合、回调保存最优权重、以及一个容易被忽略的shuffleFalse。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) checkpoint ModelCheckpoint(lstm_stock.keras, monitorval_loss, save_best_onlyTrue) history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs100, batch_size32, shuffleFalse, callbacks[early_stop, checkpoint], verbose1 )参数说明patience10表示验证集损失连续 10 个 epoch 没有改善就停止训练restore_best_weightsTrue让模型回到验证集表现最好的那组权重避免最后几个 epoch 的过拟合权重被保留。ModelCheckpoint把最优模型落盘到lstm_stock.keras下次直接load_model加载不用重新训练。shuffleFalse极其重要默认的shuffleTrue会打乱训练样本而 LSTM 的样本是按时间顺序截出来的一旦打乱模型学到的时序依赖关系会瞬间崩溃。batch_size32是 GPU 显存和训练速度的折中如果你用 CPU 跑可以把 batch 调小到 16内存压力更小。4. 训练与验证如何判断模型真的“学到”而不是“记住”4.1 读训练曲线过拟合的第一现场训练完成后第一件事不是看预测图而是看损失曲线。把history里的训练集 loss 和测试集 loss 画在一起能直观判断模型是不是在死记硬背。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss, linewidth1) plt.plot(history.history[val_loss], labelval_loss, linewidth1) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.show()正常情况是两条线都下降train_loss 略低于 val_loss两者差距保持稳定。如果 train_loss 一路降到接近 0而 val_loss 在某个 epoch 后反弹这就是经典的过拟合模型把训练样本的噪声都背下来了对没见过的数据毫无泛化能力。这时候回头调 Dropout、降低层数或增加早停的 patience 都没用要回到数据量和模型容量上去找平衡。如果两条线都在高位震荡不下降优先怀疑学习率——0.001 不行就试 0.0005 或 0.0001。4.2 预测与反归一化把数值还原成真实价格模型输出的是 0 到 1 之间的缩放值必须反归一化才能和真实价格对比。这一步出错的人不在少数最常见的错误是忘记y_test需要 reshape 成二维。pred_scaled model.predict(X_test, verbose0) pred scaler.inverse_transform(pred_scaled) y_test_orig scaler.inverse_transform(y_test.reshape(-1, 1))y_test从make_sequences出来形状是(样本数,)而scaler.inverse_transform要求输入是二维(样本数, 1)所以必须先reshape(-1, 1)。pred_scaled本来就有第二维可以直接反变换。这里有个真实存在的边界情况如果测试段价格低于训练段最低价反归一化后会出现“压着历史最低价走”的预测这不是模型学会了什么支撑位而是缩放函数把数值截断了。遇到这种情况说明训练段覆盖的价格区间不够完整应该加长历史数据或者改用 StandardScaler。4.3 评估指标方向准确率比RMSE更重要回归任务常用 RMSE 和 MAE 评估绝对误差但做股票预测我还会额外看一个方向准确率预测的涨跌方向与实际涨跌方向是否一致。原因很简单股价 180 元 RMSE 是 2 元看着不错可如果方向全错交易照样亏钱。from sklearn.metrics import mean_squared_error, mean_absolute_error rmse np.sqrt(mean_squared_error(y_test_orig, pred)) mae mean_absolute_error(y_test_orig, pred) real_diff np.diff(y_test_orig.flatten()) pred_diff np.diff(pred.flatten()) direction_acc np.mean(np.sign(real_diff) np.sign(pred_diff)) print(fRMSE{rmse:.4f}, MAE{mae:.4f}, DirectionAcc{direction_acc:.2%})同比时一定要加一个基线如果直接用“预测等于昨天的价格”这种极简策略它的 RMSE 是多少方向准确率又是多少。LSTM 模型如果连这个基线都赢不了说明模型没有学到超越随机游走的信息。direction_acc 的数学期望是 50%所以看到 55% 以上才算有统计意义上的预测能力而 RMSE 单独看很容易骗人因为股价本身波动大绝对值误差天然就高。5. 避坑与常见问题数据泄漏、滞后预测与过拟合的血泪经验5.1 数据泄漏Scaler提前fit在全体数据上现象验证集 loss 低得离谱回测曲线几乎完美贴合真实价格一到新数据就翻车。原因不少教程先把整个价格序列和 MinMaxScaler 的 fit 做完再做 train/test 切分。scaler.fit(全体数据)时测试段的最高价和最低价已经参与了 min/max 计算模型在训练阶段就用到了未来的统计信息。这不是模型预测能力强而是数据泄漏。更隐蔽的形式是把scaler.fit_transform应用在全量数据后再手动构造滑动窗口检测起来很困难。解决任何涉及全局统计量的预处理包括 MinMaxScaler、StandardScaler、PCA 拟合都必须严格遵守先切分、再 fit 训练段、然后 transform 测试段的顺序。如果用了特征工程时序类的滚动均值可以用历史窗口计算但像布林带这类需要全序列统计的特征同样要用训练段的参数去套测试段。我在项目里会用管道把 scaler 和模型包在一起防止不同环节的人误用。5.2 预测曲线整体滞后一拍现象预测曲线形状和真实曲线高度相似但整体向右平移了一天像是抄作业抄慢了半拍。原因股票价格接近随机游走MSE 损失下模型发现“最优预测”就是上一时刻的价格因为相邻价格差异在数值上很小复制上一天能把损失压到极低。LSTM 学到了这个捷径并没有真正抓住趋势。解决很多人因此断言 LSTM 对股票无效但更合理的说法是“用收盘价预测收盘价”这个任务设置本身有问题。改法有两种一种是标签从收盘价改成未来收益率(close[t1] - close[t]) / close[t]模型不再需要预测绝对价格另一种是加入更多特征比如成交量、日内高低价差、RSI迫使模型寻找价格之外的驱动信号。同时要把方向准确率作为主要指标RMSE 降低但方向准确率只有 50% 是完全没有意义的。5.3 时序样本乱序shuffle的默认值现象训练过程 val_loss 忽高忽低收敛非常困难模型表现毫无规律。原因Keras 的model.fit默认shuffleTrue它会把样本随机打乱再分批训练。对普通图像分类问题这没问题但对时间序列打乱后的样本破坏了时间上下文LSTM 学的依赖关系全部错位验证集指标自然不稳。解决显式设置shuffleFalse。我之前为了图省事没写这个参数结果同样的数据换了几组随机种子指标忽上忽下排查了很久才发现是默认打乱在捣乱。如果你确实需要随机性来防止过拟合应该通过 Dropout 来实现而不是打乱时间顺序。5.4 lookback拍脑袋超参比想象中更敏感现象把 lookback 从 60 改成 30模型成绩骤降从 30 改成 90 又回升。每次调整都有“新的发现”但更换股票代码后结论又变了。原因lookback 本质是让模型回顾多长的历史它和标的的波动周期强相关。日内交易为主的价格序列60 分钟级别的 lookback 意义不大日线级别的趋势股60 个交易日可能刚好覆盖一轮中期趋势。没有理论公式能算出最优值只能实验。解决把 lookback 30、60、90 当成一个小的网格实验固定其他超参分别训练三组模型比较验证集的方向准确率和 RMSE。Lookback 过大还会增加 LSTM 的记忆负担并放大过拟合不是越长越好。我自己的习惯是把 60 作为起点每次只变一个变量用早停后的 val_loss 排序而不是靠手感拍板。5.5 回测与实盘次日开盘价与手续费的真实差距现象历史回测年化收益高得吓人实盘一跑就亏损“模拟盘大神”实盘稀烂。原因回测里用了当日收盘价作为成交价但模型的预测结果在当日收盘后才产生你根本不可能用这个价格成交。真实成交价应该是次日开盘价两者之间差一个跳空缺口。再加上手续费、滑点回测收益率会显著缩水。解决预测标签改成次日收盘价或次日开盘价评估时用次日开盘价计算收益并把单边手续费 0.1% 到 0.3%、滑点成本计进去。再进一步别用收盘价预测改成用收益率预测加阈值过滤只有预测收益超过手续费成本时才触发买入信号这样模型才算真正从“预测走势”走向“可执行策略”。6. 进阶用法滚动预测与方向准确率验证前面模型在测试集上是逐点预测每个样本都输入真实历史窗口没有用到预测值本身。但实际使用场景是站在今天预测未来 N 天这时候需要滚动预测把每步预测结果接回窗口末尾再预测下一步。def rolling_forecast(model, last_seq, steps10): current last_seq.copy() preds [] for _ in range(steps): next_val model.predict( current.reshape(1, lookback, 1), verbose0 ) preds.append(next_val[0, 0]) current np.roll(current, -1, axis0) current[-1, 0] next_val[0, 0] return scaler.inverse_transform( np.array(preds).reshape(-1, 1) )np.roll把整个窗口往前平移一位最前面的旧价格被丢弃新预测值放到窗口末尾。这样滚动 10 次就得到未来 10 天的预测序列。必须清醒认识到误差会随步数累积第 2 天的预测依赖第 1 天的预测值第 3 天依赖前两天的误差叠加通常滚动到第 5 步之后预测就只剩趋势方向参考价值绝对价格几乎没有意义。所以滚动预测最大的用途是验证方向准确率而不是拿预测价格直接挂单。验证技巧上我习惯用 walk-forward 方式把数据切成多个连续的时间段模型在第一个训练段学完预测下一个验证段然后扩展训练段再预测下一步反复滚动。这种方法比一次性 train/test 切分更接近真实交易环境能暴露模型在不同市场状态下的稳定性。最后要做一个基线对比最简单的是“今天的价格就是明天的预测价”如果滚动预测的方向准确率在多个时间窗口里都跑不赢 50% 这条线我会直接放弃这个标的而不是继续调参。我早期做过一个类似的收盘价预测项目回测 RMSE 很低方向准确率只有 52%扣掉手续费后基本白忙一场。后来我换成了收益率预测加方向过滤才让模型真正有了交易参考价值。这个方向能不能赚钱不取决于 LSTM 本身而取决于你怎么定义标签、怎么防止数据泄漏、怎么诚实评估。拿着这套流程从 AAPL 开始跑一遍你会比市面上九成教程都要更接近真实。希望帮到你。本文还有配套的精品资源点击获取