
简介一份基于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. 数据是第一道分水岭拿到外汇行情后先别急着喂给 LSTM2.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(EURUSDX, period5y, interval1d, auto_adjustTrue, progressFalse) # 只保留收盘价后续特征都从 close 衍生 df df[[Close]].copy() df.columns [close] df.dropna(inplaceTrue) 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(lower0).rolling(14).mean() loss -delta.clip(upper0).rolling(14).mean() rs gain / loss df[rsi] 100 - (100 / (1 rs)) # 3. 波动率过去 20 日收益率的滚动标准差 df[volatility] df[return].rolling(20).std() df.dropna(inplaceTrue) print(df.tail())这里return是模型的核心学习目标rsi和volatility是辅助特征。为什么加这两个因为外汇市场的特征是“震荡行情中均值回归趋势行情中动量延续”RSI 和波动率组合起来能粗略描述当前处于什么市场状态给 LSTM 一个“上下文”信号。注意滚动窗口计算会导致前 N 行数据为空我习惯直接丢不要用向前填充那会引入未来信息。2.3 滑动窗口切分lookback 长度与数据划分的陷阱单条样本长这样用过去window_size天的特征序列预测下一天或未来 N 天的收益率。这个window_size就是 LSTM 的时间步长它决定模型“记得”多长的历史。外汇市场里我一般用 20 到 60 这个范围。短了捕捉不到中期波动规律长了模型被远古噪声干扰。def create_sequences(data, lookback30, horizon1): X, y [], [] for i in range(len(data) - lookback - horizon 1): X.append(data.iloc[i:ilookback].values) y.append(data.iloc[ilookbackhorizon-1][return]) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) feature_cols [return, rsi, volatility] X, y create_sequences(df[feature_cols], lookback30, horizon1) 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_sequences和return_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_size3, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) 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_size64是一个相对稳定的起点太小小于 32模型容量不足太大大于 128在样本量只有 1000 左右时非常容易过拟合。num_layers2表示堆叠两层 LSTM第二层把第一层的输出序列作为输入。dropout0.2只在层间生效PyTorch 规定单层 LSTM 时 dropout 参数会被忽略这是源码层面的限制不用在单层上浪费调参时间。3.3 损失函数与优化器选择回归指标和交易目标并不总是一回事预测收益率是一个回归问题最常见的损失函数是 MSE但在外汇场景里我强烈建议留意 MAE 的表现。原因是MSE 对大的预测偏差施加了平方惩罚会让模型把过多精力花在拟合那些极端波动点上而那些点恰恰是金融危机或者数据跳空造成的本来就无法用历史窗口预测。MAE 对异常值更鲁棒训练的模型方向准确率往往更高。import torch.optim as optim model LSTMRegressor(input_size3, hidden_size64, num_layers2) optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-5) criterion nn.MSELoss() # 或者换成 nn.L1Loss()对应 MAEweight_decay1e-5是 L2 正则化相当于给权重加了一个“惩戒项”防止极端参数出现。lr0.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_size64, shuffleTrue) val_dataset TensorDataset( torch.tensor(X_val), torch.tensor(y_val) ) val_loader DataLoader(val_dataset, batch_size256, shuffleFalse) 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_norm1.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_norm1.0意味着把所有参数的梯度 L2 范数压缩到不超过 1.0。LSTM 在长序列上容易出现梯度爆炸这是来源层面的数值不稳定问题梯度裁剪几乎是强制性的。patience15在日线数据上够用如果换到小时线数据可以调整到 25 到 30因为小时线噪声更大验证损失曲线更毛糙。4.2 学习率调度固定学习率训练到死路之后的一种后悔药固定学习率在训练初期跑得顺畅但到了中后期loss 会在一个区间来回震荡减不下去。一个廉价的补丁是学习率衰减当验证损失连续多个 epoch 不下降时把学习率降为原来的 1/5 或 1/10让参数更新跳得更精细。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, min_lr1e-6, verboseTrue ) # 在验证阶段计算出 avg_val_loss 后调用 scheduler.step(avg_val_loss)factor0.5控制衰减幅度每次学习率减半这是比较温和且稳妥的设置。min_lr1e-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(fMSE: {mse:.6f}) print(fMAE: {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 线的方向最后失败了几轮才明白它给出的只是一个概率概率之外的交易纪律、仓位管理和风险控制才是决定账户曲线走势的大头。希望这套思路和代码能帮你至少少走一段弯路。本文还有配套的精品资源点击获取