如何在 MQL5 中集成 ONNX 模型的示例(基础篇)
在 MT5 里跑通第一个 ONNX 模型
MT5 从 build 2875 起原生支持 ONNX 推理,意味着你不必再靠 Python 桥接或 DLL 中转,就能把训练好的轻量模型直接丢进 EA 里做实时预测。官方示例里一个分类网络在 EURUSD 的 M1 上单次前向耗时约 0.3 毫秒,对 tick 级策略几乎无感。
集成核心是用 ONNX::Initialize 加载 .onnx 文件,再用 ONNX::Run 喂入标准化后的特征张量。模型输入输出节点名必须和导出时严格一致,否则会返回 ONNX_ERROR_INVALID_PARAMETER。
外汇与贵金属杠杆高、滑点跳空频繁,模型在历史样本上表现好不等于实盘稳健,上实盘前建议先用 MT5 策略测试器做至少 3 个月样本外回测。
◍ 把多个 ONNX 模型揉进一个 EA
做稳定交易通常建议策略和工具都分散开,机器学习也一样:堆几个简单模型,比死磕一个复杂模型更稳。但把多个训练好的模型塞进同一个 ONNX 文件往往很麻烦。 MQL5 里可以绕开这个限制——在一个程序里直接加载并组合多个已训练的 ONNX 模型。本文要拆的其中一种办法叫表决分类器(voting classifier),思路是让多个模型各自给信号,再按多数票决定最终方向。 实现上并不重,核心就是在 EA 里多次调用 OnnxRun,把每个模型的输出收拢后做计数。下面这段是加载三个模型并取各自二分类结果的最小骨架,你在 MT5 里建个脚本就能跑通验证逻辑。
「回归与分类双模型怎么喂 EURUSD D1」
示例里只用了两个轻量模型:一个回归模型预测具体数值,一个分类模型预测方向类别。两者本质区别在输出类型——回归给数量,分类给标签,但输入端都依赖常规化后的价格序列来提速收敛。 回归模型用 2003 年至 2022 年底的 EURUSD D1 数据训练,输入是连续 10 根 OHLC 柱。常规化做法是将序列均值除标准差,把数据压到均值 0、离散度 1 的区间,训练时梯度下降更容易收敛。它只预测次日收盘价,结构极简,纯演示用。 分类模型训练区间缩到 2010–2022 年底,输入是 63 根收盘价序列,输出三选一:跌、±10 点内不动、涨。之所以从 2010 年起步,是因为 2009 年 EURUSD 报价从 4 位小数切到 5 位,旧 1 点等于新 10 点,更早数据会让「10 点内」的标签定义失真。 两个模型都在 2022 年底前的数据上训完,想看实际表现就把策略测试器区间锁在 2022 年底之前。外汇与贵金属杠杆高,模型输出只是概率倾向,不能直接当入场指令。 下面这段 Python 是回归模型的数据装载与常规化原型,核心在 collect_dataset 与归一公式,复制到本地改 inp_history_size 就能跑不同窗口。
# Copyright class="num">2023, MetaQuotes Ltd. # [MQL5官方文档] from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime class="kw">import MetaTrader5 as mt5 class="kw">import tensorflow as tf class="kw">import numpy as np class="kw">import pandas as pd class="kw">import tf2onnx from sklearn.model_selection class="kw">import train_test_split from tqdm class="kw">import tqdm from sys class="kw">import argv if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save generated onnx-file near the our script data_path=argv[class="num">0] last_index=data_path.rfind("\\")+class="num">1 data_path=data_path[class="num">0:last_index] print("data path to save onnx model",data_path) # input parameters inp_model_name = "model.eurusd.D1.class="num">10.onnx" inp_history_size = class="num">10 inp_start_date = class="type">class="kw">datetime(class="num">2003, class="num">1, class="num">1, class="num">0) inp_end_date = class="type">class="kw">datetime(class="num">2023, class="num">1, class="num">1, class="num">0) # get data from client terminal exrusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: class="type">int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: D1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :class="kw">return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + class="num">1] x = w[[&class="macro">#x27;open&class="macro">#x27;, &class="macro">#x27;high&class="macro">#x27;, &class="macro">#x27;low&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;]].iloc[:-class="num">1].values y = w.iloc[-class="num">1][&class="macro">#x27;close&class="macro">#x27;] xs.append(x) ys.append(y) X = np.array(xs) y = np.array(ys) class="kw">return X, y ### # get prices X, y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=class="num">1, keepdims=True) s = X.std(axis=class="num">1, keepdims=True) X_norm = (X - m) / s y_norm = (y - m[:, class="num">0, class="num">3]) / s[:, class="num">0, class="num">3] # split data to train and test sets X_train, X_test, y_train, y_test = train_test_split(X_norm, y_norm, test_size=class="num">0.2, random_state=class="num">0) # define model model = tf.keras.Sequential([ tf.keras.layers.LSTM(class="num">64, input_shape=(inp_history_size, class="num">4)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(class="num">0.1),
把 Keras 模型导出成 ONNX 交给 MT5 跑
这段脚本把前面训练好的回归网络直接落地成 ONNX 文件,供 MT5 在 EURUSD 的 D1 周期上做下一根 K 线收盘价预测。模型结构是两层 32 维 relu 全连接,中间插了 BatchNormalization 和 0.1 的 Dropout,输出层是单神经元线性回归头。 训练时跑 50 个 epoch,验证集切了 15%,并用 ReduceLROnPlateau 监控 val_loss:连续 3 轮不降就乘 0.1 缩学习率,下限卡在 0.000001。这种调度在 2010–2023 的 EURUSD D1 数据上可能让收敛更稳,但具体 test_mae 要看你本地 evaluate 的打印值。 导出的文件名写死成 model.eurusd.D1.63.onnx,历史窗口取 63 根 D1 棒。外汇和贵金属杠杆高、滑点大,模型在历史段表现好不代表实盘概率同向,开 MT5 跑之前先确认 copy_rates_range 能拉到 2010 年数据。 最后 mt5.shutdown() 释放终端连接,整个流程从 initialize 到保存 onnx 一气呵成,你改 inp_history_size 或 inp_model_name 就能换周期重训。
tf.keras.layers.Dense(class="num">32, activation=&class="macro">#x27;relu&class="macro">#x27;), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(class="num">0.1), tf.keras.layers.Dense(class="num">32, activation=&class="macro">#x27;relu&class="macro">#x27;), tf.keras.layers.Dense(class="num">1) ]) model.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mse&class="macro">#x27;, metrics=[&class="macro">#x27;mae&class="macro">#x27;]) # model training for class="num">50 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor=&class="macro">#x27;val_loss&class="macro">#x27;, factor=class="num">0.1, patience=class="num">3, min_lr=class="num">0.000001) history = model.fit(X_train, y_train, epochs=class="num">50, verbose=class="num">2, validation_split=class="num">0.15, callbacks=[lr_reduction]) # model evaluation test_loss, test_mae = model.evaluate(X_test, y_test) print(f"test_loss={test_loss:.3f}") print(f"test_mae={test_mae:.3f}") # save model to onnx output_path = data_path+inp_model_name onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"saved model to {output_path}") # finish mt5.shutdown() # Copyright class="num">2023, MetaQuotes Ltd. # [MQL5官方文档] # # Classification model # class="num">0,class="num">0,class="num">1 - predict price down # class="num">0,class="num">1,class="num">0 - predict price same # class="num">1,class="num">0,class="num">0 - predict price up # from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime class="kw">import MetaTrader5 as mt5 class="kw">import tensorflow as tf class="kw">import numpy as np class="kw">import pandas as pd class="kw">import tf2onnx from sklearn.model_selection class="kw">import train_test_split from tqdm class="kw">import tqdm from keras.models class="kw">import Sequential from keras.layers class="kw">import Dense, Activation,Dropout, BatchNormalization, LeakyReLU from keras.optimizers class="kw">import SGD from keras class="kw">import regularizers from sys class="kw">import argv # initialize MetaTrader class="num">5 client terminal if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save the generated onnx-file near the our script data_path=argv[class="num">0] last_index=data_path.rfind("\")+class="num">1 data_path=data_path[class="num">0:last_index] print("data path to save onnx model",data_path) # input parameters inp_model_name = "model.eurusd.D1.class="num">63.onnx" inp_history_size = class="num">63 inp_start_date = class="type">class="kw">datetime(class="num">2010, class="num">1, class="num">1, class="num">0) inp_end_date = class="type">class="kw">datetime(class="num">2023, class="num">1, class="num">1, class="num">0) # get data from the client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: class="type">int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: H1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction