ZigZag 指标基类开发:为价格行为分析铺好数据地基(基础篇)
📐

ZigZag 指标基类开发:为价格行为分析铺好数据地基(基础篇)

(1/3)· 多数策略忽略点差与极值类别,导致回测漂亮实盘失真,本文从基类开始纠偏

含代码示例 第 1/3 篇
直接用裸 ZigZag 低点做信号,却忘了买入价和卖出价中间隔着点差,夜盘跳空时这种极值根本成交不了。把只基于 bid 的折线当价格行为真相,回测好看实盘挨打。基类不理清数据类别,后面所有统计都是空中楼阁。

给 ZigZag 指标写个能复用的基类

ZigZag 之字折线在价格行为分析里不只是画线工具,它把市场摆动点显式抽出来,给突破、回调和波段边界判断提供可计算的坐标。要在 MT5 里反复做各种 ZigZag 变体,最省事的办法是先开发一套指标基类,把公共点位计算、缓冲区和参数接口收拢到一起。 基类至少该封装三件事:极值识别的回溯深度参数、ZigZag 缓冲区的初始化、以及每段折线的起点与终点更新逻辑。这样后续写普通 ZigZag、彩色 ZigZag 或带通道的 ZigZag,都只改派生类里的判定规则,不用重写底层。 MT5 自带的 ZigZag 默认回溯点数(Deviation)常设为 12,但这在黄金 15 分钟图上容易把噪音当拐点。实盘前建议在 MT5 里把参数从 5 到 20 逐档回看,记录各档位下拐点数量与假突破比例,再定自己的基线。外汇与贵金属杠杆高、跳空频繁,ZigZag 拐点滞后属正常现象,用它做确认而非预测更稳妥。

「ZigZag 扩展版的六个落点」

原文把扩展 ZigZag 指标的开发拆成六块:先给概述,再做指标本身的扩展版本,接着澄清 ZigZag 输出数据到底属于哪一类(拐点序列还是连续折线),然后用可视化把拿到的数据集画出来看形态。 剩下两块是实打实能跑的:写一个 EA 专门测试前面得到的结果,以及把 CZigZagModule 类的开发继续补完。对 MT5 用户来说,这六步就是一条从指标改写、数据定性到回测验证的完整链路,开终端照这个顺序做就不会卡在「数据到底怎么用」这一步。 外汇与贵金属波动大、滑点随机,这类指标改写只解决结构识别,不解决执行风险,动手前先开模拟盘跑通再上实盘。

◍ 为什么先挑品种再调参数

上一篇演示了把 RSI 这类指标做成既能抓趋势也能识别横盘的信号源,但这类工具普遍缺一项关键能力:界定当前的价格行为状态。什么时候该进场、什么时候该停手,往往就卡在这一点上。 很多研究者要么轻视价格行为判定,要么直接堆机器学习、神经网络之类的黑盒子。问题不在于模型本身,而是喂给模型训练的数据该怎么选,这件事常常被跳过。 本文系列换一条路:先用改版的 ZigZag 指标和配套代码类把价格骨架抽出来,再谈品种筛选。系列会落地改版 ZigZag、取数类、测试 EA、价格行为指标、带 GUI 的统计 EA,以及跟信号的实盘 EA。 外汇与贵金属波动受杠杆和突发事件影响,用 ZigZag 划分波段只是概率上的结构参考,不代表走势必然延续,实盘前请在 MT5 策略测试器里跑一遍样本。

把点差塞进之字折线的极值里

普通 ZigZag 只看每根 K 线的最高最低,默认买价卖价重合。实盘里并非如此,尤其夜间流动性薄时,ask 明显高出 bid,EURMXN M5 在夜段点差可拉到常态的数倍。若仍只用 bid 系低点画折线的下极值,等于在根本没法成交的价位上找支撑,策略一跑就偏。 修订思路不复杂:下行线段的极值取『最低 ask』,上行线段的极值取『最高 bid』。相当于把成交通道内的可执行边界当作折线锚点,图表上一眼能看穿哪里真能进场。这比在 EA 里事后补点差更直观,也少写几层判断。 实现上要开 6 个指标缓冲区、绘图用 5 个:low_ask_buffer 存最低要价,high_bid_buffer 存最高出价,zz_H/zz_L 画主折线,total_zz_h/total_zz_l 留全部拐点。外部参数 NumberOfBars=0 表示全图计算,MinImpulseSize 默认 100 点,价格反向偏离超该阈值才翻线段。 FillAskBidBuffers() 把 High 数组值塞进 bid 缓冲、Low 数组加点点差后塞进 ask 缓冲;FillIndicatorBuffers() 按当前 direction_zz 分流:上行时比最大 bid 是否刷新,下行时比最小 ask 是否破前低,不满足就检查反向穿越阈值。EURUSD D1 上两条线几乎重叠,但换到 EURMXN M5 夜间,红色低点明显抬高——那就是点差被吃掉的痕迹。

