
简介一套面向毕业设计、期末作业与课程设计的Python股市预测项目基于LSTM神经网络对股票指数进行建模与趋势预测适合金融商贸相关专业学生快速上手。资源内含完整源代码、预训练模型与真实行情数据集代码辅以注释部署门槛低实测可运行是获得导师认可的高分项目。包体共13个文件大小357KB。其中5个py脚本划分了数据读取、模型定义、训练与评估等模块pkl文件保存训练完成的LSTM模型csv提供上证指数历史数据png展示预测效果md为说明文档pyc为编译缓存。整体结构清晰便于按流程理解与复现。目前已有392人学习下载资源整体简洁轻量适合希望快速掌握LSTM时序预测应用、完成课程展示或答辩的读者。1. 用LSTM预测股市的高分项目到手第一件事不是跑代码在GitHub上搜“Python LSTM 预测股市源码”会弹出一批挂着“高分项目”标签的压缩包。很多人解压后第一件事就是跑train.py然后对着屏幕上那条和真实行情几乎重叠的预测曲线兴奋半天——直到换一段没见过的行情模型立刻现出原形。这个方向本身是把LSTM用于股票时间序列预测历史行情数据经过滑窗切样、归一化喂给长短时记忆网络拟合下一阶段的价格或收益率。源码负责把数据管线、模型定义和训练流程串起来模型文件是可复用的权重数据集则决定整个模型的上限。这篇笔记不评价哪个包抄得好而是把一个能跑通、能改、能写进论文的完整实现路径拆开。适合课程设计要交差、打算入门时间序列预测、或者想把LSTM接进量化策略的从业者。2. 看懂LSTM预测项目的骨架数据文件、网络结构与训练脚本2.1 LSTM在股价序列上到底解决了什么问题普通RNN在反向传播时梯度随时间步连乘超过一定步数就指数级消失导致网络只能记住最近几步。股价序列恰恰有跨时间尺度的关联比如20日均线附近的支撑压力本质上是一种“记忆过去二十天价格分布”的长期依赖。LSTM在单元里加了三个门——遗忘门决定丢弃哪些历史信息输入门决定写入哪些新信息输出门决定这个隐状态对外露出多少。这个结构让信息可以按需保留几十个时间步所以LSTM对这类带滞后效应的序列是少数开箱即用且效果稳定的深度学习方案。但别把LSTM当印钞机。股票序列是非平稳的真实规律会漂移LSTM拟合的是历史分布不是未来的确定性。一个清醒的用法是把它当作结构化序列特征提取器输入收盘价、成交量这类原始特征输出一个对未来一段时间价格走向的估计然后把它和别的信号叠加使用。在这个项目里LSTM学出来的特征比如对均线位置的隐含表达比单纯看单日涨跌要有信息量多数“高分项目”能出好看的预测图靠的也是这个结构。另外值得说的是这一整套“滑窗LSTM回归”的流程不是股市专用。设备寿命预测、电网负荷预测、传感信号趋势分析用的几乎同一套代码换数据就能平移这也是为什么股市预测这个练手项目在面试时经常被当成时间序列建模能力的样本。2.2 一个典型项目包里的目录结构和代码分工这类“源码模型数据集”项目无论打包的人是谁核心文件通常跑不出下面四块。拿到压缩包先不要急着跑在IDE里把目录过一遍找到下表这些文件的位置。文件/目录常见内容拿到后先看什么data/日线行情CSV一般有date、open、high、low、close、volume字段数据覆盖时段、是否有缺失值prepare_data.py 或 utils.py数据清洗、滑窗切样、归一化逻辑归一化的fit发生在哪一步model.pyLSTM网络定义out[:, -1, :]之后接了什么东西train.py训练循环、损失函数、模型保存是否按时间序切分了训练/验证集predict.py 或 evaluate.py加载模型做预测、画图、算指标指标用的是MSE还是方向准确率拿到项目先盯prepare_data.py里的归一化代码因为数据泄漏是这类项目最常见的“高分造假”手段先对整段行情做MinMaxScaler再切窗口模型在测试集上看到的极值已经参与过缩放训练测试误差严重虚低。这个坑的具体表现和排查方法在第5章展开这里先记住一条判断标准归一化只能fit在训练部分。train.py的重点是数据集划分。一批项目用的是train_test_split(X, y, shuffleTrue)就是把股票序列按随机方式打散时间上下文直接被破坏训练出来的模型没有时序意义。标准做法是顺序切分前70%训练、中间15%验证、最后15%测试验证集和测试集永远排在时间上的未来位置。2.3 数据口径检查复权、缺失值、列名对应关系选定一个项目后第一段要跑的代码不是train.py而是把数据读进来摸一遍底。import pandas as pd df pd.read_csv(data/stock.csv) print(columns:, df.columns.tolist()) print(shape:, df.shape) print(df.head(3)) print(df.isna().sum())这段的作用是确认三件事列名是不是OHLCV标准字段、日期是否升序且无重复、有没有NaN。多数代码包对列名的读取是写死的比如某段代码里用df[close]而你的数据文件列名是收盘第一行就会报KeyError所以先打印列名能省掉十分钟定位时间。数据口径里最容易翻车的是复权。课程设计用的数据很多是从数据接口直接拉的不复权日线遇到分红除权价格会突然从30掉到15形成一根教科书级的“假大阴线”。LSTM可不管这是除权还是暴跌它会努力把这个跳空当成正常波动学进去训练出来的模型在除权日附近预测一团糟。检查办法很简单算单日涨跌幅把超过20%的日子全部列出来。daily_ret df[close].pct_change() abnormal_days df[daily_ret.abs() 0.2] print(abnormal_days[[date, close]])单日涨跌幅超过20%在A股除了涨跌停大概率是除权或者数据错误。如果发现这些异常点回到数据接口用前复权参数重新拉一遍或者在预处理里做拆分调整。这个检查只花十秒钟但能挡掉后面整个训练阶段在瞎学。3. 滑动窗口与数据集构建三个决定预测上限的参数3.1 窗口长度和预测步长怎么配对LSTM不吃整段历史它吃的是一个固定长度的滑窗。把股价序列切成[过去N天特征] - [未来M天价格]这样的样本对是所有基于LSTM的预测项目的基础管线。这里有两个参数直接决定模型能看到什么窗口长度N和预测步长M。窗口长度对应社区里常说的“用过去多少天预测未来”。股票分析里有均线的习惯5日、10日、20日均线的意义分别是短期持仓成本、中短周期趋势、月度趋势LSTM要学到的其实就是类似“当前价格相对20日均线的位置”这样的隐含特征。窗口太短比如5天模型只能看到几根K线的噪音窗口太长比如120天样本总量不变但输入维度变大训练变慢还容易过拟合。实践里从20天起步比较稳对应20日交易均线的周期做T1预测时20到30这个区间是性价比最高的。预测步长M却往往被忽略。很多代码包把M写成1也就是预测下一交易日收盘价。M1的训练最稳定但离真实交易场景有距离——A股T1交易制度下当日收盘后的信号最快次日开盘才能执行若模型预测的是次日收盘中间还隔了一个完整的持有决策窗口所以很多实盘项目会把预测目标对齐到T2甚至T3的收盘价。M加大后学习难度指数级上升连LSTM都会退化成“复读昨日价格”。我的习惯是先把M1跑通全流程、确认没有数据泄漏再加M调整策略不要一上来直接挑战M5。3.2 按时间顺序切分训练/验证/测试集别让未来数据混进来先看滑窗切样的标准实现import numpy as np def make_sequence_samples(array, window20, horizon1): X, y [], [] total len(array) for i in range(total - window - horizon 1): X.append(array[i:i window]) # 过去window天的全部特征 y.append(array[i window:i window horizon, -1]) # 未来horizon天的收盘价 return np.array(X), np.array(y)这个函数把连续的二维特征矩阵array切成X: [样本数, window, 特征数]的三维张量每个样本附带一段目标值y。注意y取的是array[iwindow : iwindowhorizon, -1]这里最后一列对应收盘价如果你的特征矩阵里收盘价不是最后一列-1取到的就是别的变量预测目标就错了。我见过不止一个项目在这里形状对但语义错跑出来的loss特别小——因为它在预测成交量或者开盘价。数据切分的正确做法是按时间先后切不是随机切n_train int(len(X) * 0.7) n_val int(len(X) * 0.15) train_X, train_y X[:n_train], y[:n_train] val_X, val_y X[n_train:n_train n_val], y[n_train:n_train n_val] test_X, test_y X[n_train n_val:], y[n_train n_val:]用train_test_split(shuffleTrue)会把2020年的样本和2024年的样本混在同一个批次里LSTM连“现在是哪一年”都分不清。滑窗样本还有一个隐蔽问题样本之间高度重叠第i天和第i1天的窗口只差一天本质上是近亲样本。训练集和验证集如果紧紧挨着验证集里的样本和训练集末尾的样本高度相似验证误差会被严重低估。应对办法是在训练集和验证集之间隔一段gap比如隔10个交易日再开始采样验证集让验证集真正“没见过”训练集末尾的连续语境。3.3 归一化只学训练集的统计量股价序列的数值范围会随行情漂移不归一化直接喂LSTM梯度会被大数值特征主导。常见的MinMaxScaler实现本身很简单难的是该fit在哪部分数据上。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() # 先切分再fit只fit训练集 train_flat train_X.reshape(-1, train_X.shape[2]) scaler.fit(train_flat) train_X_scaled scaler.transform(train_flat).reshape(train_X.shape) val_X_scaled scaler.transform(val_X.reshape(-1, train_X.shape[2])).reshape(val_X.shape) test_X_scaled scaler.transform(test_X.reshape(-1, train_X.shape[2])).reshape(test_X.shape)关键在scaler.fit(train_flat)这行统计量只来自训练样本。如果先对整段序列做scaler.fit(all_data)再切窗测试集的最大值、最小值已经进入缩放参数测试误差里就混进了未来信息。这个问题单独看不大但和滑窗重叠、早停等细节叠加会做出一套在历史数据上非常好看、一上新数据就崩坏的模型。训练完成以后scaler要跟着模型一起保存推理阶段对新数据做transform时还得用它。提示归一化的顺序是“先切分、再fit、后transform”这条顺序一旦反了后面所有评估都不可信。4. 把LSTM训练起来网络结构、损失函数与训练日志4.1 输入张量形状与LSTM的forward路径定义模型之前先明确输入输出形状这是LSTM项目里新手最容易卡住的地方。一个滑窗样本的形状是[window, features]一整个batch就是[batch_size, window, features]。PyTorch的nn.LSTM默认把序列长度放在第0维也就是[seq_len, batch, input]很多人忘了传batch_firstTrue导致训练报维度错误。import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, n_features, hidden_size128, num_layers2, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) # out: [batch, window, hidden_size] last out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(last) # [batch, output_size]最后一行out[:, -1, :]是全项目最常被问的一行LSTM每个时间步都会输出一个隐状态out[:, -1, :]取的是序列最后一个时间步的输出它理论上携带了前面所有时间步压缩后的信息再经过全连接层映射到未来价格。这是一个标准的sequence-to-point结构——输入一段序列、输出一个点。如果项目改成了sequence-to-sequence输出未来N天的整段曲线fc的输入就要换成所有时间步输出拉平或直接接Linear(hidden_size, horizon)然后循环预测。课程设计阶段先用sequence-to-point跑通改起来最省力。4.2 网络参数怎么选hidden_size、num_layers、dropout超参选择是LSTM预测里最玄学的一环没有放之四海皆准的答案但有相对稳妥的经验区间。股票行情数据往往只有几百到几千个交易日这决定了模型容量不能太大——同一个LSTM在图像任务里可以堆到几千万参数在股价任务里堆这么大会直接把历史走势背下来。参数常见范围选型理由hidden_size64 ~ 256表示隐状态维度64起步训练快上128后拟合能力够用再大就靠数据量硬扛num_layers1 ~ 22层能建模稍复杂的非线性3层以上在几千样本下基本过拟合dropout0.1 ~ 0.3只对层间连接生效增强泛化单层时dropout不会起作用learning_rate1e-3 ~ 5e-4股票loss曲面比较陡峭1e-3起步配合调度器降LR最稳batch_size32 ~ 64滑窗样本重叠度高batch太大会让梯度被相邻样本主导output_size1 ~ horizon和预测步长一致M1就填1两个容易被忽略的点第一dropout在num_layers1时是无效的PyTorch文档明确写了单层LSTM不会应用dropout很多人加了dropout发现val loss一点变化都没有不是代码写错是本身不生效第二hidden_size不必是2的幂但从64、128这个序列去试比较顺遇到内存或显存不够先从hidden_size减半开始调不要先动batch_size。4.3 训练循环、早停与模型保存训练脚本的骨架如下这个结构可以直接替换到项目里from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset( torch.tensor(train_X_scaled, dtypetorch.float32), torch.tensor(train_y, dtypetorch.float32).unsqueeze(-1) ) train_loader DataLoader(train_dataset, batch_size32, shuffleFalse) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience10 ) criterion nn.MSELoss() def evaluate(model, loader): model.eval() total 0.0 with torch.no_grad(): for xb, yb in loader: total criterion(model(xb), yb).item() * xb.size(0) return total / len(loader.dataset) best_val_loss float(inf) for epoch in range(200): model.train() total_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() * xb.size(0) val_loss evaluate(model, val_loader) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pth)DataLoader里的shuffleFalse是刻意为之滑窗样本时间上高度重叠shuffleTrue会让相邻样本的梯度在同一个batch出现反而增加方差保持False让模型按时间顺序学习和验证集、测试集的评估逻辑保持一致。clip_grad_norm_这行很多人会删掉但股票训练里梯度爆炸非常频繁尤其是用MSE损失碰上极端行情跳空时一个异常样本就能让loss冲上nan。clip到5.0的范数上限相当于给梯度上了一个保险丝。见过太多项目删掉这行等loss变成nan才开始找后悔药。整个循环里要注意的是用val_loss来决定是否保存模型而不是训练loss。训练loss会随epoch增加单调下降用它选模型一定会选出过拟合版本。ReduceLROnPlateau在val_loss平台期自动降低学习率配合200的上限epoch通常30到80轮就能收敛跑不到200轮就会被patience机制挡住。5. LSTM预测股市常见问题排查5条踩坑记录5.1 预测曲线永远慢半拍“看着很准”其实没有信息量现象画出来的预测曲线和真实收盘价几乎重合但把两条线并排看预测总是比真实晚一天像一条被向右平移过的行情线。MSE小得惊人一算方向准确率只有50%抛硬币水平。原因股价原始序列是强自相关的今天的收盘价和昨天的收盘价往往只差零点几个百分点。LSTM在MSE目标下发现最简单的降误差方式就是输出“昨天的价格”因为昨天价格本身就是对今天价格的最小二乘最优常数预测。网络并没有学会任何市场规律它只是学会了一个聪明的复读机。解决不要直接对原始价格做回归先对序列做差分或转成对数收益率让目标变成“涨跌幅”消除价格尺度上的强自相关。更工程化的做法是同时报告方向准确率def directional_accuracy(y_true, y_pred): y_true np.asarray(y_true).flatten() y_pred np.asarray(y_pred).flatten() true_dir np.sign(y_true[1:] - y_true[:-1]) pred_dir np.sign(y_pred[1:] - y_pred[:-1]) return np.mean(true_dir pred_dir)方向准确率高于0.55才说明模型有超额信息低于这个值MSE再小也要回头改数据和目标定义。这条是判断一个“高分项目”是真有货还是曲线拟合最直接的办法。5.2 归一化泄漏未来数据训练集指标好看、实盘失效现象训练和测试阶段的MSE都很好但模型一旦接到一段没有经过训练的新行情预测能力立刻崩盘。原因先对全体数据执行了scaler.fit(all_data)再切窗。MinMaxScaler用全部行情的最小值和最大值做归一化测试期间的价格极值早在训练阶段就被模型“见过”测试误差被系统性压低。解决归一化统计量严格只从训练集提取验证集和测试集用同一个scaler做transform。推理阶段要把scaler像模型一样持久化保存线上新数据先走同一个transform再进模型。换一段全新增量数据后重新评估如果指标明显变差优先怀疑这条。5.3 把时间序列随机打散模型丢掉时序上下文现象训练时train loss下降快但val loss剧烈波动模型在验证集上反复横跳最终保存的模型看不出稳定规律。原因代码里用了train_test_split(X, y, shuffleTrue)或者DataLoader里shuffleTrue。时间序列一旦被打散LSTM读到的“前一天”是随机抽取的另一天序列的先后因果链被截断模型只能在残缺语境里瞎猜而且滑窗样本重叠shuffle之后训练集中混入了验证集/测试集的近邻样本评估形同虚设。解决切窗时按时间顺序切分DataLoader传shuffleFalse。如果觉得训练不收敛想增加随机性可以在每个epoch内部对样本做一次合法shuffle——只打乱样本之间的出现顺序不打乱样本内部的时间顺序这样能增加梯度随机性又不破坏序列结构。但最直接可靠的还是关掉shuffle。5.4 未复权价格里的除权跳空模型学进一堆假特征现象预测曲线在某个日期附近出现异常的尖峰误差test loss整体还行单独看那一段惨不忍睹翻数据发现那天收盘价从30元直接变到15元。原因这种跳空大概率是股票除权比如10送10除权后股票内在价值没变但价格被重新标定。LSTM不知道什么是除权它把这个虚假的50%跌幅当成真正的基本面变化来学等于往训练数据里塞了一个极端错误标签。解决重新拉取前复权数据让历史价格序列保持连续。如果数据已经固定改不了在预处理里把异常跳空的样本剔除或者把那几天单独标成一个特殊特征让LSTM学会忽略。排查套路就是第2章里那段pct_change阈值检查单日涨跌超20%这天高亮标出来判断是除权还是数据错误。5.5 loss不降或变成nan先查数据再调学习率现象训练到几十个epochloss变成nan或者loss一直在一个水平附近震荡怎么调都不降。原因nan通常来自梯度爆炸、数据里有inf或NaN、或学习率过大。loss不降而数据正常时多半是学习率跟模型规模不匹配或者网络层数太深发现在这种小数据集上根本训不动。解决按顺序排查。第一步检查数据里有没有NaN和infnp.isnan(X).sum()一行就能定位。第二步把学习率从1e-3降到3e-4或1e-4重训学习率是LSTM项目里改动回报率最高的旋钮。第三步在backward之后加clip_grad_norm_把梯度范数限制在1.0到5.0之间。第四步把hidden_size从128降到64层数从2降到1先让模型跑出一个比随机略好的loss再逐步加容量。这个顺序不要跳跳过第二步直接调网络结构经常会浪费很多时间在一个其实只差学习率的模型上。6. 从价格到信号方向准确率与样本外验证6.1 把回归输出转成涨跌信号价格预测本身很难直接当交易信号因为预测的收盘价即使只偏差0.5%方向反了照样亏钱。一种常见做法是把模型输出转成涨跌方向再叠加阈值过滤pred_ret np.diff(y_pred.flatten()) / y_true[:-1] signal (pred_ret 0.02).astype(int) # 预测涨幅超过2%的日子标记信号 true_ret np.diff(y_true.flatten()) / y_true[:-1] hit_ratio np.mean((signal 1) (true_ret 0.02))hit_ratio的含义是模型说“明天要涨2%”的时候市场到底涨没涨。这个指标比MSE更贴近交易场景也让模型的输出具备可解释性。阈值0.02可以按股票波动率调整波动大的股票抬高到0.03波动小的压到0.01目的是让信号覆盖的日子数量不至于太少或太多。6.2 最后一公里我的封存测试习惯我最后想说的是模型验证。很多项目把测试集评估当成终点但测试集在调参过程里被反复看过指标会慢慢失真。我的做法是把最后一到两个月的行情完全封存训练、验证、调超参、选模型都不碰它全部定稿后只跑一次。这个习惯帮我挡掉过不少自我感觉良好的模型——有的在旧测试集上DA做到0.6一碰封存数据立刻跌回0.5说明模型学到的是那一段行情的特定噪声。反过来封存数据上方向和收益同时为正才值得把它接进真实的量化策略流程。如果你手上这个项目跑完不妨也留一段行情封存训练过程再急也不去碰它。希望帮到你。本文还有配套的精品资源点击获取