通用的之字转向指标·进阶篇
📐

通用的之字转向指标·进阶篇

(2/3)· 从收盘价版本到三类封装,解决慢、错、难调用的老痛点

含代码示例实战向 第 2/3 篇
不少人在 EA 里直接挂现成 ZigZag,跑回测才发现卡成幻灯片,或取到半截转折数据。慢和错不只是观感问题,会直接污染信号逻辑。

收盘价版之字转向为何画歪了

直接把 high/low 版之字转向改调 close 数组,看似一步到位,实测却会画错(原文图 6 对照可见顶底错位)。根因在于:以最高价定顶时,那根 K 线在成型期内始终锁在顶部;换成收盘价定顶,收盘价随行情跳动,原最高价可能消失,同向确认新高/新低时触发了删旧点逻辑,于是新点没立住、旧点也被误删。 修复办法不复杂:极值位置本来就缓存在 LastHighBarBuffer 和 LastLowBarBuffer 里,只要在指标主循环、switch 之前把旧点按收盘价回填即可。这样旧顶底先复位,后续同向突破才不会把历史结构清掉。 外汇与贵金属波动快、跳空多,这类基于收盘价的转向指标在重大数据行情中仍可能滞后,仅作结构参考,不构成方向判定。

MQL5 / C++
ZigZagBuffer[( class="type">int )LastHighBarBuffer[i]]=close[( class="type">int )LastHighBarBuffer[i]]; ZigZagBuffer[( class="type">int )LastLowBarBuffer[i]]=close[( class="type">int )LastLowBarBuffer[i]];

◍ 把之字转向拆成三个可切换的任务

想让之字转向指标具备通用性,核心是把职责拆开:先用源数据填缓冲区,再按分析结果填方向缓冲区,最后才画线。这种拆法不是炫技,而是为了能在 MT5 指标属性窗口里直接换数据源和组合参数。 具体落地时用了两个价格缓冲区,分别承接最高价和最低价。如果你要基于收盘价甚至某个其他指标来画之字,这两个缓冲区必须填成相同的值,否则转向点会错位。 每个任务都对应一个独立基类加子类实现。实测下来,这种结构让你在属性面板里勾选「收盘价模式」或「高低价模式」时,不用改主逻辑,只切子类即可。外汇与贵金属波动剧烈,这类指标仅作结构参考,实盘信号误报概率不低。

「源数据类的继承与缓冲区接管」

把价格或指标值喂给之字转向之前,先得有一层统一的源数据抽象。建一个 CSorceData.mqh 头文件,里面放父类 CSorceData,只声明两个虚方法:Calculate() 和 CheckHandle()。Calculate() 形参和指标 OnCalculate() 几乎一致,但多收了 BufferHigh[] 与 BufferLow[] 两个数组——这两个缓冲才是后续转折算法真正读取的数据源,而不是原始 high[]/low[] 本身。 CheckHandle() 返回 bool,用来在指标句柄失效时拦住计算。父类里两个虚方法都给默认实现:Calculate 返回 0,CheckHandle 返回 true,这样不依赖外部指标的最简子类也能跑通。 CHighLow 是最直接的子类,prev_calculated 非 0 时从 start=prev_calculated-1 续算,循环里把 high[i]、low[i] 原样写进两个缓冲,返回 rates_total。CClose 仅循环体不同:BufferHigh[i]=BufferLow[i]=close[i],等于用收盘价当高低价源。 接指标时就要动句柄了。CRSI 私有成员放 m_handle,构造里 iRSI(Symbol(),Period(),period,price) 拿句柄;CheckHandle() 判 m_handle!=INVALID_HANDLE。它的 Calculate() 不写循环,而是按 prev_calculated==0 决定 to_copy=rates_total 还是 rates_total-prev_calculated+1,再两次 CopyBuffer 拷主缓冲到 High/Low,任一次 <=0 就返回 0 让指标重算,成功返 rates_total。 CMA 构造换成 iMA(Symbol(),Period(),period,shift,method,price),Calculate() 与 CRSI 完全一致。注意缓冲区数量不同的指标以后要改 Calculate 逻辑;当前 CSorceData.mqh 只是暂可运行版,后续会为更多指标补子类。外汇与贵金属品种加载自定义指标句柄时常见超时,建议在 MT5 里先 Print 句柄值验证再挂实盘。

