如何利用 MQL5 创建简单的多币种智能交易系统(第 7 部分):依据动量振荡器指标的之字折线·进阶篇
🧩

如何利用 MQL5 创建简单的多币种智能交易系统(第 7 部分):依据动量振荡器指标的之字折线·进阶篇

(2/3)· 28外汇对加黄金白银共30品种,如何用ZigZag接动量振荡器彼此过滤信号而不乱套

案例拆解新手友好 第 2/3 篇
不少人在 MT5 里硬抄单品种 ZigZag 逻辑去跑多币种,结果前缀后缀品种直接漏单或错单。把 30 对塞进一个 EA 却不做名称归一,回测和实盘会出现你根本查不到的幽灵成交。

◍ 止损止盈与跟踪价的自动算价逻辑

这段 MQL5 类方法把挂单/持仓的 SL、TP 和跟踪止损触发价都收敛到几个开关里:use_sl/use_tp 控制是否启用,autosl/autotp 控制用固定点数还是外部参数 SLval/TPval。外汇与贵金属波动大,自动算价前务必在策略测试器跑一遍,确认 NormalizePrice 后的点位符合经纪商止损距离限制,否则可能直接拒单。 自动 SL 对卖单取 atprice+38*pip(autosl=Yes),自动 TP 对买单取 atprice+50*pip、卖单取 atprice-50*pip。38 与 50 都是硬编码点数,想改风偏直接动这两个数字,不用碰整体结构。 跟踪止损 TSPrice() 用 byprice 模式时,多单跟踪价 = Bid()-TSval*pip,空单 = Ask()+TSval*pip,返回前同样走 NormalizePrice。实盘里 TSval 设太小容易被噪音扫掉,EURUSD 这类品种建议先试 15~30 pip 区间观察触发频率。

MQL5 / C++
class="type">class="kw">double MCEA::OrderSLSet(const class="type">class="kw">string xsymb,ENUM_ORDER_TYPE type,class="type">class="kw">double atprice)
  {
class=class="str">"cmt">//---
  slv=class="num">0.0;
  class="type">int x=PairsIdxArray(xsymb);
  Pips(xsymb);
  RefreshTick(xsymb);
  class=class="str">"cmt">//--
  class="kw">switch(type)
    {
     case (ORDER_TYPE_BUY):
      {
       if(use_sl==Yes && autosl==Yes) slv=mc_symbol.NormalizePrice(atprice-class="num">38*pip);
       else
       if(use_sl==Yes && autosl==No)  slv=mc_symbol.NormalizePrice(atprice-SLval*pip);
       else slv=class="num">0.0;
       class=class="str">"cmt">//--
       break;
      }
     case (ORDER_TYPE_SELL):
      {
       if(use_sl==Yes && autosl==Yes) slv=mc_symbol.NormalizePrice(atprice+class="num">38*pip);
       else
       if(use_sl==Yes && autosl==No)  slv=mc_symbol.NormalizePrice(atprice+SLval*pip);
       else slv=class="num">0.0;
      }
    }
  class=class="str">"cmt">//---
  class="kw">return(slv);
class=class="str">"cmt">//---
  } class=class="str">"cmt">//-end OrderSLSet()
class=class="str">"cmt">//---------//
class="type">class="kw">double MCEA::OrderTPSet(const class="type">class="kw">string xsymb,ENUM_ORDER_TYPE type,class="type">class="kw">double atprice)
  {
class=class="str">"cmt">//---
  tpv=class="num">0.0;
  class="type">int x=PairsIdxArray(xsymb);
  Pips(xsymb);
  RefreshTick(xsymb);
  class=class="str">"cmt">//--
  class="kw">switch(type)
    {
     case (ORDER_TYPE_BUY):
      {
       if(use_tp==Yes && autotp==Yes) tpv=mc_symbol.NormalizePrice(atprice+class="num">50*pip);
       else
       if(use_tp==Yes && autotp==No)  tpv=mc_symbol.NormalizePrice(atprice+TPval*pip);
       else tpv=class="num">0.0;
       class=class="str">"cmt">//--
       break;
      }
     case (ORDER_TYPE_SELL):
      {
       if(use_tp==Yes && autotp==Yes) tpv=mc_symbol.NormalizePrice(atprice-class="num">50*pip);
       else
       if(use_tp==Yes && autotp==No)  tpv=mc_symbol.NormalizePrice(atprice-TPval*pip);
       else tpv=class="num">0.0;
      }
    }
  class=class="str">"cmt">//---
  class="kw">return(tpv);
class=class="str">"cmt">//---
  } class=class="str">"cmt">//-end OrderTPSet()
