如何在 MQL5 中集成 ONNX 模型的示例·进阶篇
「把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 区间,说明纯价量序列做三分类在贵金属上概率优势有限,高风险品种别直接当信号源。
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 不可用于实盘账户。 别把正态当圣经 准备输入数据必须复用训练时的归一化规则,推理完再把输出值反变换回价格;分类靠序列末根收盘与预测价的差值判定,规则写错一步,回测曲线就会假漂亮。
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 判断平仓或开仓,分歧时不出手,这能过滤掉一部分单模型误判,但外汇与贵金属杠杆高,共识信号也只代表概率倾向,不代表方向必现。
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。
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); }