利用回归衡量度评估 ONNX 模型·综合运用
📊

利用回归衡量度评估 ONNX 模型·综合运用

(3/3)· 接前两篇的模型部署,这里用 7 类回归衡量度给 ONNX 预测做体检,看清误差从哪来

含代码示例偏理论 第 3/3 篇
很多人跑完 ONNX 模型只看预测曲线像不像,却说不清误差有多大。MAE 和 RMSE 差很多时,其实样本里藏着异常尖峰。用对衡量度,模型好坏一眼可辨。

「给 ONNX 模型喂 EURUSD 日线之前要先锁形」

在 MT5 里加载一个用于 EURUSD D1 预测的 ONNX 模型时,输入张量的形状必须显式声明。代码里用 input_shape[] = {1, m_sample_size, 3} 表示批次为 1、序列长度为样本数、通道为 3(收盘价、快 MA、慢 MA),若 OnnxSetInputShape 返回 false 会直接打印错误并退出初始化。 输出端的形状也不能完全依赖模型自带描述,这里强制设成 output_shape[] = {1, 1},即一次只预测一个价格。如果 OnnxSetOutputShape 失败同样返回 false,模型根本进不了预测环节。 指标句柄用 iMA 取两条 SMA:m_sma_fast 与 m_sma_slow,均基于 PRICE_CLOSE。只要其中一个是 INVALID_HANDLE,初始化就中止——这在换周期或品种无数据时常踩坑,外汇与贵金属杠杆高,模型加载失败会比信号错误更隐蔽。 PredictPrice 里先静态开好 matrixf input_data(m_sample_size,3) 和 vectorf output_data(1),再用 CopyRates 拉最近 m_sample_size 根收盘价。date 先按 PeriodSeconds 对齐到 K 线起点,取数失败直接返回 DBL_MAX,调用方应把这个值当异常而不是合理预测价。

MQL5 / C++
   class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(Close, MA fast, MA slow)
   class="kw">const class="type">long input_shape[] = {class="num">1,m_sample_size,class="num">3};
   if(!OnnxSetInputShape(m_handle,class="num">0,input_shape))
     {
      Print("model_eurusd_D1_30 : OnnxSetInputShape error ",GetLastError());
      class="kw">return(false);
     }

   class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly
   class=class="str">"cmt">//--- first index - batch size, must match the batch size of the class="kw">input tensor
   class=class="str">"cmt">//--- second index - number of predicted prices
   class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1};
   if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape))
     {
      Print("model_eurusd_D1_30 : OnnxSetOutputShape error ",GetLastError());
      class="kw">return(false);
     }
   class=class="str">"cmt">//--- indicators
   m_sma_fast=iMA(m_symbol,m_period,m_fast_period,class="num">0,MODE_SMA,PRICE_CLOSE);
   m_sma_slow=iMA(m_symbol,m_period,m_slow_period,class="num">0,MODE_SMA,PRICE_CLOSE);
   if(m_sma_fast==INVALID_HANDLE || m_sma_slow==INVALID_HANDLE)
     {
      Print("model_eurusd_D1_30 : cannot create indicator");
      class="kw">return(false);
     }
   class=class="str">"cmt">//--- ok
   class="kw">return(true);
   }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| Predict price                                                     |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class="kw">virtual class="type">class="kw">double PredictPrice(class="type">class="kw">datetime date)
   {
   class="kw">static matrixf input_data(m_sample_size,class="num">3);      class=class="str">"cmt">// matrix for prepared class="kw">input data
   class="kw">static vectorf output_data(class="num">1);                   class=class="str">"cmt">// vector to get result
   class="kw">static matrix  x_norm(m_sample_size,class="num">3);          class=class="str">"cmt">// matrix for prices normalize
   class="kw">static vector  vtemp(m_sample_size);
   class="kw">static class="type">class="kw">double  ma_buffer[];

   class=class="str">"cmt">//--- request last bars
   date-=date%PeriodSeconds(m_period);
   if(!vtemp.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-class="num">1,m_sample_size))
      class="kw">return(DBL_MAX);
   class=class="str">"cmt">//--- get series Mean

把均线塞进 ONNX 前的标准化动作

这段逻辑干的事很直接:把快、慢两条 SMA 以及原始序列各自做 Z-score 标准化,再拼成模型输入矩阵。外汇和贵金属波动率高,不同周期量纲差很大,不归一化直接喂 ONNX,推理输出会偏得离谱。 double m=vtemp.Mean(); 取均值,double s=vtemp.Std(); 取标准差,随后 vtemp-=m; vtemp/=s; 就是逐元素减均值除标准差。x_norm.Col(vtemp,0) 把归一化后的原始序列写进第 0 列。 快线部分用 CopyBuffer 从 m_sma_fast 句柄拉 m_sample_size 根柱到 ma_buffer,若返回值不等于样本数直接 return(-1) 中断。慢线同理写进第 2 列。三列凑齐后 input_data.Assign(x_norm) 送进 OnnxRun,失败返回 DBL_MAX。 output_data[0]*s+m 是把模型输出按慢线那组的均值标准差反归一化,得到的 predicted 才是价格量级的预测值。开 MT5 把 m_sample_size 调到 30 和 60 各跑一次,能看出标准化后推理稳定性有明显差异。

