Indicator · Pine Script v6 · On the chart

ZigZag Patterns [WavesUnchained]

by casoon

This indicator calculates a custom ZigZag engine directly on the price chart to identify market structure and classic chart patterns.

View on GitHub

Repaints

GitHub stars9on the repository
Pine versionv6current
Lines of code668
Settings23inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

668 lines of Pine Script v6 from indicators/market_structure/zigzag_patterns_framework/zigzag_patterns_framework.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

zigzag_patterns_framework.pine
//@version=6
// ============================================================================
// ZigZag Patterns [WavesUnchained]
// Version: 1.2.0
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// ZigZag pivot engine with ABC, triangle, Wolfe, 1-2-3 reversal detection, and
// smart pivot labels.
//
// Features:
//   ✓ Configurable ZigZag pivot engine with stored pivot history
//   ✓ ABC correction detection from pivot sequences
//   ✓ Triangle formation detection
//   ✓ Wolfe wave pattern detection
//   ✓ 1-2-3 reversal detection (Sperandeo) with prior-trend precondition and break confirmation
//   ✓ Smart pivot labels (HH / LH / HL / LL)
//
// ============================================================================
indicator("ZigZag Patterns [WavesUnchained]", overlay=true, max_lines_count=500, max_labels_count=500)

// ─────────────────────────────────────────────────────────────────────────────
// Inputs
// ─────────────────────────────────────────────────────────────────────────────
groupZZ = "ZigZag"
len      = input.int(5, "Pivot Length", minval=2, group=groupZZ)
maxPiv   = input.int(80, "Max Pivots stored", minval=10, maxval=300, group=groupZZ)
maxDraw  = input.int(200, "Max ZigZag lines", minval=20, maxval=500, group=groupZZ)
showZZ   = input.bool(true, "Draw ZigZag", group=groupZZ)
showLbl  = input.bool(false, "Pivot Labels (HH/LH/HL/LL)", group=groupZZ)

groupLbl = "Smart Pivot Labels"
enableSmartLbl = input.bool(true, "Enable Smart Labels", group=groupLbl)
lblMinSignals   = input.int(1, "Min signals to show label", minval=1, maxval=4, group=groupLbl)
showHHLL        = input.bool(true, "Show HH/LH/HL/LL", group=groupLbl)
showMomentumDiv = input.bool(false, "RSI Divergence Markers (annotation only)", group=groupLbl)
rsiLen          = input.int(14, "RSI Len", minval=2, group=groupLbl)
useWickReject   = input.bool(true, "Wick rejection", group=groupLbl)
useLegStrength  = input.bool(true, "Leg strength (Impulse/Correction)", group=groupLbl)

groupPat = "Patterns"
enableABC      = input.bool(true, "ABC (simple)", group=groupPat)
enableTri      = input.bool(true, "Triangles (sym / rising / falling)", group=groupPat)
enableWolfe    = input.bool(true, "Wolfe (quality wedge)", group=groupPat)
enable123      = input.bool(true, "1-2-3 Reversal (Sperandeo)", group=groupPat)

groupFilt = "Filters"
atrLen    = input.int(14, "ATR Len", group=groupFilt)
flatATR   = input.float(0.25, "Flat tolerance (ATR mult)", step=0.05, group=groupFilt)
minRetr   = input.float(0.382, "ABC min retrace", step=0.001, group=groupFilt)
maxRetr   = input.float(0.886, "ABC max retrace", step=0.001, group=groupFilt)
minLegATR = input.float(0.8, "Min leg size (ATR) to count as impulse", step=0.1, group=groupFilt)
wickATR   = input.float(0.6, "Min wick (ATR) for rejection", step=0.1, group=groupFilt)

atr = ta.atr(atrLen)
rsi = ta.rsi(close, rsiLen)

// ─────────────────────────────────────────────────────────────────────────────
// Storage
// ─────────────────────────────────────────────────────────────────────────────
// pivotType: +1 = High pivot, -1 = Low pivot
var float[] pivPrice = array.new_float()
var int[]   pivIndex = array.new_int()
var int[]   pivType  = array.new_int()

// drawing objects (zigzag)
var line[]  zzLines  = array.new_line()
var label[] zzLabels = array.new_label()

