ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

ARIMA+LSTM组合模型预测上证指数:从原理到实战避坑指南

ARIMA+LSTM组合模型预测上证指数:从原理到实战避坑指南

简介:这份资源面向金融时间序列预测的学习者与研究者,提供基于MATLAB实现的ARIMA与LSTM组合预测方案,用于上证指数及沪深300等指数的走势建模。针对新版MATLAB中ARIMA函数易报错的问题,代码做了兼容处理,并利用LSTM对时序数据的建模优势提升预测效果,适合本科及以上层次用于课程设计、论文复现或扩展创新。压缩包共19个文件,约3.23MB,包含10个m脚本、5个csv数据、2个mat文件、1个xls及1个asv备份,脚本覆盖ARIMA与LSTM的建模、训练与预测流程,数据文件提供指数及加密货币等序列,mat文件保存中间结果,xls用于原始行情导入。资源已有198人学习下载,代码完整且带注释,便于读者直接运行、理解组合模型思路,并在此基础上替换数据或调整网络结构开展自己的实验。

1. 从单模型翻车说起:ARIMA+LSTM 做上证指数预测到底靠不靠谱

很多人第一次做上证指数预测,都是先拿 ARIMA 单独跑一遍,发现短期还行、拐点全错;再换 LSTM 单独跑,发现拟合曲线漂亮得像作弊,一到实盘就滞后半拍。这不是模型不行,而是你把线性部分和非线性部分硬塞给了一个模型。上证指数这种日频数据,既有明显的趋势和均值回归(线性可建模),又有政策消息、情绪脉冲带来的非线性跳变(LSTM 的强项),单一模型必然顾此失彼。ARIMA+LSTM 的组合思路就是:先用 ARIMA 把线性结构吃掉,再把残差交给 LSTM 去啃非线性尾巴,最后把两段预测相加。这套方案适合有 Python 基础、做过时间序列入门、想把手上的沪深指数数据真正跑出一套可复现流程的人。代码完整、数据齐全不是重点,重点是每一步为什么这么设、参数怎么调、哪里最容易翻车。

2. 数据准备与平稳性检验:上证指数日线怎么清洗才不会污染模型

2.1 数据字段选择与缺失值处理

做上证指数预测,最常用的数据源是日频 OHLCV(开盘、最高、最低、收盘、成交量)。但真正喂给模型的,通常只用收盘价序列,因为 ARIMA 和 LSTM 都是单变量时间序列模型,多变量虽然能上,但会引入额外噪声和维度灾难。我一般会保留日期、收盘价、成交量三列,成交量作为后续验证的辅助变量,不直接进主模型。

缺失值处理是第一个坑。沪深指数在交易日不会缺数据,但如果你用的是自己拼接的数据,节假日、停牌、数据源切换都可能造成缺口。常见做法是:先按交易日历 reindex,再用前向填充(ffill)补价格,成交量缺口用 0 或均值填。不要用插值,插值会人为制造平滑,让模型误以为那段没有波动。

import pandas as pd import numpy as np # 读取上证指数日线数据,假设字段为 date, open, high, low, close, volume df = pd.read_csv("sh_index.csv", parse_dates=["date"]) df = df.sort_values("date").set_index("date") # 只保留收盘价和成交量 df = df[["close", "volume"]].copy() # 按交易日历重采样,缺失交易日先标记 df = df.asfreq("B") # B 表示工作日,实际应按交易所日历 df["close"] = df["close"].ffill() df["volume"] = df["volume"].fillna(0) # 删除仍然为空的行(通常是数据起始段) df = df.dropna() print(df.head()) print("样本区间:", df.index.min(), "至", df.index.max(), "共", len(df), "条")

这段代码的逻辑是:先按日期排序并设为索引,保证时间顺序;然后用asfreq("B")把缺失的工作日补出来,价格前向填充、成交量补零;最后 dropna 清掉开头没有价格的空行。参数上,asfreq的频率要根据实际交易所日历调整,A 股不是简单的工作日,春节、国庆调休都会影响,严谨做法是引入pandas_market_calendars或交易所日历文件。成交量补零而不是补均值,是因为停牌日确实没有成交,补均值会制造虚假信息。

2.2 平稳性检验与差分阶数确定

