在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓·综合运用
🛡️

在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓·综合运用

(3/3)· 均值回归策略下,为什么「放宽止损」反而让你更快亏光,以及 MQL5 里怎么落地硬规则

案例拆解新手友好 第 3/3 篇

很多交易者明明看对了方向,却倒在回调扫损上,于是本能地把止损拉宽。这种「放宽止损」的习惯没有统一规则兜底,只会让每笔交易的风险敞口漂移。本篇用 EURUSD 小时图上的真实扫损场景,拆一套固定止损的均值回归解法。

模型信号与高低点突破的联立过滤

这段逻辑把 AI 模型预测和价格行为过滤绑在一起:先确认模型能出预测,否则直接在图表上提示并退出,不浪费后续计算。 若次级周期(TF_2)连续两根 K 线的最高价都低于昨日低点,且最近一根收盘站在均线 ma[0] 上方,才触发 check_buy() 做多条件评估;对称地,连续两根最低价高于昨日高点且收盘在均线下方,才进入 check_sell()。 check_buy 内部还要求模型预测值同时高于均线和当前现价,并且预测位在 bid - SL_SIZE 之上,才用 Trade.Buy 以 ask 进场、bid-SL_SIZE 挂损、bid+SL_SIZE 挂盈。check_sell 镜像处理。外汇与贵金属杠杆高,这种联立条件也只是降低噪音,信号失效概率依旧存在。 update_sl 函数负责持仓后移动止损:先用 PositionSelect 锁定当前符号持仓,读出 current_sl、current_tp、open_price,flag==0 表示仅在可行时前推止损与止盈。开 MT5 把这段挂到 EA 里,改 TF_2 和 SL_SIZE 就能看过滤松紧对信号数的影响。