// pattern drawings (keep lightweight)
var line[]  patLines = array.new_line()
var label[] patLbls  = array.new_label()

// 1-2-3 Reversal (Sperandeo) — pending setup state, resolved by a price break, not a new pivot
var bool  p123Pending = false
var int   p123Dir     = 0      // +1 = bullish reversal (point1 = Low), -1 = bearish reversal (point1 = High)
var float p123P1      = na
var float p123P2      = na
var float p123P3      = na
var float p123Retr    = na

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
f_cap_lines(line[] arr, int cap) =>
    while array.size(arr) > cap
        line.delete(array.shift(arr))

f_cap_labels(label[] arr, int cap) =>
    while array.size(arr) > cap
        label.delete(array.shift(arr))

f_push_pivot(int pIndex, float pPrice, int pType) =>
    // enforce alternation: if same type as last, replace if more extreme
    bool added = false
    int n = array.size(pivType)
    if n > 0 and array.get(pivType, n - 1) == pType
        float lastPrice = array.get(pivPrice, n - 1)
        bool replace = pType == 1 ? (pPrice > lastPrice) : (pPrice < lastPrice)
        if replace
            array.set(pivPrice, n - 1, pPrice)
            array.set(pivIndex, n - 1, pIndex)
    else
        array.push(pivType,  pType)
        array.push(pivPrice, pPrice)
        array.push(pivIndex, pIndex)
        // cap pivots
        while array.size(pivType) > maxPiv
            array.shift(pivType)
            array.shift(pivPrice)
            array.shift(pivIndex)
        added := true
    added

f_dir_tag(int i) =>
    // HH/LH/HL/LL vs previous same-type pivot
    int t = array.get(pivType, i)
    float v = array.get(pivPrice, i)

    // find previous pivot of same type
    float prev = na
    if i >= 1
        for k = i - 1 to 0 by 1
            if array.get(pivType, k) == t
                prev := array.get(pivPrice, k)
                break
    string tag = "NA"
    if not na(prev)
        if t == 1
            tag := v > prev ? "HH" : "LH"
        else
            tag := v > prev ? "HL" : "LL"
    tag

f_draw_last_segment() =>
    int n = array.size(pivType)
    if showZZ and n >= 2
        int i1 = array.get(pivIndex, n - 2)
        float y1 = array.get(pivPrice, n - 2)
        int i2 = array.get(pivIndex, n - 1)
        float y2 = array.get(pivPrice, n - 1)

        line ln = line.new(i1, y1, i2, y2, xloc=xloc.bar_index, extend=extend.none)
        array.push(zzLines, ln)
        f_cap_lines(zzLines, maxDraw)

        if showLbl
            string tag = f_dir_tag(n - 1)
            int t = array.get(pivType, n - 1)
            label lb = label.new(i2, y2, tag, xloc=xloc.bar_index, style=(t == 1 ? label.style_label_down : label.style_label_up))
            array.push(zzLabels, lb)
            f_cap_labels(zzLabels, 250)

f_clear_patterns_if_too_many() =>
    f_cap_lines(patLines, 120)
    f_cap_labels(patLbls, 120)

// ─────────────────────────────────────────────────────────────────────────────
// Pattern detection helpers
// ─────────────────────────────────────────────────────────────────────────────
f_lastN_ok(int need) =>
    array.size(pivType) >= need

f_get(int back, int which) =>
    // back=0 => last pivot, back=1 => previous...
    int n = array.size(pivType)
    int idx = n - 1 - back
    which == 0 ? array.get(pivPrice, idx) : which == 1 ? float(array.get(pivIndex, idx)) : float(array.get(pivType, idx))

f_line(int x1, float y1, int x2, float y2, st, int w) =>
    line ln = line.new(x1, y1, x2, y2, xloc=xloc.bar_index, extend=extend.none, style=st, width=w)
    array.push(patLines, ln)
    ln

f_label(int x, float y, string txt, bool up) =>
    label lb = label.new(x, y, txt, xloc=xloc.bar_index, style=(up ? label.style_label_up : label.style_label_down))
    array.push(patLbls, lb)
    lb

