LSTM外汇预测实战:从特征工程到交易信号
2026/9/24 18:09:16 网站建设 项目流程

简介:一份基于LSTM网络的外汇预测模型Python实现,面向计算机相关专业学生、教师及金融数据分析人员,适用于课程设计、毕业设计或深度学习与时间序列分析入门。资源包含完整源码、历史汇率数据(CSV)与训练好的模型文件(pb/bin),并配有README说明、Python通用脚本及Jupyter Notebook示例,可帮助读者从数据清洗、标准化、序列切分到模型训练与预测形成完整链路。压缩包内共31个文件,以py/ipynb脚本、csv数据、TensorFlow模型权重及辅助配置文档为主,整体约13.41MB,体积适中、目录结构清晰,便于本地复现。目前已有46人学习下载。需说明的是,外汇市场复杂且充满不确定性,LSTM预测结果更适合作为教学参考与实验基线,使用者可针对具体货币对调整参数并优化策略。

1. 外汇预测用 LSTM:先弄清楚它到底在解什么题

如果你用滚动预测的方式去测外汇数据,很快会发现一个反直觉的现象:模型在训练集上画出的曲线和真实价格几乎重合,但一放到样本外,预测值就像被“拽住尾巴”一样,总比真实行情慢半拍。这不是你的代码写错了,而是把 LSTM 当成了万能回归器,却忽略了金融时序数据的两个底层特性:非平稳和噪声主导。基于 LSTM 的外汇预测模型,本质上不是在“预测价格涨到多少”,而是在“给定过去一段窗口的市场状态,估计下一时刻的条件分布中心”。价值在于把价格的离散波动转成一个可回测、可量化的方向信号,为入场时点和止损位提供依据。

这个方向适合两类人:一类是做量化交易的开发者,想把深度序列模型接入自己的策略框架;另一类是学习时序建模的工程师,想找一个 LSTM 的真实数据集练手,而不是课堂上那种 sin 波形。你不需要有金融背景,但得懂 Python、会处理 DataFrame,并且有一颗接受模型经常翻车的心——外汇数据的信噪比极低,任何模型都做不到稳定盈利,LSTM 也不例外。它跟股票的区别在于:外汇是 24 小时连续交易、杠杆高、受宏观消息驱动明显,同样的模型在股票日线上可能有效,切到外汇小时线上就全面失效。搞清这一点,后面调参才有方向感。

2. 数据是第一道分水岭:拿到外汇行情后,先别急着喂给 LSTM

2.1 用 yfinance 拉取主流货币对:参数与字段选型

外汇历史数据最常见的获取途径是 Yahoo Finance 的接口,Python 的yfinance库封装得比较完善,不需要自己去拼 HTTP 请求。安装命令很简单:pip install yfinance pandas numpy。数据粒度上,日线数据免费且稳定,小时线数据存在缺失风险,分钟级数据我一般不建议碰——外汇市场的流动性在不同时段有显著差异,分钟级数据的噪声占比过高,对 LSTM 训练几乎没帮助。

import yfinance as yf import pandas as pd # 拉取 EURUSD 最近 5 年的日线数据 df = yf.download("EURUSD=X", period="5y", interval="1d", auto_adjust=True, progress=False) # 只保留收盘价,后续特征都从 close 衍生 df = df[["Close"]].copy() df.columns = ["close"] df.dropna(inplace=True) print(df.head()) print(df.shape)

字段选择上有个常见误区:很多人把 Open、High、Low、Close 全塞进模型,期望 LSTM 自己学出关系。但外汇数据的 OHLC 之间相关性极高,加上高频噪声,四列输入和只用 Close 列输入的效果几乎没有差别,反而增加了过拟合风险。我一般只从收盘价衍生特征,把原始 OHLC 保留给后续做回测时计算真实收益。

2.2 特征工程的三个核心动作:收益率、技术指标、窗口序列化