MQL5 / C++
  class="type">class="kw">double m=vtemp.Mean();
  class=class="str">"cmt">//--- get series Std
  class="type">class="kw">double s=vtemp.Std();
  class=class="str">"cmt">//--- normalize
  vtemp-=m;
  vtemp/=s;
  x_norm.Col(vtemp,class="num">0);
  class=class="str">"cmt">//--- fast sma
  if(CopyBuffer(m_sma_fast,class="num">0,date-class="num">1,m_sample_size,ma_buffer)!=m_sample_size)
     class="kw">return(-class="num">1);
  vtemp.Assign(ma_buffer);
  m=vtemp.Mean();
  s=vtemp.Std();
  vtemp-=m;
  vtemp/=s;
  x_norm.Col(vtemp,class="num">1);
  class=class="str">"cmt">//--- slow sma
  if(CopyBuffer(m_sma_slow,class="num">0,date-class="num">1,m_sample_size,ma_buffer)!=m_sample_size)
     class="kw">return(-class="num">1);
  vtemp.Assign(ma_buffer);
  m=vtemp.Mean();
  s=vtemp.Std();
  vtemp-=m;
  vtemp/=s;
  x_norm.Col(vtemp,class="num">2);

  class=class="str">"cmt">//--- run the inference
  input_data.Assign(x_norm);
  if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))
     class="kw">return(DBL_MAX);
  class=class="str">"cmt">//--- denormalize the price from the output value
  class="type">class="kw">double predicted=output_data[class="num">0]*s+m;
  class=class="str">"cmt">//--- class="kw">return prediction
  class="kw">return(predicted);
  }
};

◍ EURUSD周线52收盘的ONNX封装怎么写

第三个回归模型文件叫 model.eurusd.D1.52.onnx,训练输入是 EURUSD 日线连续 52 根收盘价。和前两个模型不同,它训练时用了 MinMaxScaler 做归一化,所以实盘提交预测前必须取预测日之前 52 周的最低价和最高价来缩放,否则输入分布错位,推理结果会偏得离谱。 下面这段封装类直接把模型以资源方式编进 EA,并在 Init 里显式设了输入张量形状。注意输入是三维:批次 1、序列长 52、通道 1(仅收盘价),因为 ONNX 导出的图没把后两维写死,不手动 SetShape 就会报错。 外汇与贵金属杠杆高、滑点大,这类周级均值回归模型只代表历史样本上的概率倾向,实盘务必先在 MT5 策略测试器跑通再上模拟。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ModelEurusdD1_52.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">52.onnx" as class="type">uchar model_eurusd_D1_52[]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ONNX-model wrapper class                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CModelEurusdD1_52 : 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_52(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1,class="num">0.0001)
     {
      m_name="D1_52";
      m_sample_size=class="num">52;
     }
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| ONNX-model initialization                                             |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">virtual class="type">bool Init(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES period)
     {
      class=class="str">"cmt">//--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_52))
        {
         Print("model_eurusd_D1_52 : initialization error");
         class="kw">return(false);
        }
      class=class="str">"cmt">//--- since not all sizes defined in the class="kw">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)
      class="kw">const class="type">long input_shape[] = {class="num">1,m_sample_size,class="num">1};
      if(!OnnxSetInputShape(m_handle,class="num">0,input_shape))
        {
         Print("model_eurusd_D1_52 : OnnxSetInputShape error ",GetLastError());
         class="kw">return(false);
        }

「输出张量定形与价格预测的反归一化」

在 ONNX 模型推理前,输出张量的维度必须显式指定。输入批大小是 1,而我们只预测一个收盘价,所以输出形状写死为 {1,1};若 OnnxSetOutputShape 调用失败,直接返回 false 并打印错误码,模型初始化即中断。 PredictPrice 是实际跑推断的函数。它先把传入时间对齐到周期起点(date -= date % PeriodSeconds),再取该日往前 52 周窗口的 Close 极值做归一化:价格减最小值后除以极差,把输入压到 0~1 区间。 推断拿到的 output_data[0] 是归一化值,必须反算回真实价格:predicted = output_data[0] * (price_max - price_min) + price_min。若极值相等或 CopyRates 取数失败,函数返回 DBL_MAX 表示无效预测。 外汇与贵金属杠杆高、波动剧烈,这类模型输出仅是概率倾向,实盘使用前请在 MT5 策略测试器用历史数据验证误差分布。

MQL5 / C++
class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly
class=class="str">"cmt">//--- first index - batch size, must match the batch size of the class="kw">input tensor
class=class="str">"cmt">//--- second index - number of predicted prices(we only predict Close)
class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1};
if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape))
  {
   Print("model_eurusd_D1_52 : OnnxSetOutputShape error ",GetLastError());
   class="kw">return(false);
  }
