开发一个跨平台网格 EA·综合运用
📘

开发一个跨平台网格 EA·综合运用

第 3/3 篇

◍ 给图表装一个一键清仓的入口

EA 好不好用,不只看策略逻辑,还看你能不能在图表上直接看清当前仓位和利润,并随手关掉所有单子。我们在图表右上角放一个 Close All 按钮,点一下就平掉本 EA 在当前品种上的全部头寸与挂单,省去手动逐个关闭的麻烦。 所有图形对象都统一加前缀 grider_,避免和手动画线或其他 EA 的对象重名。MQL5 里用 PositionsTotal() 枚举持仓,MQL4 里用 OrdersTotal() 配合 OrderSelect(),所以这段代码用 #ifdef __MQL5__ 做条件编译,同一套函数名在两套环境里都能跑。 getmeinfo_btn() 负责统计:posPlus 累加买入仓位手数,posMinus 累加卖出仓位手数,profit 把每单的浮盈加隔夜利息(MQL5)或佣金(MQL4 分支)都算进去。只要 positionExist 被置为 true,图表上就会显示按钮和汇总数据;没有持仓时按钮不出现。 点击响应写在 OnChartEvent() 里,捕捉到按钮的 chart event 后调用 CloseAllPos()。这个函数本体在下一篇才实现,本篇只接好触发链路。另外 EA 退出时由 OnDeInit() 删除带 grider_ 前缀的对象,防止图表残留。 外汇和贵金属杠杆高,一键全平可能在大滑点行情下成交价偏离预期,实盘前务必在 MT5 策略测试器用历史数据跑一遍按钮逻辑。

MQL5 / C++
class="type">class="kw">string prefix_graph="grider_";
class="type">void getmeinfo_btn(class="type">class="kw">string symname){
  class="type">class="kw">double posPlus=class="num">0;
  class="type">class="kw">double posMinus=class="num">0;
  class="type">class="kw">double profit=class="num">0;
  class="type">class="kw">double positionExist=false;
  class=class="str">"cmt">// 对开启的买入和卖出仓位计数,
  class=class="str">"cmt">// 并计算它们的总利润
  class="macro">#ifdef __MQL5__
    class="type">int cntMyPos=PositionsTotal();
    for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
      if(PositionGetSymbol(ti)!=symname) class="kw">continue;
      if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue;
      
      positionExist=true;
      
      profit+=PositionGetDouble(POSITION_PROFIT);
      profit+=PositionGetDouble(POSITION_SWAP);
      
      if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){
        posPlus+=PositionGetDouble(POSITION_VOLUME);
      }else{
        posMinus+=PositionGetDouble(POSITION_VOLUME);
      }
    }
  class="macro">#else
    class="type">int cntMyPos=OrdersTotal();
    if(cntMyPos>class="num">0){
      for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
        if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue;
        if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ class="kw">continue; }
        if(OrderSymbol()!=symname) class="kw">continue;
        if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue;
        
        positionExist=true;
        
        profit+=OrderCommission();

「一键平仓按钮的盈亏与仓位回显」

这段逻辑干的事很直接:遍历当前图表挂单与持仓,把每笔的浮动盈利和隔夜利息累加到 profit,再按买卖方向分别统计手数到 posPlus(多)与 posMinus(空)。 当检测到有持仓时,脚本在图表右上角偏移 233 像素处生成一个名为 prefix_graph+"delall" 的按钮,标题格式为「关闭全部 (盈亏两位小数) L: 多单手数 S: 空单手数」。例如当前多 0.50 空 0.20、浮盈 12.34,按钮就显示 closeAll (12.34) L: 0.5 S: 0.2。 若无持仓但按钮仍存在,则用 ObjectFind 判断后 ObjectDelete 清掉,避免界面残留。最后 ChartRedraw(0) 强制重绘,让变化立刻可见。 createObject 的入参是按钮名、横向权重值(此处传 233)和标题文本;函数内先算图表宽度减 87 像素作为右边距,再两轮循环扫描带 prefix_graph 前缀的对象,取最大 YDISTANCE 做纵向避让,保证新按钮不压住已有的同类控件。外汇与贵金属杠杆高,这类一键平仓在剧烈滑点行情中可能以偏离预期的价格成交,实盘前务必在策略测试器里跑通。

MQL5 / C++
profit+=OrderProfit();
profit+=OrderSwap();

if(OrderType()==OP_BUY){
   posPlus+=OrderLots();
}else{
   posMinus+=OrderLots();
}