ARIMA 的“I”就是差分,差几次由平稳性检验决定。上证指数收盘价几乎肯定是非平稳的,直接做 ADF 检验 p 值大概率大于 0.05。一阶差分后通常就平稳了,极少数阶段可能需要二阶,但二阶差分容易过度差分,把有用信号也差掉。

from statsmodels.tsa.stattools import adfuller def adf_test(series, name): result = adfuller(series.dropna(), autolag="AIC") print(f"{name} ADF 统计量: {result[0]:.4f}, p 值: {result[1]:.4f}") for key, value in result[4].items(): print(f" 临界值 {key}: {value:.4f}") return result[1] # 原始序列 p_raw = adf_test(df["close"], "原始收盘价") # 一阶差分 p_diff1 = adf_test(df["close"].diff().dropna(), "一阶差分") # 二阶差分(备用) p_diff2 = adf_test(df["close"].diff().diff().dropna(), "二阶差分")

逻辑说明:ADF 检验的原假设是“存在单位根,非平稳”。p 值小于 0.05 才能拒绝原假设,认为平稳。参数autolag="AIC"让 statsmodels 自动选滞后阶数,避免手动试。实际跑下来,原始收盘价 p 值通常接近 1,一阶差分后 p 值会掉到 0.01 以下,所以 d=1 是绝大多数情况下的选择。如果一阶差分还不平稳,先别急着上二阶,检查是不是数据里混入了异常值或结构断点,比如股权分置改革、熔断机制那几段,可以考虑分段建模或加虚拟变量。

2.3 训练集/测试集划分的时间顺序原则

时间序列不能随机打乱划分,这是血泪经验。必须按时间切:前 80% 做训练,后 20% 做测试,或者用滚动窗口。更严谨的做法是 walk-forward validation:每次用前 N 天训练,预测后 M 天,然后窗口向前滑动。这样评估出来的误差才接近实盘。

train_size = int(len(df) * 0.8) train = df["close"][:train_size] test = df["close"][train_size:] print("训练集:", train.index.min(), "至", train.index.max(), "共", len(train)) print("测试集:", test.index.min(), "至", test.index.max(), "共", len(test))

参数上,80/20 是常用起点,但如果你的数据只有几百条,测试集太短会导致评估不稳定,可以改成 70/30 或直接用滚动预测。注意:测试集绝对不能参与任何拟合、差分阶数选择、归一化参数计算,否则就是数据泄露,回测结果会好得离谱,实盘一塌糊涂。

3. ARIMA 建模:p、d、q 三个参数怎么定才不会靠猜

3.1 用 ACF/PACF 初定阶数,再用 AIC 网格搜索兜底

ARIMA(p,d,q) 里 d 已经由差分确定,剩下 p 和 q。传统做法是看 ACF 和 PACF 图:ACF 拖尾、PACF 截尾选 AR(p),ACF 截尾、PACF 拖尾选 MA(q),都拖尾就上 ARMA。但看图这件事玄学成分大,不同人看出不同阶数。我一般会用 AIC 网格搜索兜底,把 p 和 q 在 0 到 5 之间遍历,选 AIC 最小的组合。

import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings("ignore") best_aic = np.inf best_order = None best_model = None for p in range(0, 6): for q in range(0, 6): try: model = ARIMA(train, order=(p, 1, q)) fitted = model.fit() if fitted.aic < best_aic: best_aic = fitted.aic best_order = (p, 1, q) best_model = fitted except Exception as e: continue print("最优阶数:", best_order, "AIC:", best_aic) print(best_model.summary())

逻辑说明:order=(p,1,q)里的 1 就是前面确定的差分阶数 d。遍历范围 0 到 5 是经验值,上证指数日线一般不会超过这个范围,再大容易过拟合。AIC 越小越好,但要注意:AIC 只衡量拟合优度和复杂度,不保证残差白噪声。跑完还要看残差检验。

参数上,ARIMA类来自 statsmodels,fit()默认用 MLE。如果数据量大,可以加enforce_stationarity=False和enforce_invertibility=False加速,但会牺牲一些稳定性。实际项目中,我一般先跑网格,再对最优的几个模型做残差 Ljung-Box 检验,选残差最接近白噪声的那个。

3.2 残差提取:LSTM 要学的到底是什么

ARIMA 拟合完之后,残差 = 真实值 - ARIMA 预测值。这个残差里剩下的就是线性模型没抓到的非线性结构。LSTM 的任务就是学这个残差的模式。