f_pivot_label(int x, float y, string txt, bool up) =>
    label lb = label.new(x, y, txt, xloc=xloc.bar_index, style=(up ? label.style_label_up : label.style_label_down))
    array.push(zzLabels, lb)
    f_cap_labels(zzLabels, 250)
    lb

// ─────────────────────────────────────────────────────────────────────────────
// Smart Label logic
// ─────────────────────────────────────────────────────────────────────────────
f_wick_reject_score(int pivotBar, int pType) =>
    // Use the pivot candle itself (at pivotBar). We are in realtime bar_index now,
    // so we refer by offset = bar_index - pivotBar
    bool rej = false
    string txt = ""
    int off = bar_index - pivotBar
    if off >= 0 and off <= 2000
        float o = open[off]
        float h = high[off]
        float l = low[off]
        float c = close[off]
        float upW = h - math.max(o, c)
        float dnW = math.min(o, c) - l
        float body = math.abs(c - o)

        if pType == 1
            // High pivot: big upper wick, small-ish body
            rej := upW >= atr * wickATR and upW > dnW and body <= upW
            txt := rej ? "Rej↑" : ""
        else
            // Low pivot: big lower wick
            rej := dnW >= atr * wickATR and dnW > upW and body <= dnW
            txt := rej ? "Rej↓" : ""
    [rej, txt]

f_leg_strength_txt() =>
    // Needs at least 3 pivots: measure last leg size vs prev leg size
    bool ok = false
    string txt = ""
    if f_lastN_ok(3)
        int n = array.size(pivType)
        float C = array.get(pivPrice, n - 1)
        float B = array.get(pivPrice, n - 2)
        float A = array.get(pivPrice, n - 3)
        float leg1 = math.abs(B - A)
        float leg2 = math.abs(C - B)
        bool impulse = leg2 >= atr * minLegATR and (leg2 > leg1 * 1.15)
        bool correction = (leg2 < leg1 * 0.85)
        txt := impulse ? "Imp" : correction ? "Corr" : ""
        ok := impulse or correction
    [ok, txt]

f_rsi_div_txt(int pType) =>
    bool div = false
    string txt = ""
    if showMomentumDiv and f_lastN_ok(3)
        int n = array.size(pivType)
        // we compare current pivot with previous same-type pivot
        int iCur = n - 1
        int iPrev = na
        for k = iCur - 1 to 0 by 1
            if array.get(pivType, k) == pType
                iPrev := k
                break
        if not na(iPrev)
            int xCur = array.get(pivIndex, iCur)
            int xPrev = array.get(pivIndex, iPrev)
            int offCur = bar_index - xCur
            int offPrev = bar_index - xPrev
            if offCur >= 0 and offPrev >= 0 and offCur <= 2000 and offPrev <= 2000
                float pCur = array.get(pivPrice, iCur)
                float pPrev = array.get(pivPrice, iPrev)
                float rCur = rsi[offCur]
                float rPrev = rsi[offPrev]

                if pType == 1
                    // bearish div: price HH, RSI lower high
                    div := (pCur > pPrev) and (rCur < rPrev)
                else
                    // bullish div: price LL, RSI higher low
                    div := (pCur < pPrev) and (rCur > rPrev)
                txt := div ? "RSI div" : ""
    [div, txt]

f_make_smart_label() =>
    if enableSmartLbl and f_lastN_ok(2)
        int n = array.size(pivType)
        int pType = array.get(pivType, n - 1)
        int pBar  = array.get(pivIndex, n - 1)
        float pPx = array.get(pivPrice, n - 1)

        int signals = 0
        string txt = ""

        if showHHLL
            string hhll = f_dir_tag(n - 1)
            if hhll != "NA"
                txt += hhll
                signals += 1

        // Leg strength (Imp/Corr)
        if useLegStrength
            [okLeg, legTxt] = f_leg_strength_txt()
            if okLeg and legTxt != ""
                txt += (txt == "" ? "" : " | ") + legTxt
                signals += 1

        // Wick rejection
        if useWickReject
            [okW, wTxt] = f_wick_reject_score(pBar, pType)
            if okW and wTxt != ""
                txt += (txt == "" ? "" : " | ") + wTxt
                signals += 1

        // Draw RSI divergence as a separate OPTIONAL momentum annotation.
        // This is a quality overlay only — it never contributes to `signals`
        // and never alters the structural HH/LH/HL/LL pivot label.
        if showMomentumDiv
            [isDiv, divTxt] = f_rsi_div_txt(pType)
            if isDiv
                color divColor = pType == 1 ? color.new(color.red, 20) : color.new(color.green, 20)
                label.new(pBar, pPx, text=pType == 1 ? "Bear Div" : "Bull Div", 
                          xloc=xloc.bar_index, 
                          style=pType == 1 ? label.style_label_down : label.style_label_up,
                          textcolor=color.white,
                          color=divColor,
                          size=size.small)

        if signals >= lblMinSignals and txt != ""
            // position: highs label down, lows label up
            f_pivot_label(pBar, pPx, txt, pType == -1)