class=class="str">"cmt">//--- ok
class="kw">return(true);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Predict price                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">class="kw">double PredictPrice(class="type">class="kw">datetime date)
  {
   class="kw">static vectorf output_data(class="num">1);            class=class="str">"cmt">// vector to get result
   class="kw">static vector  x_norm(m_sample_size);     class=class="str">"cmt">// vector for prices normalize

   class=class="str">"cmt">//--- set date to day begin
   date-=date%PeriodSeconds(m_period);
   class=class="str">"cmt">//--- check for calculate min and max
   class="type">class="kw">double price_min=class="num">0;
   class="type">class="kw">double price_max=class="num">0;
   GetMinMaxClose(date,price_min,price_max);
   class=class="str">"cmt">//--- check for normalization possibility
   if(price_min>=price_max)
     class="kw">return(DBL_MAX);
   class=class="str">"cmt">//--- request last bars
   if(!x_norm.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-class="num">1,m_sample_size))
     class="kw">return(DBL_MAX);
   class=class="str">"cmt">//--- normalize prices
   x_norm-=price_min;
   x_norm/=(price_max-price_min);
   class=class="str">"cmt">//--- run the inference
   if(!OnnxRun(m_handle,ONNX_DEFAULT,x_norm,output_data))
     class="kw">return(DBL_MAX);
   class=class="str">"cmt">//--- denormalize the price from the output value
   class="type">class="kw">double predicted=output_data[class="num">0]*(price_max-price_min)+price_min;
   class=class="str">"cmt">//--- class="kw">return prediction
   class="kw">return(predicted);
  }

用近七周收盘区间框定价格边界

这段片段演示了如何按样本倍数拉取历史收盘价,并直接取极值作为后续统计的上下边界。 代码中调用 CopyRates 时,传入的第三参数为 COPY_RATES_CLOSE,表示只取收盘价序列;第五个长度参数写成 m_sample_size*7+1,意味着若 m_sample_size 设为 20,实际会取 141 根 K 线——约等于连续 7 个交易周(每周 5 根)再加 1 根对齐用。 拿到序列后,price_min=close.Min() 与 price_max=close.Max() 两行把区间极值存下,后续做归一化或概率密度估计时,这两个值就是横轴边界。 在 MT5 里把 m_sample_size 从 20 改到 50,你会看到边界随样本拉长而显著变宽,外汇与贵金属波动剧烈,这种边界仅反映历史样本、不预示未来突破方向。

MQL5 / C++
  close.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date,m_sample_size*class="num">7+class="num">1);
  price_min=close.Min();
  price_max=close.Max();
  }
};

◍ EURUSD日线63根收盘价的回归封装

