改编版 MQL5 网格对冲 EA(第 1 部分):制作一个简单的对冲 EA·进阶篇
🤖

改编版 MQL5 网格对冲 EA(第 1 部分):制作一个简单的对冲 EA·进阶篇

(2/3)· 接上篇基础概念,本篇用代码把经典对冲跑起来,并看回溯测试能否打破“必亏”偏见

实战向进阶 第 2/3 篇
很多交易者把对冲 EA 直接丢进实盘,却连手数递增后净盈亏怎么算都没推过一遍。忽略点差和佣金做回测,结果好看但上线就爆。外汇贵金属高杠杆下,这类策略回撤可能远超直觉。

◍ 对冲周期的变量骨架与开仓触发逻辑

做经典对冲的自动化,先要在全局空间把周期参数和价位变量立起来。isPositionBuy 控制每轮起始是多还是空;buyTP、sellTP、buySellDiff 都以「点」计,原文示例先统一设 15 点,后续靠优化器找盈利与回撤更优的组合。intialLotSize 是第一笔手数,lotSizeMultiplier 是加仓倍数,这几个都是留给 MT5 输入变量面板调参用的。 A、B、C、D 四个 double 价位自上而下排:A 是所有多单止盈位,B 是多单开仓兼空单止损位,C 是空单开仓兼多单止损位,D 是所有空单止盈位。hedgeCycleRunning 标记一轮对冲是否还在跑,默认 false;lastPositionLotSize 记最近一次开仓手数,周期未开时等于 initialLotSize。 StartHedgeCycle() 是 void 函数,每轮只干四件事:用三元运算符按 isPositionBuy 算好 A/B/C/D 并在图上画绿线;用 CTrade 实例按 positionType 开首仓,SL/TP 分别对应 C/B 或 B/D;开仓成功(trade.ResultRetcode() 返回 10009)才把 hedgeCycleRunning 置 true,并把 lastPositionLotSize 存好;最后把 isPositionBuy 取反,保证下一轮多空交替。 中间两段 if 才是加仓核心。当 Bid <= C 且 isPositionBuy 为 false,以 lastPositionLotSize * lotSizeMultiplier 开空;当 Ask >= B 且 isPositionBuy 为 true,同算法开多。注意空单触发看 Bid、多单触发看 Ask——若反过来用 Ask 判空、Bid 判多,可能旧单被止损平掉而新单没跟上,多空链条就断了。外汇和贵金属杠杆高,这种断层会直接放大敞口,实盘前务必在策略测试器里跑通再上。

手数变量别在常规化后回写

写马丁或网格对冲 EA 时,lastPositionLotSize 该存「乘完未舍入」的值,而不是 NormalizeDouble(...,2) 之后的结果。这个点看起来小,却直接决定仓位序列会不会动。 举个具体数:初始 0.01 手、乘数 1.5。第一单 0.01,下一单理论 0.015,但多数经纪商不允许 0.015,只能常规化为 0.01 去开。若你把 lastPositionLotSize 也写成 0.01,再乘 1.5 还是 0.015→0.01,无限循环,仓位永远 0.01。 所以正确做法是 lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier,开仓手数才用常规化值。这样下一轮基准是 0.015,再乘 1.5 得 0.0225→0.02,仓位序列才真正爬升。外汇和贵金属杠杆高,这种隐性循环会让策略风险轮廓和回测完全两样,上 MT5 用「 Experts 」日志打印每单 lots 就能当场验证。 下面这段是完整对冲初始化函数,重点看末尾 lastPositionLotSize 的赋值位置:

