在MQL5中开发马丁格尔(Martingale)区域恢复策略(基础篇)
◍ 在MT5里搭一个马丁格尔区域恢复框架
马丁格尔类策略在外汇与贵金属品种上属于高风险玩法,价格反向走远时仓位指数级放大,账户可能在数根K线内触及强平。本文基于 MQL5 在 MetaTrader 5 上实现一个区域恢复(Zone Recovery)思路:不在固定点数挂倍仓,而是把亏损单归入一个价格区间,在区间另一边界补反向单,用对冲而非无限加仓摊平。 实际工程里,这类 EA 在 2024 年 12 月一篇社区帖中被打开 1 576 次,说明手动马丁变自动区域恢复仍是高频需求。开 MT5 后新建 EA,先声明区间上/下边界与每区手数梯度,是验证该逻辑的第一步。 外汇与贵金属杠杆普遍偏高,区域恢复只能控制单次暴露,无法消除系统性跳空风险,实盘前必须用历史数据做点差与滑点压力测试。
马丁格尔区域恢复到底在做什么
马丁格尔区域恢复(Zone Recovery)是一类在 MT5 里常被做成 EA 的逆价加仓逻辑:当首单浮亏后,不在原方向加仓,而是开一张方向相反、手数略大的单,试图用后续波段把两边盈亏对冲掉。 它本质不赌单边,而是赌市场终会在某个区间内走出一段可识别的上升或下降趋势,让反向单盈利覆盖原亏损。外汇与贵金属杠杆高,这种结构在极端单边行情下浮亏会指数级放大,实盘前必须先在策略测试器里跑通。 本文系列会把这个逻辑从定义、描述一路落到 MQL5 代码实现与回测,读者可以跟着每一节在 MT5 里逐步复刻,而不是只看结论。
「用反向仓位织一张恢复网」
区域恢复策略的本质,是先框定一个你预期价格会回来的区间,而不是等亏损扩大才手忙脚乱。当首笔单子走反、价格跌进预设的损失带,你不开溜,而是按节奏挂出反向单,把整组仓位铺成一个‘回到这带就能平本或翻红’的恢复网。 这套打法靠的是对冲加均价下沉。首单反向后先开等量反向仓锁一部分风险;若价格继续往外走,就在关键间距补新仓,把平均成本往下拽。统计上,只要价格回碰该区间,后补仓的累计盈余有概率覆盖首单亏损——但这依赖区间画得准,不是无脑加仓。 它最大的实用点是:不必押中方向也能把死单救活,尤其适合 EURUSD、XAUUSD 这类毛刺多、假突破频发的时段。不过外汇和贵金属杠杆高,恢复网越织保证金占用越凶,一波单边就可能直接拉爆,老手也得先算好最大加仓层数再动手。
◍ 反向加倍如何把浮亏拖回盈亏线
区域恢复策略的起点很朴素:先按方向预期开初始仓,比如看涨就先建多。价格走到预定盈利位就平掉落袋,这一步和裸趋势单没区别,不依赖任何复杂逻辑。 真正触发机制的是反向:当多头被打到预定止损位,不选认亏退出,而是开一张规模翻倍的空单(常见为前仓的 2 倍手数)。靠这张更大空单的潜在盈利去盖住第一笔多头的浮亏,本质是用仓位不对称做区间内对冲,赌价格至少在恢复区里横住或回头。 后续若价格继续下探到另一个预设点,初始多 + 加倍空的组合会推到整体盈亏平衡甚至微利,这时双边一起平掉,用后发空单的利润填前仓的坑。这套动作对计算和入场时点的精度要求很高,外汇与贵金属杠杆环境下反向加倍会快速放大敞口,属于高风险玩法,实际触发前必须在 MT5 上用历史数据验算倍数与区间间距。
用 MQL5 把区域恢复位置先钉死在代码里
做可视化区域恢复 EA,第一步不是画图,而是把四个关键价格位置用常量固化下来。MQL5 里用 #define 给区域高点、低点、目标高点、目标低点分配易读名称:ZONE_H 对应 'ZH',ZONE_L 对应 'ZL',ZONE_T_H 对应 'ZTH',ZONE_T_L 对应 'ZTL'。这些常量后续直接挂到水平线对象上,避免在逻辑里到处写魔法字符串。 开仓部分走官方交易库最省事。用 #include <Trade/Trade.MQH> 把 CTrade 类拉进来,实例化一个指针 obj_trade,后面所有下单、平仓、选仓都靠它。信号源这里用 RSI 举例,rsi_handle 在 OnInit 里初始化,rsiData 数组靠 CopyBuffer 取数;overBoughtLevel 和 overSoldLevel 写死 70 和 30 作为阈值,zoneHigh / zoneLow / zoneTargetHigh / zoneTargetLow 先置 0,等真有单子再填。 OnTick 里先抓最新 ASK/BID:用 SymbolInfoDouble(_Symbol, SYMBOL_ASK) 拿询价、SYMBOL_BID 拿出价,再乘 _Point 得到区间范围和目标大小的绝对价格。实用变量全加 static——LastDirection 记上次方向(买 +1 / 卖 -1),recovery_lot 存补仓量,isBuyDone / isSellDone 防同侧重复开。static 保证 OnTick 每次重入不丢状态。 每根 K 线只取一次指标:用 iBars(_Symbol, _Period) 存进 bars,和上次 totalBars 比对,相等就 return,不相等才更新并继续。CopyBuffer(rsi_handle, 0, 1, 2, rsiData) 只取前两根,索引 1 小于 30 且索引 0 大于 30 即判买入交叉;卖相反。下单后拿 ticket 选仓取开盘价:买仓开盘价作 zoneHigh,减区间得 zoneLow,加减目标得两个目标位,最后 NormalizeDouble 到品种小数位。 四个水平线靠自写 void 函数画:ObjectCreate 建横线、ObjectSetInteger 设颜色和宽度,名字传 ZH/ZL/ZTH/ZTL。平仓监控放在 K 线限制之前——买价破目标高或破目标低就 deleteZoneLevels 删线,循环平掉所有仓,再把 zone 变量归 0、方向和恢复手数手动重置。恢复仓判断 zone 线 >0 仍在跑,lots_rec 按账户类型 NormalizeDouble 到 3 位(微手账户),出价破 zoneHigh 且没买过就补买,破 zoneLow 补卖,手数翻倍进 recovery_lot。 下面这段是常量定义的原文起点,开 MT5 新建 EA 时直接粘到头部就能编译通过:
class="macro">#define ZONE_H "ZH" class=class="str">"cmt">// Define a constant for the high zone line name
「把区域与RSI句柄落进EA骨架」
这段初始化代码把后续Zone策略要用的常量与变量一次性铺开:用 ZL / ZTH / ZTL 三个宏定义标识低区线与双向目标区线,并引入 CTrade 对象接管下单。RSI 句柄设为14周期收盘价计算,超买70、超卖30是硬阈值,区域宽度写死为200点、目标延展400点,这些数字直接决定Zone绘制的疏密。 OnInit 里只做一件事:向当前图表绑定 iRSI,返回 INIT_SUCCEEDED;OnDeinit 则释放句柄并清空 rsiData 数组,避免EA重载时内存泄漏。开MT5把这段贴进新EA,编译通过后能在初始化日志看到无报错,即说明RSI句柄已就位。 Tick函数前半段先抓 ask/bid,用静态变量 lastDirection、recovery_lot、isBuyDone/isSellDone 记录上一次方向与恢复状态;再用 iBars 比对 totalBars,若K线数未增加直接 return,有持仓也 return——这意味着策略只在「新K线 + 无持仓」条件下才往下走。外汇与贵金属杠杆高,这种空仓闸门能降低重复触发风险,但是否过滤有效仍需你回测验证。
class="macro">#define ZONE_L "ZL" class=class="str">"cmt">// Define a constant for the low zone line name class="macro">#define ZONE_T_H "ZTH" class=class="str">"cmt">// Define a constant for the target high zone line name class="macro">#define ZONE_T_L "ZTL" class=class="str">"cmt">// Define a constant for the target low zone line name class="macro">#include <Trade/Trade.mqh> CTrade obj_trade; class="type">int rsi_handle; class=class="str">"cmt">// Handle for the RSI indicator class="type">class="kw">double rsiData[]; class=class="str">"cmt">// Array to store RSI data class="type">int totalBars = class="num">0; class=class="str">"cmt">// Variable to keep track of the total number of bars class="type">class="kw">double overBoughtLevel = class="num">70.0; class=class="str">"cmt">// Overbought level for RSI class="type">class="kw">double overSoldLevel = class="num">30.0; class=class="str">"cmt">// Oversold level for RSI class="type">class="kw">double zoneHigh = class="num">0; class=class="str">"cmt">// Variable to store the high zone price class="type">class="kw">double zoneLow = class="num">0; class=class="str">"cmt">// Variable to store the low zone price class="type">class="kw">double zoneTargetHigh = class="num">0; class=class="str">"cmt">// Variable to store the target high zone price class="type">class="kw">double zoneTargetLow = class="num">0; class=class="str">"cmt">// Variable to store the target low zone price class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Initialize the RSI indicator rsi_handle = iRSI(_Symbol, PERIOD_CURRENT, class="num">14, PRICE_CLOSE); class=class="str">"cmt">//--- Return initialization result 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">//--- Remove RSI indicator from memory IndicatorRelease(rsi_handle); ArrayFree(rsiData); class=class="str">"cmt">// Free the RSI data array } class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); class="type">class="kw">double zoneRange = class="num">200*_Point; class="type">class="kw">double zoneTarget = class="num">400*_Point; class="kw">static class="type">int lastDirection = class="num">0; class=class="str">"cmt">//-class="num">1 = sell, class="num">1 = buy class="kw">static class="type">class="kw">double recovery_lot = class="num">0.0; class="kw">static class="type">bool isBuyDone = false, isSellDone = false; class="type">int bars = iBars(_Symbol,PERIOD_CURRENT); if (totalBars == bars) class="kw">return; totalBars = bars; if (PositionsTotal() > class="num">0) class="kw">return;