ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于LSTM的外汇预测模型:Python源码、数据与训练全流程

基于LSTM的外汇预测模型:Python源码、数据与训练全流程 简介这是一份基于LSTM网络的外汇预测模型学习资源面向计算机相关专业学生、教师及企业员工可用于深度学习与时间序列分析的入门实践也可作为课程设计、毕业设计或作业的参考案例。资源包共31个文件约13.41MB包含Python脚本、Jupyter Notebook、CSV汇率数据、模型权重与配置文件等覆盖数据准备、模型训练、测试与预测的完整流程。其中Notebook便于分步阅读与调试脚本文件封装了通用变量与时间序列处理逻辑CSV数据可直接用于复现实验。已有48人学习下载。通过阅读README可快速了解依赖安装与使用方法借助训练与测试代码读者能掌握LSTM门控机制、数据标准化与划分、模型保存与加载等关键环节并理解汇率预测中非线性与不确定性的处理思路适合作为深度学习实战的练手项目。1. 从一份汇率 CSV 到能跑起来的 LSTM 外汇预测模型手里只有一份 EUR/USD 的日线 CSV想用 LSTM 做一个能滚动预测下一根 K 线的模型这是很多做量化、做金融数据分析的工程师真正会遇到的起点。标题里说的「基于 LSTM 网络的外汇预测模型 Python 源码 数据 模型」拆开就是三件事一份时间序列数据、一套 PyTorch 或 Keras 写的 LSTM 源码、一个能保存下来复用的权重文件。它解决的不是「预测明天涨跌发财」这种问题而是把外汇价格序列喂进 LSTM、跑通训练、拿到可评估的预测输出这条完整链路。适合已经会一点 Python、装过 PyTorch 或 TensorFlow但没系统做过时间序列预测的人也适合做过股票预测、想换到外汇数据上验证的熟手。下面按数据、模型、训练、评估、避坑的顺序把这条链路讲透。2. 外汇时间序列的数据准备从原始 CSV 到 LSTM 能吃的张量2.1 外汇数据和普通 CSV 的差别在哪外汇数据看起来就是时间加价格但真正喂模型前有几个绕不开的特点。第一是时间不连续周末休市周五收盘到周一开盘之间没有数据如果直接按行号做滑动窗口窗口里会混进跨周末的跳跃模型学到的「相邻两根」其实隔了两天。第二是量纲问题EUR/USD 在 1.08 附近波动USD/JPY 在 150 附近波动如果混着训练不归一化的话梯度会被大数值主导。第三是缺失和重复不同数据源对同一分钟的报价可能重复或者节假日出现空行。常见做法是只保留收盘价close做单变量预测或者加上最高、最低、开盘做多变量。我一般先用单变量把链路跑通再考虑加特征。数据来源上很多公开数据集和券商导出的 CSV 都能用格式无非是datetime, open, high, low, close, volume这几列重点是确认时间列能被正确解析成时间戳。2.2 用 pandas 清洗并构造滑动窗口下面这段代码做三件事读 CSV、按时间排序去重、用滑动窗口把序列切成(样本数, 时间步, 特征数)的三维张量。这是 LSTM 输入的标准形状。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler # 读取外汇数据parse_dates 把时间列转成 Timestamp df pd.read_csv(eurusd_daily.csv, parse_dates[datetime]) df df.sort_values(datetime).drop_duplicates(datetime) df df.dropna(subset[close]) # 收盘价缺失的行直接丢 # 只取收盘价reshape 成 (n, 1) 供 scaler 使用 close df[close].values.reshape(-1, 1) # 归一化到 [0,1]注意 scaler 要保存预测时要反变换 scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(close) def make_windows(series, lookback60): 把一维序列切成 (样本, lookback, 1) 的监督学习样本 X, y [], [] for i in range(lookback, len(series)): X.append(series[i - lookback:i, 0]) # 过去 lookback 天 y.append(series[i, 0]) # 预测下一天 X np.array(X).reshape(-1, lookback, 1) y np.array(y) return X, y LOOKBACK 60 X, y make_windows(scaled, LOOKBACK) # 按时间顺序切分绝不能随机打乱否则未来信息泄漏 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] print(X_train.shape, X_test.shape) # 例如 (1200, 60, 1) (300, 60, 1)逻辑说明make_windows里lookback60表示用过去 60 个交易日预测第 61 天这个值不是随便定的外汇日线常用 20 到 120 之间太短学不到趋势太长训练慢且容易过拟合。MinMaxScaler必须fit在训练集上再transform测试集我这里为了简洁先对全序列 fit严谨做法是只用训练段 fit否则测试集的极值会泄漏进归一化参数。切分用X[:split]而不是train_test_split(shuffleTrue)时间序列一旦打乱模型就是在用未来预测过去评估结果会虚高得离谱。2.3 归一化和反归一化的坑归一化本身简单坑在反变换。模型输出的是 [0,1] 区间的值你要拿它和真实价格比必须用同一个 scaler 做inverse_transform。很多人训练时归一化、评估时忘了反变换算出来的 MAE 是 0.01 这种「看起来很好」的数字实际对应价格误差是几百点。记住一条scaler 是模型的一部分训练完要和权重一起存下来。3. LSTM 模型结构PyTorch 源码逐层拆解与参数设置3.1 为什么外汇预测常用 LSTM 而不是普通 RNN普通 RNN 在反向传播时梯度会指数衰减序列一长比如 60 步前面的信息基本传不到后面这就是梯度消失。LSTM 靠输入门、遗忘门、输出门三个门控结构把「记住什么、忘掉什么」变成可学习的参数长序列上的表现稳定得多。外汇价格这种带趋势和噪声的序列LSTM 能捕捉到一定程度的滞后相关性这也是它在这个场景里被反复使用的原因。GRU 是 LSTM 的简化版参数少、训练快效果常常接近如果算力紧张可以先试 GRU。3.2 一个最小可用的 LSTM 网络定义下面用 PyTorch 定义一个单层 LSTM 加全连接输出的模型输入维度 1隐藏层 64输出 1 个预测值。import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers1, dropout0.0): super().__init__() self.hidden_size hidden_size self.num_layers num_layers # batch_firstTrue 让输入形状为 (batch, seq, feature) self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, # num_layers1 时 dropout 无效 ) self.fc nn.Linear(hidden_size, 1) # 把隐藏状态映射成单值预测 def forward(self, x): # h0, c0 默认全零也可显式初始化 out, (h_n, c_n) self.lstm(x) last out[:, -1, :] # 取最后一个时间步的隐藏输出 return self.fc(last).squeeze(-1) model LSTMForecaster(input_size1, hidden_size64, num_layers1) print(sum(p.numel() for p in model.parameters())) # 参数量约 1.7 万逻辑说明batch_firstTrue是关键PyTorch 的 LSTM 默认输入是(seq, batch, feature)设成 True 后才是我们习惯的(batch, seq, feature)忘了设会导致维度对不上或者结果错乱。out[:, -1, :]取的是序列最后一个时间步的输出因为我们要预测的是「看完过去 60 天后」的下一天。self.fc把 64 维隐藏状态压成 1 维预测值。参数说明hidden_size控制记忆容量外汇日线数据量通常几千条64 到 128 够用再大容易过拟合num_layers超过 2 层收益递减且训练变慢一般 1 到 2 层dropout只在多层时生效单层想正则化得在 LSTM 外面单独加nn.Dropout。3.3 训练循环和损失函数怎么选import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader # 转成 tensor X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32) loader DataLoader(TensorDataset(X_train_t, y_train_t), batch_size32, shuffleTrue) criterion nn.MSELoss() # 回归任务用 MSE optimizer optim.Adam(model.parameters(), lr1e-3) for epoch in range(50): model.train() total_loss 0 for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: print(fepoch {epoch1}, loss {total_loss/len(loader):.6f})逻辑说明shuffleTrue在训练集内部打乱 batch 是可以的因为每个样本内部的时间顺序没被破坏打乱的是样本之间的顺序不影响时序建模。clip_grad_norm_是 LSTM 训练的标配梯度爆炸在长序列上很常见不裁剪的话 loss 会突然变成 nan。学习率1e-3是 Adam 的常用起点loss 不降就降到1e-4。参数说明batch_size32 或 64 都行数据量小就用小 batchepoch看 loss 曲线通常 30 到 100 之间配合早停更好。MSE 对异常值敏感如果数据里有极端跳空可以换nn.L1Loss()或 HuberLoss。4. 训练完怎么评估反归一化、指标和滚动预测4.1 反归一化后算真实误差model.eval() with torch.no_grad(): X_test_t torch.tensor(X_test, dtypetorch.float32) pred_scaled model(X_test_t).numpy().reshape(-1, 1) # 关键一步反归一化回真实价格 pred_price scaler.inverse_transform(pred_scaled).flatten() true_price scaler.inverse_transform(y_test.reshape(-1, 1)).flatten() from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(true_price, pred_price) rmse np.sqrt(mean_squared_error(true_price, pred_price)) print(fMAE{mae:.5f}, RMSE{rmse:.5f})逻辑说明model.eval()关掉 dropout 等训练态行为torch.no_grad()省显存。反归一化必须用训练时那个 scaler这里为了演示用了全序列 fit 的 scaler严谨项目里要单独保存训练段 scaler。MAE 和 RMSE 是回归常用指标外汇日线上 MAE 能到 0.002 到 0.005 已经算不错具体看货币对波动率。4.2 别被「预测得很像」骗了把预测曲线和真实曲线画在一起经常看起来贴合得很好但这是滞后效应造成的假象模型输出的往往是前一天的平滑值曲线整体右移一点就能「像」。真正要看的是方向准确率也就是预测的涨跌方向和真实涨跌方向一致的比例。# 用相邻真实值算真实方向用相邻预测值算预测方向 true_dir np.sign(np.diff(true_price)) pred_dir np.sign(np.diff(pred_price)) hit (true_dir pred_dir).mean() print(f方向准确率: {hit:.3f})如果方向准确率在 0.5 附近说明模型没学到方向信息只是拟合了水平。这时候加特征成交量、其他货币对、换 lookback、调网络结构都比盲目加 epoch 有用。4.3 保存模型和 scalertorch.save({ model_state: model.state_dict(), lookback: LOOKBACK, scaler_min: scaler.data_min_, scaler_max: scaler.data_max_, }, lstm_fx.pth)把 scaler 的 min/max 一起存推理时重建 scaler避免「训练归一化、推理没归一化」这种低级但高频的翻车。5. 避坑与排查外汇 LSTM 训练里最容易翻车的 5 个点5.1 现象loss 一直不降停在某个值不动原因通常是学习率太大导致震荡或者输入没归一化数值范围差几个数量级。解决先把学习率降到1e-4确认输入已经过 scaler再检查batch_first是否设对维度错了模型其实在学噪声。5.2 现象训练 loss 很低测试 loss 高得离谱典型过拟合。外汇数据噪声大、有效信号少模型很容易记住训练段的波动。解决减小hidden_size、加 dropout、减少num_layers或者增加训练数据量。别指望靠调大模型解决方向反了。5.3 现象预测曲线整体平移方向准确率约 0.5这是滞后预测模型学的是「下一天约等于今天」。原因可能是 lookback 太长、特征太单一。解决缩短 lookback 到 20 试试加入收益率diff而不是原始价格作为目标让模型学变化量而不是水平值。5.4 现象评估指标好得不像话MAE 接近 0先怀疑数据泄漏。检查是不是用了train_test_split(shuffleTrue)是不是在全序列上 fit 了 scaler是不是把测试段混进了训练。时间序列里这三条任意一条都会让指标虚高。5.5 现象换一个货币对模型完全失效不同货币对的波动率、趋势性差别很大在 EUR/USD 上调好的超参不一定适用 USD/JPY。解决每个货币对单独归一化、单独调 lookback或者做多货币对联合训练时对每个序列分别标准化。6. 让预测更靠谱的两个进阶技巧差分建模与多步滚动单变量直接预测价格模型很容易退化成「复制上一天」。我后来习惯改成预测收益率也就是(price_t - price_{t-1}) / price_{t-1}这样目标序列围绕 0 波动模型必须真的学方向才能降低 loss。做法很简单在构造窗口前先算 diffret df[close].pct_change().dropna().values.reshape(-1, 1) scaled_ret MinMaxScaler(feature_range(-1, 1)).fit_transform(ret) X, y make_windows(scaled_ret, LOOKBACK)注意这里 scaler 范围改成(-1, 1)因为收益率有正有负。预测出收益率后用price_t price_{t-1} * (1 pred_ret)还原价格。这个改动通常能把方向准确率从 0.5 拉到 0.52 到 0.55别小看这几个点外汇上已经有意义。第二个技巧是多步滚动预测。上面都是单步预测实际用的时候你想要未来 5 天。做法是把模型预测出的值填回输入序列末尾再预测下一步循环 5 次。代价是误差会累积5 步之后基本只能看趋势不能看点位。我的习惯是单步预测用来验证模型有没有学到东西多步预测只用来画趋势参考绝不拿第 5 步的点位去做决策。还有一个验证习惯值得养成把测试集按时间切成 3 段分别算方向准确率如果某一段特别差说明模型对某种市场状态比如横盘或急涨不适应这比看一个总体指标有用得多。做外汇预测这几年我最大的教训就是别被一条贴合的可视化曲线骗了方向准确率和分段稳定性才是能拿来做判断的东西。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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