MQL5 / C++
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers class="num">6
class="macro">#class="kw">property indicator_plots   class="num">5
class=class="str">"cmt">//---
class="macro">#class="kw">property indicator_color1   clrRed
class="macro">#class="kw">property indicator_color2   clrCornflowerBlue
class="macro">#class="kw">property indicator_color3   clrGold
class="macro">#class="kw">property indicator_color4   clrOrangeRed
class="macro">#class="kw">property indicator_color5   clrSkyBlue
class=class="str">"cmt">//--- Indicator buffers:
class="type">class="kw">double low_ask_buffer[];     class=class="str">"cmt">// 最低要价
class="type">class="kw">double high_bid_buffer[];    class=class="str">"cmt">// 最高出价
class="type">class="kw">double zz_H_buffer[];        class=class="str">"cmt">// 高点
class="type">class="kw">double zz_L_buffer[];        class=class="str">"cmt">// 低点
class="type">class="kw">double total_zz_h_buffer[];  class=class="str">"cmt">// 所有高点
class="type">class="kw">double total_zz_l_buffer[];  class=class="str">"cmt">// 所有低点
class=class="str">"cmt">//--- 外部参数
class="kw">input class="type">int   NumberOfBars   =class="num">0;         class=class="str">"cmt">// 柱线数量
class="kw">input class="type">int   MinImpulseSize =class="num">100;       class=class="str">"cmt">// 线段中的最小点数
class="kw">input class="type">bool  ShowAskBid     =false;     class=class="str">"cmt">// 显示要加/出价
class="kw">input class="type">bool  ShowAllPoints  =false;     class=class="str">"cmt">// 显示所有点
class="kw">input class="type">color RayColor       =clrGold;   class=class="str">"cmt">// 线段颜色
class=class="str">"cmt">//--- ZZ 变量
class="type">int    last_zz_max  =class="num">0;
class="type">int    last_zz_min  =class="num">0;
class="type">int    direction_zz =class="num">0;
class="type">class="kw">double min_low_ask  =class="num">0;
class="type">class="kw">double max_high_bid =class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 填充出价高点和要价低点指标缓冲区                                |
class=class="str">"cmt">//+------------------------------------------------------------------+

「用 Bid/Ask 重建 ZigZag 拐点的缓冲区逻辑」

在 MT5 自定义指标里,若直接用 High/Low 画 ZigZag,会忽略点差造成的 ask 偏移。下面这段把 bid 侧高点和 ask 侧低点分开存缓冲:high_bid_buffer 直接取 high[i],low_ask_buffer 则在 low[i] 上加 spread[i]*_Point,使下方拐点落在真实卖价轨迹上。 FillAskBidBuffers 先卡 first_date,早于它的 K 线直接 return,避免回测起点前的脏数据污染序列。FillIndicatorBuffers 则按 direction_zz 正负分两种状态机:上行时遇新高就刷新 last_zz_max 与 max_high_bid,下行突破判定用 fabs(low_ask_buffer[i]-zz_H_buffer[last_zz_max])>MinImpulseSize*_Point 做最小脉冲过滤,小于该值的折返倾向不翻转方向。 实盘中外汇与贵金属点差跳动频繁,XAUUSD 在流动性薄的时段 spread 可能从 15 点扩到 50 点以上,此时 low_ask_buffer 相对 low[] 的偏移不可忽略。把这段代码贴进指标 OnCalculate 前的预处理函数,开 MT5 用 EURUSD 的 M1 跑一遍,对比原版 ZigZag 与本版拐点数量,可能少 3%~8% 的毛刺拐点。

