在类中包装 ONNX 模型·进阶篇
📘

在类中包装 ONNX 模型·进阶篇

第 2/3 篇

EURUSD日线10根K线的分类模型封装

第一个落地模型叫 model.eurusd.D1.10.class.onnx,它用 EURUSD 的 D1 周期、连续 10 根 OHLC 价格序列训练出一个分类器。三种模型只在输入数据规模和预处理上有区别,这个类只重写了 Init 和 PredictClass 两个方法,其余逻辑留在基类里复用。 Init 里先调基类 CheckInit 创建 ONNX 会话,再显式把输入张量形状钉死为 {1, 10, 4}:第一维是 batch,第二维是 10 根 K 线,第三维是 OHLC 四个通道。注释比代码多,是因为 ONNX 导出的模型没把全部维度写进输入描述,不手动设就会推理报错。 PredictClass 要求输入数据和训练时完全一致——常规化(normalized)后的 OHLC 矩阵。外汇和贵金属杠杆高、跳空频繁,这种日线级模型只给出分类倾向,实盘前务必在 MT5 用历史数据跑一遍一致性校验。 下面这段是类定义的头半部分,重点看资源挂载和形状写死的位置。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ModelEurusdD1_10Class.mqh |
class=class="str">"cmt">//|                                                                 Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                                      [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "ModelSymbolPeriod.mqh"
class="macro">#resource "Python/model.eurusd.D1.class="num">10.class.onnx" as class="type">uchar model_eurusd_D1_10_class[]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ONNX-model wrapper class                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CModelEurusdD1_10Class : class="kw">public CModelSymbolPeriod
  {
class="kw">private:
   class="type">int               m_sample_size;
class="kw">public:
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| Constructor                                                                  |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   CModelEurusdD1_10Class(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1)
     {
      m_sample_size=class="num">10;
     }
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| ONNX-model initialization                                                      |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">virtual class="type">bool Init(const class="type">class="kw">string symbol, const ENUM_TIMEFRAMES period)
     {
      class=class="str">"cmt">//--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_10_class))
        {
         Print("model_eurusd_D1_10_class : initialization error");
         class="kw">return(false);
        }
      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(OHLC)
      const class="type">long input_shape[] = {class="num">1,m_sample_size,class="num">4};
      if(!OnnxSetInputShape(m_handle,class="num">0,input_shape))
        {
         Print("model_eurusd_D1_10_class : OnnxSetInputShape error ",GetLastError());
         class="kw">return(false);
        }

◍ 给ONNX输出张量显式定形与归一化推理

把训练好的 EURUSD D1 分类模型接进 MT5,第一步不是跑推理,而是先把输出张量的形状钉死。OnnxSetOutputShape 的第二个参数填 0 表示第 0 个输出节点,形状数组 {1,3} 里 1 是 batch(必须和输入张量 batch 一致),3 对应涨 / 平 / 跌三类。若返回 false,直接打印 GetLastError 并退出,避免后续拿到空结果还当信号用。 预测阶段真正吃算力的是归一化。代码里先 CopyRates 拉最近 m_sample_size 根 OHLC 柱,用 Mean(1) 和 Std(1) 沿列求均值与标准差,再铺成同尺寸的 mm、ms 矩阵做逐元素减均值除标准差。注意模型要的是竖向 OHLC 向量,所以 rates 必须 Transpose 转置后再归一,否则维度对不上 OnnxRun 会直接报错返回 -1。 外汇与贵金属自带高杠杆与跳空风险,这套归一化推理只解决「喂给模型的数据长相对」,不保证分类命中率;实盘前建议在策略测试器用 2020—2023 年数据回测,观察三类输出的概率分布是否随波动率 regime 偏移。

MQL5 / C++
const class="type">long output_shape[] = {class="num">1,class="num">3};
if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape))
  {
   Print("model_eurusd_D1_10_class : OnnxSetOutputShape error ",GetLastError());
   class="kw">return(false);
  }
class="kw">return(true);