class=class="str">"cmt">// 有开启的仓位
class=class="str">"cmt">// 增加关闭按钮
if(positionExist){
   createObject(prefix_graph+"delall", class="num">233, langs.closeAll+" ("+DoubleToString(profit, class="num">2)+") L: "+(class="type">class="kw">string) posPlus+" S: "+(class="type">class="kw">string) posMinus);
}else{
   class=class="str">"cmt">// otherwise, class="kw">delete the button for closing positions
   if(ObjectFind(class="num">0, prefix_graph+"delall")>class="num">0){
      ObjectDelete(class="num">0, prefix_graph+"delall");
   }
}

class=class="str">"cmt">// 更新当前图表
class=class="str">"cmt">// 显示所实现的变化
ChartRedraw(class="num">0);
}
class="type">void createObject(class="type">class="kw">string name, class="type">int weight, class="type">class="kw">string title){
   class=class="str">"cmt">// 如果图表上没有 &class="macro">#x27;name&class="macro">#x27; 按钮,就创建它
   if(ObjectFind(class="num">0, name)<class="num">0){
      class=class="str">"cmt">// 定义相对于要显示按钮的图表右上角的偏移
      class="type">long offset= ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS)-class="num">87;
      class="type">long offsetY=class="num">0;
      for(class="type">int ti=class="num">0; ti<ObjectsTotal((class="type">long) class="num">0); ti++){
         class="type">class="kw">string objName= ObjectName(class="num">0, ti);
         if( StringFind(objName, prefix_graph)<class="num">0 ){
            class="kw">continue;
         }
         class="type">long tmpOffset=ObjectGetInteger(class="num">0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset>offsetY){
            offsetY=tmpOffset;
         }
      }
      
