价格行为分析工具包开发(第19部分):ZigZag分析器·进阶篇
(2/3)·手动连高低点漏掉隐性波段?ZigZag算法把趋势线绘制交给代码实时跑
「高低点分别做线性回归画线」
把前 N 个摆动高点与低点的时间、价格分别套进一元线性回归,得到两条通道边界的斜率与截距。代码里对高点、低点各跑一遍相同的累加逻辑:先排除最近一个摆动点(highCount/lowCount 够 4 才减 1),再用 usedHighCount、usedLowCount 参与拟合,样本少于 3 直接跳过。 回归核心就是那几句:sumT 累加时间、sumP 累加价格、sumTP 累加时间×价格、sumT2 累加时间平方;分母 denominator = N*sumT2 - sumT*sumT,不为零时斜率 m = (N*sumTP - sumT*sumP)/denominator,截距 b = (sumP - m*sumT)/N。若分母为零(所有时间点重合的极端情况)则只取价格均值当水平线。
class=class="str">"cmt">// 排除最近一个摆动点(若样本够4) 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">// 高点回归 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">// 低点回归(结构同上) 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">// 时间边界 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">// 双回归有效才画线 if(validHigh && validLow) {
class=class="str">"cmt">// 排除最近一个摆动点(若样本够4) 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">// 高点回归 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">// 低点回归(结构同上) 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">// 时间边界 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">// 双回归有效才画线 if(validHigh && validLow) {
同向斜率下的平行通道画法
当高低点回归斜率同号(mHigh*mLow>0)时,说明价格上下边界倾向朝同一方向运行,此时用两者均值 mParallel=(mHigh+mLow)/2.0 作为平行通道的统一斜率,比分别画两条不同斜率的线更贴近通道本意。 代码先以 highPrices[0] 和 lowPrices[0] 分别减去 mParallel 乘对应时间,得到上下轨截距 bHighParallel、bLowParallel,再按 pastTime 到 futureTime 拉出射线。两条线都开了 OBJPROP_RAY_LEFT 与 OBJPROP_RAY_RIGHT,意味着左右双向延伸,适合在 MT5 上直观看价格是否持续贴着通道走。 若斜率异号则走 else 分支,高低点各用自身斜率 mHigh、mLow 与截距 bHigh、bLow 画独立趋势线,不强行平行。外汇与贵金属波动受杠杆与消息面影响大,通道被刺穿的概率不低,实盘使用前建议在历史数据上验证命中率。
class=class="str">"cmt">// When slopes have the same sign, use average slope for parallel lines 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=class="str">"cmt">// Draw separate trendlines if slopes differ { 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;
◍ 用回归直线在图上拉出趋势射线
当高低点两组坐标都通过有效性校验时,代码会分别用线性回归斜率 mHigh/bHigh 与 mLow/bLow 算出过去与未来两个时间点的价格,再调用 ObjectCreate 画 OBJ_TREND 趋势线。高点线命名为 Downtrend_HighLine 并染红,低点线命名为 Uptrend_LowLine 并染绿,两条线都开启 OBJPROP_RAY_LEFT 与 OBJPROP_RAY_RIGHT,意味着向左右双向无限延伸。 若只有 validHigh 成立而低点回归无效,则只画红色高点射线,跳过绿色线,避免用残缺数据强行绘线误导判断。外汇与贵金属波动剧烈,射线仅表达历史回归倾向,实际触碰后的反转概率需结合实时成交量与周期确认。 下面这段是双线绘制的核心片段,逐行看逻辑:if 判断 ObjectCreate 是否返回 false,失败就 Print 报错;成功则进 else 设置颜色与双向射线属性。低点线对称处理,只是把变量换成 mLow/bLow 和绿色。
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 = 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); }
「用枢轴点数组画双向射线趋势线」
这段逻辑在 validLow 成立时,把线性回归算出的下轨延成一条双向射线。lowStartTime 取回看起点 pastTime,lowStartPrice 用斜率 mLow 乘时间再加截距 bLow 得到;futureTime 对应的 lowEndPrice 同理推算,等于把拟合直线向右也铺开。 ObjectCreate 失败会 Print 报错,成功则用 ObjectSetInteger 把 Uptrend_LowLine 染成 clrGreen,并同时打开 OBJPROP_RAY_LEFT 与 OBJPROP_RAY_RIGHT。左右都射线化后,这条线在 MT5 主图会贯穿历史与未成交区,方便肉眼读支撑。 DrawSupportResistance 另开一个例程,用 confirmedHighs[10]、confirmedLows[10] 两个定长数组承接之字转向缓冲里的极值。循环里只收 iHigh/iLow 与 zzBuffer[i] 相等的点,且各自上限 10 个,超出直接丢弃。 usedHighCount 与 usedLowCount 有个细节:当确认点数 ≥4 时故意减 1,相当于留最后一个极值不进主统计。major/minor 阻力支撑初值分别设为 -1e9 与 1e9,后续遍历 usedHighCount 找最大正值填 majorResistance,这部分截断了,但思路是拿数组极值当价位锚。外汇与贵金属波动剧烈,射线支撑阻力仅作概率参考,实盘须自担高风险。
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; 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="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) {
主次阻支水平的判定与画线落地
这段逻辑在已确认高低点数组中做二次筛选:主阻力取所有 confirmedHighs 里的最小值,次阻力取仅次于主阻力的那个高值;支撑方向完全对称,主支撑取 confirmedLows 最大值,次支撑取次低值。判定时用了 -1e9 / 1e9 作为未初始化哨兵值,tempHigh 大于该负哨兵才把结果写进 minorResistance,避免把空数据当真实价位画出来。 画线部分用 OBJ_HLINE 在图表挂水平线,主阻力品红(clrMagenta)、次阻力紫红(clrFuchsia)、主支撑浅青(clrAqua)、次支撑蓝(clrBlue)。注意次线有交叉保护:minorResistance 必须严格小于 majorResistance 才画,minorSupport 必须严格大于 majorSupport 才画,防止主次线重叠误导。 开 MT5 把下面代码塞进你的支撑阻力函数末尾,加载到 EURUSD 小时图,可能看到最多四条彩色水平线标出当前结构位。外汇与贵金属波动剧烈,这些线仅作结构参考,实际突破概率随会话背景变化。
tempHigh = majorResistance;
majorResistance = confirmedHighs[i];
}
else if(confirmedHighs[i] > tempHigh)
{
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 折点画成可延伸的趋势线
下面这段 MT5 脚本把标准 ZigZag 指标加载进来,并在最近 200 根 K 线里抓转折折点,再把下降趋势的高点连线、上升趋势的低点连线向后延伸 100 根,方便肉眼读结构。外汇与贵金属波动剧烈,这类辅助画线仅描述已发生结构,不代表后续必然沿原方向运行。 代码用 iCustom 挂系统自带的 "ZigZag",三个参数 Depth=12、Deviation=5、Backstep=3,属于偏敏感的设置,折点出现频率高,适合抓短线波段。LookBackBars 与 ExtendFutureBars 分别控制回看与前瞻根数,你直接调这两个输入就能改变画线密度。 OnInit 里若句柄返回 INVALID_HANDLE 会打印错误并中断初始化;OnDeinit 则负责删掉名为 Downtrend_HighLine 与 Uptrend_LowLine 的两个对象,避免切换周期时残留旧线。复制进 MT5 新建的 mq5 文件即可编译,实盘前先在 EURUSD 的 M15 上验证折点识别是否符合你的肉眼判断。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ZigZag Analyzer.mq5| class=class="str">"cmt">//| Copyright class="num">2025, MetaQuotes Ltd.| class=class="str">"cmt">//| [MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.0" class="macro">#class="kw">property strict class=class="str">"cmt">// Input parameters input ENUM_TIMEFRAMES InpTimeFrame = PERIOD_CURRENT; class=class="str">"cmt">// Timeframe to analyze input class="type">int ZZ_Depth = class="num">12; class=class="str">"cmt">// ZigZag depth input class="type">int ZZ_Deviation = class="num">5; class=class="str">"cmt">// ZigZag deviation input class="type">int ZZ_Backstep = class="num">3; class=class="str">"cmt">// ZigZag backstep input class="type">int LookBackBars = class="num">200; class=class="str">"cmt">// Bars to search for pivots input class="type">int ExtendFutureBars = class="num">100; class=class="str">"cmt">// Bars to extend trendlines into the future class=class="str">"cmt">// Global indicator handle for ZigZag class="type">int zzHandle; class=class="str">"cmt">// Arrays for ZigZag data and pivot storage class="type">class="kw">double zzBuffer[]; class="type">class="kw">datetime pivotTimes[]; class="type">class="kw">double pivotPrices[]; class="type">bool pivotIsPeak[]; class=class="str">"cmt">// Variable to detect new bars class="type">class="kw">datetime lastBarTime = class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { zzHandle = iCustom(_Symbol, InpTimeFrame, "ZigZag", ZZ_Depth, ZZ_Deviation, ZZ_Backstep); if(zzHandle == INVALID_HANDLE) { Print("Error creating ZigZag handle"); class="kw">return(INIT_FAILED); } class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { ObjectDelete(class="num">0, "Downtrend_HighLine"); ObjectDelete(class="num">0, "Uptrend_LowLine");