如何在 MQL5 中集成 ONNX 模型的示例·进阶篇
📘

如何在 MQL5 中集成 ONNX 模型的示例·进阶篇

第 2/3 篇

「把K线窗口压成三类动作标签」

这段 Python 片段干的事很直接:用滑动窗口把历史收盘价序列切成样本,再按窗口末尾的涨跌幅度打上三类标签。阈值卡在 0.0001,小于它算横盘(0,1,0),跌了是(1,0,0),涨了是(0,0,1)——外汇和贵金属点差环境下,这个 0.0001 的死区能滤掉大量噪声假信号,但也可能漏掉微动量突破,实际用 EURUSD 的 M5 数据跑,history_size 取 30 时样本数约为 n-30。 归一化按每个样本自身均值和标准差做,axis=1 保持时间维度不动,这样不同价格区间的窗口被拉到同一量纲。训练集测试集按 9:1 切,random_state=0 保证可复现;模型是三层全连接,输入维等于 history_size,L2 正则 0.01 配 0.3 dropout,倾向压制过拟合。 训练跑 300 个 epoch,batch_size 128,学习率用 SGD 0.01 加 0.9 动量,还挂了 ReduceLROnPlateau:val_loss 连 5 轮不降就乘 0.9 缩率,最低到 1e-5。最终把 Keras 模型转 ONNX 丢给 MT5 端调用,测试集 accuracy 打印出来一般落在 0.4~0.6 区间,说明纯价量序列做三分类在贵金属上概率优势有限,高风险品种别直接当信号源。

