融合LSTM与ARIMA的黄金价格预测系统设计
2026/9/10 9:37:20 网站建设 项目流程

简介:针对黄金价格预测这一金融分析场景,这份资源提供了一套结合LSTM神经网络与ARIMA时间序列模型的融合预测方案,面向投资者、金融从业者及数据科学初学者与研究者,包含完整可运行的算法代码。资源基于历史黄金价格数据完成预处理,先由ARIMA模型识别线性趋势并给出初步预测,再借助LSTM深度学习网络学习长期依赖与非线性特征,对初步结果进行优化,从而降低过拟合风险并提升预测准确性。压缩包共8个文件,涵盖Python算法脚本、CSV历史数据、说明文档、Docx教程与README等,整体约58KB,内容覆盖数据读取、建模、训练到预测全流程,并附有各文件用途说明,方便快速上手。目前已有161人参与学习,可用作理解模型融合原理、复现实验或课程设计、毕业设计、金融量化研究的参考框架;通过该资源既能掌握ARIMA与LSTM的建模要点,也能体会两种方法在时序预测中的协同互补,具备较强的实践价值。

1. 融合LSTM与ARIMA的黄金价格预测系统:主线是分工,不是堆模型

把黄金价格直接丢给LSTM训练的模型,结果经常不如一个简单的ARIMA(2,1,2)外推——原因不在深度学习不行,而在于LSTM把一个本来可以用线性方程描述的缓慢趋势反复学了很多遍,浪费了记忆容量。我一般不会让LSTM单独面对原始价格序列,而是先让ARIMA把趋势、周期这些线性成分吸收掉,再把ARIMA拟合后剩下的残差序列交给LSTM去捕捉跳变和波动聚集,最后把两路预测相加。这个“先ARIMA初步预测、LSTM再在残差上优化结果”的融合路线,是做黄金价格日频预测时最容易落地的一条路径。对刚接触时间序列的人来说,这套实现能同时讲清楚统计模型和深度学习在同一任务里的分工;对有经验的人而言,残差归因、滑窗设计和回测防泄漏才是真正需要盯住的部分。

2. 先让ARIMA把线性趋势“榨干”:差分、定阶与残差白噪声检验

2.1 ARIMA为何只做“初步预测”不做终局模型

黄金日线价格不是平稳序列:它既有长期趋势,也有由避险情绪、美元指数变动驱动的跳跃。ARIMA的本质是线性模型——自回归项描述当前值与过去值的线性组合,移动平均项描述过去误差的叠加,d次差分用来压制趋势和周期性。这个线性框架天然适合把趋势和周期“榨”出来,所以让ARIMA先跑第一轮预测是合理的第一步。

但ARIMA也有明确边界:它对波动聚集、尖峰厚尾、突发跳变这类非线性特征缺乏表达能力。黄金价格在宏观数据公布后常出现“先跳后回”的形态,这是典型非线性行为;ARIMA的残差里会留下明显结构,而不是白噪声。恰恰是这部分残差,可以作为LSTM的学习材料。这就是“ARIMA初步预测+LSTM二次优化”融合思想的核心——ARIMA做线性降维,LSTM做残差纠偏。

2.2 用ADF与KPSS双检验确认差分阶数 d

差分阶数 d 是ARIMA的“地基”。常用两种检验:ADF检验的原假设是“序列存在单位根,非平稳”,p值小于0.05时拒绝原假设,认为序列平稳;KPSS检验则反过来,原假设是“序列平稳”。单看ADF可能在高波动序列上误判,所以我习惯把两者并排跑:两个检验都通过,才把 d 定下来。

import pandas as pd from statsmodels.tsa.stattools import adfuller, kpss def stationarity_report(series, name="gold_price"): series = series.dropna() # ADF: H0=存在单位根,拒绝H0才平稳 adf_stat, adf_p, *_ = adfuller(series, autolag="AIC") # KPSS: H0=序列平稳,不拒绝H0才平稳 kpss_stat, kpss_p, _, _ = kpss(series, regression="ct") print(f"[{name}] ADF p={adf_p:.4f}, KPSS p={kpss_p:.4f}") if adf_p < 0.05 and kpss_p > 0.05: print("结论: 两种检验口径下都支持平稳") else: print("结论: 需要进一步差分,先试 d=1") # prices 是含日频收盘价的 DataFrame,列名为 close stationarity_report(prices["close"])

