用于时间序列挖掘的数据标签(第 6 部分):使用 ONNX 在 EA 中应用和测试·综合运用
📘

用于时间序列挖掘的数据标签(第 6 部分):使用 ONNX 在 EA 中应用和测试·综合运用

第 3/3 篇

◍ 跑通 ONNX 模型的第一次回测

动手前先确认一件事:你训练好的 ONNX 模型文件必须和 onnx.mq5 放在同一目录,否则加载阶段就会报错断在 OnInit。这是很多新手卡住的第一步,不是代码逻辑问题,是路径约定。 在 MT5 编辑器里点编译,生成 ex5 后按 Ctrl+F5 进调试模式回测,会弹出独立窗口跑测试过程。我本地日志最后一行是「我们成功了!」,说明从缓冲区创建模型句柄、读输入张量这一条链路是通的。 下面这段是最小可跑的初始化与释放骨架,重点看资源声明和 OnnxCreateFromBuffer 的调用方式:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 onnx.mq5 |
class=class="str">"cmt">//|                                         Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#resource "NBeats.onnx" as class="type">uchar ExtModel[]
class="type">long handle;
vectorf y=vector<class="type">float>::Zeros(class="num">20);
vectorf backcast=vector<class="type">float>::Zeros(class="num">96);
vectorf trend=vector<class="type">float>::Zeros(class="num">116);
vectorf seasonality=vector<class="type">float>::Zeros(class="num">116);
vectorf generic=vector<class="type">float>::Zeros(class="num">116);
class=class="str">"cmt">//class="type">bool is_pre=class="kw">false;
class="type">class="kw">string pre=NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   EventSetTimer(class="num">300);
   handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS);
   class=class="str">"cmt">//— specify the shape of the class="kw">input data 
   class="type">long in_ct=OnnxGetInputCount(handle);
   OnnxTypeInfo inf;
   for(class="type">int i=class="num">0;i<in_ct;i++){

   Print(OnnxGetInputName(handle,i));
   class="type">bool re=OnnxGetInputTypeInfo(handle,i,inf);
   class=class="str">"cmt">//Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type);
   Print(re,GetLastError());
   }
   class=class="str">"cmt">//class="type">long in_nm=OnnxGetInputName()


class=class="str">"cmt">//— class="kw">return initialization result 
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
   class=class="str">"cmt">//— destroy timer 
  EventKillTimer();
  class=class="str">"cmt">//— complete operation 
  OnnxRelease(handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行拆一下关键处:#resource "NBeats.onnx" as uchar ExtModel[] 把模型以字节数组编进 ex5,避免外部依赖;OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS) 从内存缓冲建句柄,开调试日志方便看张量名;EventSetTimer(300) 设 300 毫秒心跳,回测里用来周期触发;OnnxGetInputCount 配合循环打印输入名和类型,确认模型接口形状(如 backcast 长 96、trend 长 116)。外汇与贵金属杠杆高,回测通过仅代表链路通,实盘信号概率仍受滑点与点差干扰。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 onnx.mq5 |
class=class="str">"cmt">//|                                         Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#resource "NBeats.onnx" as class="type">uchar ExtModel[]
class="type">long handle;
vectorf y=vector<class="type">float>::Zeros(class="num">20);
vectorf backcast=vector<class="type">float>::Zeros(class="num">96);
vectorf trend=vector<class="type">float>::Zeros(class="num">116);
vectorf seasonality=vector<class="type">float>::Zeros(class="num">116);
vectorf generic=vector<class="type">float>::Zeros(class="num">116);
class=class="str">"cmt">//class="type">bool is_pre=class="kw">false;
class="type">class="kw">string pre=NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   EventSetTimer(class="num">300);
   handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS);
   class=class="str">"cmt">//— specify the shape of the class="kw">input data 
   class="type">long in_ct=OnnxGetInputCount(handle);
   OnnxTypeInfo inf;
   for(class="type">int i=class="num">0;i<in_ct;i++){

   Print(OnnxGetInputName(handle,i));
   class="type">bool re=OnnxGetInputTypeInfo(handle,i,inf);
   class=class="str">"cmt">//Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type);
   Print(re,GetLastError());
   }
   class=class="str">"cmt">//class="type">long in_nm=OnnxGetInputName()


