通用的之字转向指标·进阶篇
(2/3)· 从收盘价版本到三类封装,解决慢、错、难调用的老痛点
收盘价版之字转向为何画歪了
直接把 high/low 版之字转向改调 close 数组,看似一步到位,实测却会画错(原文图 6 对照可见顶底错位)。根因在于:以最高价定顶时,那根 K 线在成型期内始终锁在顶部;换成收盘价定顶,收盘价随行情跳动,原最高价可能消失,同向确认新高/新低时触发了删旧点逻辑,于是新点没立住、旧点也被误删。 修复办法不复杂:极值位置本来就缓存在 LastHighBarBuffer 和 LastLowBarBuffer 里,只要在指标主循环、switch 之前把旧点按收盘价回填即可。这样旧顶底先复位,后续同向突破才不会把历史结构清掉。 外汇与贵金属波动快、跳空多,这类基于收盘价的转向指标在重大数据行情中仍可能滞后,仅作结构参考,不构成方向判定。
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 句柄值验证再挂实盘。
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 分钟图就能看方向缓冲区的实际翻转节奏。
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 对称处理空头段。
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 仅作结构参考,信号可能滞后,请先在策略测试器跑历史数据验证。
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();