开发基于振荡器的之字折线 (ZigZag) 指标。 执行需求规范的示例·进阶篇
📐

开发基于振荡器的之字折线 (ZigZag) 指标。 执行需求规范的示例·进阶篇

(2/3)·从规范拆解到图形界面,手把手把振荡器ZigZag指标的开发坑位走通

新手友好 第 2/3 篇
很多人拿到指标需求规范直接写代码,结果之字折线顶点和价格高低点对不上才回头返工。先按阶段拆任务,把振荡器越级和价格极值的关系理清楚,能省掉大半调试时间。

把之字折线极值缓存起来再谈形态

检测形态前,得先拿到最近 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 为渲染效果。

MQL5 / C++
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-2li-2 高于 li-4,同时 li-1 高于 li-3——也就是偶数点逐级抬、奇数点也抬,符合多头推进。向下则全部取反。命中后 ZZDot[li].pat=true,随后第二段循环把最近 4 个点(遇到中间已 pat 则断)的 ZigZagColors 写成 1,完成可视化着色。 实盘里把 cnt[i]>=5 这个阈值调到 7,能过滤掉部分四波假突破,但会迟一步确认;外汇与贵金属波动跳变多,这类形态识别仅作概率参考,杠杆品种风险偏高,请先在 MT5 策略测试器跑历史段验证。

MQL5 / C++
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 观察假突破概率,别直接挂实盘。

MQL5 / C++
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),指示器初始化直接失败。外汇与贵金属品种切换时,不同品种对指标句柄的兼容性可能波动,建议每次换品种后先在策略测试器里跑一遍初始化日志。

MQL5 / C++
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 的自定义指标仅作辅助判势,实盘信号仍可能失效。

MQL5 / C++
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();
}
交给小布盯盘看盘口
这些振荡器越界与之字折线逆转的判定逻辑,小布盯盘的 AIGC 已内置到对应品种页,打开即可对照实盘形态,你只需专注策略决策。

常见问题

因为方向切换看振荡器跨入超买超卖的柱线,但新极值按价格最高最低算,两者时间差会造成顶点偏移,需回查更早的价极值。
会。若带新极值的柱线上发生逆转,该高点或低点就被取消,这是价格定义极值带来的特征。
不能。默认样式无法独立绘制垂直和相邻倾斜两段,也不支持回溯改历史段落颜色,需自定义缓冲与重绘。
可以,小布盯盘内置了振荡器越级与折线逆转的诊断,品种页直接呈现,不用自己写指标也能看结构。
在重绘历史之字折线段落时若每次全量刷新,容易造成卡顿,应按变动范围局部更新控件与缓冲。