zigzag_patterns_framework.pine
Copy
//@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}}")
Show all 668 lines