改编版 MQL5 网格对冲 EA(第 1 部分):制作一个简单的对冲 EA·进阶篇
(2/3)· 接上篇基础概念,本篇用代码把经典对冲跑起来,并看回溯测试能否打破“必亏”偏见
◍ 对冲周期的变量骨架与开仓触发逻辑
做经典对冲的自动化,先要在全局空间把周期参数和价位变量立起来。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 的赋值位置:
<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 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> StartHedgeCycle() { isPositionBuy = initialPositionBuy; <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>); 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>; B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * <span class="predefines">_Point</span> * <span class="number">class="num">10</span>; C = isPositionBuy ? initialPrice - buySellDiff * <span class="predefines">_Point</span> * <span class="number">class="num">10</span> : initialPrice; 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>; <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); <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>); <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); <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>); <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); <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>); <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); <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>); <span class="macro">ENUM_ORDER_TYPE</span> positionType = isPositionBuy ? <span class="macro">ORDER_TYPE_BUY</span> : <span class="macro">ORDER_TYPE_SELL</span>; <span class="keyword">class="type">class="kw">double</span> SL = isPositionBuy ? C : B; <span class="keyword">class="type">class="kw">double</span> TP = isPositionBuy ? A : D; CTrade trade; trade.PositionOpen(<span class="predefines">_Symbol</span>, positionType, initialLotSize, initialPrice, SL, TP); 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。
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 能直接编译跑。
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">//+------------------------------------------------------------------+