逻辑说明:adfuller的 autolag 参数用AIC自动选择最优滞后阶数;kpss的 regression="ct" 表示原假设中带有常数项和趋势项。两个结果的解读不是“OR”而是“AND”:只有ADF拒绝单位根且KPSS不拒绝平稳,才能放心使用当前的d。如果ADF的p值大于0.05而KPSS的p值小于0.05,说明非平稳迹象明显,应把d设为1再重跑一次。日频黄金价格一般差分一阶后就能满足要求,差分两阶要警惕过度差分导致的信息损失。

2.3 用ACF、PACF和auto_arima定 p、q

定完 d,再定 p 和 q。看相关图的规律是:AR项对应PACF截尾、ACF拖尾;MA项对应ACF截尾、PACF拖尾;ARMA项则两者都拖尾。这个判断对新手来说主观成分太大,所以自动定阶反而更可靠:

序列特征ACF行为PACF行为对应模型
过去价格影响现在拖尾(缓慢衰减)截尾(k步后突变为0)AR(p) 或带AR成分
过去误差影响现在截尾拖尾MA(q) 或带MA成分
价格与误差都影响现在拖尾拖尾ARMA(p,q)
所有阶都无显著全接近0全接近0白噪声,不宜建模
from pmdarima import auto_arima # stepwise=True 用逐步搜索,减少组合数量 model = auto_arima( prices["close"], start_p=0, max_p=5, start_q=0, max_q=5, d=1, # 由平稳性检验确定 seasonal=False, trace=False, error_action="ignore", suppress_warnings=True, stepwise=True, information_criterion="aic", ) print(model.order) # (p, d, q) print(model.summary())

逻辑说明:auto_arima的内部逻辑是按信息准则比较多个候选ARIMA模型;d直接传入差分阶数,避免它自己从头搜索。information_criterion="aic"表示用AIC做模型比较,样本量较大时可以换bic对参数数量惩罚更重。stepwise=True不会遍历全部组合,速度更快,但个别边界组合可能漏检;数据量小时开stepwise=False做全表搜索也行。打印model.order可以拿到最终定阶结果,例如 (2,1,1),代表AR(2)、一次差分、MA(1)。

2.4 Ljung-Box残差检验:决定哪些残差交给LSTM

ARIMA拟合完成后,要把残差序列提取出来,先判断它是否还存在自相关。Ljung-Box检验的原假设是“残差序列相互独立”,p值大于0.05说明残差接近白噪声,这时融合收益有限;p值小于0.05说明残差里还残留可学习的结构,LSTM才有用武之地。

from statsmodels.stats.diagnostic import acorr_ljungbox # fitted 是ARIMA模型的拟合值,resid 是残差序列 resid = prices["close"] - fitted lb_test = acorr_ljungbox(resid, lags=[10, 20], return_df=True) print(lb_test)

逻辑说明:lags=[10, 20]分别检验10阶和20阶滞后范围内是否存在自相关。return_df=True让输出变成DataFrame,方便读取lb_pvalue列。如果10阶和20阶的p值都小于0.05,残差里仍有明显的线性或非线性结构,值得进入LSTM训练流程;如果p值接近1,说明ARIMA已经把信息提取干净,后续融合优先考虑其他特征工程,而不是硬接LSTM。

3. 用PyTorch搭LSTM训练管道:滑窗、归一化与门控细节

3.1 把价格序列切成“滑窗+标签”:LSTM的样本到底是什么

LSTM的输入是一个三维张量(batch, seq_len, input_size)seq_len是回看的天数,input_size是每个时间步的特征数量。对日频黄金价格,我用过去20到60个交易日预测未来一天或未来三天均值,窗口太短学不到趋势惯性,太长则把几年前的旧行情也带进来,反而干扰判断。

import numpy as np import torch def make_windows(data, seq_len=30, horizon=1): xs, ys = [], [] for i in range(len(data) - seq_len - horizon + 1): xs.append(data[i:i + seq_len]) ys.append(data[i + seq_len:i + seq_len + horizon].mean()) return np.array(xs, dtype=np.float32), np.array(ys, dtype=np.float32) # prices 是一维numpy数组,提前做了归一化 X, y = make_windows(prices, seq_len=30, horizon=1)

