简介:这份资源是面向机器学习初学者与高校学生的LSTM股票/基金预测实战项目,适合用作课程设计、期末大作业或入门时序预测的练手案例。压缩包共11个文件,约155KB,包含3个Python脚本负责模型搭建与数据获取、5个xlsx表格存放训练集、测试集及预测结果、1份README说明文档,另有1张运行结果图和1个缓存文件,结构紧凑、开箱即用。项目围绕长短期记忆网络对基金与股票净值走势建模,涵盖数据读取、特征整理、模型训练与预测输出等完整环节,可直接对照代码理解LSTM在金融时序上的应用思路。目前已有224人学习下载,适合想快速跑通一个完整预测流程、积累项目经验的读者参考借鉴。
1. 从一份"高分项目"压缩包说起:LSTM 做股票/基金预测到底靠不靠谱
很多人第一次接触"基于机器学习 LSTM 的股票/基金预测模型"这个标题,是在课程设计、毕设选题或者简历项目里。压缩包解压出来通常是一份 Jupyter Notebook、一份 CSV 数据、一张预测曲线对比图,跑一遍发现拟合得还挺像,于是心里犯嘀咕:这玩意儿真能拿来炒股吗?先把结论摆前面——LSTM 做股票/基金预测,能做出一个工程上完整、指标上好看的模型,但它不是印钞机。它擅长的是从历史量价序列里学出短期趋势和波动模式,而不是预测明天涨停。这个方向真正值钱的地方,是让你完整走一遍"数据获取 → 特征工程 → 序列建模 → 回测评估"的量化流水线,这套能力放到设备寿命预测、销量预测、流量预测上都能复用。
适合读这篇的人有三类:一是要交课程设计或毕设、需要一份能跑通、能讲清楚原理的项目;二是想入门量化、用 Python 机器学习做点股票组合分析练手的工程师;三是已经会调sklearn,但没系统做过时间序列、想搞明白 LSTM 时间序列预测 Python 实现细节的人。下面我按自己实际做过的路径,把数据怎么拿、模型怎么搭、参数怎么调、哪里最容易翻车,一条条讲清楚。
2. 数据从哪来、怎么变成 LSTM 能吃的序列
2.1 用 akshare 获取股票数据:最小可用代码
做股票预测,第一步永远是数据。国内免费数据源里,akshare是这几年用得比较多的一个,接口稳定、字段全,取 A 股日线一行代码就够。我一般先拉一只票的日线,确认字段结构再批量拉。
import akshare as ak import pandas as pd # 拉取贵州茅台日线,adjust="qfq" 表示前复权 df = ak.stock_zh_a_hist( symbol="600519", period="daily", start_date="20180101", end_date="20241231", adjust="qfq" ) # 统一列名,方便后续处理 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.shape, df.columns.tolist()) print(df.tail(3))逻辑说明:stock_zh_a_hist返回的是中文列名,先重命名成英文,避免后面写特征时中英混用。adjust="qfq"是关键参数,前复权能消除分红送股造成的价格跳空,否则模型会把这些跳空当成真实波动去学,预测必然失真。period可选daily/weekly/monthly,做 LSTM 短期预测一般用日线。参数上start_date建议至少给 3 年以上,LSTM 是数据饥渴型模型,样本太少学不出东西。
提示:
akshare接口偶尔会因为数据源调整而字段变动,跑之前先print(df.columns)确认一遍,别直接硬编码列名。
2.2 特征工程:别只喂收盘价
新手最容易犯的错,是把close一列丢进 LSTM 就完事。单变量序列能跑,但信息量太低,模型学不到量价关系。我一般会构造一组基础特征,让模型有东西可学:
| 特征名 | 计算方式 | 作用 |
|---|---|---|
| ret_1 | close.pct_change(1) | 日收益率,平稳性比价格好 |
| ma5 / ma20 | close.rolling(n).mean() | 短期/中期趋势 |
| vol_ratio | volume / volume.rolling(5).mean() | 放量缩量信号 |
| high_low | (high - low) / close | 当日振幅 |
| close_ma20 | close / ma20 - 1 | 偏离均线程度 |
df["ret_1"] = df["close"].pct_change() df["ma5"] = df["close"].rolling(5).mean() df["ma20"] = df["close"].rolling(20).mean() df["vol_ratio"] = df["volume"] / df["volume"].rolling(5).mean() df["high_low"] = (df["high"] - df["low"]) / df["close"] df["close_ma20"] = df["close"] / df["ma20"] - 1 # 预测目标:下一日收益率 df["target"] = df["close"].pct_change().shift(-1) df = df.dropna().reset_index(drop=True)逻辑说明:pct_change把绝对价格转成收益率,这是时间序列建模的常规操作,因为价格是非平稳的,直接喂给模型容易过拟合趋势。shift(-1)是把"明天"的收益率对齐到"今天"这一行,这是构造监督学习标签的标准做法,方向千万别搞反,shift(1)就变成用未来预测现在了。dropna会砍掉前面均线没算出来的行,正常损失 20 行左右。
2.3 滑动窗口:把表格变成三维张量
LSTM 要的输入是(样本数, 时间步, 特征数)的三维张量,所以得用滑动窗口把二维表切成序列。窗口长度seq_len是核心参数,我一般取 20 或 30,对应一个月左右的交易日。
import numpy as np def make_sequences(data, target, seq_len=20): X, y = [], [] for i in range(len(data) - seq_len): X.append(data[i:i + seq_len]) # 过去 seq_len 天 y.append(target[i + seq_len]) # 第 seq_len+1 天的收益 return np.array(X), np.array(y) feat_cols = ["ret_1", "ma5", "ma20", "vol_ratio", "high_low", "close_ma20"] X, y = make_sequences(df[feat_cols].values, df["target"].values, seq_len=20) print(X.shape, y.shape) # (N, 20, 6)逻辑说明:循环里data[i:i+seq_len]取窗口,target[i+seq_len]取窗口后一天,保证不泄露未来信息。seq_len太小(比如 5)模型看不到趋势,太大(比如 120)样本数骤减且容易过拟合,20~30 是日线数据的经验区间。特征维度这里是 6,后面模型输入层要跟它对齐。
注意:划分训练集/测试集时绝对不能随机打乱,时间序列必须按时间先后切,否则就是典型的数据泄露,测试集指标会虚高得离谱。
3. PyTorch LSTM 模型搭建与训练:从网络结构到早停
3.1 LSTM 单元在学什么:门控机制一句话讲透
LSTM(长短期记忆网络,Long Short-Term Memory)1997 年由 Hochreiter 和 Schmidhuber 提出,核心是三个门:遗忘门决定丢掉多少旧记忆,输入门决定写入多少新信息,输出门决定当前输出多少。相比普通 RNN,它靠这条"细胞状态"高速公路缓解了梯度消失,所以能记住几十步之前的信息。放到股票场景,就是让模型自己判断"20 天前的放量"对"明天涨跌"还有没有参考价值。你不需要手推公式,但要理解一点:LSTM 的记忆能力是有代价的,序列越长、参数越多,越容易在小样本上过拟合,这也是为什么股票预测里 LSTM 经常"训练集完美、测试集拉胯"。
3.2 一个能直接跑的 PyTorch LSTM 源码
下面是我常用的最小模型结构,两层 LSTM 加一层全连接,带 Dropout 防过拟合。
import torch import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, # 输入格式 (batch, seq, feature) dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq, hidden) out = out[:, -1, :] # 只取最后一个时间步 return self.fc(out).squeeze(-1) model = LSTMModel(input_size=len(feat_cols)) print(model)逻辑说明:batch_first=True让输入维度是(batch, seq_len, feature),跟前面构造的X对齐,忘了设这个参数是最常见的翻车点,会报维度不匹配。out[:, -1, :]只取最后一个时间步的隐状态,因为我们要预测的是窗口之后那一天,前面时间步的输出对最终预测没直接用途。hidden_size=64、num_layers=2是中小数据集的稳妥起点,数据量上万条可以加到 128。dropout=0.3只在多层 LSTM 之间生效,单层时这个参数无效。
3.3 训练循环与早停:参数怎么设
from torch.utils.data import TensorDataset, DataLoader # 按时间切分,前 80% 训练,后 20% 测试 split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] train_ds = TensorDataset(torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)) train_loader = DataLoader(train_ds, batch_size=64, shuffle=False) # 不打乱 criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) best_loss, patience, wait = float("inf"), 10, 0 for epoch in range(200): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪 optimizer.step() model.eval() with torch.no_grad(): val_loss = criterion(model(torch.tensor(X_test, dtype=torch.float32)), torch.tensor(y_test, dtype=torch.float32)).item() if val_loss < best_loss: best_loss, wait = val_loss, 0 torch.save(model.state_dict(), "best_lstm.pt") else: wait += 1 if wait >= patience: print(f"early stop at epoch {epoch}") break print(f"epoch {epoch}, val_loss {val_loss:.6f}")逻辑说明:shuffle=False在时间序列里很重要,打乱会破坏时序依赖。clip_grad_norm_把梯度范数限制在 1.0,LSTM 在收益率这种小数值上训练时梯度容易爆炸,这个操作能显著稳住训练。学习率1e-3是 Adam 的常用值,如果 loss 震荡就降到5e-4。早停patience=10表示验证损失连续 10 轮不降就停,这是防过拟合最省事的手段。batch_size=64在几千条样本上比较合适,样本少就降到 32。
提示:收益率数值很小(0.01 量级),MSE 损失会非常小,看 loss 时别被
0.0001这种数字迷惑,重点看它有没有持续下降、测试集有没有反弹。
4. 评估与回测:模型预测股票涨跌为什么每次结果不一样
4.1 别只看 MSE,方向准确率才是关键
MSE 小不代表能赚钱。收益率预测里,模型很容易学成"永远预测接近 0",MSE 很低但毫无用处。真正要看的是方向准确率:预测涨、实际也涨的比例。
model.eval() with torch.no_grad(): pred = model(torch.tensor(X_test, dtype=torch.float32)).numpy() direction_acc = ((pred > 0) == (y_test > 0)).mean() print(f"方向准确率: {direction_acc:.4f}") # 简单策略回测:预测涨就持有,预测跌就空仓 strategy_ret = np.where(pred > 0, y_test, 0) cum_strategy = (1 + strategy_ret).cumprod() cum_buyhold = (1 + y_test).cumprod() print(f"策略累计: {cum_strategy[-1]:.4f}, 买入持有: {cum_buyhold[-1]:.4f}")逻辑说明:方向准确率能到 52%~55% 就已经算有信号了,别指望 70%,那是过拟合或者数据泄露。回测里np.where(pred > 0, y_test, 0)是最朴素的择时策略,预测涨就吃当天收益,预测跌就空仓。对比"买入持有"基准很重要,很多模型跑不赢躺平不动。
4.2 结果每次不一样:随机性从哪来
"模型预测股票涨跌每次结果不一样"是高频疑问,原因有三:一是 PyTorch 权重随机初始化,二是 DataLoader 即使shuffle=False,某些操作仍有非确定性,三是 Dropout 在训练时随机丢弃神经元。要复现结果,固定随机种子:
import random def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)逻辑说明:cudnn.deterministic=True会让 GPU 卷积算法确定化,代价是速度略降。固定种子后同一份数据、同一份代码结果可复现,但换一段测试区间结果还是会变,这是市场本身的问题,不是 bug。评估模型要跑多个时间窗口取平均,单次结果没有说服力。
4.3 基金预测和股票预测的差别
基金(尤其指数基金)的净值序列比个股平滑得多,噪声小、趋势性强,LSTM 在基金净值上的方向准确率通常比个股高几个点。但代价是波动小、可套利空间也小。做基金预测时我一般把seq_len拉长到 30~60,因为基金趋势持续时间更长;特征里加上跟踪指数的收益率、市场整体涨跌作为外部变量,效果比纯净值序列好。个股则相反,seq_len短一点、特征里多放量价关系更合适。
5. 避坑与排查:LSTM 股票预测最常见的 5 个翻车现场
5.1 现象:测试集准确率高得离谱,实盘完全不行
原因:数据泄露。最常见的是归一化时用了全量数据的均值和方差,或者划分数据集时随机打乱,让模型"看到"了未来信息。解决:归一化参数只能用训练集算,再应用到测试集;切分严格按时间顺序。检查方法很简单——把测试集整体后移一天,如果指标暴跌,说明之前泄露了。
5.2 现象:loss 一直不降,或者变成 nan
原因:学习率太大、收益率数值太小导致梯度问题,或者输入里有 NaN。解决:先把学习率降到1e-4试;加梯度裁剪;训练前assert not np.isnan(X).any()检查数据。收益率量级小的时候,也可以把 target 放大 100 倍(用百分比表示),训练完再缩回去。
5.3 现象:训练集 loss 很低,测试集 loss 高得吓人
原因:过拟合。LSTM 参数量相对几千条样本还是偏大。解决:减小hidden_size(64 降到 32)、增大dropout(0.3 提到 0.5)、加 L2 正则(优化器里设weight_decay=1e-4)、缩短seq_len。如果还不行,说明特征太多,砍到 3~4 个核心特征。
5.4 现象:预测曲线是一条几乎水平的直线
原因:模型学成了"预测均值",因为收益率本身接近零均值,MSE 损失下这是局部最优。解决:换损失函数,用方向性的损失或者对涨跌样本加权;或者把任务从回归改成分类(涨/跌二分类),用交叉熵,模型会更愿意给出有倾向的预测。
5.5 现象:换一只股票就要重调所有参数
原因:不同股票波动率差异巨大,统一参数不通用。解决:对每只票做单独的标准化,或者用波动率归一化收益率(收益率除以滚动标准差)。做股票组合分析时,我一般按行业或市值分组,组内共享一套超参数,比全市场一套参数稳得多。
6. 把单模型升级成能用的信号:几个我压箱底的技巧
单只 LSTM 跑出来的预测噪声很大,直接拿来交易基本是送钱。我实际做的时候会加两层处理。第一层是多模型集成:用不同随机种子训练 5 个 LSTM,把预测取平均,方向准确率通常能稳 2~3 个点,因为单模型的随机波动被平滑掉了。第二层是信号过滤:只在模型预测的绝对值超过某个阈值时才动手,比如预测收益率绝对值大于 0.5% 才认为有信号,否则视为噪声空仓。这个阈值用验证集调,别用测试集。
# 多模型集成 + 阈值过滤 preds = [] for seed in [42, 7, 123, 2024, 999]: set_seed(seed) m = LSTMModel(input_size=len(feat_cols)) # ... 训练代码同上,省略 ... m.load_state_dict(torch.load(f"best_lstm_{seed}.pt")) m.eval() with torch.no_grad(): preds.append(m(torch.tensor(X_test, dtype=torch.float32)).numpy()) ensemble_pred = np.mean(preds, axis=0) threshold = 0.005 signal = np.where(np.abs(ensemble_pred) > threshold, np.sign(ensemble_pred), 0) strategy_ret = signal * y_test print(f"集成后方向准确率: {((ensemble_pred > 0) == (y_test > 0)).mean():.4f}")逻辑说明:np.sign把连续预测转成 -1/0/1 的仓位信号,threshold控制交易频率,阈值越高交易越少、越保守。这个组合在回测里通常能把最大回撤压下来,代价是错过一些小幅上涨。参数上阈值别设太大,0.5%~1% 是日线级别的合理区间。
还有一个容易被忽略的点:滚动重训。市场风格会变,一个模型训完就不管,几个月后必然失效。我的习惯是每 60 个交易日重新训练一次,用最近 3 年数据,这样模型能跟上最新的波动特征。这套流程跑下来,你会发现 LSTM 股票预测真正的价值不在预测精度,而在于它逼你把数据管道、特征、回测、风控整条链路都搭明白——这套东西迁移到任何时间序列任务上都成立。我自己踩过最深的坑就是一开始迷信模型精度,花两周调网络结构,最后发现把数据泄露修掉、把回测做扎实,比换十个模型都管用。希望帮到你。
本文还有配套的精品资源,点击获取