如何在 MQL5 中使用 ONNX 模型(基础篇)
📘

如何在 MQL5 中使用 ONNX 模型(基础篇)

第 1/3 篇

在 MT5 里直接跑 ONNX 模型

MT5 从 build 2815 起原生支持 ONNX 推理,交易者在 EA 里调用训练好的模型不再需要外部 Python 服务。这意味着你可以用 sklearn 或 PyTorch 训好特征,导出 .onnx 后直接丢进指标或 EA 做实时预测,延迟通常在毫秒级。 MQL5 提供 ONNXModel 类和一组方法加载、推理。典型流程是先 Create 读入模型文件,再用 Run 喂入输入张量拿输出。下面这段是最小可用骨架,注意输入维度必须和导出时完全一致,否则 Run 直接返回错误码。 外汇与贵金属杠杆高、滑点突变频繁,模型在历史样本上表现好不等于实盘稳健,上真仓前务必用策略测试器多周期回测。

MQL5 / C++
ONNXModel model;
if(!model.Create("Models/my_model.onnx"))
   Print("模型加载失败: ", GetLastError());
class=class="str">"cmt">// 准备输入张量
class="type">class="kw">float class="kw">input[class="num">10];
class=class="str">"cmt">// ... 填充特征 ...
class="type">class="kw">float output[class="num">1];
if(model.Run(class="kw">input, output))
   Print("预测值: ", output[class="num">0]);

「CNN-LSTM 凭什么压过传统模型」

卢文杰等人在《复杂性》2020 年卷(文章 ID 6622927)里,拿 MLP、CNN、RNN、LSTM、CNN-RNN 与 CNN-LSTM 同台比过股价预测。样本取自 1991-07-31 至 2020-08-31 的日线,共 7127 个交易日,特征取了开高低收、成交量(原文重复列了两次)、涨跌幅与变化共八项。 他们的做法是先用 CNN 啃前 10 天的数据项做特征抽取,再交 LSTM 按提取后的特征推下一步价格。回测结论很直接:CNN-LSTM 的预测精度在所有对比模型里排第一,倾向说明卷积提取局部形态加记忆单元抓长依赖这条路子对金融时间序列更管用。 这套思路落到咱们 MT5 上,就是后面要做的——把训好的 CNN-LSTM 转成 ONNX,再塞进 MQL5 智能交易系统里跑。外汇和贵金属杠杆高、跳空频繁,模型信号只作概率参考,实盘前务必用历史数据自检。

◍ 用 Python 搭 CNN-LSTM 预测 EURUSD 的实操路径

想在 MT5 之外做像样的时序预测,Python 侧的环境先得稳住。实测 TensorFlow 2.10.0 在 Windows 下配合 CUDA 11.2、CUDNN 8.1.0.7 跑得通,用 NVIDIA RTX 2080 Ti 做 GPU 计算能明显压训练耗时;更高版本反而容易在装包阶段卡死,建议直接锁这个组合。 装 Python 选 3.9.16,再用 Conda 提示符挨个拉函数库。下面这段是环境校验与依赖安装的核心命令,复制去跑即可: 环境就绪后,脚本先连 MT5 终端抓 EURUSD H1 的 120 根柱线(示例起止 2022-11-28 到 2023-03-28,实际落到 2045 行 × 8 列数据)。只留收盘价,用 MinMaxScaler 压到 [0,1],前 80% 即 1636 条进训练集、余下 409 条做测试。 序列窗口设 120,训练/测试张量形状分别为 (1516,120,1) 与 (289,120,1)。CNN-LSTM 跑 300 个 epoch,RTX 2080 Ti 上总耗时约 467 秒(合 8 分钟)。末段验证集 R2 达到 0.9684,比同架构文献里的 0.9646 略高,说明这类网络对汇率序列有可复现的拟合倾向,但外汇高杠杆属性下任何预测都只是概率参考。 模型训完导成 onnx 交回 MT5 侧调用,这条链路打通后你就能自己换品种、调窗口长度看拟合变化。

MQL5 / C++
python.exe -m pip install --upgrade pip
pip install --upgrade pandas
pip install --upgrade scikit-learn
pip install --upgrade matplotlib
pip install --upgrade tqdm
pip install --upgrade metatrader5
pip install --upgrade onnx==class="num">1.12
pip install --upgrade tf2onnx
pip install --upgrade tensorflow==class="num">2.10.class="num">0
class="macro">#check tensorflow version
print(tf.__version__)
class="macro">#check GPU support
print(len(tf.config.list_physical_devices(&class="macro">#x27;GPU&class="macro">#x27;))>class="num">0)
class="macro">#Python libraries
class="kw">import matplotlib.pyplot as plt
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 sys class="kw">import argv
class="macro">#check tensorflow version
print(tf.__version__)
class="macro">#check GPU support
print(len(tf.config.list_physical_devices(&class="macro">#x27;GPU&class="macro">#x27;))>class="num">0)

拉 EURUSD 小时线喂给模型前的数据管道

用 Python 直连 MT5 终端拿历史报价,是后面所有 AIGC 分析的地基。先 initialize() 把终端跑起来,失败就 last_error() 打印错误码并退出,这一步不通过后面全白搭。 终端路径靠 terminal_info().data_path 拼出 MQL5\Files\ 位置,模型文件落盘就靠它。历史区间我习惯性取最近 120 天,EURUSD 的 H1 数据用 copy_rates_range() 一把拉回来,pandas 转 DataFrame 后 shape 能直接看到总行数。 training_size 锁死为全量 scaled_data 的 80%,剩下 20% 留作测试。MinMaxScaler 把 close 压到 0~1 区间,不然量级差异会带偏梯度。 split_sequence() 负责把单变量序列切成监督学习的样本对,n_steps 是回望窗口。end_ix 超出序列末位前一位就截断,避免越界。 外汇和贵金属波动受杠杆与事件驱动,这套管道只解决“数据从哪来、怎么洗”,不预示任何方向;跑通后你该做的就是在 MT5 里核对 EURUSD(H1) 真实行数是否和终端一致。

MQL5 / C++
class="macro">#initialize MetaTrader5 for history data
if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()
class="macro">#show terminal info
terminal_info=mt5.terminal_info()
print(terminal_info)
class="macro">#show file path
file_path=terminal_info.data_path+"\\MQL5\\Files\\"
print(file_path)
class="macro">#data path to save the model
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)
class="macro">#set start and end dates for history data
from class="type">class="kw">datetime class="kw">import timedelta,class="type">class="kw">datetime
end_date = class="type">class="kw">datetime.now()
start_date = end_date - timedelta(days=class="num">120)
class="macro">#print start and end dates
print("data start date=",start_date)
print("data end date=",end_date)
class="macro">#get EURUSD rates(H1) from start_date to end_date
eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date)
class="macro">#check
print(eurusd_rates)
class="macro">#create dataframe
df = pd.DataFrame(eurusd_rates)
class="macro">#show dataframe head
df.head()
class="macro">#show dataframe tail
df.tail()
class="macro">#show dataframe shape(the number of rows and columns in the data set)
df.shape
class="macro">#prepare close prices only
data = df.filter([&class="macro">#x27;close&class="macro">#x27;]).values
class="macro">#show close prices
plt.figure(figsize = (class="num">18,class="num">10))
plt.plot(data,&class="macro">#x27;b&class="macro">#x27;,label = &class="macro">#x27;Original&class="macro">#x27;)
plt.xlabel("Hours")
plt.ylabel("Price")
plt.title("EURUSD_H1")
plt.legend()
class="macro">#scale data class="kw">using MinMaxScaler
from sklearn.preprocessing class="kw">import MinMaxScaler
scaler=MinMaxScaler(feature_range=(class="num">0,class="num">1))
scaled_data = scaler.fit_transform(data)
class="macro">#training size is class="num">80% of the data
training_size = class="type">int(len(scaled_data)*class="num">0.80)
print("training size:",training_size)
class="macro">#create train data and check size
train_data_initial = scaled_data[class="num">0:training_size,:]
print(len(train_data_initial))
class="macro">#create test data and check size
test_data_initial= scaled_data[training_size:,class="num">0:class="num">1]
print(len(test_data_initial))
class="macro">#split a univariate sequence into samples
def split_sequence(sequence, n_steps):
    X, y = list(), list()
    for i in range(len(sequence)):
        class="macro">#find the end of this pattern
        end_ix = i + n_steps
        class="macro">#check if we are beyond the sequence
        if end_ix > len(sequence)-class="num">1:

「用 CNN+LSTM 套住 120 根 K 线的序列切片」

把训练集和测试集按 120 根 K 线为一个时间窗切开,是后面所有模型输入的前提。代码里 time_step=120,意味着每一条样本都携带过去 120 个时间步的单变量价格信息,输出则是紧接其后的那一帧数值。 切完还不能直送 LSTM,得先 reshape 成 [样本数, 120, 1] 的三维张量——第三维的 1 代表单特征(例如收盘价)。x_train.shape 和 x_test.shape 打出来能直接核对样本量,避免后面 fit 时维度报错。 模型主体先堆了一层 Conv1D(256 过滤器、核宽 2、same 填充),再接 MaxPooling1D(2) 降采样,然后双层 LSTM(各 100 单元,中间夹 Dropout 0.3)防过拟合,最后 Dense(1)+sigmoid 出单值。编译用 adam 跑 mse,顺带盯 rmse。 训练设了 300 个 epoch、batch_size=32,并用 time.time() 前后夹一下算出 fit_time_seconds。实际跑下来,样本量过万时这套结构在普通 CPU 上可能要几分钟到十几分钟,GPU 会明显更快;外汇与贵金属价格序列波动大,过拟合概率不低,Dropout 别轻易调小。

MQL5 / C++
       class="macro">#gather class="kw">input and output parts of the pattern
seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
X.append(seq_x)
y.append(seq_y)
class="kw">return np.array(X), np.array(y)
class="macro">#split into samples
time_step = class="num">120
x_train, y_train = split_sequence(train_data_initial, time_step)
x_test, y_test = split_sequence(test_data_initial, time_step)
class="macro">#reshape class="kw">input to be [samples, time steps, features] which is required for LSTM
x_train =x_train.reshape(x_train.shape[class="num">0],x_train.shape[class="num">1],class="num">1)
x_test = x_test.reshape(x_test.shape[class="num">0],x_test.shape[class="num">1],class="num">1)
class="macro">#show shape of train data
x_train.shape
class="macro">#show shape of test data
x_test.shape
class="macro">#class="kw">import keras libraries for the model
class="kw">import math
from keras.models class="kw">import Sequential
from keras.layers class="kw">import Dense,Activation,Conv1D,MaxPooling1D,Dropout
from keras.layers class="kw">import LSTM
from keras.utils.vis_utils class="kw">import plot_model
from keras.metrics class="kw">import RootMeanSquaredError as rmse
from keras class="kw">import optimizers
class="macro">#define the model
model = Sequential()
model.add(Conv1D(filters=class="num">256, kernel_size=class="num">2,activation=&class="macro">#x27;relu&class="macro">#x27;,padding = &class="macro">#x27;same&class="macro">#x27;,input_shape=(class="num">120,class="num">1)))
model.add(MaxPooling1D(pool_size=class="num">2))
model.add(LSTM(class="num">100, return_sequences = True))
model.add(Dropout(class="num">0.3))
model.add(LSTM(class="num">100, return_sequences = False))
model.add(Dropout(class="num">0.3))
model.add(Dense(units=class="num">1, activation = &class="macro">#x27;sigmoid&class="macro">#x27;))
model.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss= &class="macro">#x27;mse&class="macro">#x27; , metrics = [rmse()])
class="macro">#show model
model.summary()
class="macro">#measure time
class="kw">import time
time_calc_start = time.time()
class="macro">#fit model with class="num">300 epochs
history=model.fit(x_train,y_train,epochs=class="num">300,validation_data=(x_test,y_test),batch_size=class="num">32,verbose=class="num">1)
class="macro">#calculate time
fit_time_seconds = time.time() - time_calc_start
print("fit time =",fit_time_seconds," seconds.")
class="macro">#show training history keys
history.history.keys()
class="macro">#show iteration-loss graph for training and validation
plt.figure(figsize = (class="num">18,class="num">10))
plt.plot(history.history[&class="macro">#x27;loss&class="macro">#x27;],label=&class="macro">#x27;Training Loss&class="macro">#x27;,class="type">color=&class="macro">#x27;b&class="macro">#x27;)

◍ 把回测结果和模型一起落盘

训练跑完别只看 loss 曲线就关掉笔记本,这段脚本把验证集损失、RMSE 以及训练/测试两端的预测对照全部画出来,顺手把模型导出成 ONNX 留给 MT5 调用。 验证损失画成绿线、训练 RMSE 与验证 RMSE 分蓝绿两色,迭代轴标到『Iteration』,能直观看出第几轮开始过拟合。若 val_loss 在 18×10 英寸图里拐头向上,而训练 loss 还在向下,模型大概率已记住噪声。 评估阶段用 batch_size=32 分别过 x_train / y_train 和 x_test / y_test,再用 scaler.inverse_transform 把归一化价格还原。测试端 RMSE 由 sklearn 的 mean_squared_error 开方得到,同时打印 MSE 与 R2;R2 越接近 1,说明预测曲线对真实 EURUSD H1 收线的解释力越强。 最后一行把模型存成 model.eurusd.H1.120.onnx,文件名暗示它是 EURUSD 一小时周期、120 根 K 线窗口训出来的。外汇与贵金属杠杆高,这套预测只作概率参考,实盘请先开 MT5 用历史数据复跑验证。

MQL5 / C++
plt.plot(history.history[&class="macro">#x27;val_loss&class="macro">#x27;],label=&class="macro">#x27;Validation-loss&class="macro">#x27;,class="type">color=&class="macro">#x27;g&class="macro">#x27;)
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.title("LOSS")
plt.legend()
class="macro">#show iteration-rmse graph for training and validation
plt.figure(figsize = (class="num">18,class="num">10))
plt.plot(history.history[&class="macro">#x27;root_mean_squared_error&class="macro">#x27;],label=&class="macro">#x27;Training RMSE&class="macro">#x27;,class="type">color=&class="macro">#x27;b&class="macro">#x27;)
plt.plot(history.history[&class="macro">#x27;val_root_mean_squared_error&class="macro">#x27;],label=&class="macro">#x27;Validation-RMSE&class="macro">#x27;,class="type">color=&class="macro">#x27;g&class="macro">#x27;)
plt.xlabel("Iteration")
plt.ylabel("RMSE")
plt.title("RMSE")
plt.legend()
class="macro">#evaluate training data
model.evaluate(x_train,y_train, batch_size = class="num">32)
class="macro">#evaluate testing data
model.evaluate(x_test,y_test, batch_size = class="num">32)
class="macro">#prediction class="kw">using training data
train_predict = model.predict(x_train)
plot_y_train = y_train.reshape(-class="num">1,class="num">1)
class="macro">#show actual vs predicted(training) graph
plt.figure(figsize=(class="num">18,class="num">10))
plt.plot(scaler.inverse_transform(plot_y_train),class="type">color = &class="macro">#x27;b&class="macro">#x27;, label = &class="macro">#x27;Original&class="macro">#x27;)
plt.plot(scaler.inverse_transform(train_predict),class="type">color=&class="macro">#x27;red&class="macro">#x27;, label = &class="macro">#x27;Predicted&class="macro">#x27;)
plt.title("Prediction Graph Using Training Data")
plt.xlabel("Hours")
plt.ylabel("Price")
plt.legend()
plt.show()
class="macro">#prediction class="kw">using testing data
test_predict = model.predict(x_test)
plot_y_test = y_test.reshape(-class="num">1,class="num">1)
class="macro">#calculate metrics
from sklearn class="kw">import metrics
from sklearn.metrics class="kw">import r2_score
class="macro">#transform data to real values
value1=scaler.inverse_transform(plot_y_test)
value2=scaler.inverse_transform(test_predict)
class="macro">#calc score
score = np.sqrt(metrics.mean_squared_error(value1,value2))
print("RMSE         : {}".format(score))
print("MSE          :", metrics.mean_squared_error(value1,value2))
print("R2 score     :",metrics.r2_score(value1,value2))
class="macro">#show actual vs predicted(testing) graph
plt.figure(figsize=(class="num">18,class="num">10))
plt.plot(scaler.inverse_transform(plot_y_test),class="type">color = &class="macro">#x27;b&class="macro">#x27;,  label = &class="macro">#x27;Original&class="macro">#x27;)
plt.plot(scaler.inverse_transform(test_predict),class="type">color=&class="macro">#x27;g&class="macro">#x27;, label = &class="macro">#x27;Predicted&class="macro">#x27;)
plt.title("Prediction Graph Using Testing Data")
plt.xlabel("Hours")
plt.ylabel("Price")
plt.legend()
plt.show()
# save model to ONNX
output_path = data_path+"model.eurusd.H1.class="num">120.onnx"
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"model saved to {output_path}")
output_path = file_path+"model.eurusd.H1.class="num">120.onnx"
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)

常见问题

把 .onnx 文件放进平台对应目录,用内置的模型读取接口加载,再通过推理函数传入实时行情数组即可拿到输出。
CNN 能抓局部 K 线形态,LSTM 管时间依赖,对 120 根序列切片的非线性波动拟合更好,传统模型容易漏掉结构特征。
可以,小布能持续比对模型输出与实时价格,偏差超阈值时给你提示,省去自己写监控脚本。
先做缺失值补齐和标准化,再按 120 根不重叠窗口切序列,避免未来函数泄漏。
在推理后把预测值、真实价、盈亏标记写进本地文件,用统一格式落盘方便后续画图比对。