用于时间序列挖掘的数据标签(第 6 部分):使用 ONNX 在 EA 中应用和测试·综合运用
◍ 跑通 ONNX 模型的第一次回测
动手前先确认一件事:你训练好的 ONNX 模型文件必须和 onnx.mq5 放在同一目录,否则加载阶段就会报错断在 OnInit。这是很多新手卡住的第一步,不是代码逻辑问题,是路径约定。 在 MT5 编辑器里点编译,生成 ex5 后按 Ctrl+F5 进调试模式回测,会弹出独立窗口跑测试过程。我本地日志最后一行是「我们成功了!」,说明从缓冲区创建模型句柄、读输入张量这一条链路是通的。 下面这段是最小可跑的初始化与释放骨架,重点看资源声明和 OnnxCreateFromBuffer 的调用方式:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| onnx.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">#resource "NBeats.onnx" as class="type">uchar ExtModel[] class="type">long handle; vectorf y=vector<class="type">float>::Zeros(class="num">20); vectorf backcast=vector<class="type">float>::Zeros(class="num">96); vectorf trend=vector<class="type">float>::Zeros(class="num">116); vectorf seasonality=vector<class="type">float>::Zeros(class="num">116); vectorf generic=vector<class="type">float>::Zeros(class="num">116); class=class="str">"cmt">//class="type">bool is_pre=class="kw">false; class="type">class="kw">string pre=NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- EventSetTimer(class="num">300); handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS); class=class="str">"cmt">//— specify the shape of the class="kw">input data class="type">long in_ct=OnnxGetInputCount(handle); OnnxTypeInfo inf; for(class="type">int i=class="num">0;i<in_ct;i++){ Print(OnnxGetInputName(handle,i)); class="type">bool re=OnnxGetInputTypeInfo(handle,i,inf); class=class="str">"cmt">//Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type); Print(re,GetLastError()); } class=class="str">"cmt">//class="type">long in_nm=OnnxGetInputName() class=class="str">"cmt">//— class="kw">return initialization result 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) { class=class="str">"cmt">//--- class=class="str">"cmt">//— destroy timer EventKillTimer(); class=class="str">"cmt">//— complete operation OnnxRelease(handle); } class=class="str">"cmt">//+------------------------------------------------------------------+
#resource "NBeats.onnx" as uchar ExtModel[] 把模型以字节数组编进 ex5,避免外部依赖;OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS) 从内存缓冲建句柄,开调试日志方便看张量名;EventSetTimer(300) 设 300 毫秒心跳,回测里用来周期触发;OnnxGetInputCount 配合循环打印输入名和类型,确认模型接口形状(如 backcast 长 96、trend 长 116)。外汇与贵金属杠杆高,回测通过仅代表链路通,实盘信号概率仍受滑点与点差干扰。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| onnx.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">#resource "NBeats.onnx" as class="type">uchar ExtModel[] class="type">long handle; vectorf y=vector<class="type">float>::Zeros(class="num">20); vectorf backcast=vector<class="type">float>::Zeros(class="num">96); vectorf trend=vector<class="type">float>::Zeros(class="num">116); vectorf seasonality=vector<class="type">float>::Zeros(class="num">116); vectorf generic=vector<class="type">float>::Zeros(class="num">116); class=class="str">"cmt">//class="type">bool is_pre=class="kw">false; class="type">class="kw">string pre=NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- EventSetTimer(class="num">300); handle=OnnxCreateFromBuffer(ExtModel,ONNX_DEBUG_LOGS); class=class="str">"cmt">//— specify the shape of the class="kw">input data class="type">long in_ct=OnnxGetInputCount(handle); OnnxTypeInfo inf; for(class="type">int i=class="num">0;i<in_ct;i++){ Print(OnnxGetInputName(handle,i)); class="type">bool re=OnnxGetInputTypeInfo(handle,i,inf); class=class="str">"cmt">//Print("map:",inf.map,"seq:",inf.sequence,"tensor:",inf.tensor,"type:",inf.type); Print(re,GetLastError()); } class=class="str">"cmt">//class="type">long in_nm=OnnxGetInputName() class=class="str">"cmt">//— class="kw">return initialization result 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) { class=class="str">"cmt">//--- class=class="str">"cmt">//— destroy timer EventKillTimer(); class=class="str">"cmt">//— complete operation OnnxRelease(handle); } class=class="str">"cmt">//+------------------------------------------------------------------+
「用 OnTick 把预测信号变成下单动作」
EA 的核心执行单元是 OnTick(),每次报价跳动都会触发。把上游模型输出的预测字符串 pre 接进这里,就能让 MT5 自动完成持仓检查与报单,不需要手动干预。 下面这段逻辑先清掉旧请求内存,再用 PositionsTotal() 抓当前持仓数。若已有仓位,取最后一单的 ticket 并读取 POSITION_TYPE:如果预测是 buy 但手里已是多单,直接 return 不重复开仓;若是空单则把 request.position 设为该 ticket,走平仓再反手。 报单参数写死 volume=0.1、deviation=5、type_filling=ORDER_FILLING_IOC,价格取 SYMBOL_ASK。外汇与贵金属杠杆高,0.1 手虽小,滑点扩大时仍可能触发非预期亏损,上线前建议在策略测试器用真实点差回测。 别把预测字符串当圣旨 pre 等于 "buy" 才动作,但模型误判时这条分支会频繁平仓反手,佣金和库存费会吃掉概率优势。建议先打印 pre 跑一周观察信号分布,再决定要不要接 OrderSend。
class="type">void OnTick() { class=class="str">"cmt">//--- class="type">MqlTradeRequest request; class="type">MqlTradeResult result; class=class="str">"cmt">//class="type">int x=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE); if (pre!=NULL) { class=class="str">"cmt">//Print("The predicted value is:",pre); class="type">class="kw">ulong numt=class="num">0; class="type">class="kw">ulong tik=class="num">0; class="type">bool sod=class="kw">false; class="type">class="kw">ulong tpt=-class="num">1; ZeroMemory(request); numt=PositionsTotal(); class=class="str">"cmt">//Print("All tickets: ",numt); if (numt>class="num">0) { tik=PositionGetTicket(numt-class="num">1); sod=PositionSelectByTicket(tik); tpt=PositionGetInteger(POSITION_TYPE);class=class="str">"cmt">//ORDER_TYPE_BUY or ORDER_TYPE_SELL if (tik==class="num">0 || sod==class="kw">false || tpt==class="num">0) class="kw">return; } if (pre=="buy") { if (tpt==POSITION_TYPE_BUY) class="kw">return; request.action=TRADE_ACTION_DEAL; request.symbol=Symbol(); request.volume=class="num">0.1; request.deviation=class="num">5; request.type_filling=ORDER_FILLING_IOC; request.type = ORDER_TYPE_BUY; request.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); if(tpt==POSITION_TYPE_SELL) { request.position=tik; Print("Close sell order."); } else{ Print("Open buy order."); } OrderSend(request, result); } else{ if (tpt==POSITION_TYPE_SELL) class="kw">return;
用均值方差把收盘价喂给模型
这段逻辑在 OnTimer 里跑,先把最近 96 根 K 线的收盘价抓进 96×1 矩阵 in0_m,每根只取 close 字段。 接着算整列均值 m 和标准差 s,再把这两个标量分别铺成 96 行相同的 mm 和 ms 矩阵,用 in0_m 减均值、除标准差完成标准化。标准化后转成 float 矩阵 in_normf,才能直接丢进 ONNX 模型做推断。 顺带一提,上面那段交易请求代码里,挂的是市价卖单:volume 写死 0.1、deviation 给 5 点、type_filling 用 IOC,价格取当前 SYMBOL_BID。若持仓是 BUY 就打印 Close buy order 并绑定 position ticket 去平,否则打印 OPen sell order 直接开空。 外汇和贵金属杠杆高,这段代码真在 MT5 实盘跑之前,务必先在策略测试器用 0.1 手回测,确认标准化和下单分支不会在极端点差下误触发。
request.action = TRADE_ACTION_DEAL; request.symbol = Symbol(); request.volume = class="num">0.1; request.type = ORDER_TYPE_SELL; request.price = SymbolInfoDouble(Symbol(), SYMBOL_BID); request.deviation = class="num">5; class=class="str">"cmt">//request.type_filling=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE); request.type_filling=ORDER_FILLING_IOC; if(tpt==POSITION_TYPE_BUY) { request.position=tik; Print("Close buy order."); } else{ Print("OPen sell order."); } OrderSend(request, result); } class=class="str">"cmt">//is_pre=class="kw">false; } pre=NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { class=class="str">"cmt">//class="type">float in0[class="num">1][class="num">96][class="num">1]; matrixf in_normf; class="type">float in1[class="num">1][class="num">2]; class=class="str">"cmt">//— get the last class="num">10 bars class="type">MqlRates rates[]; if(!CopyRates(_Symbol,_Period,class="num">0,class="num">96,rates)) class="kw">return; class=class="str">"cmt">//— class="kw">input a set of OHLC vectors class=class="str">"cmt">//class="type">class="kw">double out[class="num">1][class="num">20]; matrix in0_m(class="num">96,class="num">1); for(class="type">int i=class="num">0; i<class="num">96; i++) { in0_m[i][class="num">0]= rates[i].close; } class=class="str">"cmt">//— normalize the class="kw">input data class=class="str">"cmt">// matrix x_norm=x; vector m=in0_m.Mean(class="num">0); vector s=in0_m.Std(class="num">0); in1[class="num">0][class="num">0]=m[class="num">0]; in1[class="num">0][class="num">1]=s[class="num">0]; matrix mm(class="num">96,class="num">1); matrix ms(class="num">96,class="num">1); class=class="str">"cmt">// //— fill in the normalization matrices for(class="type">int i=class="num">0; i<class="num">96; i++) { mm.Row(m,i); ms.Row(s,i); } class=class="str">"cmt">// //— normalize the class="kw">input data in0_m-=mm; in0_m/=ms; class=class="str">"cmt">// //— convert normalized class="kw">input data to class="type">float type in_normf.Assign(in0_m); class=class="str">"cmt">//— get the output data of the model here, i.e. the price prediction
◍ 跑模型并拿均值判多空
调用 OnnxRun 时把调试日志与禁止自动转换两个标志位叠用,输入张量 in_normf、in1 进模型,输出 y、backcast、trend、seasonality、generic 一并取回。若返回失败,直接打印错误码并释放句柄退出,避免句柄泄漏拖慢 EA。 模型输出 y 是个向量,取 Mean() 后和当前 K 线的高低中点比:中点 = iHigh(_Symbol,_Period,0)/2 + iLow(_Symbol,_Period,0)/2。y 均值高于该中点就给 pre 赋 "buy",否则 "sell"。 这套判定只依赖单根 K 线中点,对外汇与贵金属这类高波动品种仅作概率倾向参考,实盘前务必在 MT5 策略测试器里用历史 tick 验证信号翻转频率。
class=class="str">"cmt">//— run the model if(!OnnxRun(handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, in_normf, in1, y, backcast, trend, seasonality, generic)) { Print("OnnxRun failed, error ",GetLastError()); OnnxRelease(handle); class="kw">return; } class=class="str">"cmt">//— print the output value of the model to the log class=class="str">"cmt">//Print(y); class=class="str">"cmt">//is_pre=true; if (y.Mean()>iHigh(_Symbol,_Period,class="num">0)/class="num">2+iLow(_Symbol,_Period,class="num">0)/class="num">2) pre="buy"; else pre="sell"; }
「把 NBeats 输出画到图上才有用」
整套流程走完,最核心的产出其实是一个 6949 KB 的 NBeats.onnx 文件,加上 11.99 KB 的 onnx.mq5 适配器。模型能从历史序列里拆出趋势项和季节项,但原版测试 EA 只把结果用于内部判断,图表上什么都看不见。 一个直接能做的改造:在 onnx.mq5 里把 NBeats 输出的 trend 和 seasonal 分量用 PlotIndexSetDouble 画成两条独立线。这样你开 MT5 加载 EA 后,能直观看到模型认为的「惯性方向」和「周期扰动」分家到什么程度,比只看信号箭头实在。 外汇和贵金属杠杆高、滑点随机,这套 ONNX 推理只是概率层面的辅助,别把输出当下单指令直接跑实盘。先在本机回测里把两条线调出来,确认分量与价格走势的领先滞后关系,再谈扩展。