外汇价格本身是非平稳的,直接用原始价格做 LSTM 输入,模型会误以为“昨天的价格”是最强预测因子,导致预测结果接近“把昨天的值挪到今天”。正确做法是把价格转换成收益率序列,这相当于做了一个差分,让序列变成弱平稳。同时叠加几个经典技术指标作为辅助特征,帮助模型捕捉动量和波动状态。

import numpy as np # 1. 对数收益率:比简单百分比更平滑 df["return"] = np.log(df["close"] / df["close"].shift(1)) # 2. RSI 14 日:刻画超买超卖状态 delta = df["close"].diff() gain = delta.clip(lower=0).rolling(14).mean() loss = -delta.clip(upper=0).rolling(14).mean() rs = gain / loss df["rsi"] = 100 - (100 / (1 + rs)) # 3. 波动率:过去 20 日收益率的滚动标准差 df["volatility"] = df["return"].rolling(20).std() df.dropna(inplace=True) print(df.tail())

这里return是模型的核心学习目标,rsivolatility是辅助特征。为什么加这两个?因为外汇市场的特征是“震荡行情中均值回归,趋势行情中动量延续”,RSI 和波动率组合起来,能粗略描述当前处于什么市场状态,给 LSTM 一个“上下文”信号。注意:滚动窗口计算会导致前 N 行数据为空,我习惯直接丢,不要用向前填充,那会引入未来信息。

2.3 滑动窗口切分:lookback 长度与数据划分的陷阱

单条样本长这样:用过去window_size天的特征序列,预测下一天(或未来 N 天)的收益率。这个window_size就是 LSTM 的时间步长,它决定模型“记得”多长的历史。外汇市场里,我一般用 20 到 60 这个范围。短了捕捉不到中期波动规律,长了模型被远古噪声干扰。

def create_sequences(data, lookback=30, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data.iloc[i:i+lookback].values) y.append(data.iloc[i+lookback+horizon-1]["return"]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) feature_cols = ["return", "rsi", "volatility"] X, y = create_sequences(df[feature_cols], lookback=30, horizon=1) print(f"样本数: {X.shape[0]}, 特征维度: {X.shape[2]}, 序列长度: {X.shape[1]}")

切分数据集时必须按时间顺序切,绝不能随机打乱。金融数据一旦乱序,模型就“偷看”了未来,测试集的评估结果会虚高。我一般的切分比例是 7:1.5:1.5,训练集在前 70%,验证集接着 15%,测试集是最后 15%。预测目标也可以改成未来 3 天或 5 天的累计收益率,这取决于你想做短线还是波段,horizon参数就是干这个的。

3. LSTM 模型搭建:输入张量形状推导与 PyTorch 实现

3.1 为什么选 PyTorch 而不是 Keras 或自定义实现

外汇预测项目里,Keras 的上手速度确实快,几行代码就能跑通。但到了调参和调试阶段,PyTorch 的优势会突显出来:可以随时打印每一层的梯度、自由修改 forward 逻辑、在验证集上做条件控制。Keras 的Return_sequencesreturn_state参数经常让人糊涂,而 PyTorch 里一切层对象都是显式的。另一点是,LSTM 的最新衍生改进——比如引入注意力机制、双向 LSTM 做序列编码——在 PyTorch 里实现成本低得多,后续想升级模型不用重写整套流程。

3.2 核心网络结构:从 data 到 output 的形状流转

这是整个项目最需要耐心的地方。LSTM 的输入形状永远是(batch_size, sequence_length, input_size)input_size是特征数,即["return", "rsi", "volatility"]对应的 3。经过 LSTM 层后,输出形状是(batch_size, sequence_length, hidden_size),注意这个过程会把时间维保留。我们用最后一步的输出来接全连接层做回归预测,所以要output[:, -1, :]取最后时刻。

import torch import torch.nn as nn class LSTMRegressor(nn.Module): def __init__(self, input_size=3, hidden_size=64, 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.regressor = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) ) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, _ = self.lstm(x) # 取最后一个时间步的输出 last_hidden = lstm_out[:, -1, :] return self.regressor(last_hidden).squeeze(-1)

