在类中包装 ONNX 模型·进阶篇
EURUSD日线10根K线的分类模型封装
第一个落地模型叫 model.eurusd.D1.10.class.onnx,它用 EURUSD 的 D1 周期、连续 10 根 OHLC 价格序列训练出一个分类器。三种模型只在输入数据规模和预处理上有区别,这个类只重写了 Init 和 PredictClass 两个方法,其余逻辑留在基类里复用。 Init 里先调基类 CheckInit 创建 ONNX 会话,再显式把输入张量形状钉死为 {1, 10, 4}:第一维是 batch,第二维是 10 根 K 线,第三维是 OHLC 四个通道。注释比代码多,是因为 ONNX 导出的模型没把全部维度写进输入描述,不手动设就会推理报错。 PredictClass 要求输入数据和训练时完全一致——常规化(normalized)后的 OHLC 矩阵。外汇和贵金属杠杆高、跳空频繁,这种日线级模型只给出分类倾向,实盘前务必在 MT5 用历史数据跑一遍一致性校验。 下面这段是类定义的头半部分,重点看资源挂载和形状写死的位置。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ModelEurusdD1_10Class.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">10.class.onnx" as class="type">uchar model_eurusd_D1_10_class[] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model wrapper class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CModelEurusdD1_10Class : 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_10Class(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_sample_size=class="num">10; } 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_10_class)) { Print("model_eurusd_D1_10_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, third index - number of series(OHLC) const class="type">long input_shape[] = {class="num">1,m_sample_size,class="num">4}; if(!OnnxSetInputShape(m_handle,class="num">0,input_shape)) { Print("model_eurusd_D1_10_class : OnnxSetInputShape error ",GetLastError()); class="kw">return(false); }
◍ 给ONNX输出张量显式定形与归一化推理
把训练好的 EURUSD D1 分类模型接进 MT5,第一步不是跑推理,而是先把输出张量的形状钉死。OnnxSetOutputShape 的第二个参数填 0 表示第 0 个输出节点,形状数组 {1,3} 里 1 是 batch(必须和输入张量 batch 一致),3 对应涨 / 平 / 跌三类。若返回 false,直接打印 GetLastError 并退出,避免后续拿到空结果还当信号用。 预测阶段真正吃算力的是归一化。代码里先 CopyRates 拉最近 m_sample_size 根 OHLC 柱,用 Mean(1) 和 Std(1) 沿列求均值与标准差,再铺成同尺寸的 mm、ms 矩阵做逐元素减均值除标准差。注意模型要的是竖向 OHLC 向量,所以 rates 必须 Transpose 转置后再归一,否则维度对不上 OnnxRun 会直接报错返回 -1。 外汇与贵金属自带高杠杆与跳空风险,这套归一化推理只解决「喂给模型的数据长相对」,不保证分类命中率;实盘前建议在策略测试器用 2020—2023 年数据回测,观察三类输出的概率分布是否随波动率 regime 偏移。
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_10_class : OnnxSetOutputShape error ",GetLastError()); class="kw">return(false); } class="kw">return(true); class="kw">virtual class="type">int PredictClass(class="type">void) { class="kw">static matrixf input_data(m_sample_size,class="num">4); class="kw">static vectorf output_data(class="num">3); class="kw">static matrix mm(m_sample_size,class="num">4); class="kw">static matrix ms(m_sample_size,class="num">4); class="kw">static matrix x_norm(m_sample_size,class="num">4); matrix rates; if(!rates.CopyRates(m_symbol,m_period,COPY_RATES_OHLC,class="num">1,m_sample_size)) class="kw">return(-class="num">1); vector m=rates.Mean(class="num">1); vector s=rates.Std(class="num">1); for(class="type">int i=class="num">0; i<m_sample_size; i++) { mm.Row(m,i); ms.Row(s,i); } x_norm=rates.Transpose(); x_norm-=mm; x_norm/=ms; input_data.Assign(x_norm); if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data)) class="kw">return(-class="num">1);
「用 ArgMax 把网络输出转成交易信号」
上面这段闭包是预测评估的收口:模型前向跑完之后,不直接把浮点向量抛给策略层,而是用 ArgMax 取最大分量下标,再强转成 int 返回。 ArgMax 的语义很直白——哪一类神经元的激活值最高,就判为当前样本所属类别。在分类式行情判别里,这步相当于把「概率分布」压缩成「唯一标签」,下游 EA 拿到 0/1/2 这类离散值就能直接进规则判断。 需要注意,ArgMax 只给「最可能」那一类,不附带置信度。若两类输出接近,返回结果可能在相邻标签间跳变,实盘里建议额外读 output_data 原始分量做阈值过滤,外汇与贵金属波动剧烈,纯靠硬分类信号进场属于高风险操作。
class=class="str">"cmt">//--- evaluate prediction class="kw">return(class="type">int(output_data.ArgMax())); } }; class=class="str">"cmt">//+------------------------------------------------------------------+
用紧凑EA验证类预测能力
为验证封装类的实际表现,我们搭了一个极简智能交易系统,不堆指标、不绕逻辑,只做一件事:把类输出的预测类别直接转成开平仓动作。 模型训练数据截止 2022 年末,所以测试窗口选在 2023 年 1 月 1 日之后,避免未来信息泄漏。跑下来模型能正常吐出预测类别并触发交易,说明类的接口在实盘 tick 流下是通的。 下面这段 EA 源码就是验证载体,核心在 OnTick 里:先让 ExtModel.CheckOnTick 过滤非新柱,再拿 PredictClass 取类别,>=0 才进 PositionSelect 分支决定开或平。 外汇与贵金属属于高风险品种,这类基于历史数据训练的模型在样本外可能失效,任何回测通过都不构成稳健盈利保证,上 MT5 前请先用策略测试器跑 2023 年数据自查。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX.eurusd.D1.Prediction.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">#include "ModelEurusdD1_10Class.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_10Class ExtModel; CTrade ExtTrade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if(!ExtModel.Init(_Symbol,_Period)) class="kw">return(INIT_FAILED); 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(const class="type">int reason) { ExtModel.Shutdown(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { if(!ExtModel.CheckOnTick()) class="kw">return; class=class="str">"cmt">//--- predict next price movement class="type">int predicted_class=ExtModel.PredictClass(); 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); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for open position conditions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CheckForOpen(const class="type">int predicted_class) { ENUM_ORDER_TYPE signal=WRONG_VALUE; class=class="str">"cmt">//--- check signals
◍ 信号触发后的开仓与反手逻辑
模型给出预测分类后,EA 并不直接下单,而是先把它映射成 MT5 的订单方向常量。predicted_class 等于 PRICE_DOWN 时记空单信号,等于 PRICE_UP 时记多单信号,其余情况 signal 保持 WRONG_VALUE 不动作。 开仓前用 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 确认终端允许交易,再按信号方向取 BID 或 ASK 报价。ExtTrade.PositionOpen 以 InpLots 手数市价入场,滑点和止损止盈暂传 0,实盘需自行补参数。 平仓判断在 CheckForClose 里完成:已持多单且预测转空、或已持空单且预测转多,bsignal 置真。此时先 PositionClose(_Symbol,3) 平掉原仓,3 为异步关闭模式偏差值,随后立刻调用 CheckForOpen 反手开反向单。 外汇与贵金属杠杆高,这类预测反手链在震荡市可能连续挨扫,建议先在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍,观察 signal 翻转频率再决定要不要加过滤。
if(predicted_class==PRICE_DOWN) signal=ORDER_TYPE_SELL; class=class="str">"cmt">// sell condition else { if(predicted_class==PRICE_UP) signal=ORDER_TYPE_BUY; class=class="str">"cmt">// buy condition } class=class="str">"cmt">//--- open position if possible according to signal if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { class="type">class="kw">double price=SymbolInfoDouble(_Symbol,(signal==ORDER_TYPE_SELL) ? SYMBOL_BID : SYMBOL_ASK); ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,class="num">0,class="num">0); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for close position conditions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CheckForClose(const class="type">int predicted_class) { class="type">bool bsignal=false; class=class="str">"cmt">//--- position already selected before class="type">long type=PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- check signals if(type==POSITION_TYPE_BUY && predicted_class==PRICE_DOWN) bsignal=true; if(type==POSITION_TYPE_SELL && predicted_class==PRICE_UP) bsignal=true; class=class="str">"cmt">//--- close position if possible if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { ExtTrade.PositionClose(_Symbol,class="num">3); class=class="str">"cmt">//--- open opposite CheckForOpen(predicted_class); } }
「EURUSD日线30根K线的分类模型怎么接」
第二类模型文件叫 model.eurusd.D1.30.class.onnx,训练输入是 EURUSD 日线连续 30 个收盘价,加上周期 21 与 34 的两条 SMA。和前一类一样,Init 里直接调基类的 CheckInit,由它建 ONNX 会话并显式给定输入输出张量尺寸——这点不能省,因为模型文件里没把输入维度写全。 PredictClass 负责喂入 30 根历史收盘价和算好的两条均线,归一化方式必须和训练时一致,否则输出类别会漂移。验证时只需改测试 EA 两行代码、参数不动,跑出来模型是有效的,说明换品种周期类封装的接口是通的。 外汇与贵金属杠杆高、滑点大,这类日线分类信号只代表历史样本下的倾向,实盘前务必在 MT5 策略测试器用 EURUSD D1 复跑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ModelEurusdD1_30Class.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">30.class.onnx" as class="type">uchar model_eurusd_D1_30_class[] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model wrapper class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CModelEurusdD1_30Class : class="kw">public CModelSymbolPeriod { class="kw">private: class="type">int m_sample_size; class="type">int m_fast_period; class="type">int m_slow_period; class="type">int m_sma_fast; class="type">int m_sma_slow; class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CModelEurusdD1_30Class(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_sample_size=class="num">30; m_fast_period=class="num">21; m_slow_period=class="num">34; m_sma_fast=INVALID_HANDLE; m_sma_slow=INVALID_HANDLE; } 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_30_class)) { Print("model_eurusd_D1_30_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