# 用最优 ARIMA 模型对训练集做拟合,提取残差 train_pred = best_model.predict(start=train.index[0], end=train.index[-1]) residuals = train - train_pred residuals = residuals.dropna() print("残差统计:") print(residuals.describe()) print("残差 ADF p 值:", adfuller(residuals, autolag="AIC")[1])

逻辑说明:predict返回的是 ARIMA 对训练集的拟合值,残差就是真实减拟合。残差应该围绕 0 波动,如果均值明显不为 0,说明 ARIMA 有系统偏差。ADF 检验残差是为了确认它平稳,平稳序列 LSTM 才好学。如果残差还有明显趋势,说明 d 选少了或者数据有结构断点。

参数上,残差序列的长度和训练集一致,但开头几个点因为差分和滞后会缺失,dropna 之后会短一点。LSTM 训练时要用这个残差序列,而不是原始价格序列,这是整个组合模型的核心。

3.3 用 ARIMA 做基准预测并保存模型

训练好 ARIMA 后,要对测试集做预测,作为最终组合的线性部分。同时把模型保存下来,方便复现。

import joblib # 对测试集做预测 test_pred_arima = best_model.predict(start=test.index[0], end=test.index[-1]) # 保存模型 joblib.dump(best_model, "arima_model.pkl") print("ARIMA 测试集预测前 5 条:") print(test_pred_arima.head())

逻辑说明:predict的 start 和 end 用测试集索引,statsmodels 会自动做样本外预测。注意:ARIMA 的样本外预测是递归的,每一步都用前一步的预测值,误差会累积,所以预测步长越长越不准。保存模型用 joblib,比 pickle 对 numpy 数组更友好。

参数上,如果测试集很长,可以只预测前 30 到 60 个点,再滚动更新模型,这样更接近实盘。保存的模型文件后续可以直接 load 回来,不用重新拟合。

4. LSTM 残差建模:把非线性尾巴交给神经网络

4.1 残差序列的归一化与滑窗构造

LSTM 对输入尺度敏感,残差虽然数值不大,但仍要归一化。我一般用 MinMaxScaler 把残差缩到 [-1, 1] 或 [0, 1]。然后构造滑窗:用前 look_back 天的残差预测下一天。

from sklearn.preprocessing import MinMaxScaler from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout scaler = MinMaxScaler(feature_range=(-1, 1)) residuals_scaled = scaler.fit_transform(residuals.values.reshape(-1, 1)) def create_dataset(data, look_back=20): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i+look_back, 0]) y.append(data[i+look_back, 0]) return np.array(X), np.array(y) look_back = 20 X_train, y_train = create_dataset(residuals_scaled, look_back) X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1)) print("LSTM 训练集形状:", X_train.shape, y_train.shape)

逻辑说明:MinMaxScaler的 fit 只能在训练集残差上做,测试集残差要用同一个 scaler 变换,否则泄露。create_dataset把序列切成滑窗,look_back=20 表示用过去 20 天预测第 21 天。这个参数很关键:太小抓不到模式,太大训练慢且容易过拟合。上证指数日线我一般试 10、20、30,用验证集选。

参数上,reshape成 (样本数, 时间步, 特征数),LSTM 要求三维输入。特征数为 1 是因为只用了残差单变量。如果想加成交量残差,可以扩到 2,但会复杂化。

4.2 LSTM 网络结构与训练参数设置

网络结构不用太深,两层 LSTM 加 Dropout 足够。上证指数残差模式不会特别复杂,层数多了反而过拟合。

model = Sequential() model.add(LSTM(64, return_sequences=True, input_shape=(look_back, 1))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(1)) model.compile(optimizer="adam", loss="mse") model.summary() history = model.fit( X_train, y_train, epochs=50, batch_size=32, validation_split=0.1, verbose=1 )

逻辑说明:第一层 LSTM 64 个单元,return_sequences=True把序列传给下一层;第二层 32 个单元,输出最后一步;Dropout 0.2 防过拟合;Dense(1) 输出预测值。损失用 MSE,优化器 Adam 是默认首选。

参数上,epochs 50 是起点,看 validation loss 是否还在降,早停可以加EarlyStopping。batch_size 32 适合几千条样本。validation_split=0.1 从训练集里再切 10% 做验证,不碰测试集。如果训练 loss 降但验证 loss 升,就是过拟合,减层数或加 Dropout。