逻辑说明:seq_len=30表示用过去30个交易日预测未来,horizon=1表示预测一步(这里取未来一天均值)。xs的形状是(样本数, 30, 1),但此时还缺一个维度,送到PyTorch前要 reshape为(样本数, 30, 1)。窗口步长为1会导致相邻样本高度重叠,训练时模型容易“背答案”,常见做法是把步长调成seq_len//2或直接随机采样,让训练集样本之间没那么强的相关性。

3.2 归一化里最容易漏的泄漏:scaler只fit训练段

LSTM对输入尺度非常敏感,黄金价格从1500美元涨到2500美元,数值范围变化很大,不归一化会拖慢收敛甚至让Loss不降。一般用Min-Max归一化把价格缩放到 [0,1] 或 [-1,1]。关键一步是:fit只能作用在训练段上,验证段和测试段都用训练段的scaler做transform

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) train_len = int(len(prices) * 0.7) # fit_transform 只用在训练段 scaled_train = scaler.fit_transform(prices[:train_len].reshape(-1, 1)) # transform 不带 fit,防止测试段信息泄漏 scaled_test = scaler.transform(prices[train_len:].reshape(-1, 1))

逻辑说明:如果对整个序列一起fit_transform,测试段的最高价、最低价会提前参与归一化,导致训练阶段就“看到”了未来信息。这个问题在时序预测里极其隐蔽,回测时表现很好,一到实盘就失效,根源往往就在这里。选择feature_range=(0,1)而不是(-1,1)时要注意LSTM默认激活函数是tanh,输出范围在(-1,1)之间,输入范围越接近输出范围,前向传播的数值越稳定;因此用(-1,1)往往比(0,1)收敛更平缓。

3.3 PyTorch模型定义与训练循环

黄金价格日频数据量不大,LSTM层数1到2层就足够,隐藏单元数量128到256之间比较常用。下面是一个可以直接在Notebook里跑的最小实现:

import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=128, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) return self.fc(out[:, -1, :]) # 取最后一个时间步的隐状态 model = LSTMPredictor() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss()

逻辑说明:batch_first=True让输入形状变成(batch, seq_len, input_size),和NumPy的窗口数组排列一致,省去转置麻烦。out[:, -1, :]取出序列最后一个时间步的隐状态,代表模型读完整个窗口后的“浓缩理解”,接一层全连接输出价格。dropout=0.2只对多层LSTM的层间连接生效,单层时会被PyTorch忽略,这是常见误解。hidden_size大并不一定好,黄金日线数据样本通常只有几千条,隐藏单元超过256后容易过拟合,验证损失开始上升而训练损失还在下降。

训练循环里,我会加上梯度裁剪和早停:

from torch.utils.data import DataLoader, TensorDataset dataset = TensorDataset(torch.from_numpy(X), torch.from_numpy(y)) loader = DataLoader(dataset, batch_size=64, shuffle=True) for epoch in range(60): model.train() epoch_loss = 0.0 for x_batch, y_batch in loader: pred = model(x_batch.unsqueeze(-1)) loss = loss_fn(pred.squeeze(), y_batch) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() epoch_loss += loss.item() if (epoch + 1) % 10 == 0: print(f"epoch {epoch + 1}, loss={epoch_loss / len(loader):.6f}")

逻辑说明:unsqueeze(-1)(batch, seq_len)变成(batch, seq_len, 1),匹配input_size=1max_norm=5.0是梯度裁剪的常见默认值,LSTM训练时梯度在时间步上连乘,容易出现梯度爆炸,裁剪阈值设在5到10之间能稳定训练。shuffle=True在纯序列预测里要谨慎:相邻样本高度重叠,shuffle后训练更平稳,但模型学到的时间依赖可能变弱;如果验证集表现不佳,可以改成shuffle=False对比一次。

3.4 遗忘门、输入门、输出门在价格预测里都在干什么

LSTM靠三个门控制信息流动。遗忘门决定“过去的价格模式哪些该忘掉”,比如黄金进入新的震荡区间后,半年前的支撑位信息就应该衰减;输入门决定“当前这个价格波动有多少值得写进记忆”,突发跳水这类小概率事件会通过输入门被放大;输出门决定“此刻把记忆中哪些部分转成预测信号”。

