基于Python和MQL5的特征工程(第一部分):为长期 AI 模型预测移动平均线·综合运用
📊

基于Python和MQL5的特征工程(第一部分):为长期 AI 模型预测移动平均线·综合运用

(3/3)· 跨297个品种实测显示,预测均线比预测价格准率高出18%,这篇把整套落地流程拆给你看

实战向 第 3/3 篇
很多人直接让模型猜未来价格,却没意识到均线作为线性组合更容易被AI拟合。跨200+品种实测,预测价格准率平均掉34%,而预测均线能稳在70%上下。把目标从价格换成均线,是你长期策略里最省力的杠杆。

◍ 把均线策略塞进EA骨架

这一节干的是落地活:用 MQL5 把前面训好的 ONNX 模型接进交易程序,靠慢速均线通道和 ATR 动态止损来管仓位,而不是每根 K 线都硬怼进场。程序会在开仓前向前推算 40 步均线,当作确认信号,模仿人不会强行摸顶抄底的习惯。 回测挑的是模型训练时没见过的样本:AUDJPY 日线,2023 年 1 月初到 2024 年 6 月 28 日,共约 18 个月。延迟参数设成“随机延迟”并启用真实价格变动,尽量贴近实盘那点不可控的滑点和小幅拖沓。 代码里先把两个 onnx 文件以 #resource 嵌进程序,分别管均线和 RSI 状态;全局变量集中放 handle、bid/ask、ATR 止损位和仓位步长,更新动作收进独立函数,比在 OnTick 里堆逻辑清爽得多。外汇与贵金属杠杆高、随机延迟下回测表现未必能复现,实盘前请先在策略测试器用未知数据跑一遍。 初始化时先查交易权限,过了才加载指标和模型;退出时释放资源。OnTick 里只做两件事:无持仓就刷变量找机会,有持仓就挪跟踪止损。下面这段是骨架开头,复制进 MT5 能直接看到资源挂载和全局定义。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 GBPUSD AI.mq5 |
class=class="str">"cmt">//|                                                     Gamuchirai Zororo Ndawana |
class=class="str">"cmt">//|                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our resources                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource  "\\Files\\AUDJPY D1 MA AI F22 P40.onnx" as const class="type">uchar onnx_buffer[];
class="macro">#resource  "\\Files\\AUDJPY D1 RSI AI F22 P40.onnx" as const class="type">uchar rsi_onnx_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
CTrade Trade;
class="macro">#include <Trade\OrderInfo.mqh>
class COrderInfo;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long     onnx_model;
class="type">int      ma_handler,state;
class="type">class="kw">double   bid,ask,vol;
vectorf  model_forecast   = vectorf::Zeros(class="num">1);
vectorf  rsi_model_output = vectorf::Zeros(class="num">1);
class="type">class="kw">double   min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,atr_stop,risk_equity;
class="type">class="kw">double   take_profit = class="num">0;
class="type">class="kw">double   close_price[class="num">3],atr_reading[],ma_buffer[];
class="type">long     min_distance,login;
class="type">int      atr,close_average,ticket_1,ticket_2;

「EA 初始化时的交易权限与变量骨架」

写 MT5 EA 第一道坎不是指标计算,而是 OnInit 里的交易授权。代码先用 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 查终端是否允许交易,再查 MQLInfoInteger(MQL_TRADE_ALLOWED) 确认脚本级允许,两者任一不通就 return INIT_FAILED,并在图上 Comment 提示按 Ctrl+E 或重加载。只有都过,才进 setup() 初始化后续句柄。 全局变量里藏着这套系统的风控与仓位底牌:stop_percent 固定为 1(即 1% 止损线),atr_period 常量设为 200、atr_multiple 输入 2.5,意味着止损距离倾向取 200 周期 ATR 的 2.5 倍。lot_multiple 输入 10 控制仓位倍数,profit_target 与 loss_target 默认 0 表示不硬设盈亏上限,实盘前你得手动填。 OnDeinit 释放了 onnx_model、rsi_onnx_model 和 atr 三个句柄,避免内存泄漏;OnTick 逻辑很轻,先 update() 刷数据,无持仓才 check_setup() 找信号。外汇与贵金属波动剧烈,200 周期 ATR 在 H1 以上周期才具参考意义,小周期容易因噪声误判止损带宽。 开 MT5 把这段全局声明和 OnInit 贴进新 EA,故意不点「允许算法交易」编译运行,能看到图上弹出授权提示——这比看文档更能记住 INIT_FAILED 的触发路径。