      for(class="type">int ti=class="num">0; ti<ObjectsTotal((class="type">long) class="num">0); ti++){
         class="type">class="kw">string objName= ObjectName(class="num">0, ti);
         if( StringFind(objName, prefix_graph)<class="num">0 ){
            class="kw">continue;
         }
         class="type">long tmpOffset=ObjectGetInteger(class="num">0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset!=offsetY ){
            class="kw">continue;
         }
      }

按钮排布与一键平仓的事件绑定

这段逻辑解决的是面板按钮在图表上的自动换行与点击响应。先遍历已有对象取最小水平间距 tmpOffset,若落在 0 到当前 offset 之间就刷新 offset,随后 offset 减去按钮宽度 weight+1 像素;当 offset 算出来小于 0,就回退到图表宽度减 87 像素处,并把 offsetY 下移 25 像素重新排,保证按钮不溢出右边界。 创建按钮时用 ObjectCreate 指定 OBJ_BUTTON,依次写死 XDISTANCE/YDISTANCE 定位、OBJPROP_TEXT 标题、XSIZE=weight、FONTSIZE=8、YSIZE=25,背景色 clrLightGray、文字 clrBlack,最后 ChartRedraw 让 MT5 立即重绘。若对象已存在则只改 OBJPROP_TEXT,避免重复建按钮。 交互层挂在 OnChartEvent 的 CHARTEVENT_OBJECT_CLICK 上:当 sparam 等于 prefix_graph+"delall" 时调用 closeAllPos() 平掉所有持仓。外汇与贵金属杠杆高,这类一键平仓在滑点行情中可能瞬间放大回撤,实盘前务必在策略测试器用历史数据验证点击逻辑。 析构用 OnDeinit 里的 ObjectsDeleteAll(0, prefix_graph) 按前缀清场,切换周期或移除 EA 时不留垃圾对象。

MQL5 / C++
  tmpOffset=ObjectGetInteger(class="num">0, objName, OBJPROP_XDISTANCE);
  if( tmpOffset>class="num">0 && tmpOffset<offset){
      offset=tmpOffset;
  }
  }
  offset-=(weight+class="num">1);
  if(offset<class="num">0){
     offset=ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS)-class="num">87;
     offsetY+=class="num">25;
     offset-=(weight+class="num">1);
  }

   ObjectCreate(class="num">0, name, OBJ_BUTTON, class="num">0, class="num">0, class="num">0);
   ObjectSetInteger(class="num">0,name,OBJPROP_XDISTANCE,offset);
   ObjectSetInteger(class="num">0,name,OBJPROP_YDISTANCE,offsetY);
   ObjectSetString(class="num">0,name,OBJPROP_TEXT, title);
   ObjectSetInteger(class="num">0,name,OBJPROP_XSIZE,weight);
   ObjectSetInteger(class="num">0,name,OBJPROP_FONTSIZE, class="num">8);
   ObjectSetInteger(class="num">0,name,OBJPROP_COLOR, clrBlack);
   ObjectSetInteger(class="num">0,name,OBJPROP_YSIZE,class="num">25);
   ObjectSetInteger(class="num">0,name,OBJPROP_BGCOLOR, clrLightGray);
   ChartRedraw(class="num">0);
 }else{
   ObjectSetString(class="num">0,name,OBJPROP_TEXT, title);
 }
}
class="type">void OnChartEvent(const class="type">int id,      class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整型的事件参数
                  const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度浮点型的事件参数
                  const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数
{
  class="type">class="kw">string text="";
  class="kw">switch(id){
   case CHARTEVENT_OBJECT_CLICK:
     class=class="str">"cmt">// 如果按钮名称是 prefix_graph+"delall", 则
     if (sparam==prefix_graph+"delall"){
        closeAllPos();
     }
     class="kw">break;
  }
}
class="type">void OnDeinit(const class="type">int reason)
  {
     ObjectsDeleteAll(class="num">0, prefix_graph);
  }

◍ 用反向单对锁平仓省点差

EA 里一键清场不能只会无脑删仓。若同一品种同时持有一手多和一手空,MQL5 下走 TRADE_ACTION_CLOSE_BY 让经纪商以反向仓位抵消,能把开双边时付的两道点差收回来一部分,账户净值倾向比普通市价平仓略多一截。 closeByPos() 先扫一遍持仓,对符号和魔术码(EA_Magic)做过滤,只在手数完全相等的 BUY 与 SELL 之间发起 close by。若成交量不同则跳过,留给后续常规平仓处理。 函数发单成功后置 repeatOpen=true 并自我递归:因为持仓量可能不等,一次抵消只减不掉完,重跑才能把残余对锁清干净。外汇和贵金属杠杆高,这类对冲清仓能否省点差完全看经纪商是否支持该指令,实盘前务必在 MT5 策略测试器用真实点差验证。 下面这段是 MQL5 头文件和 closeByPos 前半段,CTrade 对象已在 __MQL5__ 宏下挂好: #ifdef __MQL5__ // 仅 MQL5 环境编译 #include <Trade\Trade.mqh> // 引入交易类 CTrade Trade; // 创建交易对象 #endif void closeByPos(){ // 定义反向平仓函数 bool repeatOpen=false; // 是否需要重跑标记 #ifdef __MQL5__ int cntMyPos=PositionsTotal(); // 当前持仓总数 for(int ti=cntMyPos-1; ti>=0; ti--){ // 逆序遍历持仓 if(PositionGetSymbol(ti)!=_Symbol) continue; // 非当前品种跳过 if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; // 魔术码不符跳过 if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){ // 找到多单 long closefirst=PositionGetInteger(POSITION_TICKET); // 多单 ticket double closeLots=PositionGetDouble(POSITION_VOLUME); // 多单手数 for(int ti2=cntMyPos-1; ti2>=0; ti2--){ // 再扫一遍找空单 if(PositionGetSymbol(ti2)!=_Symbol) continue; // 非本品种跳过 if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; // 魔术码不符跳过 if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue; // 不是空单跳过 if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue; // 手数不等跳过 MqlTradeRequest request; // 交易请求结构 MqlTradeResult result; // 交易结果结构 ZeroMemory(request); // 清空请求 ZeroMemory(result); // 清空结果 request.action=TRADE_ACTION_CLOSE_BY; // 反向平仓动作 request.position=closefirst; // 被平多单 request.position_by=PositionGetInteger(POSITION_TICKET); // 用来抵消的空单 if(EA_Magic>0) request.magic=EA_Magic; // 带魔术码 if(OrderSend(request,result)){ // 发单成功 repeatOpen=true; // 标记重跑 break; // 跳出空单循环 } }

MQL5 / C++
class="macro">#ifdef __MQL5__
  class="macro">#include <Trade\Trade.mqh>
  CTrade Trade;
class="macro">#endif
class="type">void closeByPos(){
  class="type">bool repeatOpen=false;
  class="macro">#ifdef __MQL5__
    class="type">int cntMyPos=PositionsTotal();
    for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
      if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue;
      if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue;
      
      if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){
        class="type">long closefirst=PositionGetInteger(POSITION_TICKET);
        class="type">class="kw">double closeLots=PositionGetDouble(POSITION_VOLUME);
        
        for(class="type">int ti2=cntMyPos-class="num">1; ti2>=class="num">0; ti2--){
          if(PositionGetSymbol(ti2)!=_Symbol) class="kw">continue;
          if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue;
          if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) class="kw">continue;
          if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) class="kw">continue;
          
          class="type">MqlTradeRequest request;
          class="type">MqlTradeResult  result;
          ZeroMemory(request);
          ZeroMemory(result);
          request.action=TRADE_ACTION_CLOSE_BY;
          request.position=closefirst;
          request.position_by=PositionGetInteger(POSITION_TICKET);
          if(EA_Magic>class="num">0) request.magic=EA_Magic;
          if(OrderSend(request,result)){
            repeatOpen=true;
            class="kw">break;
          }
        }