class=class="str">"cmt">//— class="kw">return initialization result 
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
   class=class="str">"cmt">//— destroy timer 
  EventKillTimer();
  class=class="str">"cmt">//— complete operation 
  OnnxRelease(handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「用 OnTick 把预测信号变成下单动作」

EA 的核心执行单元是 OnTick(),每次报价跳动都会触发。把上游模型输出的预测字符串 pre 接进这里,就能让 MT5 自动完成持仓检查与报单,不需要手动干预。 下面这段逻辑先清掉旧请求内存,再用 PositionsTotal() 抓当前持仓数。若已有仓位,取最后一单的 ticket 并读取 POSITION_TYPE:如果预测是 buy 但手里已是多单,直接 return 不重复开仓;若是空单则把 request.position 设为该 ticket,走平仓再反手。 报单参数写死 volume=0.1、deviation=5、type_filling=ORDER_FILLING_IOC,价格取 SYMBOL_ASK。外汇与贵金属杠杆高,0.1 手虽小,滑点扩大时仍可能触发非预期亏损,上线前建议在策略测试器用真实点差回测。 别把预测字符串当圣旨 pre 等于 "buy" 才动作,但模型误判时这条分支会频繁平仓反手,佣金和库存费会吃掉概率优势。建议先打印 pre 跑一周观察信号分布,再决定要不要接 OrderSend。

MQL5 / C++
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="type">MqlTradeRequest request;
   class="type">MqlTradeResult result;
   class=class="str">"cmt">//class="type">int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
    if (pre!=NULL)
    {
      class=class="str">"cmt">//Print("The predicted value is:",pre);
      class="type">class="kw">ulong numt=class="num">0;
      class="type">class="kw">ulong tik=class="num">0;
      class="type">bool sod=class="kw">false;
      class="type">class="kw">ulong tpt=-class="num">1;
      ZeroMemory(request);
      numt=PositionsTotal();
      class=class="str">"cmt">//Print("All tickets: ",numt);
      if (numt>class="num">0)
       {  tik=PositionGetTicket(numt-class="num">1);
          sod=PositionSelectByTicket(tik);
          tpt=PositionGetInteger(POSITION_TYPE);class=class="str">"cmt">//ORDER_TYPE_BUY or ORDER_TYPE_SELL
          if (tik==class="num">0 || sod==class="kw">false || tpt==class="num">0) class="kw">return;
          }
      if (pre=="buy")
      {  
         if (tpt==POSITION_TYPE_BUY)
            class="kw">return;
            
         request.action=TRADE_ACTION_DEAL;
         request.symbol=Symbol();
         request.volume=class="num">0.1;
         request.deviation=class="num">5;
         request.type_filling=ORDER_FILLING_IOC;
         request.type = ORDER_TYPE_BUY;  
         request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
       if(tpt==POSITION_TYPE_SELL)
          {
           request.position=tik;
           Print("Close sell order.");
               }
       else{  

           Print("Open buy order.");
               }
         OrderSend(request, result);
         }
      else{
       if (tpt==POSITION_TYPE_SELL)
            class="kw">return;

用均值方差把收盘价喂给模型

这段逻辑在 OnTimer 里跑,先把最近 96 根 K 线的收盘价抓进 96×1 矩阵 in0_m,每根只取 close 字段。 接着算整列均值 m 和标准差 s,再把这两个标量分别铺成 96 行相同的 mm 和 ms 矩阵,用 in0_m 减均值、除标准差完成标准化。标准化后转成 float 矩阵 in_normf,才能直接丢进 ONNX 模型做推断。 顺带一提,上面那段交易请求代码里,挂的是市价卖单:volume 写死 0.1、deviation 给 5 点、type_filling 用 IOC,价格取当前 SYMBOL_BID。若持仓是 BUY 就打印 Close buy order 并绑定 position ticket 去平,否则打印 OPen sell order 直接开空。 外汇和贵金属杠杆高,这段代码真在 MT5 实盘跑之前,务必先在策略测试器用 0.1 手回测,确认标准化和下单分支不会在极端点差下误触发。

MQL5 / C++
request.action = TRADE_ACTION_DEAL;
request.symbol = Symbol();
request.volume = class="num">0.1;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);
request.deviation = class="num">5;
class=class="str">"cmt">//request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
request.type_filling=ORDER_FILLING_IOC;
 if(tpt==POSITION_TYPE_BUY)
  {
   request.position=tik;
   Print("Close buy order.");
  }
 else{
   Print("OPen sell order.");
  }

OrderSend(request, result);
	}
   class=class="str">"cmt">//is_pre=class="kw">false;
   }
 pre=NULL;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
{
 class=class="str">"cmt">//class="type">float in0[class="num">1][class="num">96][class="num">1];
 matrixf in_normf;
 class="type">float in1[class="num">1][class="num">2];
class=class="str">"cmt">//— get the last class="num">10 bars 
 class="type">MqlRates rates[];
 if(!CopyRates(_Symbol,_Period,class="num">0,class="num">96,rates)) class="kw">return;
class=class="str">"cmt">//— class="kw">input a set of OHLC vectors 
 class=class="str">"cmt">//class="type">class="kw">double out[class="num">1][class="num">20];
 matrix in0_m(class="num">96,class="num">1);
 for(class="type">int i=class="num">0; i<class="num">96; i++)
  {
   in0_m[i][class="num">0]= rates[i].close;
   }
 class=class="str">"cmt">//— normalize the class="kw">input data 
 class=class="str">"cmt">// matrix x_norm=x; 
  vector m=in0_m.Mean(class="num">0);  
  vector s=in0_m.Std(class="num">0);
  
  in1[class="num">0][class="num">0]=m[class="num">0];
  in1[class="num">0][class="num">1]=s[class="num">0];
  matrix mm(class="num">96,class="num">1);
  matrix ms(class="num">96,class="num">1);
 class=class="str">"cmt">//    //— fill in the normalization matrices 
  for(class="type">int i=class="num">0; i<class="num">96; i++)
  {
     mm.Row(m,i);  
     ms.Row(s,i);
     }
  class=class="str">"cmt">//    //— normalize the class="kw">input data 
  in0_m-=mm;  
  in0_m/=ms;
  class=class="str">"cmt">// //— convert normalized class="kw">input data to class="type">float type 
 
  in_normf.Assign(in0_m);
   class=class="str">"cmt">//— get the output data of the model here, i.e. the price prediction 

◍ 跑模型并拿均值判多空

调用 OnnxRun 时把调试日志与禁止自动转换两个标志位叠用,输入张量 in_normf、in1 进模型,输出 y、backcast、trend、seasonality、generic 一并取回。若返回失败,直接打印错误码并释放句柄退出,避免句柄泄漏拖慢 EA。 模型输出 y 是个向量,取 Mean() 后和当前 K 线的高低中点比:中点 = iHigh(_Symbol,_Period,0)/2 + iLow(_Symbol,_Period,0)/2。y 均值高于该中点就给 pre 赋 "buy",否则 "sell"。 这套判定只依赖单根 K 线中点,对外汇与贵金属这类高波动品种仅作概率倾向参考,实盘前务必在 MT5 策略测试器里用历史 tick 验证信号翻转频率。

MQL5 / C++
  class=class="str">"cmt">//— run the model 
  if(!OnnxRun(handle,
              ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION,
              in_normf,
              in1,
              y,
              backcast,
              trend,
              seasonality,
              generic))
    {
     Print("OnnxRun failed, error ",GetLastError());
     OnnxRelease(handle);
     class="kw">return;
     }
  class=class="str">"cmt">//— print the output value of the model to the log 
  class=class="str">"cmt">//Print(y); 
  class=class="str">"cmt">//is_pre=true;
  if (y.Mean()>iHigh(_Symbol,_Period,class="num">0)/class="num">2+iLow(_Symbol,_Period,class="num">0)/class="num">2)
     pre="buy";
  else
     pre="sell";
}