MQL5 / C++
if(!model_predict())
  {
   Comment("Failed to get a forecast from our model");
   class="kw">return;
  }
 if((iHigh(Symbol(),TF_2,class="num">1) < yesterday_low) && (iHigh(Symbol(),TF_2,class="num">2) < yesterday_low))
  {
   if(iClose(Symbol(),TF_2,class="num">1) > ma[class="num">0])
    {
     check_buy();
    }
  }
 if((iLow(Symbol(),TF_2,class="num">1) > yesterday_high) && (iLow(Symbol(),TF_2,class="num">2) > yesterday_high))
  {
   if(iClose(Symbol(),TF_2,class="num">1) < ma[class="num">0])
    {
     check_sell();
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have a valid buy setup                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_buy(class="type">void)
  {
  if((model_forecast[class="num">0] > ma[class="num">0]) && (model_forecast[class="num">0] > iClose(Symbol(),TF_2,class="num">0)))
   {
    if(model_forecast[class="num">0] > (bid - (SL_SIZE)))
     Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE)));
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have a valid sell setup                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_sell(class="type">void)
  {
  if((model_forecast[class="num">0] < ma[class="num">0]) && (model_forecast[class="num">0] < iClose(Symbol(),TF_2,class="num">0)))
   {
    if(model_forecast[class="num">0] < (ask + (SL_SIZE)))
     Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE)));
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our stop loss                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update_sl(class="type">int flag)
  {
  class=class="str">"cmt">//--- First find our open position
  if(PositionSelect(Symbol()))
   {
    class="type">class="kw">double current_sl = PositionGetDouble(POSITION_SL);
    class="type">class="kw">double current_tp = PositionGetDouble(POSITION_TP);
    class="type">class="kw">double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
    class=class="str">"cmt">//--- Flag class="num">0 means we just want to push the stop loss and take profit forward if its possible
    if(flag == class="num">0)
     {

「用模型预测值去挪止损的两种分支」

这段逻辑跑在 flag==1 的情境下:先调 model_predict() 拿到 model_forecast[0],再按持仓方向决定怎么改止损。外汇与贵金属杠杆高,模型只是概率工具,改损动作不代表方向判断必胜。 多头仓(current_tp > current_sl)里,若预测值低于当前止损,且同时高于均线 ma[0] 与昨低 yesterday_low,就把止损提到预测值,保留原止盈。若预测值还低于开仓价 open_price,则止损直接设为预测值的 1.5 倍——相当于往亏损方向再让出半段空间,倾向用于过滤假突破。 空头仓反过来:预测值高于当前止损、又低于 ma[0] 和昨高时,止损下移到预测值;若预测值高于开仓价,止损取预测值的 0.5 倍,把风险收紧一半。 普通追踪(flag!=1)那段更粗暴:买仓里只要 (bid - SL_SIZE) 比 current_sl 大就平移止损到 bid 上下各挂 SL_SIZE;卖仓对称用 ask。两类写法可以并存在同一个 EA 里,靠 flag 切换。

MQL5 / C++
   class=class="str">"cmt">//--- Buy Setup
   if(current_tp > current_sl)
     {
       if((bid - SL_SIZE) > current_sl)
         Trade.PositionModify(Symbol(),(bid - SL_SIZE),(bid + SL_SIZE));
     }
   class=class="str">"cmt">//--- Sell setup
   if(current_tp < current_sl)
     {
       if((ask + SL_SIZE) < current_sl)
         Trade.PositionModify(Symbol(),(ask + SL_SIZE),(ask - SL_SIZE));
     }
   }
   class=class="str">"cmt">//--- Flag class="num">1 means we want to check if the stop loss may be hit soon, and act accordingly
   if(flag == class="num">1)
     {
     model_predict();
     class=class="str">"cmt">//--- Buy setup
     if(current_tp > current_sl)
       {
        
       if(model_forecast[class="num">0] < current_sl)
         {
          if((model_forecast[class="num">0] > ma[class="num">0]) && (model_forecast[class="num">0] > yesterday_low))
             Trade.PositionModify(Symbol(),model_forecast[class="num">0],current_tp);
         }
       if(model_forecast[class="num">0] < open_price)
          Trade.PositionModify(Symbol(),model_forecast[class="num">0] * class="num">1.5,current_tp);
       }
     class=class="str">"cmt">//--- Sell setup
     if(current_tp < current_sl)
       {
       if(model_forecast[class="num">0] > current_sl)
         {
          if((model_forecast[class="num">0] < ma[class="num">0]) && (model_forecast[class="num">0] < yesterday_high))
             Trade.PositionModify(Symbol(),model_forecast[class="num">0],current_tp);
         }
       if(model_forecast[class="num">0] > open_price)
          Trade.PositionModify(Symbol(),model_forecast[class="num">0] * class="num">0.5,current_tp);
       }
     }
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Perform our update routines                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update()
  {
class=class="str">"cmt">//--- Daily procedures
  {
    class="kw">static class="type">class="kw">datetime time_stamp;
    class="type">class="kw">datetime current_time = iTime(Symbol(),TF_1,class="num">0);

◍ 多周期钩子里怎么喂数据给模型

这段逻辑把日线前高前低、M30 的实时报价与均线、以及逐 tick 的止损更新拆进了不同作用域,核心是用时间截比对避免每 tick 重算。日线块里只在 time_stamp 变化时取 iHigh/iLow(Symbol(),TF_1,1) 的前一根高低,并删旧建新两条 OBJ_HLINE,等于把「昨天区间」钉在图上供价格行为观察。 M30 块用 static datetime 存上次 K 线时间,iTime(Symbol(),TF_2,0) 变了才刷新 bid/ask、CopyBuffer 取均线最新值;空仓调 find_setup() 找形态,有仓调 update_sl(1) 推止损。这里若 TF_2 设为 PERIOD_M30,回测里每根 30 分钟 K 线只进一次,能压住计算开销。 逐 tick 块只放 update_sl(0),原文特意提醒此作用域的函数调用会拖慢回测速度——外汇与贵金属点差跳变频繁,每 tick 跑重逻辑可能让 1 年回测从几分钟变几十分钟,概率上影响参数寻优效率。 model_predict() 把 M30 前一根 OHLC 加一根均线值塞进 vectorf 送 ONNX 推理,输入维度靠 ONNX_MODEL_INPUTS 约束;CopyBuffer(ma_handler,0,1,1,ma_input) 取的是偏移 1 的均线值,也就是「已完成那根」而非当前未闭合值,避免未来函数泄漏。

MQL5 / C++
if(time_stamp != current_time)
  {
  yesterday_high = iHigh(Symbol(),TF_1,class="num">1);
  yesterday_low = iLow(Symbol(),TF_1,class="num">1);
  class=class="str">"cmt">//--- Mark yesterday&class="macro">#x27;s levels
  ObjectDelete(class="num">0,last_high);
  ObjectDelete(class="num">0,last_low);
  ObjectCreate(class="num">0,last_high,OBJ_HLINE,class="num">0,class="num">0,yesterday_high);
  ObjectCreate(class="num">0,last_low,OBJ_HLINE,class="num">0,class="num">0,yesterday_low);
  }
  }
class=class="str">"cmt">//--- M30 procedures
  {
  class="kw">static class="type">class="kw">datetime time_stamp;
  class="type">class="kw">datetime current_time = iTime(Symbol(),TF_2,class="num">0);
  if(time_stamp != current_time)
    {
    time_stamp = current_time;
    class=class="str">"cmt">//--- Get updated prices
    bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
    ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
    class=class="str">"cmt">//--- Update our technical indicators
    CopyBuffer(ma_handler,class="num">0,class="num">0,class="num">1,ma);
    class=class="str">"cmt">//--- Check for a setup
    if(PositionsTotal()==class="num">0)
      find_setup();
    class=class="str">"cmt">//--- Check for a setup
    if(PositionsTotal() > class="num">0)
      update_sl(class="num">1);
    }
  }
class=class="str">"cmt">//--- Per tick procedures
  {
  class=class="str">"cmt">//--- These function calls can become expensive and may slow down the speed of your back tests
  class=class="str">"cmt">//--- Be thoughtful when placing any function calls in this scope
  update_sl(class="num">0);
  }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get a forecast from our deep neural network                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool model_predict(class="type">void)
  {
  class="type">class="kw">double ma_input[] = {class="num">0};
  CopyBuffer(ma_handler,class="num">0,class="num">1,class="num">1,ma_input);
  vectorf model_inputs =
    {
      (class="type">float) iOpen(Symbol(),TF_2,class="num">1),
      (class="type">float) iHigh(Symbol(),TF_2,class="num">1),
      (class="type">float) iLow(Symbol(),TF_2,class="num">1),
      (class="type">float) iClose(Symbol(),TF_2,class="num">1),
      (class="type">float) ma_input[class="num">0]
    };
  for(class="type">int i = class="num">0; i < ONNX_MODEL_INPUTS;i++)

标准化输入与 ONNX 推理的收尾动作

把特征喂给模型前,必须先做 Z-score 标准化:每个维度减去对应均值再除以标准差,转成 float 送入 model_inputs。跳过这步,ONNX 模型大概率输出漂移严重的预测值。 推理调用 OnnxRun 走 ONNX_DEFAULT 配置,失败就用 Comment 把 GetLastError 抛到图表,并直接 return(false) 阻断后续逻辑。成功则在图表打印 Expected MA Value,取 model_forecast[0] 作为均线预期。 跑完记得在 release 里 OnnxRelease 释放模型、IndicatorRelease 释放 MA 句柄,否则 MT5 反复加载 EA 会漏内存。外汇与贵金属行情受杠杆与跳空影响,模型输出只是概率倾向,实盘前务必在策略测试器跑至少 3 个月 tick 数据验证。

MQL5 / C++
  {
      model_inputs[i] = (class="type">float)((model_inputs[i] - mean_values[i]) / std_values[i]);
  }
  if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast))
  {
      Comment("Failed to obtain forecast: ",GetLastError());
      class="kw">return(false);
  }
  Comment(StringFormat("Expected MA Value: %f",model_forecast[class="num">0]));
  class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Free resources we are no longer class="kw">using up                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void release(class="type">void)
  {
   OnnxRelease(onnx_model);
   IndicatorRelease(ma_handler);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「别急着下结论」

把盈利单被止损扫掉这件事彻底消灭,在外汇和贵金属这种高杠杆市场里基本是幻想。每套量化框架补上一个漏洞,往往顺手开出另一个口子,EURUSD 这类品种尤其吃微观流动性,模型外推要谨慎。 附件里 Baseline_Model.mq5 仅 5.65 KB,而 Stop_Out_Prevention_Model.mq5 到了 12 KB,体量差背后是深度神经网络接管的止损逻辑;想验证就开 MT5 把两个 EA 在同一周期回测对照,看被扫损频次的差值是否值得那 6 KB 多的复杂度。 真要落地,先跑 Fetch_Data_MA.mq5 把行情拉进 EURUSD_Stop_Out_Moving_Average_Model.ipynb 做一遍分布检查,再决定 .onnx 那个 1.84 KB 的网络能不能上实盘。过滤掉无意义磨损单是概率层面的优化,不是账户保本的开关。

把回测诊断交给小布
这些 EURUSD 上的扫损诊断与固定止损回测,小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史时段里的被止损出局频率,你只管定规则。

常见问题

确认往往只是推迟了价格触碰止损的时间,并没有消除波动扫损的结构性问题,均值回归前的大幅反向刺穿依旧可能发生。
用 iHigh/iLow 取前日高低,配合 iMA 判断收盘价站上均线后再反向开仓,具体代码由技术小节给出,这里只说逻辑。
框架可迁移,但贵金属跳空更频繁,固定止损间距需按品种波动重新标定,外汇贵金属均属高风险,参数不能直接套用。
可以,品种页的 AIGC 诊断会按你选定的周期统计扫损分布,帮你看清哪些交易日容易盈利单被洗。
固定止损控制的是单笔风险习惯,极端行情概率上仍可能连续触损,但它避免了随意放宽带来的敞口失控。