class="kw">virtual class="type">int PredictClass(class="type">void)
  {
   class="kw">static matrixf input_data(m_sample_size,class="num">4);
   class="kw">static vectorf output_data(class="num">3);
   class="kw">static matrix  mm(m_sample_size,class="num">4);
   class="kw">static matrix  ms(m_sample_size,class="num">4);
   class="kw">static matrix  x_norm(m_sample_size,class="num">4);

   matrix rates;
   if(!rates.CopyRates(m_symbol,m_period,COPY_RATES_OHLC,class="num">1,m_sample_size))
      class="kw">return(-class="num">1);
   vector m=rates.Mean(class="num">1);
   vector s=rates.Std(class="num">1);
   for(class="type">int i=class="num">0; i<m_sample_size; i++)
     {
      mm.Row(m,i);
      ms.Row(s,i);
     }
   x_norm=rates.Transpose();
   x_norm-=mm;
   x_norm/=ms;

   input_data.Assign(x_norm);
   if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))
      class="kw">return(-class="num">1);

「用 ArgMax 把网络输出转成交易信号」

上面这段闭包是预测评估的收口:模型前向跑完之后,不直接把浮点向量抛给策略层,而是用 ArgMax 取最大分量下标,再强转成 int 返回。 ArgMax 的语义很直白——哪一类神经元的激活值最高,就判为当前样本所属类别。在分类式行情判别里,这步相当于把「概率分布」压缩成「唯一标签」,下游 EA 拿到 0/1/2 这类离散值就能直接进规则判断。 需要注意,ArgMax 只给「最可能」那一类,不附带置信度。若两类输出接近,返回结果可能在相邻标签间跳变,实盘里建议额外读 output_data 原始分量做阈值过滤,外汇与贵金属波动剧烈,纯靠硬分类信号进场属于高风险操作。

MQL5 / C++
class=class="str">"cmt">//--- evaluate prediction
   class="kw">return(class="type">int(output_data.ArgMax()));
   }
};
class=class="str">"cmt">//+------------------------------------------------------------------+

用紧凑EA验证类预测能力

为验证封装类的实际表现,我们搭了一个极简智能交易系统,不堆指标、不绕逻辑,只做一件事:把类输出的预测类别直接转成开平仓动作。 模型训练数据截止 2022 年末,所以测试窗口选在 2023 年 1 月 1 日之后,避免未来信息泄漏。跑下来模型能正常吐出预测类别并触发交易,说明类的接口在实盘 tick 流下是通的。 下面这段 EA 源码就是验证载体,核心在 OnTick 里:先让 ExtModel.CheckOnTick 过滤非新柱,再拿 PredictClass 取类别,>=0 才进 PositionSelect 分支决定开或平。 外汇与贵金属属于高风险品种,这类基于历史数据训练的模型在样本外可能失效,任何回测通过都不构成稳健盈利保证,上 MT5 前请先用策略测试器跑 2023 年数据自查。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ONNX.eurusd.D1.Prediction.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">#include "ModelEurusdD1_10Class.mqh"
class="macro">#include <Trade\Trade.mqh>
input class="type">class="kw">double InpLots = class="num">1.0;     class=class="str">"cmt">// Lots amount to open position
CModelEurusdD1_10Class ExtModel;
CTrade                 ExtTrade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   if(!ExtModel.Init(_Symbol,_Period))
      class="kw">return(INIT_FAILED);
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(const class="type">int reason)
  {
   ExtModel.Shutdown();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   if(!ExtModel.CheckOnTick())
      class="kw">return;
class=class="str">"cmt">//--- predict next price movement
   class="type">int predicted_class=ExtModel.PredictClass();
class=class="str">"cmt">//--- check trading according to prediction
   if(predicted_class>=class="num">0)
      if(PositionSelect(_Symbol))
         CheckForClose(predicted_class);
      else
         CheckForOpen(predicted_class);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check for open position conditions                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckForOpen(const class="type">int predicted_class)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
class=class="str">"cmt">//--- check signals

◍ 信号触发后的开仓与反手逻辑

模型给出预测分类后,EA 并不直接下单,而是先把它映射成 MT5 的订单方向常量。predicted_class 等于 PRICE_DOWN 时记空单信号,等于 PRICE_UP 时记多单信号,其余情况 signal 保持 WRONG_VALUE 不动作。 开仓前用 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 确认终端允许交易,再按信号方向取 BID 或 ASK 报价。ExtTrade.PositionOpen 以 InpLots 手数市价入场,滑点和止损止盈暂传 0,实盘需自行补参数。 平仓判断在 CheckForClose 里完成:已持多单且预测转空、或已持空单且预测转多,bsignal 置真。此时先 PositionClose(_Symbol,3) 平掉原仓,3 为异步关闭模式偏差值,随后立刻调用 CheckForOpen 反手开反向单。 外汇与贵金属杠杆高,这类预测反手链在震荡市可能连续挨扫,建议先在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍,观察 signal 翻转频率再决定要不要加过滤。

MQL5 / C++
  if(predicted_class==PRICE_DOWN)
      signal=ORDER_TYPE_SELL;    class=class="str">"cmt">// sell condition
  else
   {
    if(predicted_class==PRICE_UP)
       signal=ORDER_TYPE_BUY;   class=class="str">"cmt">// buy condition
   }
class=class="str">"cmt">//--- open position if possible according to signal
  if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
   {
    class="type">class="kw">double price=SymbolInfoDouble(_Symbol,(signal==ORDER_TYPE_SELL) ? SYMBOL_BID : SYMBOL_ASK);
    ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,class="num">0,class="num">0);
   }
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check for close position conditions                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckForClose(const class="type">int predicted_class)
  {
  class="type">bool bsignal=false;
class=class="str">"cmt">//--- position already selected before
  class="type">long type=PositionGetInteger(POSITION_TYPE);
class=class="str">"cmt">//--- check signals
  if(type==POSITION_TYPE_BUY && predicted_class==PRICE_DOWN)
     bsignal=true;
  if(type==POSITION_TYPE_SELL && predicted_class==PRICE_UP)
     bsignal=true;
class=class="str">"cmt">//--- close position if possible
  if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
   {
    ExtTrade.PositionClose(_Symbol,class="num">3);
    class=class="str">"cmt">//--- open opposite
    CheckForOpen(predicted_class);
   }
  }