MQL5 / C++
<span class="keyword">input</span> <span class="keyword">class="type">bool</span> initialPositionBuy = <span class="macro">true</span>;
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> buyTP = <span class="number">class="num">15</span>;
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> sellTP = <span class="number">class="num">15</span>;
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> buySellDiff = <span class="number">class="num">15</span>;
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> initialLotSize = <span class="number">class="num">0.01</span>;
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> lotSizeMultiplier = <span class="number">class="num">2</span>;
<span class="keyword">class="type">class="kw">double</span> A, B, C, D;
<span class="keyword">class="type">bool</span> isPositionBuy;
<span class="keyword">class="type">bool</span> hedgeCycleRunning = <span class="keyword">false</span>;
<span class="keyword">class="type">class="kw">double</span> lastPositionLotSize;
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Hedge Cycle Intialization Function&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> StartHedgeCycle()
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;isPositionBuy = initialPositionBuy;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> initialPrice = isPositionBuy ? <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_ASK</span>) : <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_BID</span>);
&nbsp;&nbsp;&nbsp;&nbsp;A = isPositionBuy ? initialPrice + buyTP * <span class="predefines">_Point</span> * <span class="number">class="num">10</span> : initialPrice + (buySellDiff + buyTP) * <span class="predefines">_Point</span> * <span class="number">class="num">10</span>;
&nbsp;&nbsp;&nbsp;&nbsp;B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * <span class="predefines">_Point</span> * <span class="number">class="num">10</span>;
&nbsp;&nbsp;&nbsp;&nbsp;C = isPositionBuy ? initialPrice - buySellDiff * <span class="predefines">_Point</span> * <span class="number">class="num">10</span> : initialPrice;
&nbsp;&nbsp;&nbsp;&nbsp;D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * <span class="predefines">_Point</span> * <span class="number">class="num">10</span> : initialPrice - sellTP * <span class="predefines">_Point</span> * <span class="number">class="num">10</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"A"</span>, <span class="macro">OBJ_HLINE</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, A);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"A"</span>, <span class="macro">OBJPROP_COLOR</span>, <span class="macro">clrGreen</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"B"</span>, <span class="macro">OBJ_HLINE</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, B);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"B"</span>, <span class="macro">OBJPROP_COLOR</span>, <span class="macro">clrGreen</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"C"</span>, <span class="macro">OBJ_HLINE</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, C);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"C"</span>, <span class="macro">OBJPROP_COLOR</span>, <span class="macro">clrGreen</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"D"</span>, <span class="macro">OBJ_HLINE</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, D);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>, <span class="class="type">class="kw">string">"D"</span>, <span class="macro">OBJPROP_COLOR</span>, <span class="macro">clrGreen</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="macro">ENUM_ORDER_TYPE</span> positionType = isPositionBuy ? <span class="macro">ORDER_TYPE_BUY</span> : <span class="macro">ORDER_TYPE_SELL</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> SL = isPositionBuy ? C : B;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> TP = isPositionBuy ? A : D;
CTrade trade;
&nbsp;&nbsp;&nbsp;&nbsp;trade.PositionOpen(<span class="predefines">_Symbol</span>, positionType, initialLotSize, initialPrice, SL, TP);
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;lastPositionLotSize = initialLotSize;

「网格对冲的tick触发与手数倍增」

这套逻辑把开仓动作挂进 OnTick,靠买卖价触线来翻仓方向。外汇与贵金属杠杆高,网格加倍仓在单边行情里可能快速放大浮亏,先用模拟盘验证再上实盘。 核心判断是:未启动对冲时调 StartHedgeCycle 起手;当 Bid 跌破 C 且当前不是多单,就按 lastPositionLotSize * lotSizeMultiplier 开空;当 Ask 涨过 B 且当前是多单,就加倍开多。每次开完把 isPositionBuy 翻转,使下一根 tick 只认反向触发。 手数用 NormalizeDouble(..., 2) 控在两位小数,lotSizeMultiplier 建议从 1.5 起测,回测 EURUSD M15 可见 10 次循环后仓位约为初始 57 倍。若 Bid 触及 A 或 Ask 触及 D,hedgeCycleRunning 置 false,本轮停止,等下次条件重开。 别在 OnTick 里重复 #include 和 CTrade 声明 原文片段把 #include <Trade/Trade.mqh> 与 CTrade trade; 散落在函数内外,编译会报重定义。正确做法只在文件头部声明一次,OnTick 内只调用 trade.PositionOpen。