// ─────────────────────────────────────────────────────────────────────────────
// ABC Pattern (simple)
// last 3 pivots must be alternating: Low-High-Low (bullish) or High-Low-High (bearish)
// retrace filter on BC vs AB
// ─────────────────────────────────────────────────────────────────────────────
f_scan_abc() =>
    if enableABC and f_lastN_ok(3)
        float C = f_get(0, 0)
        float B = f_get(1, 0)
        float A = f_get(2, 0)
        int Ci = int(f_get(0, 1))
        int Bi = int(f_get(1, 1))
        int Ai = int(f_get(2, 1))
        int Ct = int(f_get(0, 2))
        int Bt = int(f_get(1, 2))
        int At = int(f_get(2, 2))

        // bullish: A low, B high, C low (higher low preferred)
        bool bull = (At == -1 and Bt == 1 and Ct == -1)
        // bearish: A high, B low, C high (lower high preferred)
        bool bear = (At ==  1 and Bt == -1 and Ct ==  1)

        if bull
            float AB = math.abs(B - A)
            float BC = math.abs(B - C)
            float retr = AB > 0 ? (BC / AB) : na
            bool ok = not na(retr) and retr >= minRetr and retr <= maxRetr and C > A
            if ok
                f_line(Ai, A, Bi, B, line.style_dotted, 1)
                f_line(Bi, B, Ci, C, line.style_dotted, 1)
                f_label(Ci, C, "ABC Bull", true)

        if bear
            float AB = math.abs(A - B)
            float BC = math.abs(C - B)
            float retr = AB > 0 ? (BC / AB) : na
            bool ok = not na(retr) and retr >= minRetr and retr <= maxRetr and C < A
            if ok
                f_line(Ai, A, Bi, B, line.style_dotted, 1)
                f_line(Bi, B, Ci, C, line.style_dotted, 1)
                f_label(Ci, C, "ABC Bear", false)