「用 OrderCloseBy 对冲反手仓位」

这段逻辑跑在 MQL5 的 #else 分支里,专门针对老式持仓遍历方式做反手平仓。它先取 OrdersTotal() 拿到当前持仓数,再从尾到头扫一遍,只挑本符号且 Magic 匹配的订单,避免误伤其他 EA 的仓位。 核心动作是:找到一个 BUY 单后,再嵌套一轮扫描去找同符号、同 Magic 且手数不小于它的 SELL 单,然后调用 OrderCloseBy(closefirst, OrderTicket()) 用反向单直接抵销。外汇与贵金属杠杆高,这种抵销若手数匹配不当,可能留下残仓继续暴露风险。 只要内层抵销成功,repeatOpen 置真并 break,外层回到 closeByPos() 递归再跑一遍,直到没有可配对的反向仓。你可以在 MT5 策略测试器里把 EA_Magic 设成固定值,挂一多一空看 repeatOpen 的触发次数来验证配对效率。

MQL5 / C++
class="macro">#else
   class="type">int cntMyPos=OrdersTotal();
   if(cntMyPos>class="num">0){
      for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
         if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue;
         if( OrderSymbol()!=_Symbol ) class="kw">continue;
         if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue;
         
         if( OrderType()==OP_BUY ){
            class="type">int closefirst=OrderTicket();
            class="type">class="kw">double closeLots=OrderLots();
            
            for(class="type">int ti2=cntMyPos-class="num">1; ti2>=class="num">0; ti2--){
               if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue;
               if( OrderSymbol()!=_Symbol ) class="kw">continue;
               if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue;
               if( OrderType()!=OP_SELL ) class="kw">continue;
               if( OrderLots()<closeLots ) class="kw">continue;
               
               if( OrderCloseBy(closefirst, OrderTicket()) ){
                  repeatOpen=true;
                  class="kw">break;
               }
            }
            if(repeatOpen){
               class="kw">break;
            }
         }
      }
   }
