基于LSTM的外汇预测模型:从USDJPY分钟数据到多步预测实战
发布时间:2026/9/26 11:25:29来源:尧图网络
简介一份面向金融数据分析与深度学习入门者的LSTM外汇预测实践资源聚焦汇率时间序列建模全流程。资源从LSTM网络的门控原理入手涵盖数据清洗、标准化、训练/验证/测试集划分等预处理步骤并融入经济指标、市场情绪等特征工程思路。在模型层面完整展示了多层LSTM搭建、MSE损失函数、Adam优化器、批次与迭代次数调节以及验证集早停防过拟合的实践方法训练完成后提供RMSE、MAE等评估指标分析可支撑交易策略辅助决策。压缩包共27个文件以Jupyter Notebook交互式案例、Python脚本、CSV数据文件、训练好的模型权重及说明文档为主整体大小约13.41MB目录结构清晰直接打开Notebook即可复现。目前已有119人学习适合希望掌握LSTM在金融时序预测中落地方法的开发者参考作为进入该领域的实践起点。1. 基于LSTM网络的外汇预测模型从USDJPY分钟数据到可落地的预测权重外汇价格序列在教科书里经常被描述成接近随机游走LSTM依然是量化入门里被尝试最多的方向。这个基于LSTM网络的外汇预测模型项目把整条链路补得很完整从USDJPY的M1分钟级CSV出发经过滑动窗口构造、标准归一化、数据集划分再到PyTorch LSTM训练、权重落盘、单步和多步预测每一步都有独立notebook能单独跑。我完整拆过一遍后最直接的感受是模型本身不是瓶颈数据划分和scaler的保存加载才是最容易翻车的地方。这份资源适合想用Python做时间序列预测、尤其是想把LSTM在外汇高频数据上跑通的开发者它自带train/validate/test三份CSV和可直接重训的工程骨架不是那种只有模型结构没有数据的半成品。2. LSTM原理与数据预处理门控机制怎么适配1分钟汇率序列LSTM能在外汇这类时间序列上站住脚核心在于它的门控机制。传统RNN在反向传播时梯度经过时间步连乘会指数级衰减或爆炸导致网络学不到十几步之前的信息。LSTM引入细胞状态和三个门控单元让信息可以选择性通过在长序列上保留长期依赖。USDJPY的M1数据正好是这种场景单根1分钟K线的噪声很大但日内趋势、波动周期这类状态会持续较长时间LSTM需要在“跟住最近几根K线”和“保留半小时前的方向判断”之间做权衡。2.1 从梯度消失到门控机制输入门、遗忘门、输出门各自管什么LSTM每一步计算三个门控信号核心公式可以简化成三个门的更新遗忘门决定从细胞状态里丢掉多少历史信息输入门决定当前输入有多少写入细胞状态输出门决定最终暴露给隐藏状态的信息量。具体来说遗忘门 (f_t \sigma(W_f \cdot [h_{t-1}, x_t] b_f))输出接近1时保留历史接近0时清空。输入门 (i_t \sigma(W_i \cdot [h_{t-1}, x_t] b_i))配合候选值 (\tilde{C}_t) 更新细胞状态。输出门 (o_t \sigma(W_o \cdot [h_{t-1}, x_t] b_o))控制当前隐藏状态输出多少给下一层。对外汇价格序列来说这个机制很实用当价格进入横盘震荡遗忘门可以削弱十几分钟前的剧烈波动当趋势形成输入门可以把新的突破动作写入状态。相比ARIMA这类线性模型LSTM能同时捕捉水平的短期动量和方差的缓慢变化这是它在外汇预测里被反复尝试的根本原因。2.2 数据划分为什么USDJPY.M1的train/validate/test不能随机洗牌项目data目录下有四份CSVUSDJPY.M1.csv是完整数据另外三份分别对应训练、验证和测试集。prep_and_split.ipynb这个notebook干的就是切分和序列化的工作。时间序列数据划分和普通分类任务有个本质区别不能用 sklearn 的 train_test_split 默认参数随机打乱否则验证集里会出现比训练集更早的数据等价于用未来信息训练测试指标会虚高。我一般会按时间顺序做比例切分常见做法是前70%训练、中间15%验证、最后15%测试。核心代码是import pandas as pd # 读取完整分钟数据注意确认列名 df pd.read_csv(data/USDJPY.M1.csv) df[time] pd.to_datetime(df[time]) df df.sort_values(time).drop_duplicates(subsettime).reset_index(dropTrue) # 按时间顺序切分禁止shuffle n len(df) train_end int(n * 0.70) val_end int(n * 0.85) train_df df.iloc[:train_end].copy() val_df df.iloc[train_end:val_end].copy() test_df df.iloc[val_end:].copy() # 保存划分结果供后续notebook复用 train_df.to_csv(data/USDJPY.M1-train.csv, indexFalse) val_df.to_csv(data/USDJPY.M1-validate.csv, indexFalse) test_df.to_csv(data/USDJPY.M1-test.csv, indexFalse)这里有个细节drop_duplicates(subsettime) 是为了处理外汇数据里常见的重复时间戳M1数据偶尔会因为交易平台补数据产生同一分钟内多条记录不清理会导致序列错位。切分比例不是固定的如果数据覆盖多年可以把训练比例提到80%。验证集的作用是早期停止测试集只能最后用一次不能反复拿它调参否则就变成了变相过拟合。2.3 滑动窗口与scaler32步输入、14步输出与bin文件的对应关系LSTM不能直接吃一整段价格序列需要切成固定长度的窗口。项目命名里的“32-256-14”基本对应三个关键数字输入序列长度32、LSTM隐藏单元数256、预测步长14。对M1数据来说输入32根1分钟K线输出未来14分钟的价格走势。构建滑动窗口的代码在time_series.py里核心逻辑import numpy as np def create_sequences(data: np.ndarray, seq_len: int 32, pred_steps: int 14): X, y [], [] for i in range(len(data) - seq_len - pred_steps 1): X.append(data[i:i seq_len]) # 过去32个时刻 y.append(data[i seq_len:i seq_len pred_steps]) # 未来14个时刻 return np.array(X), np.array(y) # 使用示例假设scaled是归一化后的(样本数, 1)二维数组 X_train, y_train create_sequences(train_scaled, seq_len32, pred_steps14) print(X_train.shape, y_train.shape) # 大约 (N-45, 32, 1) 和 (N-45, 14)参数seq_len32意味着用32分钟的历史走势pred_steps14意味着一次输出未来14个价格点。这个“多步直接输出”策略比“预测1步再递归滚下去”更稳因为递归方式会把误差逐步放大。窗口长度和预测步长的组合是一种取舍窗口太长会稀释近期价格变动的影响太短则学不到日内趋势。32和14是这套数据上比较常见的起点。数据归一化用的是MinMaxScaler把价格缩放到0到1之间。注意scaler必须只用训练集fit验证集和测试集只transformfrom sklearn.preprocessing import MinMaxScaler import joblib # 只用训练集fit避免泄漏未来取值范围 scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_df[[close]]) val_scaled scaler.transform(val_df[[close]]) test_scaled scaler.transform(test_df[[close]]) # 保存scaler推理时必须加载它做反变换 joblib.dump(scaler, scalers/USDJPY.M1-32-256-14.bin)注意bin文件命名和模型权重命名完全一致都是“货币对-时间刻度-输入步长-隐藏单元-输出步长”这个格式。保存scaler这步特别容易被忽略很多刚上手的人训练完直接把预测结果画出来发现曲线严重偏离真实价格区间就是因为忘了做inverse_transform。3. 搭建LSTM预测模型架构选择、损失函数与训练流程模型定义和训练逻辑集中在train_model.ipynb里。这个项目的设计思路是常见的“LSTM特征提取加全连接回归头”LSTM层负责从序列里提炼时序特征后面的全连接层把特征映射到14个未来价格点上。相比直接堆叠多层LSTM这个结构参数量更可控也更容易收敛。3.1 模型架构为什么取最后一个时间步输出我用PyTorch复现这个模型结构按项目命名里的256隐藏单元配置import torch import torch.nn as nn SEQ_LEN 32 # 输入时间步 HIDDEN_SIZE 256 # LSTM隐藏单元数 NUM_LAYERS 2 # LSTM层数 OUT_STEPS 14 # 预测未来14个点 DROPOUT 0.2 # 层间dropout比例 class LSTMForecast(nn.Module): def __init__(self): super().__init__() self.lstm nn.LSTM( input_size1, hidden_sizeHIDDEN_SIZE, num_layersNUM_LAYERS, batch_firstTrue, dropoutDROPOUT, ) self.head nn.Sequential( nn.Linear(HIDDEN_SIZE, 64), nn.ReLU(), nn.Dropout(0.1), nn.Linear(64, OUT_STEPS), ) def forward(self, x): # x形状: (batch, seq_len, 1) out, (h_n, c_n) self.lstm(x) # out包含每个时间步的输出取最后一个 last_out out[:, -1, :] # 形状: (batch, hidden_size) return self.head(last_out)逻辑说明batch_firstTrue让输入张量的形状是(batch, seq_len, input_size)这和TensorFlow Keras的习惯一致。LSTM返回的out是每个时间步的隐藏状态形状为(batch, seq_len, hidden_size)取[:, -1, :]意味着只看最后一个时间步的隐状态。这里有个理解误区不是只看最后一根K线而是LSTM在遍历完32个时间步后最后一个隐状态已经“压缩”了整个序列的信息。如果你把h_n拿来做输出效果等同但out[:, -1, :]更直观。全连接回归头从256维压缩到64维再映射到14维输出中间插入ReLU和非线性和Dropout。这个设计比直接接一个Linear(256, 14)多了一层非线性变换能让模型拟合更复杂的映射。注意nn.LSTM里的dropout只在num_layers大于1时才生效我设了2层所以dropout作用于第一层到第二层之间。3.2 损失函数与优化器MSE、Adam和batch_size怎么配合回归任务默认用均方误差MSE它对大误差的惩罚是平方级的意味着模型会优先压低那些偏离特别大的预测。外汇预测里偶尔会出现剧烈波动如果损失权重过多放在这些极端点上模型会变得保守倾向预测一个接近均值的中间值。我在评估阶段还会额外看MAE和方向准确率但训练损失用MSE是没问题的。优化器用Adam这是LSTM训练里的标配选择因为它对学习率没有SGD那么敏感import torch.optim as optim model LSTMForecast() criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5)Adam的momentum机制让它在金融时间序列这种带噪声的梯度上更稳定。学习率从1e-3起步如果验证损失下降变慢可以降到5e-4。weight_decay加1e-5是为了限制权重幅度作用比Dropout温和。batch_size的选择要看显存和序列长度对32步输入、256隐藏单元这个体量batch_size64是比较安全的起点显存充裕可以提到128但不要一上来就256——M1数据量大大批次虽然训练速度快但梯度估计更平滑容易陷在局部最优里出不来。3.3 训练循环验证集早停和权重文件保存训练循环的框架是标准的PyTorch流程但有两个关键细节每个epoch结束要在验证集上算loss连续多个epoch验证loss不再下降就早停保存权重时带上完整的超参数标识这就是项目里models/USDJPY.M1-32-256-14这种文件名的由来。import numpy as np from torch.utils.data import DataLoader, TensorDataset # 假设X_train, y_train已由create_sequences生成 train_dataset TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train) ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) # 训练循环 def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss 0 for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止RNN梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(loader) # 验证与模型保存 best_val float(inf) patience 10 counter 0 for epoch in range(100): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss evaluate(model, val_loader, criterion) if val_loss best_val: best_val val_loss counter 0 torch.save(model.state_dict(), models/USDJPY.M1-32-256-14) else: counter 1 if counter patience: print(fearly stop at epoch {epoch}) break几个关键参数说明clip_grad_norm_把梯度模长限制在1.0以内防止梯度爆炸——这是LSTM训练比CNN更需要的一步。patience10表示连续10个epoch验证loss没有创新低就停。保存的文件没有.pth后缀这是项目自己的命名习惯。加载时要注意需要用同一个模型类实例化后再load_state_dict不能直接torch.load完拿来预测。4. 验证、评估与多步预测从test_model.ipynb到multi_pred_model.ipynb模型训练完只是第一步外汇预测里“测试集上数字好看”和“实际交易里能用”是两码事。test_model.ipynb负责加载权重和scaler在测试集上跑评估multi_pred_model.ipynb进一步用训练好的模型生成连续的未来预测曲线。4.1 评估指标RMSE、MAE与方向准确率要一起看标准回归指标有三个MSE、RMSE、MAE。RMSE是MSE开方单位回到价格本身MAE是绝对误差的平均对大误差不那么敏感。对USDJPY这种小数点后三位的报价RMSE可能在0.05到0.15之间单独看会以为模型很准但实际价格点差收益是几十个pips噪声完全可能淹没信号。所以我在评估时一定会加一个方向准确率预测的未来14个价格点中最后一个相对当前是涨还是跌和真实走势是否一致。这个指标对交易决策更直接import numpy as np def direction_accuracy(y_true: np.ndarray, y_pred: np.ndarray) - float: # y_true, y_pred 形状都是 (样本数, 14) # 看每组最后一个预测点相对第一个点的方向 true_dir np.sign(y_true[:, -1] - y_true[:, 0]) pred_dir np.sign(y_pred[:, -1] - y_pred[:, 0]) return float(np.mean(true_dir pred_dir))注意这里的y_true和y_pred都应该是inverse_transform还原后的真实价格在0到1的归一化空间里算方向没有意义。4.2 单步测试加载权重和scaler的完整推理流程test_model.ipynb的测试流程可以拆成三步加载模型权重、加载scaler还原价格、把预测结果和真实值对齐。这里最容易错的是测试集构建时用的窗口偏移量。训练时每个样本是i到i31作为输入、i32到i45作为输出测试集也要完全按照同样的偏移去切不能从0开始重新切否则样本分布和训练时不一致。import torch import joblib import numpy as np # 用相同结构实例化模型 model LSTMForecast() state torch.load(models/USDJPY.M1-32-256-14, map_locationcpu) model.load_state_dict(state) model.eval() # 加载scaler做反变换 scaler joblib.load(scalers/USDJPY.M1-32-256-14.bin) # 构造测试输入必须是shape (batch, seq_len, 1) test_input torch.FloatTensor(X_test[:1]) # 取第一个测试样本 with torch.no_grad(): pred_scaled model(test_input).numpy() # 反变换回真实价格 pred_price scaler.inverse_transform(pred_scaled)反变换这步在机器学习里很容易忽略模型学习的是0到1区间内的规律画图或计算误差前必须还原。在金融数据上scaler通常用MinMaxScaler而不用StandardScaler原因是价格序列的分布不是高斯分布用均值和标准差标准化后尾部极端价格会被压缩得看不出形态。4.3 多步预测multi_pred_model.ipynb两种策略的取舍multi_pred_model.notebook里实现的多步预测核心思路是“滚动窗口”。模型一次输出未来14个点但如果你想要预测未来1小时甚至更长时间就需要把预测结果拼接到输入序列尾部然后再次预测。这个滚动过程叫递归多步预测def recursive_forecast(model, init_seq: np.ndarray, scaler, steps: int 60, pred_steps: int 14): init_seq: 初始输入序列长度必须等于SEQ_LEN steps: 总共要预测的时间步数 model.eval() # 归一化输入 current scaler.transform(init_seq.reshape(-1, 1)).flatten() result [] while len(result) steps: # 构建形状 (1, seq_len, 1) x torch.FloatTensor(current[-SEQ_LEN:]).reshape(1, SEQ_LEN, 1) with torch.no_grad(): y model(x).numpy().flatten() # 反变换回真实价格 y_price scaler.inverse_transform(y.reshape(-1, 1)).flatten() result.extend(y_price) # 把预测结果作为下一轮输入的一部分 current np.concatenate([current, y]) return np.array(result[:steps])这个方案有个明显问题预测误差会一步一步累积递归次数越多后续预测越偏。所以multi_pred_model一般不会真的递归几百步而是每14步作为一个批次生成长度为14的预测段后用最新真实价格重新校准输入窗口。实盘里最常见的用法是每根新K线到来时重新抓取最近32个真实价格做一次预测而不是用预测值去滚动。5. 避坑指南外汇LSTM训练与推理的五个常见问题这部分是我拆项目时踩过和复盘过的坑每一条都是现象对应原因再给解决方案。5.1 训练损失持续下降验证损失却开始反弹现象前几个epoch训练loss和验证loss同步下降到第10个epoch附近验证loss掉头向上但训练loss还在降。很多人的第一反应是学习率太大调低后继续训练发现验证loss还是回不来。原因这是典型的过拟合。LSTM隐藏单元256、两层结构在M1数据上参数量并不小而分钟级价格序列里真正有效的模式有限模型开始记住训练集的噪声。Dropout设成0.2在两层LSTM里算偏低。解决把nn.LSTM里的dropout提到0.3到0.4同时加大全连接头里的Dropout到0.2。早停的patience不要设太大10足够。如果过拟合还是很严重把hidden_size从256降到128训练速度还会变快。5.2 预测曲线整体偏移幅度也对不上现象画测试集预测曲线发现预测值和真实值形态接近但整体高出一截或者波动幅度明显被压缩。有人以为模型学偏了重新训练了好几次都一样。原因99%是scaler的fit范围不对。如果在预处理阶段用整个数据集fit的MinMaxScaler训练集缩放时就已经混进了未来数据的最大值最小值。训练集和测试集的价格区间不同推理时transform出来的数值分布会和训练时不一致。解决严格只用训练集fit验证集和测试集只transform。如果你是加载项目现成的scaler先用scaler.data_min_和scaler.data_max_确认它的fit范围来自哪个数据集。5.3 加载模型报size mismatch现象torch.load后执行model.load_state_dict报错提示某个Linear层的权重维度对不上比如“size mismatch for head.0.weight: copying a param with shape torch.Size([64, 256]) from checkpoint, the shape in current model is torch.Size([64, 128])”。原因模型文件名只写了32-256-14你大概知道256是隐藏单元数但实例化模型时可能把hidden_size写成了128或者把num_layers写成了1。LSTM层参数和全连接层维度全部由hidden_size决定。解决严格按模型文件名来实例化或者直接把模型定义和超参数写进common_variables.py每次改超参数就改一个地方。如果你不确定checkpoint里保存的隐藏单元数可以torch.load后打印state_dict里lstm.weight_ih_l0的shape第二维除以4就是input_size第一维除以4就是hidden_size。5.4 USDJPY的MSE已经很小但方向准确率只有52%现象测试集RMSE在0.08左右看起来精度很高但统计预测方向准确率只有52%跟抛硬币差不多无法形成交易策略。原因这是外汇价格序列的常态。价格波动中大部分是噪声RMSE衡量的是数值贴近程度但LSTM倾向于预测一个接近历史均值的平滑值这个平滑值在数值上离真实值不远在方向上却常常慢半拍。本质上MSE损失不直接优化方向准确率。解决在训练损失里加一个辅助的方向惩罚项比如把方向不匹配的样本损失加权放大。实际做法是用方向准确率作为早停的监控指标而不是只盯val_loss。5.5 M1数据量太大训练速度无法接受现象数据文件有几十万行一次性把全部序列加载进内存构建X_train内存直接满了或者训练一个epoch要十几分钟。原因create_sequences用for循环逐样本append对50万条数据会产生约50万个窗口样本每个样本包含32个时间步内存占用是百万级浮点数。训练速度慢则是因为没把数据喂进DataLoader的多线程机制。解决用numpy的滑动窗口向量化构造替代for循环比如用stride_tricks或先把数据pad成批量矩阵。训练时把batch_size调大并设置num_workers4让CPU提前准备数据。如果机器配置一般先把M1降采样成M5或M15再训练模型参数不用变。6. 进阶从USDJPY迁移到GBPUSD与序列参数微调项目里还带了gbpusd-32-256-14的权重和scaler文件说明这套工程骨架不只是为USDJPY准备的。两个货币对用同样的32步输入、256隐藏单元、14步输出结构迁移成本很低。如果要在新货币对EURUSD上训练最省事的做法是直接复制common_variables.py里的超参数换成EURUSD.M1数据重跑prep_and_split、train_model、test_model三条链路。我一般会这样调整先把seq_len缩短到16跑一次快速实验确认数据加载和训练流程没报错再跑完整的32步训练这样能省掉大半天的排错时间。序列长度的调整有一套经验法则seq_len对应的是你希望模型关注的“记忆长度”对M1数据来说32步是半个多小时如果改成H1小时线seq_len可以缩短到16或24。pred_steps从14改成7或28也可以试但要注意输出步长越长多步预测的不确定性越大。模型的权重文件命名已经把参数写进去了所以每次实验都生成带参数标识的权重文件后期对比实验记录会非常清楚。还有一个实战里比较实用的做法把模型的14个预测点拿出来只取最后3个点的均值作为方向信号不要直接跟单前几个点。因为远期预测的可靠性低于近期最后几个点包含了模型对整个窗口趋势的综合判断。我踩过这个坑一开始只取第一个预测点做信号回测表现波动极大后来改成综合尾部预测值稳定性明显提升。从那以后我每次用LSTM做外汇预测都会强制走一遍“训练集fit scaler、窗口对齐、方向准确率兜底”这三道检查。这份资源把基础链路铺好了如果你正卡在LSTM时间序列预测的某个环节可以直接下载工程包对照自己的数据从prep_and_split开始跑通一遍希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网