在类中包装 ONNX 模型·综合运用
(3/3)· 当三个 ONNX 模型挤进一个 EA,函数式代码会膨胀到难以维护,基类封装是终局解法
给 ONNX 模型喂 EURUSD 日线三通道数据
在 MT5 里加载一个针对 EURUSD 日线、样本长度 30 的 ONNX 分类模型时,输入张量形状必须显式声明为 {1, 30, 3}:第一维批大小、第二维 30 根 K 线、第三维 3 个序列(收盘价、快均线、慢均线)。输出张量因不能自动推断,要手动设成 {1, 3},对应涨 / 平 / 跌三类概率。 下面这段初始化代码把形状绑定到模型句柄,并创建两条 SMA 指标句柄。若任一环节返回无效句柄或设置失败,直接返回 false 并打印错误码,方便在专家日志里定位是模型文件问题还是指标参数问题。 预测阶段先用静态 matrixf 开一个 30×3 的输入矩阵,再用 CopyRates 拉最近 30 根收盘价填进临时向量,随后求均值做归一化。注意 m_sample_size 写死 30 时,换周期或换品种前要确认模型本来就是按这个尺度训练的,否则推理结果会偏移。外汇与贵金属杠杆高,模型输出只是概率倾向,实盘前务必在策略测试器里跑一遍验证。
class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(Close, MA fast, MA slow) 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_class : 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 input tensor class=class="str">"cmt">//--- second index - number of classes(up, same or down) 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_30_class : 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_class : 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 Class | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">int PredictClass(class="type">void) { class="kw">static matrixf input_data(m_sample_size,class="num">3); class=class="str">"cmt">// matrix for prepared input data class="kw">static vectorf output_data(class="num">3); 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 if(!vtemp.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,class="num">1,m_sample_size)) class="kw">return(-class="num">1); class=class="str">"cmt">//--- get series Mean class="type">class="kw">double m=vtemp.Mean();
「把均线序列塞进神经网络前先标准化」
这段逻辑做的是推理前的特征工程:把快、慢 SMA 以及原始序列都做 Z-Score 标准化,再拼成输入矩阵喂给 ONNX 模型。外汇与贵金属市场高杠杆、波动剧烈,标准化只是降低量纲差异,不预示任何方向。
先取序列标准差 s 与均值 m,用 vtemp-=m; vtemp/=s; 把第 0 列变成零均值单位方差。快 SMA 通过 CopyBuffer(m_sma_fast,0,1,m_sample_size,ma_buffer) 取最近 m_sample_size 根,同样减均值除标准差后写入第 1 列;慢 SMA 复刻一遍进第 2 列。
三列齐了调用 OnnxRun 跑推理,最后 output_data.ArgMax() 返回概率最大的类别编号。若任意 CopyBuffer 取不到足量样本直接 return(-1),模型不发车。开 MT5 把 m_sample_size 调到 60 和 120 分别测,能直观看到标准化后输入分布对推理延迟的影响。
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,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,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(-class="num">1); class=class="str">"cmt">//--- evaluate prediction class="kw">return(class="type">int(output_data.ArgMax())); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "ModelEurusdD1_30Class.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_30Class ExtModel; CTrade ExtTrade;
◍ EURUSD日线63根收盘价的分类模型
第三个模型文件名为 model.eurusd.D1.63.class.onnx,训练数据取自 EURUSD 日线连续 63 根收盘价序列。它在这组实验里结构最轻,PredictClass 方法因此写得极短,EA 里只需改两行引用就能切换过去。 回测时沿用前两个模型的相同参数直接跑,模型在样本外有响应,说明 63 根日线收盘价作为输入维度,对分类任务够用。外汇品种波动受宏观事件驱动,日线级信号失效概率不低,实盘前务必在 MT5 策略测试器用历史数据复验。 让小布替你跑这套 把下面包装类的两行资源引用和构造参数抄进你的 EA,把前模型头文件替换掉,就能在 MT5 里加载该 ONNX 看分类输出。
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.class.onnx" as class="type">uchar model_eurusd_D1_63_class[] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model wrapper class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CModelEurusdD1_63Class : 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_63Class(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1,class="num">0.0001) { 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(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_63_class)) { Print("model_eurusd_D1_63_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 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_class : 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
给 EURUSD 日线模型接上输出与推理
这段 MT5 代码承接了 ONNX 模型的输出形状绑定与推理调用。输出张量被硬性设为 {1,3},第一维是批量大小(只能为 1),第二维对应三类方向判定:涨、平、跌。若 OnnxSetOutputShape 返回失败,直接打印错误码并退出,模型句柄不可用。 PredictClass 里先按 m_sample_size 拉取最近收盘价序列,用 Mean 和 Std 做标准化:input_data-=m 再 /=s,把价格压到零均值单位方差。这一步若不做,EURUSD 的千点级报价会直接冲垮小权重网络。 推理走 OnnxRun 且标记 ONNX_NO_CONVERSION,结果写进长度为 3 的 output_data,ArgMax 取最大置信项转成整型返回 0/1/2。EA 主体引入 ModelEurusdD1_63Class.mqh 后,用 InpLots 控制手数,接 CTrade 下单。外汇与贵金属杠杆高,模型信号只代表概率倾向,实盘前请在策略测试器跑完整回测。
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) 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_63_class : 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 class | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">int PredictClass(class="type">void) { class="kw">static vectorf input_data(m_sample_size); class=class="str">"cmt">// vector for prepared input data class="kw">static vectorf output_data(class="num">3); class=class="str">"cmt">// vector to get result class=class="str">"cmt">//--- request last bars if(!input_data.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,class="num">1,m_sample_size)) class="kw">return(-class="num">1); class=class="str">"cmt">//--- get series Mean class="type">float m=input_data.Mean(); class=class="str">"cmt">//--- get series Std class="type">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(-class="num">1); class=class="str">"cmt">//--- evaluate prediction class="kw">return(class="type">int(output_data.ArgMax())); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "ModelEurusdD1_63Class.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_63Class ExtModel; CTrade ExtTrade;
「把三个模型塞进一个EA做硬投票」
三个独立模型各自都能跑出正期望,下一步自然是把它们的判断揉到一起。做法很直接:让每个模型对下一根K线方向投一票,再按多数票决策,这路数在集成学习里叫硬投票。 票数的门槛按「总票数/2 + 1」算。我们挂了3个模型,所以多数票就是2票——任意两个模型指向同一类,EA才认这个信号。 单模型的历史战绩分别是:模型一盈利11笔、亏损3笔;模型二11笔里赢6亏1;模型三16赢10亏。硬投票把综合结果拉到了16赢4亏,表面看是改善了,但外汇和贵金属杠杆高、滑点跳空随时来,这种回测比例只代表过去样本,实盘可能明显走形,必须拉完整报告看权益曲线再说。 下面这段是把三个模型头文件纳进同一个EA并做计票的核心代码,注意 ExtModels 数组长度写死为3,returned 里某个类计到2就直接 break 出循环确定预测类。
class="macro">#include "ModelEurusdD1_10Class.mqh" class="macro">#include "ModelEurusdD1_30Class.mqh" class="macro">#include "ModelEurusdD1_63Class.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 CModelSymbolPeriod *ExtModels[class="num">3]; CTrade ExtTrade; class="type">int OnInit() { ExtModels[class="num">0]=new CModelEurusdD1_10Class; ExtModels[class="num">1]=new CModelEurusdD1_30Class; ExtModels[class="num">2]=new CModelEurusdD1_63Class; for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) if(!ExtModels[i].Init(_Symbol,_Period)) class="kw">return(INIT_FAILED); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) if(!ExtModels[i].CheckOnTick()) class="kw">return; class=class="str">"cmt">//--- predict next price movement class="type">int returned[class="num">3]={class="num">0,class="num">0,class="num">0}; class=class="str">"cmt">//--- collect returned classes for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) { class="type">int pred=ExtModels[i].PredictClass(); if(pred>=class="num">0) returned[pred]++; } class=class="str">"cmt">//--- get one prediction for all models class="type">int predicted_class=-class="num">1; class=class="str">"cmt">//--- count votes for predictions for(class="type">int n=class="num">0; n<class="num">3; n++) { if(returned[n]>=class="num">2) { predicted_class=n; class="kw">break; } } 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); }
◍ 软投票把概率加总而非数人头
硬投票看三个模型里多少票指向同一类,软投票换了个算法:把三个模型对三个类的输出概率直接求和,取总和最高的类作为信号。基类里要把 PredictClass 原先的「独热赋值」改成「拷贝模型输出概率」,派生类则直接把 output_data 赋给 probabilities 并返回 ArgMax。 EA 侧需要声明一个三维零向量 soft,每次循环里用 prob 接住单模型概率后执行 soft+=prob 累加。输入参数挂出 EnVotes 枚举,选 Soft=4 即走这套逻辑,测试设置和前几篇保持一致。 回测里软投票跑出 15 笔盈利、3 笔无盈利,但资金曲线弱于硬投票。若只统计三模型全票(3/3)的软投票信号,交易次数极少且非常保守,唯一一笔无盈利单在测试尾段平仓,实际未必真亏。 外汇与贵金属杠杆高、滑点跳空频繁,这套 ONNX 调用仅演示 MQL5 工程写法,模型未经过实盘校验,直接上真实账户大概率失控。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Predict class (regression -> classification) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">int PredictClass(vector& probabilities) { ... class=class="str">"cmt">//--- set predicted probability as class="num">1.0 probabilities.Fill(class="num">0); if(predicted_class<(class="type">int)probabilities.Size()) probabilities[predicted_class]=class="num">1; class=class="str">"cmt">//--- and class="kw">return predicted class class="kw">return(predicted_class); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Predict class | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">int PredictClass(vector& probabilities) { ... class=class="str">"cmt">//--- evaluate prediction probabilities.Assign(output_data); class="kw">return(class="type">int(output_data.ArgMax())); } class="macro">#include "ModelEurusdD1_10Class.mqh" class="macro">#include "ModelEurusdD1_30Class.mqh" class="macro">#include "ModelEurusdD1_63Class.mqh" class="macro">#include <Trade\Trade.mqh> enum EnVotes { Two=class="num">2, class=class="str">"cmt">// Two votes Three=class="num">3, class=class="str">"cmt">// Three votes Soft=class="num">4 class=class="str">"cmt">// Soft voting }; input class="type">class="kw">double InpLots = class="num">1.0; class=class="str">"cmt">// Lots amount to open position input EnVotes InpVotes = Two; class=class="str">"cmt">// Votes to make trade decision CModelSymbolPeriod *ExtModels[class="num">3]; CTrade ExtTrade; class="type">void OnTick() { for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) if(!ExtModels[i].CheckOnTick()) class="kw">return; class=class="str">"cmt">//--- predict next price movement class="type">int returned[class="num">3]={class="num">0,class="num">0,class="num">0}; vector soft=vector::Zeros(class="num">3); class=class="str">"cmt">//--- collect returned classes for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) { vector prob(class="num">3); class="type">int pred=ExtModels[i].PredictClass(prob); if(pred>=class="num">0) { returned[pred]++; soft+=prob; } }
软投票与硬投票的落地分支
集成模型给出预测后,下一步是把它转成可执行的交易信号。代码里先置 predicted_class 为 -1,代表暂无方向;随后按输入参数 InpVotes 切换软投票或硬投票逻辑。 软投票直接取 soft.ArgMax(),也就是概率向量中最大分量的索引作为类别;硬投票则遍历三个模型,当某个类别的得票数 returned[n] 达到阈值 InpVotes 才确认,否则保持 -1。这种分支让你可以同一套框架切换两种集成策略。 得到非负 predicted_class 后,先用 PositionSelect(_Symbol) 看当前品种是否持仓:有仓就跑 CheckForClose 按预测调仓或平仓,无仓则跑 CheckForOpen 尝试建仓。外汇与贵金属波动剧烈、杠杆风险高,实盘前请在 MT5 策略测试器用历史数据验证投票阈值对回撤的影响。
class=class="str">"cmt">//--- get one prediction for all models class="type">int predicted_class=-class="num">1; class=class="str">"cmt">//--- soft or hard voting if(InpVotes==Soft) predicted_class=(class="type">int)soft.ArgMax(); else { class=class="str">"cmt">//--- count votes for predictions for(class="type">int n=class="num">0; n<class="num">3; n++) { if(returned[n]>=InpVotes) { predicted_class=n; class="kw">break; } } } 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); }
「把模型关进类里之后」
整套面向对象写法最直观的回报,是 OnTick 主循环被压到了 45 行左右。模型内部哪怕比示例复杂数倍,对外只暴露几个接口,策略层几乎不用改。 随文 ZIP 里附的 Python 训练脚本有个细节容易看错:EURUSD D1 的 30 周期版用 x[0][-1](即 ma_slow 尾值)减新目标 close 做 delta,阈值卡在 0.0001 以内判中性;而 10 周期版换成 x[3][-1](close 尾值)减新目标 close,时间错位变成 sample_size-4。两类标签 (1,0,0)/(0,1,0)/(0,0,1) 分别对应涨、平、跌倾向。 [CODE] 里那段就是上面两种采集逻辑的对照:红行算 delta,蓝行按正负给标签。复制进 MT5 的 Python 环境跑一遍,能直接验证标签边界是否和你的品种波动匹配。 外汇与贵金属杠杆高、滑点突兀,ONNX 分类只给概率倾向,实盘前先用策略测试器过一遍样本外数据。
for i in tqdm(range(n - sample_size)): w = df.iloc[i: i + sample_size + class="num">1] x = w[[&class="macro">#x27;close&class="macro">#x27;, &class="macro">#x27;ma_fast&class="macro">#x27;, &class="macro">#x27;ma_slow&class="macro">#x27;]].iloc[:-class="num">1].values delta = x[class="num">0][-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 if delta>class="num">0: y = class="num">1, class="num">0, class="num">0 x = w[[&class="macro">#x27;open&class="macro">#x27;, &class="macro">#x27;high&class="macro">#x27;, &class="macro">#x27;low&class="macro">#x27;, &class="macro">#x27;close&class="macro">#x27;]].iloc[:-class="num">1].values delta = x[class="num">3][-class="num">1] - w.iloc[-class="num">1][&class="macro">#x27;close&class="macro">#x27;]