
简介基于PSO-LSTM神经网络的股票调整收盘价预测Python源码是一份面向高校期末大作业与课程设计的可直接运行项目。代码含完整注释并通过粒子群算法自动寻优学习率、隐含层节点等关键超参数结合LSTM完成对调整收盘价的单步预测逻辑清晰新手也能快速读懂全流程。压缩包共10个文件核心为1个Python预测脚本另配有7个CSV格式的股票历史数据集涵盖美股、A股、黄金等不同标的、1份项目说明与1个备注文本总计仅490KB轻量易部署。目前已有171人学习适合金融时序预测、深度学习相关课程的高分参考。下载后即可直接运行按注释修改数据路径与模型参数再利用多组行情数据完成对比验证为期末答辩或实验报告提供扎实的代码与数据支撑。1. 为什么是 PSO-LSTM调整收盘价预测这个场景传统模型先输一半拿到「基于 PSO-LSTM 神经网络的股票调整收盘价预测 Python 源码.zip」这类项目第一反应通常是两个疑问LSTM 做股价预测不早就被讲烂了吗PSO 又是来干嘛的实际上当目标从「收盘价」换成「调整收盘价」之后问题性质变了序列里藏着除权除息造成的价格跳空普通模型会把跳空当趋势学预测曲线一出测试集就“飞”了。LSTM 能记住较长周期内的涨跌结构但对超参数极其敏感hidden units 设成 32 还是 128预测滞后能从 1 个交易日拉长到 5 个交易日。PSO 在这里解决的就是“LSTM 参数靠手感”的问题用粒子群把学习率、隐层节点数、时间步长这批超参自动搜出来。这条技术路线适合两类人一类是刚接触量化但不想只停留在调用 sklearn 回归的初学者想看看神经网络怎么端到端地吃行情数据另一类是已经跑过 LSTM 但被调参折磨过的熟手想找一个可复现的超参数搜索方案。源码包的落地价值不在“预测准确率有多高”而在于它把「数据预处理 → PSO 寻参 → LSTM 训练 → 反归一化回测」这条链路完整走通了。下面按这条链路拆开讲。2. PSO 和 LSTM 为什么要组合从“黑匣子”到参数可解释2.1 LSTM 被选中的理由时序记忆与调整收盘价的连续性在股票序列建模里常见神经网络候选有四类前馈神经网络BP 神经网络、卷积神经网络、LSTM 神经网络、Transformer 神经网络。BP 神经网络和卷积神经网络有一个共同短板——它们默认样本独立要把时间上下文硬塞进特征工程里比如用过去 5 天的涨跌幅拼成一个 5 维向量。这种做法在行情波动平缓时勉强够用一旦遇到连续跳空或长时间的横盘震荡手工特征很难把“这个走势和三个月前某段走势相似”这种长程依赖表达出来。LSTM 神经网络的核心机制是门控单元输入门决定新信息写入多少遗忘门决定记忆单元丢弃多少输出门决定当前时刻放出多少。三个门配合让梯度在时间维度上能传得更远这就是它比 BP 神经网络更适配金融时序的原因。调整收盘价序列有一个特点——经过复权处理后价格是连续的但连续性里嵌入了“除权除息日附近波动率骤增”的局部异常LSTM 的遗忘门可以学会在这些局部异常之后把旧状态重置这是普通前馈网络做不到的。2.2 PSO 到底在优化什么超参数搜索的三种常见姿势LSTM 的超参数组合空间极大时间步长seq_len、隐层节点数hidden_units、网络层数num_layers、学习率、batch size、dropout、优化器类型。每个参数取 5 个候选值组合数就是 5 的 6 次方接近 1.6 万种逐个跑网格搜索在 CPU 上够跑一个通宵。常见做法有三种替代方案。第一种是随机搜索从参数空间里随机采样 N 组跑完取最优第二种是贝叶斯优化用高斯过程拟合“参数 → 验证集误差”的响应面每次迭代选最有潜力的点第三种就是粒子群优化PSO。PSO 的思路很直白初始化一群粒子每个粒子代表一组超参数粒子在参数空间里飞飞行的方向由两个因素决定——粒子自己历史上找到过的最优位置pbest和整个群体找到过的最优位置gbest。PSO 相对网格搜索的核心优势是“用梯度无关的方式做智能搜索”它不假设参数和目标函数之间有什么光滑关系在离散参数如层数和连续参数如学习率混合的场景里可以直接编码。相对贝叶斯优化PSO 的实现更简单不需要维护代理模型一个速度更新公式加一个位置更新公式就能迭代。它的代价是需要多次评估适应度每一轮都要完整训练一次 LSTM所以实际工程里通常把粒子数控制在 815迭代次数控制在 510 轮。2.3 PSO-LSTM 的最小数据流从行情列表到预测张量把整条链路的张量形状理清楚后面读源码会顺畅很多。假设拿到的历史行情是 DataFrame包含 date、open、close、high、low、volume、adj_close 七列按日期升序排列。第一步是构造滑窗样本。设定 seq_len20意思是“用过去 20 个交易日的调整收盘价预测下一个交易日的调整收盘价”。滑动窗口产生 N-20 个样本每个样本的形状是 (20, feature_dim)其中 feature_dim 是特征列数。如果只用调整收盘价一个特征feature_dim1如果加入成交量、涨跌幅、最高最低价差等特征feature_dim 会变大。第二步是划分数据集。注意这里是时间序列不能随机打乱。常见切法是前 70% 做训练集中间 15% 做验证集最后 15% 做测试集。验证集在 PSO 流程里承担“适应度评估”的角色测试集从头到尾不参与任何参数选择只在最后评估一次。第三步是归一化。调整收盘价序列的量纲通常在几元到几百元之间而 LSTM 内部激活函数对输入尺度敏感所以要用 MinMaxScaler 把数据压到 [0,1] 区间。这里有三个细节容易被忽略一是 scaler 只能 fit 训练集验证集和测试集用同一个 scaler 做 transform防止未来数据信息泄漏二是如果序列里有极端值比如某天因为异常交易出现 50% 的巨幅波动MinMax 会被拉偏可以考虑改用 RobustScaler三是预测出的结果必须做反归一化才能和真实价格对比否则 RMSE 数值小得漂亮但毫无意义。3. 调好“调整收盘价”这个输入前后复权、缺失值与特征工程3.1 调整收盘价 vs 不复权收盘价为什么必须用复权序列很多初学者拿到行情数据后直接取 close 列开始训练这是第一个翻车点。股票在除权除息日会有一个价格跳变10 送 10 的股票除权日开盘价直接腰斩。如果把这个跳变喂给 LSTM模型会学出一个“莫名其妙跌了 50%”的模式后续预测全部被带偏。调整收盘价adjusted close解决的就是这个问题。它的计算方式是把历史价格按分红送股比例统一折算到当前口径常见有两种前复权和后复权。前复权保持最新价格不变往前调整历史价格后复权保持最早价格不变往后调整。对训练 LSTM 来说后复权序列更合理——最新价格是动态变化的前复权会随着每次新除权事件把整段历史价格重新折算一遍导致训练数据的数值每天都在变模型不可复现。实际工程坑在于不同数据源的调整收盘价算法不一样。有的数据源只考虑现金分红不考虑送转股有的把两者都算进去还有的会把配股也折算进去。同一只股票两个数据源给出的 adjusted close 可能差 3%5%训练出来的模型在测试集上的表现会完全不同。所以项目里必须在数据加载阶段打印数据源名称和复权方式否则预测结果“漂移”了都不知道是模型问题还是数据问题。3.2 数据清洗与滑窗构造训练集/验证集/测试集的时间顺序股票数据不像 MNIST 那样拿过来就能训练清洗步骤缺一不可。第一步是去重同一交易日出现多行记录通常是因为盘中多次采集保留最后一行的收盘数据即可。第二步是去停牌日停牌期间没有交易价格是空值或前值填充这些行要直接删掉否则滑窗里混入大量重复价格等于给模型灌了噪声。第三步是跳空检测交易日之间如果出现超过 20% 的涨跌幅要检查是否为数据错误比如单位搞错、小数点移位。滑窗构造里还有个容易忽略的细节窗口之间是否重叠。如果 stride1相邻两个样本有 19 天的数据重叠这会让验证集和训练集之间存在大量信息重复模型评估指标虚高。更稳妥的做法是训练集内允许重叠来增加样本量但验证集和测试集的起始位置要在时间上严格后移保证验证集完全不包含训练集窗口里的数据。代码层面滑窗构造通常写成下面这样import numpy as np import pandas as pd def create_sequences(data, seq_len20, stride1): xs, ys [], [] for i in range(0, len(data) - seq_len, stride): x data[i : i seq_len] y data[i seq_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) # 假设 df 已按日期升序排列且只取 adj_close 列 values df[adj_close].values.reshape(-1, 1) train_val_cut int(len(values) * 0.7) val_test_cut int(len(values) * 0.85) train_raw values[:train_val_cut] val_raw values[train_val_cut:val_test_cut] test_raw values[val_test_cut:] X_train, y_train create_sequences(train_raw, seq_len20) X_val, y_val create_sequences(val_raw, seq_len20) X_test, y_test create_sequences(test_raw, seq_len20)这段代码里有个细节create_sequences 对训练集和验证集分别调用而不是先拼起来再切就是为了防止窗口跨段导致数据泄漏。如果你发现验证集效果比训练集还好先怀疑是不是这里写错了。3.3 特征归一化MinMaxScaler 的拟合时机与回测陷阱归一化是整个流程里看起来最简单、实际最容易埋雷的一步。错误示范是先对全量数据做 MinMaxScaler再切训练集/验证集/测试集——测试集的 min 和 max 已经参与了缩放等于让模型在训练时就“瞥见”了未来的价格区间回测指标会好得离谱实盘直接打回原形。正确做法是先切分、再归一化。训练集上 fit 出 scaler然后对验证集和测试集分别 transform。写成代码是from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw) val_scaled scaler.transform(val_raw) test_scaled scaler.transform(test_raw)注意 predict 之后的反归一化必须使用同一个 scaler 对象而不是重新 fit。很多源码包在这里写错最后画出来的预测曲线和真实曲线数值对不上。反归一化这行代码在预测阶段要反复用到建议封装成一个函数避免在多个 notebook 单元格里复制粘贴出错。4. 把源码跑通PSO-LSTM 核心代码与关键参数说明4.1 项目文件结构与运行顺序这类源码包的结构通常遵循“数据 → 模型 → 优化 → 回测”的四层约定。拿到 zip 解压后先看有没有 README 或 requirements.txt前者告诉你文件用途后者告诉你依赖库清单。一个典型结构大致是这样data/ 目录存放行情数据抓取脚本或 CSV 文件pso.py粒子群优化算法实现lstm_model.pyLSTM 网络结构定义train_pso.py主程序串联数据加载、PSO 迭代、模型训练evaluate.py测试集评估与可视化运行顺序是先跑数据准备脚本生成 CSV再跑 train_pso.py最后跑 evaluate.py。如果跳过数据准备直接训练大概率报文件不存在错误。这里不建议在 Windows 命令行里直接python train_pso.py一把梭建议用 vscode 打开项目根目录配置好 Python 环境后逐文件运行遇到 import 报错能在编辑器里直接追溯。4.2 PSO 粒子与适应度函数PSO 的 Python 实现核心是粒子类。每个粒子的位置是一个一维向量每一位对应一个超参数。常见的编码方式是[learning_rate, hidden_units, num_layers, seq_len, batch_size]其中 learning_rate 是连续变量后四个是整数变量。位置更新公式需要处理整数取整问题通常的做法是每次更新后对相应维度做round()并限制在搜索边界内。import numpy as np class Particle: def __init__(self, bounds): self.position np.array([ np.random.uniform(low, high) for low, high in bounds ]) self.velocity np.zeros(len(bounds)) self.pbest_position self.position.copy() self.pbest_value float(inf) def clamp(self, bounds): for i in range(len(bounds)): low, high bounds[i] self.position[i] np.clip(self.position[i], low, high) if i 0: # 整数参数取整 self.position[i] round(self.position[i])这里对第 0 维learning_rate不做取整因为它需要连续值其他维度取整后送入 LSTM 模型才能作为 hidden_units、num_layers 这类整数参数使用。适应度函数的写法决定了 PSO 能否收敛。它接收一组超参数构建 LSTM在训练集上训练若干个 epoch返回验证集上的 RMSE。这个函数会被反复调用所以一定要控制训练时间和 epoch 数。def fitness_function(params, X_train, y_train, X_val, y_val): lr, hidden_units, num_layers, seq_len, batch_size params hidden_units int(hidden_units) num_layers int(num_layers) seq_len int(seq_len) batch_size int(batch_size) model LSTMPredictor( input_size1, hidden_unitshidden_units, num_layersnum_layers ) history train_model( model, X_train, y_train, lrlr, epochs15, batch_sizebatch_size ) val_pred predict(model, X_val) rmse np.sqrt(np.mean((y_val - val_pred) ** 2)) return rmse关键细节是 epochs 不能设太大否则一个粒子就要跑几分钟整个 PSO 流程在地铁上都跑不完。1520 个 epoch 足够分出参数好坏最后拿到最优参数后再用更多 epoch 做正式训练这是 PSO-LSTM 项目里最常见的省时间技巧。4.3 LSTM 模型构建与训练LSTM 网络结构在 PyTorch 下的写法比较固定。单层或多层 LSTM 后接一个全连接层输出预测值。注意输入张量的维度顺序是(batch, seq_len, input_size)很多人在这里翻车把维度传成(seq_len, batch, input_size)导致训练时形状不匹配。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_units64, num_layers2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_units, num_layersnum_layers, batch_firstTrue, dropout0.2 if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_units, 1) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的输出 out out[:, -1, :] return self.fc(out)batch_firstTrue 这个参数值得专门提一句PyTorch 的 LSTM 默认把 batch 放在第 1 维第 0 维是时间步而 PyTorch 的 DataLoader 产出的张量是 batch 在第 0 维加了 batch_firstTrue 之后两者才对得上省去转置的麻烦。训练循环里损失函数用 MSELoss优化器用 Adam。Adam 对学习率的敏感度比 SGD 低配合 PSO 搜出来的 lr 值更稳健。代码里有一个工程细节训练过程中记录每个 epoch 在验证集上的 loss如果连续 5 个 epoch 没有改善就 early stop避免粒子评估浪费时间。4.4 预测、反归一化与结果输出训练完成后进入预测阶段。预测的核心逻辑是用最后 seq_len 个训练数据作为初始窗口逐日滚动预测。每预测出一天的值就把它加入窗口末尾、丢掉窗口最前面的值保持窗口长度不变。这是时间序列预测的标准做法但实现时有个坑预测出的值要反归一化后再塞回窗口下一次预测时再重新归一化否则误差会累积。def rolling_predict(model, scaler, last_window, predict_days30): model.eval() window last_window.copy() predictions [] for _ in range(predict_days): with torch.no_grad(): x torch.FloatTensor(window).unsqueeze(0) # (1, seq_len, 1) pred_scaled model(x).item() pred_price scaler.inverse_transform( np.array([[pred_scaled]]) )[0][0] predictions.append(pred_price) # 更新窗口用反归一化后的真实价格尺度 new_val pred_scaled window np.roll(window, -1, axis0) window[-1] new_val return np.array(predictions)这段代码里 window 是 shape 为(seq_len, 1)的二维数组np.roll 把整体前移最后一个位置填入新预测值。注意 new_val 用的是归一化后的值因为 window 本身是归一化尺度混入反归一化价格会破坏输入分布。评估阶段输出三样东西测试集预测曲线图、RMSE 数值、方向准确率预测涨跌与真实涨跌一致的占比。方向准确率比 RMSE 更能反映模型的实际交易价值源码包里通常用一行np.mean((np.diff(y_true) * np.diff(y_pred)) 0)计算这个指标建议保留。5. PSO-LSTM 调参与踩坑5 个高频问题的现象、原因与解法5.1 适应度曲线不下降每个粒子都在原地打转现象PSO 迭代 5 轮gbest 的 RMSE 几乎没有变化打印每个粒子的位置发现大家集中在初始位置附近。原因多半是适应度函数写错了粒子位置变化后没有正确映射到模型超参数典型错误是把整数参数直接浮点传入了 nn.LSTM 的 hidden_sizePyTorch 里 hidden_size 是浮点时不会立刻报错但会被截断成同一个值导致所有粒子的模型结构完全相同。另一个常见原因是归一化写在了滑窗之前导致每个粒子的训练数据都一样模型毫无区分度。解决在 fitness_function 开头打印当前粒子的位置和训练数据 shape人工验证两三轮把 hidden_units、num_layers、seq_len 全部强制 int() 并断言值大于 0确认归一化是 fit 在训练集上。5.2 训练集拟合得很好测试集预测曲线是一条平移的滞后线现象测试集上预测曲线和真实曲线形状几乎一致但整体向右平移了 12 个交易日方向准确率只有 50% 上下。这是 LSTM 单步预测的经典翻车现场。原因是模型学习到“明天的价格约等于今天的价格”这种朴素规律在平稳行情里这已经是最小化 MSE 的最优解而 PSO 的适应度函数恰恰是 RMSE于是它会把参数往“最懒预测”的方向引导。解决换适应度函数。把 RMSE 改成混合指标例如0.5 * RMSE 0.5 * (1 - direction_accuracy)让粒子不仅要预测准价格还要预测准方向或者把预测目标从“明日收盘价”改成“未来 5 日收益率”迫使模型学习中期趋势而不是拟合一阶自相关。5.3 PSO 过早收敛到局部最优搜索到的参数和随机生成的差不多现象gbest 在第二轮就不再更新最终参数接近初始化值的中心点。原因通常是粒子数量和迭代次数不匹配——粒子数太少比如 4 个、迭代次数太少比如 3 轮粒子还没飞开就停了也可能是速度更新式的惯性权重 w 设得太小小于 0.3粒子缺乏全局探索能力。解决惯性权重 w 从 0.9 线性衰减到 0.4这是 PSO 论文里最经典的设置。c1 和 c2 分别设成 2.0 和 2.0如果发现收敛太快就调大 w 上限到 1.2。粒子数建议 1012 个迭代次数至少 8 轮一轮跑不完就缩短单粒子的训练 epoch。5.4 调整收盘价数据源不一致导致预测结果漂移现象同一时间段、同一只股票换了一个数据源之后RMSE 从 1.2 变成了 2.8但你根本没改模型代码。原因就是数据源复权算法不一致。有的数据源对分红送股做的是“等比复权”有的是“等差复权”两者的历史序列在前 5 年会差出一大截。还有的数据源把未复权价格和复权因子分开给源码里如果只读了 close 而没乘上复权因子就回到了第一节说的“跳空陷阱”。解决在数据加载后加一个断言校验——检查最近一个交易日的 adj_close 是否等于 close正常情况下它们应该完全相等因为最新价格不需要复权再往历史方向抽查 3 个除权日附近的价格确认没有 50% 左右的跳变。两处都通过再进入训练流程。5.5 CPU 环境下的训练时间失控一个粒子要跑 10 分钟现象PSO 共 10 个粒子、迭代 8 轮每轮每个粒子训练 30 个 epoch总计 2400 次模型训练CPU 上跑完要十几个小时。原因不是代码慢而是评估次数太多。解决思路有三个方向第一个方向是缩小搜索空间把 seq_len 固定在 20只搜 learning_rate、hidden_units、num_layers 三个维度第二个方向是提前终止验证集 loss 连续 3 个 epoch 不降就跳出训练第三个方向是群体记忆把每个粒子历史训练过的超参数组合缓存起来遇到相同组合直接复用历史 RMSE不重复训练。第三种方式在粒子数较多时能省掉 30% 以上的训练时间值得写进源码。6. 从“跑通”到“可信”残差检验与滚动预测的进阶验证源码包跑通只是起点评测 PSO-LSTM 值不值得用于实盘必须做两件事残差检验和滚动预测。残差检验关注的是预测误差是否还有可利用的模式。把测试集预测值和真实值做差得到残差序列然后看三样东西残差均值是否接近 0残差是否存在显著自相关用 Ljung-Box 检验p 值小于 0.05 说明残差里还有信息没被模型学到残差方差是否随时间变化。如果残差表现出明显的“波动聚集”——大涨大跌后残差也大、平稳期残差也小说明模型没捕捉到波动率变化此时再调 LSTM 结构意义不大应该考虑把 GARCH 类模型或波动率特征加进去。滚动预测比一次性切分更接近实盘场景。按顺序做 20 次实验每次用前 80% 的数据训练预测接下来 5 个交易日然后整个窗口往后推 5 天。20 次预测串起来形成一个连续的预测序列在这条序列上计算 RMSE 和方向准确率。滚动预测的意义在于它暴露了模型在不同市场状态下的稳定性——单次测试集只代表一段行情滚动的结果才能看出模型在上涨、下跌、横盘三种状态下分别什么表现。如果滚动预测结果表现不错再考虑能不能把预测结果落成一个简单的交易信号比如“预测未来 5 日收益为正且超过阈值时持有”。但这里有一条必须守住的红线任何基于历史价格的预测模型在真实市场里都要面对无法消除的不确定性回测指标永远只是下限而非上限。我自己的习惯是跑完 PSO-LSTM 之后先把测试集预测图保存下来和真实 K 线叠加肉眼确认预测曲线在关键拐点处的滞后程度这个步骤比任何指标都更能让人清醒——模型学到的是规律还是幻觉一眼就能看出来。记住神经网络是黑匣子但你的验收流程不能是黑匣子。希望这篇拆解能帮你把这个源码包从“跑通”推进到“看懂”少走几步我曾经踩过的弯路。本文还有配套的精品资源点击获取