class=class="str">"cmt">//---------//
class="type">class="kw">double MCEA::TSPrice(const class="type">class="kw">string xsymb,class="type">ENUM_POSITION_TYPE ptype,class="type">int TS_type)
  {
class=class="str">"cmt">//---
  class="type">int br=class="num">2;
  class="type">class="kw">double pval=class="num">0.0;
  class="type">int x=PairsIdxArray(xsymb);
  Pips(xsymb);
  class=class="str">"cmt">//--
  class="kw">switch(TS_type)
    {
     case byprice:
      {
       RefreshTick(xsymb);
       if(ptype==POSITION_TYPE_BUY)  pval=mc_symbol.NormalizePrice(mc_symbol.Bid()-TSval*pip);
       if(ptype==POSITION_TYPE_SELL) pval=mc_symbol.NormalizePrice(mc_symbol.Ask()+TSval*pip);
       break;
      }
     case byindi:
      {

「追踪止损的两种触发逻辑与挂单改SL实现」

这段逻辑展示了追踪止损价位(TSPrice)在两种模式下的取法:一种是引用外部指标缓冲区 VIDyAv,另一种是直接用最近两根K线的高低点。外汇与贵金属杠杆高,回测前先在策略测试器确认品种点值 pip 计算无误,否则 SL 可能偏离预期。 VIDyAv 模式里,多头持仓时若指标当前值低于「买价 - TSval×pip」的归一化价格,就把 pval 设为指标值;空头则反过来,指标值高于「卖价 + TSval×pip」才采纳。这等于把 trailing stop 的锚点交给指标曲线,而不是固定点数。 byHiLo 模式不依赖指标:多头看 HIGH[0]>HIGH[1] 就把前一根低点 LOW[1] 当止损位,空头看 LOW[0]<LOW[1] 取 HIGH[1]。代码只取了 2 根 Bar(UpdatePrice 参数 2),意味着它只用最新两根K线做突破判定。 ModifyOrdersSL 负责真正改单:遍历持仓,筛选同品种且 magic 匹配的单子,对 BUY 单取 TSPrice 算出的 vtrsb 作为新 SL 候选。注意 price=PriceCurrent() 与 vtrsb 是分开取的,实盘里若 tick 跳变快,二者可能有微小时差,建议在 MT5 用真实点差品种跑一遍观察。

MQL5 / C++
class="type">class="kw">double VIDyAv[];
ArrayResize(VIDyAv,br,br);
ArraySetAsSeries(VIDyAv,true);
CopyBuffer(hVIDyAv[x],class="num">0,class="num">0,br,VIDyAv);
RefreshPrice(xsymb,TFt,br);
class=class="str">"cmt">//--
if(ptype==POSITION_TYPE_BUY  && (VIDyAv[class="num">0]<mc_symbol.NormalizePrice(mc_symbol.Bid()-TSval*pip)))
   pval=VIDyAv[class="num">0];
if(ptype==POSITION_TYPE_SELL && (VIDyAv[class="num">0]>mc_symbol.NormalizePrice(mc_symbol.Ask()+TSval*pip)))
   pval=VIDyAv[class="num">0];
break;
      case byHiLo:
      {
      UpdatePrice(xsymb,TFt,class="num">2);
      class=class="str">"cmt">//--
      if(ptype==POSITION_TYPE_BUY  && (HIGH[class="num">0]>HIGH[class="num">1]))
         pval=LOW[class="num">1];
      if(ptype==POSITION_TYPE_SELL && (LOW[class="num">0]<LOW[class="num">1]))
         pval=HIGH[class="num">1];
      break;
      }
   }
class=class="str">"cmt">//--
class="kw">return(pval);
class=class="str">"cmt">//---
  } class=class="str">"cmt">//-end TSPrice()
class=class="str">"cmt">//---------//
class="type">bool MCEA::ModifyOrdersSL(const class="type">class="kw">string symbx,class="type">int TS_type)
  {
class=class="str">"cmt">//---
   ResetLastError();
   class="type">MqlTradeRequest req={};
   class="type">MqlTradeResult  res={};
   MqlTradeCheckResult check={};
   class=class="str">"cmt">//--
   class="type">int TRSP=TS_type;
   class="type">bool modist=false;
   class="type">int x=PairsIdxArray(symbx);
   Pips(symbx);
   class=class="str">"cmt">//--
   class="type">int total=PositionsTotal();
   class=class="str">"cmt">//--          
   for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
    {
      class="type">class="kw">string symbol=PositionGetSymbol(i);
      if(symbol==symbx && mc_position.Magic()==magicEA)
        {
          class="type">ENUM_POSITION_TYPE opstype = mc_position.PositionType();
          if(opstype==POSITION_TYPE_BUY)
            {
              RefreshTick(symbol);
              class="type">class="kw">double price = mc_position.PriceCurrent();
              class="type">class="kw">double vtrsb = mc_symbol.NormalizePrice(TSPrice(symbx,opstype,TRSP));
              class="type">class="kw">double pos_open   = mc_position.PriceOpen();

多单与空单的止损推进判定

这段逻辑处理持仓止损向有利方向移动的具体条件,分多单和空单两套对称判断。核心是先算出净收益 netp(浮动盈利+隔夜利息+佣金),只有 netp 大于预设 minprofit 才允许改单,避免微利就移动止损徒增摩擦成本。 多单分支里,modstart 以开仓价加 TSmin 个 pip 作为初始参考位,modminsl 用追踪基准 vtrsb 再加(TSmin-1)pip 算出允许修改的下限。只有当当前价大于 modminsl、且目标买止损 modbuysl 高于 modstart、同时原止损为空或新止损更优时,modbuy 才为真。 空单分支镜像处理:modstart 为开仓价减 TSmin*pip,modminsl 用 vtrss 减去(TSmin+1)pip。判定 modsel 要求现价低于 modminsl、新卖止损低于 modstart 且优于原止损。两个条件均满足 netp>minprofit 后,调用 PositionModify 仅改止损、保留原止盈。 外汇与贵金属杠杆高,这类自动推损逻辑在快速反转中可能连续改单失效,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍,观察 minprofit 设 5 美元与 20 美元时成交次数差异。

MQL5 / C++
class="type">class="kw">double pos_stop   = mc_position.StopLoss();
class="type">class="kw">double pos_tp     = mc_position.TakeProfit();
class="type">class="kw">double pos_profit = mc_position.Profit();
class="type">class="kw">double pos_swap   = mc_position.Swap();
class="type">class="kw">double pos_comm   = mc_position.Commission();
class="type">class="kw">double netp=pos_profit+pos_swap+pos_comm;
class="type">class="kw">double modstart=mc_symbol.NormalizePrice(pos_open+TSmin*pip);
class="type">class="kw">double modminsl=mc_symbol.NormalizePrice(vtrsb+((TSmin-class="num">1.0)*pip));
class="type">class="kw">double modbuysl=vtrsb;
class="type">bool modbuy = (price>modminsl && modbuysl>modstart && (pos_stop==class="num">0.0||modbuysl>pos_stop));
class=class="str">"cmt">//--
if(modbuy && netp>minprofit)
  {
   modist=mc_trade.PositionModify(symbol,modbuysl,pos_tp);
  }  
 }
if(opstype==POSITION_TYPE_SELL)
  {
   RefreshTick(symbol);
   class="type">class="kw">double price = mc_position.PriceCurrent();
   class="type">class="kw">double vtrss = mc_symbol.NormalizePrice(TSPrice(symbx,opstype,TRSP));
   class="type">class="kw">double pos_open   = mc_position.PriceOpen();
   class="type">class="kw">double pos_stop   = mc_position.StopLoss();
   class="type">class="kw">double pos_tp     = mc_position.TakeProfit();
   class="type">class="kw">double pos_profit = mc_position.Profit();
   class="type">class="kw">double pos_swap   = mc_position.Swap();
   class="type">class="kw">double pos_comm   = mc_position.Commission();
   class="type">class="kw">double netp=pos_profit+pos_swap+pos_comm;
   class="type">class="kw">double modstart=mc_symbol.NormalizePrice(pos_open-TSmin*pip);
   class="type">class="kw">double modminsl=mc_symbol.NormalizePrice(vtrss-((TSmin+class="num">1.0)*pip));
   class="type">class="kw">double modselsl=vtrss;
   class="type">bool modsel = (price<modminsl && modselsl<modstart && (pos_stop==class="num">0.0||modselsl<pos_stop));
class=class="str">"cmt">//--
   if(modsel && netp>minprofit)
     {
      modist=mc_trade.PositionModify(symbol,modselsl,pos_tp);
     }  
   }
  }
 }
class=class="str">"cmt">//--
class="kw">return(modist);

◍ 盈利后把止盈推到近端

这段逻辑只干一件事:当多单或空单已经浮盈,且原止盈距离当前价不足 TPmin 个 pip 时,把 TakeProfit 改成贴近市价的近端止盈,锁住部分空间。外汇与贵金属波动快,这种动态平移大概率能减少回吐,但仍是高风险操作,不保证胜率。 核心判断在多单分支:price>pos_open 且 (原TP到现价差 < TPmin*pip) 且 原TP < 新近端TP 时才改。空单对称处理,方向反号。 实盘验证建议:把 TPmin 设成 15~30 pip 区间,在 XAUUSD 的 M5 上跑一周,观察改单后是否被扫在近端而非原 TP。若频繁过早离场,说明 TPmin 偏小。

MQL5 / C++
class="type">bool MCEA::ModifyOrdersTP(const class="type">class="kw">string symbx)
  {
class=class="str">"cmt">//---
  ResetLastError();
  class="type">MqlTradeRequest req={};
  class="type">MqlTradeResult  res={};
  MqlTradeCheckResult check={};
  class=class="str">"cmt">//--
  class="type">bool modist=false;
  class="type">int x=PairsIdxArray(symbx);
  Pips(symbx);
  class=class="str">"cmt">//--
  class="type">int total=PositionsTotal();
  class=class="str">"cmt">//--        
  for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
    {
      class="type">class="kw">string symbol=PositionGetSymbol(i);
      if(symbol==symbx && mc_position.Magic()==magicEA)
        {
          class="type">ENUM_POSITION_TYPE opstype = mc_position.PositionType();
          if(opstype==POSITION_TYPE_BUY)
            {
              RefreshTick(symbol);
              class="type">class="kw">double price     = mc_position.PriceCurrent();
              class="type">class="kw">double pos_open = mc_position.PriceOpen();
              class="type">class="kw">double pos_stop = mc_position.StopLoss();
              class="type">class="kw">double pos_tp   = mc_position.TakeProfit();
              class="type">class="kw">double modbuytp = pos_tp==class="num">0.0 ? mc_symbol.NormalizePrice(pos_open+TPmin*pip) : pos_tp;
              class="type">class="kw">double modpostp = mc_symbol.NormalizePrice(price+TPmin*pip);
              class="type">bool modtpb = (price>pos_open && modbuytp-price<TPmin*pip && pos_tp<modpostp);
              class=class="str">"cmt">//--
              if(modtpb)
                {
                  modist=mc_trade.PositionModify(symbol,pos_stop,modpostp);
                }  
            }
          if(opstype==POSITION_TYPE_SELL)
            {
              RefreshTick(symbol);
              class="type">class="kw">double price     = mc_position.PriceCurrent();
              class="type">class="kw">double pos_open = mc_position.PriceOpen();
              class="type">class="kw">double pos_stop = mc_position.StopLoss();
              class="type">class="kw">double pos_tp   = mc_position.TakeProfit();
              class="type">class="kw">double modseltp = pos_tp==class="num">0.0 ? mc_symbol.NormalizePrice(pos_open-TPmin*pip) : pos_tp;
              class="type">class="kw">double modpostp = mc_symbol.NormalizePrice(price-TPmin*pip);
              class="type">bool modtps = (price<pos_open && price-modseltp<TPmin*pip && pos_tp>modpostp);
              class=class="str">"cmt">//--
              if(modtps)
                {

「反向信号触发后的卖单平仓逻辑」

当面板开关 Close_by_Opps 设为 Yes 且空头机会计数 mc.xos[x] 大于 0 时,EA 会调用 CloseSellPositions 把当前符号下的空单全平掉。这段实现直接遍历 PositionsTotal() 拿到的持仓总数,从尾到头倒序索引,避免平仓后仓位重排导致漏选。 函数里先固化 closetype = POSITION_TYPE_SELL、type_req = ORDER_TYPE_BUY,再用 mc_position.SelectByIndex(i) 逐仓选中。只有 position_Symbol 与传入 symbol 一致、且 Magic 等于 magicEA 的仓位才进入处理,防止误伤手动单或其他 EA 的仓。 命中空单后,RefreshTick 先刷最新报价,再用 mc_trade.PositionClose(position_Symbol, slip) 以允许滑点 slip 市价回购平仓,并通过 PrintFormat 打出 '#ticket 符号 类型' 的日志便于回放。返回值 sellclose 只反映最后一次平仓调用是否成功,多次平仓时前面失败不会被覆盖记录。 对称地,mc.xob[x] > 0 时走 CloseBuyPositions,结构完全一致,只是 closetype 换成 POSITION_TYPE_BUY。外汇与贵金属杠杆高,这类按信号批量反手平仓在跳空行情可能触发大滑点,实盘前建议在 MT5 策略测试器用真实点差回测确认。

MQL5 / C++
if(Close_by_Opps==Yes && mc.xos[x]>class="num">0) mc.CloseSellPositions(symbol);
class="type">bool MCEA::CloseSellPositions(const class="type">class="kw">string symbol)
  {
  class=class="str">"cmt">//---
  ResetLastError();
  class="type">bool sellclose=false;
  class="type">int total=PositionsTotal(); class=class="str">"cmt">// number of open positions
  class="type">ENUM_POSITION_TYPE closetype = POSITION_TYPE_SELL;
  ENUM_ORDER_TYPE      type_req = ORDER_TYPE_BUY;
  class=class="str">"cmt">//--
  class="type">MqlTradeRequest req={};
  class="type">MqlTradeResult  res={};
  MqlTradeCheckResult check={};
  class=class="str">"cmt">//--
  class="type">int x=PairsIdxArray(symbol);
  class=class="str">"cmt">//--- iterate over all open positions
  for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
    {
    if(mc_position.SelectByIndex(i))
      {
      class=class="str">"cmt">//--- Parameters of the order
      class="type">class="kw">string position_Symbol   = PositionGetSymbol(i);
      class="type">ulong  position_ticket   = PositionGetTicket(i);
      class="type">ENUM_POSITION_TYPE   type = mc_position.PositionType();
      class=class="str">"cmt">//--- if the MagicNumber matches
      if((position_Symbol==symbol) && (mc_position.Magic()==magicEA))
        {
        class=class="str">"cmt">//--
        if(type==closetype)
          {
          RefreshTick(position_Symbol);
          sellclose=mc_trade.PositionClose(position_Symbol,slip);
          class=class="str">"cmt">//--- output information about the closure
          PrintFormat("Close Sell #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type));
          }
        }
      }
    }
  class=class="str">"cmt">//---
  class="kw">return(sellclose);
  } class=class="str">"cmt">//-end CloseSellPositions()
if(Close_by_Opps==Yes && mc.xob[x]>class="num">0) mc.CloseBuyPositions(symbol);
class="type">bool MCEA::CloseBuyPositions(const class="type">class="kw">string symbol)
  {
class=class="str">"cmt">//---
  ResetLastError();
  class="type">bool buyclose=false;

用持仓遍历把多单平掉

这段逻辑干的事很直接:拿到当前账户所有未平仓位,挨个筛符号与魔法码,命中后把多单按市价平仓。外汇与贵金属杠杆高,批量平仓若滑点设得太小,实盘可能以偏离预期的价格成交,仓位越大偏离越明显。 代码里 total=PositionsTotal() 先取总仓数,循环从 i=total-1 倒序走到 0,避免平仓后索引错位。mc_position.SelectByIndex(i) 选中第 i 仓,再用 PositionGetSymbol(i)PositionGetTicket(i) 取品种与ticket。 只有当 position_Symbol==symbolmc_position.Magic()==magicEA 才继续,这是 EA 自管自仓的基本围栏。类型判断 type==closetypeclosetype=POSITION_TYPE_BUY 时,调 RefreshTick 刷新报价后执行 mc_trade.PositionClose(position_Symbol,slip),并返回 buyclose 标记。 另有一段补在后面的检查:Close_by_Opps==No 且同品种开仓多空合计 mc.xob[x]+mc.xos[x]>1 时,跑 CheckProfitLoss 看是否触损,真触了就 Do_Alerts 弹“Close order due stop in loss.”。把 slip 调成 2~3 个点,开 MT5 策略测试器用欧美 1 分钟复盘,能看出平仓成交价和请求价的差。

MQL5 / C++
class="type">int total=PositionsTotal(); class=class="str">"cmt">// number of open positions
class="type">ENUM_POSITION_TYPE closetype = POSITION_TYPE_BUY;
ENUM_ORDER_TYPE      type_req = ORDER_TYPE_SELL;
class=class="str">"cmt">//--
class="type">MqlTradeRequest req={};
class="type">MqlTradeResult   res={};
MqlTradeCheckResult check={};
class=class="str">"cmt">//--
class="type">int x=PairsIdxArray(symbol);
class=class="str">"cmt">//--- iterate over all open positions
for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
  {
  if(mc_position.SelectByIndex(i))
    {
    class=class="str">"cmt">//--- Parameters of the order
    class="type">class="kw">string position_Symbol   = PositionGetSymbol(i);
    class="type">ulong  position_ticket   = PositionGetTicket(i);
    class="type">ENUM_POSITION_TYPE   type = mc_position.PositionType();
    class=class="str">"cmt">//--- if the MagicNumber matches
    if((position_Symbol==symbol) && (mc_position.Magic()==magicEA))
      {
      class=class="str">"cmt">//--
      if(type==closetype)
        {
        RefreshTick(position_Symbol);
        buyclose=mc_trade.PositionClose(position_Symbol,slip);
        class=class="str">"cmt">//--- output information about the closure
        PrintFormat("Close Buy #%I64d %s %s",position_ticket,position_Symbol,EnumToString(type));
        }
      }
    }
  }
 class=class="str">"cmt">//---
 class="kw">return(buyclose);
class=class="str">"cmt">//----
 } class=class="str">"cmt">//-end CloseBuyPositions()
class=class="str">"cmt">//---------//
            class=class="str">"cmt">//--
            mc.CheckOpenPMx(symbol);
            if(Close_by_Opps==No && (mc.xob[x]+mc.xos[x]>class="num">1))
              {
              if(mc.CheckProfitLoss(symbol))
                mc.Do_Alerts(symbol,"Close order due stop in loss.");
              }
            class=class="str">"cmt">//--
class="type">bool MCEA::CheckProfitLoss(const class="type">class="kw">string symbol)
  {
class=class="str">"cmt">//----
   ResetLastError();
   class=class="str">"cmt">//--
   class="type">bool closeinloss=false;
   class="type">class="kw">string isloss="due stop in loss.";
   class=class="str">"cmt">//--
   class="type">int xx=PairsIdxArray(symbol);
   class=class="str">"cmt">//--
交给小布盯盘看盘口
这些多品种信号状态小布盯盘的 AIGC 已内置,打开对应品种页即可看到哪些对触发了 ZigZag 动量过滤,你只管判强弱。

常见问题

需写一个归一化函数剥离经纪商前缀后缀,仅针对外汇与 XAUUSD、XAGUSD 等金属对生效,指数和特殊品种不在范围内。
可以,输入里切到 Single Pair 模式后 EA 只在其所挂图表品种上交易,不需另写独立程序。
能,小布盯盘对应品种页已内置该类多品种诊断视图,省去你自己逐对挂指标的重复劳动。
两种都支持,文中方案允许用动量振荡器过滤 ZigZag 信号,也允许两类指标彼此交叉过滤,具体看输入切换。
外汇贵金属杠杆高、点差跳空频繁,全品种同策略在极端行情可能同步回撤,实盘前需用策略测试器分段验证。