MQL5 / C++
class="type">void FillAskBidBuffers(class="kw">const class="type">int i,class="kw">const class="type">class="kw">datetime &time[],class="kw">const class="type">class="kw">double &high[],class="kw">const class="type">class="kw">double &low[],class="kw">const class="type">int &spread[])
  {
class=class="str">"cmt">//--- 如果未达到初始日期,则退出
   if(time[i]<first_date)
        class="kw">return;
class=class="str">"cmt">//---
   high_bid_buffer[i] =high[i];
   low_ask_buffer[i]  =low[i]+(spread[i]*_Point);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 填充 ZZ 指标缓冲区                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FillIndicatorBuffers(class="kw">const class="type">int i,class="kw">const class="type">class="kw">datetime &time[])
  {
   if(time[i]<first_date)
        class="kw">return;
class=class="str">"cmt">//--- 如果 ZZ 上行
   if(direction_zz>class="num">0)
     {
      class=class="str">"cmt">//--- 如果出现新高
      if(high_bid_buffer[i]>=max_high_bid)
        {
         zz_H_buffer[last_zz_max] =class="num">0;
         last_zz_max               =i;
         max_high_bid              =high_bid_buffer[i];
         zz_H_buffer[i]            =high_bid_buffer[i];
         total_zz_h_buffer[i]      =high_bid_buffer[i];
        }
      class=class="str">"cmt">//--- 如果方向发生变化(下行)
      else
        {
         if(low_ask_buffer[i]<max_high_bid &&
            fabs(low_ask_buffer[i]-zz_H_buffer[last_zz_max])>MinImpulseSize*_Point)
           {
            last_zz_min     =i;
            direction_zz    =-class="num">1;
            min_low_ask     =low_ask_buffer[i];
            zz_L_buffer[i]  =low_ask_buffer[i];
            total_zz_l_buffer[i]=low_ask_buffer[i];
           }
        }
     }
class=class="str">"cmt">//--- 如果 ZZ 下行
   else
     {
      class=class="str">"cmt">//--- 如果出现新低
      if(low_ask_buffer[i]<=min_low_ask)
        {
         zz_L_buffer[last_zz_min] =class="num">0;
         last_zz_min               =i;
         min_low_ask               =low_ask_buffer[i];
         zz_L_buffer[i]            =low_ask_buffer[i];
         total_zz_l_buffer[i]      =low_ask_buffer[i];
        }
      class=class="str">"cmt">//--- 如果方向发生变化(上行)
      else
        {
         if(high_bid_buffer[i]>min_low_ask &&

◍ 让指标只在必要时重算

这段 OnCalculate 的写法核心在于控制重算范围:当 prev_calculated 已等于 rates_total 时直接 return,跳过每次即时报价触发的全量循环,MT5 终端里能明显看到 CPU 占用下降。 首次计算走 prev_calculated==0 分支,先清零缓冲区和变量,再经 CheckDataAvailable 判断数据够不够,DetermineNumberData 和 DetermineBeginForCalculate 确定复制量与各品种起绘柱。非首次则只从 prev_calculated-1 开始补算最后一根,避免重复跑历史。 两个 for 循环分工明确:第一个填 ask/bid 缓冲(含 spread 处理),第二个填指标主缓冲但只到 rates_total-1,留一根等下一 tick。外汇与贵金属点差跳变频繁,这种增量计算在 M15 以上周期可能让回测速度提升 20%~40%,但实际开销取决于品种数和历史深度,高杠杆品种仍属高风险。

MQL5 / C++
class="type">int OnCalculate(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=class="str">"cmt">//--- 避免每次即时报价时计算
   if(prev_calculated==rates_total)
      class="kw">return(rates_total);
class=class="str">"cmt">//--- 如果这是首次计算
   if(prev_calculated==class="num">0)
     {
      class=class="str">"cmt">//--- 将指标缓冲区设置为零
      ZeroIndicatorBuffers();
      class=class="str">"cmt">//--- 将变量设置为零
      ZeroIndicatorData();
      class=class="str">"cmt">//--- 检查可用数据量
      if(!CheckDataAvailable())
         class="kw">return(class="num">0);
      class=class="str">"cmt">//--- 如果已指定了更多复制数据,则使用当前数量
      DetermineNumberData();
      class=class="str">"cmt">//--- 定义每个品种起始绘制柱线
      DetermineBeginForCalculate(rates_total);
     }
   else
     {
      class=class="str">"cmt">//--- 仅计算最后一个值
      start=prev_calculated-class="num">1;
     }
class=class="str">"cmt">//--- 填写最高出价和最低要价指标缓冲区
   for(class="type">int i=start; i<rates_total; i++)
      FillAskBidBuffers(i,time,high,low,spread);
class=class="str">"cmt">//--- 用数据填充指标缓冲区
   for(class="type">int i=start; i<rates_total-class="num">1; i++)
      FillIndicatorBuffers(i,time);
class=class="str">"cmt">//--- 返回数据数组大小
   class="kw">return(rates_total);
  }
把极值分类交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到高低点是否按 ask/bid 分离标注,省去自己写缓冲区的功夫。

常见问题

因为实际成交时买入用 ask、卖出用 bid,仅用柱线低点构建极值会忽略点差,导致策略在夜盘等点差扩大时信号无法执行,分离缓冲区才能还原可成交的价格行为。
可以,小布盯盘的品种页已内置按 ask/bid 分离的极值可视化,你无需自行编译指标即可快速判断哪个交易品种的价格行为更规整、更适合跑后续策略。
最低要价和最高出价作为原始数据缓冲用于计算,不直接绘线;高点、低点、上行线段高点、下行线段低点五个用于图表呈现,所以可见缓冲为五个。
后续篇会基于该类写测试 EA、价格行为定义指标、带 GUI 的统计收集 EA 以及信号跟随 EA,完整讨论见《ZigZag 的力量·实战篇》。