掌握ONNX:MQL5交易者的游戏规则改变者·综合运用
(3/3)·从模型导出到实时推理,打通AI策略在MT5落地的最后一段路
◍ 正文
pred = self.model.predict(self.test_x) acc = metrics.r2_score(self.test_y, pred) # Saving the plot containing information about predictions and actual values plt.figure(figsize=(<span class="number">7</span>, <span class="number">5</span>)) plt.plot(count, self.test_y, label = "Actual") plt.plot(count, pred, label = "forecast") plt.xlabel('Actuals') plt.ylabel('Preds') plt.title(title+f" | Train acc={acc}") plt.legend() plt.savefig(fname=f"{self.plots_directory}\\"+title) <span class="keyword">if</span> not os.path.exists(self.plots_directory): #create plots path <span class="keyword">if</span> it doesn't exist <span class="keyword">for</span> saving the train-test plots &n
把回归模型落进 MT5 能读的 ONNX
训练完的 Keras 回归模型先存成 h5,真正要在 MT5 里跑推理,得再转一道 ONNX。下面这段就是保存与转换的核心逻辑,直接在 Python 端执行即可验证。
转换时先拼出 MQL5\Files\ONNX Models 这个目录,不存在就建;模型名用目标列和样本数拼接,例如 MLP.REG.close.5000.onnx,样本数来自 self.data.shape[0],换数据规模文件名自动变。
用 tf2onnx.convert.from_keras 读回 h5 再写出 onnx,路径写进 data_path + "\MQL5\Files\" 之下,MT5 的 OnnxRun 类才能按相对路径加载。外汇与贵金属行情高波动,模型推理仅作概率参考,实盘前务必在策略测试器跑过历史样本。
self.model.save(f"Models\MLP.REG.{self.target_column}.{self.data.shape[class="num">0]}.h5") class="macro">#saving the model in h5 format, this will help us to easily convert this model to onnx later self.saveONNXModel() def saveONNXModel(self, folder="ONNX Models"): path = data_path + "\MQL5\Files\" + folder if not os.path.exists(path): # create this path if it doesn&class="macro">#x27;t exist os.makedirs(path) onnx_model_name = f"MLP.REG.{self.target_column}.{self.data.shape[class="num">0]}.onnx" path += "\" + onnx_model_name loaded_keras_model = load_model(f"Models\MLP.REG.{self.target_column}.{self.data.shape[class="num">0]}.h5") onnx_model, _ = tf2onnx.convert.from_keras(loaded_keras_model, output_path=path) onnx.save(onnx_model, path ) print(f&class="macro">#x27;Saved model to {path}&class="macro">#x27;)
「把训练好的ONNX塞进EA里跑起来」
在MT5里调用离线训练好的ONNX模型,第一步是把模型文件编译进EA资源。用 #resource 指令把 onnx 二进制读入 uchar 数组,再在 OnInit 里通过 OnnxCreateFromBuffer 拿到句柄,这一步失败就会直接 INIT_FAILED,日志里能看到 GetLastError 的具体码。 实时推理前必须还原训练时的归一化。代码里的 norm_x 是 CPreprocessing 实例,在 LoadNormParams() 中从CSV载入均值方差等参数;若文件缺失,OnInit 会提示你先用数据收集脚本补数据并重训。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必在策略测试器验证。 拿到句柄后别想当然认为维度已知。示例里用 OnnxGetInputCount / OnnxGetOutputCount 循环打印名字与张量尺寸,再把维度 ArrayCopy 进 inputs、outputs 数组——这一圈打印能让你在调试模式确认载入的到底是Close单序列还是多序列模型。
class="macro">#resource "\\Files\\ONNX Models\\MLP.REG.CLOSE.class="num">10000.onnx" as class="type">uchar RNNModel[] class="type">long mlp_onnxhandle; class="macro">#include <MALE5\preprocessing.mqh> CPreprocessing<vectorf, matrixf> *norm_x; class="type">int inputs[], outputs[]; vectorf OPEN, HIGH, LOW; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if (!LoadNormParams()) class=class="str">"cmt">//Load the normalization parameters we saved once { Print("Normalization parameters csv files couldn&class="macro">#x27;t be found \nEnsure you are collecting data and Normalizing them class="kw">using [ONNX get data.ex5] Script \nTrain the Python model again if necessary"); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- ONNX SETTINGS mlp_onnxhandle = OnnxCreateFromBuffer(RNNModel, MQLInfoInteger(MQL_DEBUG) ? ONNX_DEBUG_LOGS : ONNX_DEFAULT); class=class="str">"cmt">//creating onnx handle buffer | rUN DEGUG MODE during debug mode if (mlp_onnxhandle == INVALID_HANDLE) { Print("OnnxCreateFromBuffer Error = ",GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close) OnnxTypeInfo type_info; class=class="str">"cmt">//Getting onnx information for Reference In case you forgot what the loaded ONNX is all about class="type">long input_count=OnnxGetInputCount(mlp_onnxhandle); Print("model has ",input_count," input(s)"); for(class="type">long i=class="num">0; i<input_count; i++) { class="type">class="kw">string input_name=OnnxGetInputName(mlp_onnxhandle,i); Print(i," input name is ",input_name); if(OnnxGetInputTypeInfo(mlp_onnxhandle,i,type_info)) { PrintTypeInfo(i,"input",type_info); ArrayCopy(inputs, type_info.tensor.dimensions); } } class="type">long output_count=OnnxGetOutputCount(mlp_onnxhandle); Print("model has ",output_count," output(s)"); for(class="type">long i=class="num">0; i<output_count; i++) { class="type">class="kw">string output_name=OnnxGetOutputName(mlp_onnxhandle,i); Print(i," output name is ",output_name); if(OnnxGetOutputTypeInfo(mlp_onnxhandle,i,type_info)) { PrintTypeInfo(i,"output",type_info); ArrayCopy(outputs, type_info.tensor.dimensions); } }
◍ 给 ONNX 模型喂对输入输出维度
在 MT5 里加载完 ONNX 句柄后,第一步是把张量形状钉死。下面这段代码演示了如何给输入层设成 {batch_size, 3}、输出层设成 {batch_size, 1},并在调试模式下把 inputs/outputs 打印出来核对。 [CODE] if(MQLInfoInteger(MQL_DEBUG)) { Print("Inputs & Outputs"); ArrayPrint(inputs); ArrayPrint(outputs); } const long input_shape[] = {batch_size, 3}; if(!OnnxSetInputShape(mlp_onnxhandle, 0, input_shape)) { printf("Failed to set the input shape Err=%d",GetLastError()); return INIT_FAILED; } const long output_shape[] = {batch_size, 1}; if(!OnnxSetOutputShape(mlp_onnxhandle, 0, output_shape)) { printf("Failed to set the input shape Err=%d",GetLastError()); return INIT_FAILED; } //--- return(INIT_SUCCEEDED); } [/CODE] 逐行看:MQLInfoInteger(MQL_DEBUG) 为真时才打印,避免实盘刷屏;input_shape 写死第 0 个输入节点维度为 batch_size×3,对应 3 个特征。OnnxSetInputShape 返回 false 就抓 GetLastError 并 INIT_FAILED,不往下跑。 输出侧对称处理,output_shape 是 batch_size×1,即每根 K 线只吐一个预测值。从 EURUSD H1 实测日志看,会话初始化耗时约 4 毫秒(18:57:10.265 到 .269),且确认模型只有 1 个输入、名字叫 input_1、类型为 ONNX_TYPE_TENSOR。 开 MT5 把这段塞进 OnInit,把 batch_size 改成你实际推理的条数,再开调试看 ArrayPrint 出来的维度是否对得上,外汇与贵金属波动剧烈、模型误判概率不低,仅作辅助信号。
if(MQLInfoInteger(MQL_DEBUG)) { Print("Inputs & Outputs"); ArrayPrint(inputs); ArrayPrint(outputs); } const class="type">long input_shape[] = {batch_size, class="num">3}; if(!OnnxSetInputShape(mlp_onnxhandle, class="num">0, input_shape)) { printf("Failed to set the input shape Err=%d",GetLastError()); class="kw">return INIT_FAILED; } const class="type">long output_shape[] = {batch_size, class="num">1}; if(!OnnxSetOutputShape(mlp_onnxhandle, class="num">0, output_shape)) { printf("Failed to set the input shape Err=%d",GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
读懂 ONNX 在 EURUSD H1 上的张量握手
把训练好的 ONNX 模型挂到 MT5 的 EURUSD H1 图表时,日志会在 18:57:10.269 一口气打印出输入输出契约。输入被声明为 shape [-1, 3] 的 ONNX_TYPE_TENSOR,输出则是 shape [-1, 1] 的同类型张量,模型仅有 1 个名为 dense_1 的输出节点。 未定义维度(前面的 -1)在推理前必须显式指定。若把该维设为 1,输入可压缩为 [3]、输出可压缩为 [1],这意味着单根 K 线就能跑一次前向。外汇与贵金属杠杆高,模型误判会迅速放大亏损,任何信号都只是概率倾向。 下面这段函数就是按 [total, 3] 拼实时矩阵的骨架:开高低三个字段各用 CopyRates 拉取当前周期数据。你直接把 OPEN/HIGH/LOW 声明为全局 vector 后粘进 EA,就能在 MT5 里验证张量维度是否对得上日志。 别把日志当噪声 那些 PM/HI/FS 前缀是 MT5 内部打印标记,重点看 shape 与 output name。dense_1 若和你训练时不一致,说明导出图被重命名,推理会直接报维度错。
matrixf GetLiveData(class="type">uint start, class="type">uint total) { matrixf return_matrix(total, class="num">3); OPEN.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_OPEN, start, total); HIGH.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_HIGH, start, total); LOW.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_LOW, start, total);
「归一化参数的两种加载路径」
把 OHLC 塞进 return_matrix 后,立刻调用 norm_x.Normalization() 做标准化;若返回 false,直接 Print("Failed to Normalize") 并带着原矩阵退出。这一步决定了后续特征喂给模型时量纲是否一致,外汇与贵金属 tick 波动跨度大,跳过归一化会让梯度更新倾向被高价品种绑架。 LoadNormParams() 按 NORM 枚举分支读取磁盘上的 scaler 参数:NORM_MEAN_NORM 需要同时载入 mean、min、max 三个 vectorf,文件名拼接规则是 Symbol()+Period()+total_bars 再加 .mean_norm_scaler.xxx.csv;NORM_MIN_MAX_SCALER 只取 min 与 max。调试模式下 MQL_DEBUG 为真会打印向量内容,方便你核对 EURUSD.H1.5000 这类命名是否对得上本地 csv。 两个分支都有防御性判断:mean、min、max 的 Sum() 同时 ≤0 才视参数为空并返回 false。实盘前建议手动删掉旧 csv 让函数重新生成,避免跨周期复用 scaler 导致归一化偏移。 别把正态当圣经 标准化假设特征近似高斯分布,但贵金属跳空开盘的 returns 明显厚尾;若你用 NORM_STANDARDIZATION,最好先对 return_matrix 做一遍偏度检查再决定是否换 min-max。
return_matrix.Col(OPEN, class="num">0); return_matrix.Col(HIGH, class="num">1); return_matrix.Col(LOW, class="num">2); if (!norm_x.Normalization(return_matrix)) Print("Failed to Normalize"); class="kw">return return_matrix; } class="type">bool LoadNormParams() { vectorf min = {}, max ={}, mean={} , std = {}; csv_name_ = Symbol()+"."+EnumToString(Period())+"."+class="type">class="kw">string(total_bars); class="kw">switch(NORM) { case NORM_MEAN_NORM: mean = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv"); class=class="str">"cmt">//--- Loading the mean min = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.min.csv"); class=class="str">"cmt">//--- Loading the min max = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.max.csv"); class=class="str">"cmt">//--- Loading the max if (MQLInfoInteger(MQL_DEBUG)) Print(EnumToString(NORM),"\nMean ",mean,"\nMin ",min,"\nMax ",max); norm_x = new CPreprocessing<vectorf,matrixf>(max, mean, min); if (mean.Sum()<=class="num">0 && min.Sum()<=class="num">0 && max.Sum() <=class="num">0) class="kw">return false; break; case NORM_MIN_MAX_SCALER: min = ReadCsvVector(normparams_folder+csv_name_+".min_max_scaler.min.csv"); class=class="str">"cmt">//--- Loading the min max = ReadCsvVector(normparams_folder+csv_name_+".min_max_scaler.max.csv"); class=class="str">"cmt">//--- Loading the max if (MQLInfoInteger(MQL_DEBUG)) Print(EnumToString(NORM),"\nMin ",min,"\nMax ",max); norm_x = new CPreprocessing<vectorf,matrixf>(max, min); if (min.Sum()<=class="num">0 && max.Sum() <=class="num">0) class="kw">return false; break; case NORM_STANDARDIZATION: mean = ReadCsvVector(normparams_folder+csv_name_+".standardization_scaler.mean.csv"); class=class="str">"cmt">//--- Loading the mean
◍ 标准化参数载入与空值拦截
做特征标准化时,均值和标准差通常从离线 CSV 里读回来,而不是在线重算。下面这段逻辑先按文件名拼出 .standardization_scaler.std.csv 路径,用 ReadCsvVector 把标准差向量读进 std,若处于 MQL_DEBUG 模式则把归一化类型、mean、std 一并 Print 出来方便排查。
读完后用 mean 和 std 构造 CPreprocessing 实例,归一化类型传 NORM_STANDARDIZATION。这里有个硬拦截:当 mean.Sum()<=0 且 std.Sum()<=0 时直接 return false,避免拿全零参数去缩放特征导致后续矩阵全是脏值。
实盘前建议在 MT5 策略测试器里故意放一份空 scaler 文件,确认 return false 分支能触发;外汇与贵金属波动剧烈,特征缩放参数失效会让模型信号失真,属于高概率踩坑点。
std = ReadCsvVector(normparams_folder+csv_name_+".standardization_scaler.std.csv"); class=class="str">"cmt">//--- Loading the std if (MQLInfoInteger(MQL_DEBUG)) Print(EnumToString(NORM),"\nMean ",mean,"\nStd ",std); norm_x = new CPreprocessing<vectorf,matrixf>(mean, std, NORM_STANDARDIZATION); if (mean.Sum()<=class="num">0 && std.Sum() <=class="num">0) class="kw">return false; break; } class="kw">return true; }
在 OnTick 里跑通 ONNX 推理
把 Python 训好的神经网络塞进 MT5 实盘,核心就是 OnTick 里调一次 OnnxRun。输入用 matrixf 或 vectorf 装 float 特征,输出容器必须提前按模型输出节点数 resize,否则会直接报 5805(ERR_ONNX_INVALID_PARAMETER)。 单输出网络最省事:向量开 size=1,矩阵就开 1 行 1 列。上面这段 EA 代码里 output_data 显式声明成 vectorf(1),注释也写了 resize 是致命步骤,漏掉这行基本跑不起来。 调用时传 ONNX_NO_CONVERSION 让句柄自己处理张量布局,失败就 Print 错误码并 ExpertRemove 退出,不拖着残局继续跑。Comment 把输入和预测值打印到图表,方便你肉眼核对特征工程有没有串列。 外汇与贵金属杠杆高、滑点跳空频繁,模型信号仅作概率参考,实盘前请在策略测试器用历史 tick 回放验证推理一致性。
class="type">void OnTick() { class=class="str">"cmt">//--- matrixf input_data = GetLiveData(class="num">0,class="num">1); vectorf output_data(class="num">1); class=class="str">"cmt">//It is very crucial to resize this vector or matrix if (!OnnxRun(mlp_onnxhandle, ONNX_NO_CONVERSION, input_data, output_data)) { Print("Failed to Get the Predictions Err=",GetLastError()); ExpertRemove(); class="kw">return; } Comment("inputs_data ",input_data," predictions ",output_data); }
「为什么把ONNX搬进MT5值得一试」
在MT5里直接跑ONNX模型,最实在的一点就是不用把TensorFlow或PyTorch训好的网络用MQL5重写一遍。原本要花几天硬编码的前馈或卷积结构,现在导出一个.onnx文件就能在EA里调用,省下的时间够你多跑几组品种验证。 ONNX覆盖的模型类型很宽,从普通DNN到RNN、CNN都支持,意味着你做序列预测(比如多帧波动率)或图像化形态识别(K线转图)都能直接复用现成训练成果。它还是跨平台的,Windows下导出的模型丢到Linux VPS的MT5里不需改代码,配合ONNX Runtime在边缘设备上的优化,推理延迟通常能压到毫秒级。 社区侧也不是空架子:PyTorch的torch.onnx.export、sklearn-onnx都是官方级导出路径,模型版本还能带演进兼容,旧EA加载新模型不至崩。外汇和贵金属杠杆高、滑点跳空频繁,用ONNX做信号增强前,建议先在历史 Tick 上回测推理稳定性,再上模拟盘。
◍ 把这条线请下神坛
ONNX 真正好用的地方,是当你要在 PyTorch、TensorFlow 之间倒模型,或者把训练好的网络丢到 MT5 实盘里跑的时候,它省掉了重写前向推理的麻烦。本文走通的 5 个步骤只是骨架,EA 文件 ONNX mt5.mq5 和取数脚本 ONNX get data.mq5 才是能直接编译验证的实体,模型主体则在 neuralnet.py 里用 Python 实现。
一个原文没覆盖但实测会卡住人的点:策略测试器里跑这套 EA,必须先在测试器内部读入规范化后的 CSV,否则归一化链路是断的。社区里有用户反馈 MALE5 3.0.0 已弃用 CPreprocessing,得把预处理.mqh 放 include 目录,或改调 RobustScaler / MinMaxScaler / StandardScaler 单独类。
MT5 端对模型复杂度不挑,从浅层 MLP 到本地 LLM 都能塞进 ONNX 节点跑,外汇与贵金属杠杆高、滑点跳空频繁,任何模型信号都只是概率倾向,别把跨框架推理这层皮当成胜率保证。
class="macro">#include <preprocessing.mqh> fit_transform( const matrix &X) transform( const matrix &X) transform(const vector &X)