资讯动态

LSTM外汇预测实战:从数据到模型,避开时序建模的坑

发布时间:2026/10/9 21:41:49 来源:尧图企业网站定制
简介这是一份面向金融量化初学者与算法实践者的外汇汇率预测项目源码包围绕LSTM长短期记忆网络构建完整的时间序列预测流程帮助读者理解循环神经网络在汇率走势建模中的落地方式。包内共27个文件以ipynb交互式笔记本、py脚本、csv汇率数据、xml配置及bin模型权重为主涵盖数据预处理、特征工程、模型搭建、训练验证与结果评估等环节压缩包约13.41MB目录按数据、模型、脚本分层组织便于按模块查阅。资源中提供了USDJPY与GBPUSD的分钟级训练、验证与测试数据以及已保存的模型文件和标准化参数可直接复现训练与预测过程并对比不同币种的建模效果。目前已有119人学习下载适合希望从零掌握LSTM金融预测框架、积累量化项目经验的开发者参考。1. 外汇预测这件事LSTM 到底能不能打很多人第一次听到「基于 LSTM 网络的外汇预测模型」脑子里蹦出来的画面是把历史汇率丢进网络训练几个 epoch然后坐等它预测明天的价格从此财务自由。我当年也这么想过直到自己动手跑了一遍才发现事情远没有这么简单。外汇市场是全球流动性最强的市场之一每天成交额以万亿计参与者从央行到散户信息几乎瞬时反映在价格里。用 LSTM 做预测本质上是在赌「历史序列里存在可被网络捕捉的短期模式」而不是在赌「网络能算准未来」。那这个方向到底值不值得做我的判断是作为量化研究的入门项目、作为时序建模的练手场景它非常值得但如果你指望它直接产出可实盘的信号那大概率会失望。LSTM 擅长的是带记忆的序列建模它能学到「过去 N 根 K 线的形态对下一根的影响」但它学不到「某国央行突然加息」这种外生冲击。所以这篇笔记的定位很明确带你从零把一套 LSTM 外汇预测的流程跑通理解每一步在干什么、参数怎么调、坑在哪里最后你能自己判断这个模型在你的场景下有没有用。适合有 Python 基础、懂一点深度学习、想进入量化时序领域的人。2. 数据准备与特征工程模型的上限在这里就定了2.1 外汇数据从哪来、怎么选周期外汇数据和股票不一样它没有统一的交易所不同数据源的同名货币对可能有细微差异。常见做法是用公开的历史数据接口拉取日线或小时线数据货币对一般选 EUR/USD、GBP/USD、USD/JPY 这类流动性最好的。周期选择上日线数据噪声相对小、样本量适中适合入门小时线数据量大但噪声也大模型容易过拟合。我一般建议先用日线跑通全流程再考虑降周期。数据字段至少要有时间戳、开盘价、最高价、最低价、收盘价。有些数据源还会给成交量但外汇是场外市场成交量数据参考价值有限不建议作为核心特征。import pandas as pd import numpy as np # 读取历史汇率数据假设是 CSV 格式 df pd.read_csv(eurusd_daily.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 只保留核心字段检查缺失 df df[[date, open, high, low, close]] print(缺失值统计\n, df.isnull().sum()) # 前向填充处理偶发缺失再删除仍为空的行 df df.ffill().dropna().reset_index(dropTrue) print(数据范围, df[date].iloc[0], 到, df[date].iloc[-1]) print(总样本数, len(df))这段代码做了三件事按时间排序、检查缺失、前向填充。逻辑说明外汇数据在周末和节假日没有报价如果数据源没有剔除这些日期会出现时间不连续但价格序列本身不需要补周末直接按交易日排列即可。参数说明ffill()是前向填充适合处理偶发的单点缺失如果缺失连续超过 3 天建议直接删除该段而不是填充否则会引入虚假的平稳段。2.2 特征工程别只盯着收盘价只用收盘价做输入模型能学到的信息非常有限。常见做法是构造一组技术指标作为额外特征比如收益率、移动平均线差值、波动率等。注意特征不是越多越好冗余特征会拖慢训练并增加过拟合风险。# 构造特征 df[return] df[close].pct_change() # 日收益率 df[ma5] df[close].rolling(5).mean() # 5 日均线 df[ma20] df[close].rolling(20).mean() # 20 日均线 df[ma_diff] df[ma5] - df[ma20] # 均线差 df[volatility] df[return].rolling(10).std() # 10 日波动率 # 去掉滚动窗口产生的空值 df df.dropna().reset_index(dropTrue) # 查看特征相关性剔除高度冗余的 feature_cols [return, ma_diff, volatility] print(df[feature_cols].corr())逻辑说明收益率是平稳序列比原始价格更适合作为模型输入均线差反映趋势方向波动率反映市场状态。参数说明均线窗口 5 和 20 是常见组合分别代表短期和中期趋势波动率窗口 10 是经验值窗口太短噪声大太长反应迟钝。相关性高于 0.9 的特征建议只保留一个。2.3 归一化与滑动窗口构造LSTM 对输入尺度敏感必须做归一化。注意归一化参数只能从训练集计算然后应用到验证集和测试集否则会引入未来信息这是时序任务里最隐蔽的坑之一。from sklearn.preprocessing import MinMaxScaler # 按时间切分前 70% 训练中间 15% 验证最后 15% 测试 n len(df) train_end int(n * 0.7) val_end int(n * 0.85) train_df df.iloc[:train_end] val_df df.iloc[train_end:val_end] test_df df.iloc[val_end:] # 只在训练集上拟合 scaler scaler MinMaxScaler() scaler.fit(train_df[feature_cols]) def transform(data): return scaler.transform(data[feature_cols]) train_scaled transform(train_df) val_scaled transform(val_df) test_scaled transform(test_df) # 构造滑动窗口用过去 seq_len 天预测下一天 def make_sequences(data, target, seq_len20): X, y [], [] for i in range(len(data) - seq_len): X.append(data[i:iseq_len]) y.append(target[iseq_len]) return np.array(X), np.array(y) # 目标用收盘价收益率 train_target train_df[return].values val_target val_df[return].values test_target test_df[return].values X_train, y_train make_sequences(train_scaled, train_target) X_val, y_val make_sequences(val_scaled, val_target) X_test, y_test make_sequences(test_scaled, test_target) print(训练集形状, X_train.shape, y_train.shape)逻辑说明滑动窗口把时序转成监督学习格式seq_len是回看天数。参数说明seq_len20大约对应一个月交易日是外汇日线的常用值太短捕捉不到趋势太长引入过多噪声且训练变慢。归一化用 MinMaxScaler 把特征压到 0 到 1注意 scaler 只在训练集拟合这是避免数据泄露的关键。3. 搭一个能跑的 LSTM 模型结构、训练与评估3.1 模型结构怎么定入门阶段不需要堆很深的网络。一层 LSTM 加一层全连接输出就够了隐藏单元数从 32 或 64 起步。层数越多、单元越多过拟合风险越大而外汇数据的信噪比很低复杂模型往往死得更快。import torch import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_size, hidden_size64, num_layers1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的输出 last out[:, -1, :] return self.fc(last) model LSTMForecaster(input_sizelen(feature_cols)) print(model)逻辑说明LSTM 输出整个序列的隐状态预测只用最后一个时间步因为它包含了整个窗口的信息。参数说明hidden_size64是入门常用值num_layers1先跑通再考虑加深dropout0.2用于正则化但单层 LSTM 时 PyTorch 的 dropout 不生效所以代码里做了条件判断。3.2 训练循环与早停外汇预测的损失函数一般用 MSE 或 MAE。训练时一定要加早停否则模型会在训练集上越跑越好验证集上越来越差。from torch.utils.data import DataLoader, TensorDataset # 转成 Tensor X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32).unsqueeze(1) X_val_t torch.tensor(X_val, dtypetorch.float32) y_val_t torch.tensor(y_val, dtypetorch.float32).unsqueeze(1) train_loader DataLoader(TensorDataset(X_train_t, y_train_t), batch_size32, shuffleTrue) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() best_val_loss float(inf) patience, wait 10, 0 for epoch in range(200): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): val_pred model(X_val_t) val_loss criterion(val_pred, y_val_t).item() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_lstm.pt) wait 0 else: wait 1 if wait patience: print(f早停于 epoch {epoch}) break if epoch % 10 0: print(fepoch {epoch}, val_loss {val_loss:.6f})逻辑说明每个 epoch 训练完在验证集上评估只保存验证损失最低的模型。参数说明batch_size32适合几千条样本lr1e-3是 Adam 的常用学习率patience10表示验证损失连续 10 个 epoch 不下降就停。注意shuffleTrue在训练集上打乱批次但验证集不能打乱。3.3 评估指标别只看 MSEMSE 小不代表模型有用。外汇预测里更关心方向准确率也就是预测涨跌方向对不对。如果方向准确率接近 50%那模型基本没有实用价值。model.load_state_dict(torch.load(best_lstm.pt)) model.eval() with torch.no_grad(): test_pred model(torch.tensor(X_test, dtypetorch.float32)).numpy().flatten() # 方向准确率 direction_acc np.mean(np.sign(test_pred) np.sign(y_test)) print(f方向准确率{direction_acc:.4f}) # MSE mse np.mean((test_pred - y_test) ** 2) print(f测试集 MSE{mse:.8f})逻辑说明np.sign把预测值和真实值转成涨跌方向比较是否一致。参数说明方向准确率高于 0.55 才值得进一步研究低于 0.52 基本可以认为没有预测能力。MSE 只作为辅助参考因为收益率本身数值很小MSE 天然就小。4. 避坑与排查那些让我重跑过很多次的坑4.1 数据泄露归一化和特征构造最容易翻车现象验证集和测试集表现异常好实盘或换一段数据就崩。原因归一化时用了全量数据拟合 scaler或者构造滚动特征时用了未来数据。解决所有统计量只从训练集计算滚动特征用shift确保只看到过去。4.2 过拟合训练损失降验证损失升现象训练集 MSE 一路下降验证集 MSE 先降后升。原因模型容量过大、训练轮数过多、样本量不足。解决减小 hidden_size、加 dropout、加早停、增加训练数据量。外汇日线数据通常只有几千条模型参数量控制在几万以内比较稳妥。4.3 预测值几乎不变模型学会了偷懒现象模型输出的预测值方差极小几乎等于收益率的均值。原因收益率信噪比太低模型发现预测均值就能让 MSE 很小于是不再学习波动。解决改用方向作为目标做分类或者在损失函数里加权让模型更关注大幅波动样本。4.4 时间切分错误随机切分时序数据现象用train_test_split随机切分后测试集表现虚高。原因随机切分让未来数据进入了训练集。解决时序数据必须按时间顺序切分训练集在前验证和测试在后绝不能打乱。4.5 货币对选择不当冷门货币对数据质量差现象某些货币对点差大、数据跳空多模型难以收敛。原因流动性差的货币对报价不连续噪声大。解决优先选 EUR/USD、USD/JPY 这类主流货币对数据连续性和质量都有保障。5. 进阶技巧让 LSTM 外汇模型更接近可用跑通基础版之后如果想让模型更有实用价值有几个方向可以试。第一把回归改成分类直接预测下一根 K 线涨跌用交叉熵损失这样模型不会因为预测均值而偷懒。第二引入多尺度特征比如同时输入日线和周线的均线状态让模型看到不同时间尺度的趋势。第三用 walk-forward 验证替代单次切分每次用过去 N 年训练、下一年测试滚动前进这样评估结果更接近真实使用场景。# walk-forward 验证框架示意 def walk_forward(df, train_years3, test_months6): results [] start df[date].min() end df[date].max() train_start start while True: train_end train_start pd.DateOffset(yearstrain_years) test_end train_end pd.DateOffset(monthstest_months) if test_end end: break train df[(df[date] train_start) (df[date] train_end)] test df[(df[date] train_end) (df[date] test_end)] # 在这里训练模型并在 test 上评估 results.append((train_end, len(train), len(test))) train_start train_start pd.DateOffset(monthstest_months) return results splits walk_forward(df) for s in splits: print(f训练截止 {s[0].date()}, 训练样本 {s[1]}, 测试样本 {s[2]})逻辑说明walk-forward 每次只用历史数据训练预测未来一段然后窗口前移。参数说明train_years3保证训练样本量test_months6让测试段足够长以观察稳定性。这个框架比单次切分更耗时但评估结果可信得多。还有一个我踩过的坑不要用未来数据做特征筛选。比如你先看了全量数据发现某个特征和收益率相关性高再把它加进模型这本身就是数据泄露。特征选择必须只在训练集上做。另外外汇市场存在 regime change某段时期有效的模式换一段时期可能完全失效所以任何模型都要定期重训不能一劳永逸。我自己的习惯是每次跑完模型先看方向准确率再看预测值的分布最后画一段预测和真实的对比图。如果预测曲线几乎是一条直线那这个模型就不用往下看了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