在MQL5中开发马丁格尔(Martingale)区域恢复策略·进阶篇
🧮

在MQL5中开发马丁格尔(Martingale)区域恢复策略·进阶篇

(2/3)·从策略定义到代码实现,手把手把区域恢复 EA 跑进 MT5 回测里看真实表现

偏理论 第 2/3 篇
很多交易者把区域恢复当成「无限加仓必解套」的保险,却忽略了保证金螺旋和单边行情下的爆仓概率。本篇把这套逻辑拆开,先看它到底在赌什么、再谈怎么用 MQL5 写进去。

RSI 上穿超卖后画 zone 并开仓

当 RSI 前一根柱低于超卖线、当前柱回升突破超卖线时,代码判定为空头动能衰减、倾向反手做多。此时用 obj_trade.Buy(0.01) 以 0.01 手开立市价买单,外汇与贵金属品种波动剧烈,0.01 手仅作验证用途,实盘须自担高风险。 订单回执 ticket 大于 0 说明下单指令已提交,再用 PositionSelectByTicket 锁定该持仓,读取开仓价 openPrice 作为 zone 基准。zoneHigh 取归一化开仓价,zoneLow = zoneHigh - zoneRange,上下目标则各加减 zoneTarget,这四个价位构成后续观察区间。 drawZoneLevel 负责把价位画成水平线:ObjectCreate 建 OBJ_HLINE,ObjectSetInteger 设颜色与线宽。绿线 zoneHigh、红线 zoneLow 宽 2,蓝线双向目标宽 3,肉眼可立刻看到突破与回撤边界。 最后把 lastDirection 置 1、recovery_lot 翻倍为 0.02,isBuyDone 开 true、isSellDone 关 false。开 MT5 把这段贴进 EA,调 overSoldLevel 与 zoneRange 就能看 zone 是否贴合你的周期。

