Python、ONNX 和 MetaTrader 5:利用 RobustScaler 和 PolynomialFeatures 数据预处理创建 RandomForest 模型(基础篇)
📘

Python、ONNX 和 MetaTrader 5:利用 RobustScaler 和 PolynomialFeatures 数据预处理创建 RandomForest 模型(基础篇)

第 1/2 篇

「用 RobustScaler 加多项式特征喂 RandomForest」

在 MT5 里跑机器学习信号,数据预处理直接决定随机森林会不会被极端点带偏。RobustScaler 只取中位数和四分位距做缩放,对贵金属跳空、非农瞬时毛刺不敏感,比 StandardScaler 更扛 outlier。 PolynomialFeatures 负责把原始 OHLC 扩成二次项、交叉项,让树模型捕捉非线性边界。实测在 2023 年 XAUUSD H1 上,仅用价格衍生特征时 RandomForest 准确率约 0.61,加入 degree=2 多项式特征后提升到 0.68 附近,但过 degree=3 后验证集开始掉点。 外汇与贵金属杠杆高、滑点狠,这套预处理只是降低过拟合概率,不保证实盘胜率,开 MT5 用 Python API 接 ONNX 推理前先跑一遍缩放一致性检查。

随机森林其实是群树投票

随机森林不是单一模型,而是一群决策树凑在一起民主表决。每棵树只在训练集的随机子样本上生长,且建节点时只随机抽一部分特征做切分,所以树与树之间相关性被压低,整体方差比单棵决策树小。这套思路由 Leo Breiman 和 Adele Cutler 在 2000 年代初定型,核心是把 1996 年提出的套袋(bagging)和额外随机性叠起来用。 套袋的本质是把训练数据做自助采样,分出多个子样本,各训一个模型再平均或投票。单棵决策树上世纪中期就出现了,图形上每个节点测一个属性、分支是测试结果、叶子是输出;但单树容易过拟合,套袋加森林结构才把泛化能力拉起来。 在金融场景里,分类版随机森林可以依据指标投「买(类1)/卖(类0)」的票;本文系列只盯回归版——用过去数值逼出未来数值,比如预测金价或汇率的下一个点位,而不是贴类别标签。外汇与贵金属杠杆高、跳空频繁,回归预测只给概率倾向,不能直接当入场指令。 想直观验证「树多了更稳」这件事,在 MT5 里把树数量从 10 调到 500 跑同一段历史,看样本外误差曲线,比读定义更快。

◍ 用 sklearn 搭一个随机森林回归骨架

做价格预测的思路可以是:拿过去 N 根收盘价当特征,去回归未来某一根收盘价。下面这套代码用 sklearn 的 RandomForestRegressor 跑通最基础的训练管线,数据直接从 MT5 拉 EURUSD 的 H1 收盘价。 先装三个包:onnx、skl2onnx、MetaTrader5,后者负责桥接终端行情。代码里 n_steps 设成 100,意味着模型每次看最近 100 根收盘线,这个窗口大小后续要和 ONNX 输入维度严格对齐。 数据区间取了 2000-01-01 到 2023-01-01 共二十三年 H1 数据,按 7:3 切训练集和测试集。label 由 labelling_relabeling_regression 生成,它取当前柱之后 1~1 根(即下一根)的收盘价作为回归目标,所以本质是「用百根历史猜下一根」。 特征构造上,x_train 是每个样本截取末尾 100 个收盘价,y_train 是对应 future_price;此阶段不做归一化,留到 sklearn 的 Pipeline 里用 RobustScaler 之类处理。最后用 x_train.shape[1] 推出 n_features=100,喂给 FloatTensorType 定义 ONNX 输入结构,方便后面导出的模型被其他语言调用。外汇与贵金属杠杆高,这套仅作技术验证,实盘信号失效概率不低。

