
简介面向金融时序预测与深度学习入门者的外汇预测完整工程基于LSTM循环神经网络构建聚焦美元兑日元、英镑兑美元等主要货币对的一分钟级数据建模与多步预测。资源内置毕设级可运行代码适合计算机、人工智能、金融等相关专业学生开展课程设计或毕业设计也适合对量化交易感兴趣的学习者从数据准备到模型训练全程实践。整包共26个文件约13.41MB包含4个ipynb格式的交互式脚本完整覆盖数据集切分、模型训练、测试与多步预测另有csv格式历史行情数据、py格式的公共函数与配置脚本、bin格式的已训练模型权重及归一化器参数配套README说明文档目录结构紧凑清晰。从原始CSV加载、滑动窗口切分到模型训练与多步预测的每个环节均有具体实现便于逐段阅读和调试。已有87人参与学习代码按模块组织可快速复现LSTM外汇预测实验亦可作为扩展其他金融时序预测任务的基础框架。1. LSTM外汇预测先问清楚能预测什么、再动手外汇预测是量化交易里最常见的 LSTM 落地场景之一。基于 LSTM 网络的外汇预测模型核心做法是把 EURUSD 这类货币对的分钟线或小时线切成固定长度的历史窗口交给 LSTM 神经网络去学序列里的时间依赖再输出未来若干时刻的走势。这类项目适合两类人一类是想把时间序列预测从 ARIMA 切到深度学习的 Python 工程师另一类是刚入行量化、想亲手跑通“源码 数据 模型”完整闭环的从业者。我得把丑话说在前面这类模型能把走势形态学得很像但距离稳定盈利还隔着数据质量、点差成本和回测陷阱三道坎。下面就把这三道坎拆开让你从数据到模型每一步都知道自己在做什么。2. 数据准备外汇原始数据怎么变成一份合格的 LSTM 数据集LSTM 吃的是固定步长的张量不是整条 K 线序列。很多人在这一步就翻车拿着原始 CSV 直接塞模型或者把归一化放在全部数据上做完才想起来要切训练集。这一章把“原始数据 → 干净序列 → 样本矩阵 → 归一化”这条链走完到训练环节你手上应该是一个形状为(samples, lookback, features)的 numpy 数组。2.1 数据源与字段清洗先拿能下载的历史数据外汇历史数据没有统一的免费接口常见做法是从 Dukascopy、HistData 这类渠道下载 CSV或者用 MT4/MT5 自带的导出功能把货币对历史行情导出来。文件一般包含六列time、open、high、low、close、volume。有两件事必须处理干净第一是时区很多平台导出的时间戳是欧洲中部时区和 UTC 相差一到两小时第二是重复和缺失外汇周末休市周一开盘容易产生跳空人工行情里还可能有重复的分钟 K 线。时区不一致会导致不同货币对拼在一起时对齐错位重复 K 线会导致模型把同一条数据既当训练又当验证。import pandas as pd # 读取 MT4 导出的 M15 周期 EURUSD 历史数据 df pd.read_csv( EURUSD_M15.csv, header0, names[time, open, high, low, close, volume] ) # 时间列标准化为 UTC并去重、排序 df[time] pd.to_datetime(df[time], utcTrue) df ( df.sort_values(time) .drop_duplicates(subsettime, keeplast) .reset_index(dropTrue) ) # 删掉成交量为 0 的时段周末或节假日挂零 df df[df[volume] 0].reset_index(dropTrue) print(df.head())这段代码做的事情是按时间升序排序、去掉重复时间戳、过滤零成交量。注意drop_duplicates的keep参数我用了last因为外汇平台在 K 线收口时偶尔会先落一条未完成的数据再覆盖保留最后一条更安全。数据量上M15 周期一年大约三万五千根 K 线训练一个 LSTM 完全够用不建议一开始就上 TICK 级别文件大且噪声高。拿到数据后先别急着往下走看一眼覆盖的时间区间如果只有几个月模型只会见过一种市场状态后面一换月份就容易崩。2.2 滑动窗口一个有复用价值的 LSTM 数据集切法LSTM 最短的输入也要是一个二维序列所以在喂模型之前要把连续的价格序列切成样本。这一步决定了“用过去多少根 K 线、预测未来多少根”也就是 lookback 和 horizon。常见做法是 lookback 取 60 到 120对应 M15 级别的是 15 到 30 小时的历史这个范围既能覆盖短周期趋势又不至于让输入维度太大拖慢训练。这里有个新手常见的直觉误区以为 lookback 越大信息越多。其实外汇市场的日内噪声很重太长的窗口反而会把干扰信息一起喂进去。窗口切法建议封装成一个函数训练、验证、测试三段都复用它避免三套代码各切各的导致形状对不上。import numpy as np def make_samples(series, lookback64, horizon1): X, y [], [] for i in range(len(series) - lookback - horizon 1): X.append(series[i : i lookback]) y.append(series[i lookback : i lookback horizon]) return np.array(X), np.array(y) # 以收盘价为输入特征预测未来一根 K 线的收盘价 close df[close].values.astype(np.float32) X, y make_samples(close, lookback64, horizon1) print(X.shape, y.shape) # 输出类似 (34936, 64, 1) (34936, 1)X 的形状是(样本数, 64, 1)第三个维度是特征数哪怕只有收盘价一个特征也要保留这个维度因为 Keras 和 PyTorch 都约定输入包含时间步维度少一维会直接报错。horizon1时 y 是单步目标后面想做多步预测把horizon调大y 的形状会变成(样本数, horizon, 1)。第一次跑通建议只用收盘价一个特征先把基线跑出来再加成交量、RSI 之类。多特征有一个隐蔽的坑RSI 默认是 14 根 K 线的均值和收盘价拼接时前 13 个位置的值是无效的必须从第 14 根开始拼否则模型在序列起始会读到一堆窗口没填满的噪声。2.3 归一化与反变换训练集独自 fit别让未来数据进场价格序列数值很大LSTM 内部激活函数对较大输入会饱和所以必须归一化。外汇预测项目里最常用的不是 StandardScaler 而是 MinMaxScaler原因很简单预测回来要还原成真实价格MinMax 的逆变换是一一对应的线性逆运算不会出现负数价格这种尴尬事。但这里有一个几乎所有新手都会踩的坑在整个数据集上 fit再切 train/valid/test。这样验证集和测试集的“未来”信息已经通过最大值最小值泄露进了训练时的预处理里回测结果会比真实情况好看很多。正确做法是先切好区间再让 Scaler 只看训练集。from sklearn.preprocessing import MinMaxScaler train_size int(len(close) * 0.70) val_size int(len(close) * 0.15) train_raw close[:train_size].reshape(-1, 1) val_raw close[train_size:train_size val_size].reshape(-1, 1) test_raw close[train_size val_size:].reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) # 只在训练段 fit验证与测试段用同一个 scaler transform scaled_train scaler.fit_transform(train_raw) scaled_val scaler.transform(val_raw) scaled_test scaler.transform(test_raw)这一段是数据管线的核心。scaler.fit_transform只发生在train_raw上val和test用的是同一个 scaler 的transform这样测试阶段完全模拟“只见过历史数据”的在线预测场景。后面模型输出的预测值要还原成价格直接scaler.inverse_transform就行不需要重新训练任何东西。顺便提一句切分比例 70/15/15 是这类项目的常见配置数据量少可以改成 80/10/10但验证集最好不要少于 3000 根 K 线否则 EarlyStopping 判断不出来的过拟合会被噪声掩盖。3. 搭建 LSTM 网络张量形状、层数与训练参数数据管道完成之后下一步是把样本喂进 LSTM。这一章先讲清楚输入张量的形状逻辑再给出一套在外汇预测上不容易翻车的网络配置最后把训练参数和早停机制一起定下来。很多人拿到一份 LSTM 模型代码直接跑loss 低到 0.0001 就以为成功了其实往往只是把上一条 K 线的价格复读了一遍。3.1 输入张量形状Keras 和 PyTorch 各自要什么Keras 的 LSTM 层接受(batch_size, timesteps, features)三阶张量PyTorch 的 LSTM 则接受(seq_len, batch, input_size)第一次用时很容易在这两个框架之间切换出错。我们刚才用make_samples已经保证了 X 的形状是(samples, lookback, features)但有一个细节要确认当多个特征混在一起时特征的列顺序要固定LSTM 会在最后一个维度上做全连接计算特征列顺序不同后面想解释哪个特征起了作用就对不上号。import tensorflow as tf # X 形状: (samples, lookback, features) model tf.keras.Sequential([ tf.keras.layers.Input(shape(X.shape[1], X.shape[2])), tf.keras.layers.LSTM(64, return_sequencesTrue), tf.keras.layers.Dropout(0.20), tf.keras.layers.LSTM(32, return_sequencesFalse), tf.keras.layers.Dropout(0.20), tf.keras.layers.Dense(1) ]) model.summary()这段代码里Input的 shape 只写时间步和特征数两项batch 维度由训练时自动填充。return_sequencesTrue让第一层 LSTM 输出完整序列给第二层第二层return_sequencesFalse只保留最后一个时刻的输出再接Dense层。如果两层都写成 False第二层会收到一个二维向量而不是序列维度错误在 Keras 里会直接爆出来。这里用 TensorFlow/Keras 而不是 PyTorch不是框架优劣问题而是 LSTM 时间序列预测的验证环节需要频繁做inverse_transform和窗口重切Keras 的fit接口把数据管道和训练循环绑得比较松中途检查输入输出更方便用 PyTorch 当然也能做只是要自己写训练循环的样板代码会多一些。3.2 隐藏单元与层数外汇噪声数据下的保守配置外汇价格序列的本质是噪声很大的非平稳信号网络结构太保守学不到形态太深又容易过拟合。我在外汇项目上常用的配置是两层 LSTM第一层 64 单元、第二层 32 单元dropout 取 0.2 到 0.3。这个配置在 EURUSD 和 GBPUSD 的 M15 数据上表现都比较可靠。单层 LSTM 在数据量不足时反而更稳因为外汇数据虽然有数万根 K 线但有效“模式”数量不多深层网络容易把噪声当成规律。三层以上一般没必要训练时间翻倍验证集 loss 并不会显著下降。参数取值说明lookback64过去 64 根 M15 K 线约 16 小时horizon1预测下一根 K 线收盘价LSTM 层数2第一层 64 单元第二层 32 单元dropout0.2每层 LSTM 之后接 dropoutlearning_rate0.0005Adam 优化器batch_size6464 或 128 均可epochs50配合早停实际 15 到 25 轮收敛patience10验证集不改善 10 轮就停这张表是一套比较省心的起点新手可以直接照抄跑通后再逐项改。要改参数时一次只改一个比如把lookback从 64 调到 128观察验证集 loss 是变好还是变坏否则几个参数一起动出了问题根本不知道是谁的锅。3.3 训练参数学习率、batch size 和早停不少 LSTM 模型翻车不是网络搭错了而是训练参数没调好就上全量 epoch。先说学习率Adam 默认的 0.001 在外汇预测这种小 loss 场景里偏乐观我一般降到 0.0005。batch size 取 64 或 128太小的话梯度更新抖动大太大则一个 epoch 里的更新次数少收敛变慢。epoch 上限设 50配上 EarlyStopping 看验证集 losspatience设 10这样既不会欠拟合也不会死磕到过拟合。model.compile(optimizertf.keras.optimizers.Adam(learning_rate0.0005), lossmse, metrics[mae]) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs50, batch_size64, callbacks[early_stop], verbose1 )损失函数我选 MSE 而不是 MAE原因是 MSE 对偏离较大的预测惩罚更重能逼着模型在价格突变处更保守缺点是它对噪声敏感所以评估时我还会单独看 MAE两个指标一起观察。还有一个容易忽略的细节训练和验证数据绝对不能 shuffle。LSTM 里的时间依赖被打乱后模型学到的是“随机窗口到随机窗口”的映射验证集 loss 会失真。如果非要在训练时做 shuffle也要保证每个样本内部的 K 线顺序不被破坏TensorFlow 的默认shuffleTrue只打乱样本顺序、不打乱窗口内部顺序但理解这个机制能避免你自己写数据管道时把序列倒过来。4. 跑通 Python 源码训练、预测、保存的最小闭环现在把前面两个部分拼成一个可运行的 Python 工程。这套源码结构适用于大多数 LSTM 时间序列预测不仅限外汇。建议按文件拆成data_loader.py、train.py、predict.py三块这样后面换数据源、改参数不用重写。本质上这就是一套 python 量化交易策略代码的雏形数据换成本地股票或期货 K 线模型部分完全不用动。4.1 数据加载脚本把清洗、切窗、归一化收进一个函数import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler def load_data(pathEURUSD_M15.csv, lookback64, horizon1): df pd.read_csv(path, header0, names[time, open, high, low, close, volume]) df[time] pd.to_datetime(df[time], utcTrue) df df.sort_values(time).drop_duplicates(subsettime, keeplast) df df[df[volume] 0].reset_index(dropTrue) close df[close].values.astype(np.float32) train_raw close[:int(len(close) * 0.70)].reshape(-1, 1) val_raw close[int(len(close) * 0.70):int(len(close) * 0.85)].reshape(-1, 1) test_raw close[int(len(close) * 0.85):].reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw) val_scaled scaler.transform(val_raw) test_scaled scaler.transform(test_raw) def make_samples(series): X, y [], [] for i in range(len(series) - lookback - horizon 1): X.append(series[i:i lookback]) y.append(series[i lookback:i lookback horizon]) return np.array(X), np.array(y) X_train, y_train make_samples(train_scaled) X_val, y_val make_samples(val_scaled) X_test, y_test make_samples(test_scaled) return X_train, y_train, X_val, y_val, X_test, y_test, scaler这个load_data把第 2 章所有关键逻辑收在一起返回值包含训练、验证、测试三组样本和归一化器。注意make_samples定义在函数内部三组数据共用一套切法不会出现训练集窗口是 64、测试集窗口变成 128 的错位问题。scaler也要返回因为预测阶段还原价格必须用它。4.2 训练脚本编译、早停、保存一条龙import tensorflow as tf from data_loader import load_data X_train, y_train, X_val, y_val, _, _, scaler load_data() model tf.keras.Sequential([ tf.keras.layers.Input(shape(X_train.shape[1], X_train.shape[2])), tf.keras.layers.LSTM(64, return_sequencesTrue), tf.keras.layers.Dropout(0.20), tf.keras.layers.LSTM(32, return_sequencesFalse), tf.keras.layers.Dropout(0.20), tf.keras.layers.Dense(y_train.shape[1]) ]) model.compile(optimizertf.keras.optimizers.Adam(learning_rate0.0005), lossmse, metrics[mae]) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) model.fit(X_train, y_train, validation_data(X_val, y_val), epochs50, batch_size64, callbacks[early_stop], verbose1) model.save(lstm_fx_model.keras)这段代码直接读load_data的返回值训练模型Dense层的输出维度用y_train.shape[1]动态设置这样把horizon改成 5 或 10 时不用改动网络定义。保存后缀用.keras而不是.h5TensorFlow 2.x 新格式更干净兼容性也更好。训练过程里盯着val_loss看它应该在前 10 到 20 轮快速下降然后变平如果val_loss连续 10 轮不降EarlyStopping 会自动回滚到最佳权重不用手动打断。4.3 预测脚本把归一化数值还原成可读价格import numpy as np import tensorflow as tf from data_loader import load_data X_train, y_train, X_val, y_val, X_test, y_test, scaler load_data() model tf.keras.models.load_model(lstm_fx_model.keras) pred model.predict(X_test) pred_price scaler.inverse_transform(pred.reshape(-1, 1)).ravel() true_price scaler.inverse_transform(y_test.reshape(-1, 1)).ravel() for i in range(5): print(ft1 真实 {true_price[i]:.5f} 预测 {pred_price[i]:.5f})预测脚本的关键在于inverse_transform前必须把输出reshape(-1, 1)因为 pred 的形状是(样本数, horizon)而 scikit-learn 的 Scaler 要求二维列向量。这里我顺便把真实值也还原出来方便肉眼对比。第一次跑通后你会发现预测值和真实值贴得很近这是正常现象因为单步价格预测本身是个自回归任务上一条 K 线已经包含了大量信息。真正要看的是方向对不对这个放到第 6 章细讲。5. 外汇 LSTM 避坑五个最容易翻车的地方模型跑通只是开始真正的麻烦在评估和上线阶段。这一章列五个我在外汇 LSTM 项目里反复踩过的坑每一条都按现象、原因、解决的顺序写清楚希望能让你少走几趟弯路。5.1 归一化把未来信息泄露了现象训练集和验证集 loss 都低得漂亮曲线贴合度极高模型参数也没有明显过拟合但一旦换成新的时间段做样本外测试预测结果立刻失真。原因在第 2 章已经提到如果MinMaxScaler在整个数据集上fit那么验证集和测试集的最大最小值已经参与了训练数据的缩放。模型在训练时见过的数值范围隐含了未来的价格高低点回测自然好看实盘时新的价格突破历史区间transform出来的值就超出了训练分布。解决严格按“训练集 fit → 验证集 transform → 测试集 transform”的顺序走。scaler只保存训练集的data_min_和data_max_上线后每次新数据都用同一个 scaler 处理不要因为一个月没更新就重新 fit。5.2 预测曲线比真实曲线滞后一根 K 线现象把预测值和真实值叠在图上预测线整体向右偏移了一根 K 线像是把昨天的价格搬到了今天。很多人在这一步误以为模型学到了趋势实际上是学到了“上一根 K 线收盘价”。原因这是单步预测最典型的失效模式。当目标变量是价格本身且价格序列自相关性极强时LSTM 的最优策略就是最小化下一个时刻与当前时刻的差值也就是复读。MSE 会奖励这个行为因为预测值等于上一个值在统计上确实让误差最小。解决换一个目标变量比如预测对数收益率r log(close_t / close_{t-1})或者在评估时用方向准确率而不是 MAE。这样模型没法靠复读拿分因为收益率的自相关性远低于价格。我一般会同时保留价格作为输入特征但训练标签改成下一根 K 线的收益率。5.3 训练 loss 降得很好测试集却一塌糊涂现象训练集 loss 一路降到 0.00001验证集 loss 在某个点之后开始反弹测试集上更是看不出任何规律。原因典型过拟合。常见触发点有三个一是网络层数多、单元数大二是 dropout 设成 0三是训练轮数太长且没开 EarlyStopping。外汇数据噪声大模型很容易把噪声当模式记住。解决先检查 EarlyStopping 是否生效restore_best_weightsTrue必须开着然后把第一层 LSTM 的单元数从 64 降到 32或者把 dropout 提到 0.3如果还不行检查训练样本量是不是太少最少也要有一万根 K 线。每改一个参数就重新训练一次对比验证集 loss不要同时改多个。5.4 特征列顺序和时间索引错位现象加了多特征之后指标偶尔出现极端值预测曲线在某些点突然跳出一个离谱的价格而单特征模型没有这个问题。原因特征工程时没对齐时间索引。比如收盘价从第 1 根开始RSI 从第 15 根开始有效直接把两个数组拼成矩阵前 14 行的 RSI 是 0 或 NaNLSTM 在序列开头读到一堆假数据学出的特征映射是错的。解决所有特征必须从同一个有效起点开始对齐。最简单的做法是先算好每个特征统一截取valid_start max(feature_lookbacks)之后的部分再做窗口切分。遇到指标内部用到了未来数据的情况比如某些平滑滤波确认它的窗口是因果的。5.5 模型只会预测均值单边行情里彻底失效现象loss 很小预测值集中在当前价格附近的一个窄区间里行情单边上涨时预测永远跟不上实际方向准确率只有 50% 左右。原因回归模型在 MSE 损失下天然偏向预测条件均值。外汇价格的日变化率通常很小均值接近当前价格所以模型输出贴着上一个价格本质上是在赌“价格不变”。这和第二条的滞后问题同源但即使换对数收益做标签这种不敏感在震荡市里也会出现。解决训练时把预测目标改成方向分类比如输出上涨概率或者使用分位数损失来捕捉区间。另一个更实用的做法是不要指望 LSTM 预测精确价格把它输出当成一个技术指标比如预测未来 N 根的累计收益排名再叠加一个简单的趋势过滤。6. 验证与进阶多步预测、方向准确率和实盘边界单步预测在量化策略里实用性有限因为下单、撮合、滑点都会吃掉时间。常见做法是推进到多步预测一般有两种方式递归预测和直接多输出。递归预测是把上一步的预测值当输入一步步往后滚缺点是误差会累积50 步之后基本等于白噪声直接多输出是让Dense层一次性输出未来 N 个值训练时 y 的形状从(samples, 1)变成(samples, horizon)代价是输出维度变高、拟合难度增大。我更常用的是折中方案预测未来 5 到 12 根 K 线的方向而不是逐根拟合。把 y 改成1 if 未来N根收益0 else 0模型结构里Dense层改成units1, activationsigmoid损失函数换成binary_crossentropy。这样模型不再纠结小数点后第五位反而能抓住波段级别的方向。评估指标上价格预测看 MAE但量化策略真正该盯的是方向准确率DADA 大于 52% 时配合点差成本策略才可能活下来。回测时一定要把点差算进去EURUSD 常见的点差是 0.6 到 1.2 个点看起来小但高频预测下按天累计就是一大块成本。我的习惯是先在样本外数据上跑至少三个月确认方向准确率稳定再考虑对接模拟盘。模型更新频率建议每天或每周重新训练一次因为外汇市场的状态会缓慢漂移一次训练终身使用大概率会翻车。最后说一句经验教训这类项目最容易让人执着于调参降低 loss而忽略数据质量、点差成本和实际方向准确率。我自己早期就吃过亏花了一周把验证集 loss 压到极低结果换到新的三个月数据上直接亏成负数。现在做任何 LSTM 预测我都会先把评估指标定成方向准确率和扣除点差后的净收益再去动网络结构。方向走对了模型才不会变成昂贵的玩具。希望帮到你。本文还有配套的精品资源点击获取