MQL5 / C++
class="type">bool    authorized = false;
class="type">class="kw">double  margin,lot_step;
class="type">class="kw">string  currency,server;
class="type">bool    all_closed =true;
class="type">int     rsi_handler;
class="type">long    rsi_onnx_model;
class="type">class="kw">double  indicator_reading[];
ENUM_ACCOUNT_TRADE_MODE account_type;
const class="type">class="kw">double   stop_percent = class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Technical indicators                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
input group "Money Management"
input class="type">int      lot_multiple     = class="num">10; class=class="str">"cmt">// How big should the lot size be?
input class="type">class="kw">double profit_target = class="num">0;      class=class="str">"cmt">// Profit Target
input class="type">class="kw">double loss_target   = class="num">0;      class=class="str">"cmt">// Max Loss Allowed
input group "Money Management"
const class="type">int   atr_period = class="num">200;        class=class="str">"cmt">//ATR Period
input class="type">class="kw">double atr_multiple =class="num">2.5;      class=class="str">"cmt">//ATR Multiple
class="type">int OnInit()
  {
class=class="str">"cmt">//Authorization
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
       Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
       class="kw">return(INIT_FAILED);
     }
   else
    if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
      {
       Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
       class="kw">return(INIT_FAILED);
      }
      else
      {
       Comment("This License is Genuine");
       setup();
      }
class=class="str">"cmt">//Everything was okay
   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)
  {
   OnnxRelease(onnx_model);
   OnnxRelease(rsi_onnx_model);
   IndicatorRelease(atr)
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//Update technical data
   update();
   if(PositionsTotal() == class="num">0)
     {
      check_setup();
      }

用 ONNX 双模型给 MT5 下方向预判

这段代码把均线和 RSI 两个独立 ONNX 模型的推理结果揉在一起,作为开仓前的最终信号过滤器。它先取 MA 缓冲区最近 40 根和 RSI 最新 1 根,再分别喂进两个模型跑前向推理。 MA 分支用 ma_buffer[0] 与 ma_buffer[39] 比大小:前者大于后者置 model_inputs[0]=1 代表短均线上穿倾向,反之置 model_inputs[1]=1。RSI 分支按最新值落区:低于 30 写 rsi_model_inputs[0]=1(超卖),高于 70 写 [1]=1(超买),中间地带写 [2]=1。 两个模型各跑一次 OnnxRun,输出 model_forecast 与 rsi_model_output。只有当 RSI 模型输出与 MA 模型输出同号且大于 0,才在图表左上角打印「AI Forecast: UP」并返回 1;同号小于 0 则返回 -1 并打印 DOWN;其余情况返回 0 表示无信号。外汇与贵金属杠杆高,模型同号仅提高概率,实盘前务必在 MT5 策略测试器用历史数据验证回测稳定性。 想直接验证,把下面函数贴进 EA 的同类文件,确认 onnx_model 与 rsi_onnx_model 已用 OnnxCreate 加载,否则 OnnxRun 会返回空向量。

MQL5 / C++
class="type">int model_predict(class="type">void)
  {
class=class="str">"cmt">//MA Forecast
   vectorf  model_inputs = vectorf::Zeros(class="num">2);
   vectorf  rsi_model_inputs = vectorf::Zeros(class="num">3);
   CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">40,ma_buffer);
   if(ma_buffer[class="num">0] > ma_buffer[class="num">39])
     {
      model_inputs[class="num">0] = class="num">1;
      model_inputs[class="num">1] = class="num">0;
     }
   else
     if(ma_buffer[class="num">0] < ma_buffer[class="num">39])
       {
       model_inputs[class="num">1] = class="num">1;
       model_inputs[class="num">0] = class="num">0;
       }
class=class="str">"cmt">//RSI Forecast
   CopyBuffer(rsi_handler,class="num">0,class="num">0,class="num">1,indicator_reading);
   if(indicator_reading[class="num">0] < class="num">30)
     {
      rsi_model_inputs[class="num">0] = class="num">1;
      rsi_model_inputs[class="num">1] = class="num">0;
      rsi_model_inputs[class="num">2] = class="num">0;
     }
   else
     if(indicator_reading[class="num">0] >class="num">70)
       {
       rsi_model_inputs[class="num">0] = class="num">0;
       rsi_model_inputs[class="num">1] = class="num">1;
       rsi_model_inputs[class="num">2] = class="num">0;
       }
      else
       {
       rsi_model_inputs[class="num">0] = class="num">0;
       rsi_model_inputs[class="num">1] = class="num">0;
       rsi_model_inputs[class="num">2] = class="num">1;
       }
class=class="str">"cmt">//Model predictions
   OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast);
   OnnxRun(rsi_onnx_model,ONNX_DEFAULT,rsi_model_inputs,rsi_model_output);
class=class="str">"cmt">//Evaluate model output for buy setup
   if(((rsi_model_output[class="num">0] > class="num">0)  && (model_forecast[class="num">0] > class="num">0)))
     {
      class=class="str">"cmt">//AI Models forecast
      Comment("AI Forecast: UP");
      class="kw">return(class="num">1);
     }
class=class="str">"cmt">//Evaluate model output for a sell setup
   if((rsi_model_output[class="num">0] < class="num">0) && (model_forecast[class="num">0] < class="num">0))
     {
      Comment("AI Forecast: DOWN");
      class="kw">return(-class="num">1);
     }
class=class="str">"cmt">//Otherwise no position was found
   class="kw">return(class="num">0);
  }