class="macro">#endif
class=class="str">"cmt">// 如果我们根据反向仓位关闭了仓位
class=class="str">"cmt">// 再次运行 closeByPos 函数
if(repeatOpen){
   closeByPos();
}
}
class="type">void closeAllPos(){
   closeByPos();
   class="macro">#ifdef __MQL5__

MT4与MT5双版本的平仓撤单写法

EA在出场环节必须兼容两套终端接口:MT5用PositionsTotal/OrdersTotal遍历,MT4则靠OrdersTotal加OrderSelect按位选取。两者都靠EA_Magic过滤自身订单,避免误动手动单或其他EA的单子。 MT5分支先清持仓再删挂单:从后往前循环,PositionGetSymbol不匹配_Symbol就continue, magic不对也跳过,命中后Trade.PositionClose按ticket平掉;挂单侧用OrderGetTicket取ticket,同样过滤符号与magic,再Trade.OrderDelete撤单。 MT4分支没有持仓/挂单分离概念,OrdersTotal>0时逆序OrderSelect,OrderSymbol和OrderMagicNumber双重过滤。买单用SymbolInfoTick取bid、OrderClose按原手数平(滑点给100点);卖单逻辑对称,先取tick再Close。外汇与贵金属杠杆高,这类全平代码若magic设错可能一次清掉无关仓位,上MT5前先用策略测试器跑一遍。

MQL5 / C++
class="type">int cntMyPos=PositionsTotal();
for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
   if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue;
   if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue;
   Trade.PositionClose(PositionGetInteger(POSITION_TICKET));
}
class="type">int cntMyPosO=OrdersTotal();
for(class="type">int ti=cntMyPosO-class="num">1; ti>=class="num">0; ti--){
   class="type">ulong orderTicket=OrderGetTicket(ti);
   if(OrderGetString(ORDER_SYMBOL)!=_Symbol) class="kw">continue;
   if(EA_Magic>class="num">0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) class="kw">continue;
   Trade.OrderDelete(orderTicket);
}
class="macro">#else
class="type">int cntMyPos=OrdersTotal();
if(cntMyPos>class="num">0){
   for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
      if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue;
      if( OrderSymbol()!=_Symbol ) class="kw">continue;
      if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue;
      if( OrderType()==OP_BUY ){
         class="type">MqlTick latest_price;
         if(!SymbolInfoTick(OrderSymbol(),latest_price)){
            Alert(GetLastError());
            class="kw">return;
         }
         if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,class="num">100)){
         }
      }else if(OrderType()==OP_SELL){
         class="type">MqlTick latest_price;
         if(!SymbolInfoTick(OrderSymbol(),latest_price)){
            Alert(GetLastError());
            class="kw">return;
         }

◍ 平仓与挂单的清理逻辑

这段片段处理的是持仓和平仓后的界面清理:对市价单调用 OrderClose 按当前 ask 平仓,滑点给到 100 点;若单子是挂单则走 OrderDelete 直接删掉,失败时不额外报错只留空块。 OrderClose 的第三个参数用了 latest_price.ask,意味着只处理了多单平仓路径,空单若复用同一段逻辑会以买价平仓,实盘里可能吃掉额外点差。外汇与贵金属杠杆高,这种硬编码价格方向在剧烈行情中可能触发非预期成交。 结尾的 ObjectFind 检查名为 delall 的图形对象,存在就 ObjectDelete 清掉——这是把‘一键清仓’按钮从图表移除的动作,通常放在循环退出后执行,避免每帧重复查找。 把滑点参数 100 改成经纪商实际允许值再去 MT5 回测,能看出成交率差异;若做空较多,需补一个 bid 分支而非直接套 ask。

MQL5 / C++
if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,class="num">100)){
  }
}else{
  if(!OrderDelete(OrderTicket())){
  }
}
      
    }
  }
 class="macro">#endif
 class=class="str">"cmt">// 删除关闭仓位的按钮
 if(ObjectFind(class="num">0, prefix_graph+"delall")>class="num">0){
   ObjectDelete(class="num">0, prefix_graph+"delall");
 }

「把下单逻辑塞进 OnTick 的骨架」

EA 的主循环落在 OnTick(),但每个 tick 都跑网格检查纯属浪费。实际做法是在函数头部加一根「新柱判断」栅栏:不是 M5 新柱的首 tick 就直接 return,相当于把检查频率压到每 5 分钟一次,EA 也必须挂在 M5 周期上跑。 新柱判定靠 pdxIsNewBar():用静态变量存上根柱时间,CopyTime 取当前柱时间,不一致就刷新并返回 true。这段逻辑本身不依赖任何外部指标,复制进 MT5 能立刻编译。 进网格之前有两道前置:先 checkTakeProfit() 把同品种、同 magic 的持仓盈亏加总(含 swap/commission),超过 takeProfit 输入值就调 closeAllPos() 全平;再补一句 Close All 按钮的显示调用,否则前面写的面板控件永远是隐身。 最后才轮到网格本体——existLimits() 返回 true 就不动,返回 false 才 initLimits() 铺新单。这样 OnTick 的分工就清晰了:拦 tick、查利润、亮按钮、补网格,四步顺序不能乱。外汇与贵金属杠杆高,网格马丁类逻辑回撤可能超出本金,上模拟盘验证前别挂实盘。

MQL5 / C++
  if( !pdxIsNewBar() ){
      class="kw">return;
  }
class="type">bool pdxIsNewBar(){
   class="kw">static class="type">class="kw">datetime Old_Time;
   class="type">class="kw">datetime New_Time[class="num">1];
   if(CopyTime(_Symbol,_Period,class="num">0,class="num">1,New_Time)>class="num">0){
      if(Old_Time!=New_Time[class="num">0]){
         Old_Time=New_Time[class="num">0];
         class="kw">return true;
      }
   }
   class="kw">return false;
}
   if(checkTakeProfit()){
      closeAllPos();
   }
class="type">bool checkTakeProfit(){
   if( takeProfit<=class="num">0 ) class="kw">return false;
   class="type">class="kw">double curProfit=class="num">0;
   class="type">class="kw">double profit=class="num">0;