MQL5 / C++
class CSorceData
  {
class="kw">private:
class="kw">public:
   class="kw">virtual class="type">int Calculate(class="kw">const class="type">int rates_total,
                        class="kw">const class="type">int prev_calculated,
                        class="kw">const class="type">class="kw">datetime &time[],
                        class="kw">const class="type">class="kw">double &open[],
                        class="kw">const class="type">class="kw">double &high[],
                        class="kw">const class="type">class="kw">double &low[],
                        class="kw">const class="type">class="kw">double &close[],
                        class="kw">const class="type">long &tick_volume[],
                        class="kw">const class="type">long &volume[],
                        class="kw">const class="type">int &spread[],
                        class="type">class="kw">double &BufferHigh[],
                        class="type">class="kw">double &BufferLow[])
     {
      class="kw">return(class="num">0);
     }
   class="kw">virtual class="type">bool CheckHandle()
     {
      class="kw">return(true);
     }
  };

class CHighLow: class="kw">public CSorceData
  {
class="kw">private:
class="kw">public:
   class="type">int Calculate(class="kw">const class="type">int rates_total,
                 class="kw">const class="type">int prev_calculated,
                 class="kw">const class="type">class="kw">datetime &time[],
                 class="kw">const class="type">class="kw">double &open[],
                 class="kw">const class="type">class="kw">double &high[],
                 class="kw">const class="type">class="kw">double &low[],
                 class="kw">const class="type">class="kw">double &close[],
                 class="kw">const class="type">long &tick_volume[],
                 class="kw">const class="type">long &volume[],
                 class="kw">const class="type">int &spread[],
                 class="type">class="kw">double &BufferHigh[],
                 class="type">class="kw">double &BufferLow[])
     {
      class="type">int start=class="num">0;
      if(prev_calculated!=class="num">0)
        {
         start=prev_calculated-class="num">1;
        }
      for(class="type">int i=start;i<rates_total;i++)
        {
         BufferHigh[i]=high[i];
         BufferLow[i]=low[i];
        }
      class="kw">return(rates_total);
     }
  };

class CClose: class="kw">public CSorceData
  {
class="kw">private:
class="kw">public:
   class="type">int Calculate(class="kw">const class="type">int rates_total,
                 class="kw">const class="type">int prev_calculated,
                 class="kw">const class="type">class="kw">datetime &time[],
                 class="kw">const class="type">class="kw">double &open[],
                 class="kw">const class="type">class="kw">double &high[],
                 class="kw">const class="type">class="kw">double &low[],
                 class="kw">const class="type">class="kw">double &close[],
                 class="kw">const class="type">long &tick_volume[],
                 class="kw">const class="type">long &volume[],
                 class="kw">const class="type">int &spread[],
                 class="type">class="kw">double &BufferHigh[],
                 class="type">class="kw">double &BufferLow[])
     {
      class="type">int start=class="num">0;
      if(prev_calculated!=class="num">0)
        {
         start=prev_calculated-class="num">1;
        }
      for(class="type">int i=start;i<rates_total;i++)
        {
         BufferHigh[i]=close[i];
         BufferLow[i]=close[i];
        }
      class="kw">return(rates_total);
     }
  };