4.3 残差预测还原与组合预测

LSTM 输出的是归一化后的残差预测,要反归一化,再加回 ARIMA 的预测,才是最终价格预测。

# 对测试集残差做预测 # 先用 ARIMA 对测试集的预测值算出测试集残差(真实 - ARIMA) test_residuals = test - test_pred_arima test_residuals_scaled = scaler.transform(test_residuals.values.reshape(-1, 1)) X_test, y_test = create_dataset(test_residuals_scaled, look_back) X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1)) lstm_pred_scaled = model.predict(X_test) lstm_pred = scaler.inverse_transform(lstm_pred_scaled) # 组合预测:ARIMA 预测 + LSTM 残差预测 # 注意对齐长度,LSTM 预测从第 look_back 天开始 final_pred = test_pred_arima.values[look_back:] + lstm_pred.flatten() # 真实值对齐 true_values = test.values[look_back:] # 评估 from sklearn.metrics import mean_squared_error, mean_absolute_error rmse = np.sqrt(mean_squared_error(true_values, final_pred)) mae = mean_absolute_error(true_values, final_pred) print(f"组合模型 RMSE: {rmse:.2f}, MAE: {mae:.2f}")

逻辑说明:测试集残差 = 测试集真实值 - ARIMA 测试集预测值。用训练好的 scaler 变换,构造滑窗,LSTM 预测。反归一化后,从第 look_back 天开始与 ARIMA 预测相加。长度对齐是关键,LSTM 因为滑窗会少掉前 look_back 个点。

参数上,RMSE 和 MAE 是常用指标,但要注意:价格预测的 RMSE 受价格量级影响,上证指数 3000 点附近,RMSE 几十点算正常。更直观的是看 MAPE 或方向准确率。如果组合模型还不如单独 ARIMA,检查残差是否真的还有非线性,或者 LSTM 过拟合了。

5. 避坑与排查:这套组合模型最容易翻车的 5 个地方

5.1 现象:回测 RMSE 极低,实盘完全对不上

原因:数据泄露。最常见的是归一化 scaler 用了全量数据 fit,或者差分阶数、ARIMA 阶数用测试集选出来的。另一个隐蔽原因是滑窗构造时,训练集和测试集边界处混入了未来信息。

解决:严格按时间切分,所有 fit 操作只在训练集上做。测试集变换用训练集的 scaler。ARIMA 阶数选择只用训练集。滑窗构造时,测试集的第一个窗口应该来自测试集内部,不能跨到训练集末尾。

5.2 现象:LSTM 训练 loss 一直不降,或者降了但预测是一条直线

原因:残差序列本身接近白噪声,没有可学的模式。或者学习率、batch_size 设置不当,梯度消失。

解决:先画残差 ACF/PACF,如果残差已经像白噪声,说明 ARIMA 已经把线性部分吃干净了,LSTM 学不到东西,这时候组合模型不会比 ARIMA 好。可以尝试换更复杂的 ARIMA 或者直接放弃 LSTM。如果残差还有结构,检查 LSTM 输入是否归一化、学习率是否太大(试 0.001 或 0.0001)、batch_size 是否太小。

5.3 现象:组合预测在拐点处滞后严重

原因:LSTM 本质是自回归,对突变反应慢。ARIMA 也是线性外推,拐点处两者都滞后。

解决:这是模型固有局限,不要指望纯时序模型抓拐点。可以加入外部变量,比如成交量突变、波动率指标,或者用分类模型先判断涨跌方向,再让回归模型预测幅度。另一个做法是缩短 look_back,让模型更关注近期变化,但会牺牲稳定性。

5.4 现象:ARIMA 拟合报错 “Non-stationary starting autoregressive parameters”

原因:数据差分后仍不平稳,或者 p 阶数太高导致参数估计不稳定。

解决:先确认 d 是否选够,ADF 检验一阶差分 p 值是否小于 0.05。如果还不行,检查数据有没有异常值,比如单日暴涨暴跌,可以 winsorize 处理。或者降低 p 和 q 的上限,从 0 到 3 重新搜。statsmodels 的 ARIMA 可以加enforce_stationarity=False绕过,但预测会不可靠,不推荐。

5.5 现象:LSTM 预测结果每次跑都不一样

