恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
LSTM股票预测源码全解析:从数据清洗到模型避坑
首页
资讯中心
/
LSTM股票预测源码全解析:从数据清洗到模型避坑
LSTM股票预测源码全解析:从数据清洗到模型避坑
发布时间:2026/10/18 18:17:30
简介基于LSTM神经网络预测股市的Python完整项目资源包面向机器学习初学者、金融数据分析爱好者以及需要完成课程设计的高校学生。项目使用TensorFlow/Keras搭建LSTM模型对股票历史序列进行训练与预测代码注释详尽结构清晰解压后即可正常运行也支持二次开发。资源包共89个文件包含24个Python源码、预训练模型文件、股票数据集、说明文档及可视化图表等压缩包大小仅8.87MB便于快速下载与本地部署。已有793人学习下载具有较高的参考与实战价值。通过该项目读者可掌握时间序列数据预处理、LSTM模型构建、训练评估与预测可视化的完整流程并可直接复用其工程代码是课程设计或期末大作业的优质参考。1. 先看明白这个LSTM股票预测源码包到底在解决什么问题一个 Python基于LSTM模型实现预测股市源代码模型数据集 的压缩包几乎是每个接触股票时间序列的人都会遇到的第一份复现材料。它把三样东西打包在一起能直接跑的 Python 代码、已经训练好的 LSTM 模型文件以及一份或多份历史行情数据集。对还没搭过深度学习环境的人来说它的价值是省掉从零写数据处理、搭模型、调参的路直接跑通一条用历史价格预测未来价格的完整流水线。但我要先泼一盆冷水这类包跑通很容易靠它赚钱是另一回事。它的真实用途是学习链路和验证想法不是躺赢策略。适合两类人一是想搞清楚 LSTM 时间序列建模从数据到模型每步在做什么的初学者二是想拿现成源码做二次开发、加自己的特征和回测逻辑的从业者。这篇文章会从原理、数据处理、模型搭建到避坑把整条链路拆开讲透。2. 用LSTM预测股价的原理与选型为什么ARIMA不够LSTM也非万能2.1 LSTM的长期记忆机制与股价时间序列的匹配点LSTM长短期记忆网络是循环神经网络的一种变体核心创新是引入了细胞状态加上输入门、遗忘门、输出门三个门控结构。通俗理解遗忘门决定要不要丢掉上一时刻带来的旧信息输入门决定当前时刻的信息有多少写进长期记忆输出门决定最终对外输出什么。正因为有了这条通路LSTM 能保留几十步之前的趋势信息而不像普通 RNN 那样在反向传播时梯度快速消失。股票日线序列恰好表现出两类特征一是短期惯性今天涨跌和最近几天高度相关二是中期状态切换比如从上升趋势转向震荡。LSTM 的门控机制理论上可以同时容纳这两种依赖这也是它被大量用在金融预测上的直接原因。但需要清醒认识到LSTM 最初是为语言序列设计的语言有明确的语法边界而股票数据是高度噪声的非平稳序列LSTM 并没有任何针对金融的先天优势它只是比普通 RNN 更能扛长依赖。经常有人问我LSTM 预测股价是不是玄学。我的回答是门控机制确实给了它建模时间依赖的能力但模型把所有变量之间的关系都藏在高维权重里你无法像看回归系数那样解释每个特征的影响。正因为有这层黑匣子落地前必须用严格回测证明它的预测力而不是靠看曲线拟合得漂不漂亮。从选型看ARIMA 类模型对单变量平稳序列有效但面对突变、波动率聚集和交叉特征时需要人工做差分、定阶、外生变量处理工程链路很长。LSTM 可以直接吃多特征窗口自动学习变量间交互。我一般这样判断样本量超过几千条、特征不止价格还包括量比、成交额等维度时优先试 LSTM如果数据量很小或者序列已被处理得很平稳ARIMA 更省事也更可解释。两者是互补不是替代关系。下表是我在项目选型时常用的对比口径对比项ARIMALSTM数据要求单变量、平稳性要求高多变量、可处理复杂非线性长依赖建模靠差分与滞后阶数显式建模靠门控自动记忆可解释性较高有明确系数黑匣子几乎不可解释训练成本低秒级高需 GPU 或较长 CPU 时间典型失败模式结构突变后预测失效数据泄漏时回测虚高实盘崩溃2.2 模型输入三维张量时间步、特征维度与滑窗构造在 Keras 里LSTM 输入张量必须形如 (样本数, 时间步, 特征数)。时间步指每次预测用回看多少天特征数指每个交易日用多少列。假如用前20天收盘价预测下一天特征只有一列收盘价一个样本的 shape 就是 (20, 1)。1000 天数据window20滑窗可产生约 980 个样本训练张量形如 (980, 20, 1)。同类源码包里滑窗构造有三种要分清用途。第一种是单步预测只预测下一个交易日结构最简单适合入门代码包里最常见。第二种是多步递归预测模型还是单步但预测完把结果放回输入窗口继续预测下一天这种做法好处是复用同一个模型坏处是误差像滚雪球一样累积。第三种是直接多步输出模型的最后一层直接输出未来5个或10个值训练时需要构造多目标标签代码量和训练难度都更高。多层特征输入时可以这样扩展features df[[close, volume]].values # 两列特征 scaled_features scaler.fit_transform(features) X_all, y_all build_sequences(scaled_features, window20) print(X_all.shape) # (样本数, 20, 2)逻辑说明build_sequences 接收二维数组后切片 X.append(values[i-window:i]) 会保留最后一维的特征数所以输出的三维张量第三维自动变成2。参数说明特征列的顺序会影响模型行为把价格放第一列、成交量放第二列是常见做法但要保证训练和预测时列顺序完全一致否则模型拿到的输入分布就变了。拿到源码包后不要急着训练第一件事找到构造滑窗的函数看它用的是哪种模式。窗口大小建议从20到30起步对应约一个交易月设成5到10模型很容易被单日噪声带偏设成60以上样本数骤降且容易记住不规律的长周期波动。窗口大小是整个 Pipeline 里最值得先调的超参数它同时影响样本数量和模型记忆长度。2.3 训练/验证/测试集切分如何避免未来函数泄露时间序列建模不能像分类模型那样随机打乱切分。随机切分会让训练集里混入测试段的信息模型在训练阶段就看到未来的统计特征回测指标自然虚高。时间序列的标准动作是按时间顺序切三段前60%训练中间20%验证最后20%测试。# 切分必须在保持时间顺序的前提下进行 split_1 int(len(scaled_data) * 0.6) split_2 int(len(scaled_data) * 0.8) train_data scaled_data[:split_1] val_data scaled_data[split_1:split_2] test_data scaled_data[split_2:]参数说明0.6 和 0.8 是常见起步点。数据只有一年的建议训练占0.7、验证0.15、测试0.15数据有十年的建议测试固定留最近两年不再按比例。注意先切分后构造滑窗与先构造滑窗后切分得到的样本数略有差异前者更严格能保证测试集样本的输入窗口不会跨到训练段时间范围之外。我的习惯是先按位置切分再在各段内部独立构造样本。验证集还有一个容易被忽视的约束它只能用来做早停和比较模型不能反复拿来调参调上几十次。否则验证集的分布特征会通过你的手动调参间接写进模型选择过程最终测试集的可信度也会下降。另外未来函数泄露不只指显式地偷看未来数据还包括归一化阶段用了全量数据的上下界以及特征里不小心用了次日数据。后面的章节会在具体代码里反复提到这个点因为它是源码包翻车率最高的环节。提示切分、归一化、构造滑窗这三步的顺序直接决定你的回测是否存在信息泄露值得在动手前想清楚。3. 数据集处理实操从日线行情到LSTM能直接吃的三维张量3.1 清洗异常数据去重、补全停牌与复权处理拿到源码包里的数据集第一反应不是跑模型而是把 CSV 的列名和样本量搞清楚。日线数据最少应该包含五列交易日期、开盘价、最高价、最低价、收盘价最好再带成交量。任何一个字段缺失后面的滑窗构造都会出问题而有些经过转手的行情文件会自带脏数据最常见的有三类。第一类是同一交易日期出现多条记录原因可能是盘前盘后修正或者除权事件处理方式是按交易日期去重并保留最后一条。第二类是停牌日期的 NaN 值常见做法是用前一个交易日的收盘价向前填充同时把成交量为0的异常行剔除。第三类是未复权价格如果标的在区间内有大比例分红送转未复权的收盘价会出现向下的跳空LSTM 会把这种人为跳变当成真实下跌预测未来时自然出错。我自己遇到这种情况通常选择直接用复权后的收盘价训练或者至少在代码里判断数据源是否已含复权因子。df pd.read_csv(stock_data.csv, parse_dates[trade_date]) df df.sort_values(trade_date).drop_duplicates(subsettrade_date).reset_index(dropTrue) df[close] df[close].ffill() df df[df[volume] 0].reset_index(dropTrue) print(清洗后行数:, len(df))逻辑说明先按日期排序并去重确保时间序列严格递增再用 ffill 填充连续停牌期的收盘价最后过滤成交量为0的无效交易记录。参数说明parse_dates 能把日期列解析成 datetime后续排序和画图都更方便drop_duplicates 默认保留第一条如果你希望保留除权后的记录要在函数前先按报告期排序。ffill 只适合短暂停牌如果某个标的停牌超过一个季度直接填充会把模型带到沟里去此时应该在特征里增加一个是否停牌标记而不是硬填。3.2 用滑窗把收盘价变成监督学习样本的代码清洗完后下一步是把价格序列转换成输入窗口目标值的监督样本。这个过程在源码包里一般是一个循环函数核心代码很短但索引边界最容易抄错。def build_sequences(values, window20): X, y [], [] for i in range(window, len(values)): X.append(values[i - window:i]) y.append(values[i]) return np.array(X), np.array(y) window 20 X_all, y_all build_sequences(scaled_close, window) print(X_all.shape, y_all.shape)逻辑说明对第 i 个位置取它前面的 window 个值作为输入当前位置的值作为标签i 从 window 开始保证第一个样本也有完整窗口。参数说明values 是归一化后的一维价格数组window 设20代表用20个连续交易日预测第21天。如果把 values 换成多维特征数组比如形状为 (样本数, 特征数) 的数组这个函数会直接输出 (样本数-window, window, 特征数) 的三维张量LSTM 就能直接食用。这里有一个容易踩的边界坑有的代码在循环里用了 i-window:i1 这种切法把目标值也包进输入导致模型看到答案去预测答案。判断方法很简单训练完成后手动预测一组数据看预测序列是否明显滞后于真实序列。如果预测线整体向右平移了一天那几乎可以断定输入窗口里混入了当天目标值需要回头检查下标。3.3 MinMaxScaler的正确打开方式测试集为什么不能参与拟合归一化是压缩数据分布的关键步骤。LSTM 激活函数对输入范围敏感几百块钱的股价不压缩直接训练梯度会极其不稳定。最常用的操作是用 sklearn 的 MinMaxScaler 把价格映射到 (0,1) 区间。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) scaled_train scaler.fit_transform(train_close.reshape(-1, 1)) scaled_val scaler.transform(val_close.reshape(-1, 1)) scaled_test scaler.transform(test_close.reshape(-1, 1))逻辑说明fit_transform 只在训练段调用一次验证段和测试段都只做 transform。参数说明feature_range 默认 (0,1)可以改成 (-1,1)与 tanh 配合有时收敛更快但预测完的逆变换必须用同一个 scaler不能手动改公式。MinMaxScaler 只接受二维数组所以单列价格必须先 reshape(-1,1)三维张量不能直接丢进去。测试集为什么不能参与 fit因为 fit 会记录这段数据的最小值和最大值如果测试段的极值被提前知道相当于模型在测试前就偷看了未来价格的尺度。这比显式的未来函数更隐蔽因为很多教材就是先对整个数据集 fit再切分训练集测试集流程看起来很顺测试指标也好看到不行但一换到新数据就崩。更严格的做法是滚动归一化每次预测前只用预测时刻之前的窗口数据计算极值实时更新 scaler。源码包很少默认提供这种模式但你自己改造时值得优先加上。4. 复现源码包里的模型三层LSTM训练、保存与载入的完整链路4.1 两层LSTM加Dropout的模型结构参数为什么这样设源码包里的模型文件通常是 .h5 或 .keras对应 Keras 的 Sequential 模型。最常见的工业配方不是单层 LSTM而是两层 LSTM 叠加中间穿插 Dropout最后接一个全连接层输出预测价格。为什么是两层第一层把原始时间窗口转化成更高维度的隐状态序列return_sequencesTrue 保留每一步的输出第二层进一步提取抽象特征只保留最后一步的隐状态传给全连接层。单层 LSTM 的表示能力有限但堆到三层以上对日线数据又容易过拟合两层是回测里最常见的折中。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(units64, input_shape(window, 1), return_sequencesTrue)) model.add(Dropout(0.2)) model.add(LSTM(units32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizeradam, lossmse) print(model.summary())参数说明units64 是第一层隐状态维度也就是每个时间步输出向量的长度。input_shape(window, 1) 中的 window 必须与构造滑窗时的窗口一致1 是特征数如果你在数据集处理那步加入了成交量这里要改成2。return_sequencesTrue 是为了让第一层在每一个时间步都产出状态给第二层第二层设置为 False 则丢掉中间状态只保留最后一步。Dropout(0.2) 在训练时随机丢弃20%的连接防止两层结构学到数据噪声。输出 Dense(1) 没有加激活函数默认是线性输出预测价格这种回归任务不能乱加 sigmoid。模型结构里最容易改坏的就是 input_shape。很多初学者把 shape 写成 (window, features, 1)或者把 window 和 samples 搞混运行时直接报维度错误。我的习惯是先打印 model.summary() 核对每一层的输出形状再开始训练。另一个值得交代的点是参数量6432 两层 LSTM 的总参数量通常在几万级别对几千条日线数据足够如果某个源码包模型里有几百万参数它的训练集规模一定数倍于此否则几乎必然过拟合。4.2 训练循环与早停让模型在过拟合之前停下来训练 LSTM 预测股票最忌讳的是一次跑满 100 个 epoch 然后把最后一个 epoch 的权重保存下来因为第 100 个 epoch 大概率已经过拟合。正确做法是加上 EarlyStopping 和 ModelCheckpoint 两个回调让训练在验证集 loss 连续多轮不降时自动停止并且始终保存验证集表现最好的一次权重。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_val, y_val), epochs100, batch_size32, callbacks[early_stop, checkpoint], verbose1, )参数说明monitorval_loss 表示关注验证集损失patience10 允许连续 10 个 epoch 不下降超过就触发早停restore_best_weightsTrue 在停止时自动回滚到最佳权重否则模型保存的可能是最后一轮的过拟合状态。batch_size32 是常用折中梯度更新平滑显存占用也不高数据量小改16数据量大且用 GPU 可以试着改成64。epochs100 只是上限实际早停通常在 30 到 50 轮内发生。训练结束后别急着看测试集先观察 history 里的两条曲线train loss 在降但 val loss 开始回升说明过拟合启动此时可以把 Dropout 调高或增加 L2 正则两条 loss 都在高位震荡不降优先检查归一化是否做对、learning rate 是否默认过大而不是加层加参。模型是黑匣子没有特征重要性可看我们只能靠这类间接信号判断学得好坏。4.3 保存模型时把Scaler一起落盘预测才不会翻车模型训练完的落盘动作在源码包里经常只做了一半只有 model 文件没有保存训练时用的 Scaler。这个缺漏会让你的预测结果完全失真。正确的做法是把模型权重和归一化器两个文件一起保存。model.save(lstm_stock.keras) import joblib joblib.dump(scaler, close_scaler.pkl)载入预测时配套使用from tensorflow.keras.models import load_model model load_model(lstm_stock.keras) scaler joblib.load(close_scaler.pkl) raw_context raw_close[-window:].reshape(-1, 1) scaled_context scaler.transform(raw_context).reshape(1, window, 1) pred_scaled model.predict(scaled_context, verbose0) pred_price scaler.inverse_transform(pred_scaled)[0, 0] print(明日预测收盘价:, round(pred_price, 2))逻辑说明raw_context 取最近 window 天真实价格顺序必须从旧到新scaler.transform 用训练时保存的统计量完成映射模型输出仍是归一化值必须 inverse_transform 还原成原始价格。参数说明reshape(-1,1) 是给 Scaler 用的二维格式reshape(1, window, 1) 是给模型用的三维格式少一个维度就会报错。如果载入时报 Unknown layer 或 optimizer 不匹配多半是 Keras 版本不一致优先改用 .keras 格式重存如果只想要权重用 model.load_weights 也能加载但前提是当前定义的模型结构和原训练结构完全一致差一个 units 都会报错。很多源码包附带一个预测脚本但脚本里重新 fit 了一个新 Scaler用它把最近一段数据压缩后再预测输出的数值看起来合理实质已经和模型训练时不一致。判断方法很简单预测几组不同行情段的数据如果输出几乎不变说明 Scaler 不对此时去训练脚本里找到训练时的 Scaler 对象序列化保存后替换掉。5. LSTM股票预测避坑清单5个最常见翻车位与排查方法5.1 坑一全量数据fit归一化器回测曲线虚高现象训练损失和验证损失都很低预测曲线和真实曲线几乎重合但把数据换成另一段行情后预测立刻发散。原因代码里对整个数据集调用 scaler.fit_transform。fit 过程已经看到了测试段的最大值和最小值测试段的尺度信息在训练前就被模型间接获得属于典型的归一化泄露。解决严格按时间顺序先切分再仅对训练段 fit验证与测试段各用 transform一行代码都不妥协。这也是我检查源码时的第一个固定动作搜索代码里有没有直接对全量数据 fit。5.2 坑二shuffleTrue把时间顺序打乱模型只学会贴最近价现象训练正常收敛但预测曲线几乎变成前一日价格的平移方向准确率惨不忍睹。原因很多代码模板会带 shuffleTrue 来增强泛化但在时间序列里打乱的是样本间先后关系模型无法学到连续时间上的依赖退化成输出约等于输入窗口末值的平滑解。解决训练时不传 shuffle 参数或显式写 shuffleFalse。如果既想增加随机性又怕破坏时序可以按 batch 内部洗牌而非样本级洗牌用 tf.data 的 window 和 batch 组合实现。注意验证集喂给 model.fit 时不会 shuffle只要训练集不乱就行。5.3 坑三直接预测价格MSE很低但方向命中率不过半现象loss 值很漂亮价格误差看起来很小但拉出涨幅方向统计命中率长期在 50% 以下。原因股票价格序列高度自相关模型只需要复制最近收盘价就能得到非常低的 MSE但复制操作根本没有预测信息对方向判断不产生增量。解决不要只看误差绝对值。把监督目标从原始价格改成明日收益率或对数收益率模型被迫学习价格变化的方向。更直接的做法是把输出层改成分类预测明日上涨还是下跌把回归问题化为分类问题。源码包如果坚持回归至少要在预测后计算方向命中率再上策略。5.4 坑四特征里混进未来信息回测收益漂亮得不真实现象回测模拟成绩非常好胜率逼近70%换到模拟项目X或者某个其他标的上连手续费都覆盖不了。原因特征工程没卡可见时刻。典型例子是用当日收盘后才会更新的指标配合下一个交易日的标签或者统计特征里混入了包含当天的移动平均导致训练标签和输入特征存在同源信息。解决为每一类特征标记它的产生时刻。收盘价预测的输入只允许用当日及之前的信息当日成交额、当日最高最低在盘中是变化的若你的预测点设在收盘后它们已确定可以入特征但次日开盘价绝对不许出现。把特征构建和预测写在同一个函数里管线化执行能在工程上杜绝这类作弊。5.5 坑五模型文件能载入但预测恒为常数Scaler没跟着保存现象load_model 不报错predict 也能返回数组但 inverse_transform 后所有预测值几乎相同。原因预测代码里新建了一个 Scaler用最近几十天的数据重新 fit而这个新 Scaler 的统计量和训练时完全不同把模型输出映射回了错误的价格区间。解决模型文件和 Scaler 必须成对保存、成对载入。如果训练时的 Scaler 文件确实丢了回退方案是用训练阶段的数据区间手动构造一个近似 Scaler并在结论里注明存在精度损失。千万别用测试段或最近行情段去 fit 替代品那样等于又一次把未来信息带到预测过程。6. 进阶验证把预测结果真正用于决策前的三个习惯6.1 用方向准确率替代MSE做模型质检模型训练完保存后先不管价格误差立刻计算方向命中率预测涨跌方向和真实涨跌方向一致的比例。diff_pred np.diff(pred_price.flatten()) diff_real np.diff(y_real.flatten()) hit (diff_pred * diff_real) 0 direction_accuracy hit.mean() print(方向命中率:, round(direction_accuracy, 4))这段代码把价格级误差折算成方向级判断。如果方向命中率能稳过 52%才值得继续往下做策略长期低于 50%说明对当前标的没有预测效用继续调参收益不大。6.2 滚动预测让误差暴露模型的真实有效窗口一次性外推只能证明模型在某个时点工作滚动预测才接近实盘。每次预测完把预测值放进输入窗口尾部用变化后的上下文预测下一天误差会逐步累积正好用来判断模型几天内的预测还有意义。def rolling_predict(model, scaler, recent_raw, window, steps5): preds [] context recent_raw.copy() for _ in range(steps): scaled scaler.transform(context[-window:].reshape(-1, 1)) x scaled.reshape(1, window, 1) p model.predict(x, verbose0) p_price scaler.inverse_transform(p)[0, 0] preds.append(p_price) context np.append(context, p_price) return np.array(preds)注意context 每轮追加的是预测值而非真实值所以步数越多误差越明显这是刻意设计的压力测试。如果第 1 天预测合理、第 3 天后完全失真说明模型的有效预测窗口就两三天策略周期必须匹配这个数字。6.3 和一个baseline对比才能判断LSTM到底有没有增量最后养成的习惯是把一切结果和用今天收盘价预测明天收盘价的基线比较。如果 LSTM 的滚动预测方向准确率只比基线高一个百分点那这点增量大概率被滑点和手续费吃掉。比较时固定同一段测试集、同一个方向指标把结果记录成表格基线、LSTM 单步、LSTM 滚动3步每次换参数都能快速看出谁在真正进步。说句实话我拆解过好多个同类的股价预测源码包最终发现代码差异都不大拉开差距的全是数据处理细节是否泄露了归一化信息、有没有关闭 shuffle、Scaler 有没有跟模型一起保存。你只要按本文的顺序把这三处检查落实即使预测结果不赚钱至少能保证回测是诚实的不会用虚高的指标误导自己。做模型和做策略是两件事先学会诚实验证再去想怎么赚波动的钱。希望这些思路能帮到你也希望你拿到下一份源码包时先检查数据管线再谈参数。本文还有配套的精品资源点击获取