hidden_size=64是一个相对稳定的起点,太小(小于 32)模型容量不足,太大(大于 128)在样本量只有 1000 左右时非常容易过拟合。num_layers=2表示堆叠两层 LSTM,第二层把第一层的输出序列作为输入。dropout=0.2只在层间生效,PyTorch 规定单层 LSTM 时 dropout 参数会被忽略,这是源码层面的限制,不用在单层上浪费调参时间。

3.3 损失函数与优化器选择:回归指标和交易目标并不总是一回事

预测收益率是一个回归问题,最常见的损失函数是 MSE,但在外汇场景里,我强烈建议留意 MAE 的表现。原因是:MSE 对大的预测偏差施加了平方惩罚,会让模型把过多精力花在拟合那些极端波动点上,而那些点恰恰是金融危机或者数据跳空造成的,本来就无法用历史窗口预测。MAE 对异常值更鲁棒,训练的模型方向准确率往往更高。

import torch.optim as optim model = LSTMRegressor(input_size=3, hidden_size=64, num_layers=2) optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5) criterion = nn.MSELoss() # 或者换成 nn.L1Loss(),对应 MAE

weight_decay=1e-5是 L2 正则化,相当于给权重加了一个“惩戒项”,防止极端参数出现。lr=0.001是 Adam 优化器常见的默认学习率,适配大多数 LSTM 结构。优化器的选择上,SGD 在外汇这类低频数据上收敛太慢,RMSprop 比 Adam 更容易在时间序列任务上陷入局部震荡,Adam 是最省心的默认选项。

4. 训练与验证:早停法、学习率调度与回测评估

4.1 训练循环的完整写法:早停法是防过拟合的第一道保险

LSTM 训练最典型的失败模式是:训练集损失一路下降,验证集损失在第 20 个 epoch 触底后开始反弹。这就是过拟合——模型把训练数据里的噪声背下来了,放到新数据上就失灵。早停法(Early Stopping)是实践中最可靠的缓解手段:监控验证集损失,连续 N 个 epoch 没有改善就终止训练,同时保存历史上验证集损失最低的权重。