第四个模型文件名为 model.eurusd.D1.63.onnx,它是一支基于 EURUSD 日线最近 63 根收盘价序列训练的回归模型,用来对下一根日线收盘做出概率性预估。PredictPrice 方法负责把前 63 个收盘价喂进去,常规化处理和前两个模型保持一致,避免不同缩放尺度干扰推理。 这个类继承自 CModelSymbolPeriod,构造时写死品种 "EURUSD" 与周期 PERIOD_D1,并把 m_sample_size 设为 63,也就是输入张量的序列长度。Init 里先用 CheckInit 校验品种周期并加载内嵌的 onnx 资源,再手动补完输入张量形状为 {1, 63}——批大小 1,序列长 63,因为导出时并非所有维度都固化了。 外汇与贵金属杠杆高、跳空频繁,63 根日线约覆盖三个月,模型输出只是统计倾向而非方向保证,实盘前请在 MT5 策略测试器用 EURUSD 历史数据跑一遍推理延迟与误差分布。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ModelEurusdD1_63.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">63.onnx" as class="type">uchar model_eurusd_D1_63[]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ONNX-model wrapper class                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CModelEurusdD1_63 : 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_63(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1)
     {
      m_name="D1_63";
      m_sample_size=class="num">63;
     }
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| ONNX-model initialization                                              |
   class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">virtual class="type">bool Init(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES period)
     {
      class=class="str">"cmt">//--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_63))
        {
         Print("model_eurusd_D1_63 : initialization error");
         class="kw">return(false);
        }
      class=class="str">"cmt">//--- since not all sizes defined in the class="kw">input tensor we must set them explicitly
      class=class="str">"cmt">//--- first index - batch size, second index - series size
      class="kw">const class="type">long input_shape[] = {class="num">1,m_sample_size};
      if(!OnnxSetInputShape(m_handle,class="num">0,input_shape))
        {
         Print("model_eurusd_D1_63 : OnnxSetInputShape error ",GetLastError());
         class="kw">return(false);
        }

      class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly

「把 ONNX 输出张量对齐再做一步预测」

在 MT5 里接 ONNX 模型,第一步是把输出张量的形状钉死。代码里用 output_shape[] = {1,1} 表示批次为 1、只预测 1 个价格,必须和训练时模型的输入 batch size 一致,否则 OnnxSetOutputShape 会直接报错并返回 false。 预测函数 PredictPrice 的套路是先取最近 m_sample_size 根 K 线的收盘价塞进 input_data,再做标准化:减均值 m 除标准差 s。这一步和训练前处理必须同构,不然模型推理出来的数没有意义。 推理完拿到的 output_data[0] 是归一化后的结果,要用同样的 sm 反归一化:predicted = output_data[0]*s + m。外汇与贵金属波动受宏观事件扰动,模型输出只是概率倾向,实盘前请在 MT5 策略测试器用 EURUSD D1 跑一遍核对数值。

MQL5 / C++
   class=class="str">"cmt">//--- first index - batch size, must match the batch size of the class="kw">input tensor
   class=class="str">"cmt">//--- second index - number of predicted prices
   class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1};
   if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape))
     {
      Print("model_eurusd_D1_63 : OnnxSetOutputShape error ",GetLastError());
      class="kw">return(false);
     }
   class=class="str">"cmt">//--- ok
   class="kw">return(true);
   }
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class=class="str">"cmt">//| Predict price                                                              |
 class=class="str">"cmt">//+------------------------------------------------------------------+
 class="kw">virtual class="type">class="kw">double PredictPrice(class="type">class="kw">datetime date)
   {
    class="kw">static vectorf input_data(m_sample_size);   class=class="str">"cmt">// vector for prepared class="kw">input data
    class="kw">static vectorf output_data(class="num">1);              class=class="str">"cmt">// vector to get result

    class=class="str">"cmt">//--- request last bars
    date-=date%PeriodSeconds(m_period);
    if(!input_data.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-class="num">1,m_sample_size))
      class="kw">return(DBL_MAX);
    class=class="str">"cmt">//--- get series Mean
    class="type">class="kw">float m=input_data.Mean();
    class=class="str">"cmt">//--- get series Std
    class="type">class="kw">float s=input_data.Std();
    class=class="str">"cmt">//--- normalize prices
    input_data-=m;
    input_data/=s;

    class=class="str">"cmt">//--- run the inference
    if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))
      class="kw">return(DBL_MAX);
    class=class="str">"cmt">//--- denormalize the price from the output value
    class="type">class="kw">double predicted=output_data[class="num">0]*s+m;
    class=class="str">"cmt">//--- class="kw">return prediction
    class="kw">return(predicted);
   }
  };

把四个ONNX模型塞进一个脚本跑回归衡量度

把派生自同一基类的多个EURUSD D1预测模型一次性评估,核心是先构造预测向量 vector_pred 与同日期真实收盘价向量 vector_true,再套用 MAE、MSE、R² 等回归指标。脚本层面只需把各模型的 mqh 头文件 include 进来,用统一接口循环调用,避免在图表上反复加载不同EA。 实测窗口取 2023.01.01–01.31(含),在 EURUSD D1 上跑通。D1_30 这一行直接给出 R² 负值,说明该模型在此回归框架下无效——它本是为前一篇分类任务开发的,并非回归模型,其 MAE 显示 1809 个价位误差,预测图也明显脱离实际收盘价与其它模型轨迹。 D1_10 的 RMSLE 几乎无意义,因为预测值散布在 1.05–1.09,远不足一个数量级;EURUSD 汇率彼此太近,MAPE、MSPE 数值贴近 MAE、MSE,但百分比偏差计算里没有取绝对偏差,导致 MSPE 非对称——预测高于实际时会放大误差。D1_63 的衡量度数值与 D1_10 高度相似,直觉上也像同一类输出。D1_52 的 R² 虽大于 0.5,但几乎所有预测价都低于收盘,相比前两者也没给出更乐观的信号。外汇与贵金属杠杆高、滑点突发的特性,会让这类离线衡量度与实盘表现出现偏离,验证时务必用 MT5 历史数据复跑。 下面这段脚本就是合并评估的骨架,改 InpStartDate / InpStopDate 就能换样本区间,接自己的模型头文件即可。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  ONNX.eurusd.D1.4M.Metrics.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">#define MODELS class="num">4
class="macro">#include "ModelEurusdD1_10.mqh"
class="macro">#include "ModelEurusdD1_30.mqh"
class="macro">#include "ModelEurusdD1_52.mqh"
class="macro">#include "ModelEurusdD1_63.mqh"
class="macro">#class="kw">property script_show_inputs
class="kw">input class="type">class="kw">datetime InpStartDate = D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;;
class="kw">input class="type">class="kw">datetime InpStopDate  = D&class="macro">#x27;class="num">2023.01.class="num">31&class="macro">#x27;;
CModelSymbolPeriod *ExtModels[MODELS];
class="kw">struct PredictedPrices
  {
   class="type">class="kw">string            model;
   class="type">class="kw">double            pred[];
  };