MQL5 / C++
if (!CopyBuffer(rsi_handle,class="num">0,class="num">1,class="num">2,rsiData)) class="kw">return;
if (rsiData[class="num">1] < overSoldLevel && rsiData[class="num">0] > overSoldLevel){
      obj_trade.Buy(class="num">0.01);
      class="type">ulong pos_ticket = obj_trade.ResultOrder();
      if (pos_ticket > class="num">0){
         if (PositionSelectByTicket(pos_ticket)){
            class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
            zoneHigh = NormalizeDouble(openPrice,_Digits);
            zoneLow = NormalizeDouble(zoneHigh - zoneRange,_Digits);
            zoneTargetHigh = NormalizeDouble(zoneHigh + zoneTarget,_Digits);
            zoneTargetLow = NormalizeDouble(zoneLow - zoneTarget,_Digits);
            drawZoneLevel(ZONE_H,zoneHigh,clrGreen,class="num">2);
            drawZoneLevel(ZONE_L,zoneLow,clrRed,class="num">2);
            drawZoneLevel(ZONE_T_H,zoneTargetHigh,clrBlue,class="num">3);
            drawZoneLevel(ZONE_T_L,zoneTargetLow,clrBlue,class="num">3);
class="type">void drawZoneLevel(class="type">class="kw">string levelName, class="type">class="kw">double price, class="type">color clr, class="type">int width) {
   ObjectCreate(class="num">0, levelName, OBJ_HLINE, class="num">0, TimeCurrent(), price); class=class="str">"cmt">// Create a horizontal line object
   ObjectSetInteger(class="num">0, levelName, OBJPROP_COLOR, clr); class=class="str">"cmt">// Set the line class="type">color
   ObjectSetInteger(class="num">0, levelName, OBJPROP_WIDTH, width); class=class="str">"cmt">// Set the line width
}
            lastDirection = class="num">1;
            recovery_lot = class="num">0.01*class="num">2;
            isBuyDone = true; isSellDone = false;

◍ RSI 拐点触发双向建仓与区域绘制

当 RSI 前一根柱大于超买线、当前柱跌破超买线时,脚本以 0.01 手卖出,并用持仓开盘价锚定 zoneLow,再向上加 zoneRange 得 zoneHigh、向两侧加 zoneTarget 得目标区。 买侧逻辑镜像对称:RSI 上穿超卖后开 0.01 手多单,同样以开盘价定 zoneLow/zoneHigh 及蓝线目标区,并调用 drawZoneLevel 用绿/红/蓝三色画线,线宽分别为 2、2、3。 开仓成功后 lastDirection 被置为 1 或 -1,recovery_lot 初值固定为 0.01*2=0.02,isBuyDone/isSellDone 互斥标记。外汇与贵金属杠杆高,这类固定手数恢复逻辑在连错时可能快速放大敞口,上 MT5 用策略测试器跑 EURUSD 15M 能直接看回撤曲线。

MQL5 / C++
drawZoneLevel(ZONE_H, zoneHigh, clrGreen, class="num">2); class=class="str">"cmt">// Draw the high zone line
drawZoneLevel(ZONE_L, zoneLow, clrRed, class="num">2); class=class="str">"cmt">// Draw the low zone line
drawZoneLevel(ZONE_T_H, zoneTargetHigh, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target high zone line
drawZoneLevel(ZONE_T_L, zoneTargetLow, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target low zone line

lastDirection = class="num">1;      class=class="str">"cmt">// Set the last direction to buy
recovery_lot = class="num">0.01 * class="num">2; class=class="str">"cmt">// Set the initial recovery lot size
isBuyDone = true;       class=class="str">"cmt">// Mark buy trade as done
isSellDone = false;     class=class="str">"cmt">// Reset sell trade flag

else if (rsiData[class="num">1] > overBoughtLevel && rsiData[class="num">0] < overBoughtLevel) {
   obj_trade.Sell(class="num">0.01); class=class="str">"cmt">// Open a sell trade with class="num">0.01 lots
   class="type">ulong pos_ticket = obj_trade.ResultOrder();
   if (pos_ticket > class="num">0) {
      if (PositionSelectByTicket(pos_ticket)) {
         class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
         zoneLow = NormalizeDouble(openPrice, _Digits); class=class="str">"cmt">// Set the low zone price
         zoneHigh = NormalizeDouble(zoneLow + zoneRange, _Digits); class=class="str">"cmt">// Set the high zone price
         zoneTargetHigh = NormalizeDouble(zoneHigh + zoneTarget, _Digits); class=class="str">"cmt">// Set the target high zone price
         zoneTargetLow = NormalizeDouble(zoneLow - zoneTarget, _Digits); class=class="str">"cmt">// Set the target low zone price
         drawZoneLevel(ZONE_H, zoneHigh, clrGreen, class="num">2); class=class="str">"cmt">// Draw the high zone line
         drawZoneLevel(ZONE_L, zoneLow, clrRed, class="num">2); class=class="str">"cmt">// Draw the low zone line
         drawZoneLevel(ZONE_T_H, zoneTargetHigh, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target high zone line
         drawZoneLevel(ZONE_T_L, zoneTargetLow, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target low zone line

         lastDirection = -class="num">1;      class=class="str">"cmt">// Set the last direction to sell
         recovery_lot = class="num">0.01 * class="num">2; class=class="str">"cmt">// Set the initial recovery lot size
         isBuyDone = false;       class=class="str">"cmt">// Reset buy trade flag
         isSellDone = true;       class=class="str">"cmt">// Mark sell trade as done
      }
   }
}
class="type">void OnTick()
{

「RSI 穿越阈值后的开仓与区域画线」

这段逻辑跑在 EA 的 OnTick 里,先拿 iBars 判断 K 线总数是否变化,没变就直接 return,避免同一根 bar 重复触发。若已有持仓(PositionsTotal()>0)也退出,保证单笔单向先行。 CopyBuffer 从 RSI 句柄取最近两根缓冲值到 rsiData 数组,索引 1 是上一根、0 是当前根。若上一根低于超卖线且当前根上穿超卖线,视为反转倾向,调用 obj_trade.Buy(0.01) 以 0.01 手试多。 成交后用 PositionSelectByTicket 抓到开仓价,按 zoneRange 向下算 zoneLow,再按 zoneTarget 向上下扩展出目标区。drawZoneLevel 用绿/红/蓝线把这四个价位画在图上,蓝线线宽 3、其余 2。lastDirection 置 1,recovery_lot 设为 0.02(初始手数翻倍),isBuyDone 锁真。 对称的,上一根高于超买线、当前根下穿则 obj_trade.Sell(0.01) 试空,同样记录价位区域并重置买卖标记。外汇与贵金属波动剧烈,这种固定 0.01 手试单在极端跳空下可能迅速扩大浮亏,务必在 MT5 策略测试器用真实点差回测。

MQL5 / C++
  class="type">int bars = iBars(_Symbol,PERIOD_CURRENT);
  if (totalBars == bars) class="kw">return;
  totalBars = bars;

  if (PositionsTotal() > class="num">0) class="kw">return;

  if (!CopyBuffer(rsi_handle,class="num">0,class="num">1,class="num">2,rsiData)) class="kw">return;

  class=class="str">"cmt">//--- Check for oversold condition and open a buy position
  if (rsiData[class="num">1] < overSoldLevel && rsiData[class="num">0] > overSoldLevel) {
      obj_trade.Buy(class="num">0.01); class=class="str">"cmt">// Open a buy trade with class="num">0.01 lots
      class="type">ulong pos_ticket = obj_trade.ResultOrder();
      if (pos_ticket > class="num">0) {
        if (PositionSelectByTicket(pos_ticket)) {
          class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
          zoneHigh = NormalizeDouble(openPrice, _Digits); class=class="str">"cmt">// Set the high zone price
          zoneLow = NormalizeDouble(zoneHigh - zoneRange, _Digits); class=class="str">"cmt">// Set the low zone price
          zoneTargetHigh = NormalizeDouble(zoneHigh + zoneTarget, _Digits); class=class="str">"cmt">// Set the target high zone price
          zoneTargetLow = NormalizeDouble(zoneLow - zoneTarget, _Digits); class=class="str">"cmt">// Set the target low zone price
          drawZoneLevel(ZONE_H, zoneHigh, clrGreen, class="num">2); class=class="str">"cmt">// Draw the high zone line
          drawZoneLevel(ZONE_L, zoneLow, clrRed, class="num">2); class=class="str">"cmt">// Draw the low zone line
          drawZoneLevel(ZONE_T_H, zoneTargetHigh, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target high zone line
          drawZoneLevel(ZONE_T_L, zoneTargetLow, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target low zone line
          
          lastDirection = class="num">1;      class=class="str">"cmt">// Set the last direction to buy
          recovery_lot = class="num">0.01 * class="num">2; class=class="str">"cmt">// Set the initial recovery lot size
          isBuyDone = true;       class=class="str">"cmt">// Mark buy trade as done
          isSellDone = false;     class=class="str">"cmt">// Reset sell trade flag
        }
      }
  }
  class=class="str">"cmt">//--- Check for overbought condition and open a sell position
  else if (rsiData[class="num">1] > overBoughtLevel && rsiData[class="num">0] < overBoughtLevel) {
      obj_trade.Sell(class="num">0.01); class=class="str">"cmt">// Open a sell trade with class="num">0.01 lots
      class="type">ulong pos_ticket = obj_trade.ResultOrder();
      if (pos_ticket > class="num">0) {
        if (PositionSelectByTicket(pos_ticket)) {
          class="type">class="kw">double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);

区间边界与平仓触发的代码实现

这段逻辑负责把计算好的价格区间落到图表上,并设定后续平仓的判定条件。zoneLow 以开盘价归一化到当前品种小数位,zoneHigh 在其上加 zoneRange,上下目标位则分别加减 zoneTarget,四个边界都用 NormalizeDouble 对齐 _Digits,避免 MT5 浮点显示错位。 drawZoneLevel 用不同颜色和线宽区分边界:红绿实线标 zoneLow/zoneHigh(线宽2),蓝色线宽3标 zoneTargetLow/zoneTargetHigh,视觉上把「触发带」和「缓冲带」分开。方向状态被置为 -1(卖),recovery_lot 初始化为 0.02(0.01*2),同时翻转 isBuyDone/isSellDone 标志。 平仓判定很直接:当 bid 向上击穿 zoneTargetHigh 或向下击穿 zoneTargetLow 时,PositionClose 平掉当前符号仓位并 deleteZoneLevels 清图。deleteZoneLevels 用 ObjectDelete(0,...) 逐个删掉四个对象名,防止残留水平线干扰下一轮。 批量平仓则用 for 循环从 PositionsTotal()-1 向下遍历,PositionGetTicket 取ticket后 PositionSelectByTicket 选中再关闭,最后把四个 zone 变量归零。外汇与贵金属杠杆高,这类击穿平仓若遇滑点可能不在目标价成交,实盘前建议在 MT5 策略测试器用历史数据验证触发频率。

MQL5 / C++
zoneLow = NormalizeDouble(openPrice, _Digits); class=class="str">"cmt">// Set the low zone price
zoneHigh = NormalizeDouble(zoneLow + zoneRange, _Digits); class=class="str">"cmt">// Set the high zone price
zoneTargetHigh = NormalizeDouble(zoneHigh + zoneTarget, _Digits); class=class="str">"cmt">// Set the target high zone price
zoneTargetLow = NormalizeDouble(zoneLow - zoneTarget, _Digits); class=class="str">"cmt">// Set the target low zone price
drawZoneLevel(ZONE_H, zoneHigh, clrGreen, class="num">2); class=class="str">"cmt">// Draw the high zone line
drawZoneLevel(ZONE_L, zoneLow, clrRed, class="num">2); class=class="str">"cmt">// Draw the low zone line
drawZoneLevel(ZONE_T_H, zoneTargetHigh, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target high zone line
drawZoneLevel(ZONE_T_L, zoneTargetLow, clrBlue, class="num">3); class=class="str">"cmt">// Draw the target low zone line

lastDirection = -class="num">1;      class=class="str">"cmt">// Set the last direction to sell
recovery_lot = class="num">0.01 * class="num">2; class=class="str">"cmt">// Set the initial recovery lot size
isBuyDone = false;       class=class="str">"cmt">// Reset buy trade flag
isSellDone = true;       class=class="str">"cmt">// Mark sell trade as done

if (zoneTargetHigh > class="num">0 && zoneTargetLow > class="num">0){
   if (bid > zoneTargetHigh || bid < zoneTargetLow){
       obj_trade.PositionClose(_Symbol);
       deleteZoneLevels();
       ...
   }
}
class="type">void deleteZoneLevels(){
   ObjectDelete(class="num">0,ZONE_H);
   ObjectDelete(class="num">0,ZONE_L);
   ObjectDelete(class="num">0,ZONE_T_H);
   ObjectDelete(class="num">0,ZONE_T_L);
}

for (class="type">int i = PositionsTotal()-class="num">1; i >= class="num">0; i--){
   class="type">ulong ticket = PositionGetTicket(i);
   if (ticket > class="num">0){
      if (PositionSelectByTicket(ticket)){
         obj_trade.PositionClose(ticket);
      }
   }
}
class=class="str">"cmt">//closed all, reset all
zoneHigh=class="num">0;zoneLow=class="num">0;zoneTargetHigh=class="num">0;zoneTargetLow=class="num">0;

◍ 价格越界即清仓与反向加倍逻辑

当 bid 报价突破预设的上下目标区(zoneTargetHigh / zoneTargetLow 均大于 0),系统会先平掉当前符号持仓,再循环遍历所有持仓凭 ticket 逐一平仓,并清空图上绘制的区间线。这一步把方向变量 lastDirection 和 recovery_lot 都归零,等于把网格状态机复位到初始。 清仓之后,若 bid 高于 zoneHigh 且此前未做多、或上次方向为卖(lastDirection < 0),则调用 obj_trade.Buy 以 lots_Rec 手数开多,并把 lastDirection 置 1、recovery_lot 乘以 2、isBuyDone 置真、isSellDone 置假。反之 bid 跌破 zoneLow 时镜像处理开空,recovery_lot 同样翻倍。 注意 recovery_lot 每次触发都 ×2,这意味着连续反向时仓位呈几何增长,外汇与贵金属杠杆环境下回撤可能急剧放大,属高风险机制。开 MT5 把这段挂到 EA 里,改 zoneTargetHigh / Low 的赋值,就能直观看到清仓与加倍频率随区间宽度变化。

MQL5 / C++
   lastDirection=class="num">0;
   recovery_lot = class="num">0;
   class=class="str">"cmt">//--- Close all positions if the bid price is outside target zones
   if (zoneTargetHigh > class="num">0 && zoneTargetLow > class="num">0) {
      if (bid > zoneTargetHigh || bid < zoneTargetLow) {
         obj_trade.PositionClose(_Symbol); class=class="str">"cmt">// Close the current position
         deleteZoneLevels();               class=class="str">"cmt">// Delete all drawn zone levels
         for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) {
            class="type">ulong ticket = PositionGetTicket(i);
            if (ticket > class="num">0) {
               if (PositionSelectByTicket(ticket)) {
                  obj_trade.PositionClose(ticket); class=class="str">"cmt">// Close positions by ticket
               }
            }
         }
         class=class="str">"cmt">//--- Reset all zone and direction variables
         zoneHigh = class="num">0;
         zoneLow = class="num">0;
         zoneTargetHigh = class="num">0;
         zoneTargetLow = class="num">0;
         lastDirection = class="num">0;
         recovery_lot = class="num">0;
      }
   }
   if (zoneHigh > class="num">0 && zoneLow > class="num">0){  
      class="type">class="kw">double lots_Rec = class="num">0;
      lots_Rec = NormalizeDouble(recovery_lot,class="num">2);
      ...
   }
   if (bid > zoneHigh) {
         if (isBuyDone == false || lastDirection < class="num">0) {
            obj_trade.Buy(lots_Rec); class=class="str">"cmt">// Open a buy trade
            
            lastDirection = class="num">1;       class=class="str">"cmt">// Set the last direction to buy
            recovery_lot = recovery_lot * class="num">2; class=class="str">"cmt">// Double the recovery lot size
            isBuyDone = true;       class=class="str">"cmt">// Mark buy trade as done
            isSellDone = false;     class=class="str">"cmt">// Reset sell trade flag
         }
      }
else if (bid < zoneLow) {
         if (isSellDone == false || lastDirection > class="num">0) {
            obj_trade.Sell(lots_Rec); class=class="str">"cmt">// Open a sell trade
            
            lastDirection = -class="num">1;       class=class="str">"cmt">// Set the last direction to sell
            recovery_lot = recovery_lot * class="num">2; class=class="str">"cmt">// Double the recovery lot size
            isBuyDone = false;        class=class="str">"cmt">// Reset buy trade flag

「区间触发与仓位翻倍的执行尾段」

这段逻辑接在 zone 判定之后,核心是在 bid 触碰到 zoneHigh 或 zoneLow 时按方向开仓,并立刻把 recovery_lot 翻倍。注意开仓前用 isBuyDone / isSellDone 和 lastDirection 做互斥,避免同一侧连续重复发单。 实际跑起来,若 zoneHigh=1.0950、zoneLow=1.0900,bid 上破 1.0950 且上次方向为卖(lastDirection<0),会先 NormalizeDouble(recovery_lot,2) 规范手数到 2 位小数,再 obj_trade.Buy(lots_Rec),随后 recovery_lot 乘 2、isBuyDone 置真、isSellDone 复位。 外汇与贵金属杠杆高,马丁式翻倍加仓在单边行情里可能快速放大浮亏,MT5 上用策略测试器跑这段前先把 recovery_lot 初值设小,观察 10 次连续反向后仓位曲线再决定要不要上实盘。

MQL5 / C++
         isSellDone = true;       class=class="str">"cmt">// Mark sell trade as done
      }
   }
   class=class="str">"cmt">//--- Check if price is within defined zones and take action
   if (zoneHigh > class="num">0 && zoneLow > class="num">0) {  
      class="type">class="kw">double lots_Rec = NormalizeDouble(recovery_lot, class="num">2); class=class="str">"cmt">// Normalize the recovery lot size to class="num">2 decimal places
      if (bid > zoneHigh) {
         if (isBuyDone == false || lastDirection < class="num">0) {
            obj_trade.Buy(lots_Rec); class=class="str">"cmt">// Open a buy trade
            
            lastDirection = class="num">1;       class=class="str">"cmt">// Set the last direction to buy
            recovery_lot = recovery_lot * class="num">2; class=class="str">"cmt">// Double the recovery lot size
            isBuyDone = true;        class=class="str">"cmt">// Mark buy trade as done
            isSellDone = false;      class=class="str">"cmt">// Reset sell trade flag
         }
      } else if (bid < zoneLow) {
         if (isSellDone == false || lastDirection > class="num">0) {
            obj_trade.Sell(lots_Rec); class=class="str">"cmt">// Open a sell trade
            
            lastDirection = -class="num">1;      class=class="str">"cmt">// Set the last direction to sell
            recovery_lot = recovery_lot * class="num">2; class=class="str">"cmt">// Double the recovery lot size
            isBuyDone = false;       class=class="str">"cmt">// Reset buy trade flag
            isSellDone = true;       class=class="str">"cmt">// Mark sell trade as done
         }
      }
   }
把回测监控交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到区域恢复仓位的浮动盈亏与保证金占用曲线,你只管判断要不要手动干预。

常见问题

普通马丁格尔只在亏损后单向加倍押注同一方向,区域恢复是开反向更大头寸构建对冲区间,靠价格回归区间而非单向反转来解套,概率结构不同但风险都随仓位放大。
通常用首单入场价加减预设点数作为边界,当报价穿透亏损边界时由 EA 的 OnTick 事件检查并下达反向单,具体阈值应在输入参数里外置方便优化。
可以,小布盯盘的品种页支持读取 MT5 账户持仓结构,能把恢复区间的上下轨和当前价位置可视化,省去自己画线的重复劳动。
影响显著,因为该策略靠密集间隔加仓摊平成本,滑点会抬高每次反向单实际成交价,可能让本应盈亏平衡的区间变成净亏,建议在回测中按经纪商真实点差加滑点建模。
持仓量随修复层数指数增长,遇到长期单边行情保证金需求会超出账户承受力,外汇贵金属本身高杠杆高风险,缺乏风控计划的实盘倾向快速穿仓。