三者的计算都依赖当前输入和上一时刻的隐状态,并通过sigmoid输出0到1之间权重。记忆单元内部用的是tanh激活,把候选值压制在(-1,1)之间,防止数值无限增长。所以LSTM的超参数里,hidden_size本质上是“记忆容量”,num_layers是“特征抽象的层级数”。对黄金价格这种单变量输入,我通常把特征扩大到“收盘价、日均振幅、20日滚动波动率”再进LSTM,这样input_size=3,记忆容量128即可。特征增加后,遗忘门和输入门的组合会在训练中自动学习“震荡期少更新记忆、趋势期多写入当前价格”的切换逻辑。

参数默认参考值调整方向
seq_len30序列越长,趋势信息越多,但训练样本越少
hidden_size128过拟合时调小,欠拟合时调大
num_layers2超过2层在千级样本上收益有限
dropout0.2只在多层LSTM之间生效
lr1e-3验证loss震荡时降到3e-4
batch_size64样本少时降到32更稳

4. ARIMA初预测+LSTM残差修正:三种融合线路与误差传导

4.1 残差叠加:最贴近“ARIMA初步预测后LSTM优化结果”的做法

先让ARIMA对历史价格完成拟合,得到拟合值;真实价格减去拟合值得到残差序列。把这段残差当作LSTM的“新价格序列”来训练,预测未来残差。最后将ARIMA的未来预测值加上LSTM的残差预测值,就是融合系统的最终输出。

import numpy as np # arima_model 来自第2章的 auto_arima in_sample_pred = arima_model.predict_in_sample() resid = prices["close"].values - in_sample_pred # 对残差序列做滑窗,训练LSTM resid_scaler = MinMaxScaler(feature_range=(-1, 1)) resid_scaled = resid_scaler.fit_transform(resid.reshape(-1, 1)) X_resid, y_resid = make_windows(resid_scaled.flatten(), seq_len=30, horizon=1) # 训练LSTM残差模型,代码同第3章 # lstm_resid_model.predict(X_resid) 得到残差预测 future_arima = arima_model.predict(n_periods=5) # ARIMA的未来预测 future_resid = lstm_resid_model.rescale_and_predict(5) # LSTM的未来残差预测 final_forecast = future_arima + future_resid

逻辑说明:predict_in_sample拿的是ARIMA对训练期的拟合值,对应的残差才是LSTM可以学习的材料;如果用滚动预测的predict结果,残差里会混入预测误差,训练目标不稳定。resid_scaler的范围设为(-1,1),因为LSTM的隐状态经过tanh,残差序列本身有正有负,映射到这个区间能保留符号信息。最终预测是两段直接相加:ARIMA提供趋势基准,残差LSTM只在基准上下做修正。这个方案的好处是ARIMA的线性预测逻辑完整保留,坏处是残差预测误差会原样传导到最终结果,如果ARIMA的残差本身已经接近白噪声,融合收益会非常小。

4.2 特征拼接:把ARIMA预测值当成LSTM的外生特征

残差叠加之外,另一种常见路线是把ARIMA的预测输出当作LSTM的输入特征。具体做法是:每次滑窗训练时,ARIMA都对这个窗口的未来做出一个“初步预测”,把预测值作为一维额外特征拼在滑窗末端,LSTM学的是“在ARIMA的判断之上,价格会怎么偏离”。这个方案对LSTM的压力更大,因为它要同时理解ARIMA的预测逻辑和价格本身的动态;但好处是误差不会像残差叠加那样逐层传导,LSTM有机会在训练中自动纠正ARIMA的线性偏差。

实际操作中,特征拼接对特征工程的要求更高。ARIMA预测值必须和滑窗的末端时刻对齐,否则LSTM会把一个错位的参考值当作正确基准。我一般只在外生变量数量明确(比如美元指数、实际利率)时才做拼接,单独用ARIMA输出做特征的效果并不比残差叠加明显。

4.3 加权集成:在验证集上求解最优权重

把ARIMA和LSTM当成两个完全独立的预测器,在验证集上寻找最优加权系数,是融合的最后一种常见做法。权重不能手拍,需要一个盲测区间。