原因:神经网络随机初始化,且没有设随机种子。

解决:在 import tensorflow 之后、建模型之前设tf.random.set_seed(42)和np.random.seed(42)。但即使这样,GPU 上的某些操作仍可能有微小非确定性。如果要求完全可复现,用 CPU 训练,或者固定所有随机源。实际项目中,我一般跑 5 次取平均,减少随机性影响。

6. 进阶技巧:用滚动预测和方向准确率验证组合模型是否值得上

6.1 滚动窗口预测:更接近实盘的评估方式

一次性预测整个测试集,误差会累积,评估偏乐观。滚动预测是每次只预测下一天,然后把真实值加入历史,重新拟合或更新模型。这样更接近实盘,但计算量大。

def walk_forward_arima_lstm(series, train_size, look_back=20): predictions = [] for i in range(train_size, len(series)): train_data = series[:i] # ARIMA 拟合 model_arima = ARIMA(train_data, order=best_order).fit() arima_pred = model_arima.forecast(steps=1)[0] # 残差 residuals = train_data - model_arima.predict(start=train_data.index[0], end=train_data.index[-1]) residuals = residuals.dropna() # 这里简化:实际应重新训练 LSTM 或用已训练模型 # 为演示,直接用已训练 LSTM 对最新残差窗口预测 # 真实项目中建议每 N 步重新训练一次 LSTM predictions.append(arima_pred) # 占位,实际应加 LSTM 残差预测 return predictions

逻辑说明:这个函数演示了滚动框架,但完整实现需要每步重新提取残差、构造滑窗、用 LSTM 预测。实际中为了效率,可以每 20 或 60 步重新训练一次 LSTM,中间用已训练模型推理。参数上,train_size 是初始训练长度,look_back 和之前一致。

6.2 方向准确率:比 RMSE 更实用的指标

价格预测的 RMSE 受量级影响,方向准确率(涨跌判断正确比例)更直观。如果方向准确率能稳定在 55% 以上,这套模型就有实用价值。

def direction_accuracy(true_values, pred_values): true_diff = np.diff(true_values) pred_diff = np.diff(pred_values) correct = np.sum((true_diff > 0) & (pred_diff > 0)) + np.sum((true_diff < 0) & (pred_diff < 0)) return correct / len(true_diff) acc = direction_accuracy(true_values, final_pred) print(f"方向准确率:{acc:.2%}")

逻辑说明:比较真实涨跌和预测涨跌的符号,一致就算对。注意:这个指标对随机波动敏感,短期日频数据方向准确率很难超过 60%,50% 左右就是随机水平。如果只有 50%,说明模型没有方向判断能力,只能做幅度参考。

6.3 组合权重:ARIMA 和 LSTM 一定要 1:1 相加吗

不一定。可以给 ARIMA 和 LSTM 的预测各加一个权重,用验证集优化。常见做法是线性回归融合,或者简单网格搜索权重。

best_w = 0 best_rmse = np.inf for w in np.arange(0, 1.1, 0.1): combined = w * test_pred_arima.values[look_back:] + (1 - w) * lstm_pred.flatten() rmse = np.sqrt(mean_squared_error(true_values, combined)) if rmse < best_rmse: best_rmse = rmse best_w = w print(f"最优 ARIMA 权重:{best_w:.1f}, RMSE:{best_rmse:.2f}")

逻辑说明:w 是 ARIMA 权重,1-w 是 LSTM 权重。在验证集上搜最优 w,再应用到测试集。参数上,步长 0.1 够用,更细可以 0.05。注意:权重也要在验证集上选,不能用测试集,否则又是泄露。

6.4 我踩过的坑与固定习惯

这套 ARIMA+LSTM 组合我前后跑了不下十遍,最大的教训是:不要迷信复杂模型。很多时候 ARIMA 单独跑就能达到组合模型 90% 的效果,LSTM 带来的提升有限,但工程复杂度翻倍。如果方向准确率没有显著提升,不如把精力花在数据质量和特征工程上。我现在的固定习惯是:先跑 ARIMA 基准,记录 RMSE 和方向准确率;再上 LSTM 残差,只有验证集上方向准确率提升超过 3 个百分点,才考虑保留。另外,随机种子、数据版本、参数配置全部写进配置文件,不然两周后自己都复现不出来。希望帮到你。

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

RELATED READING

延伸阅读

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