简介:面向外汇量化入门者与机器学习开发者,这份基于LSTM网络的外汇预测模型压缩包完整演示了从M1级汇率数据清洗、特征构造、样本划分到多步预测的建模流程,适合希望用深度学习处理金融时间序列的读者。包内共27个文件,以ipynb教学笔记、py源码、csv数据集和bin模型权重为主,另含pb模型文件、README说明与配置信息;其中csv已按训练、验证、测试划分,bin为可直接加载的预训练权重,notebook与脚本可对照运行。资源总大小13.41MB,已有119人学习。模型针对USDJPY、GBPUSD等常见货币对训练,覆盖LSTM门控原理、标准化、损失函数选择、训练验证及RMSE/MAE误差评估等关键环节,还讨论了GRU、Transformer及集成学习等改进方向。下载后可沿数据准备—模型构建—训练评估—预测应用主线循序渐进,快速搭起自己的外汇预测实验框架。
1. 用LSTM做外汇预测,先想清楚“预测什么”再动手
在外汇预测这个方向上,LSTM 几乎是入门必碰的模型——但大多数人下载完开源代码、跑通训练、看到一张拟合漂亮的 loss 曲线后,放上实盘就翻车。问题不出在 LSTM 本身,而出在大多数人没想明白一个问题:你究竟要预测下一根K线的收盘价,还是下一根K线的方向?这两个目标对应的标签、损失函数、评估方式完全不同,代码里的差别往往只有几行,效果却天差地别。
这篇笔记用一个基于 PyTorch 的 LSTM 外汇预测模型为主线,从数据怎么切、标签怎么造、模型怎么搭、训练怎么避坑,一直写到怎么用回测指标判断“这个模型到底有没有用”。同时我会把所有踩过的坑拆开来讲——包括数据泄漏、预测滞后、归一化陷阱这些在你跑通代码后大概率会遇到的问题。新手可以按步骤复现整个流程,老手可以直接跳到第 5 章看坑位。
2. 数据与标签构建:把汇率序列变成 LSTM 能学的样本
2.1 数据集选择:先解决“预测什么”,再谈“怎么预测”
常见做法是用主流货币对的小时线或日线数据,比如 EURUSD、GBPUSD、USDJPY。不要一上来就上分钟级数据,外汇 tick 数据的微观结构噪声很大,LSTM 在这种尺度上很难学到稳定规律,训练时间和过拟合风险却成倍增加。我一般建议先跑日线或 4 小时线,周期越大,信噪比越高,对验证模型思路更友好。
数据获取方面,yfinance 是个很实用的免费渠道。下面这段代码会拉取 EURUSD 近十年的日线数据,并做基础清洗:
import yfinance as yf import pandas as pd import numpy as np # 下载 EURUSD 日线数据,十年周期足够覆盖多轮趋势和震荡 df = yf.download("EURUSD=X", start="2013-01-01", end="2023-12-31", interval="1d") # 只保留收盘价,做预测任务用 Close 就够了 df = df[["Close"]].copy() df.columns = ["close"] # 缺失值处理:外汇市场周末休市,周一的 open 可能产生异常跳动,用前向填充 df = df.fillna(method="ffill") # 删除重复索引,防止数据源重复推送导致样本重叠 df = df[~df.index.duplicated(keep="last")] print(f"数据范围: {df.index[0]} ~ {df.index[-1]}, 共 {len(df)} 根K线")这段代码里有三个容易被忽略的点。第一,EURUSD=X表示欧元兑美元的现货汇率,X 后缀是 Yahoo Finance 的特定写法,换成JPY=X就是美元兑日元。第二,ffill填充对日线数据够用,但如果你换到分钟级别,需要把NaN行直接删除而不是填充,否则会把不存在的交易时段填出虚假价格。第三,重复索引在数据源多次拉取时非常常见,不去重的话同一根K线会被当成多个时间步喂给模型。
2.2 序列窗口化:把时间序列变成监督学习样本
LSTM 不能直接吃一根单独的价格,它需要的是一个连续窗口——用过去 N 根K线的价格来预测未来 M 个周期。这个 N 的取值是整个项目里最值得花时间调的参数。有人叫它“序列长度”,有人叫它“时间步长”,也有人叫“lookback window”,对应 LSTM 的sequence_length。太短,模型看不到趋势;太长,早期的信息已经被遗忘门过滤掉了,而且训练样本数量会急剧减少。
我惯用的窗口化函数是这样的:
def create_sequences(data, seq_len, pred_len): """ 将收盘价序列转换为监督学习的输入和标签 data: 一维价格序列 seq_len: 用过去多少根K线作为输入 pred_len: 预测未来多少根K线(回归任务中通常=1) """ X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data[i:i + seq_len]) y.append(data[i + seq_len:i + seq_len + pred_len]) return np.array(X), np.array(y) # 参数设定:过去 60 根日K预测未来 1 根 SEQ_LEN = 60 PRED_LEN = 1 close_values = df["close"].values.reshape(-1, 1) X, y = create_sequences(close_values, SEQ_LEN, PRED_LEN) print(f"输入张量形状: {X.shape}") # (样本数, 60, 1) print(f"标签张量形状: {y.shape}") # (样本数, 1, 1)窗口滑动的逻辑很直白:从第 0 根K线开始,取连续 60 根作为 X,第 61 根作为 y,然后窗口整体右移一格。这里有两个参数需要你特别关注。seq_len=60对日线数据来说大约是三个月的交易日数量,能覆盖一段中等长度的趋势;如果你做的是 4 小时线,60 根只代表 10 天,这时候建议加大到 120 或 240。pred_len在预测下一根K线时就是 1,如果你想做多步预测,把这个值调大,但要注意误差会随预测步长增加而快速累积——这个在第 5 章会详细讲。
2.3 数据切分:时间序列不能随机打乱
很多初学者会把数据集用train_test_split(X, y, test_size=0.2, shuffle=True)直接切分,这是时间序列任务里最严重的错误之一。外汇数据本质上是按时间排序的,一旦打乱,模型会把“未来的信息”和“过去的信息”混在一起学,训练出来的指标会虚高得失去参考意义。
正确的切法必须严格按时间顺序:
# 按时间顺序切分:前 80% 训练,后 20% 测试 train_size = int(len(X) * 0.8) X_train, X_test = X[:train_size], X[train_size:] y_train, y_test = y[:train_size], y[train_size:] # 关键:验证集要从训练集尾部切,而不是从中间随机切 val_size = int(len(X_train) * 0.15) X_val, y_val = X_train[-val_size:], y_train[-val_size:] X_train, y_train = X_train[:-val_size], y_train[:-val_size] print(f"训练集: {X_train.shape}, 验证集: {X_val.shape}, 测试集: {X_test.shape}")这个切分方式的原则是:验证集必须紧邻训练集的尾部,而不是从训练集中间抽出来。原因是外汇市场存在明显的 regime switching(市场状态切换),比如 2020 年疫情时期的波动率和 2022 年加息周期的波动率完全不同。如果验证集是从训练集随机抽取的,模型验证时看到的分布和它后续真正要预测的分布不一致,训练出来的早停模型就不具备实战参考价值。
3. PyTorch搭建LSTM外汇预测模型:三层结构与参数取舍
3.1 模型结构:双层 LSTM + Dropout + 回归头
在搭建具体模型前,需要明确一个认知:外汇预测本质是一个回归任务,不是分类任务。目标是在给定过去 60 根K线的条件下,预测下一根K线的收盘价。所以模型的最后一层必须是一个输出维度为 1 的线性层,而不是 softmax。
我常用的模型结构是双层 LSTM 加一个全连接回归头:
import torch import torch.nn as nn class ForexLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2): """ input_size: 每个时间步的特征维度,单变量预测时为 1 hidden_size: LSTM 隐藏层维度,决定模型记忆容量 num_layers: LSTM 堆叠层数,2 层可以学到更高层的时序抽象 dropout: 除最后一层外,其余层输出施加 dropout,防止过拟合 """ super(ForexLSTM, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, dropout=dropout, batch_first=True # 输入形状为 (batch, seq_len, features) ) # 回归头:将 LSTM 最后一个时间步的输出映射为预测价格 self.regressor = nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size // 2, 1) ) def forward(self, x): # x 形状: (batch, seq_len, input_size) lstm_out, (hidden, cell) = self.lstm(x) # 取最后一个时间步的输出 last_step = lstm_out[:, -1, :] # (batch, hidden_size) output = self.regressor(last_step) return output模型结构里最值得说明的是为什么取lstm_out[:, -1, :]而不是用hidden[-1]。lstm_out包含所有时间步的隐藏状态,取最后一个时间步意味着“看完整的 60 根K线后做出的判断”,这是回归任务的标准做法。而hidden[-1]只返回最后一层 LSTM 的最终隐藏状态,二者在大部分情况下等价,但在多层 LSTM 中,hidden[-1]只代表最后一层的顶层输出,语义上不如lstm_out[:, -1, :]直观。
3.2 参数为什么这样设:hidden_size、num_layers 和 dropout 的取舍逻辑
hidden_size=64对单变量时间序列来说是一个性价比很高的起点。小于 32,模型表达力不够,很难捕捉几十根K线内的趋势变化;大于 128,训练时间明显变长,而在单变量输入下并不会带来等比的精度提升。我做过对比实验,在 EURUSD 日线上,从 64 加到 128,RMSE 大概只改善 2% 左右,训练时间却翻了接近一倍。
num_layers=2也是一个折中。一层 LSTM 对外汇这种信噪比很低的数据往往学不到足够抽象的模式;三层以上在金融时序上收益递减,而且训练难度显著上升,梯度消失更明显,需要更多轮次才能收敛。两层是在“模型深度”和“可训练性”之间比较稳的一个平衡点,尤其适合新手建立基线。
dropout=0.2则是一个经验值。金融时间序列过拟合非常快,尤其是用 MSE 做损失时,模型很容易死记硬背住训练集里的噪声。Dropout 太低起不到正则化作用,太高会导致验证集 loss 不降甚至上升。你在调参时如果发现训练 loss 持续下降而验证 loss 在第 10 轮就开始反弹,优先尝试把 dropout 提到 0.3 或 0.4,这比降低 hidden_size 更有效。
3.3 如何处理多变量输入:加入技术指标特征
上面模型默认输入是单变量——只有收盘价。但实际做外汇预测时,绝大多数人会加入技术指标作为额外特征,比如 RSI、MACD、布林带位置。这时候不是改模型结构,而是改输入维度。
# 示例:构造一个包含 3 个特征的外汇数据集 def add_technical_features(df): """ 在收盘价基础上增加简单技术指标 这里使用 pandas 的 rolling 计算,避免引入额外库依赖 """ df = df.copy() # 5 周期和 20 周期均线,反映短中期趋势 df["ma5"] = df["close"].rolling(window=5).mean() df["ma20"] = df["close"].rolling(window=20).mean() # 简单动量:今日收盘与 5 日前收盘的差值 df["momentum"] = df["close"] - df["close"].shift(5) # 去掉 NaN 行,因为前面几个窗口无法计算均线和动量 df = df.dropna() return df # 用多列数据构建序列 feature_cols = ["close", "ma5", "ma20", "momentum"] full_data = df[feature_cols].values # 窗口化函数不变,但每个时间步的特征维度从 1 变成 4 # 调用 create_sequences(full_data, SEQ_LEN, PRED_LEN) 即可加了技术指标后,模型定义里的input_size要改成len(feature_cols),其余结构完全不用动。但必须提醒一句:不是特征越多越好。外汇价格本身已经是高度有效市场下的定价结果,你加入的特征如果是从同一个价格序列派生出来的(均线、MACD 本质上还是价格),它们的增量信息有限,反而会增加过拟合风险。更值得尝试的特征是不同品种之间的联动关系,比如同时输入 EURUSD 和 USDJPY 的价格走势来预测其中一个——这种跨品种信息在趋势共振行情里非常有用。
4. 训练与评估:归一化、滚动预测与回测指标怎么设
4.1 归一化方案:MinMaxScaler 的正确用法
LSTM 对输入数据的尺度非常敏感。外汇价格如果直接在原始数值上训练,loss 会被大数值主导,梯度更新不稳定。归一化几乎是必须的——但归一化也是最容易埋雷的地方,因为很多人不自觉地用整个数据集的取值范围来缩放训练集,这就造成了未来数据泄漏。
正确的做法是:只在训练集上计算缩放参数,然后用同一组参数去缩放验证集和测试集。
from sklearn.preprocessing import MinMaxScaler # 先把所有数据展平,方便统一缩放 all_values = df["close"].values.reshape(-1, 1) # 注意:这里只初始化 scaler,不调用 fit # 思路是先按训练集比例切分原始数据,再分别缩放 train_size = int(len(all_values) * 0.8) train_raw = all_values[:train_size] test_raw = all_values[train_size:] # 关键:只对训练集 fit,防止 min/max 受未来数据影响 scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_raw) # 测试集用训练集的参数做 transform,而不是重新 fit test_scaled = scaler.transform(test_raw) # 之后再用 train_scaled 和 test_scaled 分别做窗口化 X_train, y_train = create_sequences(train_scaled, SEQ_LEN, PRED_LEN) X_test, y_test = create_sequences(test_scaled, SEQ_LEN, PRED_LEN)这里最核心的纪律是:fit_transform只能出现在训练集上,测试集必须只做transform。如果测试集参与计算了 min 和 max,训练时模型就间接“见过”了未来价格的高低区间,测试指标会变得乐观到失真。很多下载来的开源代码在这个点上处理得很随意,你要自己逐一检查。
预测完成后要把结果反归一化回真实价格再来计算误差指标。反变换的代码很简单,用同一个 scaler 做inverse_transform即可,但要注意形状匹配。
4.2 训练循环:梯度裁剪与早停
训练 LSTM 和训练普通前馈网络有一个重要区别:LSTM 在时间步上的展开很容易产生梯度爆炸。外汇数据波动率大,某些极端K线(比如央行决议后的瞬间跳空)会让 loss 出现尖峰,反向传播的梯度会呈指数级放大。所以在每个 batch 的 loss 反向传播之后,我一般会加一个梯度裁剪:
import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # 转换为 PyTorch 张量,float32 是 LSTM 的默认精度 X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32) X_val_t = torch.tensor(X_val, dtype=torch.float32) y_val_t = torch.tensor(y_val, dtype=torch.float32) # 构建 DataLoader,batch_size=64 是外汇日线上的常见选择 train_dataset = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_dataset = TensorDataset(X_val_t, y_val_t) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False) # 初始化模型与优化器 model = ForexLSTM(input_size=1, hidden_size=64, num_layers=2, dropout=0.2) optimizer = optim.Adam(model.parameters(), lr=0.001) # MSE 适合连续价格回归,不采用 MAE 是因为其梯度在零点不可导,训练更不稳定 criterion = nn.MSELoss() # 训练轮次:60 轮是日线数据的一个常见起点 epochs = 60 best_val_loss = float("inf") for epoch in range(epochs): model.train() train_loss = 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() outputs = model(batch_X) # y 的形状是 (batch, 1, 1),需要压缩为 (batch, 1) loss = criterion(outputs, batch_y.squeeze(1)) loss.backward() # 梯度裁剪:max_norm=1.0 表示梯度的 L2 范数上限为 1 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * batch_X.size(0) train_loss /= len(train_dataset) # 验证集上评估,同样要确保模型处于 eval 模式,关闭 dropout model.eval() val_loss = 0.0 with torch.no_grad(): for batch_X, batch_y in val_loader: outputs = model(batch_X) loss = criterion(outputs, batch_y.squeeze(1)) val_loss += loss.item() * batch_X.size(0) val_loss /= len(val_dataset) # 记录验证集效果最好的模型,作为最终的模型参数 if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pt") if epoch % 5 == 0: print(f"Epoch {epoch}: Train Loss={train_loss:.6f}, Val Loss={val_loss:.6f}")clip_grad_norm_(max_norm=1.0)这一行在整个训练中的地位很关键。在实际项目中,我见过太多人删掉这行后 loss 在中途变成NaN。原因就是某个 batch 的梯度爆炸导致权重更新跨度过大。梯度裁剪的值建议在 0.5 到 5.0 之间尝试,1.0 是一个比较稳妥的起点——太小会让模型收敛变慢,太大就失去了裁剪的意义。
batch_size=64对日线十年数据来说是合理的。如果用的是分钟级数据,可以加大到 128 或 256;如果数据量较小,可以考虑 32。需要留意的是shuffle=True在训练集的 DataLoader 中是可以的,因为窗口化后的样本已经是按时间构造完好的,打乱的是样本之间的先后顺序,不影响每个样本内部的时间结构——这个和前面 2.3 节说的“不能打乱原始时间序列”是两码事,不要混淆。
4.3 评估指标:RMSE 和方向准确率缺一不可
训练结束后,大部分人第一件事就是看测试集上的 RMSE,然后发现数值“看起来很准”,觉得自己模型不错。这是典型的被单一指标误导。RMSE 低只能说明预测价格和实际价格的欧氏距离小,但外汇交易关心的不是价格差了多少,而是方向判断对不对——你做多 EURUSD,只要方向涨,哪怕预测价低了 30 个点,这笔交易还是赚钱的。
所以我评估模型时固定看两个指标:RMSE 衡量回归精度,方向准确率(Directional Accuracy)衡量交易价值。
# 测试集预测 model.eval() with torch.no_grad(): X_test_t = torch.tensor(X_test, dtype=torch.float32) y_pred_scaled = model(X_test_t).numpy() # 反归一化回原始价格尺度 y_pred = scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)) y_true = scaler.inverse_transform(y_test.reshape(-1, 1)) # 指标1:RMSE,衡量预测价格与真实价格的偏差 rmse = np.sqrt(np.mean((y_pred - y_true) ** 2)) print(f"RMSE: {rmse:.5f}") # 指标2:方向准确率,判断预测的涨跌方向是否和真实一致 # 注意:需要把预测值和真实值都滞后一位,与前一交易日收盘价比较 prev_close = y_true[:-1] pred_diff = np.sign(y_pred[1:] - y_true[:-1]) # 修正:预测值与上一时刻真实值比较 true_diff = np.sign(y_true[1:] - y_true[:-1]) directional_acc = np.mean(pred_diff == true_diff) print(f"Directional Accuracy: {directional_acc:.2%}")这里方向准确率的计算有一个容易搞错的地方。y_pred[1:]是第 1 根到最后一根的预测值,应该和y_true[:-1](即第 0 根到倒数第二根的真实值)比较方向变化。也就是说,判断的是“模型预测明天比今天涨还是跌”。如果你拿y_pred直接和y_true逐元素比较,得到的是“预测的绝对价格是否接近真实价格”,而不是方向。
随机猜测的方向准确率是 50%。一个 LSTM 模型在 EURUSD 日线上如果能做到 52%~55%,虽然听起来不高,但在扣掉点差和手续费后可能已经接近盈亏平衡的临界线。如果连 52% 都不到,这个模型直接用于实盘交易是没有意义的——这就是为什么方向准确率比 RMSE 更能说明问题。
5. 避坑:LSTM外汇预测的7个实际问题与排查路径
5.1 预测结果整体滞后,看上去拟合完美但其实没用
这是 LSTM 做时间序列预测最经典的翻车现场。你把预测值和真实值画在同一张图上,发现两条曲线几乎重合,只是预测曲线整体右移了一格,视觉上非常漂亮,loss 也很低。这时候如果你的方向准确率只有 50% 左右,基本可以断定模型学到的只是“把昨天的价格当成今天的预测”——因为金融序列接近随机游走,最佳回归策略就是预测值约等于上一个观测值。
原因在于:LSTM 在 MSE 损失下发现“复制上一个时间步的输出”能最小化误差,因为相邻两日收盘价的差通常远小于价格本身的波动幅度。解决的办法不是换模型,而是换标签:把预测目标从“收盘价”改为“收益率”或“价格变化量”,即y[i] = close[i+1] - close[i]。这样模型无法通过复制上一时刻来最小化损失,必须真正学习涨跌规律。
5.2 归一化时把测试集的最小最大值混进了训练集
当你看到测试集上的 RMSE 异常低、低到几乎和训练集一样,先检查两个不同数据集的 min/max 是否被分别计算了。常见错误是:在全量数据集上做fit_transform,然后再手动按索引切分。这样一来,测试集的价格区间泄漏给了训练过程,测试指标没有任何参考价值。正确做法在 4.1 节已经讲过了——scaler 必须只fit在训练集上。
5.3 训练到一半 loss 突然变成 NaN
训练过程中 loss 跳成NaN是 LSTM 外汇预测的高频问题。首要原因是梯度爆炸,尤其在外汇数据里有极端行情(如闪崩、央行突然加息)时,单个K线的异常大波动会放大梯度。解法是在每次loss.backward()后增加梯度裁剪。如果加了裁剪还是出现NaN,检查学习率——lr=0.001对 Adam 是常见配置,但如果你把模型加深到 3 层以上,这个学习率可能需要降到0.0005。
5.4 验证集 loss 在下降,但测试集上方向准确率不升反降
这说明发生了过拟合,模型开始死记训练集里的噪声。Dropout 没起作用或作用不够是首要嫌疑。检查模型在train和eval模式之间切换是否正确——如果推理时遗漏了model.eval(),Dropout 仍然处于激活状态,预测结果会带上随机性。如果确认开关没问题,把 Dropout 从 0.2 提高到 0.3 或 0.4,同时把训练轮次减少三分之一。
5.5 多步预测时误差滚雪球
如果你把PRED_LEN设为 5 或 10,做了多步预测,会发现预测越远越不准,到第 10 步时几乎变成一条水平线。原因很朴素:多步预测的每一步都会把上一步的预测值当作输入,误差在递归过程里被不断放大,最终收敛到一个“平均价格”附近。解决思路有两个:一是用教师强迫(teacher forcing),训练时喂真实值、推理时喂预测值,但推理误差仍然存在;二是干脆放弃多步预测,滚动采用单步预测——每预测完一根K线,把真实值加入窗口再预测下一根。后者在实践中更稳定。
5.6 换一个货币对后模型全部失灵
在 EURUSD 上训练好的模型参数,直接套到 USDJPY 或 GBPJPY 上,方向准确率往往掉回 50% 以下。原因很简单:不同货币对的波动特征、趋势持续性和均值回归强度差异很大,USDJPY 长期受利差驱动,趋势性强;EURUSD 更偏震荡。这不是模型结构的问题,而是你需要针对每个品种重新做窗口长度选择和归一化。更务实的做法是:把多品种数据合并训练一个模型,输入中加入品种标识特征,让模型自己学到不同品种的行为模式差异。
5.7 数据里有未来函数而不自知
这是最隐蔽的坑。很多人使用 TA-Lib 之类的高效库计算的技术指标来构造特征,但这些特征里包含了未来K线的信息。举个例子,你计算当天的 RSI 时用了 14 根K线的窗口,如果你的数据对齐方式有误,第 t 根K线的特征实际用了第 t+1 根K线的数据——这种情况在特征工程和序列窗口化的衔接处非常容易出现。排查方法是:构造完特征后,手动检查某一行的特征值和对应时间戳的K线数据范围是否匹配,同时对每个特征做shift(1)对齐到“当前K线收盘后可知”的状态。
6. 验证模型有没有用:残差分析、阈值过滤与稳健性检查
训练完模型只是第一步,投入精力验证“模型是否真的学到了规律”才是决定这个项目值不值得做下去的关键。很多人在这里就停了,拿着训练集上的漂亮曲线去写结论,结果实盘一跑就露馅。我一般会做三层验证。
第一层是残差分析。计算测试集上的残差r = y_true - y_pred,然后检查残差的自相关性。理想情况下,残差应该是白噪声——也就是说,模型已经捕获了价格序列中所有可利用的线性规律,剩下的只是不可预测的市场噪声。如果残差序列有明显的自相关(比如连续多日为正,然后连续多日为负),说明模型遗漏了某些模式,值得回去调窗口长度或增加特征。用 Python 的pandas.plotting.autocorrelation_plot就能快速判断。
第二层是阈值过滤。原始的预测结果中,模型所有时刻都会给出一个方向判断,但有些判断模型本身“没有信心”——预测价格和当前价格只差 0.0001,方向随机性很高。如果你不加筛选地全部拿来交易,预测方向和真实方向的对齐率会被这类模糊信号稀释。常见的做法是设置一个最小变化阈值,只有当abs(pred_price - current_price) / current_price超过某个百分比(比如 0.1%)时才生成交易信号。做过阈值过滤后,参与交易的样本数量虽然减少,但方向准确率通常会显著提高,这正是从“预测模型”走向“交易策略”的关键一步。
第三层是滚动重训测试。把时间窗口整体向后平移,比如用 2015-2020 年训练、2021 年测试,再用 2016-2021 年训练、2022 年测试,观察方向准确率是否稳定。单一固定时间段的效果好很可能是那一段行情特别适合你的模型,如果换一个时间窗口准确率大幅波动,说明模型的泛化能力还远远不够。
我现在的习惯是:任何 LSTM 外汇预测项目上线之前,先跑三层验证,再定义交易规则;验证不达标的模型直接放弃,不浪费时间调参。这么做之后最大的收获是,我不再会被一张拟合完美的曲线图误导。预测模型的最终评判标准不是图好不好看、loss 低不低,而是方向准确率能不能稳定超过 52% 并且维持到滚动测试中。希望这份实战拆解能让你少走一些我走过的弯路,也希望你手里的那份源码,最终不只是一堆能跑通的代码,而是一套经得起回测检验的决策系统。
本文还有配套的精品资源,点击获取