基于LSTM的股票预测算法:从数据构造到回测避坑指南

📅 发布时间:2026/10/6 1:40:26
基于LSTM的股票预测算法:从数据构造到回测避坑指南
简介这是一份面向深度学习、机器学习与金融数据建模研究者的LSTM股票预测算法论文资料完整收录了基于长短期记忆网络对股票最高价进行预测的研究与实验过程。内容涵盖LSTM神经网络的细胞状态与门结构原理、数据预处理与标准化方法、PyTorch框架下的模型搭建以及梯度消失与梯度爆炸问题的应对思路。论文以股票最高价、最低价、收盘价、开盘价、日成交量和成交金额六个属性为输入采用前15天数据预测第16天最高价并通过前80%数据训练、后20%数据测试的方式验证模型效果适合作为时间序列预测入门、神经网络应用及课题研究的参考资料。资源为单个PDF文档大小约897KB正文包含实验流程图、网络结构图、公式推导与预测结果拟合曲线便于直接阅读、引用和复现文中对参数微调、选择记忆性等关键点亦有清晰阐述。目前已有294人学习使用读者可从中系统地理解LSTM在金融数据建模中的完整应用链路。1. 基于LSTM神经网络的股票预测算法先把“预测目标”定清楚再谈建模很多人看到“基于LSTM神经网络的股票预测算法研究”这个标题第一反应是打开行情软件拉一堆日线把LSTM模型跑一遍然后对着上涨的预测曲线高兴半天等实盘一买就发现曲线掉头向下。这个标题真正想解决的问题不是“能不能预测准”而是“怎么把LSTM作为时间序列预测模型合理地用在股票数据上并能用回测去验证效果”。股票数据是典型的非平稳、含噪、有波动聚集性的序列LSTM作为RNN循环神经网络的一种确实能通过学习窗口内前后依赖关系比BP神经网络、前馈神经网络更适合处理这种序列但前提是数据准备、模型设计、验证方式都站得住脚。我见过太多新手直接把收盘价丢进LSTM输出也是收盘价训练集准确率很高一到样本外就彻底失真。这篇笔记我会按一个可复现的研究思路来讲从K线数据怎么变成数据集到LSTM模型怎么搭、参数怎么整再到回测和避坑。适合拿这个题目做毕业设计、论文复现或者刚入门量化想用神经网络做因子预测的读者。你会发现LSTM股票预测的难点从来不是模型本身而是数据泄漏、归一化方式、预测目标定义这些“看不见”的细节。2. 把K线变成LSTM能吃的样本数据集构造与归一化是第一个分水岭LSTM模型的输入并不是“一张K线图”而是一个形状为[samples, time_steps, features]的三维张量。samples是滑窗切出来的样本数time_steps是你给模型看多少天历史features是每天有几个维度的特征。这个数据的组织方式直接决定了LSTM能不能学到可泛化的规律。2.1 原始行情数据准备前复权、频率与时间窗口怎么选股票原始行情的第一坑是复权。A股经常除权除息比如某只股票10派5元除息日股价会凭空低开一大截。如果直接用不复权数据模型会把这种“价格断层”当作暴跌信号预测结果完全乱套。我在处理数据时一律用前复权价格这样历史价格会被下调连续性保持住了而最近价格与实际盘面一致适合研究和可视化。后复权也是可选项但呈现出来的历史价格很大不利于归一化。频率方面我建议先做日线。日线数据干净、噪声相对较小而且没有隔夜跳空和分钟级的微观结构噪声适合验证算法逻辑。分钟线看起来信息多但LSTM要学的东西会从“趋势”变成“微观噪声”训练时间翻几倍预测稳定性反而下降。时间窗口我常用20个交易日大约一个自然月time_steps20特征选开盘价、最高价、最低价、收盘价和成交量这5个就够了不需要一上来就堆MACD、RSI这些衍生指标。衍生指标会让模型追着指标自身的滞后跑反而失去纯粹性。2.2 用Python生成滑窗样本input维度是[samples, time_steps, features]切样本的常规方法是滑窗。假设你有2000天日线数据窗口20天步长1天样本数量大约是1980个。下面这个函数是很多LSTM股票预测项目的基础工具import numpy as np import pandas as pd def build_sliding_windows(df, time_steps20, pred_horizon1): 从日线DataFrame构造LSTM输入样本 df: 包含open/high/low/close/volume列的DataFrame已按日期升序 time_steps: 每个样本用多少天历史 pred_horizon: 预测未来第几天的目标 返回X和yX形状为 [samples, time_steps, n_features] feature_cols [open, high, low, close, volume] n_features len(feature_cols) # 这里选close的列索引用于构造回归目标 close_col feature_cols.index(close) arr df[feature_cols].values X, y [], [] # 样本起点从0到 len(arr) - time_steps - pred_horizon for i in range(len(arr) - time_steps - pred_horizon 1): X.append(arr[i: i time_steps]) # 如果pred_horizon1y就是下一天收盘价 y.append(arr[i time_steps: i time_steps pred_horizon, close_col]) return np.array(X), np.array(y)这段代码里X.append(arr[i: itime_steps])取的是从第i天开始连续time_steps天的所有特征形状是[time_steps, n_features]y.append取的是第itime_steps天到itime_stepspred_horizon-1天的收盘价如果你的pred_horizon1y就是一个长度为1的序列。列表收集完成后np.array(X)自然变成[样本数, 20, 5]。注意这里步长是1意味着相邻两个样本有19天重叠相关性很强。如果想让训练样本更独立一些可以设置step5或step10减少样本数量同时也减少过拟合风险。有一个细节容易被忽略df必须按日期升序排列。如果数据源返回的是降序直接切窗会把未来的数据当作过去整个模型就变成“偷看未来”了。我的习惯是在构造窗口前先执行df.sort_values(date)再做索引重置避免隐藏问题。2.3 归一化参数要“按训练集拟合再映射验证集”否则预测值会整体失真LSTM对输入尺度很敏感尤其是配合 tanh 和 sigmoid 激活函数时。常见做法是 MinMaxScaler 把特征缩放到[0, 1]。但这里的坑在于不能把整个时间序列拿去做scaler.fit_transform因为这样归一化参数min和max是包含验证集和测试集所有数据的。一旦测试集里出现历史最高价scaler会把训练集的价格整体压低等于在训练时偷偷看到了未来的价格范围这在论文里属于数据泄漏。正确姿势是先用时间切分把数据集按比例分成训练段、验证段、测试段然后只在训练段上fit归一化参数再用同一个已经拟合好的 scaler 去transform验证段和测试段from sklearn.preprocessing import MinMaxScaler # 假设df是已经按时间升序排列的完整数据 train_size int(len(df) * 0.7) val_size int(len(df) * 0.15) train_df df.iloc[:train_size] val_df df.iloc[train_size: train_size val_size] test_df df.iloc[train_size val_size:] feature_cols [open, high, low, close, volume] # 重点只在训练段上fit scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_df[feature_cols]) # 验证集和测试集用同一个scaler做transform val_scaled scaler.transform(val_df[feature_cols]) test_scaled scaler.transform(test_df[feature_cols])MinMaxScaler在fit_transform时会记录训练段每个特征的最小值和最大值之后transform只是做一次线性映射。如果你直接对整个df调用fit_transform再切回三段测试集的信息就污染了训练集。这个问题的后果往往是训练损失很漂亮验证集损失也说得过去等模型真实面对一段完全没见过的新行情时预测值全都偏在一个很小的区间内因为尺度和训练时对不上。对于股票这种可能长期趋势向上的数据MinMaxScaler 还有一个缺点测试集价格一旦超过历史最大值映射后会出现大于1的值模型没见过这样的输入预测就会失灵。如果发现这种情况我一般改用标准化StandardScaler或者对收益率做归一化而不是对绝对价格归一化。这也是后面第4章要细化的一点与其预测绝对收盘价不如预测涨跌幅。3. 构建股票预测里的LSTM模型从单向到多层选择比调参更重要数据窗口造好了归一化也做好了才轮到模型本身。LSTM并不是什么黑匣子魔法它和BP神经网络、CNN卷积神经网络一样在反向传播过程中更新权重。区别在于LSTM引入了门控机制让信息可以跨多个时间步传递。如果只把LSTM当作一个“更深的神经网络”来使不去理解它的行为调参时很容易翻车。3.1 LSTM单元如何记住涨跌行情门控结构比BP神经网络多了“状态”先看一张常见的LSTM单元内部结构你会发现它其实是在维护一个细胞状态c_t通过遗忘门、输入门、输出门来控制信息的读写。对比普通RNN循环神经网络LSTM最直接的优势是缓解了梯度消失让第20天的输入仍然能影响第20天的输出对比BP神经网络这种前馈网络LSTM天然接受时序输入能捕捉到“连续放量几天后出现回调”这类时序模式。我见过很多初学者用一维卷积神经网络或者BP神经网络去做股票预测也不是不行但用LSTM的原因通常是希望模型学到窗口内的先后依赖。这里有一个务实的判断如果窗口长度只有5天LSTM的优势不明显用简单的逻辑回归也能做得差不多如果窗口长度拉到30天以上LSTM的长依赖建模能力才体现出来。实践里我通常设置time_steps20hidden_size64num_layers2两层LSTM比单层能捕捉更抽象的序列特征但继续加到4层以上收益骤减而且训练不稳定。3.2 搭建一个可运行的PyTorch LSTM模型输入输出与损失函数下面是我常用的一段PyTorch实现只保留了最核心的结构。batch_firstTrue能让输入形状直观地保持[batch_size, time_steps, features]对新手友好。import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size5, hidden_size64, num_layers2, output_size1): super().__init__() # batch_firstTrue 输入形状 [batch, seq_len, input_size] self.lstm nn.LSTM(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): # out的形状 [batch, seq_len, hidden_size] out, _ self.lstm(x) # 取最后一个时间步的隐藏状态作为预测依据 last_step out[:, -1, :] # 通过全连接层输出一个数值 return self.fc(last_step)代码里的out[:, -1, :]是整个序列最后一个时间步的隐藏状态这是回归股票价格时最常见的取法。你也可以试试out.mean(dim1)对所有时间步输出取平均这相当于让模型综合整段窗口的信息能稍微平滑预测结果。损失函数我用nn.MSELoss()因为收盘价或涨跌幅都是连续值有些研究把涨跌方向转成二分类来用CrossEntropyLoss但这会丢掉幅度信息我不建议作为第一个模型。多层LSTM中加入dropout0.2可以抑制过拟合但要注意PyTorch的nn.LSTM在num_layers1时不接受dropout参数只有层数大于1时dropout才生效。实际运行时输入x的维度是[batch_size, time_steps, 5]输出维度是[batch_size, 1]目标y的shape需要统一成[batch_size, 1]否则MSE计算时会对不上。3.3 训练策略早停、学习率衰减和随机种子为什么影响结果模型的训练没那么难但很多人失败在“训练几轮就loss不降”。下面的训练循环是基础版本加入了早停和学习率衰减def train_model(model, X_train, y_train, X_val, y_val, epochs80, lr1e-3, patience10): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5) loss_fn nn.MSELoss() best_val_loss float(inf) best_state None no_improve 0 X_t torch.tensor(X_train, dtypetorch.float32) y_t torch.tensor(y_train, dtypetorch.float32).unsqueeze(1) X_v torch.tensor(X_val, dtypetorch.float32) y_v torch.tensor(y_val, dtypetorch.float32).unsqueeze(1) for epoch in range(epochs): model.train() optimizer.zero_grad() pred model(X_t) loss loss_fn(pred, y_t) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() model.eval() with torch.no_grad(): val_pred model(X_v) val_loss loss_fn(val_pred, y_v) scheduler.step(val_loss) print(fepoch {epoch1}: train{loss.item():.5f}, val{val_loss.item():.5f}) if val_loss best_val_loss: best_val_loss val_loss best_state model.state_dict() no_improve 0 else: no_improve 1 if no_improve patience: break model.load_state_dict(best_state) return model这里有几个参数值得强调。lr1e-3是Adam的常用起步值如果数据已经标准化到[0,1]这个学习率一般能稳定下降。clip_grad_norm_是给梯度裁剪整个二范数防止LSTM在反向传播时梯度爆炸这在长序列股票数据里很常见是“训练突然出现NaN”的头号克星。ReduceLROnPlateau在验证损失连续5轮不降时把学习率减半等于不断用更小的步长去逼近局部最优。还有一个影响巨大的细节随机种子。LSTM的权重初始化是随机的同一份数据两次训练结果可能完全不同。如果要在论文里报告准确率或回测收益必须固定随机种子否则别人复现不了你的结果。我在代码开头固定torch.manual_seed(42)和np.random.seed(42)同时设torch.backends.cudnn.deterministic True。但这样只能保证同一机器上可复现不同版本PyTorch之间仍会有微小差异记录环境版本信息也是研究的一部分。4. 股票预测算法有没有效用回测和不存在未来函数的方式验证模型训练完损失函数好看不代表能赚钱。更准确地说LSTM股票预测算法的“研究价值”重点在验证环节。如果验证方式有未来函数再好的模型也是一个自欺欺人的工具。这一章讲怎么把模型输出变成可评估的交易策略并计算几个能说服自己的指标。4.1 区分回归预测与交易信号预测涨跌幅比预测价格更稳定直接预测收盘价有个问题价格受历史水平影响模型本质上是在学“最近的收盘价是多少”而不是“明天会涨还是会跌”。所以我把目标从价格换成未来一天或未来五天的对数涨跌幅# 用收盘价计算未来一天的对数涨跌幅 df[ret_t1] np.log(df[close]).shift(-1) - np.log(df[close])这样y的范围通常只有百分之几分布更加平稳。LSTM学到的就不是某一个绝对价位而是当前状态到未来状态的变化量。实际中预测未来1日收益噪声极大预测未来5日收益更平滑但样本数会变少。我的建议是先用pred_horizon1验证整个流程能跑通再切换到更长的horizon看趋势是否保持一致。当目标变成涨跌幅时归一化策略也更简单。输入用标准化的价格特征或者还是MinMaxScaler输出不用做归一化因为本身就在一个合理范围内。模型输出一个正数可以理解为模型“认为”未来会上涨输出一个负数则相反。真正交易时不需要精确预测数值只需要方向的准确率。4.2 套用移动平均滑窗回测把预测结果变成可执行的仓位回测切忌引入未来数据。下面是一个最朴素的回测流程在测试集上每日做出预测预测为正就持有多头仓位预测为负就空仓或持有现金再按移动平均的方式逐日累计收益。def backtest_with_predictions(df, pred_signal): df: 包含真实ret_t1的测试集DataFrame pred_signal: 与ret_t1对齐的预测涨跌幅数组 返回: 累计策略收益与累计基准收益 # 信号转换预测 0 则持有1否则0 position np.where(pred_signal 0, 1, 0) # 真实收益是T1的所以position必须是前一天收盘后确定的 # 这里假设pred_signal在第T天做出对应的真实收益在第T1天发生 strat_ret position * df[ret_t1].values bench_ret df[ret_t1].values # 扣掉单边万分之二的交易成本仅当仓位变化时计费 trade_cost 0.0002 cost np.abs(np.diff(position, prependposition[0])) * trade_cost strat_ret strat_ret - cost cum_strat np.cumprod(1 strat_ret) cum_bench np.cumprod(1 bench_ret) return cum_strat, cum_bench注意代码里position和ret_t1的对齐关系。如果第t天的模型用到了当天的收盘数据预测的是第t1天的收益那你应该用t1天才产生的收益去评估而不是直接把当天的收益拿来算。许多回测结果虚高都是因为在同一天既用了收盘价信号又把当天收盘价当成执行价等于偷看了收盘结果。另一种常见的未来函数是把整个测试集都放进滑窗样本再训练到某个早于测试集的时间点。比如用2023年数据训练却让它去预测2022年这就是穿帮。最后的做法是数据切分后训练集永远是测试集之前的时间段不允许交叉。4.3 指标怎么看RMSE、MAE、方向和盈亏统计回测曲线出来后别只看累计收益。我在衡量LSTM股票预测效果时至少会看四类指标指标计算方式说明RMSEsqrt(mean((真实涨跌幅-预测涨跌幅)^2))反映数值平均偏差越小越好MAEmean(abs(真实涨跌幅-预测涨跌幅))比RMSE更稳健不容易被大误差带偏方向准确率mean((预测涨跌幅 * 真实涨跌幅) 0)直接回答“涨没涨对”最大回撤max(1 - 策略累计净值/历史最大净值)回撤越大策略越难承受RMSE和MAE可以用sklearn很方便地算出来但方向准确率更能说明模型有没有预测能力。股票预测中如果方向准确率只有51%看起来赢面不大但配合换手率很低的持仓在复利效应下也能跑出不错的曲线。如果方向准确率低于50%那模型基本是在反向工作直接把信号取反都比它强。还有一点很现实测试集的时间长度会影响结论。只拿一年回测遇到单边熊市再好的LSTM预测结果也可能是亏的。最好选取至少包括下跌、震荡、上涨三种行情的区间来测试这样得出的结论才不是择时运气。5. LSTM股票预测的常见问题与排查过拟合、数据泄漏和“预测值滞后”三座大山这一章写的都是我在实操中真实踩过的坑。表面上看很多是模型问题最后查下来都是数据准备和验证逻辑的问题。下面按“现象 → 原因 → 解决”的格式列出来方便你照着排查。5.1 验证集loss低但实盘翻车先查是否用了未来数据做归一化现象模型在训练集和验证集上损失都很低回测曲线漂亮但一用到最近几个月的新数据预测值几乎恒定不变或者严重偏移。原因最常发生的情况是在构造样本之前对整个时间序列做了归一化。比如scaler MinMaxScaler()然后scaler.fit_transform(df[[close]])再把归一化后的序列切为训练集和测试集。测试集的历史极值已经参与了归一化参数的确定模型在训练时等于见过未来价格的上限和下限验证集loss自然好看但真实样本来自一个超出该范围的价格输入分布就错乱了。解决回到2.3节严格按照时间切分只在训练段上fit归一化参数。更稳妥的检验方式是把测试集单独留到最后训练和调参阶段完全不接触它。如果预测值还是偏移就把训练集中的最后一小段再次切出来做内部验证确认不是行情风格切换导致的。5.2 预测曲线总是比真实值延迟一拍是LSTM的天性不一定是你写错现象把预测收盘价和真实收盘价画在图上预测曲线看起来就是真实曲线向右平移了一根K线滞后非常明显。原因当模型目标是最小化MSE时最优策略往往是“预测为当前均值”而不是“预测为即将发生的突变”。因为股票价格具有自相关性下一时刻与当前时刻差异很小LSTM学会“用当前值预测未来值”自然比学会“捕捉反转”更容易拿到低loss。这里的滞后本质是回归问题本身的天性不是代码bug。解决如果目的是预测涨跌方向就不要用价格做目标直接用涨跌幅并计算方向准确率。我发现把目标换成涨跌幅后滞后现象会显著减弱。另一种缓解方式是把预测目标从下一日改为未来5日平均涨跌幅让模型必须看到短期趋势而不是单纯拟合随机噪声。止步于“看图像不像”是新手常见思维定量指标才是更客观的衡量。5.3 训练loss不下降学习率、数据尺度、以及把预测当成分类问题现象训练几十轮loss一直停在某个数值附近几乎不动比如0.020到0.025之间没有变化。原因第一数据尺度有问题比如原始价格从几元到几百元都直接进了LSTMMSE会因为价格绝对值很大而出现梯度异常第二学习率设置过大或过小Adam对学习率其实蛮敏感lr0.01可能让loss震荡lr1e-4可能下降极慢第三你可能把回归任务错误地做成了分类使用CrossEntropyLoss但输出是单个连续值模型不知道怎么优化。解决先对输入做归一化或标准化确保特征范围接近。然后把学习率设为1e-3跑10轮观察loss曲线如果完全不降再扩大到1e-2或缩小到1e-4。最后确认损失函数和输出层匹配价格回归用MSE涨跌分类用softmax CrossEntropyLoss并且输出维度改成2。还有一个排错技巧拿6个样本做一次手动前向和反向看能不能把loss降到0附近如果不能说明前向传播有问题而不是数据问题。5.4 同一段代码两次运行结果不同随机种子与不确定性处理现象连续训练两次验证集RMSE差了很多回测收益一个盈利一个亏损。原因LSTM初始权重随机、数据加载顺序随机、GPU计算本身不保证完全一致这些都导致结果漂移。如果不固定随机种子论文里报告的数字就可能是多次运行里的“最高分”而不是稳定水平。解决在训练脚本开头固定随机种子。我用的是下面这段基本可以覆盖大部分随机源import os import random import numpy as np import torch def set_seed(seed42): os.environ[PYTHONHASHSEED] str(seed) 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固定随机种子之后结果仍然可能因为不同平台、不同PyTorch版本而有差异。更严格的做法是把多次不同种子下的结果都记录下来取平均收益、最好最坏收益区间。如果你的方案在5个不同种子下都能保持正向收益那才说明这个LSTM股票预测算法不是因为某一次偶然初始化才“有效”。5.5 预测分数高但换到另一只股票就失效样本内过拟合与分布外泛化现象在某一只股票上调好模型RMSE和方向准确率都不错但换到另一只股票或者另一个板块预测能力基本归零。原因单只股票的行情有明显的个股特征模型学到了那只股票特有的波动模式比如某个时间段内涨跌交替频率。换到其他股票后波动率、价格的绝对水平、换手率特征都不同LSTM学到的参数不再适用。解决交叉验证时不要只做时间序列内部切分还要做“跨标的外推验证”。我一般把模型在沪深300里随机选5到10只股票上分别做训练和测试取平均性能来评估。如果想做一个相对通用的股票预测算法特征里最好加入“当日涨跌幅、振幅、成交量变化率”这类相对量而不是绝对价格和成交量。相对量在不同股票之间具有可比性模型迁移能力会强不少。6. 让这个研究真正变成“可复现论文”的最后一个技巧把每一次实验都记录成固定配置说到这里你已经有了完整的数据集构造、LSTM模型、训练和回测流程。如果只是要给自己看现在就可以结束但如果是写论文、做汇报还需要一个收尾动作把实验系统化记录下来。我吃过最大的亏就是调参一晚上第二天忘了哪组参数跑出了这个结果最后只能凭感觉重跑导致报告里的数字和实际不一致。我的做法是建一个配置文件把模型参数、数据范围、随机种子、归一化方式全部写进去。每次实验用一份独立的配置命名输出的指标和图表都挂在这个配置下。比如这样一段简单的配置config { symbol: 000300, # 回测标的 time_steps: 20, # 窗口长度 pred_horizon: 1, # 预测未来天数 features: [open, high, low, close, volume], target_type: return, # 回归目标类型return / price normalizer: MinMaxScaler, # 归一化方式 model: { hidden_size: 64, num_layers: 2, dropout: 0.2 }, train: { epochs: 80, lr: 1e-3, patience: 10, batch_size: 32 }, seed: 42, data_split: [0.7, 0.15, 0.15] }把配置文件存成JSON模型输出和回测结果一起归档。这样无论过多久都能复现同一套实验也能在换参数时清楚看到哪个变量影响最大。对于基于LSTM神经网络的股票预测算法研究这比在代码注释里写“改了窗口长度”要可靠得多。最后还想多提一句股票预测是一个信噪比极低的建模场景不要追求100%准确率也不要在看到一次漂亮回测后就认为模型“战胜了市场”。更好的心态是把LSTM当成一个研究工具用它来验证数据工程、时序建模和回测方法是否正确。哪怕模型最终没有稳定盈利只要你的数据切分是干净的、回测里没有未来函数这个研究本身就有很大的价值。希望帮到你。本文还有配套的精品资源点击获取