   class="macro">#ifdef __MQL5__
      class="type">int cntMyPos=PositionsTotal();
      for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue;
         if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue;

         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   class="macro">#else
      class="type">int cntMyPos=OrdersTotal();
      if(cntMyPos>class="num">0){
         for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ class="kw">continue; }
            if(OrderSymbol()!=_Symbol) class="kw">continue;
            if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue;

            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   class="macro">#endif
   if(profit>takeProfit){
      class="kw">return true;
   }

仓位存在性判定与网格初始化

网格 EA 启动后第一件事不是直接挂单,而是先确认当前品种是否已有本魔术码(EA_Magic)的未结仓位或挂单。existLimits() 函数就是干这个的:MQL5 环境下分别遍历 PositionsTotal() 与 OrdersTotal(),用 PositionGetSymbol / OrderGetString 比对 _Symbol,再用 PositionGetInteger(POSITION_MAGIC) / OrderGetInteger(ORDER_MAGIC) 过滤魔术码,命中即返回 true。 如果 existLimits() 返回 false,才进入 initLimits() 铺设网格。这里有个容易被忽略的护栏:先用 SymbolInfoTick 取 lastme.bid,若 bid 为 0 直接 return,避免在没有有效报价时瞎挂单。 minStop 的计算很关键——它等于 SYMBOL_POINT 乘以 SYMBOL_TRADE_STOPS_LEVEL,也就是经纪商要求的止损/挂单最小距离。循环中 curPrice 从 bid 起每次加 ST(步长),若 curPrice - ask 小于 minStop 就 continue 跳过,防止订单因距离不足被服务器拒掉。外汇与贵金属杠杆高,这类距离限制随品种波动,实盘前务必在 MT5 用具体品种核对 SYMBOL_TRADE_STOPS_LEVEL 数值。

MQL5 / C++
class="type">bool existLimits(){
  class="macro">#ifdef __MQL5__
    class="type">int cntMyPos=PositionsTotal();
    for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
      if(PositionGetSymbol(ti)!=_Symbol) class="kw">continue;
      if(EA_Magic>class="num">0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) class="kw">continue;
      class="kw">return true;
    }
    class="type">int cntMyPosO=OrdersTotal();
    for(class="type">int ti=cntMyPosO-class="num">1; ti>=class="num">0; ti--){
      class="type">ulong orderTicket=OrderGetTicket(ti);
      if(OrderGetString(ORDER_SYMBOL)!=_Symbol) class="kw">continue;
      if(EA_Magic>class="num">0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) class="kw">continue;
      class="kw">return true;
    }
  class="macro">#else
    class="type">int cntMyPos=OrdersTotal();
    if(cntMyPos>class="num">0){
      for(class="type">int ti=cntMyPos-class="num">1; ti>=class="num">0; ti--){
        if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) class="kw">continue;
        if(OrderSymbol()!=_Symbol) class="kw">continue;
        if(EA_Magic>class="num">0 && OrderMagicNumber()!=EA_Magic) class="kw">continue;
        class="kw">return true;
      }
    }
  class="macro">#endif
  class="kw">return false;
}
class="type">void initLimits(){
  class="type">class="kw">double curPrice;
  class="type">MqlTick lastme;
  SymbolInfoTick(_Symbol, lastme);
  if( lastme.bid==class="num">0 ){
    class="kw">return;
  }
  class="type">class="kw">double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
  curPrice=lastme.bid;
  for(class="type">uint i=class="num">0; i<maxLimits; i++){
    curPrice+=ST;
    if( curPrice-lastme.ask < minStop ) class="kw">continue;

◍ 挂单网格的距离与止损校验

这段逻辑在循环里批量铺设卖出止损单,核心是先取当前 ask 价,再按固定步长 ST 逐格下移挂价。每下一格都先减 ST,避免所有卖单挤在同一价位。 下单价与实时 bid 的落差若小于 minStop,直接 continue 跳过,防止触发经纪商最小止损距离限制导致报错。外汇与贵金属杠杆高,这种距离校验在剧烈波动时可能救你于大量 reject 订单之外。 pdxSendOrder 封装了 MY_SELLSTOP 类型、手数 Lot 与当前品种 _Symbol,返回失败也不在此处处理,调用方需自行补日志或重试。开 MT5 把 ST 和 minStop 打印出来比对点差,能立刻看出网格在哪些价位会失效。

MQL5 / C++
  class=class="str">"cmt">// 设置卖出订单
  curPrice=lastme.ask;
  for(class="type">uint i=class="num">0; i<maxLimits; i++){
    curPrice-=ST;
    if( lastme.bid-curPrice < minStop ) class="kw">continue;
    if(!pdxSendOrder(MY_SELLSTOP, curPrice, class="num">0, class="num">0, Lot, class="num">0, "", _Symbol)){
    }
  }
}

「用 MT5 回测网格 EA 的真实表现」

EA 写完后先不做优化,直接在 MT5 跑一轮基础测试。选 EURUSD、M5 周期、2018-08-01 到 2019-01-01,测试模式用 1 Minute OHLC,参数全保持默认:0.01 手、步长 10 点、每网格 7 单、获利 $1。 前几个月曲线平稳,累计盈利接近 $100,回撤控制在 $30 左右。转折出现在 2018-09-13 16:15 之后:价格先触发 1 个买单,接着激活 2 个卖单和 2 个买单,最后还留了 5 个卖单挂著。到 9 月 20 日价格拉回高位,剩余 4 个买单全部成交,账户变成 7 买 7 卖,多空完全锁死,获利功能失效。 后续价格再涨约 80 点——如果网格容量是 13 单,大概率能解套反赚;若放到 30 单,价格后来跌 200 点也理论上可盈利,但回撤会极其恐怖,且可能耗数月。把网格单数提到 13 和 20 验证:20 单时回撤约 $300,总利润仅略超 $100,策略没废但急需改。 顺手做次优化:网格单数 4–21、步长 10–20 点、获利不变 $1。结果步长 13 点、16 单最优;在「基于真实分时的每一分时」模式下 5 个月赚 $119、回撤 $221。外汇与贵金属网格策略属高风险,这种回撤利润比说明实盘前必须重构出场逻辑。

用亏损阈值强制换网格

双向挂单都被扫、浮亏持续放大时,死扛可能让回撤拖上数月。除了人工干预和叠加新网格,更硬的手段是给当前网格设一个美元亏损上限,触线就清掉全部持仓与挂单、重开一套网格——相当于认输一仗但保住本金。 具体落地只需在 EA 输入参数里加一个 takeLoss:填负数表示允许的最大亏损金额(例如 -500 即浮亏达到 500 美元就清盘)。原 checkTakeProfit() 只返回 true/false,现在改成返回所有同魔法码、同品种持仓的净盈亏合计(含 swap/库存费),MQL5 下用 PositionsTotal 遍历,MQL4 下退化为 OrdersTotal 兼容。 ontick() 里除了检查获利,再加一段:当 checkTakeProfit() 小于 takeLoss 时调用 closeAllPos() 平掉整网格。外汇与贵金属杠杆高,这种止损换网格能截断极端行情下的深套,但也可能在震荡市频繁重置、吃掉手续费,实盘前务必在 MT5 策略测试器用历史数据跑一遍确认触发频率。 下面这段是 MQL5 侧核心改动,黄底为变更点,直接复制到你的 EA 源码对照即可:

MQL5 / C++
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;takeLoss=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//如有亏损则平仓, $</span>
<span class="keyword" style="background-class="type">class="kw">color:rgb(class="num">255, class="num">221, class="num">0);">class="type">class="kw">double</span> checkTakeProfit(){
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> curProfit=<span class="number">class="num">0</span>;
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> profit=<span class="number">class="num">0</span>;
&nbsp;&nbsp; 
&nbsp;&nbsp; <span class="preprocessor">class="macro">#ifdef </span><span class="macro">__MQL5__</span> 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> cntMyPos=<span class="functions">PositionsTotal</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> ti=cntMyPos-<span class="number">class="num">1</span>; ti&gt;=<span class="number">class="num">0</span>; ti--){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(<span class="functions">PositionGetSymbol</span>(ti)!=<span class="predefines">_Symbol</span>) <span class="keyword">class="kw">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(EA_Magic&gt;<span class="number">class="num">0</span> &amp;&amp; <span class="functions">PositionGetInteger</span>(<span class="macro">POSITION_MAGIC</span>)!=EA_Magic) <span class="keyword">class="kw">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; profit+=<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_PROFIT</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; profit+=<span class="functions">PositionGetDouble</span>(<span class="macro">POSITION_SWAP</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; <span class="preprocessor">class="macro">#else
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span class="keyword">class="type">int</span> cntMyPos=<span class="functions">OrdersTotal</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(cntMyPos&gt;<span class="number">class="num">0</span>){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ti=cntMyPos-<span class="number">class="num">1</span>; ti&gt;=<span class="number">class="num">0</span>; ti--){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">OrderSelect</span>(ti,<span class="macro">SELECT_BY_POS</span>,<span class="macro">MODE_TRADES</span>)==<span class="macro">false</span>) <span class="keyword">class="kw">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>( <span class="functions">OrderType</span>()==<span class="macro">OP_BUY</span> || <span class="functions">OrderType</span>()==<span class="macro">OP_SELL</span> ){}<span class="keyword">else</span>{ <span class="keyword">class="kw">continue</span>; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">OrderSymbol</span>()!=<span class="predefines">_Symbol</span>) <span class="keyword">class="kw">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(EA_Magic&gt;<span class="number">class="num">0</span> &amp;&amp; <span class="functions">OrderMagicNumber</span>()!=EA_Magic) <span class="keyword">class="kw">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;profit+=<span class="functions">OrderCommission</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;profit+=<span class="functions">OrderProfit</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;profit+=<span class="functions">OrderSwap</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; <span class="preprocessor">class="macro">#endif 
&nbsp;&nbsp; </span><span class="keyword">class="kw">return</span> <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">221, class="num">0);">profit</span>;
}
&nbsp;&nbsp; <span class="keyword">if</span>(takeProfit&gt;<span class="number">class="num">0</span> &amp;&amp; checkTakeProfit()&gt;takeProfit){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;closeAllPos();
&nbsp;&nbsp; }<span class="keyword">else</span> <span class="keyword">if</span>(takeLoss&lt;<span class="number">class="num">0</span> &amp;&amp; checkTakeProfit()&lt;takeLoss){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;closeAllPos();
&nbsp;&nbsp; }

◍ 只调止损区间的实盘回测

把优化范围收窄到 -5 至 -100 美元的止损档位,其余参数沿用上一轮测试的结果:网格步长 13 点、单网格挂 16 张单。这样跑出来的目的很直接——验证前面那套改进到底有没有用。 回测里止损设在 -56 美元时利润最高,5 个月净赚 156 美元,最大回撤 83 美元。翻成交明细能看到,这 5 个月里止损只被触发过一次,所以利润回撤比看起来才这么漂亮。 但把时间拉长到过去五年再跑同一组参数,结果就难看了。可能额外的优化能救回来一些,但至少说明:拿网格策略做长期自动交易,指望‘浮亏仓位迟早会被后续单扛回来’这个念头本身是错的。外汇与贵金属杠杆高,这类马丁类网格在极端行情下爆仓风险极大,实盘前务必在 MT5 用历史数据自测。

「给网格单挂止损反而更脆」

把单独交易加上止损后,拿五年历史跑优化,曲线比裸网格好看一点:最优参数是止损140点、获利50点,并且若30天内当前网格没开出任何新仓,就强平旧网格重开。 最终净利351美元、最大回撤226美元,表面优于无止损版本。但拆开看,最后一次成交后未满30天就强制关网格的那些回合,全部是亏损;连撑过30天的批次,多数也以亏收场。 外汇与贵金属自动网格本身高风险,这组数字更像是样本巧合,不是可复用的规律。真要验证,自己在MT5里把止损参数从0拉到140点,对比 equity 曲线拐点。

画得少,看得清

跨 MT4 与 MT5 的网格 EA 已经跑通,代码层面双平台兼容这一目标算达成了。但几个月历史回测远远不够,除非你愿意每周手动重调参数,否则简单网格在实盘里大概率扛不住单边。 文中附带 griderEA.mq5(77.8 KB)与 ex5(112.85 KB)可供直接下载验证;评论区有用户提到震荡区间上下限挂网比裸网格更稳,回撤也可能更小。外汇与贵金属网格天然带高杠杆风险,盲用易爆。 复杂网格信号如 EURUSD/AUDUSD/AUDCAD 网格据称月利可达 100%,但那套逻辑不在本文范围内。下一篇才拆 ENUM_ORDER_TYPE 与马丁变体,这一节到此为止,剩下交给你自己开 MT5 拖一遍。

常见问题

在EA里用OBJ_BUTTON画一个按钮,绑定点击事件循环遍历持仓并平仓,按钮文字可实时回显总盈亏和总手数。
每次刷新时遍历持仓累加Profit和Volume,用ObjectSetString把字符串写到按钮标签,点差变动时也能看到。
可以,小布能读取你的EA代码并标出事件绑定和遍历平仓的漏洞,提醒你漏掉挂单或锁仓单的情况。
对锁后用OrderCloseBy让两单互抵平仓,不走市场平仓单,可省掉一次点差支出,但需注意平台是否允许。
写法差异明显:MT4用OrderClose/OrderDelete,MT5需用CTrade或PositionClose,双版本要分开封装函数。