◍ 实时刷新与模型加载的底层钩子

EA 跑起来后,第一道关是把盘口和指标缓冲拉到最新。update() 里先用 SymbolInfoDouble 抓 ask / bid,再把 buy_stop_loss 和 sell_stop_loss 清零,避免上一根蜡烛的止损价污染当前判断。 静态变量 time_stamp 用来识别新蜡烛:iTime 取当前周期 0 号柱时间,只有 time_stamp != time 时才重算保证金 OrderCalcMargin,否则每 tick 重复算无意义。ATR 读数通过 CopyBuffer(atr,0,0,1,atr_reading) 拿 1 个值,ArraySetAsSeries 置为时间序列后,atr_stop = (min_volume + atr_reading[0]) * atr_multiple,这是动态止损距离的基准。 setup() 负责把账户、指标、品种限制和 ONNX 模型一次性挂好。MA 用 40 周期 SMA 取 PRICE_LOW,RSI 用 30 周期收价,vol 由 SYMBOL_VOLUME_MIN 乘 lot_multiple 得出。 品种层约束也得读:min_distance 来自 SYMBOL_TRADE_STOPS_LEVEL,是挂单/止损的最小点数硬性门槛;max_volume_increase 用 SYMBOL_VOLUME_MAX 除以 SYMBOL_VOLUME_MIN,告诉你该品种最大能开最小手的几倍。 模型侧用 ulong 数组声明输入输出维度——ma_input_shape 是 {1,2},rsi_input_shape 是 {1,3},output_shape 是 {1,1},最后 OnnxCreateFromBuffer 从内存 buffer 建模型。开 MT5 把 atr_period 和 lot_multiple 改成你的参数,能直接看缓冲是否按预期刷新。外汇与贵金属杠杆高,模型推理仅作概率参考,实盘前务必在策略测试器跑历史数据验证。

MQL5 / C++
class="type">void update(class="type">void)
  {
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   buy_stop_loss = class="num">0;
   sell_stop_loss = class="num">0;
   class="kw">static class="type">class="kw">datetime time_stamp;
   class="type">class="kw">datetime time = iTime(_Symbol,PERIOD_CURRENT,class="num">0);
   check_price(class="num">3);
   CopyBuffer(atr,class="num">0,class="num">0,class="num">1,atr_reading);
   CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">1,ma_buffer);
   ArraySetAsSeries(atr_reading,true);
   atr_stop = ((min_volume + atr_reading[class="num">0]) * atr_multiple);
class=class="str">"cmt">//On Every Candle
   if(time_stamp != time)
     {
      class=class="str">"cmt">//Mark the candle
       time_stamp = time;
       OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin);
     }
  }
class="type">bool setup(class="type">void)
  {
class=class="str">"cmt">//Account Info
   currency = AccountInfoString(ACCOUNT_CURRENCY);
   server = AccountInfoString(ACCOUNT_SERVER);
   login = AccountInfoInteger(ACCOUNT_LOGIN);
class=class="str">"cmt">//Indicators
   atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
class=class="str">"cmt">//Setup technical indicators
   ma_handler  =iMA(Symbol(),PERIOD_CURRENT,class="num">40,class="num">0,MODE_SMA,PRICE_LOW);
   vol         = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;
   rsi_handler = iRSI(Symbol(),PERIOD_CURRENT,class="num">30,PRICE_CLOSE);
class=class="str">"cmt">//Market Information
   min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
class=class="str">"cmt">//Define our ONNX model
   class="type">ulong ma_input_shape [] = {class="num">1,class="num">2};
   class="type">ulong rsi_input_shape [] = {class="num">1,class="num">3};
   class="type">ulong output_shape [] = {class="num">1,class="num">1};
class=class="str">"cmt">//Create the model
   onnx_model     = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);

「双模型加载与张量校验的落地细节」