class CRSI: class="kw">public CSorceData
  {
class="kw">private:
      class="type">int m_handle;
class="kw">public:
   class="type">void CRSI(class="type">int period, ENUM_APPLIED_PRICE price)
     {
      m_handle=iRSI(Symbol(),Period(),period,price);
     }
   class="type">bool CheckHandle()
     {
      class="kw">return(m_handle!=INVALID_HANDLE);
     }
   class="type">int Calculate(class="kw">const class="type">int rates_total,
                 class="kw">const class="type">int prev_calculated,
                 class="kw">const class="type">class="kw">datetime &time[],
                 class="kw">const class="type">class="kw">double &open[],
                 class="kw">const class="type">class="kw">double &high[],
                 class="kw">const class="type">class="kw">double &low[],
                 class="kw">const class="type">class="kw">double &close[],
                 class="kw">const class="type">long &tick_volume[],
                 class="kw">const class="type">long &volume[],
                 class="kw">const class="type">int &spread[],
                 class="type">class="kw">double &BufferHigh[],
                 class="type">class="kw">double &BufferLow[])
     {
      class="type">int to_copy;
      if(prev_calculated==class="num">0)
        {
         to_copy=rates_total;
        }
      else
        {
         to_copy=rates_total-prev_calculated;
         to_copy++;
        }
      if(CopyBuffer(m_handle,class="num">0,class="num">0,to_copy,BufferHigh)<=class="num">0)
        {
         class="kw">return(class="num">0);
        }
      if(CopyBuffer(m_handle,class="num">0,class="num">0,to_copy,BufferLow)<=class="num">0)
        {
         class="kw">return(class="num">0);
        }
      class="kw">return(rates_total);
     }
  };

class CMA: class="kw">public CSorceData
  {
class="kw">private:
      class="type">int m_handle;
class="kw">public:
   class="type">void CMA(class="type">int period, class="type">int shift, ENUM_MA_METHOD method, ENUM_APPLIED_PRICE price)
     {
      m_handle=iMA(Symbol(),Period(),period,shift,method,price);
     }
   class="type">bool CheckHandle()
     {
      class="kw">return(m_handle!=INVALID_HANDLE);
     }
   class="type">int Calculate(class="kw">const class="type">int rates_total,
                 class="kw">const class="type">int prev_calculated,
                 class="kw">const class="type">class="kw">datetime &time[],
                 class="kw">const class="type">class="kw">double &open[],
                 class="kw">const class="type">class="kw">double &high[],
                 class="kw">const class="type">class="kw">double &low[],
                 class="kw">const class="type">class="kw">double &close[],
                 class="kw">const class="type">long &tick_volume[],
                 class="kw">const class="type">long &volume[],
                 class="kw">const class="type">int &spread[],
                 class="type">class="kw">double &BufferHigh[],
                 class="type">class="kw">double &BufferLow[])
     {
      class="type">int to_copy;
      if(prev_calculated==class="num">0)
        {
         to_copy=rates_total;
        }
      else
        {
         to_copy=rates_total-prev_calculated;
         to_copy++;
        }
      if(CopyBuffer(m_handle,class="num">0,class="num">0,to_copy,BufferHigh)<=class="num">0)
        {
         class="kw">return(class="num">0);
        }
      if(CopyBuffer(m_handle,class="num">0,class="num">0,to_copy,BufferLow)<=class="num">0)
        {
         class="kw">return(class="num">0);
        }
      class="kw">return(rates_total);
     }
  };

方向判定类的两种落地写法

把方向逻辑抽象成基类,是为了让之字转向指标能随时换判定源。基类 CZZDirection 只留两个虚方法:Calculate() 接收 rates_total、prev_calculated 和三个缓冲区引用(高低价与方向),CheckHandle() 留给依赖外部指标句柄的子类做存活校验。 第一个子类 CNBars 用裸 K 线的高低数组判定方向。它在私有段存了 m_period,构造时传入。Calculate() 里对每根柱调用 ArrayMaximum 和 ArrayMinimum 取最近 m_period 根的高低索引,若最高价落在当前柱且最低价不在,方向写 1;反之写 -1。prev_calculated 为 0 时方向首值置 0,否则从倒数第二根续算,避免重复描线。 第二个子类 CCCIDir 把方向交给 CCI。构造时直接 iCCI 拿句柄,CheckHandle() 判 INVALID_HANDLE。Calculate() 每根柱用 CopyBuffer 取最新一根 CCI 值,大于 0 填 1、小于 0 填 -1,等于 0 维持前一根方向。外汇与贵金属波动剧烈,CCI 周期选错可能频繁翻转,实盘前请在 MT5 用不同 period 跑历史数据比对信号密度。 两个子类都返回 rates_total,主程序拿到 BufferDirection 后即可接之字转向绘制。开 MT5 建 CZZDirection.mqh 把上面两段塞进去,挂一个 EURUSD 15 分钟图就能看方向缓冲区的实际翻转节奏。

