(注:使用文中代码的时候需要加上冒号后的缩进,否则报语法错误。如需复制代码建议点击文末左下角阅读原文。)
1.项目背景
黄金价格一直被视为宏观经济的重要“风向标”,它不仅受到供需关系的影响,还与通胀水平、利率政策、地缘政治风险等多种因素密切相关。在全球经济不确定性加剧的背景下,黄金作为避险资产,其价格波动呈现出更强的复杂性和非线性特征,传统基于统计方法的预测模型在刻画这种动态变化时往往存在一定局限。随着深度学习技术的发展,尤其是擅长处理时序数据的LSTM模型,为金融时间序列预测提供了新的思路。
基于此,本文以长期黄金价格数据为基础,通过构建包含多种技术指标的特征体系,引入LSTM模型对未来价格进行预测,尝试从数据驱动的角度提升对黄金价格走势的刻画能力,为相关研究和实际投资决策提供参考。
2.数据集介绍
本实验数据集来源于Kaggle,该数据集包含了2000-08-30 -> 2026-03-03的黄金股票数据。
黄金是全球最重要的宏观资产之一,通常反映风险情绪、通胀预期和宏观经济压力。
探讨的主题:
• 市场结构可视化
• 波动率聚类
• 机制检测
• 季节性行为
• 分布分析
• 动量动态
• 流动性行为
• 多维可视化
3.技术工具
Python版本:3.9
代码编辑器:jupyter notebook
4.实验过程
4.1导入数据
在时间序列建模之前,首先需要完成数据读取与基础预处理工作。本部分主要加载黄金价格数据,并对时间字段进行格式转换与排序处理,确保数据按照时间顺序排列,这对于后续基于LSTM的序列建模非常关键。同时,对字段名称进行统一规范,便于后续代码调用,并输出数据的基本信息,用于初步了解数据规模、时间范围以及价格区间。
# 导入数据处理与可视化库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# 导入PyTorch相关库
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
# 导入数据预处理与评估指标
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
# 忽略警告
import warnings
warnings.filterwarnings('ignore')
# 设置随机种子,保证实验结果可复现
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)
# 设置运行设备(GPU优先)
device = torch.device('cuda'if torch.cuda.is_available() else'cpu')
# 读取黄金价格数据
df = pd.read_csv('/kaggle/input/datasets/krupalpatel07/gold-price-dynamics/GoldUSD.csv')
# 处理日期列(原始格式为日-月-年)
df['Date'] = pd.to_datetime(df['Date'], dayfirst=True)
# 按日期排序,确保时间序列顺序正确
df = df.sort_values('Date').reset_index(drop=True)
# 重命名列,统一字段名称
df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume']
# 输出数据基本信息
print(f'Shape : {df.shape}') # 数据维度
print(f'Date : {df.Date.min().date()} -> {df.Date.max().date()}') # 时间范围
print(f'Close : min={df.Close.min():.2f} max={df.Close.max():.2f}') # 收盘价范围
# 查看前几行数据
df.head()
运行
4.2数据预处理
在完成原始数据的整理之后,需要进一步构造适用于时间序列预测的特征。本部分主要基于原始价格数据衍生出一系列技术指标,例如收益率、价格波动区间、移动平均线、波动率以及动量指标等,这些特征能够更好地刻画黄金价格的趋势与波动特性。同时,将下一时间步的收盘价作为预测目标,从而构建监督学习任务。由于滚动计算和位移操作会产生缺失值,因此需要对数据进行清洗处理,确保后续模型训练的数据完整性。
# ==============================
# 构造技术指标特征
# ==============================
# 计算收益率(反映价格变化幅度)
df['Return'] = df['Close'].pct_change()
# 计算最高价与最低价的差值(波动区间)
df['HL_range'] = df['High'] - df['Low']
# 计算收盘价与开盘价差值(日内变化)
df['OC_range'] = df['Close'] - df['Open']
# 计算不同周期的移动平均线(趋势指标)
df['MA_5'] = df['Close'].rolling(5).mean()
df['MA_20'] = df['Close'].rolling(20).mean()
df['MA_50'] = df['Close'].rolling(50).mean()
# 计算短期波动率(标准差)
df['Volatility_5'] = df['Return'].rolling(5).std()
# 计算动量指标(反映价格变化趋势)
df['Momentum_5'] = df['Close'] - df['Close'].shift(5)
df['Momentum_10'] = df['Close'] - df['Close'].shift(10)
# 计算成交量移动平均
df['Volume_MA5'] = df['Volume'].rolling(5).mean()
# 当前价格相对于MA20的偏离程度
df['Price_vs_MA20'] = (df['Close'] - df['MA_20']) / df['MA_20']
# ==============================
# 构造预测目标
# ==============================
# 将下一天的收盘价作为预测目标
df['Target'] = df['Close'].shift(-1)
# ==============================
# 数据清洗
# ==============================
# 删除因滚动计算和shift产生的缺失值
df.dropna(inplace=True)
# 重置索引
df.reset_index(drop=True, inplace=True)
# ==============================
# 定义特征列
# ==============================
# 所有用于模型输入的特征
FEATURES = ['Open', 'High', 'Low', 'Close', 'Volume',
'Return', 'HL_range', 'OC_range',
'MA_5', 'MA_20', 'MA_50',
'Volatility_5', 'Momentum_5', 'Momentum_10',
'Volume_MA5', 'Price_vs_MA20']
运行
4.3特征工程
在时间序列预测任务中,模型不仅依赖单一时刻的数据,而是需要结合一段时间窗口内的历史信息进行学习。因此,本部分将原始特征数据转化为序列形式,同时对特征和目标变量分别进行归一化处理,以提高模型训练的稳定性。随后,通过滑动窗口方式构建输入序列,并按照时间顺序划分训练集、验证集和测试集,避免数据泄露问题。最后,将数据封装为PyTorch可读取的数据集与数据加载器,以便后续模型训练使用。
# 设置时间窗口长度(使用过去30天预测下一天)
WINDOW = 30
# 初始化特征与目标的归一化器
feature_scaler = MinMaxScaler()
target_scaler = MinMaxScaler()
# 对输入特征进行归一化
X_scaled = feature_scaler.fit_transform(df[FEATURES].values)
# 对预测目标进行归一化
y_scaled = target_scaler.fit_transform(df[['Target']].values)
# ==============================
# 构建时间序列样本
# ==============================
defbuild_sequences(X, y, window):
Xs, ys = [], []
for i inrange(window, len(X)):
# 取过去window天作为输入
Xs.append(X[i - window:i])
# 当前时刻的目标值
ys.append(y[i])
return np.array(Xs, dtype=np.float32), np.array(ys, dtype=np.float32)
# 构建序列数据
X_seq, y_seq = build_sequences(X_scaled, y_scaled, WINDOW)
# ==============================
# 按时间顺序划分数据集(不能打乱)
# ==============================
# 80%训练集,10%验证集,10%测试集
split_80 = int(len(X_seq) * 0.80)
split_90 = int(len(X_seq) * 0.90)
X_train, y_train = X_seq[:split_80], y_seq[:split_80]
X_val, y_val = X_seq[split_80:split_90], y_seq[split_80:split_90]
X_test, y_test = X_seq[split_90:], y_seq[split_90:]
# 输出数据集规模与形状
print(f'Train: {len(X_train)} Val: {len(X_val)} Test: {len(X_test)}')
print(f'Sequence shape: {X_train.shape} -> (samples, window, features)')
运行
通过上述处理,已经将原始时间序列数据转化为适用于LSTM模型输入的三维结构(样本数 × 时间窗口 × 特征数),同时保证数据划分符合时间顺序,为模型训练提供了规范的数据格式。
# ==============================
# 构建PyTorch数据集类
# ==============================
classGoldDataset(Dataset):
def__init__(self, X, y):
# 将numpy数组转换为tensor
self.X = torch.tensor(X, dtype=torch.float32)
self.y = torch.tensor(y, dtype=torch.float32)
def__len__(self):
# 返回样本数量
returnlen(self.y)
def__getitem__(self, idx):
# 根据索引返回一个样本
return self.X[idx], self.y[idx]
# ==============================
# 构建数据加载器
# ==============================
# 训练集(打乱顺序)
train_loader = DataLoader(
GoldDataset(X_train, y_train),
batch_size=64,
shuffle=True
)
# 验证集(不打乱)
val_loader = DataLoader(
GoldDataset(X_val, y_val),
batch_size=64,
shuffle=False
)
# 测试集(不打乱)
test_loader = DataLoader(
GoldDataset(X_test, y_test),
batch_size=64,
shuffle=False
)
运行
通过将数据封装为Dataset与DataLoader,可以方便地按批次加载数据并输入模型进行训练,同时提升训练效率,也为后续模型训练与评估做好了准备。
4.4构建模型
在完成序列数据构建之后,这一步主要搭建用于时间序列预测的LSTM模型。这里采用的是多层堆叠的LSTM结构,用于捕捉黄金价格在时间维度上的动态变化规律,并通过全连接层完成最终的数值预测。同时,对模型参数进行初始化,并设置损失函数、优化器以及学习率调度策略,以提升训练稳定性和模型泛化能力。
# 定义LSTM模型
classGoldPricePredictor(nn.Module):
"""
Stacked LSTM -> Dropout -> Fully connected head
Input : (batch, window=30, features=16)
Output : (batch, 1) — next day scaled close price
"""
def__init__(self, input_size, hidden_size=128, num_layers=2, dropout=0.3):
super().__init__()
# LSTM层(多层堆叠)
self.lstm = nn.LSTM(
input_size = input_size, # 输入特征维度
hidden_size = hidden_size, # 隐藏层维度
num_layers = num_layers, # LSTM层数
batch_first = True, # 输入格式为(batch, seq, feature)
dropout = dropout if num_layers > 1else0.0# 多层时才启用dropout
)
# 全连接预测头
self.head = nn.Sequential(
nn.Dropout(dropout), # 防止过拟合
nn.Linear(hidden_size, 64), # 降维
nn.ReLU(), # 激活函数
nn.Dropout(dropout / 2), # 再次正则化
nn.Linear(64, 1) # 输出预测值
)
# 参数初始化(提高训练稳定性)
for name, param in self.named_parameters():
if'weight_ih'in name: nn.init.xavier_uniform_(param) # 输入权重初始化
elif'weight_hh'in name: nn.init.orthogonal_(param) # 循环权重初始化
elif'bias'in name: nn.init.zeros_(param) # 偏置初始化为0
defforward(self, x):
# LSTM前向传播
out, _ = self.lstm(x)
# 取最后一个时间步的输出作为特征
return self.head(out[:, -1, :]) # shape: (batch, hidden) -> (batch, 1)
# ==============================
# 初始化模型与训练组件
# ==============================
# 输入特征维度
input_size = len(FEATURES)
# 实例化模型并加载到设备(GPU/CPU)
model = GoldPricePredictor(input_size).to(device)
# 统计模型参数量
total_params = sum(p.numel() for p in model.parameters())
# 定义损失函数(Huber Loss,对异常值更鲁棒)
criterion = nn.HuberLoss(delta=1.0)
# 定义优化器(AdamW,带权重衰减)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
# 学习率调度器(验证集loss不下降时降低学习率)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min',
factor=0.5,
patience=10,
min_lr=1e-6
)
运行
4.5训练模型
在模型构建完成之后,这一步进入核心的训练阶段。这里采用标准的训练-验证循环,对模型参数进行迭代更新,同时结合学习率调度和早停机制来控制训练过程,避免过拟合。每一轮训练都会记录训练集和验证集的损失变化,并根据验证集表现动态调整模型状态,最终保留效果最好的模型参数。
# 设置训练轮数和早停耐心值
EPOCHS = 150
PATIENCE = 20
# 用于记录训练过程中的损失变化
history = {'train_loss': [], 'val_loss': []}
# 初始化最佳验证损失和早停计数器
best_val_loss, patience_counter, best_state = float('inf'), 0, None
# 开始训练循环
for epoch inrange(1, EPOCHS + 1):
# ==============================
# 训练阶段
# ==============================
model.train() # 设置为训练模式
tr_loss, tr_total = 0.0, 0
for Xb, yb in train_loader:
# 将数据加载到设备(GPU/CPU)
Xb, yb = Xb.to(device), yb.to(device)
optimizer.zero_grad() # 梯度清零
pred = model(Xb) # 前向传播
loss = criterion(pred, yb) # 计算损失
loss.backward() # 反向传播
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step() # 更新参数
# 累计损失
tr_loss += loss.item() * len(yb)
tr_total += len(yb)
# ==============================
# 验证阶段
# ==============================
model.eval() # 设置为评估模式
va_loss, va_total = 0.0, 0
with torch.no_grad(): # 关闭梯度计算
for Xb, yb in val_loader:
Xb, yb = Xb.to(device), yb.to(device)
pred = model(Xb) # 前向传播
loss = criterion(pred, yb) # 计算损失
va_loss += loss.item() * len(yb)
va_total += len(yb)
# 计算平均损失
tr_l = tr_loss / tr_total
va_l = va_loss / va_total
# 更新学习率(根据验证集loss)
scheduler.step(va_l)
# 记录损失
history['train_loss'].append(tr_l)
history['val_loss'].append(va_l)
# ==============================
# 早停机制
# ==============================
if va_l < best_val_loss:
# 如果验证损失下降,保存当前模型参数
best_val_loss, patience_counter = va_l, 0
best_state = {k: v.clone() for k, v in model.state_dict().items()}
else:
# 否则增加计数器
patience_counter += 1
if patience_counter >= PATIENCE:
print(f'Early stop @ epoch {epoch}')
break
# 每20轮或第1轮打印一次日志
if epoch % 20 == 0or epoch == 1:
print(f'Epoch {epoch:>4} Train Loss: {tr_l:.6f} Val Loss: {va_l:.6f}')
# 加载最优模型参数
model.load_state_dict(best_state)
# 输出最佳验证损失
print(f'nBest val loss: {best_val_loss:.6f}')
运行
4.6模型评估
在模型训练完成后,需要从多个角度对模型效果进行评估,包括训练过程收敛情况、预测精度以及误差分布等。本部分首先通过损失曲线观察模型训练过程中的表现,然后在测试集上计算回归指标,并进一步结合可视化方式分析预测值与真实值之间的差异,最后保存模型并进行简单的实际预测验证。
# ==============================
# 绘制训练与验证损失曲线
# ==============================
epochs_ran = len(history['train_loss'])
ep = range(1, epochs_ran + 1)
fig, ax = plt.subplots(figsize=(13, 5))
# 训练集与验证集损失
ax.plot(ep, history['train_loss'], label='Train Loss', color='royalblue', lw=2)
ax.plot(ep, history['val_loss'], label='Val Loss', color='tomato', lw=2, linestyle='--')
# 标记最佳epoch
best_ep = int(np.argmin(history['val_loss'])) + 1
ax.axvline(best_ep, color='green', linestyle=':', lw=1.5, label=f'Best epoch ({best_ep})')
# 填充区域增强可读性
ax.fill_between(ep, history['train_loss'], alpha=0.08, color='royalblue')
ax.fill_between(ep, history['val_loss'], alpha=0.08, color='tomato')
ax.set_title('Training & Validation Loss — Gold Price LSTM', fontsize=14, fontweight='bold')
ax.set_xlabel('Epoch'); ax.set_ylabel('Huber Loss')
ax.legend(fontsize=10); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('01_training_curves.png', dpi=150, bbox_inches='tight')
plt.show()
运行
通过损失曲线可以直观观察模型是否收敛以及是否存在过拟合情况,为后续评估提供基础。
# ==============================
# 测试集预测与指标计算
# ==============================
model.eval()
all_preds, all_actuals = [], []
# 收集测试集预测结果
with torch.no_grad():
for Xb, yb in test_loader:
preds = model(Xb.to(device)).cpu().numpy()
all_preds.extend(preds)
all_actuals.extend(yb.numpy())
# 转换为numpy数组
all_preds = np.array(all_preds).reshape(-1, 1)
all_actuals = np.array(all_actuals).reshape(-1, 1)
# 反归一化,还原为真实美元价格
preds_usd = target_scaler.inverse_transform(all_preds)
actuals_usd = target_scaler.inverse_transform(all_actuals)
# 计算评估指标
mae = mean_absolute_error(actuals_usd, preds_usd)
rmse = np.sqrt(mean_squared_error(actuals_usd, preds_usd))
r2 = r2_score(actuals_usd, preds_usd)
mape = np.mean(np.abs((actuals_usd - preds_usd) / actuals_usd)) * 100
# 输出结果
print(f'MAE : ${mae:.2f}')
print(f'RMSE : ${rmse:.2f}')
print(f'MAPE : {mape:.2f}%')
print(f'R² : {r2:.4f}')
运行
# ==============================
# 全时间序列预测可视化
# ==============================
model.eval()
all_seq_preds = []
# 构建完整数据加载器
full_loader = DataLoader(GoldDataset(X_seq, y_seq), batch_size=128, shuffle=False)
# 获取全序列预测结果
with torch.no_grad():
for Xb, _ in full_loader:
p = model(Xb.to(device)).cpu().numpy()
all_seq_preds.extend(p)
# 反归一化
all_seq_preds = target_scaler.inverse_transform(
np.array(all_seq_preds).reshape(-1, 1)
).flatten()
all_seq_actuals = target_scaler.inverse_transform(y_seq).flatten()
# 构建时间轴(与序列对齐)
dates = df['Date'].values[WINDOW:]
train_end = split_80
val_end = split_90
fig, axes = plt.subplots(2, 1, figsize=(15, 10))
fig.suptitle('Gold Price — Predicted vs Actual (Next Day Close)', fontsize=14, fontweight='bold')
# 全时间段对比
ax = axes[0]
ax.plot(dates, all_seq_actuals, label='Actual', color='#2c3e50', lw=1.2, alpha=0.9)
ax.plot(dates, all_seq_preds, label='Predicted', color='#e74c3c', lw=1.0, alpha=0.8, linestyle='--')
# 标记训练/验证/测试区间
ax.axvspan(dates[0], dates[train_end-1], alpha=0.07, color='royalblue', label='Train')
ax.axvspan(dates[train_end], dates[val_end-1], alpha=0.10, color='orange', label='Val')
ax.axvspan(dates[val_end], dates[-1], alpha=0.12, color='green', label='Test')
ax.set_title('Full Period', fontweight='bold')
ax.set_ylabel('Gold Price (USD)')
ax.legend(fontsize=9); ax.grid(True, alpha=0.3)
# 测试集局部放大
ax = axes[1]
test_dates = dates[val_end:]
test_actuals = all_seq_actuals[val_end:]
test_preds = all_seq_preds[val_end:]
ax.plot(test_dates, test_actuals, label='Actual', color='#2c3e50', lw=2)
ax.plot(test_dates, test_preds, label='Predicted', color='#e74c3c', lw=1.8, linestyle='--')
# 误差填充区域
ax.fill_between(test_dates, test_actuals, test_preds, alpha=0.15, color='tomato', label='Error')
ax.set_title(f'Test Set Zoom | MAE=${mae:.2f} RMSE=${rmse:.2f} MAPE={mape:.2f}% R²={r2:.4f}',
fontweight='bold')
ax.set_ylabel('Gold Price (USD)')
ax.set_xlabel('Date')
ax.legend(fontsize=9); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('02_prediction_vs_actual.png', dpi=150, bbox_inches='tight')
plt.show()
该部分可以直观展示模型在整体时间范围以及测试集上的预测效果,帮助判断拟合程度和误差分布。
# ==============================
# 误差分析
# ==============================
# 计算残差(真实值 - 预测值)
residuals = test_actuals - test_preds
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
fig.suptitle('Prediction Error Analysis — Test Set', fontsize=13, fontweight='bold')
# 残差随时间变化
ax = axes[0]
ax.plot(test_dates, residuals, color='steelblue', lw=1)
ax.axhline(0, color='red', linestyle='--', lw=1)
ax.fill_between(test_dates, residuals, 0, alpha=0.2, color='steelblue')
ax.set_title('Residuals Over Time', fontweight='bold')
ax.set_xlabel('Date'); ax.set_ylabel('Error (USD)')
ax.grid(True, alpha=0.3)
# 误差分布直方图
ax = axes[1]
ax.hist(residuals, bins=40, color='steelblue', edgecolor='white', alpha=0.85)
ax.axvline(0, color='red', linestyle='--', lw=1.5, label='Zero error')
ax.axvline(residuals.mean(), color='orange', linestyle='--', lw=1.5,
label=f'Mean={residuals.mean():.1f}')
ax.set_title('Error Distribution', fontweight='bold')
ax.set_xlabel('Error (USD)'); ax.set_ylabel('Frequency')
ax.legend(fontsize=9); ax.grid(True, alpha=0.3)
# 真实值 vs 预测值散点图
ax = axes[2]
ax.scatter(test_actuals, test_preds, alpha=0.4, s=15, color='steelblue')
mn = min(test_actuals.min(), test_preds.min())
mx = max(test_actuals.max(), test_preds.max())
ax.plot([mn, mx], [mn, mx], 'r--', lw=1.5, label='Perfect fit')
ax.set_title('Actual vs Predicted Scatter', fontweight='bold')
ax.set_xlabel('Actual (USD)'); ax.set_ylabel('Predicted (USD)')
ax.legend(fontsize=9); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('03_error_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
运行
误差分析有助于进一步理解模型预测偏差的分布情况,以及是否存在系统性误差。
# ==============================
# 模型保存与简单推理
# ==============================
# 保存模型及相关信息
torch.save({
'model_state_dict' : model.state_dict(),
'feature_scaler' : feature_scaler,
'target_scaler' : target_scaler,
'features' : FEATURES,
'window' : WINDOW,
'input_size' : input_size,
'test_mae_usd' : mae,
'test_rmse_usd' : rmse,
'test_r2' : r2,
}, 'gold_price_lstm.pt')
# 使用最近30天数据预测下一天价格
model.eval()
last_window = torch.tensor(X_scaled[-WINDOW:], dtype=torch.float32).unsqueeze(0).to(device)
with torch.no_grad():
pred_scaled = model(last_window).cpu().numpy()
# 反归一化得到真实价格
pred_price = target_scaler.inverse_transform(pred_scaled)[0][0]
last_close = df['Close'].iloc[-1]
# 输出预测结果
print(f'Last known close : ${last_close:.2f}')
print(f'Predicted next day: ${pred_price:.2f}')
print(f'Expected move : {((pred_price - last_close) / last_close * 100):+.2f}%')
运行
通过保存模型和进行简单推理,可以验证模型在实际应用中的可用性。
5.总结
本实验基于Kaggle提供的长期黄金价格数据,围绕时间序列预测任务构建了一个多层LSTM模型,通过引入技术指标特征并结合滑动窗口序列建模,有效刻画了黄金价格的时序依赖关系。
从训练过程来看,模型收敛较为稳定,在约百轮后即达到最优状态并提前停止,验证集损失保持在较低水平,说明模型具备较好的泛化能力。在测试集上,MAE为64.28美元、RMSE为100.89美元,MAPE控制在1.90%,R²达到0.9859,整体拟合效果较为理想,能够较准确地跟踪黄金价格的波动趋势。同时,从最终预测结果来看,模型对短期价格变化具备一定的敏感性,能够捕捉到潜在的下行趋势。
总体而言,该模型在精度与稳定性之间取得了较好的平衡,但在极端波动场景下仍可能存在偏差,后续可通过引入更多宏观变量或更复杂的时序结构进一步优化预测表现。
490