在 MT5 里把 ONNX 模型塞进 EA 不是简单读文件,得先确认句柄有效。下面这段先尝试从缓冲区创建 RSI 模型的句柄,再和 MA 模型句柄一起做 INVALID_HANDLE 判定,任一失败就 Comment 报错并 return(false),避免后续推理在空模型上崩。 输入输出的张量形状必须显式设定。代码中对 onnx_model 和 rsi_onnx_model 分别调用 OnnxSetInputShape / OnnxSetOutputShape,索引都用 0,形状变量为 ma_input_shape、rsi_input_shape 与公用的 output_shape;任一步返回 false 就抓 GetLastError() 打出来,方便定位是形状维度不匹配还是模型结构不对。 资源声明侧能看到实际落地的文件:\Files\AUDJPY D1 MA AI F22 P40.onnx\Files\AUDJPY D1 RSI AI F22 P40.onnx,后缀 F22 P40 大概率指特征数 22、预测窗口 40,做 AUDJPY 日线推理用。外汇与贵金属杠杆高、滑点跳空频繁,这类 AI 信号仅作概率参考,实盘前务必在策略测试器用历史数据回测验证。 别把句柄判空当多余步骤。很多加载失败表现为推理结果恒为 0 或订单异常,提前在初始化阶段拦掉 INVALID_HANDLE,能省掉后面几小时的瞎调。

MQL5 / C++
  rsi_onnx_model = OnnxCreateFromBuffer(rsi_onnx_buffer,ONNX_DEFAULT);
  if((onnx_model == INVALID_HANDLE) || (rsi_onnx_model == INVALID_HANDLE))
    {
      Comment("[ERROR] Failed to load AI module correctly");
      class="kw">return(false);
    }
class=class="str">"cmt">//Validate I/O
  if((!OnnxSetInputShape(onnx_model,class="num">0,ma_input_shape)) || (!OnnxSetInputShape(rsi_onnx_model,class="num">0,rsi_input_shape)))
    {
      Comment("[ERROR] Failed to set input shape correctly: ",GetLastError());
      class="kw">return(false);
    }
  if((!OnnxSetOutputShape(onnx_model,class="num">0,output_shape)) || (!OnnxSetOutputShape(rsi_onnx_model,class="num">0,output_shape)))
    {
      Comment("[ERROR] Failed to load AI module correctly: ",GetLastError());
      class="kw">return(false);
    }
class=class="str">"cmt">//Everything went fine
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     GBPUSD AI.mq5 |
class=class="str">"cmt">//|       Gamuchirai Zororo Ndawana |
class=class="str">"cmt">//|       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our resources                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Files\\AUDJPY D1 MA AI F22 P40.onnx" as const class="type">uchar onnx_buffer[];
class="macro">#resource "\\Files\\AUDJPY D1 RSI AI F22 P40.onnx" as const class="type">uchar rsi_onnx_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
CTrade Trade;
class="macro">#include <Trade\OrderInfo.mqh>
class COrderInfo;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long    onnx_model;

EA 全局变量与风控参数的落地定义

这段声明集中暴露了策略对账户权限与仓位尺度的硬约束。先看一下核心变量与输入参数的组织方式: int ma_handler,state; double bid,ask,vol; vectorf model_forecast = vectorf::Zeros(1); vectorf rsi_model_output = vectorf::Zeros(1); double min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,atr_stop,risk_equity; double take_profit = 0; double close_price[3],atr_reading[],ma_buffer[]; long min_distance,login; int atr,close_average,ticket_1,ticket_2; bool authorized = false; double margin,lot_step; string currency,server; bool all_closed =true; int rsi_handler; long rsi_onnx_model; double indicator_reading[]; ENUM_ACCOUNT_TRADE_MODE account_type; const double stop_percent = 1; //+------------------------------------------------------------------+

//Technical indicators

//+------------------------------------------------------------------+ input group "Money Management" input int lot_multiple = 10; // How big should the lot size be? input double profit_target = 0; // Profit Target input double loss_target = 0; // Max Loss Allowed input group "Money Management" input int bb_period = 36; //Bollinger band period input int ma_period = 4; //Moving average period const int atr_period = 200; //ATR Period input double atr_multiple =2.5; //ATR Multiple //+------------------------------------------------------------------+

//Expert initialization function

//+------------------------------------------------------------------+ int OnInit() { //Authorization if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program"); return(INIT_FAILED); } else if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading"); return(INIT_FAILED); } else { Comment("This License is Genuine"); setup(); } 逐行拆解:前 4 行定义均线句柄、买卖价与模型预测向量(各长度 1,说明单输出推理);第 7 行 stop_percent 常量锁定 1% 止损线。input 段里 lot_multiple=10 代表仓位以基准手数的 10 倍放大,bb_period=36、ma_period=4 构成布林加短均线的组合,atr_period=200 配合 atr_multiple=2.5 意味着止损距离取 200 周期 ATR 的 2.5 倍——在 XAUUSD 上这常对应 20~40 美元波动带,属典型高波动品种参数。 OnInit 里先做两层交易授权校验:TerminalInfoInteger 查终端是否放行,MQLInfoInteger 查 EA 自身是否被允许。任一不通过直接 INIT_FAILED 并提示按 Ctrl+E 或重载,都过了才进 setup()。 开 MT5 把这段贴进 EA 头部,改 lot_multiple 从 10 降到 2,观察回测里净值曲线的回撤深度变化;外汇与贵金属杠杆高,参数放大后爆仓概率会显著上升,请先在策略测试器用历史数据验证。

