ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LSTM股票基金预测模型实战:从数据预处理到滚动回测的完整流程

LSTM股票基金预测模型实战:从数据预处理到滚动回测的完整流程

简介:这份资源面向金融量化初学者与机器学习实践者,聚焦如何用LSTM(长短期记忆)网络对股票、基金的时间序列数据进行价格走势预测。内容围绕数据预处理、LSTM网络构建、训练验证与预测评估展开,涉及归一化、移动平均、技术指标、MSE损失与Adam优化器等具体环节,并讨论了市场政策与突发事件带来的预测挑战。压缩包共10个文件,约154KB,包含5个xlsx数据表用于训练与测试样本、3个py脚本负责模型搭建与数据获取、1个pyc缓存及1张png结果图,结构紧凑便于直接运行与二次修改。目前已有363人学习下载。读者可借此掌握从历史行情到预测输出的完整流程,理解门控机制在长期依赖建模中的作用,并获得可复用的代码骨架与评估思路,为后续优化模型精度、拓展特征工程提供参考。

1. 从一份 LSTM 股票基金预测模型压缩包说起:它到底能预测什么

很多人第一次拿到「基于机器学习 LSTM 的股票基金预测模型」这类压缩包,第一反应是打开看有没有能直接跑出收益的脚本。我当年也一样,解压、装依赖、python main.py,然后盯着屏幕上那条几乎贴着真实收盘价走的曲线兴奋了半小时,直到把它挪到最近三个月的数据上——曲线立刻变成一条迟钝的直线。这不是模型坏了,而是我们一开始就误解了 LSTM 在时序预测里能干什么。

LSTM(长短期记忆网络)本质是一个带门控的循环神经网络,它擅长从一段历史序列里学出「短期波动 + 长期趋势」的混合模式,然后外推下一格。放到股票基金场景,它能做的是基于历史价格、成交量、换手率这类数值序列,给出下一个交易日或下一周的数值估计,而不是告诉你「买还是卖」。这个区别决定了整个项目的定位:它是一个时序回归工程,不是一个交易策略。适合谁?适合想用 Python 把 LSTM 时间序列预测完整跑一遍的人——从数据清洗、窗口切分、模型搭建到回测评估,这套流程走通了,迁移到销量预测、负荷预测、流量预测都是同一套骨架。热搜里「lstm时间序列预测python」「lstm模型代码」「时序预测模型最新」这些词,说的其实就是这件事。

2. 数据准备与特征工程:LSTM 预测模型的地基怎么打

2.1 为什么股票基金数据不能直接喂给 LSTM

原始行情数据有三个绕不开的问题:缺失、量纲不统一、非平稳。停牌日没有成交,基金净值周末不更新,直接dropna会切断时间连续性;收盘价几千块、成交量几亿手,不归一化会让梯度爆炸;价格序列带趋势,均值方差随时间漂移,模型学到的「高位」和「低位」在不同时间段含义完全不同。

常见做法是先做交易日对齐,用前值填充停牌缺口,再对每个特征单独做归一化。归一化我一般用训练集的最小最大值,把scaler存下来,预测阶段复用同一组参数——这一点后面避坑章节会重点讲,因为它是新手最容易翻车的地方。

import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取行情数据,index 为交易日 df = pd.read_csv("fund_nav.csv", parse_dates=["date"], index_col="date") df = df[["close", "volume", "turnover"]].sort_index() # 交易日对齐:按自然日重采样后前向填充,再删掉开头可能产生的空值 df = df.resample("D").ffill().dropna() # 只对训练段拟合 scaler,避免未来信息泄漏 split = int(len(df) * 0.8) train_df = df.iloc[:split] scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_df.values) scaled = scaler.transform(df.values) print("训练段长度:", len(train_df), "总长度:", len(df))

这段代码的关键在scaler.fit(train_df.values)这一行。很多人图省事对全量数据fit_transform,结果测试集的极值参与了归一化,回测指标虚高,实盘一上就露馅。参数上feature_range=(0,1)是 LSTM 的常规选择,如果你用 tanh 激活,这个区间正好匹配输出范围;用 ReLU 系列可以放宽到(-1,1),但没必要。

2.2 滑动窗口切分:lookback 和 horizon 怎么定

LSTM 吃的是三维张量(样本数, 时间步, 特征数)。时间步就是 lookback,也就是「用过去多少天预测未来」。horizon 是预测未来第几天。这两个参数没有标准答案,但有经验区间:日频数据 lookback 取 20 到 60 比较稳,对应一个月到一个季度的记忆;horizon 取 1 到 5,超过 5 天误差会明显放大,因为股票基金的中短期噪声占比太高。

def make_windows(data, lookback=30, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i + lookback]) # 过去 lookback 天 y.append(data[i + lookback + horizon - 1, 0]) # 未来第 horizon 天的收盘价 return np.array(X), np.array(y) LOOKBACK, HORIZON = 30, 1 X, y = make_windows(scaled, LOOKBACK, HORIZON) # 按时间顺序切分,绝不能 shuffle split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] print(X_train.shape, X_test.shape) # (样本, 30, 3)

y只取第 0 列是因为我们预测目标是收盘价,其他特征作为辅助输入。切分必须按时间顺序,train_test_split默认 shuffle 会把未来样本混进训练集,这是时序预测里最隐蔽的数据泄漏。如果你看到验证 loss 低得离谱,先查这里。

2.3 特征选择:除了价格还能加什么

纯价格序列的 LSTM 很容易退化成「预测昨天」,因为相邻交易日相关性太高。想提升信息量,可以加几类特征:成交量与换手率反映资金活跃度;滚动波动率(比如 20 日标准差)刻画风险状态;均线偏离度(收盘价除以 MA20)把趋势位置显式编码进去。基金数据还可以加折溢价率、份额变化。特征不是越多越好,超过 10 个而样本只有几千条时,过拟合风险陡增,我一般控制在 5 到 8 个。

3. LSTM 模型搭建与训练:从 Keras 到 PyTorch 的最小可跑版本

3.1 网络结构:几层、多少隐藏单元

股票基金预测不需要深层网络。两层 LSTM 加一层全连接输出就够了,隐藏单元 32 到 128 之间。层数多了梯度回传路径变长,反而难训;单元太少欠拟合,曲线学不动。Dropout 放在 LSTM 层之间,取 0.2 左右,能明显缓解「训练 loss 降、验证 loss 升」的过拟合。