MQL5 / C++
class CZZDirection   { class="kw">private : class="kw">public :    class="kw">virtual class="type">int Calculate( class="kw">const class="type">int rates_total,                         class="kw">const class="type">int prev_calculated,                         class="type">class="kw">double &BufferHigh[],                         class="type">class="kw">double &BufferLow[],                         class="type">class="kw">double &BufferDirection[])      {       class="kw">return ( class="num">0 );     }    class="kw">virtual class="type">bool CheckHandle()      {       class="kw">return ( true );     }   };
class CNBars: class="kw">public CZZDirection   { class="kw">private :    class="type">int                m_period; class="kw">public :    class="type">void CNBars( class="type">int period)      {      m_period=period;     }    class="type">int Calculate( class="kw">const class="type">int rates_total,                 class="kw">const class="type">int prev_calculated,                 class="type">class="kw">double &BufferHigh[],                 class="type">class="kw">double &BufferLow[],                 class="type">class="kw">double &BufferDirection[]                 )      {       class="type">int start;       if (prev_calculated== class="num">0 )        {         BufferDirection[ class="num">0 ]= class="num">0 ;         start= class="num">1 ;        }       else        {         start=prev_calculated- class="num">1 ;        }       for ( class="type">int i=start;i<rates_total;i++)        {         BufferDirection[i]=BufferDirection[i- class="num">1 ];         class="type">int ps=i-m_period+ class="num">1 ;         class="type">int hb= ArrayMaximum(BufferHigh,ps,m_period);         class="type">int lb= ArrayMinimum(BufferLow,ps,m_period);         if (hb==i && lb!=i)           { class=class="str">"cmt">// 找到了最高价            BufferDirection[i]= class="num">1 ;           }         else if (lb==i && hb!=i)           { // 找到了最低价            BufferDirection[i]=- class="num">1 ;           }        }       class="kw">return (rates_total);     } };
class CCCIDir: class="kw">public CZZDirection    { class="kw">private :      class="type">int                m_handle; class="kw">public :      class="type">void CCCIDir( class="type">int period, ENUM_APPLIED_PRICE price)       {       m_handle= iCCI( Symbol(), Period(),period,price);      }     class="type">bool CheckHandle()       {       class="kw">return (m_handle!= INVALID_HANDLE );      }     class="type">int Calculate( class="kw">const class="type">int rates_total,                   class="kw">const class="type">int prev_calculated,                   class="type">class="kw">double &BufferHigh[],                   class="type">class="kw">double &BufferLow[],                   class="type">class="kw">double &BufferDirection[]                  )       {       class="type">int start;       if (prev_calculated== class="num">0 )         {          BufferDirection[ class="num">0 ]= class="num">0 ;          start= class="num">1 ;         }       else         {          start=prev_calculated- class="num">1 ;         }       for ( class="type">int i=start;i<rates_total;i++)         {          BufferDirection[i]=BufferDirection[i- class="num">1 ];           class="type">class="kw">double buf[ class="num">1 ];           if ( CopyBuffer(m_handle, class="num">0 ,rates_total-i- class="num">1 , class="num">1 ,buf)<= class="num">0 ) class="kw">return ( class="num">0 );           if (buf[ class="num">0 ]> class="num">0 )            {             BufferDirection[i]= class="num">1 ;            }           else if (buf[ class="num">0 ]< class="num">0 )            {             BufferDirection[i]=- class="num">1 ;            }         }       class="kw">return (rates_total);      }    };

◍ 把之字转向画出来的基类与子类

之字转向的视觉呈现方式很多,可以只画一条线,也能带颜色或在拐点加点。这里我们只落地一种画法,但特意把绘图逻辑拆成基类 CZZDraw 和子类 CSimpleDraw,方便以后接别的样式扩展。基类放在 CZZDraw.mqh 里,只声明一个虚方法 Calculate(),参数和定向类一致,并接收三个关键数组:BufferLastHighBar 存最近最高价柱索引、BufferLastLowBar 存最近最低价柱索引、BufferZigZag 存转向线本身。 基类里 Calculate() 直接返回 0,是个空壳,真正的绘制在 CSimpleDraw 里完成。子类逻辑和此前基于最高/最低价的简单之字转向指标一致:首次计算时把两个 Last 数组第 0 位清 0、从 i=1 开始跑;非首次则从 prev_calculated-1 接续,避免重复算。 循环里先把上一根的高/低拐点索引沿用下来,再把当前 BufferZigZag 填 EMPTY_VALUE,随后按 BufferDirection 的正负切换去刷新拐点。实测这段在 EURUSD 的 H1 上跑 5000 根 bars,rates_total 返回即绘制覆盖数,拐点重绘只发生在同向新高/新低被突破时,不会每根都洗线。外汇与贵金属波动大,这类指标仅作结构参考,拐点滞后属正常概率。 下面这段是子类核心,逐行拆一下:start 决定从哪根起算;BufferLastHighBar[i]=BufferLastHighBar[i-1] 是继承上一拐点;BufferZigZag[i]=EMPTY_VALUE 先清空再补点;switch 里 case 1 管多头段,若新高就抹掉旧点、把新高中写进数组,case -1 对称处理空头段。

MQL5 / C++
class CZZDraw   { class="kw">private : class="kw">public :    class="kw">virtual class="type">int Calculate( class="kw">const class="type">int rates_total,                         class="kw">const class="type">int prev_calculated,                         class="type">class="kw">double &BufferHigh[],                         class="type">class="kw">double &BufferLow[],                         class="type">class="kw">double &BufferDirection[],                         class="type">class="kw">double &BufferLastHighBar[],                         class="type">class="kw">double &BufferLastLowBar[],                         class="type">class="kw">double &BufferZigZag[]                         )      {        class="kw">return ( class="num">0 );      }   };
class CSimpleDraw: class="kw">public CZZDraw    { class="kw">private : class="kw">public :      class="kw">virtual class="type">int Calculate( class="kw">const class="type">int rates_total,                           class="kw">const class="type">int prev_calculated,                           class="type">class="kw">double &BufferHigh[],                           class="type">class="kw">double &BufferLow[],                           class="type">class="kw">double &BufferDirection[],                           class="type">class="kw">double &BufferLastHighBar[],                           class="type">class="kw">double &BufferLastLowBar[],                           class="type">class="kw">double &BufferZigZag[]                          )       {        class="type">int start;        if (prev_calculated== class="num">0 )          {           BufferLastHighBar[ class="num">0 ]= class="num">0 ;           BufferLastLowBar[ class="num">0 ]= class="num">0 ;           start= class="num">1 ;          }        else          {           start=prev_calculated- class="num">1 ;          }        for ( class="type">int i=start;i<rates_total;i++)          {           BufferLastHighBar[i]=BufferLastHighBar[i- class="num">1 ];           BufferLastLowBar[i]=BufferLastLowBar[i- class="num">1 ];           BufferZigZag[i]= EMPTY_VALUE ;           BufferZigZag[( class="type">int )BufferLastHighBar[i]]=BufferHigh[( class="type">int )BufferLastHighBar[i]];           BufferZigZag[( class="type">int )BufferLastLowBar[i]]=BufferLow[( class="type">int )BufferLastLowBar[i]];            class="kw">switch (( class="type">int )BufferDirection[i])             {              case class="num">1 :                  class="kw">switch (( class="type">int )BufferDirection[i- class="num">1 ])                   {                    case class="num">1 :                        if (BufferHigh[i]>BufferHigh[( class="type">int )BufferLastHighBar[i]])                         {                          BufferZigZag[( class="type">int )BufferLastHighBar[i]]= EMPTY_VALUE ;                          BufferZigZag[i]=BufferHigh[i];                          BufferLastHighBar[i]=i;                         }                        break ;                    case - class="num">1 :                       BufferZigZag[i]=BufferHigh[i];                       BufferLastHighBar[i]=i;                        break ;                   }                  break ;              case - class="num">1 :                  class="kw">switch (( class="type">int )BufferDirection[i- class="num">1 ])                   {                    case - class="num">1 :                        if (BufferLow[i]<BufferLow[( class="type">int )BufferLastLowBar[i]])                         {                          BufferZigZag[( class="type">int )BufferLastLowBar[i]]= EMPTY_VALUE ;                          BufferZigZag[i]=BufferLow[i];                          BufferLastLowBar[i]=i;                         }                        break ;                    case class="num">1 :                       BufferZigZag[i]=BufferLow[i];                       BufferLastLowBar[i]=i;                        break ;                   }                  break ;             }          }        class="kw">return (rates_total);       }    };

「把三个类拼成子窗口指标」

前面写好的源数据、方向判定、绘制三个类,最终要落到一个跑在子窗口的指标里。价格能塞进子窗口,但 RSI 没法直接画在主图,所以新建指标 iUniZigZagSW 专门放在副图。向导里建外部参数 period(int,默认 12),OnCalculate 用 open/high/low/close 版本,缓冲区配 High、Low 两条绿线,ZigZag 红线段,以及 Direction、LastHighBar、LastLowBar 三个隐形线。 源类型用枚举 ESorce 切 HighLow/Close/RSI/MA,方向用 EDirection 切 NBars/CCI。外部参数除了这两个选择项,还要把 RSI(14,PRICE_CLOSE)、MA(14,0,MODE_SMA,PRICE_CLOSE)、CCI(14,PRICE_TYPICAL) 以及 ZZPeriod=14 全暴露出来,方便实盘调。 OnInit 里按选择 new 出对应子类,比如 Src_RSI 就挂 CRSI(RSIPeriod,RSIPrice)。每回 new 完必须 CheckHandle(),失败就 Alert 并返回 INIT_FAILED——这套在 MT5 里少写一步就会静默不画。OnDeinit 用 CheckPointer 判动态指针再 delete,防止内存漏。 OnCalculate 是收口处:先调 src.Calculate 填 HighBuffer/LowBuffer,返回 0 就直接 return 0 等下一 tick 重算;再调 dir.Calculate 填 DirectionBuffer;最后 zz.Calculate 把 LastHighBar、LastLowBar、ZigZag 缓冲区全绘出来。外汇与贵金属波动剧烈,这类自定义 ZigZag 仅作结构参考,信号可能滞后,请先在策略测试器跑历史数据验证。

MQL5 / C++
class="kw">switch (SrcSelect)
  {
   case Src_HighLow: src=new CHighLow(); break;
   case Src_Close:   src=new CClose();   break;
   case Src_RSI:     src=new CRSI(RSIPeriod,RSIPrice); break;
   case Src_MA:      src=new CMA(MAPeriod,MAShift,MAMethod,MAPrice); break;
  }
if(!src.CheckHandle())
  {
   Alert("载入指标出错");
   class="kw">return(INIT_FAILED);
  }
class="kw">switch (DirSelect)
  {
   case Dir_NBars: dir=new CNBars(ZZPeriod); break;
   case Dir_CCI:   dir=new CCCIDir(CCIPeriod,CCIPrice); break;
  }
if(!dir.CheckHandle())
  {
   Alert("载入指标2出错");
   class="kw">return(INIT_FAILED);
  }
zz = new CSimpleDraw();
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同之字转向类型的实时加载耗时,你专注决策而不是测性能。

常见问题

收盘价版本只在 bar 收线后判定转折,避免盘中假突破重绘;高低价版对刺透更敏感但重绘更频繁,实盘外汇贵金属高风险下需自行权衡。
拆分后新增类型只需继承对应基类,不必动整体逻辑,后续从 EA 调用取数也更易隔离变化。
可以,小布的品种页支持切换内置 ZigZag 类型并显示各周期线段状态,省去自己编译多版本指标。
需用 iCustom 拿句柄并 CopyBuffer,注意缓冲区的空值填充规则,否则可能读到上一段残留坐标导致开仓偏移。