CatBoost 模型中的交叉验证和因果推理基础及导出为 ONNX 格式·综合运用
(3/3)·从交叉验证到 ONNX 落地,把因果判断与模型自检缝进 MT5 的最后一环
把均值偏离塞进 ONNX 输入张量
这段逻辑干的事很直接:用 H1 收盘价序列算每个周期窗口的均值偏离,再拼成模型要的特征向量。外汇与贵金属杠杆高,回测和实盘偏差可能很大,任何信号都只是概率倾向。 先看特征构造,ArrayResize(ret,1) 先给单值容器留位,随后倒序遍历 Periods 数组,对每个窗口 CopyClose 取 H1 最近 N 根收盘,用 MathMean(pr) 减最新价得到偏离值,经 ArrayInsert 累进 features,最后 ArraySetAsSeries(features,true) 让索引按时间倒序。 模型初始化时,ExtInputShape 被定为 {1, ArraySize(Periods)},也就是一次喂一根 K 线、宽度等于周期数。两个 ONNX 句柄 ExtHandle 与 ExtHandle2 都走 OnnxCreateFromBuffer,任一句柄无效就 Print 错误并 INIT_FAILED;之后分别 OnnxSetInputShape / OnnxSetOutputShape,输出形状写死为 {1}。 OnTick 里用 isNewBar 拦掉旧柱,fill_arays 填好 features 后拷贝进 f[],再调两次 OnnxRun:第一次出主预测 out,第二次出元信息 out_meta。你开 MT5 把 Periods 改成 {12,24,48} 这类组合,能直接观察输入维度变化对模型推理的影响。
ArrayResize(ret, class="num">1); for(class="type">int i=ArraySize(Periods)-class="num">1; i>=class="num">0; i--) { CopyClose(NULL,PERIOD_H1,class="num">1,Periods[i],pr); ret[class="num">0] = MathMean(pr) - pr[Periods[i]-class="num">1]; ArrayInsert(features, ret, ArraySize(features), class="num">0, WHOLE_ARRAY); } ArraySetAsSeries(features, true); } class="macro">#include <EURUSD ONNX include1.mqh> class="kw">const class="type">long ExtInputShape [] = {class="num">1, ArraySize(Periods)}; class="type">int OnInit() { ExtHandle = OnnxCreateFromBuffer(ExtModel, ONNX_DEFAULT); ExtHandle2 = OnnxCreateFromBuffer(ExtModel2, ONNX_DEFAULT); if(ExtHandle == INVALID_HANDLE || ExtHandle2 == INVALID_HANDLE) { Print("OnnxCreateFromBuffer error ", GetLastError()); class="kw">return(INIT_FAILED); } if(!OnnxSetInputShape(ExtHandle, class="num">0, ExtInputShape)) { Print("OnnxSetInputShape failed, error ", GetLastError()); OnnxRelease(ExtHandle); class="kw">return(-class="num">1); } if(!OnnxSetInputShape(ExtHandle2, class="num">0, ExtInputShape)) { Print("OnnxSetInputShape failed, error ", GetLastError()); OnnxRelease(ExtHandle2); class="kw">return(-class="num">1); } class="kw">const class="type">long output_shape[] = {class="num">1}; if(!OnnxSetOutputShape(ExtHandle, class="num">0, output_shape)) { Print("OnnxSetOutputShape error ", GetLastError()); class="kw">return(INIT_FAILED); } if(!OnnxSetOutputShape(ExtHandle2, class="num">0, output_shape)) { Print("OnnxSetOutputShape error ", GetLastError()); class="kw">return(INIT_FAILED); } class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { if(!isNewBar()) class="kw">return; class="type">class="kw">double features[]; fill_arays(features); class="type">class="kw">double f[ArraySize(Periods)]; class="type">int k = ArraySize(Periods) - class="num">1; for(class="type">int i = class="num">0; i < ArraySize(Periods); i++) { f[i] = features[i]; k--; } class="kw">static vector out(class="num">1), out_meta(class="num">1); class="kw">struct output { class="type">long label[]; class="type">class="kw">float tensor[]; }; output out2[], out2_meta[]; OnnxRun(ExtHandle, ONNX_DEBUG_LOGS, f, out, out2); OnnxRun(ExtHandle2, ONNX_DEBUG_LOGS, f, out_meta, out2_meta);
「模型信号触发后的平仓与新单逻辑」
这段逻辑跑在 EA 的 OnTick 里,前提是 meta_sig 大于 0.5,也就是元模型判定当前市场状态值得干预。此时若账户里已有 0 号(买)或 1 号(卖)品种的市场订单,就倒序遍历 OrdersTotal 逐个检查。 平仓分支很直接:买单且魔法码匹配、sig 大于 0.5 时,要求当前 Bid 与开仓价距离超过冻结级别才动手;卖单对应 sig 小于 0.5 的镜像条件。OrderClose 放在 do-while 里以 -1 为失败重试标志,每次 Sleep(50) 毫秒避免 broker 拒单后硬怼。 新单侧在 countOrders() 低于 max_orders 且 CheckMoneyForTrade 通过后才进。sig 小于 0.5 走卖单分支,stop 用 Bid - stoploss*_Point、take 用 Ask + takeprofit*_Point 计算,手数由 LotsOptimized(meta_sig) 动态给出。外汇与贵金属杠杆高,冻结级别和重试机制只是防错,不表示信号胜率,实盘前请在 MT5 策略测试器用历史数据验证这套阈值。
class="type">class="kw">double sig = out2[class="num">0].tensor[class="num">1]; class="type">class="kw">double meta_sig = out2_meta[class="num">0].tensor[class="num">1]; if(meta_sig > class="num">0.5) if(count_market_orders(class="num">0) || count_market_orders(class="num">1)) for(class="type">int b = OrdersTotal() - class="num">1; b >= class="num">0; b--) if(OrderSelect(b, SELECT_BY_POS) == true) { if(OrderType() == class="num">0 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic && sig > class="num">0.5) if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL) < MathAbs(Bid - OrderOpenPrice())) { class="type">int res = -class="num">1; do { res = OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), class="num">0, Red); Sleep(class="num">50); } class="kw">while (res == -class="num">1); } if(OrderType() == class="num">1 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic && sig < class="num">0.5) if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL) < MathAbs(Bid - OrderOpenPrice())) { class="type">int res = -class="num">1; do { res = OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), class="num">0, Red); Sleep(class="num">50); } class="kw">while (res == -class="num">1); } } if(meta_sig > class="num">0.5) if(countOrders() < max_orders && CheckMoneyForTrade(_Symbol, LotsOptimized(meta_sig), ORDER_TYPE_BUY)) { class="type">class="kw">double l = LotsOptimized(meta_sig); if(sig < class="num">0.5) { class="type">int res = -class="num">1; do { class="type">class="kw">double stop = Bid - stoploss * _Point; class="type">class="kw">double take = Ask + takeprofit * _Point;
◍ 用 do-while 兜底发单失败
在信号强度 sig 大于 0.5 时,脚本进入卖出分支,用 do-while 循环强行把 OrderSend 跑通。每次下单若返回 -1(经纪商拒绝或网络抖动),就 Sleep(50) 毫秒再试,直到 res 不是 -1 才退出循环。 卖出单的止损放在 Ask 加 stoploss 个 _Point,止盈放在 Bid 减 takeprofit 个 _Point,和买入分支的止损方向正好相反。这种对称写法能避免把止损挂到不利价位。 外汇与贵金属杠杆高,实盘里反复重试发单可能在不流动时段堆出意外仓位,建议在 MT5 策略测试器里先把 stoploss、takeprofit 和 Sleep 间隔调一遍,确认成交逻辑符合你的风控。
res = OrderSend(Symbol(), OP_BUY, l, Ask, class="num">0, stop, take, comment, OrderMagic); Sleep(class="num">50); } class="kw">while (res == -class="num">1); } else { if(sig > class="num">0.5) { class="type">int res = -class="num">1; do { class="type">class="kw">double stop = Ask + stoploss * _Point; class="type">class="kw">double take = Bid - takeprofit * _Point; res = OrderSend(Symbol(), OP_SELL, l, Bid, class="num">0, stop, take, comment, OrderMagic); Sleep(class="num">50); } class="kw">while (res == -class="num">1); } } }
把自定义回测和 MT5 原生结果对齐
接上两篇的模型封装,现在要把两个编译好的文件挂到自选模型上跑一遍。核心验证点只有一个:自定义测试器的逐笔成交与 MetaTrader 5 自带测试器的输出必须逐字一致,任何偏差都说明行情回放或下单逻辑有漏。 对齐之后,就可以切到真实 tick 报价跑机器人。重点不在看曲线漂亮,而是用 MT5 优化器去扫止损、止盈和手数组合,让样本内交易次数先堆起来,避免过拟合到几十笔的小样本上。外汇与贵金属杠杆品种跳空频繁,这类优化结果仅代表历史报价下的概率倾向,实盘仍存在较高风险。
「换个思路训练模型才有戏」
这套把交易任务按时间序列分类的做法,并没有现成的学术定论,是反复试错攒出来的路数,但在 EURUSD 上跑出来的几个 ONNX 模型文件体积差异很大——catmodel1.onnx 有 450.34 KB,而 catmodel_m2.onnx 只有 94.61 KB,说明不同时间框架压出来的模型复杂度差出好几倍。 机器学习模型有时候不能按显而易见的套路去训,架构之外,喂数据和用结果的方式才是分水岭。全自动的「交易员+研究员」流水线能跑通,需要专家干预的「老师」算法也同样轻便有效。 外汇和贵金属杠杆高、滑点狠,任何模型输出都只是概率倾向,真要上 MT5 验证,先把 ZIP 里的 ONNX_bot.mq5 和那几个 include 挂上 EURUSD 图表,看推理延迟再决定敢不敢用实盘。