// ─────────────────────────────────────────────────────────────────────────────
// Triangles (use last 5 pivots)
// Sym: highs down + lows up
// Rising: highs ~ flat + lows up
// Falling: lows ~ flat + highs down
// ─────────────────────────────────────────────────────────────────────────────
f_scan_triangles() =>
    if enableTri and f_lastN_ok(5)
        // points 1..5 (oldest to newest among last 5)
        int n = array.size(pivType)
        int i1 = n - 5
        int i5 = n - 1

        float p5 = array.get(pivPrice, i5)
        int x5 = array.get(pivIndex, i5)
        int t5 = array.get(pivType, i5)

        // Need alternating types
        bool alt = true
        for k = i1 to i5 - 1
            if array.get(pivType, k) == array.get(pivType, k + 1)
                alt := false
        if alt
            // Extract highs and lows among these 5 (3 of one side, 2 of the other)
            float[] highs = array.new_float()
            int[]   highx = array.new_int()
            float[] lows  = array.new_float()
            int[]   lowx  = array.new_int()

            for k = i1 to i5
                int tt = array.get(pivType, k)
                if tt == 1
                    array.push(highs, array.get(pivPrice, k))
                    array.push(highx, array.get(pivIndex, k))
                else
                    array.push(lows, array.get(pivPrice, k))
                    array.push(lowx, array.get(pivIndex, k))

            float tol = atr * flatATR

            bool have2H = array.size(highs) >= 2
            bool have2L = array.size(lows)  >= 2
            if have2H and have2L
                float h1 = array.get(highs, 0)
                float h2 = array.get(highs, 1)
                float l1 = array.get(lows,  0)
                float l2 = array.get(lows,  1)

                bool highsDown = h2 < h1
                bool lowsUp    = l2 > l1

                bool highsFlat = math.abs(h2 - h1) <= tol
                bool lowsFlat  = math.abs(l2 - l1) <= tol

                // Draw boundary lines using first two highs/lows found
                int hx1 = array.get(highx, 0)
                int hx2 = array.get(highx, 1)
                int lx1 = array.get(lowx,  0)
                int lx2 = array.get(lowx,  1)

                // Classes are exclusive: a side within the flat tolerance counts as
                // flat only, never also as falling/rising.
                // Sym triangle: highs down + lows up, neither side flat
                if highsDown and lowsUp and not highsFlat and not lowsFlat
                    f_line(hx1, h1, hx2, h2, line.style_solid, 1)
                    f_line(lx1, l1, lx2, l2, line.style_solid, 1)
                    f_label(x5, p5, "Triangle Sym", t5 == -1)
                else if highsFlat and lowsUp and not lowsFlat  // Rising triangle: highs flat + lows up
                    f_line(hx1, h1, hx2, h2, line.style_solid, 1)
                    f_line(lx1, l1, lx2, l2, line.style_solid, 1)
                    f_label(x5, p5, "Triangle Rising", t5 == -1)
                else if lowsFlat and highsDown and not highsFlat  // Falling triangle: lows flat + highs down
                    f_line(hx1, h1, hx2, h2, line.style_solid, 1)
                    f_line(lx1, l1, lx2, l2, line.style_solid, 1)
                    f_label(x5, p5, "Triangle Falling", t5 == -1)

// ─────────────────────────────────────────────────────────────────────────────
// Wolfe (quality wedge) using last 5 pivots (confirmed)
// Basic: contracting + alternating + point5 overshoot towards wedge edge
// EPA line: point1 -> point4
// ─────────────────────────────────────────────────────────────────────────────
f_scan_wolfe() =>
    if enableWolfe and f_lastN_ok(5)
        int n = array.size(pivType)
        int i1 = n - 5
        int i2 = n - 4
        int i3 = n - 3
        int i4 = n - 2
        int i5 = n - 1

        float P1 = array.get(pivPrice, i1)
        float P2 = array.get(pivPrice, i2)
        float P3 = array.get(pivPrice, i3)
        float P4 = array.get(pivPrice, i4)
        float P5 = array.get(pivPrice, i5)
        int X1 = array.get(pivIndex, i1)
        int X2 = array.get(pivIndex, i2)
        int X3 = array.get(pivIndex, i3)
        int X4 = array.get(pivIndex, i4)
        int X5 = array.get(pivIndex, i5)
        int T1 = array.get(pivType, i1)
        int T2 = array.get(pivType, i2)
        int T3 = array.get(pivType, i3)
        int T4 = array.get(pivType, i4)
        int T5 = array.get(pivType, i5)

        bool alt = (T1 != T2 and T2 != T3 and T3 != T4 and T4 != T5)
        if alt
            // Define wedge sides as (1-3) and (2-4)
            // Contracting heuristic: range between lines at X4 smaller than at X2 (approx)
            float rangeStart = math.abs(P1 - P2)
            float rangeMid   = math.abs(P3 - P4)
            bool contracting = rangeMid < rangeStart

            // Overshoot heuristic: point5 extends beyond its side direction
            // If last pivot is Low (T5=-1), expect P5 < P3 (lower low) in wedge context.
            // If last pivot is High (T5=+1), expect P5 > P3 (higher high).
            bool overshoot = (T5 == -1 and P5 < P3) or (T5 == 1 and P5 > P3)

            // Simple time sanity: pivots must be spaced (avoid micro noise)
            bool spaced = (X5 - X1) >= (len * 3)

            if contracting and overshoot and spaced
                // Draw wedge sides (thin)
                f_line(X1, P1, X3, P3, line.style_dashed, 1)
                f_line(X2, P2, X4, P4, line.style_dashed, 1)

                // EPA / projection: line 1 -> 4 extended to X5
                line epa = line.new(X1, P1, X4, P4, xloc=xloc.bar_index, extend=extend.right, style=line.style_dotted)
                array.push(patLines, epa)

                // Label at point 5
                f_label(X5, P5, "Wolfe", T5 == -1)