「把 NBeats 输出画到图上才有用」

整套流程走完,最核心的产出其实是一个 6949 KB 的 NBeats.onnx 文件,加上 11.99 KB 的 onnx.mq5 适配器。模型能从历史序列里拆出趋势项和季节项,但原版测试 EA 只把结果用于内部判断,图表上什么都看不见。 一个直接能做的改造:在 onnx.mq5 里把 NBeats 输出的 trend 和 seasonal 分量用 PlotIndexSetDouble 画成两条独立线。这样你开 MT5 加载 EA 后,能直观看到模型认为的「惯性方向」和「周期扰动」分家到什么程度,比只看信号箭头实在。 外汇和贵金属杠杆高、滑点随机,这套 ONNX 推理只是概率层面的辅助,别把输出当下单指令直接跑实盘。先在本机回测里把两条线调出来,确认分量与价格走势的领先滞后关系,再谈扩展。

常见问题

先确认输入特征的均值和方差与训练时完全一致,否则回测结果会严重失真;建议先用一小段历史跑通再放大样本。
在代码中加信号状态锁或订单存在判断,只在预测方向发生变化且当前无同方向持仓时才触发下单。
可以,小布盯盘的 AIGC 看板能直接加载模型输出曲线并和价格同屏,省去自己写绘图代码的麻烦。
预测值会整体偏移,多空判定阈值失效;务必用训练集统计的同一组均值方差,不能拿回测段重算。
看输出曲线拐点是否稳定领先价格拐点几个 bar,且样本外不漂移;只画不验等于没跑模型。