如何在 MQL5 中集成 ONNX 模型的示例(基础篇)
📘

如何在 MQL5 中集成 ONNX 模型的示例(基础篇)

第 1/3 篇

在 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 就能跑不同窗口。

MQL5 / C++
# 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 就能换周期重训。

MQL5 / C++
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

常见问题

先准备一个已训练好的ONNX文件,在EA初始化时用模型加载接口读入,再在OnTick里把行情特征转成张量喂给模型取输出,跑通日志不报错即成功。
不卡,分别在初始化时各加载一次模型句柄,推理时按各自输入维度分别喂数据;注意两个模型的输入特征顺序别写反,分开存结果变量即可。
可以,小布能接入你的品种页把模型输出和价格行为对照标注,帮你快速发现信号漂移或输入特征异常,省去手动核对。
要分开,回归模型喂连续数值特征、分类模型喂归一化后的离散或概率特征;两者都用D1收盘序列但按各自训练时的维度组装张量。
用keras2onnx或tf2onnx把.h5导出成.onnx,确认输入输出节点名,再交给平台侧的模型接口加载,注意算子版本兼容。