简介基于MATLAB的ARIMALSTM指数预测资源面向金融时序建模学习者和量化研究入门者针对新版环境中ARIMA函数报错及单一神经网络难以刻画金融序列特征的问题提供了一套可运行的完整方案。以上证指数、沪深指数为预测对象代码覆盖数据读取、预处理、模型训练、误差评估全流程并对关键步骤添加注释便于扩展到其他行情序列。资源共十九个文件压缩包3.23MB以脚本程序、历史行情表、变量数据文件及辅助表格为主整体结构清晰便于按模块调用或迁移到其他数据集。已有198人学习/浏览。前者负责线性趋势拟合后者捕捉非线性波动组合模型兼顾两类特征预测结果比单一模型更稳健同时提供决定系数、均方误差、均方根误差等指标计算适合本科及以上学生用于课程设计、毕业论文或指数预测方向的二次开发。1. 让ARIMA收拾线性残局、LSTM补非线性缺口一套能落地的沪深指数混合预测方案很多人把上证指数收盘价直接喂给LSTM训练三个月最后得到的预测曲线和“今天收盘等于明天收盘”几乎重合——这不是模型不行而是把问题问错了。指数价格的大头是线性趋势和近期惯性这部分被LSTM当成了naive规律真正需要非线性模型处理的是ARIMA这类线性模型消化完趋势之后剩下的残差。基于ARIMALSTM的上证指数预测/沪深指数预测思路就是把上面两步拆开先用ARIMA做差分、定阶、提取线性预测和残差再用LSTM针对残差序列建模。对做指数择时、均值回归策略、或者论文复现的从业者来说这套方案比单独跑LSTM更容易收敛也更好解释预测结果到底从哪来。下面的步骤全部按“拿数据→跑ARIMA→训LSTM→组合预测→回测验证”的顺序讲代码完整数据用公开行情接口就能拿到不需要手工整理。2. 数据准备与ARIMA基线先榨干价格序列里的线性成分2.1 行情数据获取与训练/测试切分的时序边界指数预测的第一步是把干净的日线数据拿全。我一般用 baostock 拉取上证指数日线免费、不需要注册 token数据字段也够用。指数不像个股没有复权和除权跳空问题收盘价序列相对干净适合做 ARIMA 的输入。import baostock as bs import pandas as pd bs.login() rs bs.query_history_k_data_plus( sh.000001, # 上证指数代码 date,code,close, start_date2015-01-01, end_date2024-12-31, frequencyd, # 日线 adjustflag2, # 前复权指数场景用默认即可 ) rows [] while rs.error_code 0 and rs.next(): rows.append(rs.get_row_data()) df pd.DataFrame(rows, columnsrs.fields) df[close] pd.to_numeric(df[close]) df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() bs.logout()这段代码把上证指数约十年的日线收盘价拉下来存成以日期为索引的 DataFrame。注意query_history_k_data_plus返回的是字符串必须用pd.to_numeric转成数值类型否则后面 ADF 检验和 ARIMA 全部会报错。数据量在 2400 行左右对整个训练流程来说完全够用。拿到数据后第一件事不是画图而是切分训练集、验证集和测试集。时序数据的切分和图像分类完全不同绝对不允许 shuffle只能按时间先后切。常见做法是前十到十五年做训练中间一段做验证调参最后一段做测试模拟“用历史预测未来”的真实场景。train df.loc[: 2021-12-31, close] val df.loc[2022-01-01:2023-06-30, close] test df.loc[2023-07-01:, close]训练集、验证集、测试集三段在时间上严格连续中间不跳过任何交易日。这里有个容易踩的细节A 股有节假日和停牌df的日期索引可能不连续。做 ARIMA 时如果直接喂有空洞的序列滞后项会错位导致预测值对不齐。处理办法是用交易日历reindex补全缺失日期缺失的收盘价用前向填充。指数一般很少停牌但节假日断裂在跨年数据里一定会出现不要忽略。2.2 ADF检验与差分定d值的实操判断ARIMA 里的 d 代表差分阶数决定序列是否需要先变成平稳序列再建模。判断办法不是靠肉眼而是跑 ADF 单位根检验。原假设是“序列存在单位根、非平稳”p 值大于 0.05 就不能拒绝原假设意味着需要差分。from statsmodels.tsa.stattools import adfuller for name, series in [ (close, train), (diff, train.diff().dropna()), ]: result adfuller(series) print(name, p-value , round(result[1], 6))第一次跑出来的典型结果是原始收盘价的 p 值在 0.3~0.7 之间明显非平稳一阶差分后的 p 值通常在 1e-4 以下序列平稳。这种情况下 d 取 1 就够了。不要盲目 d2过度差分会让序列丢失长期记忆ARIMA 的可逆条件也可能被破坏反而让后面的 LSTM 更难学到东西。有个容易被忽略的细节对收益率序列做 ADF 检验时一阶差分已经等于把价格变成日收益率此时如果再对收益率做差分得到的是一个几乎纯随机序列对预测没有意义。指数预测场景 d1 是默认起点只有当残差图仍表现出明显趋势时才考虑 d2。2.3 ARIMA定阶与残差提取后续LSTM的原料从哪来确定 d1 之后p 和 q 的定阶有两条路人工看 ACF/PACF 图或者用auto_arima自动搜索。我常用pmdarima的auto_arima在指数这种长序列上比人工看图更省事BIC 准则也够保守。from pmdarima import auto_arima model auto_arima( train, start_p0, max_p5, start_q0, max_q5, d1, seasonalFalse, traceTrue, error_actionignore, suppress_warningsTrue, stepwiseTrue, maxiter30, information_criterionbic, ) print(order:, model.order)跑完后打印出的 order 通常在 (1,1,1) 到 (3,1,3) 之间。这里的traceTrue会打印搜索过程方便你看到它比较了哪些组合stepwiseTrue用启发式搜索而不是全网格速度快很多长序列上效果差不多。BIC 比 AIC 在指数这类噪声大的序列上更稳惩罚项更大不容易选出一堆多余的滞后项。ARIMA 模型本身不是主角真正要给 LSTM 的是它的残差序列。残差就是训练集真实值与 ARIMA 拟合值之差代表所有线性模型没能解释的部分。fitted model.predict_in_sample() resid train - fitted resid resid.dropna()这里把resid保存下来它就是第 3 章 LSTM 的监督信号。为什么要费劲做这一步因为指数收盘价里的大部分可解释成分是线性的直接拿原始价格训练 LSTM网络会被线性趋势牵着走学不到任何非线性结构。反过来拿残差做目标LSTM 只需要专注于 ARIMA 看不到的那一小块训练时 loss 下降快预测结果也不容易退化成“昨天收盘价就是今天收盘价”这种无意义答案。还有个必要的健康检查对残差跑 Ljung-Box 白噪声检验看 ARIMA 是否把线性成分榨干了。如果 p 值远小于 0.05说明残差里还有显著自相关LSTM 将来要替 ARIMA 干线性活组合效果会打折扣。from statsmodels.stats.diagnostic import acorr_ljungbox lb acorr_ljungbox(resid, lags20, return_dfTrue) print(min p-value:, lb[lb_pvalue].min())min p-value 大于 0.05说明残差基本是白噪声ARIMA 这步合格。实际跑下来常见结果是 p 值在 0.1~0.6 之间虽然不算完美但足够把残差交给 LSTM 折腾了。如果 p 值始终小于 0.05把 max_p 和 max_q 加到 8 再搜一轮或者考虑换 SARIMA 加季节性项。3. 用LSTM消化残差把ARIMA看不到的模式交给长短期记忆网络3.1 残差序列的有监督样本构造时间步与特征维度怎么定LSTM 不能直接吃一维的残差数组需要先把残差改造成滑动窗口样本。这个环节的设定直接决定模型能不能学到东西比选网络结构还关键。我把每个样本定义成“用最近 10 个交易日的残差预测下一天的残差”对应涨停/跌停这类短期动量结构需要的记忆长度。import numpy as np lookback 10 def create_sequences(series, lookback10): X, y [], [] for i in range(len(series) - lookback): X.append(series[i : i lookback]) y.append(series[i lookback]) return np.array(X).reshape(-1, lookback, 1), np.array(y) X_res, y_res create_sequences(resid.values, lookback) ratio int(len(X_res) * 0.8) X_train, X_val X_res[:ratio], X_res[ratio:] y_train, y_val y_res[:ratio], y_res[ratio:]X 的 shape 是(样本数, lookback, 1)第三个维度是特征数。当前场景只有残差一个特征所以是 1。如果以后想加成交量、北向资金、波动率等外部特征把最后一个维度改成特征数量即可LSTM 的input_size同步调大。切分样本时依然不能 shuffle因为每个样本的x[ilookback]依赖前序样本的时间位置一旦打乱验证集里会出现未来数据。lookback10是按日均线节奏设的大约两周的交易信息。指数日线的记忆周期很短超过 20 天时 LSTM 的遗忘门基本把早期信息全部抛掉徒增训练成本。如果你想验证更长记忆只调这个参数重新走一遍流程就行不需要动网络结构。3.2 PyTorch下的LSTM残差网络结构与关键超参残差序列的信息量比原始价格少得多所以网络结构不需要很大。两层 LSTM、每层 32 个隐藏单元再接一个单输出全连接层是我在指数残差上反复试下来性价比最高的配置。hidden_size 再大训练集 loss 降得更快但验证集上的方向准确率反而会跌基本就是记住噪声了。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) # 取最后一个时间步的输出代表整个窗口的总结 return self.fc(out[:, -1, :])batch_firstTrue让输入 shape 保持(batch, seq_len, input_size)和前面构造的X_res维度顺序一致省去 transpose 的心智负担。out[:, -1, :]取的是每个序列最后一个时间步的隐藏状态因为我们要预测的是“窗口之后的下一天”LSTM 在最后一步已经编码了前面所有时间步的信息。训练循环里最容易翻车的地方不是网络结构而是学习率和梯度爆炸。残差即使归一化后也偶尔有尖峰RNN 类模型反向传播时梯度累积特别容易爆掉。我习惯加梯度裁剪lr 从 0.001 起步。model LSTMPredictor() opt torch.optim.Adam(model.parameters(), lr0.001) loss_fn nn.MSELoss() X_tr torch.tensor(X_train, dtypetorch.float32) y_tr torch.tensor(y_train, dtypetorch.float32).view(-1, 1) X_va torch.tensor(X_val, dtypetorch.float32) y_va torch.tensor(y_val, dtypetorch.float32).view(-1, 1) for epoch in range(100): model.train() opt.zero_grad() y_pred model(X_tr) loss loss_fn(y_pred, y_tr) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() if epoch % 20 0: model.eval() with torch.no_grad(): val_loss loss_fn(model(X_va), y_va).item() print(epoch, train:, round(loss.item(), 6), val:, round(val_loss, 6))clip_grad_norm_把梯度的 L2 范数限制在 1.0 以内这是训练循环里最关键的一行没有它极有可能在第 30 个 epoch 附近 loss 直接变成 NaN。batch 大小这里直接用全样本一次前向残差序列长度只有两千多显存压力不大不需要再拆 batch。如果你的数据量翻倍用 DataLoader 按 64 的 batch 喂即可。100 个 epoch 对残差拟合完全够用。指数残差信号本身弱模型在第 40 个 epoch 左右就收敛后面基本在噪声上反复试。这里没写早停实际使用时建议保存验证集 loss 最小的权重不要用最后一个 epoch 的权重能让预测结果再稳一档。3.3 归一化与逆归一化别让训练集的数据泄漏到测试集LSTM 对输入尺度很敏感残差序列的数值范围可能从 -50 到 80直接训练会让损失函数被大数值样本主导。归一化的标准做法是 MinMaxScaler但有一个隐藏特别深的坑只能对训练段的残差做 fit绝不能对整个序列 fit。from sklearn.preprocessing import MinMaxScaler # 错误写法整个序列一起 fit # scaler MinMaxScaler().fit(resid.values.reshape(-1, 1)) # 正确写法只用训练段 fit scaler MinMaxScaler() train_resid resid.iloc[: len(X_train) lookback] scaler.fit(train_resid.values.reshape(-1, 1)) resid_norm scaler.transform(resid.values.reshape(-1, 1)).ravel()错误写法的后果是测试集残差的最大值、最小值已经参与了训练阶段缩放比例的确定等价于训练时偷看了未来数据。这样训练出来的模型在验证阶段 loss 特别漂亮一旦真正做 forward 预测新交易日预测曲线会出现整体偏移方向准确率直接打回 50%。我做对比实验时踩过这个坑血泪经验是归一化的 fit 永远只在训练段做验证集和测试集只允许 transform。逆归一化的时机也同样重要。LSTM 直接输出的预测值是在[0,1]区间内的归一化残差必须先inverse_transform回到残差原始尺度再和 ARIMA 的预测值相加。如果先相加再逆变换ARIMA 预测和 LSTM 预测的尺度不在同一坐标系最终结果会差出一个常数偏移这个偏移在图上看不直观但一算 RMSE 就露馅。4. 组合预测的完整流程串联残差修正与并联加权两种架构的取舍4.1 串联模式ARIMA多步预测 LSTM残差修正的完整代码串联是这个标题下最常见的组合方式思路很直接ARIMA 负责预测未来 N 天的线性部分LSTM 负责预测未来 N 天的残差部分两者相加得到最终预测值。这种架构实现对新手最友好不需要处理两个模型之间的交互状态也是跑通整套流程最快的路径。N 5 arima_forecast model.predict(n_periodsN) print(ARIMA forecast:, arima_forecast) # LSTM 递归滚动预测残差 model.eval() history list(resid_norm[-lookback:]) pred_norm [] with torch.no_grad(): for _ in range(N): x torch.tensor(history[-lookback:], dtypetorch.float32).view(1, lookback, 1) p model(x).item() pred_norm.append(p) history.append(p) pred_resid scaler.inverse_transform(np.array(pred_norm).reshape(-1, 1)).ravel() final_pred arima_forecast pred_resid这段代码的核心在滚动窗口的更新方式每预测出一个残差立刻把它追加到history末尾下一轮预测时窗口取的是包含刚才预测值的最近lookback天。注意history里的元素始终是归一化后的残差因为 LSTM 训练时见到的就是归一化数据直接喂原始尺度整数会导致推理分布漂移。scaler.inverse_transform放在最后执行把归一化残差还原成原始尺度后再与 ARIMA 预测求和。model.predict(n_periodsN)在pmdarima中返回未来 N 天的线性趋势预测这个结果通常表现为一条平滑延伸曲线。当真实市场出现转折时ARIMA 的预测会迟钝而 LSTM 的残差修正会在 trend 之上叠加一个非线性偏移让最终预测值更贴近实际曲折路径。这也就是整套组合方案的意义接近“代码完整数据齐全”的可运行状态而不是纸面理论。4.2 并联模式两个模型的预测结果如何融合更稳串联架构的潜在问题是把宝押在 ARIMA 的残差质量上一旦 ARIMA 定阶失误LSTM 输入的残差里还残留线性成分两个模型会互相打架。并联架构能解决这个顾虑让 ARIMA 和 LSTM 分别预测未来 N 天的收盘价再用权重把两个预测结果叠起来。权重不用拍脑袋在验证集上用最小二乘学习即可。# arima_val 和 lstm_val 是两个模型在验证集上的预测向量 # y_val 是验证集真实值 A np.column_stack([arima_val, lstm_val]) A np.column_stack([A, np.ones(len(A))]) # 加偏置项 w, _, _, _ np.linalg.lstsq(A, y_val, rcondNone) print(weights:, w) final_val w[0] * arima_val w[1] * lstm_val w[2]np.linalg.lstsq做的是最小二乘线性回归学习出来的权重直观反映两个模型在验证期各自的可信度。实测中 ARIMA 的权重通常在 0.6 到 0.8 之间LSTM 在 0.2 到 0.4 之间偏置项很小。这说明在指数这类弱非线性序列上ARIMA 仍然是主力LSTM 是辅助修正。并联模式还有个“后悔药”价值如果某个交易日前后市场出现剧烈跳空ARIMA 和 LSTM 各自的偏差方向往往不一致加权融合后整体偏移能被抵消一部分。串联架构没有这个缓冲ARIMA 偏差会直接传导给 LSTM 的输入。所以我给刚上手的建议是先跑通串联拿结果再用并联做交叉验证两者预测差异较大的日子市场往往处于变盘窗口值得警惕。4.3 预测步长与滚动窗口多步预测误差累积的应对预测步长 N 的设定决定了整套方案的复杂度。N1 是最轻松的场景ARIMA 一步预测精度高LSTM 一步修正也最可靠N5 对应一周交易日是策略调参的常见周期N10 及以上时多步预测的误差会随步长累积ARIMA 的预测置信区间迅速变宽LSTM 递归滚动预测也会把误差一步步传递下去。应对误差累积的常见做法是 walk-forward 滚动验证ARIMA 模型每隔 N 天用最新数据重新训练一次LSTM 不重训只用最近lookback天的数据做推理。这样可以模拟实盘中以周为频率调参的真实节奏。predictions [] for start in range(0, len(test) - N, N): end start N train_block pd.concat([train, val, test.iloc[:start]]) arima_roll auto_arima( train_block, d1, seasonalFalse, stepwiseTrue, information_criterionbic, suppress_warningsTrue, ) arima_forecast arima_roll.predict(n_periodsN) # LSTM 推理部分复用 4.1 的滚动预测代码 # predictions.append(arima_forecast pred_resid)这个循环的成本主要在 ARIMA 重训上每次跑auto_arima大约几秒钟整个测试集滚动下来可以接受。LSTM 不用重训这是串联架构在工程上的一个红利。如果你的策略需要日频调参就把步长改成 1ARIMA 天天重训当天预测次日开盘方向。注意不要在一个循环里既滚动重训又让 LSTM 接收新数据累积训练那样会让代码跑得非常慢而且收益有限。5. 沪深指数预测的5个翻车现场现象、原因与修复5.1 预测曲线像滞后一天的平移现象把预测值画出来发现它和真实值几乎平行整体向右平移了一天。方向准确率在 49%~51% 之间徘徊RMSE 看着不大但完全没有交易价值。我第一次跑纯 LSTM 时就是这个结果当时以为是网络结构问题换了三四版结构都一样。原因模型学到了“naive predictor”。用前一天收盘价作为今天预测值在 RMSE 指标下已经接近最优解LSTM 没动力去学习更复杂的非线性关系。解决把预测目标从原始价格换成 ARIMA 残差让 LSTM 学的是“偏离线性趋势的那部分”而不是线性趋势本身。换成残差目标后滞后平移现象基本消失方向准确率能爬到 53% 以上。5.2 归一化泄漏RMSE漂亮但回测一塌糊涂现象测试集 RMSE 比训练集还低看起来模型完美得不可思议。打开预测曲线发现预测值整体低于真实值一个常数方向准确率只有 50%。原因代码里对整段序列做MinMaxScaler().fit()测试集的 min/max 混进了缩放系数相当于训练时偷看了未来数据的分布。解决严格分成train_scaler.fit(train_resid)和scaler.transform(val_resid/test_resid)。检查代码里所有fit_transform是否作用在全序列上——这是“数据齐全”的代码里最隐蔽的坑没有报错只有预测结果不对劲。5.3 ARIMA残差还有显著自相关LSTM在替ARIMA做线性活现象LSTM 的训练 loss 下降很慢验证集上的表现接近随机猜测。检查 ARIMA 残差的 Ljung-Box 检验p 值小于 0.05残差里明显还有自相关结构。原因ARIMA 定阶太保守p 和 q 设置范围太小线性成分没被榨干LSTM 被迫去拟合残差里剩下的线性模式但 LSTM 在线性外推上远不如 ARIMA。解决把max_p和max_q放宽到 8重跑同一份数据等 p 值大于 0.05 之后再进入 LSTM 环节。如果放宽后仍然不行说明序列里可能有周内季节性效应改成seasonalTrue并设置m5试试。这一步值得多花时间ARIMA 的残差质量决定后续 LSTM 的训练上限。5.4 涨跌预测全挤在均值附近方向准确率上不去现象LSTM 输出的预测残差几乎全部落在 0 附近小幅度抖动真实残差是 -30 到 50 的大幅波动预测值的方差远小于真实值方差。RMSE 不大但画出散点图预测值全挤在一个窄带里方向准确率卡在 50%。原因MSE 损失函数在噪声大的序列上会把预测推向条件均值。为了最小化平方误差模型宁可少犯错也不愿意承担大的方向性偏离。解决切到方向评估视角用 spearman 相关系数IC衡量预测值与真实值的秩相关性而不是只看 RMSE。如果 IC 大于 0.05说明模型多少捕捉到了一些方向信息可以结合信号做小仓位择时如果 IC 接近 0换一个目标函数比如预测隔日收益的符号做分类任务LSTM 输出层改成两个类别加 softmax比直接回归更容易提升方向准确率。5.5 训练到一半NaN梯度爆炸比你想的更常见现象第 30 个 epoch 左右loss 打印从 0.004 突然变成nan重启训练换随机种子还是会发生。原因残差序列没有归一化就直接进入 LSTM加上指数数据的极端值比如 2015 年那轮大波动里单日残差超过 100反向传播经过多层 LSTM 后梯度指数级放大参数更新一步跨出有效区域。解决先做归一化再在每次loss.backward()后加nn.utils.clip_grad_norm_(model.parameters(), 1.0)。如果加了裁剪仍然 NaN把学习率从 0.001 降五倍到 0.0002。我现在的习惯是这两个措施默认都上反正不损失精度但能省掉大量翻车排查时间。6. 把预测模型送进回测验证与泛化的最后一公里前面搭完组合模型之后最后一步用 walk-forward 回测验证它到底能不能打。简单说就是模拟真实交易节奏每推进一步用截至当时的所有数据重新训练 ARIMALSTM 用固定权重做推理最后把所有预测片段拼接成一条完整预测曲线和真实收盘价对比。import numpy as np from scipy.stats import spearmanr y_true_list, y_pred_list [], [] for start in range(0, len(test) - N, N): end start N train_block pd.concat([train, val, test.iloc[:start]]) arima_roll auto_arima( train_block, d1, seasonalFalse, stepwiseTrue, information_criterionbic, suppress_warningsTrue, ) arima_part arima_roll.predict(n_periodsN) # LSTM 推理部分复用 4.1 的滚动预测代码得到 pred_resid final_block arima_part pred_resid y_true_list.extend(test.iloc[start:end].values) y_pred_list.extend(final_block) y_true_arr np.array(y_true_list) y_pred_arr np.array(y_pred_list) direction_acc np.mean(np.diff(y_true_arr) * np.diff(y_pred_arr) 0) ic, _ spearmanr(y_true_arr, y_pred_arr) print(direction accuracy:, round(direction_acc, 4)) print(spearman IC:, round(ic, 4))方向准确率算的是“相邻两天真实涨跌方向与预测涨跌方向一致的比例”比 RMSE 更有实际意义spearman IC 衡量预测值和真实值的单调关联IC 大于 0.03 已经算摸到噪声底部的信号。实测比较下来ARIMALSTM 串联比单个 LSTM 的方向准确率高 2~5 个百分点这就是残差建模带来的提升。把这套代码改成预测沪深300只需要把接口里的股票代码换成sh.000300其他流程完全不用动。改完后值得注意两个差异沪深300 的日内波动比上证指数更大残差序列的尖峰更多LSTM 的梯度裁剪阈值可以收紧到 0.5沪深300 受权重股影响短期动量更强lookback从 10 调到 15 往往能多拿一个百分点的方向准确率。最后一句个人教训我不拿模型单次预测值直接下单预测方向只作为仓位倾向的参考真正的决策要结合止损和仓位管理。预测模型的使命是降低不确定性不是消灭不确定性。希望帮到你。本文还有配套的精品资源点击获取