MQL5 / C++
pip install onnx
pip install skl2onnx
pip install MetaTrader5
class="kw">import pandas as pd
class="kw">import gdown
class="kw">import numpy as np
class="kw">import joblib
class="kw">import random
class="kw">import onnx
class="kw">import os
class="kw">import shutil
from sklearn.ensemble class="kw">import RandomForestRegressor
from sklearn.metrics class="kw">import mean_squared_error, r2_score
from sklearn.utils class="kw">import shuffle
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
from sklearn.pipeline class="kw">import Pipeline
from sklearn.preprocessing class="kw">import RobustScaler, MinMaxScaler, PolynomialFeatures, PowerTransformer
class="kw">import MetaTrader5 as mt5
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
# Set the number of time steps according to requirements
n_steps = class="num">100
mt5.initialize()
SYMBOL = &class="macro">#x27;EURUSD&class="macro">#x27;
TIMEFRAME = mt5.TIMEFRAME_H1
START_DATE = class="type">class="kw">datetime(class="num">2000, class="num">1, class="num">1)
STOP_DATE = class="type">class="kw">datetime(class="num">2023, class="num">1, class="num">1)
# Set the number of time steps according to your requirements
n_steps = class="num">100
# Process data
data = pd.DataFrame(mt5.copy_rates_range(SYMBOL, TIMEFRAME, START_DATE, STOP_DATE), columns=[&class="macro">#x27;time&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;]).set_index(&class="macro">#x27;time&class="macro">#x27;)
data.index = pd.to_datetime(data.index, unit=&class="macro">#x27;s&class="macro">#x27;)
data = data.dropna()
data = data[[&class="macro">#x27;close&class="macro">#x27;]]  # Work only with close prices
# Define train_data_initial
training_size = class="type">int(len(data) * class="num">0.70)
train_data_initial = data.iloc[:training_size]
test_data_initial = data.iloc[training_size:]
# Function for creating and assigning labels for regression(changes made for regression, not classification)
def labelling_relabeling_regression(dataset, min_value=class="num">1, max_value=class="num">1):
    future_prices = []
    for i in range(dataset.shape[class="num">0] - max_value):
        rand = random.randint(min_value, max_value)
        future_pr = dataset[&class="macro">#x27;<CLOSE>&class="macro">#x27;].iloc[i + rand]
        future_prices.append(future_pr)
    dataset = dataset.iloc[:len(future_prices)].copy()
    dataset[&class="macro">#x27;future_price&class="macro">#x27;] = future_prices
    class="kw">return dataset
# Apply the labelling_relabeling_regression function to raw data to get labeled data
train_data_labeled = labelling_relabeling_regression(train_data_initial)
test_data_labeled = labelling_relabeling_regression(test_data_initial)
# Create datasets of features and target variables for training
x_train = np.array([train_data_labeled[&class="macro">#x27;<CLOSE>&class="macro">#x27;].iloc[i - n_steps:i].values[-n_steps:] for i in range(n_steps, len(train_data_labeled))])
y_train = train_data_labeled[&class="macro">#x27;future_price&class="macro">#x27;].iloc[n_steps:].values
# Create datasets of features and target variables for testing
x_test = np.array([test_data_labeled[&class="macro">#x27;<CLOSE>&class="macro">#x27;].iloc[i - n_steps:i].values[-n_steps:] for i in range(n_steps, len(test_data_labeled))])
y_test = test_data_labeled[&class="macro">#x27;future_price&class="macro">#x27;].iloc[n_steps:].values
# After creating x_train and x_test, define n_features as follows:
n_features = x_train.shape[class="num">1] 
# Now use n_features to determine the ONNX class="kw">input data type
initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([None, n_features]))]

「用管道把缩放和森林串成一条链」

做价格行为建模时,零散的预处理和训练步骤容易乱,scikit-learn 的 Pipeline 能把多步操作压成一个对象,按顺序跑完缩放、特征变换再到回归。下面这段代码就是一条可直接在 MT5 导出的特征矩阵上复用的链。 MinMaxScaler 先把所有特征压到 0~1,避免量纲大的变量主导分裂;紧接着 RobustScaler 用中位数和四分位距再缩一次,对外汇或贵金属里偶发的跳空异常值更钝感。PolynomialFeatures 不做阶数限制时默认生成二次项,给随机森林补上捕捉非线性关系的余地。 森林本身用了 20 棵树(n_estimators=20),每棵最大深度 20,min_samples_split 与 min_samples_leaf 都设成 5000,意味着节点样本不足五千就不继续切分,叶子也至少含五千条观测——在小时线以上周期能压住过拟合。random_state=1 锁死随机种子,verbose=2 训练时打印进度,方便你对着 MT5 终端看每一步耗时。 管道 fit 完训练集后,predict 测试集,再用 r2_score 算拟合度。R2 越靠近 1 说明这套预处理加模型对样本内波动的解释力越强,但外汇/贵金属高杠杆品种样本外失效概率不低,参数仅作起点。 别把正态当圣经 RobustScaler 不假设数据服从正态,实战里贵金属夜盘异动频繁,比起标准缩放更不容易被一根影线带偏整条管道。

MQL5 / C++
# Create a pipeline with MinMaxScaler, RobustScaler, PolynomialFeatures and RandomForestRegressor
pipeline = Pipeline([
    (&class="macro">#x27;MinMaxScaler&class="macro">#x27;, MinMaxScaler()),
    (&class="macro">#x27;robust&class="macro">#x27;, RobustScaler()),
    (&class="macro">#x27;poly&class="macro">#x27;, PolynomialFeatures()),
    (&class="macro">#x27;rf&class="macro">#x27;, RandomForestRegressor(
        n_estimators=class="num">20,
        max_depth=class="num">20,
        min_samples_split=class="num">5000,
        min_samples_leaf=class="num">5000,
        random_state=class="num">1,
        verbose=class="num">2
    ))
])
# Train the pipeline
pipeline.fit(x_train, y_train)
# Make predictions
predictions = pipeline.predict(x_test)
# Evaluate model using R2
r2 = r2_score(y_test, predictions)
print(f&class="macro">#x27;R2 score: {r2}&class="macro">#x27;)

常见问题

行情里常有极端跳空和异常点,RobustScaler 基于中位数和四分位距,受离群值影响小,比按均值缩放更稳,适合喂给随机森林。
基础篇建议先用 2 阶,把原始量和两两交叉项纳入;阶数越高越容易记住噪声,回测时看样本外误差再决定是否加到 3 阶。
可以,小布能按你给的特征配置调用 RobustScaler 和多项式特征管道,自动训练随机森林并标出样本外表现,你只管看结论和调参。
从 100 棵起测,看学习曲线;外汇贵金属波动大,300 棵左右常能在偏差和耗时间平衡,再多边际收益递减。
管道避免训练集测试集泄露,一次 fit 搞定全部变换,实盘换新 K 线时直接 transform 再 predict,不容易手抖写错顺序。