「EURUSD日线30根K线的分类模型怎么接」

第二类模型文件叫 model.eurusd.D1.30.class.onnx,训练输入是 EURUSD 日线连续 30 个收盘价,加上周期 21 与 34 的两条 SMA。和前一类一样,Init 里直接调基类的 CheckInit,由它建 ONNX 会话并显式给定输入输出张量尺寸——这点不能省,因为模型文件里没把输入维度写全。 PredictClass 负责喂入 30 根历史收盘价和算好的两条均线,归一化方式必须和训练时一致,否则输出类别会漂移。验证时只需改测试 EA 两行代码、参数不动,跑出来模型是有效的,说明换品种周期类封装的接口是通的。 外汇与贵金属杠杆高、滑点大,这类日线分类信号只代表历史样本下的倾向,实盘前务必在 MT5 策略测试器用 EURUSD D1 复跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ModelEurusdD1_30Class.mqh |
class=class="str">"cmt">//|                                                 Copyright class="num">2023, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "ModelSymbolPeriod.mqh"
class="macro">#resource "Python/model.eurusd.D1.class="num">30.class.onnx" as class="type">uchar model_eurusd_D1_30_class[]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ONNX-model wrapper class                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CModelEurusdD1_30Class : class="kw">public CModelSymbolPeriod
  {
class="kw">private:
   class="type">int               m_sample_size;
   class="type">int               m_fast_period;
   class="type">int               m_slow_period;
   class="type">int               m_sma_fast;
   class="type">int               m_sma_slow;
class="kw">public:
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| Constructor                                                      |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   CModelEurusdD1_30Class(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1)
     {
      m_sample_size=class="num">30;
      m_fast_period=class="num">21;
      m_slow_period=class="num">34;
      m_sma_fast=INVALID_HANDLE;
      m_sma_slow=INVALID_HANDLE;
     }
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| ONNX-model initialization                                        |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">virtual class="type">bool Init(const class="type">class="kw">string symbol, const ENUM_TIMEFRAMES period)
     {
      class=class="str">"cmt">//--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_30_class))
        {
         Print("model_eurusd_D1_30_class : initialization error");
         class="kw">return(false);
        }
      class=class="str">"cmt">//--- since not all sizes defined in the input tensor we must set them explicitly

常见问题

在类里把输入显式写成 [1,10,5] 这类形状(样本数×K线根数×特征数),并在推理前做 min-max 或 z-score 归一化,否则多数运行时直接抛异常。
对输出张量做 ArgMax 取最大下标,映射成 0=空、1=多、2=观望,EA 里据此触发开仓或反手,避免人工读概率。
可以,小布已内置 AIGC 模型诊断与信号标注,打开 EURUSD 日线页就能看到分类倾向与触发提示,你只做决策。
若持反向仓先平仓再开新仓,同向后跳过;日线周期下建议加滑点容差,外汇贵金属波动大,实盘前先历史回看。
只改类里输入形状与归一化窗口为 [1,30,5],特征提取逻辑不变;重新编译后 EA 直接调用,注意样本不足 30 根时跳过推理。