与偏颇的金融市场打交道:看清不平衡数据如何误导你的ML模型(基础篇)
(1/3)·外汇与贵金属市场历史多空分布天然失衡,直接用原始数据训模型会埋下哪些隐患
不少交易者把几千根日线丢进机器学习框架,默认收盘价涨跌各半,结果模型在USDJPY上永远喊多。这种隐性偏颇不会报错,却会让次要方向的信号被系统性忽略,回测好看实盘吃瘪。
为什么金融数据天生就偏
做 MT5 上的行情建模时,最先撞上的墙不是算法,而是数据分布本身。价格在大多数时间里处于区间纠缠,真正出现单边突破或趋势反转的样本极少,这就导致目标变量从一开始就不平衡。 以一篇 2026 年 2 月发布的系列教程为例,它把内容拆成概述、不平衡变量不足、处理技术、评估量值、测试 EA、过抽样、欠抽样、混合方法和结束语共 9 个板块,说明这类偏颇数据问题足够撑起一整条学习链路,而不是顺手调个参数就能忽略。 如果你直接拿原始 tick 或裸 K 线去训练分类器,模型会倾向于永远预测「无信号」——因为在训练集里这是准确率最高的偷懒解。外汇与贵金属杠杆高、跳空频繁,这种偏颇被放大后,实盘可能连续踩空。开 MT5 导一段 EURUSD 的 M1 数据,数数其中突破前高后的可持续行情占比,你大概就能直观看到偏到了什么程度。
◍ 训练数据里的多空失衡陷阱
把 EURUSD、XAUUSD、USDJPY 等七个品种拉出日线最近 1000 根柱线,按收盘价高于开盘价记 1(看涨)、否则记 0(看跌),没有任何一个品种是完美五五开。EURUSD 是 496 对 504,XAUUSD 是 472 对 528,而 USDJPY 直接走到 408 对 592——看涨比看跌多出 184 根,占比 59.2%。 这种历史偏颇在喂给机器学习模型时会变成隐蔽坑。模型目标通常是最小化损失并最大化准确率,当 USDJPY 数据里 59.2% 本是看涨,它就会倾向把预测全压在看涨类上:反正闭眼猜涨也有约 59.2% 命中率,是条最省力的捷径。 但外汇和贵金属属高风险市场,过去分布绝不等于未来重演。模型若因类别失衡忽视那 40.8% 的看跌信号,实盘里遇到反转就会集体失聪。下面这段 Python 是上面各品种柱线类型统计的原文代码,可在本地复跑验证。
class="kw">import pandas as pd class="kw">import numpy as np symbols = [ "EURUSD", "USTEC", "XAUUSD", "USDJPY", "BTCUSD", "CA60", "UK100" ] for symbol in symbols: df = pd.read_csv(fr"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\1640F6577B1C4EC659BF41EA9F6C38ED\MQL5\Files\{symbol}.PERIOD_D1.data.csv") df["Candle type"] = (df["Close"] > df["Open"]).astype(class="type">int) print(f"{symbol}(unique):",np.unique(df["Candle type"], return_counts=True)) EURUSD(unique): (array([class="num">0, class="num">1]), array([class="num">496, class="num">504])) USTEC(unique): (array([class="num">0, class="num">1]), array([class="num">472, class="num">528])) XAUUSD(unique): (array([class="num">0, class="num">1]), array([class="num">472, class="num">528])) USDJPY(unique): (array([class="num">0, class="num">1]), array([class="num">408, class="num">592])) BTCUSD(unique): (array([class="num">0, class="num">1]), array([class="num">478, class="num">522])) CA60(unique): (array([class="num">0, class="num">1]), array([class="num">470, class="num">530])) UK100(unique): (array([class="num">0, class="num">1]), array([class="num">463, class="num">537]))
「少数类被准确率掩盖的坑」
做分类模型时,优化整体准确率会把预测推往主类。以欺诈检测为例,99% 为非欺诈、1% 为欺诈的样本里,模型全程猜「非欺诈」就能拿到 99% 准确率,但欺诈识别实际完全失效。外汇与贵金属信号分类同样高危,行情里反转样本远少于延续样本,照搬准确率会让你误以为模型能盯盘。 评估量值也会骗人。一个联动准确率 72% 的模型,可能某一类命中 95%、另一类仅 50%,平均后看着还行,细分却已崩坏。MT5 里跑完模型别只盯总准确率,按类拆开看命中率才是正经事。 模型还会过度拟合主类噪声。医学诊断中 95% 健康、5% 患病的集合,模型可能直接丢掉患病样本规律。真实市场分布说变就变,拿失衡环境训出来的东西,遇到风格切换很快溃败。开 MT5 用历史 Tick 复训前,先确认各类样本比例是否贴近你打算交易的品种波动结构。
准确率骗了你:USDJPY 上的偏类报告怎么读
在不平衡分类里,整体准确率是最容易误导人的指标。假设模型在 USDJPY 日线样本上总准确率 0.74,看着还行,但按类拆开就会发现两个类别的识别质量严重撕裂。 我们在偏颇的 USDJPY D1 数据上训了一个 100 棵树的随机森林,训练后分类报告显示:类 0 精度 0.98、唤醒率仅 0.41、F1 0.57;类 1 精度 0.68、唤醒率 1.00、F1 0.81。类 1 的唤醒率拉满反而是一个危险信号,模型几乎把一切预测成类 1,漏掉大量真实类 0 样本。 F1 分数是精度与唤醒率的谐波平均,任一边塌了它都会掉。假阳堆高会压低精度进而拉低 F1;假阴堆高则唤醒率和 F1 同跌。所以看偏类模型,先盯 F1 和各类唤醒,别被总准确率糊弄。 下面这段代码就是生成上述报告并导出 ONNX 的训练过程,可直接在本地 python 环境跑(路径按你本机改)。外汇与贵金属波动剧烈、模型易过拟合,实盘前务必小资金验证。
class="kw">import pandas as pd class="kw">import numpy as np from sklearn.model_selection class="kw">import train_test_split from sklearn.ensemble class="kw">import RandomForestClassifier from sklearn.metrics class="kw">import classification_report # Global variables symbol = "USDJPY" timeframe = "PERIOD_D1" lookahead = class="num">1 common_path = r"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\Common\Files" df = pd.read_csv(f"{common_path}\{symbol}.{timeframe}.data.csv") # Target variable df["future_close"] = df["Close"].shift(-lookahead) # future closing price based on lookahead value df.dropna(inplace=True) df["Signal"] = (df["future_close"] > df["Close"]).astype(class="type">int) print("Signals(unique): ",np.unique(df["Signal"], return_counts=True)) X = df.drop(columns=["Signal", "future_close"]) y = df["Signal"] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=class="num">0.3, random_state=class="num">42, shuffle=False) model = RandomForestClassifier(n_estimators=class="num">100, max_depth=class="num">5, min_samples_split=class="num">3, random_state=class="num">42) model.fit(X_train, y_train) from skl2onnx class="kw">import convert_sklearn from skl2onnx.common.data_types class="kw">import FloatTensorType class="kw">import os def saveModel(model, n_features: class="type">int, technique_name: str): initial_type = [("input", FloatTensorType([None, n_features]))] onnx_model = convert_sklearn(model, initial_types=initial_type, target_opset=class="num">14) with open(os.path.join(common_path, f"{symbol}.{timeframe}.{technique_name}.onnx"), "wb") as f: f.write(onnx_model.SerializeToString()) saveModel(model=model, n_features=X_train.shape[class="num">1], technique_name="no-sampling") Train Classification report precision recall f1-score support class="num">0 class="num">0.98 class="num">0.41 class="num">0.57 class="num">158 class="num">1 class="num">0.68 class="num">1.00 class="num">0.81 class="num">204
◍ 把分类报告数字翻成实盘含义
上面那张混淆矩阵后的汇总表,给出了 362 个样本上的三类评估。accuracy 落在 0.74,说明整体约四分之一的判别被错分,外汇与贵金属行情里这已经足够提醒你别纯靠模型信号裸奔。 macro avg 的 precision 0.83、recall 0.70、f1 0.69,意味着各类别被平等加权后,抓对的比例明显低于标对的比例——模型更容易在出现信号时乱报,漏掉的真实形态比误报还多。 weighted avg 的 precision 0.81、recall 0.74、f1 0.71,是按样本量倾斜后的结果,和 macro 的差距说明少数类样本拉低了平均表现。接这套逻辑进 MT5 回测时,先把样本数调到 362 以上再看 f1,否则小样本下的 0.7 可能只是噪声。