价格行为分析工具包开发(第19部分):ZigZag分析器·综合运用
(3/3)·从趋势线骨架到ZigZag波段顶点,手把手把价格行为工具包收口落地
◍ OnTick 里先把旧图形清干净
EA 在每根新 K 线触发时,第一件事不是画新线,而是把上一根残留的支撑阻力与趋势线对象全删掉。代码里用 ObjectDelete 按名字逐个清除 Downtrend_HighLine、Uptrend_LowLine、Major/Minor_Resistance、Major/Minor_Support,避免历史对象叠在图表上干扰判读。 清场后通过 CopyBuffer 拉取 ZigZag 指标句柄的缓冲区,若返回值 ≤0 就 Print 报错并 return,这一步能拦掉九成以上的句柄失效闪退。LookBackBars 决定回看深度,默认取值若偏小,摆点提取会漏掉早期波段。 随后 ArraySetAsSeries(zzBuffer, true) 把缓冲区倒序,让索引 0 对应最新柱,再分别调用 DrawZigZagTrendlines 与 DrawSupportResistance。DrawZigZagTrendlines 内部用两个长度为 10 的数组存.swing 高/低点,循环里只认 zzBuffer[i] 不等于 0 的位置,且 highCount/lowCount 上限锁死 10,超出就不再收录。 外汇与贵金属波动剧烈,ZigZag 回调参数设错会让趋势线频繁重绘,实盘前务必在 MT5 策略测试器用历史数据跑一遍看对象刷新节奏。
ObjectDelete(class="num">0, "Major_Resistance"); ObjectDelete(class="num">0, "Major_Support"); ObjectDelete(class="num">0, "Minor_Resistance"); ObjectDelete(class="num">0, "Minor_Support"); IndicatorRelease(zzHandle); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">class="kw">datetime currentBarTime = iTime(_Symbol, InpTimeFrame, class="num">0); if(currentBarTime == lastBarTime) class="kw">return; lastBarTime = currentBarTime; class=class="str">"cmt">// Remove previous objects ObjectDelete(class="num">0, "Downtrend_HighLine"); ObjectDelete(class="num">0, "Uptrend_LowLine"); ObjectDelete(class="num">0, "Major_Resistance"); ObjectDelete(class="num">0, "Major_Support"); ObjectDelete(class="num">0, "Minor_Resistance"); ObjectDelete(class="num">0, "Minor_Support"); if(CopyBuffer(zzHandle, class="num">0, class="num">0, LookBackBars, zzBuffer) <= class="num">0) { Print("Failed to copy ZigZag data"); class="kw">return; } ArraySetAsSeries(zzBuffer, true); DrawZigZagTrendlines(); DrawSupportResistance(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw ZigZag-based Trendlines | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawZigZagTrendlines() { class="type">class="kw">double highPrices[class="num">10], lowPrices[class="num">10]; class="type">class="kw">datetime highTimes[class="num">10], lowTimes[class="num">10]; class="type">int highCount = class="num">0, lowCount = class="num">0; class=class="str">"cmt">// Extract swing points from the ZigZag buffer for(class="type">int i = class="num">0; i < LookBackBars - class="num">1; i++) { if(zzBuffer[i] != class="num">0) { if(iHigh(_Symbol, InpTimeFrame, i) == zzBuffer[i] && highCount < class="num">10) { highPrices[highCount] = zzBuffer[i]; highTimes[highCount] = iTime(_Symbol, InpTimeFrame, i); highCount++; } else if(iLow(_Symbol, InpTimeFrame, i) == zzBuffer[i] && lowCount < class="num">10) { lowPrices[lowCount] = zzBuffer[i];
「高低点分别做线性回归求通道斜率」
把前几步收集到的摆动高低点时间数组,先做一次裁剪:若高点或低点数量 ≥4,就丢弃最新一个摆动,避免右端被未闭合的毛刺带偏。代码中 usedHighCount / usedLowCount 正是这个意图,阈值 4 是硬判断。 随后对保留下来的点分别做一元线性回归。以高点为例,用 highTimes[i] 作自变量 t、highPrices[i] 作因变量 p,累加 sumT、sumP、sumTP、sumT2 四个量;斜率 mHigh = (N*sumTP - sumT*sumP) / (N*sumT2 - sumT*sumT),截距 bHigh = (sumP - mHigh*sumT)/N。当分母为零(所有 t 相同,几乎不可能但需防御)时退化为水平线 bHigh = sumP/N。低点完全同构,得到 mLow、bLow。 回归有效的前提是 usedHighCount≥3 且 usedLowCount≥3,否则 validHigh / validLow 保持 false,后面不画任何线。 最后取 pastTime = iTime(_Symbol, InpTimeFrame, LookBackBars-1) 定左边界,futureTime = lastBarTime + ExtendFutureBars * PeriodSeconds() 定右边界,为后续画趋势线框出时间窗。外汇与贵金属杠杆高,回归通道仅描述已有摆动的概率倾向,不等于价格会沿该方向运行。
class="type">int usedHighCount = (highCount >= class="num">4) ? highCount - class="num">1 : highCount; class="type">int usedLowCount = (lowCount >= class="num">4) ? lowCount - class="num">1 : lowCount; class="type">class="kw">double mHigh = class="num">0, bHigh = class="num">0, mLow = class="num">0, bLow = class="num">0; class="type">bool validHigh = false, validLow = false; class=class="str">"cmt">// Regression for highs if(usedHighCount >= class="num">3) { class="type">class="kw">double sumT = class="num">0, sumP = class="num">0, sumTP = class="num">0, sumT2 = class="num">0; for(class="type">int i = class="num">0; i < usedHighCount; i++) { class="type">class="kw">double t = (class="type">class="kw">double)highTimes[i]; class="type">class="kw">double p = highPrices[i]; sumT += t; sumP += p; sumTP += t * p; sumT2 += t * t; } class="type">int N = usedHighCount; class="type">class="kw">double denominator = N * sumT2 - sumT * sumT; if(denominator != class="num">0) { mHigh = (N * sumTP - sumT * sumP) / denominator; bHigh = (sumP - mHigh * sumT) / N; } else bHigh = sumP / N; validHigh = true; } class=class="str">"cmt">// Regression for lows if(usedLowCount >= class="num">3) { class="type">class="kw">double sumT = class="num">0, sumP = class="num">0, sumTP = class="num">0, sumT2 = class="num">0; for(class="type">int i = class="num">0; i < usedLowCount; i++) { class="type">class="kw">double t = (class="type">class="kw">double)lowTimes[i]; class="type">class="kw">double p = lowPrices[i]; sumT += t; sumP += p; sumTP += t * p; sumT2 += t * t; } class="type">int N = usedLowCount; class="type">class="kw">double denominator = N * sumT2 - sumT * sumT; if(denominator != class="num">0) { mLow = (N * sumTP - sumT * sumP) / denominator; bLow = (sumP - mLow * sumT) / N; } else bLow = sumP / N; validLow = true; } class=class="str">"cmt">// Define time limits for trendlines class="type">class="kw">datetime pastTime = iTime(_Symbol, InpTimeFrame, LookBackBars - class="num">1); class="type">class="kw">datetime futureTime = lastBarTime + ExtendFutureBars * PeriodSeconds(); class=class="str">"cmt">// Draw trendlines if both regressions are valid if(validHigh && validLow) { class=class="str">"cmt">// When slopes have the same sign, use average slope
平行通道的上下轨绘制逻辑
当 mHigh 与 mLow 同号(乘积大于 0)时,说明高低点斜率方向一致,此时可用两者均值 mParallel 构造一条平行通道,而非独立的两组斜线。 代码先算出平行斜率 mParallel = (mHigh + mLow) / 2.0,再分别用 highPrices[0]、lowPrices[0] 与对应时间求出高低轨的截距 bHighParallel、bLowParallel,得到两条永不相交的平行趋势线。 高低轨都从 pastTime 延伸到 futureTime,调用 ObjectCreate 以 OBJ_TREND 类型画图;失败则 Print 报错,成功则将颜色设为红/绿,并开启 OBJPROP_RAY_LEFT 与 OBJPROP_RAY_RIGHT,让射线向左右双向延伸。 若 mHigh * mLow <= 0,则走 else 分支,仅用各自斜率 mHigh 与截距 bHigh 绘制单侧高位趋势线,不再强制平行。外汇与贵金属波动剧烈,这类自动画线仅作结构参考,实际突破倾向需结合成交量与多周期验证。
if(mHigh * mLow > class="num">0) { class="type">class="kw">double mParallel = (mHigh + mLow) / class="num">2.0; class="type">class="kw">double bHighParallel = highPrices[class="num">0] - mParallel * (class="type">class="kw">double)highTimes[class="num">0]; class="type">class="kw">double bLowParallel = lowPrices[class="num">0] - mParallel * (class="type">class="kw">double)lowTimes[class="num">0]; class="type">class="kw">datetime highStartTime = pastTime; class="type">class="kw">double highStartPrice = mParallel * (class="type">class="kw">double)highStartTime + bHighParallel; class="type">class="kw">double highEndPrice = mParallel * (class="type">class="kw">double)futureTime + bHighParallel; if(!ObjectCreate(class="num">0, "Downtrend_HighLine", OBJ_TREND, class="num">0, highStartTime, highStartPrice, futureTime, highEndPrice)) Print("Failed to create High Trendline"); else { ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_COLOR, clrRed); ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_RAY_LEFT, true); ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_RAY_RIGHT, true); } class="type">class="kw">datetime lowStartTime = pastTime; class="type">class="kw">double lowStartPrice = mParallel * (class="type">class="kw">double)lowStartTime + bLowParallel; class="type">class="kw">double lowEndPrice = mParallel * (class="type">class="kw">double)futureTime + bLowParallel; if(!ObjectCreate(class="num">0, "Uptrend_LowLine", OBJ_TREND, class="num">0, lowStartTime, lowStartPrice, futureTime, lowEndPrice)) Print("Failed to create Low Trendline"); else { ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_COLOR, clrGreen); ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_RAY_LEFT, true); ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_RAY_RIGHT, true); } } else { class="type">class="kw">datetime highStartTime = pastTime; class="type">class="kw">double highStartPrice = mHigh * (class="type">class="kw">double)highStartTime + bHigh; class="type">class="kw">double highEndPrice = mHigh * (class="type">class="kw">double)futureTime + bHigh; if(!ObjectCreate(class="num">0, "Downtrend_HighLine", OBJ_TREND, class="num">0, highStartTime, highStartPrice, futureTime, highEndPrice))
◍ 通道边界的射线绘制与失败处理
在回归通道的上下轨生成后,代码把斜率截距换算成具体时间坐标上的价格点,再用 OBJ_TREND 画趋势线。高点线命名为 Downtrend_HighLine 并染红,低点线命名为 Uptrend_LowLine 并染绿,两条线都开启左右射线(RAY_LEFT/RAY_RIGHT 为 true),方便在 MT5 主图上向后延伸观察价格触碰。 若 ObjectCreate 返回 false,说明对象名冲突或图表上下文异常,此时直接 Print 输出 "Failed to create High/Low Trendline" 到专家日志,不会中断 EA 运行。开 MT5 跑这段时,先确认没有同名手工趋势线占着坑,否则创建必然失败。 价格点的计算用到了 mHigh*(double)highStartTime + bHigh 这类线性表达式,pastTime 与 futureTime 分别是通道回看起点和向前投射终点。外汇与贵金属波动剧烈,射线延伸仅作概率参考,实际触碰后反转或突破都可能发生,属高风险场景。
Print("Failed to create High Trendline"); else { ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_COLOR, clrRed); ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_RAY_LEFT, true); ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_RAY_RIGHT, true); } class="type">class="kw">datetime lowStartTime = pastTime; class="type">class="kw">double lowStartPrice = mLow * (class="type">class="kw">double)lowStartTime + bLow; class="type">class="kw">double lowEndPrice = mLow * (class="type">class="kw">double)futureTime + bLow; if(!ObjectCreate(class="num">0, "Uptrend_LowLine", OBJ_TREND, class="num">0, lowStartTime, lowStartPrice, futureTime, lowEndPrice)) Print("Failed to create Low Trendline"); else { ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_COLOR, clrGreen); ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_RAY_LEFT, true); ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_RAY_RIGHT, true); } } } else { if(validHigh) { class="type">class="kw">datetime highStartTime = pastTime; class="type">class="kw">double highStartPrice = mHigh * (class="type">class="kw">double)highStartTime + bHigh; class="type">class="kw">double highEndPrice = mHigh * (class="type">class="kw">double)futureTime + bHigh; if(!ObjectCreate(class="num">0, "Downtrend_HighLine", OBJ_TREND, class="num">0, highStartTime, highStartPrice, futureTime, highEndPrice)) Print("Failed to create High Trendline"); else { ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_COLOR, clrRed); ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_RAY_LEFT, true); ObjectSetInteger(class="num">0, "Downtrend_HighLine", OBJPROP_RAY_RIGHT, true); } } if(validLow) { class="type">class="kw">datetime lowStartTime = pastTime; class="type">class="kw">double lowStartPrice = mLow * (class="type">class="kw">double)lowStartTime + bLow;
「用 ZigZag 节点画支撑阻力与上升底线」
这段逻辑承接前面的 ZigZag 识别,把已确认的高低点转成可视的支撑阻力线,并补一条向右延伸的上升底线。外汇与贵金属波动剧烈,这类画线只描述历史结构,后续价格触碰后反转还是破位都只是概率事件。 上升底线的终点价格由斜率推算:lowEndPrice = mLow * (double)futureTime + bLow,其中 mLow 是前面拟合出的底线斜率,futureTime 是向右延伸到的柱索引。若 ObjectCreate 返回失败会向日志打印 Failed to create Low Trendline,成功则把颜色设为绿、左右都开射线,方便在 MT5 上持续观察。 支撑阻力的提取在 DrawSupportResistance 里完成:用两个长度 10 的数组存 confirmedHighs / confirmedLows,遍历 LookBackBars-1 根 K 线,把 zzBuffer 非空且与 iHigh / iLow 吻合的节点分别收进去,最多各存 10 个。 计数上有个细节——若确认高/低点 ≥4,实际使用数会减 1(usedHighCount / usedLowCount 的写法),相当于留最后一个节点不参与主级判定。主级支撑取所有 used 低点里的最小值(初始化 1e9),主级阻力取最大值(初始化 -1e9),次级的 tempHigh / tempLow 则承接被挤下来的次高/次低值。
class="type">class="kw">double lowEndPrice = mLow * (class="type">class="kw">double)futureTime + bLow; if(!ObjectCreate(class="num">0, "Uptrend_LowLine", OBJ_TREND, class="num">0, lowStartTime, lowStartPrice, futureTime, lowEndPrice)) Print("Failed to create Low Trendline"); else { ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_COLOR, clrGreen); ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_RAY_LEFT, true); ObjectSetInteger(class="num">0, "Uptrend_LowLine", OBJPROP_RAY_RIGHT, true); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw Support and Resistance Levels | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DrawSupportResistance() { class="type">class="kw">double confirmedHighs[class="num">10], confirmedLows[class="num">10]; class="type">int confHighCount = class="num">0, confLowCount = class="num">0; for(class="type">int i = class="num">0; i < LookBackBars - class="num">1; i++) { if(zzBuffer[i] != class="num">0) { if(iHigh(_Symbol, InpTimeFrame, i) == zzBuffer[i] && confHighCount < class="num">10) { confirmedHighs[confHighCount] = zzBuffer[i]; confHighCount++; } else if(iLow(_Symbol, InpTimeFrame, i) == zzBuffer[i] && confLowCount < class="num">10) { confirmedLows[confLowCount] = zzBuffer[i]; confLowCount++; } } } class="type">int usedHighCount = (confHighCount >= class="num">4) ? confHighCount - class="num">1 : confHighCount; class="type">int usedLowCount = (confLowCount >= class="num">4) ? confLowCount - class="num">1 : confLowCount; class="type">class="kw">double majorResistance = -class="num">1e9, majorSupport = class="num">1e9; class="type">class="kw">double minorResistance = -class="num">1e9, minorSupport = class="num">1e9; class="type">class="kw">double tempHigh = -class="num">1e9, tempLow = -class="num">1e9; for(class="type">int i = class="num">0; i < usedHighCount; i++) { if(confirmedHighs[i] > majorResistance) { tempHigh = majorResistance; majorResistance = confirmedHighs[i]; } else if(confirmedHighs[i] > tempHigh) {
把高低点数组画成四级水平线
这段逻辑紧接前面已确认的波峰波谷数组,负责把 major/minor 支撑阻力算出来并直接往 MT5 图表上甩水平线。先扫 confirmedHighs:比当前 majorResistance 高的就顶替成主阻力,否则记到 tempHigh 当次阻力,最后 tempHigh 大于 -1e9 才写进 minorResistance。 低点侧同理,遍历 confirmedLows 找比 majorSupport 更小的更新主支撑,其余落进 tempLow;tempLow 小于 1e9 才认定为 minorSupport。注意判断里用了 1e9 / -1e9 当初始哨兵值,没碰到有效值就不会误画。 画线部分用 ObjectCreate 建 OBJ_HLINE,主阻力品红(clrMagenta)、次阻力紫红(clrFuchsia)、主支撑青色(clrAqua)、次支撑蓝色(clrBlue)。次线只在「确实比主线更靠近现价」时才建——阻力要低于主阻力,支撑要高于主支撑,避免重叠乱图。 开 MT5 把这段接在你自己的波峰波谷识别后跑一遍,图表上若没出线先查 Print 里的 Failed 提示;外汇和贵金属波动快,这类静态水平线仅作概率参考,实战须自担高风险。
tempHigh = confirmedHighs[i];
}
}
if(tempHigh > -class="num">1e9)
minorResistance = tempHigh;
for(class="type">int i = class="num">0; i < usedLowCount; i++)
{
if(confirmedLows[i] < majorSupport)
{
tempLow = majorSupport;
majorSupport = confirmedLows[i];
}
else
if(confirmedLows[i] < tempLow)
{
tempLow = confirmedLows[i];
}
}
if(tempLow < class="num">1e9)
minorSupport = tempLow;
if(usedHighCount > class="num">0)
{
if(!ObjectCreate(class="num">0, "Major_Resistance", OBJ_HLINE, class="num">0, class="num">0, majorResistance))
Print("Failed to create Major Resistance");
else
ObjectSetInteger(class="num">0, "Major_Resistance", OBJPROP_COLOR, clrMagenta);
if(minorResistance > -class="num">1e9 && minorResistance < majorResistance)
{
if(!ObjectCreate(class="num">0, "Minor_Resistance", OBJ_HLINE, class="num">0, class="num">0, minorResistance))
Print("Failed to create Minor Resistance");
else
ObjectSetInteger(class="num">0, "Minor_Resistance", OBJPROP_COLOR, clrFuchsia);
}
}
if(usedLowCount > class="num">0)
{
if(!ObjectCreate(class="num">0, "Major_Support", OBJ_HLINE, class="num">0, class="num">0, majorSupport))
Print("Failed to create Major Support");
else
ObjectSetInteger(class="num">0, "Major_Support", OBJPROP_COLOR, clrAqua);
if(minorSupport < class="num">1e9 && minorSupport > majorSupport)
{
if(!ObjectCreate(class="num">0, "Minor_Support", OBJ_HLINE, class="num">0, class="num">0, minorSupport))
Print("Failed to create Minor Support");
else
ObjectSetInteger(class="num">0, "Minor_Support", OBJPROP_COLOR, clrBlue);
}
}
}◍ 把趋势线摆到图上验证
先往图表里挂上 ZigZag 指标,再跑分析器,不然结果没法对照看。阶梯指数那张图里,EA 接手后高低线都向下倾,主次要支撑阻力一并标出,低线高线同步下压就是下降结构的可视证据。 波段顶点看得更细:更低的低点、更低的高点连成趋势线,线和主要水平的交叉点正是反转高发区——最高的更低高点碰主阻力、最低的更低低点碰主支撑,这两处都出现了折返。 USDCHF 上趋势线不抢戏但市场照常尊重,触碰波动顶点的位置高亮后,系统在外汇品种上算站住了脚。波动率 25 指数也画出连贯波段连线,多种行情下线条都贴着运动路径,说明这套画法不是单品种巧合。 外汇与指数差价合约波动剧烈、杠杆风险高,上述图形结论仅代表历史片段表现,后续走势仍可能背离。
「把这条线请下神坛」
ZigZag 分析器在趋势市里能把拐点连成趋势线,测试显示支撑阻力区识别效率明显高于纯肉眼描线,但它终归只是价格行为的投影,不是决策本身。外汇与贵金属杠杆高、滑点跳空频繁,把它当唯一依据大概率会吃亏,老手也该用其他指标交叉确认。 下面这段初始化代码是工具跑起来的命门:先建 ZigZag 句柄,失败就退初始化,成功才挂到主图。若你编译后图表空白,九成是自定义指标没放进 Examples\ZigZag 路径——社区里已有两人踩过这坑。 int OnInit() 创建句柄并 ChartIndicatorAdd 挂图,逻辑干净,复制进 MT5 改 ZZ_Depth 参数就能看不同灵敏度下的线形差异。工具链从 24 年 1 月图表投影仪到 25 年 3 月 ZigZag 分析器共 19 个,挑顺手的改两行比追新更有用。
class="type">int OnInit() { zzHandle = iCustom(_Symbol, InpTimeFrame, "Examples\\ZigZag", ZZ_Depth, ZZ_Deviation, ZZ_Backstep); if(zzHandle == INVALID_HANDLE) { Print("Error creating ZigZag handle"); class="kw">return(INIT_FAILED); } ChartIndicatorAdd(class="num">0, class="num">0, zzHandle); class="kw">return(INIT_SUCCEEDED); }