from scipy.optimize import minimize def weighted_loss(w): pred = w[0] * arima_pred + (1 - w[0]) * lstm_pred return np.mean((pred - y_true) ** 2) # w 的初值取0.5,约束在0到1之间 res = minimize(weighted_loss, x0=[0.5], bounds=[(0, 1)], method="Nelder-Mead") best_w = res.x[0] print(f"最优权重: ARIMA={best_w:.3f}, LSTM={1 - best_w:.3f}")

逻辑说明:arima_predlstm_pred是在验证集上各自独立做出的预测,y_true是验证集真实值。优化目标是最小化均方误差,bounds=[(0,1)]保证权重在两个模型之间分配,不会出现负权重。样本量小的时候,验证集上求出的权重可能会过拟合这一小段行情;我一般还会多看权重在不同时间段里的稳定性,若ARIMA权重在震荡市和趋势市之间波动超过0.3,加权集成反而增加不稳定因素。

4.4 融合策略对比

融合方式可解释性误差传导适用场景
残差叠加高,ARIMA的线性判断清晰可见ARIMA残差误差直接传给最终结果数据量小、追求稳定
特征拼接中,LSTM会修正ARIMA输出误差传递被打散有多维外生特征时
加权集成低,权重依赖验证段行情两路误差相互对冲但依赖权重两个模型表现差异大时

选择时我的原则是:先用残差叠加快速看LSTM在残差上是否学到了有效结构;如果残差训练损失和验证损失都下降,说明非线性成分存在,再扩展特征拼接;加权集成放在最后,因为它会引入一个“验证集权重”,多一层过拟合风险。

5. 滚动回测验证方向准确率:融合系统上线前的最后一关

5.1 walk-forward回测怎么写

融合系统不能像普通机器学习那样随机切分训练集,必须按时间顺序滚动。每轮只“看到”过去,预测未来,然后再把真实观测值纳入下一轮训练。

def walk_forward(X, y, model_fn, train_days=700, step=250): preds, trues = [], [] start = train_days while start + step <= len(X): train_x, train_y = X[:start], y[:start] test_x, test_y = X[start:start + step], y[start:start + step] model = model_fn(train_x, train_y) # 重新训练 preds.extend(model.predict(test_x).flatten()) trues.extend(test_y.flatten()) start += step return np.array(preds), np.array(trues)

逻辑说明:train_days=700大约三年交易日,保证ARIMA和LSTM都有足够数据;step=250是一年交易日,每次预测一年后把真实值并入训练集。滚动回测的代价是训练多次,LSTM模型较慢时可以把step调大到500减少训练轮次,但回测结论会更粗。

5.2 方向准确率:投资者真正该盯的指标

一个预测系统的RMSE再低,如果涨跌方向经常判错,对买卖决策几乎没有参考价值。方向准确率(DA)的计算是:预测变动方向与真实变动方向一致的比例。

def direction_accuracy(y_true, y_pred): true_dir = np.sign(np.diff(y_true)) pred_dir = np.sign(np.diff(y_pred)) return (true_dir == pred_dir).mean() true_dir = np.sign(np.diff(y_true)) pred_dir = np.sign(np.diff(y_pred)) print(f"方向准确率: {direction_accuracy(y_true, y_pred):.2%}")

逻辑说明:np.diff得到相邻两天的差值,np.sign转成1、0、-1,逐位比较。如果方向准确率稳定在55%以上,配合标的本身的波动率可以设计简单的多空策略;低于52%时,预测结果基本接近随机,不建议直接上交易决策。回测还要看分段表现,比如震荡市和单边上涨市的方向准确率往往差异很大,融合系统如果只在趋势行情里有效,就要在文档里明确这个边界条件。

5.3 三个容易泄漏数据的位置

第一个是归一化,scaler必须只在训练段fit,测试段只能transform。第二个是滑窗拼接,训练集最后一个窗口的结束时间,不能跨过训练/测试的分割点,否则预测目标会提前出现在输入里。第三个是滚动回测重新训练时,ARIMA的d和LSTM的seq_len每次都要用截至当前的数据重新确定,不能沿用全量数据下的最优参数。这三条同时满足,融合系统的回测结果才值得当作决策依据。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询