ZigZag 指标基类开发:为价格行为分析铺好数据地基(基础篇)
(1/3)· 多数策略忽略点差与极值类别,导致回测漂亮实盘失真,本文从基类开始纠偏
给 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 夜间,红色低点明显抬高——那就是点差被吃掉的痕迹。
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% 的毛刺拐点。
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%,但实际开销取决于品种数和历史深度,高杠杆品种仍属高风险。
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); }