// ─────────────────────────────────────────────────────────────────────────────
// 1-2-3 Reversal (Sperandeo)
// Point 1 = trend extreme, Point 2 = retracement pivot, Point 3 = failed
// continuation (does not exceed Point 1). Requires a genuine prior trend into
// Point 1 (2 same-direction HH/HL-style legs immediately before it) — otherwise
// three alternating pivots are just noise, not a Sperandeo reversal.
// The actual trigger is a later close-through of Point 2's level, checked every
// bar via f_detect_123_break() below, not just on new pivots.
// ─────────────────────────────────────────────────────────────────────────────
f_score_123(float P1, float P2, float P3, float retr, float breakAtr) =>
    float retrMid    = (minRetr + maxRetr) / 2
    float retrSpan   = (maxRetr - minRetr) / 2
    float retrScore  = retrSpan > 0 ? (1 - math.min(math.abs(retr - retrMid) / retrSpan, 1)) * 35 : 0.0
    float legAB      = math.abs(P2 - P1)
    float failMargin = legAB > 0 ? math.abs(P1 - P3) / legAB : 0.0
    float failScore  = math.min(failMargin, 1) * 35
    float breakScore = math.min(math.max(breakAtr, 0), 1) * 30
    int(math.round(retrScore + failScore + breakScore))

// Pure detection — reads pivot history only, no global state mutation (Pine
// functions may not reassign outer `var` scalars; the caller applies the result).
f_detect_123() =>
    bool  valid = false
    bool  bull  = false
    float oP1 = na
    float oP2 = na
    float oP3 = na
    float oRetr = na
    int   oX1 = na
    int   oX2 = na
    int   oX3 = na

    if enable123 and f_lastN_ok(4)
        int n = array.size(pivType)
        int iP1 = n - 3
        int iP2 = n - 2
        int iP3 = n - 1

        float P1 = array.get(pivPrice, iP1)
        float P2 = array.get(pivPrice, iP2)
        float P3 = array.get(pivPrice, iP3)
        int X1 = array.get(pivIndex, iP1)
        int X2 = array.get(pivIndex, iP2)
        int X3 = array.get(pivIndex, iP3)
        int T1 = array.get(pivType, iP1)
        int T2 = array.get(pivType, iP2)
        int T3 = array.get(pivType, iP3)

        bool alt = (T1 != T2) and (T2 != T3) and (T1 == T3)
        if alt
            bool bullSetup = T1 == -1
            bool bearSetup = T1 == 1

            string tagP1   = f_dir_tag(iP1)
            string tagPrev = f_dir_tag(iP1 - 1)
            bool priorTrendOk = false
            if bullSetup
                priorTrendOk := tagP1 == "LL" and tagPrev == "LH"
            else if bearSetup
                priorTrendOk := tagP1 == "HH" and tagPrev == "HL"

            bool p3Fails = false
            if bullSetup
                p3Fails := P3 > P1
            else if bearSetup
                p3Fails := P3 < P1

            float legAB = math.abs(P2 - P1)
            float legBC = math.abs(P2 - P3)
            float retr = legAB > 0 ? legBC / legAB : na

            if priorTrendOk and p3Fails and not na(retr) and retr >= minRetr and retr <= maxRetr
                valid := true
                bull  := bullSetup
                oP1 := P1
                oP2 := P2
                oP3 := P3
                oRetr := retr
                oX1 := X1
                oX2 := X2
                oX3 := X3

    [valid, bull, oP1, oP2, oP3, oRetr, oX1, oX2, oX3]

// Break of Point 2's level — the actual reversal trigger. Evaluated every
// confirmed bar (independent of new pivots) since this is a price event, not a
// pivot event. Pure — only reads p123* state, caller applies the mutation.
f_detect_123_break() =>
    bool brokeUp    = false
    bool brokeDown  = false
    bool failedUp   = false
    bool failedDown = false
    if p123Pending and barstate.isconfirmed
        brokeUp    := p123Dir == 1  and close > p123P2
        brokeDown  := p123Dir == -1 and close < p123P2
        failedUp   := p123Dir == 1  and close < p123P1
        failedDown := p123Dir == -1 and close > p123P1
    [brokeUp, brokeDown, failedUp, failedDown]