PredictedPrices ExtPredicted[MODELS];
class="type">class="kw">double ExtClose[];
class="kw">struct Metrics
  {
   class="type">class="kw">string            model;
   class="type">class="kw">double            mae;
   class="type">class="kw">double            mse;
   class="type">class="kw">double            rmse;
   class="type">class="kw">double            r2;
   class="type">class="kw">double            mape;
   class="type">class="kw">double            mspe;
   class="type">class="kw">double            rmsle;
  };
Metrics ExtMetrics[MODELS];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- init section
   if(!Init())
      class="kw">return;
class=class="str">"cmt">//--- predictions test loop
   class="type">class="kw">datetime dates[];
   if(CopyTime(_Symbol,_Period,InpStartDate,InpStopDate,dates)<=class="num">0)
     {
      Print("Cannot get data from ",InpStartDate," to ",InpStopDate);

◍ 多模型初始化与预测回收的实现骨架

这段逻辑把 EURUSD 日线下的四个模型实例统一挂到 ExtModels 数组里,分别是 D1_10、D1_30、D1_52、D1_63,对应不同回溯窗口。Init() 里逐个调用 Init(_Symbol,_Period),只要有一个返回失败就立刻 Deinit() 并退出,避免半初始化状态污染后续计算。 GetPredictions() 按日期循环,把每个模型在指定 date 的预测价 Push 进 ExtPredicted[i].pred。注意这里预测价是逐模型、逐日期收集的,最终形成 4×N 的预测矩阵,N 等于 dates.Size()。 CalculateMetrics() 用 vector_true.Assign(ExtClose) 把实际收盘价向量化,再对每个模型的预测向量算 REGRESSION_MAE。你在 MT5 里改 ExtModels 的窗口参数(比如把 63 换成 100),MAE 数值会直接反映过拟合或滞后程度,外汇与贵金属杠杆高,模型误差放大后实盘风险显著。 Deinit() 只做一件事:遍历 ExtModels 把 new 出来的对象 delete 掉。漏掉这步在 EA 反复加载时会扯出内存泄漏,MT5 策略测试器里跑多轮回测时尤其明显。

MQL5 / C++
  class="kw">return;
   }
  for(class="type">uint n=class="num">0; n<dates.Size(); n++)
     GetPredictions(dates[n]);
     
  CopyClose(_Symbol,_Period,InpStartDate,InpStopDate,ExtClose);
  CalculateMetrics();
