LSTM股票指数预测:从时间序列特征工程到防数据泄露的PyTorch实战 简介面向计算机相关专业学生及项目实战学习者的Python神经网络资源基于LSTM模型对股票指数如上证综指、道琼斯进行预测解决时间序列预测场景中的建模与代码实现问题。该资源为作者大三期末大作业经导师指导并获99分高分代码完整可运行非常适合课程设计、期末大作业及毕业设计参考。压缩包共10个文件包含3个Jupyter Notebook源码分别针对上证综指、道琼斯等指数、1个训练好的best_model.pth权重文件、2个Markdown说明文档以及配置文件等整体仅1.65MB结构清晰、轻量易用。已有163人学习下载。通过该资源可掌握LSTM在金融时序预测中的完整流程数据预处理、模型构建、训练评估与预测代码注释详细稍作修改即可迁移至其他股票数据是快速上手神经网络项目的实用范例。1. LSTM预测股票指数先别问“准不准”问“数据怎么喂”做股票指数预测的Python项目十个人里有八个是先跑通一个LSTM模型然后对着预测曲线感叹“真像那么回事”最后实盘一用就翻车。问题往往不在LSTM本身而在数据泄露和评价方式——你用当天的收盘价预测当天走势或者用未来数据做过归一化训练集的损失能降到0.01样本外照样一塌糊涂。这个领域的入门门槛其实很低会Python、装好PyTorch、拉一段指数历史行情就能搭出一个可运行的LSTM预测项目但要把“预测曲线”变成“有决策参考价值的输出”需要把数据切分、特征构造、模型输入输出这几件事做对否则你拿到的只是一个会画图的随机数发生器。这篇笔记按照我实际做这个方向时的推进路径来写从指数数据的获取与预处理、LSTM网络结构的PyTorch实现、训练参数配置到样本外验证和滚动预测技巧。每一段都给可以直接复制的代码和参数说明也会把容易踩的坑标出来。适合刚入门深度学习、想拿金融时序数据练手的新手也适合做过分类或图像模型、但对时间序列预测不熟的从业者。2. 指数数据与特征工程把K线变成LSTM能吃的时间序列2.1 选数据源与拉取上证指数日线数据做股票指数预测首先要有干净、连续、够长的历史数据。A股指数里上证指数、沪深300、中证500都是常用标的我一般用akshare拉数据——免费、国内直连稳定、接口相对简单不需要注册Token。安装akshare之后一行代码就能拿日线。import akshare as ak import pandas as pd # 拉取上证指数日线前复权 df ak.stock_zh_index_daily(symbolsh000001) df.columns [date, open, close, high, low, volume] df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) print(df.head()) print(df.tail()) print(f总行数: {len(df)})这段代码里stock_zh_index_daily返回的是未复权指数点位数据指数本身没有拆股分红问题所以不需要像个股那样纠结复权因子。日期列要转成datetime类型并且按升序排列——LSTM按时间步读取序列顺序错了后面的窗口切分全部错位。拉下来先看head和tail确认数据覆盖年限和有没有断档我见过有人把2024年的数据排在2015年前面模型照样训练完成但结果毫无意义。数据量方面上证指数从1990年至今有8000多根日线对LSTM来说完全够用。2.2 构造特征收盘价之外还要加什么裸的收盘价序列可以做预测但效果通常很差因为单变量序列里信息量太少。我一般会加上几个常见技术指标比如5日与20日均线之差反映短期趋势、当日涨跌幅、成交量对数的滑动均值。选特征的原则是只用t时刻及其之前的信息绝不能用t1及以后的数据。下面这段代码把所有特征构造逻辑放在shift和rolling里确保每个时间步的特征都是历史可见的。import numpy as np df_feat df.copy() # 基础特征 df_feat[ret] df_feat[close].pct_change() # 当日收益率 df_feat[log_vol] np.log(df_feat[volume] 1) # 成交量取对数 # 均线类特征 df_feat[ma5] df_feat[close].rolling(5).mean() df_feat[ma20] df_feat[close].rolling(20).mean() df_feat[ma_diff] df_feat[ma5] - df_feat[ma20] # 波动率特征过去5日收益率的标准差 df_feat[volatility] df_feat[ret].rolling(5).std() # 标签未来5个交易日的累计收益率预测目标 df_feat[label] ( df_feat[close].shift(-5) / df_feat[close] - 1.0 ) # 删除空值 df_feat df_feat.replace([np.inf, -np.inf], np.nan).dropna().reset_index(dropTrue) features [close, ret, log_vol, ma_diff, volatility] print(df_feat[[date] features [label]].tail())这里的关键点是label用了shift(-5)——它把未来第5天的收盘价与当前收盘价做比值减去1得到未来5日收益率。shift(-5)在构建标签时是合法的因为训练时标签本来就来自未来但特征列绝不能shift未来值。很多入门项目把预测目标设成“下一天收盘价”直接回归价格本身这会导致模型对绝对价格水平敏感换一个指数或跨年份数据后效果剧烈波动。改成预测未来5日累计收益率相当于让模型做“方向幅度”的回归数值范围稳定在-0.1到0.1之间对LSTM的激活函数更友好。注意dropna()删掉了开头20行缺失均线的数据和末尾5行缺失标签的数据这是数据量缩减最常见的原因。如果发现序列变短了不用慌看下df_feat的行数和原数据的差值是否等于25左右。2.3 窗口切分与归一化时间序列不能随机打乱LSTM的输入是(batch, seq_len, input_size)形状的三维张量。seq_len代表用过去多少个交易日预测未来我一般取20也就是用过去一个月的日线特征预测未来一周的方向。切分方式采用滑动窗口窗口之间可以有重叠这能显著增加训练样本数。归一化这里有个大坑如果对整个数据集做fit_transform相当于用了未来数据的均值和标准差这属于数据泄露。正确做法是只用训练集的统计量去归一化训练集、验证集和测试集。from sklearn.preprocessing import StandardScaler SEQ_LEN 20 def create_sequences(data, seq_len): xs, ys [], [] for i in range(len(data) - seq_len - 4): # 减去seq_len和label的5日步长 x data.iloc[i : i seq_len][features].values y data.iloc[i seq_len][label] xs.append(x) ys.append(y) return np.array(xs, dtypenp.float32), np.array(ys, dtypenp.float32) # 切分前70%训练后15%验证最后15%测试按时间顺序 train_size int(len(df_feat) * 0.7) val_size int(len(df_feat) * 0.15) train_df df_feat.iloc[:train_size] val_df df_feat.iloc[train_size : train_size val_size] test_df df_feat.iloc[train_size val_size :] # 用训练集拟合scaler scaler StandardScaler() scaler.fit(train_df[features]) train_scaled pd.DataFrame( scaler.transform(train_df[features]), columnsfeatures ) val_scaled pd.DataFrame( scaler.transform(val_df[features]), columnsfeatures ) test_scaled pd.DataFrame( scaler.transform(test_df[features]), columnsfeatures ) X_train, y_train create_sequences(train_scaled, SEQ_LEN) X_val, y_val create_sequences(val_scaled, SEQ_LEN) X_test, y_test create_sequences(test_scaled, SEQ_LEN) print(fX_train.shape: {X_train.shape}, y_train.shape: {y_train.shape}) print(fX_val.shape: {X_val.shape}, y_test.shape: {y_test.shape})这段代码有两点必须说明。第一切分必须按时间顺序不能用train_test_split(random_state42)随机切分——时间序列一旦随机打乱训练集里混进了未来数据验证集和测试集的评估结果都会虚高。第二create_sequences里range(len(data) - seq_len - 4)最后的减4是因为label是未来5日收益率窗口末尾已经是i seq_len标签则要取到i seq_len 5的位置保险起见减掉4个样本防止越界。这里留一个自行验证的小作业把减4改成减5观察X和y的行数是否仍然对齐。3. LSTM模型结构设计从PyTorch源码到指数预测的输入输出3.1 两层LSTM加全连接层为什么不用单层股票指数预测场景下单层LSTM的表示能力有限难以同时捕捉短期波动和中期趋势。常见做法是堆两层LSTM第一层输出完整序列供第二层消费第二层只取最后时间步的隐藏状态送入全连接层做回归。下面是我在这个项目里常用的模型定义代码可以直接存成lstm_model.py。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size5, hidden_size64, num_layers2, output_size1, dropout0.2): super(LSTMPredictor, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.fc nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, output_size), ) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) last_out out[:, -1, :] # 取最后一个时间步 y_pred self.fc(last_out) return y_predbatch_firstTrue让输入维度更直观第一维是batch_size第二维是seq_len第三维是特征数。nn.LSTM内部默认的hidden_state和cell_state是一组全零张量对短期序列来说冷启动影响不大但如果预测超长序列建议用可学习的初始状态。out[:, -1, :]这一步是关键——LSTM在每个时间步都有输出但只有最后一个时间步整合了全部历史信息用来做回归是最自然的。全连接层最后没有接激活函数因为我们是回归任务预测的是一个带符号的收益率输出范围可能是负的ReLU或Sigmoid都会限制输出区间。3.2 独热编码与嵌入层这个场景不需要我看到不少LSTM股票预测项目在模型前加了一个Embedding层理由是把“星期几”“月份”等时间特征嵌入。这个做法本身没错但对于指数预测这种以价格和成交量为主特征的回归任务多一个嵌入层往往带来的是过拟合而不是精度提升。原因在于交易日历特征周一、周五、月初、月末对A股指数的解释力很弱远弱于均线差和波动率。所以我通常不加Embedding而是把全部精力放在数值特征的构造和归一化上。如果确实想加入时间特征更稳妥的方式是构造“距月初天数”和“星期几”两个数值特征直接拼进特征矩阵而不是走嵌入。# 可选添加时间特征数值型 df_feat[day_of_week] df_feat[date].dt.dayofweek df_feat[day_of_month] df_feat[date].dt.day df_feat[month] df_feat[date].dt.month features_enhanced features [day_of_week, day_of_month, month]这里真正要提醒的是加入这些特征后验证集和测试集上的表现可能会变差。这不是模型坏了而是这些特征本身就是弱信号扩大了输入维度后LSTM需要更多的数据才能拟合到有意义的相关性。遇到这种情况果断退回只用原始5个特征不要恋战。3.3 损失函数与评估指标回归任务的“看着准”陷阱训练LSTM回归模型最常见的损失函数是MSE均方误差。但股票预测里MSE低不代表预测有用——如果一个模型永远预测“收益率接近0”它的MSE可能很低却完全没有交易价值。我一般在训练时用MSE做损失在验证时额外计算方向准确率即预测值与真实值符号是否一致。方向准确率才是这个项目应该关注的“业务指标”。def directional_accuracy(y_true, y_pred, threshold0.0): y_true np.asarray(y_true).flatten() y_pred np.asarray(y_pred).flatten() pred_dir (y_pred threshold).astype(int) true_dir (y_true threshold).astype(int) return float((pred_dir true_dir).mean())这个函数统计的是预测收益率的正负号与真实收益率正负号的匹配比例。threshold可以设置成0.001之类的小正数来过滤接近零的无效预测。方向准确率超过52%在指数日线预测上已经算有统计意义超过55%基本可以认为是特征和模型配合得当而不是运气。训练过程中如果MSE在下降但方向准确率在50%附近徘徊说明模型学到的是“均值回归到0”这个模型直接放弃就好不用浪费时间调参。实际项目里我会同时打印这两个指标用方向准确率决定是否早停。4. 训练与调参20个epoch内让损失降下来的可复现配方4.1 训练循环手写for循环比封装Trainer更可控PyTorch Lightning这类高级封装确实省代码但在这个场景下我推荐手写训练循环——因为你需要精确控制每个epoch结束后的验证逻辑、早停条件和学习率调整。下面这段训练代码是完整的可运行版本。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # 准备数据加载器 train_dataset TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train)) val_dataset TensorDataset(torch.from_numpy(X_val), torch.from_numpy(y_val)) train_loader DataLoader(train_dataset, batch_size128, shuffleTrue) val_loader DataLoader(val_dataset, batch_size256, shuffleFalse) # 初始化 model LSTMPredictor(input_size5, hidden_size64, num_layers2, dropout0.2) optimizer optim.Adam(model.parameters(), lr0.001) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) criterion nn.MSELoss() # 训练参数 EPOCHS 50 best_val_loss float(inf) best_model_path best_lstm.pth patience_counter 0 EARLY_STOP_PATIENCE 8 for epoch in range(EPOCHS): model.train() train_loss_sum, train_batches 0.0, 0 for xb, yb in train_loader: optimizer.zero_grad() y_pred model(xb).flatten() loss criterion(y_pred, yb) loss.backward() # 梯度裁剪防止LSTM训练时梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss_sum loss.item() train_batches 1 # 验证 model.eval() val_loss_sum, val_batches 0.0, 0 preds, trues [], [] with torch.no_grad(): for xb, yb in val_loader: y_pred model(xb).flatten() loss criterion(y_pred, yb) val_loss_sum loss.item() val_batches 1 preds.append(y_pred.numpy()) trues.append(yb.numpy()) train_loss train_loss_sum / train_batches val_loss val_loss_sum / val_batches preds np.concatenate(preds) trues np.concatenate(trues) val_acc directional_accuracy(trues, preds) print(fEpoch {epoch1:02d} | train_loss: {train_loss:.6f} | val_loss: {val_loss:.6f} | val_acc: {val_acc:.4f}) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model_path) patience_counter 0 else: patience_counter 1 if patience_counter EARLY_STOP_PATIENCE: print(f早停于 epoch {epoch1}) break代码里有几个参数值得细说。batch_size128对几千条训练样本来说不算大但LSTM本身计算量不小太大的batch容易让模型收敛到尖锐极小值泛化性差shuffleTrue只在训练集用验证集保持时间顺序。学习率0.001是Adam的默认值对应这个任务规模够用如果损失在训练集上迟迟不降我会把学习率降到0.0005而不是调大。clip_grad_norm_这一行很多入门代码会漏掉LSTM反向传播经过多层时间展开后梯度范数很容易超过1加上裁剪后训练过程会稳定很多。ReduceLROnPlateau的作用是当验证损失停止下降时把学习率减半给模型第二次收敛机会。4.2 必调的三个超参数hidden_size、num_layers、dropouthidden_size决定了LSTM记忆容量。64是稳妥起点数据量少于5000条时不要轻易加到128以上——容量越大过拟合越快验证集方向准确率反而会掉。num_layers我固定用2不是1也不是3。1层学不到高阶时序依赖3层在几千样本量下训练时间长且收益很小2层是平衡点。dropout的取值建议在0.1到0.3之间。这里有个反直觉的经验dropout设成0.5在很多图像分类任务里效果好但在LSTM回归里会明显降低方向准确率因为时间序列的局部相关性很强过度随机丢弃会破坏序列内部的连贯信息。我通常设0.2如果验证集方向准确率还不到50%先把dropout降到0.1再试一轮。4.3 训练过程排错损失不降、验证集指标乱跳、GPU显存溢出损失不降是最常见的现象。先看是不是数据没归一化——特征列如果有几百上千的量级比如没取对数的成交量LSTM的梯度更新会被大数值特征主导损失曲线会像心电图一样上下窜。再看学习率Adam默认0.001对大部分问题有效但如果你的损失在0.5以上且几乎水平试试0.005。验证集方向准确率乱跳是另一个常见问题本质是验证集样本太少几百条单次batch的预测结果方差很大。解决方法是把验证集换成测试集做多次滚动评估或者在验证时用更大的batch让统计量稳定。GPU显存溢出一般发生在显存不足6GB的机器上解决方式很直接batch_size从128降到64seq_len从20降到15或者把hidden_size从64降到48。这几项每项都能降低约一半的显存占用。股票指数预测的数据量实际上CPU训练也能在几分钟内完成我用M1 Mac的CPU跑50个epoch也就两三分钟没必要执着于GPU。训练脚本里可以用torch.backends.mkl相关配置优化CPU线程数或者干脆在数据加载时把num_workers设成0避免多进程开销。5. 预测结果验证与踩坑数据泄露、过拟合与“看着准其实废”5.1 踩坑记录一归一化时用了全局统计量验证集指标虚高现象训练集方向准确率55%验证集方向准确率58%测试集却掉到48%且预测曲线严重滞后于真实曲线。原因这个坑我在2.3节里提过——如果在切分训练集之前就对全量数据做StandardScaler.fit_transform验证集和测试集的均值和方差已经被训练集数据“剧透”了。滞后效应的来源更隐蔽K线数据本身有自相关性用未来统计量归一化后模型从归一化数值中能推断出“当前处于历史高位还是低位”而这一点在真实预测场景里是不存在的。解决严格按“先切分后归一化”的流程重做。用训练集的scaler去transform验证集和测试集并且把测试集当作“完全没见过”的数据来对待。修正后再看方向准确率如果从58%掉到51%恭喜你这才是真实水平。5.2 踩坑记录二把预测未来5日收益率做成逐日递推测试集指标失真现象测试集方向准确率能做到62%但把模型输出画成曲线后发现预测值几乎等于真实值平移了5天。原因这是LSTM股票预测项目中最常见的“伪高准确率”——你训练时的标签是shift(-5)的未来收益率但你在测试时使用了一种不严谨的评估方式用t时刻的特征预测t5的收益率然后把它当成t1时刻的预测值来做方向判断。由于收益率本身是连续变化的t5的预测方向与t1的真实方向有一定相关性导致指标虚高。解决评估时必须严格对齐时间轴。预测值y_pred[i]对应的是t i seq_len时刻之后第5天的收益率真实值y_test[i]也必须是同一个时间点的真实收益率。比对方向时用同一个索引位置的值做比较。如果发现“预测值领先真实值5天”那不是未卜先知是索引错位。5.3 踩坑记录三模型只学到“惯性”涨完继续预测涨现象方向准确率达54%但实际按预测做多后亏损观察预测值和真实值的散点图发现预测值几乎全部集中在正区间或全部集中在负区间。原因LSTM学到了一个最简单的规律——最近几天上涨未来5天大概率继续上涨最近几天下跌未来5天大概率继续下跌。这在震荡市里表现尚可但在趋势反转节点上比如连跌后的反弹、连涨后的回落预测方向会持续错误。因为LSTM的短期记忆天然偏向捕捉惯性反转信号比如MA5下穿MA20在特征空间里表达得很弱模型权衡后选择了“随大流”。解决在特征中加入“反转类”指标比如RSI相对强弱指标或KDJ的J值这类指标在超买超卖区间对反转有较强指示。代码层面可以在特征列表里追加rsi_14列用talib库一行算出。还有一个效果更好的办法把训练标签从“未来5日收益率”改成“未来5日收益率减去过去5日收益率”让模型学习“相对动量变化”而非绝对方向。# 反转特征RSI-14 try: import talib df_feat[rsi_14] talib.RSI(df_feat[close], timeperiod14) except ImportError: # 手写简版RSI delta df_feat[close].diff() gain delta.clip(lower0).rolling(14).mean() loss (-delta.clip(upper0)).rolling(14).mean() rs gain / (loss 1e-9) df_feat[rsi_14] 100.0 - (100.0 / (1.0 rs)) features_with_rsi features [rsi_14]5.4 踩坑记录四加载模型做推理时忘记eval模式预测结果抖动现象同一个测试样本多次运行推理得到不同的预测值且预测值抖动幅度达到几十个基点。原因模型定义里有dropout层而dropout在训练和推理时的行为不同——训练时随机丢弃部分神经元推理时应当关闭丢弃行为。如果在推理时没有调用model.eval()dropout层会继续随机遮蔽神经元导致每次前向传播结果不同。解决加载模型后在推理前调用model.eval()并用with torch.no_grad():包裹推理代码这样既能关闭dropout的随机性也能省去梯度计算开销。写成固定范式model LSTMPredictor(input_size5) model.load_state_dict(torch.load(best_lstm.pth)) model.eval() with torch.no_grad(): y_pred model(torch.from_numpy(X_test[:10]))6. 从源码到实盘滚动训练、置信区间与回测的那些坑跑通了单个模型的训练和测试只能算完成这个项目的30%。真正让预测结果具备可信度的是滚动训练——每天开盘前用截至昨天的数据重新训练或增量更新模型然后预测未来5日收益率。这里有一个实用技巧不要每次全量重训那样代价大且不稳定更稳妥的做法是每周一用最近500个交易日的数据重训一次其他交易日只加载最新权重做推理。如果数据源当天更新延迟宁可跳过这次预测也不要用昨天的数据冒充今天的数据。预测结果的可信度评估方面我推荐一个简单有效的做法多次用不同随机种子训练模型得到一组预测分布取标准差作为不确定性估计。如果5个模型的预测方向一致说明信号稳定如果方向各半说明当前市场状态处于模型的能力边界之外这类预测应该被过滤掉而不是硬着头皮使用。我把这个逻辑封装成ensemble_predict函数每次推理返回预测均值、标准差和一致方向投票数实战中只采用投票数不低于4的结果。千万不要把回测结果直接等同于未来收益。这个方向最深的坑在于回测中你看到了“事后诸葛亮”的完美曲线误以为预测模型有效但实盘里交易滑点、涨跌停无法成交、停牌数据缺失都会让收益大打折扣。我自己的习惯是任何LSTM预测结果都要经过至少3个月的纸面跟踪——记录每日预测方向、真实方向和预测置信度月底统计方向准确率与平均收益。如果纸面跟踪的方向准确率无法稳定超过52%就继续回到特征构造和数据的层面去迭代而不是去调模型结构。这个领域没有银弹LSTM能做的只是从历史数据中提取统计规律它不保证未来会重演但不重演的每一天都在提醒你把注意力放在数据、特征和评估流程上比追求模型复杂度更有回报。希望帮到你。本文还有配套的精品资源点击获取