简介:一套以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或者akshare,A股日线数据用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_code='600519.SH', start_date='20150101', end_date='20241231') df.sort_values('trade_date', inplace=True) df['trade_date'] = pd.to_datetime(df['trade_date']) df.set_index('trade_date', inplace=True) 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, window=60): 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, window=60)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, window=60) X_test, y_test = create_sequences(test_scaled, window=60)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_size=2, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm1 = nn.LSTM(input_size, hidden_size, num_layers=1, batch_first=True) self.lstm2 = nn.LSTM(hidden_size, hidden_size, num_layers=1, batch_first=True) 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_first=True后,输入维度就是(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, epochs=100, lr=0.001): optimizer = torch.optim.Adam(model.parameters(), lr=lr) 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(f'epoch {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_norm=5.0)。
3.3 超参数怎么调才有说服力
超参数不能靠感觉,否则答辩时“为什么hidden_size=64”这种问题直接卡住。常见做法是把窗口长度、hidden_size、学习率、dropout做成一个网格,用小步长跑几组对比,选验证Loss最低的组合。但这里有个实操技巧:先固定epoch=50,用默认参数跑通整个流程,确认数据、代码没问题,再开始调参。一上来就调参如果代码有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, window=60, hidden_size=64): 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), dtype=torch.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都会强制走一遍方向准确率和滞后检查,再进入调参。希望帮到你。
本文还有配套的精品资源,点击获取