Python、ONNX 和 MetaTrader 5:利用 RobustScaler 和 PolynomialFeatures 数据预处理创建 RandomForest 模型(基础篇)
「用 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 输入结构,方便后面导出的模型被其他语言调用。外汇与贵金属杠杆高,这套仅作技术验证,实盘信号失效概率不低。
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 不假设数据服从正态,实战里贵金属夜盘异动频繁,比起标准缩放更不容易被一根影线带偏整条管道。
# 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;)