// ─────────────────────────────────────────────────────────────────────────────
// Pivot detection (confirmed)
// ─────────────────────────────────────────────────────────────────────────────
ph = ta.pivothigh(high, len, len)
pl = ta.pivotlow(low, len, len)

bool newPivot = false

// If both occur (rare), add in a stable order based on last pivot type
if not na(ph) or not na(pl)
    int pBar = bar_index - len

    if not na(ph) and not na(pl)
        // choose order: if last was high, add low first, else add high first
        int lastT = array.size(pivType) > 0 ? array.get(pivType, array.size(pivType) - 1) : 0
        if lastT == 1
            newPivot := f_push_pivot(pBar, pl, -1) or newPivot
            newPivot := f_push_pivot(pBar, ph,  1) or newPivot
        else
            newPivot := f_push_pivot(pBar, ph,  1) or newPivot
            newPivot := f_push_pivot(pBar, pl, -1) or newPivot
    else if not na(ph)
        newPivot := f_push_pivot(pBar, ph, 1) or newPivot
    else
        newPivot := f_push_pivot(pBar, pl, -1) or newPivot

// ─────────────────────────────────────────────────────────────────────────────
// Run drawing + scans only when a new pivot is added (saves CPU + avoids spam)
// ─────────────────────────────────────────────────────────────────────────────
if newPivot
    f_draw_last_segment()
    f_make_smart_label()
    // keep pattern drawings bounded
    f_clear_patterns_if_too_many()
    f_scan_abc()
    f_scan_triangles()
    f_scan_wolfe()

    // 1-2-3 Reversal (Sperandeo) — detection is pure; apply the resulting
    // state + drawing here, since only global-scope if-blocks (not functions)
    // may reassign `var` globals in Pine.
    [v123, bull123, P1_123, P2_123, P3_123, retr123, X1_123, X2_123, X3_123] = f_detect_123()
    // Point 3 is only known `len` bars after it formed. If price already closes
    // beyond Point 2 at confirmation, the break happened before the setup existed —
    // cancel it instead of firing a trigger from that earlier movement.
    bool late123 = v123 and ((bull123 and close > P2_123) or (not bull123 and close < P2_123))
    if v123 and not late123
        f_line(X1_123, P1_123, X2_123, P2_123, line.style_dotted, 1)
        f_line(X2_123, P2_123, X3_123, P3_123, line.style_dotted, 1)
        f_label(X3_123, P3_123, bull123 ? "1-2-3 Bull" : "1-2-3 Bear", bull123)

        line lvl123 = line.new(X2_123, P2_123, X3_123, P2_123, xloc=xloc.bar_index, extend=extend.right, style=line.style_dashed, width=1)
        array.push(patLines, lvl123)

        p123Pending := true
        p123Dir     := bull123 ? 1 : -1
        p123P1      := P1_123
        p123P2      := P2_123
        p123P3      := P3_123
        p123Retr    := retr123

// 1-2-3 break confirmation — every confirmed bar, not only on new pivots
bool long123Signal  = false
bool short123Signal = false

[brokeUp123, brokeDown123, failedUp123, failedDown123] = f_detect_123_break()
if brokeUp123 or brokeDown123
    float breakAtr123 = brokeUp123 ? (close - p123P2) / atr : (p123P2 - close) / atr
    int score123 = f_score_123(p123P1, p123P2, p123P3, p123Retr, breakAtr123)
    string txt123 = (p123Dir == 1 ? "1-2-3 Long Q" : "1-2-3 Short Q") + str.tostring(score123)
    f_label(bar_index, close, txt123, p123Dir == 1)
    long123Signal  := p123Dir == 1
    short123Signal := p123Dir == -1
    p123Pending := false
else if failedUp123 or failedDown123
    p123Pending := false

