数据科学和机器学习(第 38 部分):外汇市场中的 AI 迁移学习(基础篇)
「把预训练模型搬进外汇行情」
迁移学习的核心思路,是把在一个任务上训练好的模型参数,作为新任务的初始权重,而不是从零开始拟合。对外汇交易者来说,这意味着可以用股指或大宗商品上训过的网络,去初始化 EURUSD 的判别模型,省下大量样本与算力。 具体流程分两步:先在一个基础数据集上完成预训练,得到一组权重;再拿目标品种(如 XAUUSD 小时图)的小样本做微调。MT5 的 Python API 或内置 ONNX 推理,都能把这一步接进 EA。 迁移学习的主要优势是冷启动快、对小样本更友好。但外汇与贵金属属高杠杆高风险品种,迁移过来的模型在历史样本上表现好,不代表实盘概率优势必然延续,回测吻合度仍需用样本外数据复核。 一个朴素的基础模型可以是两层全连接网络,输入为标准化后的收益率序列,输出多空概率。连续变量直接喂入网络会带来量纲失衡,所以归一化不是可选项,而是前置硬约束。
把现成模型搬进金融市场的可行性
迁移学习的本质,是把一个已在某任务上训练好的模型改造为新任务的基础,而不是从零开始重训。这在金融场景里很现实:当你只有少量标注样本、或重训成本过高、且新任务与原始任务有结构相似性时,直接复用预训练模型的知识并做针对性优调,往往更省力。 一个直观例子是图像分类:若你只有 1000 张猫狗图,从零训练深度卷积网络很吃力;改用 ImageNet(百万级图像、1000 类)上预训练好的 ResNet50 或 VGG16,把卷积层当特征提取器,只重训自定义分类层,就能在小数据集上跑通。 金融市场也符合这个逻辑。不同品种虽代表不同资产,但多数时候受供需驱动,行为相似:烛条形态接近、指标在不同品种上展现类似形状。这也是为什么交易者常先在单一品种上打磨技术分析方法,再跨市场套用,而不管价格量级差多少。 但机器学习模型本身不会自动意识到这些市场可比。后续我们会拆解如何用迁移学习让模型识别跨品种的相似形态,以及落地时的利弊与注意点。
◍ 把旧模型的特征层搬来用
迁移学习本质上是在任务 A 上训好的模型,把学到的通用特征直接复用到任务 B。两个任务不必一致,只要市场微观结构相关,就能省下从头采集和训练的时间,且在小样本下往往比冷启动表现更稳。 在 MT5 里最典型的做法是:取一个已在 EURUSD 的某突破策略上训好的神经网络,保留它前几层卷积或循环单元——这些层捕捉的是波动聚类、影线比例这类跨品种共通形态,而非具体品种记忆。 随后只对新品种(如 XAUUSD)微调靠后的层或最后的全连接权重。外汇与贵金属杠杆高、跳空频繁,迁移后务必用 2020—2023 年分段回测验证,避免把 EURUSD 的低波动习性错误带进黄金的高波动区。可能的话,先冻结前层只训头部 10 个 epoch 观察过拟合曲线再决定解冻范围。
「迁移学习在 MT5 模型里的实在收益」
从零训练一个深度模型,在 MT5 上跑 EURUSD 的 1 分钟序列,动辄要算上数小时甚至数天;直接复用预训练特征,这个时间能砍掉一大截,对盯盘迭代来说是实打实的差別。 标记数据不够时,迁移学习倾向给出更好的表现。预训练模型已经会抓交易设置、指标形态这类关键信号,换到新任务上做决策更聪明,而不是瞎拟合噪声。 MT5 里不少贵金属或小众品种的历史数据又少又脏,硬训必过拟合。拿大样本集训好的底座来迁,能在数据薄的时候也撑住泛化,过度拟合的概率明显下降。 技术面行为跨品种常常相似,没必要 XAUUSD 训一个、GBPJPY 又训一个。共享底座省时间,也让多品种信号逻辑保持一致。外汇与贵金属杠杆高、滑点大,任何模型结论都只是概率倾向,上线前务必用 MT5 历史回放自测。
用裸 OHLC 跑出的随机森林只是抛硬币
先搭一个最朴素的随机森林分类器当基线:直接用各品种日线 OHLC 四个连续数值,不做任何加工。训练集覆盖 EURUSD、GBPUSD 等 7 个主要直盘,再混入 XAUUSD、XAUEUR 等金属交叉盘,时间窗拉到 2005–2023。 目标变量按最直白的逻辑打标:下一根日线收盘价高于当前收盘价记为看涨,低于则看跌。getXandY 函数在 Python 侧读入 CSV 后,用 shift(-1) 取出 future_close 再删空值,标签就出来了。 先用 EURUSD 训模型,再丢到其余品种上测。结果很诚实:训练品种准确率 0.54,未训练品种落在 0.48–0.50。外汇与贵金属属高杠杆高风险品种,这种跨品种泛化差异极小反而说明模型没学到结构性规律。
- 5 上下的胜率本质上就是抛硬币——连续型 OHLC 原始值作为特征,尺度随价格漂移,模型很难抓到跨品种通用的边界。下面这段 MT5 脚本负责把多品种日线 OHLC 落盘成 CSV,是后面所有实验的原材料。
class="macro">#include <pandas.mqh> class=class="str">"cmt">//[MQL5官方文档] input class="type">class="kw">datetime start_date = D&class="macro">#x27;class="num">2005.01.class="num">01&class="macro">#x27;; input class="type">class="kw">datetime end_date = D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;; input class="type">class="kw">string symbols = "EURUSD|GBPUSD|AUDUSD|USDCAD|USDJPY|USDCHF|NZDUSD|EURNZD|AUDNZD|GBPNZD|NZDCHF|NZDJPY|NZDCAD|XAUUSD|XAUJPY|XAUEUR|XAUGBP"; input ENUM_TIMEFRAMES timeframe = PERIOD_D1; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">string SymbolsArr[]; class="type">class="kw">ushort sep = StringGetCharacter("|",class="num">0); if (StringSplit(symbols, sep, SymbolsArr)<class="num">0) { printf("%s failed to split the symbols, Error %d",__FUNCTION__,GetLastError()); class="kw">return; } class=class="str">"cmt">//--- vector open, high, low, close; for (class="type">uint i=class="num">0; i<SymbolsArr.Size(); i++) { class="type">class="kw">string symbol = SymbolsArr[i]; if (!SymbolSelect(symbol, true)) { printf("%s failed to select symbol %s, Error = %d",__FUNCTION__,symbol,GetLastError()); class="kw">continue; } class=class="str">"cmt">//--- open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date); high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date); low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date); close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date); CDataFrame df; df.insert("Open", open); df.insert("High", high); df.insert("Low", low); df.insert("Close", close); df.to_csv(StringFormat("Fxdata.%s.%s.csv",symbol,EnumToString(timeframe)), true); } } def getXandY(symbol: str, timeframe: str, lookahead: class="type">int) -> tuple: df = pd.read_csv(f"/kaggle/input/ohlc-eurusd/Fxdata.{symbol}.{timeframe}.csv") # Target variable df["future_close"] = df["Close"].shift(-lookahead) df.dropna(inplace=True)
◍ 用 EURUSD 训出的模型去跑其他品种会怎样
把标签定义为「下一根 H4 收盘价高于当前收盘价」后,特征矩阵 X 剔除了 future_close 与 Signal,仅留行情衍生量;y 就是 0/1 方向标签。训练函数用 RandomForest(n_estimators=100,min_samples_split=3,max_depth=5)套 RobustScaler 管线,在 EURUSD H4 上 fit 完拿去横评其余 16 个品种。 实测准确率很直白:EURUSD 自身 0.548,GBPUSD 0.501,AUDUSD 0.503,USDCAD 0.497,USDJPY 0.495,XAUUSD 0.487,XAUJPY 0.482。除训练集外,没有一个品种显著脱离 0.5 随机线。 这说明单币种 H4 动量标签训出的树模型,迁移到别的直盘或黄金交叉盘,大概率只是抛硬币。外汇与贵金属杠杆高、跳空频繁,拿这种泛化力直接上实盘风险极高,最多当 MT5 里复算一遍的基准实验。
df["Signal"] = (df["future_close"] > df["Close"]).astype(class="type">int) # Splitting data into X and y X = df.drop(columns=[ "future_close", "Signal" ]) y = df["Signal"] class="kw">return (X, y) def trainSymbol(X_train: pd.DataFrame, y_train: pd.DataFrame) -> Pipeline: # Training a model classifier = RandomForestClassifier(n_estimators=class="num">100, min_samples_split=class="num">3, max_depth = class="num">5) pipeline = Pipeline([ ("scaler", RobustScaler()), ("classifier", classifier) ]) pipeline.fit(X_train, y_train) class="kw">return pipeline def evalSymbol(model: Pipeline, X: pd.DataFrame , y: pd.Series) -> class="type">int: # evaluating the model preds = model.predict(X) acc = accuracy_score(y, preds) class="kw">return acc symbols = ["EURUSD","GBPUSD","AUDUSD","USDCAD","USDJPY","USDCHF","NZDUSD","EURNZD","AUDNZD","GBPNZD","NZDCHF","NZDJPY","NZDCAD","XAUUSD","XAUJPY","XAUEUR","XAUGBP"] # training on EURUSD lookahead = class="num">1 X, y = getXandY(symbol=symbols[class="num">0], timeframe="PERIOD_H4", lookahead=lookahead) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=class="num">42, shuffle=True) model = trainSymbol(X_train, y_train) # Evaluating on the rest of symbols trained_symbol = symbols[class="num">0] print(f"Trained on {trained_symbol}") for symbol in symbols: X, y = getXandY(symbol=symbol, timeframe="PERIOD_H4", lookahead=class="num">1) acc = evalSymbol(model, X, y) print(f"--> {symbol} | acc: {acc}") Trained on EURUSD --> EURUSD | acc: class="num">0.5478518727715607 --> GBPUSD | acc: class="num">0.5009182736455464 --> AUDUSD | acc: class="num">0.5026133634694165 --> USDCAD | acc: class="num">0.4973701860284514 --> USDJPY | acc: class="num">0.49477401129943505 --> USDCHF | acc: class="num">0.5078731817539895 --> NZDUSD | acc: class="num">0.4976826463824518 --> EURNZD | acc: class="num">0.5071507150715071 --> AUDNZD | acc: class="num">0.5005597760895641 --> GBPNZD | acc: class="num">0.503459397596629 --> NZDCHF | acc: class="num">0.4990389436737423 --> NZDJPY | acc: class="num">0.4908841561794127 --> NZDCAD | acc: class="num">0.5023507681974645 --> XAUUSD | acc: class="num">0.48674396277970605 --> XAUJPY | acc: class="num">0.4816082121471343 --> XAUEUR | acc: class="num">0.4925268155442237 --> XAUGBP | acc: class="num">0.49455864570737607