class=class="str">"cmt">//--- deinit section
  Deinit();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Init()
  {
  ExtModels[class="num">0]=new CModelEurusdD1_10;
  ExtModels[class="num">1]=new CModelEurusdD1_30;
  ExtModels[class="num">2]=new CModelEurusdD1_52;
  ExtModels[class="num">3]=new CModelEurusdD1_63;
  for(class="type">long i=class="num">0; i<ExtModels.Size(); i++)
    {
      if(!ExtModels[i].Init(_Symbol,_Period))
        {
         Deinit();
         class="kw">return(false);
        }
    }
  for(class="type">long i=class="num">0; i<ExtModels.Size(); i++)
     ExtPredicted[i].model=ExtModels[i].GetModelName();
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Deinit()
  {
  for(class="type">uint i=class="num">0; i<ExtModels.Size(); i++)
     class="kw">delete ExtModels[i];
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void GetPredictions(class="type">class="kw">datetime date)
  {
class=class="str">"cmt">//--- collect predicted prices
  for(class="type">uint i=class="num">0; i<ExtModels.Size(); i++)
     ExtPredicted[i].pred.Push(ExtModels[i].PredictPrice(date));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CalculateMetrics()
  {
  vector vector_pred,vector_true;
  vector_true.Assign(ExtClose);
  for(class="type">uint i=class="num">0; i<ExtModels.Size(); i++)
    {
     ExtMetrics[i].model=ExtPredicted[i].model;
     vector_pred.Assign(ExtPredicted[i].pred);
     ExtMetrics[i].mae  =vector_pred.RegressionMetric(vector_true,REGRESSION_MAE);

「回归指标怎么读才不踩坑」

在 MT5 里用 vector 的 RegressionMetric 一口气把六个回归指标算出来,是横向比模型的最快路径。下面这段就是循环里给结构体填值的写法,六个宏分别对应不同误差口径。 ExtMetrics[i].mse =vector_pred.RegressionMetric(vector_true,REGRESSION_MSE); ExtMetrics[i].rmse =vector_pred.RegressionMetric(vector_true,REGRESSION_RMSE); ExtMetrics[i].r2 =vector_pred.RegressionMetric(vector_true,REGRESSION_R2); ExtMetrics[i].mape =vector_pred.RegressionMetric(vector_true,REGRESSION_MAPE); ExtMetrics[i].mspe =vector_pred.RegressionMetric(vector_true,REGRESSION_MSPE); ExtMetrics[i].rmsle=vector_pred.RegressionMetric(vector_true,REGRESSION_RMSLE); 跑完直接 ArrayPrint(ExtMetrics),终端会吐出一张表。D1_10 的 r2 是 0.77720,而 D1_30 的 r2 是 -2.05545——负 r2 说明该模型拟合能力比直接猜均值还差,外汇与贵金属行情里这种过拟合窗口很常见,高杠杆下误用可能放大回撤风险。 MAPE 对方向不对称:真值 10 预测 5 时误差 0.5,真值 5 预测 10 时误差直接 1.0。做贵金属拐点预测时,低价区低估比高价区高估更‘好看’,但实盘里两边踩空都疼,别被绝对值蒙了。 排序也有讲究,按 mae 升序后 D1_10(0.00381) 与 D1_63(0.00413) 稳居前二,D1_52(0.00472) 垫底;但结合 r2 看,D1_10 仍最稳,D1_63 次之。开 MT5 把这段粘进脚本,换你自己的 vector_true/pred 就能复算一遍。

MQL5 / C++
ExtMetrics[i].mse   =vector_pred.RegressionMetric(vector_true,REGRESSION_MSE);
ExtMetrics[i].rmse =vector_pred.RegressionMetric(vector_true,REGRESSION_RMSE);
ExtMetrics[i].r2   =vector_pred.RegressionMetric(vector_true,REGRESSION_R2);
ExtMetrics[i].mape =vector_pred.RegressionMetric(vector_true,REGRESSION_MAPE);
ExtMetrics[i].mspe =vector_pred.RegressionMetric(vector_true,REGRESSION_MSPE);
ExtMetrics[i].rmsle=vector_pred.RegressionMetric(vector_true,REGRESSION_RMSLE);
 }
 ArrayPrint(ExtMetrics);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
   [model]   [mae]   [mse]   [rmse]       [r2]   [mape]   [mspe] [rmsle]
[class="num">0] "D1_10" class="num">0.00381 class="num">0.00003 class="num">0.00530   class="num">0.77720 class="num">0.00356 class="num">0.00002 class="num">0.00257
[class="num">1] "D1_30" class="num">0.01809 class="num">0.00039 class="num">0.01963 -class="num">2.05545 class="num">0.01680 class="num">0.00033 class="num">0.00952
[class="num">2] "D1_52" class="num">0.00472 class="num">0.00004 class="num">0.00642   class="num">0.67327 class="num">0.00440 class="num">0.00004 class="num">0.00311
[class="num">3] "D1_63" class="num">0.00413 class="num">0.00003 class="num">0.00559   class="num">0.75230 class="num">0.00385 class="num">0.00003 class="num">0.00270
   [model]   [mae]   [mse]   [rmse]       [r2]   [mape]   [mspe] [rmsle]
[class="num">0] "D1_10" class="num">0.00381 class="num">0.00003 class="num">0.00530   class="num">0.77720 class="num">0.00356 class="num">0.00002 class="num">0.00257
MAPE = |(y_true-y_pred)/y_true|
如果 y_true = class="num">10 且 y_pred = class="num">5
MAPE = class="num">0.5
如果 y_true = class="num">5 且 y_pred = class="num">10
MAPE = class="num">1.0
   [model]   [mae]   [mse]   [rmse]       [r2]   [mape]   [mspe] [rmsle]
[class="num">3] "D1_63" class="num">0.00413 class="num">0.00003 class="num">0.00559   class="num">0.75230 class="num">0.00385 class="num">0.00003 class="num">0.00270
[class="num">0] "D1_10" class="num">0.00381 class="num">0.00003 class="num">0.00530   class="num">0.77720 class="num">0.00356 class="num">0.00002 class="num">0.00257
[class="num">3] "D1_63" class="num">0.00413 class="num">0.00003 class="num">0.00559   class="num">0.75230 class="num">0.00385 class="num">0.00003 class="num">0.00270
   [model]   [mae]   [mse]   [rmse]       [r2]   [mape]   [mspe] [rmsle]
[class="num">2] "D1_52" class="num">0.00472 class="num">0.00004 class="num">0.00642   class="num">0.67327 class="num">0.00440 class="num">0.00004 class="num">0.00311

测试器里轮换不同 ONNX 模型看信号

在 MT5 策略测试器里跑 ONNX 模型,最直观的办法是写个极简 EA,把不同周期的模型头文件依次挂上。原文用的 EURUSD D1 模型里,D1_52 在整个测试期只开出一笔卖出,且趋势判断始终没翻转,回归表现偏弱,测试器输出和上一节定性结论一致。 把 EA 里两行模型声明换掉,切到 D1_10,测试图形明显更有连续性,信号频率和可交易段落都更合理。再改两行切到 D1_63,图形质量比 D1_10 差一截——两个模型横向比,D1_10 的回归度量更优,测试器数据也指向同一结论。 下面这段 EA 核心就是 include 四个模型头文件、用一行 CModelEurusdD1_xx ExtModel; 决定加载哪个,OnTick 里调 PredictClass 拿预测类别。换模型只需注释/取消注释对应行,不必动逻辑。 这些模型仅用于演示 MQL5 怎么接 ONNX,EA 本身不适合直接上实盘。外汇和贵金属杠杆高、滑点跳空频繁,回测顺滑不等于实盘能复现,验证前先开模拟盘跑一轮。

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_10.mqh"
class="macro">#include "ModelEurusdD1_30.mqh"
class="macro">#include "ModelEurusdD1_52.mqh"
class="macro">#include "ModelEurusdD1_63.mqh"
class="macro">#include <Trade\Trade.mqh>
class="kw">input class="type">class="kw">double InpLots = class="num">1.0;     class=class="str">"cmt">// Lots amount to open position
class=class="str">"cmt">//CModelEurusdD1_10 ExtModel;
class=class="str">"cmt">//CModelEurusdD1_30 ExtModel;
CModelEurusdD1_52 ExtModel;
class=class="str">"cmt">//CModelEurusdD1_63 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);
   Print("model ",ExtModel.GetModelName());
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)
  {
   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
   vector prob(class="num">3);
   class="type">int    predicted_class=ExtModel.PredictClass(TimeCurrent(),prob);
   Print("predicted class ",predicted_class);
class=class="str">"cmt">//--- check trading according to prediction
   if(predicted_class>=class="num">0)
      if(PositionSelect(_Symbol))

◍ 开平仓信号怎么落进下单函数

模型给出 predicted_class 后,EA 不在主循环里直接交易,而是分流到 CheckForOpen 与 CheckForClose 两个函数,逻辑边界清晰,方便你单独改入场或出场规则。 CheckForOpen 先把预测类别映射成订单方向:PRICE_DOWN 对应 ORDER_TYPE_SELL,PRICE_UP 对应 ORDER_TYPE_BUY,其余情况 signal 保持 WRONG_VALUE 不动作。映射完才检查终端是否允许交易,再用 SymbolInfoDouble 取对应 BID 或 ASK 价格,调 ExtTrade.PositionOpen 以 InpLots 手数开仓,滑点和止损止盈传 0。 CheckForClose 则先读当前持仓类型,多单遇到 PRICE_DOWN、空单遇到 PRICE_UP 才置 bsignal=true。触发后先 PositionClose(_Symbol,3) 平掉原仓,紧接调用 CheckForOpen 反手开仓——这种「平仓即反手」结构在震荡行情可能频繁刷单,外汇与贵金属杠杆高,回撤风险需自测。 日志里能看到 EUUSD 在 2023.06.09 16:18:31 完成品种同步,收到 3720 字节符号信息;历史从 2011.01.03 一直补齐到 2023.04.07,仅加载 27 字节,说明本地已缓存大部分数据。你换品种时重点看这两行是否出现,没同步完就跑策略会读不到报价。

MQL5 / C++
   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(class="kw">const class="type">int predicted_class)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
class=class="str">"cmt">//--- check signals
   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(class="kw">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);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「回测日志里藏着的开仓触发链」

在 MT5 策略测试器中跑 ONNX 模型驱动的 EURUSD 日线专家顾问,日志会暴露完整决策路径。上面这段实测记录显示:2023.06.09 16:18:32 启动测试,历史缓存分配了 283 根日线柱,实际装载 260 根(2022.01.03 至 2022.12.30),而测试窗口只取 2023.01.01 到 2023.02.01。 模型 D1_52 在 2023.01.02 07:02:00 输出预测类别 2,EA 随即以 1.07016 即时卖出 1 手 EURUSD,买价卖价同价 1.07016,点差显示为 0.7 点(1.07023-1.07016)。这一笔成交回报 deal #2,说明预测类别到下单是同步执行的,没有挂单延迟。 留意 ONNX 运行环境:api version 1.16.0 在 32.161 毫秒完成初始化,从加载到首笔预测仅用约 30 毫秒。外汇与贵金属自带高杠杆风险,这类模型信号在历史样本外可能失效,实盘前务必用 MT5 自测不同品种的点差与滑点。

欧元日线模型连发同类信号

在 2023.06.09 16:18:32 这一秒内,EURUSD D1 图表上的 ONNX 推理脚本批量吐出了 13 条预测日志,时间戳从 .196 跨到 .224,单条间隔约 2~3 毫秒。 这些日志对应的行情起点是 2023.01.03 至 2023.01.19 的日线 bar,其中 01.05 与 01.18 的零号时间带了 30 秒偏移,其余多为 00:00:00 开盘时刻。 全部 13 条记录的预测类别都是 class 2,说明该 ONNX 模型在回看 1 月前三周欧元日线时,倾向于给出同一类状态判定(具体是多头、震荡还是反转标签,取决于你训练时定的映射)。 这种连续同类的输出在 MT5 专家日志里很容易刷屏,建议直接把日志导到 csv 用 pandas 数 class 分布,而不是肉眼翻终端——外汇和贵金属杠杆高,模型信号仅供概率参考,别当成下单指令。

◍ 回测日志里连续 class 2 与反手信号

上面这段 MT5 策略测试器日志,记录了 ONNX 模型在 EURUSD D1 上的批量预测与一笔仓位切换。从 2023.01.20 到 2023.01.31,连续 9 个交易日的预测类别都是 class 2,说明模型在该窗口内对日线方向给出了高度一致的判读。 日志末尾出现了仓位动作:测试结束前平仓价为 1.08621,原持仓是 #2 卖单(开仓价 1.07016);几乎同一时刻 #3 买单以 1.08621 成交,等于在测试截断点反手做多。 这类连续同类预测叠加反手,值得在 MT5 里把日志级别开到详细,核对 ONNX 推理间隔与订单触发条件。外汇与贵金属自带高杠杆风险,class 2 只是模型输出标签,不预示后续必涨,实盘前请用历史数据复验。

「回测日志里藏着的切换开关」

一段 EURUSD 日线回测跑完,终端打出行:final balance 8366.00 USD,123499 ticks、22 bars,整个测试 0:00:00.294 就过,预处理仅占 0.016 秒。这种日志不是装饰,它告诉你模型文件加载和品种周期对齐都没报错,才敢往实盘推。 代码里真正决定用哪个模型的是这几行:CModelEurusdD1_10 ExtModel; 是激活态,其余三个 D1_30 / D1_52 / D1_63 都被注释掉了。也就是说同一套框架,靠换头文件引用和取消注释来切模型,而不是改策略逻辑。 外汇与贵金属高杠杆品种,回测通过不等于实盘能复现,8366 这个终点只是该次样本下的结果,换一段行情可能明显偏离。开 MT5 把 ModelEurusdD1_10 注释掉、放开 D1_52,重跑同一段 2023.06 日志对应的样本,对比 final balance 差值,比看任何说明都直观。

MQL5 / C++
class="macro">#include "ModelEurusdD1_10.mqh"
class="macro">#include "ModelEurusdD1_30.mqh"
class="macro">#include "ModelEurusdD1_52.mqh"
class="macro">#include "ModelEurusdD1_63.mqh"
class="macro">#include <Trade\Trade.mqh>
class="kw">input class="type">class="kw">double InpLots = class="num">1.0;    class=class="str">"cmt">// Lots amount to open position
CModelEurusdD1_10 ExtModel;
class=class="str">"cmt">//CModelEurusdD1_30 ExtModel;
class=class="str">"cmt">//CModelEurusdD1_52 ExtModel;
class=class="str">"cmt">//CModelEurusdD1_63 ExtModel;
CTrade          ExtTrade;

衡量模型优劣的几个尺度

在价格预测模型的评估里,R-平方是最直白的拟合优度参考,数值越接近 1 代表解释力越强。把 MAE、RMSE、MAPE 三项放到一起对照,往往比单看一个误差指标更能暴露模型短板。 RMSLE 这种对数均方根误差,在价格预测里通常没必要引入,它更适用于多模型横向比较,哪怕只是同一模型改了参数后的几个变体。 文中那 22 个样本毕竟太少,谈不上统计显著,我们只是给出一种可复现的替代思路,你拿 MT5 接自己的行情跑一遍就能验证适配性。外汇与贵金属波动剧烈,任何模型输出都只是概率倾向,实盘前务必小资金压力测试。

把模型体检交给小布盯盘
这些回归衡量度的批量计算小布盯盘的 AIGC 已内置,打开对应品种页即可看到预测偏差分布,你只需判断要不要换特征。

常见问题

R2 为负只表示模型比直接取均值还差,倾向说明特征与目标关系没学到,可能需重选输入或调参,不绝对代表报废。
MAPE 对近零真实值敏感且无量纲,若金价触及低位附近真实值偏小,百分比误差会爆表,这类任务更宜用 MAE 或 RMSE。
可以,小布盯盘内置了常见回归衡量度计算,接入模型输出后自动给出 MAE、RMSE、R2 等,省去手写脚本。
由平方均值不小于均值平方的凸性可得,二者相等仅当所有误差幅度一致,差值越大代表独立误差扩散越广。
定义含对数运算,负值无定义,常用于跨数量级正数任务如成交额预测,外汇报价为正也适用但需防零值。