// Alerts (optional basic)
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (newPivot), "New Pivot", "ZPF · NEW PIVOT · {{ticker}} {{interval}}")
alertcondition(alertOk and long123Signal,  "1-2-3 Long",  "ZPF · 1-2-3 LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and short123Signal, "1-2-3 Short", "ZPF · 1-2-3 SHORT · {{ticker}} {{interval}}")

Overview

ZigZag Patterns [WavesUnchained] automatically detects ABC corrections, symmetrical and sloped triangles, Wolfe waves, and Sperandeo 1-2-3 reversal patterns. Optional smart labels highlight structural pivots (HH, LH, HL, LL), wick rejections, leg strength, and RSI momentum divergences. Key settings allow adjusting pivot length, ATR filter thresholds, Fibonacci retracement tolerances, and specific pattern toggles. As a ZigZag indicator, pivot points and connecting lines dynamically update and repaint until confirmed by the pivot length lookback.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance, Trend, Divergence

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

  • Signals can change on the live bar or after it closes (repainting)

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Pivot Lengthint5len
Max Pivots storedint80maxPiv
Max ZigZag linesint200maxDraw
Draw ZigZagbooltrueshowZZ
Pivot Labels (HH/LH/HL/LL)boolfalseshowLbl
Enable Smart LabelsbooltrueenableSmartLbl
Min signals to show labelint1lblMinSignals
Show HH/LH/HL/LLbooltrueshowHHLL
RSI Divergence Markers (annotation only)boolfalseshowMomentumDiv
RSI Lenint14rsiLen
Wick rejectionbooltrueuseWickReject
Leg strength (Impulse/Correction)booltrueuseLegStrength
Show all 23 settings
SettingTypeDefaultVariable
ABC (simple)booltrueenableABC
Triangles (sym / rising / falling)booltrueenableTri
Wolfe (quality wedge)booltrueenableWolfe
1-2-3 Reversal (Sperandeo)booltrueenable123
ATR Lenint14atrLen
Flat tolerance (ATR mult)float0.25flatATR
ABC min retracefloat0.382minRetr
ABC max retracefloat0.886maxRetr
Min leg size (ATR) to count as impulsefloat0.8minLegATR
Min wick (ATR) for rejectionfloat0.6wickATR
Alerts only on bar close (confirmed)booltruealertsOnBarClose
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

575 stars

This indicator detects equal highs and lows by comparing prices within user-defined left and right bars using a tolerance percentage. It plots horizontal lines on the price chart for equal highs in one color and lows in another. A screener allows input of mult

Indicator v5 Support & resistanceTrend
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows with Screener

Function - Linear Regression

by ricardosantos79
261 stars

This indicator fits a linear regression through stored pivot highs and lows and draws the resulting channel on the price chart. A function returns predicted values, maximum and minimum deviations, and the average absolute deviation. Confirmed two-bar pivots up

Indicator v5 TrendSupport & resistance
Updated 4 years ago · None stated GitHub repository of Function - Linear Regression

AI Trend

by dearvn
257 stars

AI Trend is an indicator that plots pivot points, trend lines, and support and resistance levels directly on the price chart. It marks high and low pivots based on user-defined length, draws trend lines that check for violations at specified points, and identi

Indicator v5 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of AI Trend

Ichimoku Cloud

by harryguiacorn
219 stars

This indicator plots Ichimoku Cloud elements on the price chart, including Tenkan-sen, Kijun-sen, Senkou Span A and B lines with optional Chikou Span. It signals trend changes via Tenkan-sen and Kijun-sen crosses and places arrows at Kijun-sen direction shifts

Indicator v5 TrendMoving averages
Updated 1 year ago · None stated GitHub repository of Ichimoku Cloud
219 stars

This indicator identifies local swing highs and swing lows to construct a Zig Zag pattern on the main price chart. It evaluates pivot points based on a customizable number of surrounding left and right bars. Using persistent variables, the script tracks previo

Indicator v4 Support & resistanceTrend Repaints
Updated 5 years ago · None stated GitHub repository of Teaching: Finding Highs and Lows 6

Dynamic Linear Regression

by TradersPost
218 stars

Dynamic Linear Regression calculates and plots linear regression trendlines and standard deviation channels directly on the price chart over a user-selected time period. Traders can configure static channels locked at the end time, dynamic channels that contin

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Dynamic Linear Regression

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers