如何在 MQL5 中使用 ONNX 模型(基础篇)
在 MT5 里直接跑 ONNX 模型
MT5 从 build 2815 起原生支持 ONNX 推理,交易者在 EA 里调用训练好的模型不再需要外部 Python 服务。这意味着你可以用 sklearn 或 PyTorch 训好特征,导出 .onnx 后直接丢进指标或 EA 做实时预测,延迟通常在毫秒级。
MQL5 提供 ONNXModel 类和一组方法加载、推理。典型流程是先 Create 读入模型文件,再用 Run 喂入输入张量拿输出。下面这段是最小可用骨架,注意输入维度必须和导出时完全一致,否则 Run 直接返回错误码。
外汇与贵金属杠杆高、滑点突变频繁,模型在历史样本上表现好不等于实盘稳健,上真仓前务必用策略测试器多周期回测。
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 侧调用,这条链路打通后你就能自己换品种、调窗口长度看拟合变化。
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) 真实行数是否和终端一致。
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 别轻易调小。
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 用历史数据复跑验证。
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)