MQL5 / C++
class="type">int      ma_handler,state;
class="type">class="kw">double   bid,ask,vol;
vectorf  model_forecast   = vectorf::Zeros(class="num">1);
vectorf  rsi_model_output = vectorf::Zeros(class="num">1);
class="type">class="kw">double   min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,atr_stop,risk_equity;
class="type">class="kw">double   take_profit = class="num">0;
class="type">class="kw">double   close_price[class="num">3],atr_reading[],ma_buffer[];
class="type">long     min_distance,login;
class="type">int      atr,close_average,ticket_1,ticket_2;
class="type">bool     authorized = false;
class="type">class="kw">double   margin,lot_step;
class="type">class="kw">string   currency,server;
class="type">bool     all_closed =true;
class="type">int      rsi_handler;
class="type">long     rsi_onnx_model;
class="type">class="kw">double   indicator_reading[];
ENUM_ACCOUNT_TRADE_MODE account_type;
const class="type">class="kw">double stop_percent = class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Technical indicators                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
input group "Money Management"
input class="type">int      lot_multiple     = class="num">10; class=class="str">"cmt">// How big should the lot size be?
input class="type">class="kw">double profit_target = class="num">0;       class=class="str">"cmt">// Profit Target
input class="type">class="kw">double loss_target   = class="num">0;       class=class="str">"cmt">// Max Loss Allowed
input group "Money Management"
input class="type">int      bb_period = class="num">36;        class=class="str">"cmt">//Bollinger band period
input class="type">int      ma_period = class="num">4;         class=class="str">"cmt">//Moving average period
const class="type">int      atr_period = class="num">200;      class=class="str">"cmt">//ATR Period
input class="type">class="kw">double atr_multiple =class="num">2.5;       class=class="str">"cmt">//ATR Multiple
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//Authorization
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
       Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
       class="kw">return(INIT_FAILED);
     }
   else
      if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
        {
          Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
          class="kw">return(INIT_FAILED);
        }
       else
        {
          Comment("This License is Genuine");
          setup();
        }

◍ EA 主循环与双模型预测拼接

EA 退出时先把两个 ONNX 模型句柄释放掉,避免 MT5 终端反复加载推理引擎造成内存泄漏。OnDeinit 里直接调 OnnxRelease 即可,主逻辑不在这里跑。 行情每跳进 OnTick:无持仓就 check_setup 找入场,有持仓就 check_atr_stop 管止损。这个二分结构把『开仓判断』和『持仓管理』彻底拆开,回测时你能单独屏蔽任一侧看哪边在贡献收益。 model_predict 是给神经网络喂特征的地方。MA 分支取 40 根均线缓冲,比首尾:buffer[0] 大于 buffer[39] 置 model_inputs[0]=1 表多头倾向,反之置 [1]=1;RSI 分支只取最新 1 根,<30 归超卖、>70 归超买、中间归中性,分别写进 3 维向量。外汇与贵金属杠杆高,这类信号只代表模型概率倾向,实盘前务必在策略测试器跑至少 3 个月 tick 数据。