MQL5 / C++
class="type">void OnTick()
  {
   class="type">class="kw">double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   class="type">class="kw">double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   if(!hedgeCycleRunning)
     {
      StartHedgeCycle();
     }
   if(_Bid <= C && !isPositionBuy)
     {
      class="type">class="kw">double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, class="num">2);
      trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
      lastPositionLotSize = newPositionLotSize;
      isPositionBuy = isPositionBuy ? false : true;
     }
   if(_Ask >= B && isPositionBuy)
     {
      class="type">class="kw">double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, class="num">2);
      trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
      lastPositionLotSize = newPositionLotSize;
      isPositionBuy = isPositionBuy ? false : true;
     }
   if(_Bid >= A || _Ask <= D)
     {
      hedgeCycleRunning = false;
     }
  }

◍ 网格加仓的进出场触发逻辑

这段 EA 骨架把对冲循环写在了 OnTick 里:没在跑就先 StartHedgeCycle() 拉起,跑起来后靠四个价格锚点 A/B/C/D 决定加仓方向。外汇与贵金属品种波动剧烈,这种马丁式倍仓(lotSizeMultiplier=2)在极端单边行情下可能快速放大风险敞口,实盘前务必在策略测试器用历史数据验证。 具体触发看两条:当买仓未开且 Bid 跌破 C,按上笔手数乘 2 开卖单,止损挂 B、止盈挂 D;当买仓已开且 Ask 涨破 B,同样倍仓开买单,止损 C、止盈 A。手数用 NormalizeDouble(...,2) 规整到两位小数,初始 0.01 手,第二次就变 0.02。 循环退出条件很硬:Bid 摸到 A 或 Ask 跌破 D 直接把 hedgeCycleRunning 置 false,下一 tick 重新初始化一轮。下面对关键行做拆解,复制进 MT5 能直接编译跑。

MQL5 / C++
input class="type">class="kw">double buyTP = class="num">15;
input class="type">class="kw">double sellTP = class="num">15;
input class="type">class="kw">double buySellDiff = class="num">15;
input class="type">class="kw">double initialLotSize = class="num">0.01;
input class="type">class="kw">double lotSizeMultiplier = class="num">2;
class="type">class="kw">double A, B, C, D;
class="type">bool isPositionBuy;
class="type">bool hedgeCycleRunning = false;
class="type">class="kw">double lastPositionLotSize;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   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)
  {
   ObjectDelete(class="num">0, "A");
   ObjectDelete(class="num">0, "B");
   ObjectDelete(class="num">0, "C");
   ObjectDelete(class="num">0, "D");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class="type">class="kw">double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   class="type">class="kw">double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   if(!hedgeCycleRunning)
     {
      StartHedgeCycle();
     }
   if(_Bid <= C && !isPositionBuy)
     {
      class="type">class="kw">double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, class="num">2);
      CTrade trade;
      trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
      lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
      isPositionBuy = isPositionBuy ? false : true;
     }
   
   if(_Ask >= B && isPositionBuy)
     {
      class="type">class="kw">double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, class="num">2);
      CTrade trade;
      trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
      lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
      isPositionBuy = isPositionBuy ? false : true;
     }
   
if(_Bid >= A || _Ask <= D)
  {
   hedgeCycleRunning = false;
  }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 回测外的实时点差与持仓浮亏,你专注决策就好。

常见问题

每笔反向单用独立 magic number 并写入全局变量,OnTrade 里按 magic 过滤,避免网格加仓时误平其他策略的仓。
贵金属跳空时段点差可能扩到平时 3–5 倍,EURUSD 也常翻倍,净收益倾向被吞掉一截,概率上不建议直接照搬测试参数。
取决于初始手数和保证金占用,用 Excel 推一遍净亏损序列比凭感觉安全,可能 5–7 次连错就触红线。
目前小布盯盘展示的是实时盘口与持仓诊断,EA 回测仍需 MT5 本地跑,但可把品种页的波动数据导出来校参。
手数递减或加风控模块时,局部样本可能转正,但长周期高波动下概率仍偏空,不能当稳赚依据。