开发基于振荡器的之字折线 (ZigZag) 指标。 执行需求规范的示例·进阶篇
(2/3)·从规范拆解到图形界面,手把手把振荡器ZigZag指标的开发坑位走通
把之字折线极值缓存起来再谈形态
检测形态前,得先拿到最近 5 个之字折线极值的端点。若每次都在指标缓冲里全量扫描,速度会塌;MT5 里指标响应慢直接影响盯盘体验。务实做法是用独立结构数组存新出现的极值,随取随用。 结构只留四个字段:柱线索引 bar、数值 val、方向 dir,以及布尔 pat(标记该极值是否处在已识别形态末端,用来给之字折线换色区分)。声明如代码段首部的 SZZDot 与 ZZDot[]。 极值不是实时搜出来的,而是在四段主逻辑末尾调 AddZZDot() 写入。传入方向、价格、极值所在 bar 和计算 bar;已占用元素数由 cnt[] 缓冲区记录,OnInit 里用 SetIndexBuffer(8,cnt,INDICATOR_CALCULATIONS) 绑定,indicator_buffers 改到 9。 别用缩减数组来删极值。同一根 bar 重算时,之前判出的逆转可能消失,这时只把 cnt[i] 计数器顺移并减 1,比 ArrayResize 廉价得多——这是该阶段提速的关键。 AddZZDot() 首次启动或整段重算时把数组开 1024 大小,写首元素并 cnt 置 1;后续调用先看末元素方向:同向就覆写末极值,反向才追加。注意若 c 触顶会按 1024 块扩容。 之字折线逆转时,反向末极值可能回退到更早 bar。所以在循环末尾把预知的极值先塞进 ZZDot 末位,新极值只在数组里更新,逆转了旧值也不丢。 攒齐数据后,只有 ZZDot 占用数 ≥5 才进形态判断:取末元素索引,无形态就给之字折线返中性色,有形态循环即停。上行涂色逻辑和清理近似,但 j=0 不跳出。阶段产物见 OscZigZagStep2.mq5,图例 7 为渲染效果。
class="kw">struct SZZDot{ class="type">int bar; class="type">class="kw">double val; class="type">int dir; class="type">bool pat; }; SZZDot ZZDot[]; if(dir[i]==class="num">1){ if(dir[i-class="num">1]==-class="num">1){ class=class="str">"cmt">//... AddZZDot(class="num">1,high[hb],hb,i); } else{ if(high[i]>ZigZagBuffer[(class="type">int)lhb[i]]){ class=class="str">"cmt">//... AddZZDot(class="num">1,high[i],i,i); } } } else if(dir[i]==-class="num">1){ if(dir[i-class="num">1]==class="num">1){ class=class="str">"cmt">//... AddZZDot(-class="num">1,low[lb],lb,i); } else{ if(low[i]<ZigZagBuffer[(class="type">int)llb[i]]){ class=class="str">"cmt">//... AddZZDot(-class="num">1,low[i],i,i); } } } class="type">class="kw">double cnt[]; SetIndexBuffer(class="num">8,cnt,INDICATOR_CALCULATIONS); class="macro">#class="kw">property indicator_buffers class="num">9 cnt[i]=cnt[i-class="num">1]; class="type">void AddZZDot(class="type">int d,class="type">class="kw">double v,class="type">int b,class="type">int i){ class="type">int c=(class="type">int)cnt[i]; if(c==class="num">0){ class=class="str">"cmt">// 在指标启动或重新计算完毕期间 ArrayResize(ZZDot,class="num">1024); ZZDot[c].dir=d; ZZDot[c].val=v; ZZDot[c].bar=b; ZZDot[c].pat=class="kw">false; cnt[i]=class="num">1; } else{ if(ZZDot[c-class="num">1].dir==d){ class=class="str">"cmt">// 更新同向极值 ZZDot[c-class="num">1].val=v; ZZDot[c-class="num">1].bar=b; } else{ class=class="str">"cmt">// 添加新的极值 class=class="str">"cmt">// 必要时,将数组增加 class="num">1024 个元素块 if(c>=ArraySize(ZZDot)){ ArrayResize(ZZDot,c+class="num">1024); } class=class="str">"cmt">// 添加新的极值 ZZDot[c].dir=d; ZZDot[c].val=v; ZZDot[c].bar=b;
「之字转向里的五点形态判定」
这段逻辑干的事,是在 ZigZag 点累积到至少 5 个之后,回头检查最近一段是否构成标准的高低-高低(或低高-低高)交替结构,并给对应 K 线打标记。
先看到 cnt[i]>=5 的分支:取最新点索引 li,先把 ZZDot[li].pat 置 false,再往前扫 4 个点,只要中间某个点已经是 pat 为真就 break,并把那几个点的 ZigZagColors 先清成 0。这一步是在重置旧着色,避免上一轮形态残留。
向上判定要求 li 高于 li-2、li-2 高于 li-4,同时 li-1 高于 li-3——也就是偶数点逐级抬、奇数点也抬,符合多头推进。向下则全部取反。命中后 ZZDot[li].pat=true,随后第二段循环把最近 4 个点(遇到中间已 pat 则断)的 ZigZagColors 写成 1,完成可视化着色。
实盘里把 cnt[i]>=5 这个阈值调到 7,能过滤掉部分四波假突破,但会迟一步确认;外汇与贵金属波动跳变多,这类形态识别仅作概率参考,杠杆品种风险偏高,请先在 MT5 策略测试器跑历史段验证。
ZZDot[c].pat=class="kw">false; cnt[i]=c+class="num">1; } } } if(cnt[i]>class="num">0){ class="type">int ub=(class="type">int)cnt[i]-class="num">1; if(ZZDot[ub].dir==class="num">1){ ZZDot[ub].bar=(class="type">int)lhb[i]; ZZDot[ub].val=high[(class="type">int)lhb[i]]; } else{ ZZDot[ub].bar=(class="type">int)llb[i]; ZZDot[ub].val=low[(class="type">int)llb[i]]; } } if(prev_calculated==class="num">0){ class=class="str">"cmt">//... cnt[class="num">0]=class="num">0; } else{ start=prev_calculated-class="num">1; } if(cnt[i]>=class="num">5) class="type">int li=(class="type">int)cnt[i]-class="num">1; ZZDot[li].pat=class="kw">false; for(class="type">int j=class="num">0;j<class="num">4;j++){ if(ZZDot[li-j].pat){ break; } ZigZagColors[ZZDot[li-j].bar]=class="num">0; } if(ZZDot[li].dir==class="num">1){ class=class="str">"cmt">// 向上 if( ZZDot[li].val>ZZDot[li-class="num">2].val && ZZDot[li-class="num">2].val>ZZDot[li-class="num">4].val && ZZDot[li-class="num">1].val>ZZDot[li-class="num">3].val ){ ZZDot[li].pat=true; class=class="str">"cmt">// 涂色 } } else{ class=class="str">"cmt">// 向下 if( ZZDot[li].val<ZZDot[li-class="num">2].val && ZZDot[li-class="num">2].val<ZZDot[li-class="num">4].val && ZZDot[li-class="num">1].val<ZZDot[li-class="num">3].val ){ ZZDot[li].pat=true; class=class="str">"cmt">// 涂色 } } for(class="type">int j=class="num">0;j<class="num">4;j++){ if(j!=class="num">0 && ZZDot[li-j].pat){ break; } ZigZagColors[ZZDot[li-j].bar]=class="num">1; }
◍ 把振荡器塞进指标属性面板
给指标加一个外部枚举,交易者就能在 MT5 属性窗口里直接挑振荡器,不用改代码重编译。下面这段声明把 WPR、CCI、Chaikin、RSI、Stochastic 五个选项暴露成下拉框,默认落在 WPR。 CCI 默认周期 14、典型价、上下界 ±100;RSI 也是 14 周期但用收盘价、边界 70/30;Stochastic 的 K=5、D=3、慢速 3,边界 80/20。Chaikin 稍重,快线 3、慢线 10、EMA 平滑、Tick 量,边界 ±1000。这些数字直接决定后面 ZigZag 拐点的触发灵敏度。 OnStart 里用 switch(Type) 把选中的振荡器句柄和上下界赋给通用变量 max、min,之后 OnCalculate 里一律拿 max/min 替换原先写死的 WPRmax/WPRmin。改完这步,附件里的 OscZigZagStep3.mq5 就能在图形界面切振荡器跑——外汇和贵金属波动大,切换后建议先上 M1 观察假突破概率,别直接挂实盘。
enum EIType{ WPR, CCI, Chaikin, RSI, Stochastic }; class="kw">input EIType Type = WPR; class=class="str">"cmt">// CCI class="kw">input class="type">int CCIperiod = class="num">14; class="kw">input ENUM_APPLIED_PRICE CCIprice = PRICE_TYPICAL; class="kw">input class="type">class="kw">double CCImax = class="num">100; class="kw">input class="type">class="kw">double CCImin = -class="num">100; class=class="str">"cmt">// Chaikin class="kw">input class="type">int CHfperiod = class="num">3; class="kw">input class="type">int CHsperiod = class="num">10; class="kw">input ENUM_MA_METHOD CHmethod = MODE_EMA; class="kw">input ENUM_APPLIED_VOLUME CHvolume = VOLUME_TICK; class="kw">input class="type">class="kw">double CHmax = class="num">1000; class="kw">input class="type">class="kw">double CHmin = -class="num">1000; class=class="str">"cmt">// RSI class="kw">input class="type">int RSIperiod = class="num">14; class="kw">input ENUM_APPLIED_PRICE RSIprice = PRICE_CLOSE; class="kw">input class="type">class="kw">double RSImax = class="num">70; class="kw">input class="type">class="kw">double RSImin = class="num">30; class=class="str">"cmt">// Stochastic class="kw">input class="type">int STperiodK = class="num">5; class="kw">input class="type">int STperiodD = class="num">3; class="kw">input class="type">int STperiodS = class="num">3; class="kw">input ENUM_MA_METHOD STmethod = MODE_EMA; class="kw">input ENUM_STO_PRICE STprice = STO_LOWHIGH; class="kw">input class="type">class="kw">double STmax = class="num">80; class="kw">input class="type">class="kw">double STmin = class="num">20; class="type">class="kw">double max,min; class="kw">switch(Type){ case WPR: max=WPRmax; min=WPRmin; h=iWPR(Symbol(),Period(),WPRperiod); break; case CCI:
多指标句柄的边界与兜底
这段 switch 分支把 CCI、Chaikin、RSI、Stochastic 四种指标的句柄分别赋值给 h,同时把各自预设的上下界写进 max 与 min。比如 CCI 分支里 max=CCImax、min=CCImin,随后用 iCCI(Symbol(),Period(),CCIperiod,CCIprice) 拿句柄;RSI 分支则走 iRSI(Symbol(),Period(),RSIperiod,RSIprice),参数更短但逻辑一致。 Chaikin 与 Stochastic 的参数明显更重:前者要快/慢周期、平滑方法与成交量开关,后者要 %K、%D、慢速周期加价格字段。在 MT5 里这类多参数指标若周期或价格常量填错,句柄会直接返回 INVALID_HANDLE。 末尾的 if(h==INVALID_HANDLE) 是硬兜底——一旦任一分支没拿到合法句柄,就 Print("无法加载指标") 并 return(INIT_FAILED),指示器初始化直接失败。外汇与贵金属品种切换时,不同品种对指标句柄的兼容性可能波动,建议每次换品种后先在策略测试器里跑一遍初始化日志。
max=CCImax; min=CCImin; h=iCCI(Symbol(),Period(),CCIperiod,CCIprice); break; case Chaikin: max=CHmax; min=CHmin; h=iChaikin(Symbol(),Period(),CHfperiod,CHsperiod,CHmethod,CHvolume); break; case RSI: max=RSImax; min=RSImin; h=iRSI(Symbol(),Period(),RSIperiod,RSIprice); break; case Stochastic: max=STmax; min=STmin; h=iStochastic(Symbol(),Period(),STperiodK,STperiodD,STperiodS,STmethod,STprice); break; } if(h==INVALID_HANDLE){ Print("无法加载指标"); class="kw">return(INIT_FAILED); }
「给之字折线振荡器套一个可交互窗体」
IncGUI_v4.mqh 是开发 MT5 图形界面的轻量封装,先把该文件丢进终端数据文件夹的 MQL5/Includes 目录,否则编译会报找不到头文件。它内部带一个 CFormTemplate 模板类,复制进指标源码后重命名为 CForm,就拥有了一个可拖拽、可折叠、带关闭键的窗体骨架。 窗体尺寸与外观在 MainProperties() 里定:m_Width=200、m_Height=150 是像素值,m_Type=2 代表底部显示关闭按钮,m_Movable 和 m_Resizable 都置 true 后,左上角和右上角会分别出现移动、折叠按钮。m_Name 作为所有子对象的前缀,必须避开与其他指标对象重名。 控件布局上用了两套 CFrame:第一套 85×97 像素、标题宽 44 像素放振荡器类型单选;第二套同尺寸、标题宽 32 像素放下边界输入。单选组 m_rg 加了 WPR、CCI、Chaikin、RSI、Stochastik 五个按钮,纵向间隔 16 像素;m_txt_max 与 m_txt_min 宽度 45、第三参数 -1 表示允许手工键入,标签分别为 Max / Min。 事件回路是关键:OnChartEvent 里调 form.Event(),返回 1 即窗体关闭,此时用 ChartIndicatorDelete 删指标。切单选会先 IndicatorRelease 释放旧句柄、再选新振荡器并启定时器——因为重算时复制的柱线数偶尔小于 Bars() 返回值,直接刷缓冲区会错位,所以用 OnTimer 反复尝试,成功才关定时器。 实测踩坑点在数据复制:只需要时间、最高、最低三个数组,但拷贝长度不足时要向后补长再喂给 OnCalculate(),否则之字折线在图表右侧可能断绘。完整可跑代码见 OscZigZagStep4.mq5,加载后窗体默认贴在左上角。外汇与贵金属波动剧烈,这类带 GUI 的自定义指标仅作辅助判势,实盘信号仍可能失效。
class="macro">#include <IncGUI_v4.mqh> m_Name = "Form"; m_Width = class="num">200; m_Height = class="num">150; m_Type = class="num">2; m_Caption = "ZigZag on Oscillator"; m_Movable = true; m_Resizable = true; m_CloseButton = true; CFrame m_frm1; class=class="str">"cmt">// 框架 class="num">1 CFrame m_frm2; class=class="str">"cmt">// 框架 class="num">2 CRadioGroup m_rg; class=class="str">"cmt">// 一组单选按钮 CInputBox m_txt_max; class=class="str">"cmt">// 上边界的文本字段 CInputBox m_txt_min; class=class="str">"cmt">// 下边界的文本字段 m_frm1.Init("frame1",class="num">85,class="num">97,"Osc Type",class="num">44); m_frm2.Init("frame2",class="num">85,class="num">97,"Levels",class="num">32); m_rg.Init(); m_rg.AddButton(" WPR",class="num">0,class="num">0); m_rg.AddButton(" CCI",class="num">0,class="num">16); m_rg.AddButton(" Chaikin",class="num">0,class="num">32); m_rg.AddButton(" RSI",class="num">0,class="num">48); m_rg.AddButton(" Stochastik",class="num">0,class="num">64); m_txt_max.Init("max",class="num">45,-class="num">1," Max"); m_txt_min.Init("min",class="num">45,-class="num">1," Min"); m_frm1.Show(aLeft+class="num">10,aTop+class="num">10); m_frm2.Show(aLeft+class="num">105,aTop+class="num">10); m_rg.Show(aLeft+class="num">17,aTop+class="num">20); m_txt_max.Show(aLeft+class="num">115,aTop+class="num">30); m_txt_min.Show(aLeft+class="num">115,aTop+class="num">50); m_frm1.Hide(); m_frm2.Hide(); m_rg.Hide(); m_txt_max.Hide(); m_txt_min.Hide(); class="type">void SetCaption(class="type">class="kw">string str){ m_Caption="ZigZag on "+str; ObjectSetString(class="num">0,m_Name+"_Caption",OBJPROP_TEXT,m_Caption); } CForm form; if(form.Event(id,lparam,dparam,sparam)==class="num">1){ ChartIndicatorDelete(class="num">0,class="num">0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw(); }