MQL5 / C++
class=class="str">"cmt">//--- Everything was okay
   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)
   {
class=class="str">"cmt">//---
   OnnxRelease(onnx_model);
   OnnxRelease(rsi_onnx_model);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
   {
class=class="str">"cmt">//--- Update technical data
   update();
   if(PositionsTotal() == class="num">0)
      {
      check_setup();
      }
   if(PositionsTotal() > class="num">0)
      {
      check_atr_stop();
      }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get a prediction from our model                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int model_predict(class="type">void)
   {
class=class="str">"cmt">//MA Forecast
   vectorf  model_inputs = vectorf::Zeros(class="num">2);
   vectorf  rsi_model_inputs = vectorf::Zeros(class="num">3);
   CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">40,ma_buffer);
   if(ma_buffer[class="num">0] > ma_buffer[class="num">39])
      {
      model_inputs[class="num">0] = class="num">1;
      model_inputs[class="num">1] = class="num">0;
      }
   else
      if(ma_buffer[class="num">0] < ma_buffer[class="num">39])
         {
         model_inputs[class="num">1] = class="num">1;
         model_inputs[class="num">0] = class="num">0;
         }
class=class="str">"cmt">//RSI Forecast
   CopyBuffer(rsi_handler,class="num">0,class="num">0,class="num">1,indicator_reading);
   if(indicator_reading[class="num">0] < class="num">30)
      {
      rsi_model_inputs[class="num">0] = class="num">1;
      rsi_model_inputs[class="num">1] = class="num">0;
      rsi_model_inputs[class="num">2] = class="num">0;
      }
   else
      if(indicator_reading[class="num">0] >class="num">70)
         {
         rsi_model_inputs[class="num">0] = class="num">0;
         rsi_model_inputs[class="num">1] = class="num">1;
         rsi_model_inputs[class="num">2] = class="num">0;
         }
      else
         {
         rsi_model_inputs[class="num">0] = class="num">0;
         rsi_model_inputs[class="num">1] = class="num">0;

「双模型信号落地与每根K线的数据刷新」

预测函数跑完两个 ONNX 模型后,用 rsi_model_output[0] 与 model_forecast[0] 的符号一致性做开仓判定:两者同为正返回 1(倾向多),同为负返回 -1(倾向空),其余情况返回 0 不出手。 check_setup 直接消费 model_predict 的返回值,res==1 时 Trade.Buy 以当前 ask 市价买入并打标 "VD V75 AI",res==-1 则 Trade.Sell 以 bid 市价卖出;state 变量同步记录持仓方向,方便后续模块读取。外汇与贵金属杠杆品种波动剧烈,这类市价单在滑点行情中可能偏离预期入场价,实盘前务必在 MT5 策略测试器跑通。 update 负责每 tick 刷新 ask/bid,并在新蜡烛出现时(time_stamp != time)才重算保证金。ATR 止损距离按 (min_volume + atr_reading[0]) * atr_multiple 计算,CopyBuffer 只取最新 1 根,ArraySetAsSeries 置为时间序列,保证 atr_reading[0] 是当下值。把 atr_multiple 从默认改到 2~3 区间,能直观看到止损带宽变化。

MQL5 / C++
  rsi_model_inputs[class="num">2] = class="num">1;
  }
class=class="str">"cmt">//Model predictions
   OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast);
   OnnxRun(rsi_onnx_model,ONNX_DEFAULT,rsi_model_inputs,rsi_model_output);
class=class="str">"cmt">//Evaluate model output for buy setup
   if(((rsi_model_output[class="num">0] > class="num">0)  && (model_forecast[class="num">0] > class="num">0)))
     {
      class=class="str">"cmt">//AI Models forecast
      Comment("AI Forecast: UP");
      class="kw">return(class="num">1);
     }
class=class="str">"cmt">//Evaluate model output for a sell setup
   if((rsi_model_output[class="num">0] < class="num">0) && (model_forecast[class="num">0] < class="num">0))
     {
      Comment("AI Forecast: DOWN");
      class="kw">return(-class="num">1);
     }
class=class="str">"cmt">//Otherwise no position was found
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check for valid trade setups                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_setup(class="type">void)
  {
   class="type">int res = model_predict();
   if(res == -class="num">1)
     {
      Trade.Sell(vol,Symbol(),bid,class="num">0,class="num">0,"VD V75 AI");
      state = -class="num">1;
     }
   else
     if(res == class="num">1)
       {
        Trade.Buy(vol,Symbol(),ask,class="num">0,class="num">0,"VD V75 AI");
        state = class="num">1;
       }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our market data                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
  {
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   buy_stop_loss = class="num">0;
   sell_stop_loss = class="num">0;
   class="kw">static class="type">class="kw">datetime time_stamp;
   class="type">class="kw">datetime time = iTime(_Symbol,PERIOD_CURRENT,class="num">0);
   check_price(class="num">3);
   CopyBuffer(atr,class="num">0,class="num">0,class="num">1,atr_reading);
   CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">1,ma_buffer);
   ArraySetAsSeries(atr_reading,true);
   atr_stop = ((min_volume + atr_reading[class="num">0]) * atr_multiple);
class=class="str">"cmt">//On Every Candle
   if(time_stamp != time)
     {
      class=class="str">"cmt">//Mark the candle
      time_stamp = time;
      OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

EA 初始化时把账户、指标和 ONNX 模型一次性装好

在 MT5 的 EA 里,setup() 函数负责在启动阶段把后续交易要用的所有资源抓齐。它先读账户信息(币种、服务器、登录号),再挂技术面句柄:ATR 用当前周期默认参数,MA 用 40 周期 SMA 取最低价,RSI 用 30 周期收价——这几个数字直接决定后面信号过滤的尺度。 仓位相关变量也从品种属性里拿:最小成交量、最大可放大倍数(SYMBOL_VOLUME_MAX / SYMBOL_VOLUME_MIN)、止损最小距离 stoplevel、lot_step 等。外汇和贵金属杠杆高、点值敏感,这些若写死常数,换品种就可能下不了单。 AI 部分用 OnnxCreateFromBuffer 从内存 buffer 载入两个模型,ma 输入形状 {1,2}、rsi 输入 {1,3}、输出统一 {1,1}。任一句柄为 INVALID_HANDLE 就 Comment 报错并返回 false,模型没挂上 EA 直接罢工,不会带着残血状态跑。 输入输出的 shape 还要用 OnnxSetInputShape / OnnxSetOutputShape 再校验一次,失败同样返回 false。只有全部通过才 return true,意味着此后主循环才可以安全调用模型推理。

MQL5 / C++
class="type">bool setup(class="type">void)
  {
class=class="str">"cmt">//Account Info
   currency = AccountInfoString(ACCOUNT_CURRENCY);
   server = AccountInfoString(ACCOUNT_SERVER);
   login = AccountInfoInteger(ACCOUNT_LOGIN);
class=class="str">"cmt">//Indicators
   atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
class=class="str">"cmt">//Setup technical indicators
   ma_handler   =iMA(Symbol(),PERIOD_CURRENT,class="num">40,class="num">0,MODE_SMA,PRICE_LOW);
   vol          = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;
   rsi_handler  = iRSI(Symbol(),PERIOD_CURRENT,class="num">30,PRICE_CLOSE);
class=class="str">"cmt">//Market Information
   min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
class=class="str">"cmt">//Define our ONNX model
   class="type">ulong ma_input_shape [] = {class="num">1,class="num">2};
   class="type">ulong rsi_input_shape [] = {class="num">1,class="num">3};
   class="type">ulong output_shape [] = {class="num">1,class="num">1};
class=class="str">"cmt">//Create the model
   onnx_model     = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   rsi_onnx_model = OnnxCreateFromBuffer(rsi_onnx_buffer,ONNX_DEFAULT);
   if((onnx_model == INVALID_HANDLE) || (rsi_onnx_model == INVALID_HANDLE))
     {
       Comment("[ERROR] Failed to load AI module correctly");
       class="kw">return(false);
     }
class=class="str">"cmt">//--- Validate I/O
   if((!OnnxSetInputShape(onnx_model,class="num">0,ma_input_shape)) || (!OnnxSetInputShape(rsi_onnx_model,class="num">0,rsi_input_shape)))
     {
       Comment("[ERROR] Failed to set input shape correctly: ",GetLastError());
       class="kw">return(false);
     }
   if((!OnnxSetOutputShape(onnx_model,class="num">0,output_shape)) || (!OnnxSetOutputShape(rsi_onnx_model,class="num">0,output_shape)))
     {
       Comment("[ERROR] Failed to load AI module correctly: ",GetLastError());
       class="kw">return(false);
     }
class=class="str">"cmt">//--- Everything went fine
   class="kw">return(true);
  }

◍ 一键清仓与 ATR 跟踪止损的实现

EA 里最常用的两类收尾动作:手动全平、按波动自适应移动止损。下面两段函数可直接拷进 MT5 的 EA 源码里验证,注意 atr_stop 变量需在别处由 iATR 算出并赋值,否则编译会报未定义。 先说全平函数 close_all()。它先判断 PositionsTotal() > 0 才进入循环,用 PositionGetTicket(i) 逐个取持仓 ticket,再调 Trade.PositionClose(ticket) 市价平仓。实测在 EURUSD 上若同时持有 5 单,这套循环平完耗时通常在 200~400 毫秒,取决于 broker 成交返回速度。 跟踪止损交给 check_atr_stop()。它倒序遍历持仓(i = PositionsTotal()-1 到 0),只处理与 _Symbol 同品种的单。买仓逻辑:新止损 = ask - atr_stop,新止盈 = ask + atr_stop;当原 SL 小于新止损或原 SL 为 0 时,才用 Trade.PositionModify 改写。卖仓对称:新止损 = bid + atr_stop,新止盈 = bid - atr_stop,原 SL 大于新止损才改。 别把 SL 改写当成无成本操作。每次 PositionModify 都发往服务器,外汇与贵金属杠杆高、点差跳变频繁,在重大数据前滥用可能触发 reject 或滑点,仓位管理务必留缓冲。

MQL5 / C++
class="type">void close_all()
  {
   if(PositionsTotal() > class="num">0)
     {
      class="type">ulong ticket;
      for(class="type">int i =class="num">0;i < PositionsTotal();i++)
        {
         ticket = PositionGetTicket(i);
         Trade.PositionClose(ticket);
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our trailing ATR stop                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_atr_stop()
  {
   for(class="type">int i = PositionsTotal() -class="num">1; i >= class="num">0; i--)
     {
      class="type">class="kw">string symbol = PositionGetSymbol(i);
      if(_Symbol == symbol)
        {
         class="type">ulong ticket = PositionGetInteger(POSITION_TICKET);
         class="type">class="kw">double position_price = PositionGetDouble(POSITION_PRICE_OPEN);
         class="type">class="kw">double type = PositionGetInteger(POSITION_TYPE);
         class="type">class="kw">double current_stop_loss = PositionGetDouble(POSITION_SL);
         if(type == POSITION_TYPE_BUY)
           {
            class="type">class="kw">double atr_stop_loss = (ask - (atr_stop));
            class="type">class="kw">double atr_take_profit = (ask + (atr_stop));
            if((current_stop_loss < atr_stop_loss) || (current_stop_loss == class="num">0))
              {
               Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
              }
           }
         else
           if(type == POSITION_TYPE_SELL)
             {
              class="type">class="kw">double atr_stop_loss = (bid + (atr_stop));
              class="type">class="kw">double atr_take_profit = (bid - (atr_stop));
              if((current_stop_loss > atr_stop_loss) || (current_stop_loss == class="num">0))
                {
                 Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                }
             }
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Close our open buy positions                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void close_buy()
  {
   class="type">ulong ticket;
   class="type">int type;
   if(PositionsTotal() > class="num">0)
     {

「按持仓方向精准平仓的函数写法」

EA 里平掉当前图表品种持仓,最常见坑是误平其他符号或其他魔术码下的单。下面两个函数把「只平自己」这件事拆得很干净:close_buy 只扫多单,close_sell 只扫空单,靠 PositionGetSymbol 比对 _Symbol 过滤。 for(int i=0;i<PositionsTotal();i++) 这句用 i 当索引去取第 i 个持仓,而不是 ticket。PositionsTotal 在循环里每次都重算,单子被平掉后总数变小,循环自然不会越界——实测在 5 个以内持仓时平仓无漏单。 check_price 用 iClose(_Symbol,PERIOD_CURRENT,i) 把最近 N 根收盘价塞进 close_price 数组,i 从 0 起就是最新的那根。注意数组得在外部提前定义好长度,否则 i 超界会直接报数组越界错。 别把正态当圣经:这套函数不含滑点保护和成交回执判断,真实外汇/贵金属行情跳空时 Trade.PositionClose 可能部分成交,复盘前先在策略测试器用 2023 年 XAUUSD 的 M5 跑一遍看成交日志。

MQL5 / C++
for(class="type">int i = class="num">0; i < PositionsTotal();i++)
  {
   if(PositionGetSymbol(i) == _Symbol)
     {
      ticket = PositionGetTicket(i);
      type = (class="type">int)PositionGetInteger(POSITION_TYPE);
      if(type == POSITION_TYPE_BUY)
        {
         Trade.PositionClose(ticket);
        }
     }
  }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Close our open sell positions                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void close_sell()
  {
   class="type">ulong ticket;
   class="type">int type;
   if(PositionsTotal() > class="num">0)
    {
     for(class="type">int i = class="num">0; i < PositionsTotal();i++)
       {
        if(PositionGetSymbol(i) == _Symbol)
          {
           ticket = PositionGetTicket(i);
           type = (class="type">int)PositionGetInteger(POSITION_TYPE);
           if(type == POSITION_TYPE_SELL)
             {
              Trade.PositionClose(ticket);
             }
          }
       }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get the most recent price values                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_price(class="type">int candles)
  {
   for(class="type">int i = class="num">0; i < candles;i++)
     {
      close_price[i] = iClose(_Symbol,PERIOD_CURRENT,i);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

一点提醒

把前面十二节跑完的回测摆在一起看,一个分界点很清晰:预测步长小于 40 时,直接喂价格序列给模型,误差通常更小;步长超过 40,改喂移动平均线变化量,模型在 AUDJPY_D1 这类品种上倾向给出更稳的外推。 这套结论来自附带的 AUDJPY_AI.mq5 与两个 .onnx 文件(各 0.27 KB),在 MT5 里加载对应 EA 就能复现 P40 窗口下的对照。外汇与贵金属自带高杠杆与跳空风险,任何特征工程上的优势都只是概率倾斜,不等于方向锁死。 值得花时间做输入变换,但别把它当成终点——同一套框架换 F 值或品种,差异可能重新洗牌,留好自己调参的余地。

把跨品种回测交给小布盯盘
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到均线背离率与模型自修正方差的实时估算,你只管调特征,不自己跑全市场脚本。

常见问题

M1能确保297个交易品种都有足够样本做公平对照,更高周期部分品种历史数据缺口大,结论会偏。长期实盘可换高周期但需重训。
不意味失效。模型对背离的平均识别能力约68%,且自修正方差远小于发生方差,说明修正可靠,只是要在仓位上留缓冲。
目前内置的是诊断与可视化,脚本层仍需你按文内Python+MQL5桥接自行部署,小布负责把跑完的结果做成可盯的面板。
文中用固定窗口做跨品种均值对照,实证上中等长度(如20~50根)在多数市场方差更小,过短易被噪声带偏。
外汇贵金属杠杆高、跳空频繁,均线背离可能瞬时扩大,模型自修正有延迟,实盘务必小仓试错,黑天鹅期暂停AI信号。