网格和马丁格尔:它们是什么?如何使用它们?·进阶篇
马丁格尔的递归手数方程
马丁格尔的核心逻辑和网格相似:不预测方向,只靠加倍手数把亏损周期填平。某一笔订单亏损后,下一笔手数相对已亏头寸总和放大 n 倍;一旦获利,周期结束、手数重置为初始值。 关键约束是:周期内最后一笔盈利单的利润必须覆盖前面所有亏损单的损失。用账户货币(非点数)表达为 Pr = L[Nl+Np] * TP[Nl+Np] * TickSize,已亏部分 Ls = Sum(1,Nl)(L[i]*SL[i]) * TickSize。 手数靠递归得出:L[1]=StartLot,从 i=2 起 L[i] = (K * Sum(1,i-1)(L[j]*SL[j])) / TP[i],其中 K 是周期利润系数。原文未计点差佣金隔夜息,但方程本身可直接改。 外汇与贵金属属高风险品种,马丁格尔在实盘里随时可能因存款不足以完成周期而爆仓,上述方程仅用于 MT5 回测验证,不代表任何盈利保证。
L[class="num">1]=StartLot for(class="num">2, Nl ) L[i]=(K*Sum(class="num">1, i-class="num">1 )(L[j]*SL[j]))/TP[i]
◍ 手搓一个最简网格EA看回测真相
先在模板里挂上 MT5 自带的 PositionInfo 和 Trade 两个类,编译不会报错,省去自己写底层仓位管理的麻烦。 网格的核心输入就这几项:通道半宽 MaxChannelSizePoints=500 点、最小平仓波动 MinMoveToClose=100 点、网格步长 GridStepPoints=20 点、回算 BarsI=999 根 K 线、非对称系数 KClose=3.5。手数走最简固定 0.01,滑点上限 15 点,Magic 编号 679034 用于隔离本 EA 的订单。 EA 重启时要能恢复上一会话的网格状态,否则异常断开后网格参数就丢了。RestoreGrid() 先给 Time/High/Low 数组按 BarsI 扩维,再一次性把历史高低和时间的序列拷进来,最后用 PositionSelect 按 Magic 匹配持仓、把开仓时间读回 GridStartTime。 回测结论很直接:趋势段网格吃得挺香,但横盘损耗把利润吐光,最终净值为负——点差吃掉了边际收益,且一次『网格不够宽』的极端波动就能击穿此前所有盈利。外汇与贵金属属高风险品种,这类裸网格在实盘中断电或滑点扩大时可能直接爆仓,仅适合用极小仓位在策略测试器里验证逻辑。
class="macro">#include <Trade\PositionInfo.mqh> class="macro">#include <Trade\Trade.mqh> CPositionInfo m_position=CPositionInfo();class=class="str">"cmt">// trade position object CTrade m_trade=CTrade(); class=class="str">"cmt">// trading object class=class="str">"cmt">///grid variables input class="type">int MaxChannelSizePoints=class="num">500;class=class="str">"cmt">//Max Of a+d input class="type">int MinMoveToClose=class="num">100;class=class="str">"cmt">//Mininum Move input class="type">int GridStepPoints=class="num">20;class=class="str">"cmt">//Grid Step In Points input class="type">int BarsI=class="num">999;class=class="str">"cmt">//Bars To Start Calculate input class="type">class="kw">double KClose=class="num">3.5;class=class="str">"cmt">//Asymmetry class=class="str">"cmt">/// class=class="str">"cmt">////////minimum trading implementation input class="type">int SlippageMaxOpen=class="num">15; class=class="str">"cmt">//Slippage For Open In Points input class="type">class="kw">double Lot=class="num">0.01;class=class="str">"cmt">//Lot input class="type">int MagicC=class="num">679034;class=class="str">"cmt">//Magic class=class="str">"cmt">///////// class="type">void DimensionAllMQL5Values()class=class="str">"cmt">////////////////////////////// { ArrayResize(Time,BarsI,class="num">0); ArrayResize(High,BarsI,class="num">0); ArrayResize(Low,BarsI,class="num">0); } class="type">void CalcAllMQL5Values()class=class="str">"cmt">/////////////////////////////////// { ArraySetAsSeries(High,false); ArraySetAsSeries(Low,false); ArraySetAsSeries(Time,false); CopyHigh(_Symbol,_Period,class="num">0,BarsI,High); CopyLow(_Symbol,_Period,class="num">0,BarsI,Low); CopyTime(_Symbol,_Period,class="num">0,BarsI,Time); ArraySetAsSeries(High,true); ArraySetAsSeries(Low,true); ArraySetAsSeries(Time,true); } class="type">void RestoreGrid()class=class="str">"cmt">//recover the grid if the robot is restarted { DimensionAllMQL5Values(); CalcAllMQL5Values(); class="type">bool ord=PositionSelect(Symbol()); if ( ord && class="type">int(PositionGetInteger(POSITION_MAGIC)) == MagicC ) { GridStartTime=class="type">class="kw">datetime(PositionGetInteger(POSITION_TIME));
「网格边界与挂单铺设的代码骨架」
这段逻辑把网格的起始价和上下边界绑定到已有持仓的开仓价上。先通过 PositionGetDouble 取出 POSITION_PRICE_OPEN 转成 double 赋给 GridStartPrice,再把 GridUpPrice 和 GridDownPrice 都初始化成同一个值,相当于以当前仓位为原点拉出通道。 随后用 for 循环扫 BarsI 根 K 线:只要某根 High 突破 GridUpPrice 就刷新上边界,Low 跌破 GridDownPrice 就刷新下边界;一旦 Time[i] 早于 GridStartTime 就 break,避免回看超过网格构建时刻的历史。扫完把 bCanUpdate 置 true、bTryedAlready 置 false,标志网格进入可更新状态。 CreateNewGrid 才是真正铺挂单的地方。它先取 SymbolInfoTick 拿最新报价,GridStartPrice 直接吃 LastTick.bid,然后以 GridStepPoints*_Point 为步长,在 ask 之上到 MaxChannelSizePoints*_Point 区间内 while 循环挂 BuyStop,在 bid 之下对称挂 SellStop——步长和通道半宽都是点值整数,改这两个参数就能直接改变网格密度。 UpdateGrid 只做一件事: tick 刷新时把 bid 与上下边界比较并扩边;ClosePosition 按 MagicC 过滤同品种仓位后凭 ticket 平仓;CleanLimitOrders 则遍历 OrdersTotal 逐个 OrderDelete。外汇与贵金属杠杆高,网格马丁类逻辑在单边行情中可能快速放大回撤,参数未经回测勿直接上实盘。
GridStartPrice=class="type">class="kw">double(PositionGetDouble(POSITION_PRICE_OPEN)); GridUpPrice=GridStartPrice; GridDownPrice=GridStartPrice; for(class="type">int i=class="num">0;i<BarsI;i++) { if ( High[i] > GridUpPrice ) GridUpPrice=High[i]; if ( Low[i] < GridDownPrice ) GridDownPrice=Low[i]; if ( Time[i] < GridStartTime ) break; } bCanUpdate=true; bTryedAlready=false; } } class="type">class="kw">datetime GridStartTime;class=class="str">"cmt">//grid construction time class="type">class="kw">double GridStartPrice;class=class="str">"cmt">//grid starting price class="type">class="kw">double GridUpPrice;class=class="str">"cmt">//upper price within the corridor class="type">class="kw">double GridDownPrice;class=class="str">"cmt">//lower price within the corridor class="type">bool bCanUpdate;class=class="str">"cmt">//whether it is possible to update the grid class="type">bool bTryedAlready;class=class="str">"cmt">//whether there was an attempt to close a position class="type">void CreateNewGrid()class=class="str">"cmt">//create a new grid { SymbolInfoTick(Symbol(),LastTick); GridStartTime=TimeCurrent(); GridStartPrice=LastTick.bid; GridUpPrice=GridStartPrice; GridDownPrice=GridStartPrice; class="type">class="kw">double SummUp=LastTick.ask+class="type">class="kw">double(GridStepPoints)*_Point; class="type">class="kw">double SummDown=LastTick.bid-class="type">class="kw">double(GridStepPoints)*_Point; while ( SummUp <= LastTick.ask+class="type">class="kw">double(MaxChannelSizePoints)*_Point ) { m_trade.BuyStop(Lot,SummUp,Symbol()); SummUp+=class="type">class="kw">double(GridStepPoints)*_Point; } while ( SummDown >= LastTick.bid-class="type">class="kw">double(MaxChannelSizePoints)*_Point ) { m_trade.SellStop(Lot,SummDown,Symbol()); SummDown-=class="type">class="kw">double(GridStepPoints)*_Point; } } class="type">void UpdateGrid()class=class="str">"cmt">//update the grid parameters { SymbolInfoTick(Symbol(),LastTick); if ( LastTick.bid > GridUpPrice ) GridUpPrice=LastTick.bid; if ( LastTick.bid < GridDownPrice ) GridDownPrice=LastTick.bid; } class="type">void ClosePosition()class=class="str">"cmt">//close a position by a symbol { class="type">bool ord; ord=PositionSelect(Symbol()); if ( ord && class="type">int(PositionGetInteger(POSITION_MAGIC)) == MagicC ) { if(m_position.SelectByIndex(class="num">0)) m_trade.PositionClose(m_position.Ticket()); } } class="type">void CleanLimitOrders()class=class="str">"cmt">//clear limit orders { class="type">int orders=OrdersTotal(); for(class="type">int i=class="num">0;i<orders;i++) { class="type">ulong ticket=OrderGetTicket(i); if(ticket!=class="num">0) { m_trade.OrderDelete(ticket); } } }
网格平仓判定与调度落点
网格策略的退出逻辑集中在 bCanClose() 这个函数里。它先区分单向网格与双向网格:当起始价等于上轨或下轨时,只要求对应一侧离起始价的点数差不小于 MinMoveToClose 即触发平仓;双向情况下还要看上下两侧距离的比值是否越过 KClose,且触发侧点数差同样满足 MinMoveToClose。 被注释掉的那段通道突破逻辑(MaxChannelSizePoints)说明作者曾考虑用绝对通道宽度强平,但当前版本已弃用,实盘里不会生效。 Trade() 把更新网格、平仓、清限价单、重建网格串成状态机:有仓且 bCanClose() 为真就平掉并标记 bTryedAlready;若已尝试过则硬平;无仓且不允许更新时清单并重开新网格。外汇与贵金属杠杆高,这套状态切换若参数 MinMoveToClose、KClose 设错,可能在震荡中频繁重建网格放大回撤。 OnInit 里 RestoreGrid() 负责断线重连后恢复网格,OnTick 每笔报价直接调 Trade()。开 MT5 把下面代码挂到回测,重点观察 bCanClose 里 KClose 从 1.5 调到 2.0 时平仓次数是否明显下降。
class="type">bool bCanClose()class=class="str">"cmt">//closure condition { if ( GridStartPrice == GridUpPrice && (GridStartPrice-GridDownPrice)/_Point >= MinMoveToClose ) class="kw">return true; if ( GridStartPrice == GridDownPrice && (GridUpPrice-GridStartPrice)/_Point >= MinMoveToClose ) class="kw">return true; if ( GridStartPrice != GridUpPrice && GridStartPrice != GridDownPrice && (GridStartPrice-GridDownPrice)/(GridUpPrice-GridStartPrice) >= KClose && (GridStartPrice-GridDownPrice)/_Point >= MinMoveToClose ) class="kw">return true; if ( GridStartPrice != GridDownPrice && GridStartPrice != GridUpPrice && (GridUpPrice-GridStartPrice)/(GridStartPrice-GridDownPrice) >= KClose && (GridUpPrice-GridStartPrice)/_Point >= MinMoveToClose ) class="kw">return true; class=class="str">"cmt">/* if ( GridUpPrice >= GridStartPrice+MaxChannelSizePoints*_Point class=class="str">"cmt">//|| GridDownPrice <= GridStartPrice-MaxChannelSizePoints*_Point ) class="kw">return true; */ class="kw">return false; } class="type">void Trade()class=class="str">"cmt">//the main function where all actions are performed { class="type">bool ord=PositionSelect(Symbol()); if ( bCanUpdate ) UpdateGrid(); if ( ord && bCanClose() )class=class="str">"cmt">//if there is a position and the closing condition is met { ClosePosition(); CleanLimitOrders(); bCanUpdate=false; bTryedAlready=true; } if ( bTryedAlready ) ClosePosition(); if ( !bCanUpdate && !ord ) { CleanLimitOrders(); CreateNewGrid(); bCanUpdate=true; bTryedAlready=false; } } class="type">int OnInit() { m_trade.SetExpertMagicNumber(MagicC);class=class="str">"cmt">//set the magic number for positions RestoreGrid();class=class="str">"cmt">//restore the grid if present class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { Trade(); }