from torch.utils.data import TensorDataset, DataLoader train_dataset = TensorDataset( torch.tensor(X_train), torch.tensor(y_train) ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_dataset = TensorDataset( torch.tensor(X_val), torch.tensor(y_val) ) val_loader = DataLoader(val_dataset, batch_size=256, shuffle=False) epochs = 100 patience = 15 best_val_loss = float("inf") epochs_no_improve = 0 best_state = None for epoch in range(epochs): model.train() train_loss = 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() predictions = model(batch_X) loss = criterion(predictions, batch_y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * batch_X.size(0) model.eval() val_loss = 0.0 with torch.no_grad(): for batch_X, batch_y in val_loader: predictions = model(batch_X) loss = criterion(predictions, batch_y) val_loss += loss.item() * batch_X.size(0) avg_val_loss = val_loss / len(val_dataset) if avg_val_loss < best_val_loss: best_val_loss = avg_val_loss epochs_no_improve = 0 best_state = {k: v.clone() for k, v in model.state_dict().items()} else: epochs_no_improve += 1 if epochs_no_improve >= patience: break model.load_state_dict(best_state)

这里的nn.utils.clip_grad_norm_是梯度裁剪,max_norm=1.0意味着把所有参数的梯度 L2 范数压缩到不超过 1.0。LSTM 在长序列上容易出现梯度爆炸,这是来源层面的数值不稳定问题,梯度裁剪几乎是强制性的。patience=15在日线数据上够用,如果换到小时线数据,可以调整到 25 到 30,因为小时线噪声更大,验证损失曲线更毛糙。

4.2 学习率调度:固定学习率训练到死路之后的一种后悔药

固定学习率在训练初期跑得顺畅,但到了中后期,loss 会在一个区间来回震荡,减不下去。一个廉价的补丁是学习率衰减:当验证损失连续多个 epoch 不下降时,把学习率降为原来的 1/5 或 1/10,让参数更新跳得更精细。

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5, min_lr=1e-6, verbose=True ) # 在验证阶段计算出 avg_val_loss 后调用 scheduler.step(avg_val_loss)

factor=0.5控制衰减幅度,每次学习率减半,这是比较温和且稳妥的设置。min_lr=1e-6防止学习率衰减到零导致模型彻底不更新。调用时机必须是在验证集评估完之后,而不是每个 batch 之后,因为ReduceLROnPlateau的语义是“监控验证损失值”。

4.3 三种评估指标:MSE、MAE、方向准确率

回归模型的评估不能只看损失值,至少要看三维:MSE 衡量大头误差,MAE 衡量平均偏差,方向准确率衡量“预测的涨和实际涨是否一致”。方向准确率才是做策略时最关心的指标,因为外汇交易的盈亏直接取决于方向判断,幅度大小是次要因素。

from sklearn.metrics import mean_squared_error, mean_absolute_error with torch.no_grad(): test_preds = model(torch.tensor(X_test)).numpy() test_actual = y_test mse = mean_squared_error(test_actual, test_preds) mae = mean_absolute_error(test_actual, test_preds) # 方向准确率:(预测涨跌幅符号与真实符号一致的比例) direction_accuracy = (np.sign(test_preds) == np.sign(test_actual)).mean() print(f"MSE: {mse:.6f}") print(f"MAE: {mae:.6f}") print(f"方向准确率: {direction_accuracy:.2%}")

日线收益率的取值通常在 -0.01 到 0.01 之间,对应的 MSE 数值会非常小,不要以为 MSE 达到 1e-5 就是模型很准。外汇日线做方向预测,准确率能稳定在 53% 以上,就已经是相当不错的模型了。如果方向准确率连 50% 都不到,说明模型不仅没学到规律,还在系统性地把方向预测反了,这时候第一个要检查的是特征里是否混入了未来信息。

5. 避坑实录:LSTM 外汇预测中最常踩的五个坑

5.1 预测曲线整体右移:滞后效应的真相与新数据重新预测的错觉

现象:测试集上模型预测的收益率曲线在视觉上完全滞后于真实曲线一个周期,看起来好像模型“只会抄袭昨天的值”。

原因:这是 LSTM 回归到条件均值导致的:当下一个时间步的收益率以噪声为主时,模型学到的最优策略就是预测一个接近零的均值。在累计价格曲线上看,就会觉得预测在复制过去。本质是数据信噪比太低,模型没找到除了动量之外的可靠信号。

解决:解决方向不是继续调参,而是换任务定义。把“预测下一日收益率”改成“预测未来 5 日累计收益率的方向”,相当于让模型在更长的时间尺度上找信号,这通常能部分缓解“贴着零预测”的问题;另一个做法是把输出目标换成np.sign()做二分类,让模型只输出上涨或下跌的概率,损失函数从 MSE 换成 BCE。

5.2 归一化把训练集和测试集放在一起算 min-max 的翻车事件

现象:训练时损失正常、验证集损失也正常,但回测时发现交易信号全被延迟甚至反向。

原因:你对全量数据做了归一化,测试集的一部分统计信息(比如最大值、最小值)被“偷看”了,这是典型的未来函数。模型在训练时已经隐式得知了测试集的价格范围,这种信息泄露会让验证结果虚高。

解决:严格按时间顺序先切分数据,再对训练集单独计算均值和标准差,把同样的参数应用到验证集和测试集。并且保存归一化参数,每次加载新数据做推理时,用训练时保存的参数做变换,而不是重新计算。

5.3 验证集损失断崖式上升:数据里混入了异常值

现象:验证集 loss 曲线平稳,但在某一个 epoch 突然暴涨到训练过程里的最大值。

原因:外汇数据里有非交易时段的跳空、也有原始数据里的脏数据。某个极端大数被模型碰到,梯度瞬间暴涨,直接毁掉了之前学好的参数。

解决:除了梯度裁剪之外,还要在数据预处理阶段做离群值过滤。我常规做法是收益率绝对值超过 5 倍滚动标准差的值直接剔除,宁可牺牲几条样本,也不能让极端噪声毁掉整个训练。

5.4 同一套代码 EURUSD 好用,GBPUSD 全盘失效

现象:把模型从 EURUSD 换到 GBPUSD 上,方向准确率从 54% 掉到 48%,怎么调参都不行。

原因:不同货币对的波动特性、趋势持续性、受宏观经济影响的方式完全不同。把单一货币对的训练策略当成通用方案,是设计层面的失误。

解决:模型结构可以通用,但特征选择、lookback 长度、早停 patience 都要按货币对风格单独搜索。美元兑日元容易走出趋势,lookback 可以适当拉到 60;欧元兑美元盘整时间更多,lookback 缩短到 20,同时加重 RSI 特征在模型中的贡献。

5.5 回测接口亏钱,但模型评估指标很漂亮

现象:模型的方向准确率在 55% 左右,测试集上的 MAE 也很理想,但按信号回测却稳定亏损。

原因:方向准确率忽视了预测概率的置信度。模型可能在大涨大跌时都给出 0.5 附近的预测,只在趋势不明时给出稍微偏向一边的答案,方向判断对了但没有足够的收益覆盖交易成本。

解决:把分位数过滤加入信号流程,例如只接受模型预测值处于历史最高 20% 和最低 20% 的时刻,其他中间预测一律当作观望区间。用这个方式把样本压缩,回测收益通常远比全量样本要好。

6. 从预测到信号:把 LSTM 输出转成可落地的交易策略

框架搭好、模型训练完,接下来就是要决定这个模型到底怎么用。外汇市场里盈利的交易者没有谁靠单一方向准确率吃饭,区别在于他们如何把模型的输出转化为可执行的决策。我这里的经验是:LSTM 的输出不直接作为交易信号,而是作为“过滤器”——只用它在高置信区间做方向判断,其他时间不交易。

先定义置信区间的概念。加载测试集预测结果,看预测值的分布,找到 80% 分位和 20% 分位:

lower_threshold = np.percentile(test_preds, 20) upper_threshold = np.percentile(test_preds, 80) # 生成信号:只有预测值位于阈值之外才产生交易 signals = np.zeros_like(test_preds) signals[test_preds > upper_threshold] = 1.0 # 做多 signals[test_preds < lower_threshold] = -1.0 # 做空 # 其余位置为 0,表示空仓观望 print(f"做多信号: {(signals == 1).sum()} 条") print(f"做空信号: {(signals == -1).sum()} 条") print(f"观望: {(signals == 0).sum()} 条")

阈值设置之后还要加一个去抖动机制:外汇市场的噪声会让模型频繁切换信号,昨天做多、今天做空、明天又做多,产生大量无效交易。我一般加一个简单的持仓冷却期——信号产生后至少持有 5 个交易日,期间信号翻转也不动作。这个机制在外汇策略里比模型本身更影响最终收益,算是血泪经验总结。

最后是模型更新的节奏。外汇市场在变,半年前的模型参数放到今天,预测分布往往已经不再匹配。我维持一个每周重新训练一次模型的节奏,把最近一周新增的真实数据并入训练集,模型性能下降时,用历史回测的方式去找原因——是数据分布发生了结构性改变,还是参数的随机性导致结果不稳定。

这个项目的取舍到最后变成一条纪律:LSTM 不是“预测机器”,而是“状态识别器”。用它判断当前市场状态是处于动量延续还是均值回归阶段,可能比让它连续输出价格更可靠。我刚开始做的时候也迷信模型能跑出每一根 K 线的方向,最后失败了几轮才明白,它给出的只是一个概率,概率之外的交易纪律、仓位管理和风险控制,才是决定账户曲线走势的大头。希望这套思路和代码能帮你至少少走一段弯路。

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

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

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

立即咨询