import tensorflow as tf from tensorflow.keras import layers, models def build_lstm(lookback, n_features): model = models.Sequential([ layers.Input(shape=(lookback, n_features)), layers.LSTM(64, return_sequences=True), # 第一层保留序列,传给下一层 layers.Dropout(0.2), layers.LSTM(32), # 第二层只输出最后时间步 layers.Dropout(0.2), layers.Dense(16, activation="relu"), layers.Dense(1) # 回归输出,无激活 ]) model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss="mse", metrics=["mae"]) return model model = build_lstm(LOOKBACK, X_train.shape[2]) model.summary()

return_sequences=True是堆叠 LSTM 的必要条件,第二层才能接到完整序列。最后一层Dense(1)不加激活函数,因为回归输出可以是任意实数。优化器用 Adam,学习率 1e-3 是稳妥起点;如果 loss 震荡,降到 5e-4。损失函数 MSE 对大误差敏感,想让模型别被极端值带偏可以换 Huber。

3.2 训练配置:epoch、batch 与早停

训练轮数不是越多越好。股票数据噪声大,通常 30 到 80 个 epoch 就收敛,再训就是记忆训练集。早停(EarlyStopping)是必备后悔药,监控验证 loss,patience 设 8 到 10,连续多轮不降就回滚最优权重。

from tensorflow.keras import callbacks es = callbacks.EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True) rlr = callbacks.ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5) history = model.fit( X_train, y_train, validation_split=0.15, # 从训练段尾部切验证,仍保持时间顺序 epochs=80, batch_size=32, callbacks=[es, rlr], verbose=1 )

validation_split=0.15是从训练集尾部切,不是随机切,这一点 Keras 默认按顺序取,符合时序要求。batch_size 32 适合几千条样本;样本上万可以提到 64 或 128,训练更快但泛化可能略降。ReduceLROnPlateau在 loss 停滞时减半学习率,配合早停能省不少调参时间。

3.3 预测与反归一化:把数字还原成价格

模型输出的是归一化空间的数值,必须用之前存的 scaler 反变换回真实价格。这里有个细节:scaler 是对多列拟合的,反变换时要构造同样列数的数组,只把预测列填进去,其余列补零,再取第 0 列。

pred_scaled = model.predict(X_test) # 构造与 scaler 维度一致的占位数组 pad = np.zeros((len(pred_scaled), scaler.n_features_in_)) pad[:, 0] = pred_scaled.flatten() pred_price = scaler.inverse_transform(pad)[:, 0] true_pad = np.zeros((len(y_test), scaler.n_features_in_)) true_pad[:, 0] = y_test true_price = scaler.inverse_transform(true_pad)[:, 0] from sklearn.metrics import mean_absolute_error, mean_squared_error mae = mean_absolute_error(true_price, pred_price) rmse = np.sqrt(mean_squared_error(true_price, pred_price)) print(f"MAE={mae:.4f} RMSE={rmse:.4f}")

评估指标别只看 MSE,它受价格量纲影响。MAE 更直观,RMSE 对大误差惩罚重。更实用的是看方向准确率:预测涨跌方向对的比例,这个指标和实际决策更相关。如果 MAE 很小但方向准确率接近 50%,说明模型只是学会了「跟着昨天走」,没有真正的预测力。

4. 避坑与排查:LSTM 股票基金预测最常见的 5 个翻车点

4.1 现象:回测曲线完美,实盘一塌糊涂

原因几乎都是数据泄漏。要么归一化用了全量数据,要么切分时 shuffle 了,要么特征里混入了未来信息(比如用了当日收盘后才公布的指标)。解决方法是把整个流程按时间切成互不重叠的训练、验证、测试三段,所有拟合操作只在训练段做,验证和测试段只做 transform。写完代码后养成一个习惯:打印每段的起止日期,肉眼确认没有重叠。

4.2 现象:训练 loss 一直不降,模型像没学

先查学习率,1e-3 对 LSTM 通常没问题,但如果你的输入没归一化,梯度会爆炸或消失,loss 卡住不动。其次查窗口切分,如果 lookback 设成 1,LSTM 退化成全连接,学不到时序模式。还有一种情况是标签构造错了,比如 horizon 偏移量算错,导致 X 和 y 错位。排查时先把模型换成线性回归跑一遍,如果线性回归能拟合而 LSTM 不能,问题在训练配置;如果线性回归也不行,问题在数据。

4.3 现象:验证 loss 远高于训练 loss

典型过拟合。股票基金数据信噪比低,几千条样本配几十万参数,过拟合几乎是必然。对策按优先级:先加 Dropout(0.2 到 0.4),再减隐藏单元(64 降到 32),再减特征数,最后才考虑加数据。L2 正则也能用,但 LSTM 里效果不如 Dropout 直接。如果这些做完验证 loss 还是高,说明这个预测目标本身太难,考虑换 horizon 或换标的。

4.4 现象:预测值是一条平滑直线

模型输出了均值。原因是损失函数 MSE 在噪声大的数据上,最优解就是预测条件均值,而股票收益的条件均值接近零。解决办法有两个方向:一是改预测目标,不预测价格而预测收益率或涨跌方向,分类任务对噪声更鲁棒;二是换损失函数,用方向性损失或者分位数损失。单纯调网络结构解决不了这个问题,这是目标定义的锅。

4.5 现象:每次训练结果差异很大

随机种子没固定。LSTM 权重初始化、Dropout、数据打乱都带随机性。做对比实验时,固定tf.random.set_seed、np.random.seed和 Python 的random.seed,并且同一组配置跑 3 到 5 次取平均。如果不同种子下指标波动超过 20%,说明模型不稳定,这时候比较两个模型的优劣没有意义,先把方差降下来。

5. 把预测模型用起来:滚动回测与方向准确率的实操技巧

训练完一个 LSTM 只是起点,真正决定这套东西值不值得投入的,是它在你关心的场景下能不能稳定输出。我一般会做两件事:滚动回测和方向准确率拆解。

滚动回测(walk-forward)模拟真实使用节奏:用前 N 天训练,预测第 N+1 天,然后把真实值并入训练集,窗口前移,再预测下一天。这样得到的指标比一次性切分更接近实盘。实现上不用每天重训,可以每 20 个交易日重训一次,中间用已训练模型滚动预测,兼顾成本和真实性。

def walk_forward(data, lookback=30, retrain_every=20): preds, trues = [], [] for start in range(lookback, len(data) - 1): if (start - lookback) % retrain_every == 0: # 用截至 start 的数据重训(此处省略训练细节,复用前面函数) X_tr, y_tr = make_windows(data[:start], lookback, 1) model = build_lstm(lookback, data.shape[1]) model.fit(X_tr, y_tr, epochs=30, batch_size=32, verbose=0) window = data[start - lookback:start][np.newaxis, ...] pred = model.predict(window, verbose=0)[0, 0] preds.append(pred) trues.append(data[start, 0]) return np.array(preds), np.array(trues)

方向准确率的算法很简单:比较预测值和当前值的差、真实值和当前值的差,符号一致就算对。这个指标比 MAE 更能说明模型有没有实用价值。我自己的经验是,日频股票基金预测方向准确率能稳定在 53% 到 56% 就已经不错,超过 60% 要警惕数据泄漏。别小看这几个百分点,扣掉交易成本后能不能剩下,才是真正要算的账。

还有一个容易被忽略的技巧:把 LSTM 的输出和简单基线做对比。基线就是「预测值等于今天收盘价」,如果 LSTM 连这个都跑不赢,说明模型没学到任何超出惯性的东西。我现在的习惯是任何时序预测项目,先跑基线,再上 LSTM,指标提升幅度才是模型真正的贡献。这套流程走下来,你会对「LSTM 能预测什么、不能预测什么」有比看十篇科普更清楚的认识。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进