MQL5 / C++
n = len(df)
xs = []
ys = []
for i in tqdm(range(n - history_size)):
    w = df.iloc[i: i + history_size + class="num">1]
    x = w[[&class="macro">#x27;close&class="macro">#x27;]].iloc[:-class="num">1].values
    delta = x[-class="num">1] - w.iloc[-class="num">1][&class="macro">#x27;close&class="macro">#x27;]
    if np.abs(delta)<=class="num">0.0001:
        y = class="num">0, class="num">1, class="num">0
    else:
        if delta<class="num">0:
            y = class="num">1, class="num">0, class="num">0
        else:
            y = class="num">0, class="num">0, class="num">1
    xs.append(x)
    ys.append(y)
X = np.array(xs)
Y = np.array(ys)
class="kw">return X, Y
###
# get prices
X, Y = collect_dataset(df, history_size=inp_history_size)
# normalize prices
m = X.mean(axis=class="num">1, keepdims=True)
s = X.std(axis=class="num">1, keepdims=True)
X_norm = (X - m) / s
# split data to train and test sets
X_train, X_test, Y_train, Y_test = train_test_split(X_norm, Y, test_size=class="num">0.1, random_state=class="num">0)
# define model
model = Sequential()
model.add(Dense(class="num">64, input_dim=inp_history_size, activity_regularizer=regularizers.l2(class="num">0.01)))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Dropout(class="num">0.3))
model.add(Dense(class="num">16, activity_regularizer=regularizers.l2(class="num">0.01)))
model.add(BatchNormalization())
model.add(LeakyReLU())
model.add(Dense(class="num">3))
model.add(Activation(&class="macro">#x27;softmax&class="macro">#x27;))
opt = SGD(learning_rate=class="num">0.01, momentum=class="num">0.9)
model.compile(optimizer=opt, loss=&class="macro">#x27;categorical_crossentropy&class="macro">#x27;, metrics=[&class="macro">#x27;accuracy&class="macro">#x27;])
# model training for class="num">300 epochs
lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor=&class="macro">#x27;val_loss&class="macro">#x27;, factor=class="num">0.9, patience=class="num">5, min_lr=class="num">0.00001)
history = model.fit(X_train, Y_train, epochs=class="num">300, validation_data=(X_test, Y_test), shuffle = True, batch_size=class="num">128, verbose=class="num">2, callbacks=[lr_reduction])
# model evaluation
test_loss, test_accuracy = model.evaluate(X_test, Y_test)
print(f"test_loss={test_loss:.3f}")
print(f"test_accuracy={test_accuracy:.3f}")
# save model to onnx
output_path = data_path+inp_model_name
onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path)
print(f"saved model to {output_path}")
# finish
mt5.shutdown()

把两个 ONNX 模型塞进 EA 里跑 EURUSD 日线

下面这段 EA 演示了在 MQL5 里直接加载 ONNX 模型做日线预测的可行性。它只跑 EURUSD 的 D1 周期,因为训练时用的是日线 OHLC,品种和周期错位会让输入分布漂移,预测倾向失效。 两个模型作为资源编译进 exe:一个用最近 10 根日线 OHLC 预测下一日收盘,另一个用 63 根序列。显式声明输入输出张量形状是硬要求——模型里没写全的维度必须在 OnInit 里用 OnnxSetInputShape / OnnxSetOutputShape 补上,否则句柄创建虽成功,推理时会直接报错。 交易逻辑极简:每天新 K 线开盘时跑一次推理,两模型分类一致(多数投票在双模型下即“一致”)才下单,涨则买、跌则卖。模型用截至 2023 年初的数据训练,测试也从年初切分。单测第二个 63 序列模型时,其表现明显强于 10 序列的第一个,印证了弱模型靠集成提升概率的思路。 外汇与贵金属属高风险品种,此处模型仅验证 MQL5 调用 ONNX 的工程链路,EA 不可用于实盘账户。 别把正态当圣经 准备输入数据必须复用训练时的归一化规则,推理完再把输出值反变换回价格;分类靠序列末根收盘与预测价的差值判定,规则写错一步,回测曲线就会假漂亮。

MQL5 / C++
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
class="macro">#resource "Python/model.eurusd.D1.class="num">10.onnx" as class="type">uchar ExtModel1[]
class="macro">#resource "Python/model.eurusd.D1.class="num">63.onnx" as class="type">uchar ExtModel2[]
class="macro">#define SAMPLE_SIZE1 class="num">10
class="macro">#define SAMPLE_SIZE2 class="num">63
class="type">long     ExtHandle1=INVALID_HANDLE;
class="type">long     ExtHandle2=INVALID_HANDLE;
class="type">int      ExtPredictedClass1=-class="num">1;
class="type">int      ExtPredictedClass2=-class="num">1;
class="type">int      ExtPredictedClass=-class="num">1;
class="type">class="kw">datetime ExtNextBar=class="num">0;
CTrade   ExtTrade;
class=class="str">"cmt">//--- price movement prediction
class="macro">#define PRICE_UP   class="num">0
class="macro">#define PRICE_SAME class="num">1
class="macro">#define PRICE_DOWN class="num">2
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   if(_Symbol!="EURUSD" || _Period!=PERIOD_D1)
     {
      Print("model must work with EURUSD,D1");
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- create first model from class="kw">static buffer
   ExtHandle1=OnnxCreateFromBuffer(ExtModel1,ONNX_DEFAULT);
   if(ExtHandle1==INVALID_HANDLE)
     {
      Print("First model 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(OHLC)
   class="kw">const class="type">long input_shape1[] = {class="num">1,SAMPLE_SIZE1,class="num">4};
   if(!OnnxSetInputShape(ExtHandle1,class="num">0,input_shape1))
     {
      Print("First model OnnxSetInputShape error ",GetLastError());
      class="kw">return(INIT_FAILED);
     }

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 input tensor
class=class="str">"cmt">//--- second index - number of predicted prices(we only predict Close)
   class="kw">const class="type">long output_shape1[] = {class="num">1,class="num">1};
   if(!OnnxSetOutputShape(ExtHandle1,class="num">0,output_shape1))
     {

◍ 双模型投票的加载与触发细节

第二个 ONNX 模型从静态缓冲区创建后,同样要显式声明张量尺寸。输入形状设为 {1, SAMPLE_SIZE2},输出形状硬编码为 {1, 3},对应涨、平、跌三类,批大小必须和输入保持一致,否则 OnnxSetOutputShape 会返回 false 并在日志抛出错误码。 初始化函数里每段失败都直接 return(INIT_FAILED),只有两个模型都跑通输入/输出形状设定,才 return(INIT_SUCCEEDED)。在 MT5 里挂这个 EA 时,若日志出现 'Second model OnnxSetInputShape error' 加一个数字,先查 SAMPLE_SIZE2 宏定义是否和训练时序列长度一致。 OnTick 用 TimeCurrent() 对齐 K 线周期,ExtNextBar 减去取余 PeriodSeconds() 再加回,等于把触发点钉在每根新柱开盘。柱内重复 tick 直接 return,避免同根柱多次推理。 Predict() 做投票分类:两个模型同判类才把 ExtPredictedClass 设为该类,不一致则置 -1。也就是说,只有两模型共识时才允许后续 PositionSelect 判断平仓或开仓,分歧时不出手,这能过滤掉一部分单模型误判,但外汇与贵金属杠杆高,共识信号也只代表概率倾向,不代表方向必现。

MQL5 / C++
  Print("First model OnnxSetOutputShape error ",GetLastError());
  class="kw">return(INIT_FAILED);
  }
class=class="str">"cmt">//--- create second model from class="kw">static buffer
  ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT);
  if(ExtHandle2==INVALID_HANDLE)
  {
   Print("Second model 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
  class="kw">const class="type">long input_shape2[] = {class="num">1,SAMPLE_SIZE2};
  if(!OnnxSetInputShape(ExtHandle2,class="num">0,input_shape2))
  {
   Print("Second model OnnxSetInputShape error ",GetLastError());
   class="kw">return(INIT_FAILED);
  }
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 input tensor
class=class="str">"cmt">//--- second index - number of classes(up, same or down)
  class="kw">const class="type">long output_shape2[] = {class="num">1,class="num">3};
  if(!OnnxSetOutputShape(ExtHandle2,class="num">0,output_shape2))
  {
   Print("Second model OnnxSetOutputShape error ",GetLastError());
   class="kw">return(INIT_FAILED);
  }
class=class="str">"cmt">//--- ok
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- check new bar
  if(TimeCurrent()<ExtNextBar)
     class="kw">return;
class=class="str">"cmt">//--- set next bar time
  ExtNextBar=TimeCurrent();
  ExtNextBar-=ExtNextBar%PeriodSeconds();
  ExtNextBar+=PeriodSeconds();
class=class="str">"cmt">//--- predict price movement
  Predict();
class=class="str">"cmt">//--- check trading according to prediction
  if(ExtPredictedClass>=class="num">0)
    if(PositionSelect(_Symbol))
       CheckForClose();
    else
       CheckForOpen();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Voting classification                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Predict(class="type">void)
  {
class=class="str">"cmt">//--- evaluate first model
   ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1);
class=class="str">"cmt">//--- evaluate second model
   ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2);
class=class="str">"cmt">//--- vote
  if(ExtPredictedClass1==ExtPredictedClass2)
     ExtPredictedClass=ExtPredictedClass1;
  else
     ExtPredictedClass=-class="num">1;
  }

「用 ONNX 模型把 OHLC 变成涨跌预判」

这段函数把最近 sample_size 根 K 线的 OHLC 喂给训练好的 ONNX 模型,输出一个分类标签:涨、跌或持平。外汇和贵金属波动剧烈,模型输出只是概率倾向,实盘前务必在 MT5 策略测试器里跑一遍验证。 归一化步骤不能省:先用 rates.Mean(1) 和 rates.Std(1) 按列算出每列均值与标准差,再把价格矩阵转置成竖向 OHLC 向量后做 (x-mean)/std。模型推理前若不做标准化,ONNX 输出的数值会严重偏移。 反归一化时只取收盘列:predicted = output_data[0]*s[3]+m[3],再用最新收盘价减去预测值得到 delta。当 fabs(delta)<=0.0001 判为 PRICE_SAME,delta<0 倾向 PRICE_UP,否则倾向 PRICE_DOWN——阈值写死在代码里,想适配不同品种点值就改这个 0.0001。

MQL5 / C++
class="type">int PredictPrice(class="kw">const class="type">long handle,class="kw">const class="type">int sample_size)
  {
   class="kw">static matrixf input_data(sample_size,class="num">4);      class=class="str">"cmt">// matrix for prepared input data
   class="kw">static vectorf output_data(class="num">1);                 class=class="str">"cmt">// vector to get result
   class="kw">static matrix  mm(sample_size,class="num">4);              class=class="str">"cmt">// matrix of horizontal vectors Mean
   class="kw">static matrix  ms(sample_size,class="num">4);              class=class="str">"cmt">// matrix of horizontal vectors Std
   class="kw">static matrix  x_norm(sample_size,class="num">4);          class=class="str">"cmt">// matrix for prices normalize
class=class="str">"cmt">//--- prepare input data
   matrix rates;
class=class="str">"cmt">//--- request last bars
   if(!rates.CopyRates(_Symbol,_Period,COPY_RATES_OHLC,class="num">1,sample_size))
      class="kw">return(-class="num">1);
class=class="str">"cmt">//--- get series Mean
   vector m=rates.Mean(class="num">1);
class=class="str">"cmt">//--- get series Std
   vector s=rates.Std(class="num">1);
class=class="str">"cmt">//--- prepare matrices for prices normalization
   for(class="type">int i=class="num">0; i<sample_size; i++)
     {
      mm.Row(m,i);
      ms.Row(s,i);
     }
class=class="str">"cmt">//--- the input of the model must be a set of vertical OHLC vectors
   x_norm=rates.Transpose();
class=class="str">"cmt">//--- normalize prices
   x_norm-=mm;
   x_norm/=ms;
class=class="str">"cmt">//--- run the inference
   input_data.Assign(x_norm);
   if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data))
      class="kw">return(-class="num">1);
class=class="str">"cmt">//--- denormalize the price from the output value
   class="type">class="kw">double predicted=output_data[class="num">0]*s[class="num">3]+m[class="num">3];
class=class="str">"cmt">//--- classify predicted price movement
   class="type">int      predicted_class=-class="num">1;
   class="type">class="kw">double delta=rates[class="num">3][sample_size-class="num">1]-predicted;
   if(fabs(delta)<=class="num">0.0001)
      predicted_class=PRICE_SAME;
   else
     {
      if(delta<class="num">0)
         predicted_class=PRICE_UP;
      else
         predicted_class=PRICE_DOWN;
     }
   class="kw">return(predicted_class);
  }

常见问题

按实体幅度与影线比例设定阈值,实体超阈值且收高于开为高,反之为低,其余归平,输出0/1/2整数标签即可。
分别用独立会话句柄加载,每根新日线棒触发一次双模型推理,取多数票为信号,互不覆盖。
小布可接入你的ONNX推理结果,在对应品种页直接标出双模型投票方向与置信度,省去手动开EA核对。
建议缩放到[-1,1]或[0,1],与原训练保持一致,否则模型输出偏移、预判概率失真。
可设平票为观望不入场,或交第三规则过滤;外汇贵金属高风险,歧义信号默认空仓更稳妥。