多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LSTM股价预测实战:从tushare数据处理到PyTorch模型搭建与回测

LSTM股价预测实战:从tushare数据处理到PyTorch模型搭建与回测 简介一套以Python与机器学习LSTM为核心的股票/基金预测模型毕业设计项目面向计算机相关专业毕设学生、课程设计及金融数据分析实战学习者。压缩包共11个文件整体155KB内含5个Excel数据文件分别对应训练集、测试集、预测结果与近一年基金排行3个Python脚本覆盖数据采集、LSTM建模和预测可视化另有README说明、效果图及缓存文件源码可独立运行并支持二次修改。已有51人学习下载。资源完整演示了从股票/基金数据获取、清洗预处理、LSTM网络设计、模型训练评估到调优预测的全流程结合时间序列分析理论能帮助读者理解门控机制如何捕捉长期趋势与短期波动。通过复现项目可同时提升Python编程、TensorFlow/Keras建模能力并为毕业设计或课程答辩提供可直接使用的完整方案。1. 为什么毕业设计和量化实战都押注LSTM做股价预测先看适用边界LSTM在股票和基金预测里被用得最多不是因为它在K线图上画线好看而是因为长短期记忆网络天生适合处理时间序列——它能在几百个历史交易日里记住“上次类似形态之后涨了还是跌了”。用Python从tushare拉数据、做滑窗、训练一个两层LSTM再把预测结果画回K线图这个流程几乎覆盖了机器学习时序预测的全部关键技术点所以很多毕业设计会选它量化入门者也拿它练手。但我要先泼一盆冷水LSTM预测的是“未来一段走势的概率分布”不是“明天涨到多少钱”。把它当成预测涨跌方向、辅助判断趋势强弱的模型它有用当成提款机必亏。这篇文章按我实际复现过的一套流程走从数据、预处理、模型搭建到评估和排坑完整给你能抄作业的代码并标出哪些地方是毕业设计答辩时最容易翻车的点。2. 数据准备与预处理从tushare到MinMaxScaler的完整闭环2.1 数据源选择与字段口径做股票或基金预测第一步是拿到干净的历史行情。常见做法是用tushare或者akshareA股日线数据用tushare的pro接口更稳定。这里强调一个口径问题预测目标到底是“收盘价”还是“收益率”。我建议先把收盘价转成对数收益率作为训练标签因为原始价格序列非平稳LSTM直接拟合价格容易让Loss震荡而且不同股票的价格绝对数值差异很大模型学到的尺度不具有迁移性。数据字段最少需要date、open、high、low、close、volume。如果拿得到pre_close一定要保留因为它能最准确地计算当日涨跌幅。import tushare as ts import pandas as pd ts.set_token(你的token) pro ts.pro_api() df pro.daily(ts_code600519.SH, start_date20150101, end_date20241231) df.sort_values(trade_date, inplaceTrue) df[trade_date] pd.to_datetime(df[trade_date]) df.set_index(trade_date, inplaceTrue) df[ret] df[close].pct_change() df[log_ret] np.log(df[close] / df[pre_close])这段代码里我同时算了pct_change和log_ret后面建模优先用log_ret。pct_change是按当天收盘相对前一天收盘计算而log_ret用pre_close计算能准确反映交易行为产生的真实收益避免因为除权除息导致价格跳变污染序列。2.2 滑窗构造监督学习样本LSTM不能直接吃一整段连续序列做预测它需要你把历史序列切成“固定窗口 下一时刻标签”的监督样本。窗口长度是第一个关键参数。常见做法是用20到60个交易日对应一个月到三个月。窗口太小模型只看到短期动量窗口太大LSTM的记忆能力被稀释而且训练样本数量骤减。我一般用60天窗口预测下一天因为A股一个月平均22个交易日60天能覆盖一个完整短期趋势周期。切窗时要注意样本之间不要重叠太多否则训练集和验证集会高度相似回测指标虚高。import numpy as np def create_sequences(data, window60): X, y [], [] for i in range(window, len(data)): X.append(data[i-window:i]) y.append(data[i]) return np.array(X), np.array(y) # 用log_ret和volume作为特征 feature_cols [log_ret, volume] scaled_features scaler.transform(df[feature_cols]) X, y create_sequences(scaled_features, window60)X的形状是(N, 60, 2)N是样本数60是时间步2是特征数。这个形状直接喂给PyTorch的LSTM不需要额外reshape。y是下一时刻的log_ret训练时用MSELoss回归。2.3 归一化与训练/验证切分归一化这里有个隐蔽坑必须用训练集的scaler去变换验证集和测试集绝对不能在切分之前对全量数据做fit。否则验证集信息通过scaler泄漏到训练过程模型在验证集上的表现会虚高。这个错误在毕业设计里出现的概率极高答辩老师随便一问就问出来了。from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) train_size int(len(df) * 0.8) train_data df[feature_cols].iloc[:train_size] test_data df[feature_cols].iloc[train_size:] scaler.fit(train_data) train_scaled scaler.transform(train_data) test_scaled scaler.transform(test_data) X_train, y_train create_sequences(train_scaled, window60) X_test, y_test create_sequences(test_scaled, window60)MinMaxScaler会把log_ret这种有正有负且数值很小的序列压缩到0到1之间有利于LSTM的tanh激活函数工作。注意create_sequences切出来的X_test和y_test它们已经比test_data短了60个样本因为前60天要当窗口。测试集评估时的“真正预测能力”严格来说是从第60个测试样本之后才开始算这点在评估指标里我会强调。3. 搭建LSTM预测模型PyTorch实现与参数详解3.1 网络结构设计对于日线级别的股价预测不需要很深的LSTM。叠加太多层容易过拟合而且训练时间成倍增长。我用得比较多的是两层LSTM加一层全连接输出第一层LSTM把序列编码成隐藏状态第二层继续抽象时间特征最后接一个线性层把隐状态映射到未来一个时间步的预测值。hidden_size取64到128之间比较稳妥。import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size2, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm1 nn.LSTM(input_size, hidden_size, num_layers1, batch_firstTrue) self.lstm2 nn.LSTM(hidden_size, hidden_size, num_layers1, batch_firstTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm1(x) out, _ self.lstm2(out) out self.dropout(out[:, -1, :]) out self.fc(out) return out这里拆成两个单层LSTM而不是一个双层LSTM目的有两个一是每一层可以单独设置dropout二是方便观察中间层的hidden state在做什么。batch_firstTrue后输入维度就是(batch, seq_len, input_size)这是PyTorch里最容易配错的地方很多人忘记加这个参数导致维度报错。out[:, -1, :]取最后一个时间步的输出因为我们要预测的是下一个时刻用完整序列编码后的最终状态。3.2 训练脚本与损失函数训练循环本身不复杂关键是几个细节优化器用Adam学习率初始0.001损失函数用MSELoss因为回归目标每个epoch做一次验证集评估保存验证Loss最低的模型权重。不要等到训练结束再统一保存LSTM训练过程波动大最好的模型往往出现在中间某一个epoch。def train_model(model, X_train, y_train, X_val, y_val, epochs100, lr0.001): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.MSELoss() best_val_loss float(inf) for epoch in range(epochs): model.train() optimizer.zero_grad() output model(X_train) loss criterion(output, y_train.unsqueeze(1)) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val.unsqueeze(1)) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_lstm.pth) if epoch % 10 0: print(fepoch {epoch}, train loss {loss.item():.6f}, val loss {val_loss.item():.6f})y_train的shape是(N,)而模型输出是(N,1)所以要unsqueeze(1)。训练时X_train必须是torch.Tensor类型前面切窗得到的是numpy数组要记得转换。一个常见的翻车点是忘记对梯度做裁剪LSTM很容易在训练中后期出现梯度爆炸Loss突然变成nan。建议在optimizer.step之前加一句torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)。3.3 超参数怎么调才有说服力超参数不能靠感觉否则答辩时“为什么hidden_size64”这种问题直接卡住。常见做法是把窗口长度、hidden_size、学习率、dropout做成一个网格用小步长跑几组对比选验证Loss最低的组合。但这里有个实操技巧先固定epoch50用默认参数跑通整个流程确认数据、代码没问题再开始调参。一上来就调参如果代码有bug浪费时间。我一般重点关注两个参数窗口长度和dropout。窗口长度影响模型能看到多长的历史依赖dropout影响过拟合程度。学习率用0.001上下浮动0.0005就够了不需要大动。hidden_size从32、64、128里选超过128对日线数据几乎没提升。调参时把每组参数对应的train/val loss记录到CSV里最后画一张对比表。这张表在毕业设计文档里是实打实的实验证据比口头说“调了很多次”有说服力得多。4. 预测效果评估回测、评价指标与可视化4.1 评估指标选什么股价预测不能只看MSELoss因为MSE对数值尺度敏感波动大的股票天然MSE高。我习惯用三个指标一起看RMSE、MAE、方向准确率。其中方向准确率是“预测的涨跌方向与实际涨跌方向是否一致”这个指标跟交易决策直接挂钩也比RMSE更直观。from sklearn.metrics import mean_squared_error, mean_absolute_error pred_close scaler.inverse_transform(np.hstack([y_pred, np.zeros((len(y_pred), 1))]))[:, 0] true_close scaler.inverse_transform(np.hstack([y_test, np.zeros((len(y_test), 1))]))[:, 0] rmse np.sqrt(mean_squared_error(true_close, pred_close)) mae mean_absolute_error(true_close, pred_close) direction_acc np.mean(np.sign(pred_close - true_close_prev) np.sign(true_close - true_close_prev))这段代码有个关键操作把预测的log_ret反归一化回原始价格。因为MinMaxScaler同时变换了log_ret和volume反归一化时需要用scaler的inverse_transform并构造一个和训练特征相同形状的数组然后取第一列。方向准确率的计算要选定一个基准价格比如前一交易日的实际值不能拿预测值自己跟自己比否则会出现序列自相关的虚假高准确率。4.2 回测逻辑要模拟真实交易评估模型不能只算指标要把它放进一个模拟交易流程里看资金曲线。最简单可用的回测逻辑是每天根据模型预测的下一日收益率若预测为正则持仓为负则空仓按收盘价成交不考虑手续费和滑点。这样能直观看出模型是否具备实际决策价值。capital 1.0 positions 0 for i in range(len(pred_close)): pred_ret pred_close[i] / true_close_prev[i] - 1 if pred_ret 0: positions 1 else: positions 0 daily_ret (true_close[i] / true_close_prev[i] - 1) * positions capital * (1 daily_ret) sharpe (capital ** (252 / len(pred_close)) - 1) / np.std(daily_ret)这个回测忽略了次日开盘跳空和涨停买不进的问题但作为毕业设计或初步验证已经足够。回测结果要跟基准比比如同期持有不动、沪深300指数否则单独看资金曲线没有意义。我见过很多学生跑出年化50%的曲线一对比基准曲线才发现基准也涨了40%模型并没有超额收益。4.3 可视化K线、预测值、误差分布的三个图可视化部分至少要三张图第一张是测试集真实收盘价和预测收盘价的对比折线图第二张是回测资金曲线与基准资金曲线对比第三张是预测残差分布直方图。前两张展示效果第三张展示残差是否近似正态分布、是否存在明显偏倚。画图用matplotlib就行但要注意反归一化之后画且要对齐索引。预测结果通常比真实值滞后画出来会看到预测曲线比真实曲线平移了一个交易日这是LSTM回归的常见现象本质是模型学到的是“用过去序列预测下一天”但下一天的真实值跟当天高度相关模型更倾向于输出近似当天值。这个问题在第5章细说。5. 股票/基金预测避坑指南数据泄漏、未来函数与归一化陷阱5.1 现象训练集准确率99%实盘一买就亏训练集上方向准确率接近100%但应用到新数据完全失效。这是典型的过拟合加数据泄漏双料翻车。原因通常是切窗时用了全量数据做归一化或者随机切分了时间序列。时间序列绝对不能随机打乱必须按时间顺序切分否则未来数据会混进训练集。解决方法是严格按时间顺序前80%训练、后10%验证、最后10%测试而且scaler只fit训练集。从那以后我每次写预处理都强制走一遍先切分再fit再transform顺序颠倒就重来。5.2 现象预测曲线整体滞后一天模型预测的收盘价曲线和真实曲线几乎重合但整体向右平移了一到两天看起来预测得很准实盘却无法执行。原因是LSTM用过去60天预测下一天而股票价格序列具有强自相关性模型发现最省力的做法是输出当前价格因为当前价格和明天价格差异本来就小。这会导致回归指标很好方向准确率却只在50%左右。解决办法是改用“预测未来第5天或第10天价格”拉长预测距离迫使模型学习趋势性特征。还有一种思路是预测涨跌分类而不是回归价格用交叉熵损失训练分类器绕过价格自回归的陷阱。5.3 现象不同随机种子训练结果差异巨大同一份数据同样的参数换一个随机种子验证Loss忽高忽低方向准确率差距超过10%。这说明模型容量偏大训练数据量不足或者早期收敛阶段不稳定。解决方法是固定随机种子做实验并增加重复实验次数取多次结果的平均值作为最终指标。在代码开头设置torch.manual_seed(42)、np.random.seed(42)、random.seed(42)同时把数据加载器的shuffle参数关掉因为时间序列不需要shuffle。最稳妥的是跑5次不同种子把指标的均值和标准差都写进实验结果答辩时反而显得严谨。5.4 现象涨停板/停牌数据导致预测漂移A股有涨跌停限制涨停时成交量快速萎缩价格被锁在涨停价上如果你直接用当天close和volume训练模型会学到涨停状态下次日必跌的错误规律。基金净值也有类似问题巨额赎回导致的净值暴涨暴跌会污染序列。解决方法是先把这些异常样本剔掉或者在特征里加一个“是否涨停”“是否停牌”的0/1标记。我一般会在预处理阶段过滤掉停牌日和涨跌停日或者用前复权价格并做异常值截断比如把绝对收益率超过0.11的样本标记为异常并排除。5.5 现象验证集指标好但不同股票上表现完全不同在贵州茅台上训练出来的模型拿去预测五粮液效果大打折扣拿到科创板上更是直接失效。原因在于不同股票的波动率、流动性、价格行为差异太大模型学到的规律高度依赖训练集的统计特征。解决办法有两个一是按股票聚类训练不同的模型比如把波动率相近的股票归为一组二是用多个股票的数据混合训练让模型学习更普适的形态特征。毕业设计如果只做单只股票一定要设计跨股票测试证明模型不是只在某一只股票上有效否则答辩时会被质疑泛化能力。6. 把模型变成毕业设计可交付封装、参数搜索与一份能答辩的结论6.1 封装成可复用的预测器把训练逻辑封装成一个类让脚本可以接受股票代码、时间范围、窗口长度等参数运行后自动输出预测结果和回测指标。这样不仅方便你做多股票实验也方便答辩时现场演示。封装有一个基本要求训练、评估、预测三阶段必须严格分离预测阶段只能加载已训练好的权重不能再接触训练数据。class LSTMPredictor: def __init__(self, stock_code, window60, hidden_size64): self.stock_code stock_code self.window window self.hidden_size hidden_size self.scaler MinMaxScaler() self.model None def load_data(self): pass def train(self): pass def predict(self, recent_data): self.model.eval() with torch.no_grad(): scaled self.scaler.transform(recent_data) seq torch.tensor(scaled[-self.window:].reshape(1, self.window, -1), dtypetorch.float32) pred self.model(seq).item() return predpredict方法的关键在于输入必须是最近一个完整窗口的数据形状是(1, window, feature_dim)。很多人测试时忘记这步直接传一条数据进去维度立刻报错。封装好之后多股票实验只需要循环调用非常省事。6.2 网格搜索与最优参数记录网格搜索别用超大组合我建议先粗后细。粗搜索用64组以内的组合每组训练50个epoch记录验证Loss找到最优附近后再做一轮细搜索。每次运行把参数、指标、日期范围、随机种子完整记录到result表里。这个表是毕业设计里“实验与分析”部分的核心素材。param_grid { window: [30, 60, 90], hidden_size: [32, 64, 128], dropout: [0.1, 0.2, 0.3], lr: [0.001, 0.0005] } best_score float(inf) for params in itertools.product(*param_grid.values()): score run_experiment(**dict(zip(param_grid.keys(), params))) if score best_score: best_score score best_params params省内存的做法是每跑完一组就释放GPU显存并清空缓存否则跑几十组之后显存肯定溢出。CPU训练日线数据其实也不慢样本量在几千级别时100个epoch也就几分钟。6.3 用一份结果表支撑结论答辩时讲的结果不需要长篇大论但必须有一份逻辑完整的结果表。我的习惯是做一个三行表格第一行是单只股票的模型指标第二行是同参数下大盘指数或基金的表现第三行是随机买入基准。横向比较看模型是否跑赢基线纵向比较看参数调整带来的增益。表格之后再附一张最优参数下的资金曲线图图注写明数据区间、手续费设置、调仓频率。这份材料要比十页纯文字更有说服力也能挡住大部分追问。做这个项目时我最大的教训是永远不要因为训练集Loss低就急着下结论必须先把预测曲线和真实曲线叠在一起看滞后情况再决定是否交付。从那以后我每次跑完LSTM都会强制走一遍方向准确率和滞后检查再进入调参。希望帮到你。本文还有配套的精品资源点击获取
返回列表