
简介基于LSTM网络的外汇预测模型完整项目面向计算机相关专业学生、研究人员及金融数据分析入门者可用于学习循环神经网络与时间序列预测也可作为毕业设计或课程设计的参考实现。包内共31个文件压缩包约13.41MB主要包含Jupyter Notebook演示脚本、Python工具模块、CSV汇率数据集、TensorFlow SavedModel模型权重文件等覆盖数据准备、模型训练、测试与预测全流程。已有46人学习下载。资源提供USDJPY、GBPUSD等货币对的历史数据与预训练模型并配有README说明、通用变量配置及时间序列处理自定义模块读者可对照Notebook逐段理解LSTM门控机制、数据标准化与序列构建方法也可直接加载权重进行预测实验。项目代码组织结构清晰适合在本地复现并扩展至其他金融序列预测场景。1. 用 LSTM 构建外汇预测模型源码、数据和真实交易边界外汇价格序列天生非平稳且受到跨时区流动性与宏观事件的双重影响很多人在跑通 LSTM 代码后很快发现训练集上漂亮的损失曲线进入测试集就不成立。真正原因不是 LSTM 不好而是滑窗构造、归一化、损失函数和数据切分这几步出了问题。这篇文章给出一套可复现的 Python 源码方案从历史行情下载、特征构造、滑窗生成到 PyTorch 训练、早停和滚动回测完整覆盖“基于 LSTM 网络的外汇预测模型”落地的每个环节。适合已经会用 pandas想把手头外汇数据做成一个可验证模型的工程师和量化爱好者所有结论都能在本地单机环境里用 CPU 复现。2. 为什么是 LSTM外汇时序统计特征与滑窗样本构造2.1 外汇价格的非平稳性和波动聚集决定了不能直接预测原始价格外汇市场的交易时间接近 24 小时亚洲盘、欧洲盘、美洲盘依次衔接流动性格局随时间变化。价格序列因此有明显的波动聚集现象大波动之后跟着大波动平静期往往持续一小段时间后又进入新的剧烈行情这种特性让“未来与过去相似”的假设只在短期内成立。若直接拿原始收盘价做回归目标模型会严重依赖上一根 K 线的绝对数值。原因是连续价格的自相关系数极高模型只要输出上一时刻价格就能把损失压得很低等于什么都没学。我一般把预测目标从价格改成对数收益即log(P_t / P_{t-1})。对数收益把价格水平的影响剥离掉还保留了收益的相加性便于不同时间尺度的收益计算。很多量化团队在输出层不直接用收益回归而是把收益的正负转换成 0/1 分类问题因为在做信号决策时方向往往比幅度更重要。该技术细节会在第四章展开。2.2 遗忘门、输入门与输出门如何实现长短期记忆LSTM 单元在每一步维护一个单元状态用遗忘门控制历史信息保留多少输入门控制新信息写入多少输出门决定当前隐藏状态向外输出多少。外汇行情里存在一些跨多个时间步的影响比如某个时区流动性突变后价格往往要经过十几根 K 线才重新回归均衡这种影响不会像股票日线持仓那样以“天”为单位而是以“小时”为单位。普通 RNN 在反向传播时梯度会随着时间步以连乘方式衰减30 步以上的历史基本被遗忘LSTM 通过逐元素加和传递状态让梯度在长路径上相对稳定。但 LSTM 不是万能记忆器。如果滑动窗口内的特征本身没有包含事件信息遗忘门也只能在已有输入里做筛选无法无中生有。窗口长度设置同样遵循此原则窗口太短捕捉不到跨时段规律窗口太长又会引入过多与当前决策无关的历史噪声。我通常先用自相关图观察目标序列在多少个滞后阶数上仍存在显著相关性再拿这个滞后数当作 seq_len 的初值。2.3 用 Pandas 把时间序列切成监督学习样本模型训练前必须把时间序列转换成监督学习格式一个样本由过去seq_len个时间步的特征张量 X 和未来目标 y 组成。下面这段代码完成了核心转换import numpy as np import pandas as pd def create_sequences(features: np.ndarray, target: np.ndarray, seq_len: int 48): 将特征和目标序列切成 (样本数, seq_len, 特征数) 与 (样本数,) 的监督数据。 features: 已归一化后的二维数组每一行是一个时间步的特征。 target: 与 features 行数相同的目标序列通常是下一时刻的对数收益。 seq_len: 用过去多少个时间步预测下一个时间步。 X, y [], [] for i in range(len(features) - seq_len): X.append(features[i : i seq_len]) y.append(target[i seq_len]) return np.array(X), np.array(y) # 假设 df 是重采样后的小时线数据包含 close 列 # df[ret] np.log(df[close]).diff() # feature_cols [ret, rsi, atr] # 注意归一化要在切窗前完成且只能用训练集统计量逻辑说明循环从第seq_len行开始截取窗口每一组连续seq_len行构成一个样本目标是窗口后一个时间步的目标值。这里有一点容易被忽视样本之间存在大量重叠前一个样本和后一个样本共享seq_len - 1行数据。这让有效样本数变得很大但信息量并没有增加那么多训练时必须靠验证集来确认没有过拟合。参数说明target[i seq_len]意味着预测的是窗口结束后的下一点收益这种一步预测是构建多步预测的基础如果你要预测未来三步可以改造成预测target[i seq_len : i seq_len 3]但输出层结构也要同步调整。seq_len的典型取值范围在小时线上是 24 到 48对应过去一天到两天的行情在 5 分钟线上常取 48 到 96。要注意序列长度一旦超过 100LSTM 的训练时间会线性增长梯度衰减问题也会重新显现。提示滑动窗口的数据重叠会在随机打乱样本时制造隐式泄漏。训练集和验证集在时间上若存在重叠验证集指标会虚假偏高。切分数据前务必把重叠检查纳入流程。3. 从原始行情到训练集数据获取、特征工程与时间切分3.1 项目目录结构与数据层的职责划分一个能长期维护的预测项目必须把源码、原始数据和模型权重分开。下面是我常用的目录结构整个环境只需要一个 Python 虚拟环境外加一份requirements.txtforex_lstm/ ├── data/ │ ├── raw/ # 从公开来源下载的原始 CSV │ └── processed/ # 重采样、清洗后的小时线特征 ├── src/ │ ├── fetch_data.py # 下载历史行情 │ ├── preprocess.py # 清洗、特征工程、归一化 │ ├── model.py # LSTM 网络结构定义 │ ├── train.py # 训练循环、早停、模型保存 │ └── predict.py # 滚动预测与结果评估 ├── checkpoints/ # 训练好的权重文件 └── config.py # 所有超参数集中管理这样的划分让重训流程变得可重复数据更新只动fetch_data.py和preprocess.py模型结构改动只动model.py超参数调整只改config.py不需要为了实验一个参数去翻整个训练脚本。配置集中管理这一点对复现和历史回溯特别重要因为 LSTM 训练结果对随机种子、学习率和窗口长度都比较敏感实验记录里必须能直接对应到一份配置文件。3.2 获取与清洗外汇小时线数据外汇历史数据的来源常常是 CSV 文件或公开 API我以 CSV 加载为例展示清洗过程。数据源给出的字段通常包含时间、开盘价、最高价、最低价、收盘价和成交量其中时间列常带着不同的时区偏移必须先统一为 UTC 再按小时重采样。import pandas as pd def load_and_resample(csv_path: str, rule: str 1h) - pd.DataFrame: csv_path: 原始分钟级或小时级数据的本地路径。 rule: pandas 重采样规则1h 表示聚合到小时线。 df pd.read_csv(csv_path) df[time] pd.to_datetime(df[time], utcTrue) df df.set_index(time).sort_index() # 统一到小时线OHLC 取对应聚合值成交量求和 ohlc df[close].resample(rule).ohlc() volume df[volume].resample(rule).sum() merged pd.concat([ohlc, volume], axis1) merged.columns [open, high, low, close, volume] # 删除没有任何报价的时区比如部分平台周末休市缺口 merged merged.dropna(subset[close]) return merged # 实际调用 # raw load_and_resample(EURUSD_M1.csv) # 缺失的中间 K 线可以先用前向填充再判断是否保留逻辑说明先把时间列转换成带 UTC 时区的 pandas 索引避免不同平台的时间戳混用再用resample把分钟线聚合成小时线。这里必须说明重采样不是简单地取整点而是把每个小时段内的报价按 OHLC 规则聚合这样后续加入的技术指标才稳定。聚合后要检查是否有整小时缺失周末和节假日经常出现连续空缺。参数说明rule1h决定了模型的时间尺度。做日内短线用15min或30min更贴近交易节奏但噪声也随粒度减小而增大做日内持仓用1h或4h更合适。数据量有限时优先用较大粒度因为 LSTM 需要大量样本学习模式粒度越细、样本间的有效信息重合度越高反而容易让验证集失真。数据清洗时最需要警惕的是“未来数据混入”。不少原始数据里会偶然出现价格跳变或重复时间戳直接用这些脏数据训练模型会把跳变当成规律去记忆。我的原则是先按时间戳去重再对超过前后各 20 根 K 线波动率 5 倍以上的异常点做标记异常点占比少于 0.1% 就直接剔除超过则需要检查数据源是否对齐错误。3.3 特征列OHLCV 与波动率指标组合LSTM 可以在一定程度上自动提取特征但人工加入少量与外汇波动强相关的技术指标往往能让模型更快收敛。我通常会在基础 OHLCV 上叠加两个特征RSI 和 ATR。RSI 刻画短期的超买超卖状态ATR 刻画当前波动规模这两个指标的计算量小、含义明确不需要额外引入复杂的自定义公式。特征列名计算方式作用returnlog(close).diff()消除非平稳性作为核心输入rsiRSI 14反映短期的动量强弱atrATR 14反映波动的绝对幅度用于风险调整hourtime.dt.hour / 23把时间周期性显式传给模型下面这段代码展示如何构造这些特征def build_features(df: pd.DataFrame, rsi_period: int 14, atr_period: int 14) - pd.DataFrame: df: 来自 load_and_resample 的小时线包含 OHLCV。 rsi_period / atr_period: 两个技术指标的周期14 是常见默认值。 out df.copy() out[return] np.log(out[close]).diff() # 用简单滑动平均实现 RSI避免依赖 TA-Lib 环境 delta out[close].diff() gain delta.clip(lower0).rolling(rsi_period).mean() loss (-delta.clip(upper0)).rolling(rsi_period).mean() rs gain / loss.replace(0, np.nan) out[rsi] 100 - 100 / (1 rs) # ATR 使用典型价格的真实波幅 prev_close out[close].shift(1) tr pd.concat([ out[high] - out[low], (out[high] - prev_close).abs(), (out[low] - prev_close).abs() ], axis1).max(axis1) out[atr] tr.rolling(atr_period).mean() # 去掉因差分和滚动产生的 NaN 行 out out.dropna() return out逻辑说明RSI 的经典计算用 Wilder 平滑这里为减少环境依赖改用标准滚动均值效果在训练集上差异不大。ATR 的关键在真实波幅tr它把跳空缺口也纳入波动计量而跳空在外汇里并不少见。hour特征只有在预测日内行为时才需要因为外汇的活跃时段有明显的周期边界。参数说明rsi_period和atr_period用的是 14 根 K 线这个数来自技术分析传统但它不一定是模型最优。窗口周期偏短时指标响应快、噪声也大周期偏长时平滑但滞后明显。我的做法是让 LSTM 同时接收多个周期的特征但控制特征总数不超过 8 个避免维度膨胀。3.4 训练、验证、测试集按时间顺序切分不能随机打乱外预测模型的标准做法是把时间序列按 6:2:2 或 7:2:1 切成三段并且严格按照时间先后顺序。随机train_test_split在这里是错误用法因为序列自相关会让模型“看见”验证集的信息。代码实现如下def temporal_split(df: pd.DataFrame, train_ratio: float 0.7, val_ratio: float 0.15): 按时间顺序切分数据。 df: 经过 build_features 处理后的完整特征表索引为时间。 train_ratio: 训练集占比val_ratio: 验证集占比测试集取剩余部分。 total len(df) train_end int(total * train_ratio) val_end int(total * (train_ratio val_ratio)) train df.iloc[:train_end] val df.iloc[train_end:val_end] test df.iloc[val_end:] return train, val, test # train, val, test temporal_split(feature_df) # 在训练集上计算均值与标准差再应用到 val 和 test逻辑说明iloc按位置切分前提是df已经按时间升序排列。切分后要做一次检查验证集的起始时间必须大于训练集的结束时间中间不能有任何重叠。切分完特征数据之后还要调用create_sequences分别生成三份样本。参数说明val_ratio取 0.15 时如果训练集有十万条小时线验证集约有一万五千条。验证集太大浪费数据太小则早停判断不稳。数据总量不足一万条时我倾向于把比例改成 0.8/0.1/0.1再用多折滚动验证去弥补验证集过小的问题。提示归一化的统计量只能从训练集计算。若先用全部数据做MinMaxScaler验证集和测试集的分布信息会提前泄漏到训练过程中最终评估结果会偏乐观。4. PyTorch 训练模型定义、损失函数与必调参数4.1 一个能直接在 CPU 上训练的单层 LSTM 模型正常情况下可以在几分钟内跑完的模型用单层 LSTM 加一个全连接输出层就足够。模型结构如下import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features: int, seq_len: int, hidden_size: int 64, num_layers: int 1, dropout: float 0.0, output_size: int 1): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0, ) self.linear nn.Linear(hidden_size, output_size) def forward(self, x): # x 形状: (batch, seq_len, n_features) lstm_out, (h_n, _) self.lstm(x) # 只取最后一个时间步的隐藏状态 h_n[-1] last_hidden h_n[-1] # 形状: (batch, hidden_size) return self.linear(last_hidden)逻辑说明batch_firstTrue让输入的维度顺序变成(batch, seq_len, feature)符合create_sequences生成的张量形状。lstm_out包含了所有时间步的输出但预测下一步只需要最后一步的隐藏状态h_n[-1]取的是最后一层 LSTM 的最后状态这层状态已经编码了整个序列的信息。输出层只有一个神经元对应单步收益的回归值。参数说明hidden_size控制 LSTM 内部记忆容量数值越大表达力越强、越容易过拟合。num_layers在数据量有限时不要超过 2加深层数会把训练时间拉长数倍收益却不明显。dropout只在层数大于 1 时生效因此单层模型要防过拟合得依靠后面提到的早停机制。4.2 回归还是分类损失函数与方向准确率的计算回归问题默认用MSELoss它着重惩罚大幅预测误差但外汇收益序列中偶发大波动会对梯度产生过分影响。另一种做法是把问题转成三分类上涨、下跌、基本持平用交叉熵损失训练。实际预测时分类模型天然输出概率便于控制信号阈值。评估指标计算方式适用场景RMSE均方根的预测误差回归任务的主指标MAE平均绝对误差对异常值不敏感适合重尾数据Directional Accuracy预测方向与实际方向一致的比例判断信号是否有效ARV均方预测误差除以目标方差比值小于 1 才说明优于基准预测方向准确率是外汇信号里最直观的业务指标计算代码如下def directional_accuracy(y_true: np.ndarray, y_pred: np.ndarray) - float: y_true: 真实下一时刻收益序列。 y_pred: 模型预测的下一时刻收益序列。 返回预测涨跌方向与真实涨跌方向一致的比例忽略完全持平的数据点。 mask y_true ! 0 if mask.sum() 0: return 0.0 direction_hit np.sign(y_true[mask]) np.sign(y_pred[mask]) return float(direction_hit.mean())逻辑说明对外汇交易信号来说预测方向正确比预测幅度精确更有价值。看方向准确率时要把取值接近 0 的数据点过滤掉因为这些点方向不稳定统计上去会稀释结果。若方向准确率稳定在 53% 以上的测试集上模型就有实际参考价值。4.3 三个影响收敛的关键参数及其设置逻辑LSTM 训练时最值得反复调整的三个超参数是seq_len、hidden_size和learning_rate。它们的交互关系大于单独作用序列长度长隐含状态需要更大容量来容纳更多历史信息学习率太大时大容量的 hidden_size 更容易让损失震荡。参数推荐范围调整逻辑seq_len24 ~ 481 小时线先看自相关显著滞后阶数再取附近整数hidden_size32 ~ 128数据量大取上限数据量小取下限learning_rate0.001 ~ 0.005使用 Adam 时从 0.001 起步震荡则减半batch_size64 ~ 256过小梯度噪声大过大收敛慢且吃内存实际训练时先固定learning_rate0.001和batch_size128只调seq_len和hidden_size观察验证集 RMSE。一般跑 20 个 epoch 就能看出哪个参数组合明显异常。调参过程不要只记最终验证集数值要记录训练集和验证集的损失曲线二者差距过大说明容量过剩需要减少hidden_size。4.4 早停、学习率衰减与模型保存训练循环里要同时实现早停和学习率衰减。早停的意思是验证集损失连续若干轮不再下降时停止训练并回滚到最佳权重。下面是一个最小实现def train_with_early_stopping(model, train_loader, val_loader, epochs50, patience7, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) criterion nn.MSELoss() best_val_loss float(inf) epochs_no_improve 0 best_state None for epoch in range(epochs): model.train() train_loss_sum 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() y_pred model(x_batch).squeeze() loss criterion(y_pred, y_batch) loss.backward() optimizer.step() train_loss_sum loss.item() * x_batch.size(0) model.eval() val_loss_sum 0.0 n_val 0 with torch.no_grad(): for x_batch, y_batch in val_loader: y_pred model(x_batch).squeeze() loss criterion(y_pred, y_batch) val_loss_sum loss.item() * x_batch.size(0) n_val y_batch.size(0) avg_val_loss val_loss_sum / n_val scheduler.step(avg_val_loss) if avg_val_loss best_val_loss: best_val_loss avg_val_loss best_state {k: v.clone() for k, v in model.state_dict().items()} epochs_no_improve 0 else: epochs_no_improve 1 if epochs_no_improve patience: break model.load_state_dict(best_state) return model逻辑说明ReduceLROnPlateau在验证损失连续 3 轮没有下降时把学习率减半这种动态衰减比固定步长衰减更省心。早停的patience7给了模型足够时间去脱离局部极小点又不会在过拟合阶段停留太久。每次进入验证阶段都要用model.eval()和torch.no_grad()否则训练时的 dropout 状态和梯度缓存会污染验证结果。参数说明patience太小会导致训练提前中断太大又浪费算力。小时线数据一般 30 轮内就能趋于稳定所以我通常设成 7 到 10。batch_size影响每个 epoch 的梯度更新次数如果训练集只有几万个样本128 是比较折中的选择。5. 滚动预测、重训策略与边界验证5.1 滚动预测用过去 N 根 K 线预测未来一根模型训练完成后预测逻辑与训练时必须保持一致。先用训练集末尾的seq_len行数据作为初始窗口预测出下一个时间步再把真实值或预测值回填进窗口继续预测下下步。推理代码非常短def rolling_predict(model, full_feature_df, start_idx, seq_len, steps): full_feature_df: 包含全部特征列的完整数据必须预先归一化。 start_idx: 预测起点即训练集末尾特征行的位置。 steps: 连续预测多少步。 model.eval() buffer full_feature_df.iloc[start_idx - seq_len:start_idx].values buffer torch.tensor(buffer, dtypetorch.float32).unsqueeze(0) preds [] with torch.no_grad(): for _ in range(steps): pred model(buffer).item() preds.append(pred) # 回填预测值保持窗口长度不变 new_row buffer[:, -1, :].clone() new_row[0, 0] pred # 这里把预测值放进第一个特征位 buffer torch.cat([buffer[:, 1:, :], new_row], dim1) return preds逻辑说明回填预测值会带来误差累积预测步数越长精度下降越明显。因此实际使用中我更推荐只做单步滚动预测每得到一个新的真实 K 线就把它纳入窗口再预测下一步而不是一次生成未来 10 根预测线。5.2 用延迟基线判断模型是否学到东西判断模型是否真的学到有效信息最简单的方法是构造一个“延迟模型”作为基线把t时刻的真实收益直接当作t1的预测值。这个模型什么都不学但它能反映序列的自相关到底有多强。如果 LSTM 的验证集方向准确率只比延迟基线高 1%说明模型捕捉到的主要是滞后效应而不是结构性规律。对比方法很简单计算测试集上 LSTM 预测与真实值的方向准确率再计算y_true滞后一位后与y_true的方向准确率。后者保持高比例的话任何强于它的模型都更有说服力。5.3 重训周期与数据漂移外汇市场结构会随时间变化一周前有效的模式三个月后会失效。我一般保留最近 200 个交易日的特征数据重新训练买入新的行情后触发滚动重训每周重训一次用全部历史数据训练到当前最新时间点。重训时保留相同的超参数和滑窗长度只更新数据范围。若模型连续几周方向准确率都低于 50%说明当前特征组合可能已经失效需要回到特征工程阶段重新设计。5.4 模型保存与快速验证流程训练一次的成本不高但最好把训练好的模型状态和配置一起保存下来方便追溯和继续训练。保存时同时记录训练结束时间、数据版本和超参数配置这样后续才能复现。验证时不必等到全部训练完成可以先在少量数据上跑通流程确认滑窗的维度对齐、损失函数正常下降、方向准确率能算出来再扩展数据量正式训练。这种从最小验证到完整训练的顺序能在排查代码问题时节省大量时间。本文还有配套的精品资源点击获取