掌握ONNX:MQL5交易者的游戏规则改变者·综合运用
🤖

掌握ONNX:MQL5交易者的游戏规则改变者·综合运用

(3/3)·从模型导出到实时推理,打通AI策略在MT5落地的最后一段路

含代码示例实战向 第 3/3 篇
很多交易者以为AI策略难在建模,其实卡死在部署:模型在Python里跑得好好的,一进MQL5就无从下手。从头重写神经网络既费时又易错,还拖慢了策略迭代节奏。

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pred = self.model.predict(self.test_x) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;acc = metrics.r2_score(self.test_y, pred) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Saving the plot containing information about predictions and actual values &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.figure(figsize=(<span class="number">7</span>, <span class="number">5</span>)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.plot(count, self.test_y, label = "Actual") &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.plot(count, pred,&nbsp;&nbsp;label = "forecast") &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.xlabel('Actuals') &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.ylabel('Preds') &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.title(title+f" | Train acc={acc}") &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.legend() &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plt.savefig(fname=f"{self.plots_directory}\\"+title)&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&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 类才能按相对路径加载。外汇与贵金属行情高波动,模型推理仅作概率参考,实盘前务必在策略测试器跑过历史样本。

MQL5 / C++
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单序列还是多序列模型。

MQL5 / C++
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 出来的维度是否对得上,外汇与贵金属波动剧烈、模型误判概率不低,仅作辅助信号。

MQL5 / C++
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 若和你训练时不一致,说明导出图被重命名,推理会直接报维度错。

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

MQL5 / C++
  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 分支能触发;外汇与贵金属波动剧烈,特征缩放参数失效会让模型信号失真,属于高概率踩坑点。

MQL5 / C++
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 回放验证推理一致性。

MQL5 / C++
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 节点跑,外汇与贵金属杠杆高、滑点跳空频繁,任何模型信号都只是概率倾向,别把跨框架推理这层皮当成胜率保证。

MQL5 / C++
class="macro">#include <preprocessing.mqh>
fit_transform( const matrix &X)
transform( const matrix &X)
transform(const vector &X)
让小布替你盯模型信号
这些ONNX模型的实时推理结果和小布盯盘的AIGC信号面板已打通,打开对应品种页就能直接看到模型输出的多空倾向,你只管结合价格行为做决策。

常见问题

先打印模型输入输出签名确认shape,常见问题是把时间序列展平方式弄反;用Python端导出时固定batch维为1再转ONNX更稳。
目前小布内置了常见分类与回归模型的推理展示,自定义模型可通过品种页信号区查看输出倾向,具体接入以客户端版本为准。
推理放在独立计算图且控制调用频率,大概率只在新K线或事件触发时跑一次,对订单执行影响有限,但仍需在高波动时段实测。
没有固定胜率,市场高杠杆高风险,模型只提供概率倾向;实盘前请用历史分窗回测并观察样本外衰减。
理解底层有助于排查ONNX数值偏差,但日常部署建议用ONNX,关于完整讨论见《掌握ONNX:MQL5交易者的游戏规则改变者·基础篇》。