Indicator · Pine Script v6 · On the chart

Vein: Structure & Zones 4H [WavesUnchained]

by casoon

Vein Structure & Zones 4H combines market structure event detection with automated support and resistance zone mapping directly on the price chart.

View on GitHub

Repaints

GitHub stars9on the repository
Pine versionv6current
Lines of code1,581
Settings40inputs
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

1,581 lines of Pine Script v6 from indicators/trend_direction/vein/vein_structure_zones.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

vein_structure_zones.pine
//@version=6
// ============================================================================
// Vein Structure & Zones 4H [WavesUnchained]
// Version: 0.3.2
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Combined structural event detection and automatic zone management.
// Merges structure_events and levels_zones into a single overlay.
//
// Features:
//   ✓ Micro/Major swing detection with quality grading
//   ✓ BOS (Micro/Major) with quality
//   ✓ Spring / Upthrust with quality
//   ✓ Climax sequence state machine (CLX→TST→BOS)
//   ✓ Rejection candles
//   ✓ Auto S/R zones from Swings, Springs, BOS Flips, Climax
//   ✓ Zone lifecycle: Fresh → Tested → Confirmed → Broken → Flipped
//   ✓ Priority-based zone rendering
//   ✓ Light / Debug / Research display modes
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   real trade volume only — MFI/POC/EVR/climax degrade to neutral
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
//
// ============================================================================
indicator("Vein: Structure & Zones 4H [WavesUnchained]", "Vein S&Z", overlay=true, max_labels_count=500, max_lines_count=200, max_boxes_count=200)

// ============================================================================
// MODULE: inputs — structure
// ============================================================================

var gSwing = "Swing / Pivot"
int leftBars       = input.int(3, "Micro Pivot Left Bars", minval=1, maxval=20, group=gSwing)
int rightBars      = input.int(3, "Micro Pivot Right Bars", minval=1, maxval=20, group=gSwing)
int majorLeftBars  = input.int(7, "Major Pivot Left Bars", minval=3, maxval=30, group=gSwing, tooltip="Longer pivots = more significant structure")
int majorRightBars = input.int(7, "Major Pivot Right Bars", minval=3, maxval=30, group=gSwing)

var gStructure = "Structure"
float bosBuffer = input.float(0.0, "BOS Buffer (points)", minval=0.0, step=0.001, group=gStructure)
float eqhEqlThreshold = input.float(0.1, "EQH/EQL Threshold (ATR mult)", minval=0.01, maxval=0.5, step=0.01, group=gStructure, tooltip="Equal Highs/Lows detection: max distance between two swing levels as fraction of ATR")
bool  showEQHEQL      = input.bool(false, "Show EQH/EQL", group=gStructure, tooltip="Equal Highs / Equal Lows — liquidity pool markers. On by default in Debug/Research mode.")

var gWyckoff = "Wyckoff Heuristics"
float minWickRatio     = input.float(0.35, "Min Wick Ratio for Spring/Upthrust", minval=0.1, maxval=0.8, step=0.05, group=gWyckoff)
float minCandleATR     = input.float(0.5, "Min Candle Range / ATR", minval=0.1, step=0.1, group=gWyckoff)
float climaxRangeATR   = input.float(1.8, "Climax: Min Range / ATR", minval=1.0, step=0.1, group=gWyckoff)
float climaxVolFactor  = input.float(1.8, "Climax: Min Volume / Avg", minval=1.0, step=0.1, group=gWyckoff)
float testRangeFactor  = input.float(0.6, "Test: Max Range vs Climax", minval=0.1, maxval=1.0, step=0.1, group=gWyckoff)
float testVolFactor    = input.float(0.7, "Test: Max Volume vs Climax", minval=0.1, maxval=1.0, step=0.1, group=gWyckoff)
int   testLookback     = input.int(10, "Test: Lookback Bars", minval=3, maxval=30, group=gWyckoff)
float rejWickRatio     = input.float(0.5, "Rejection: Min Wick Ratio", minval=0.2, maxval=0.8, step=0.05, group=gWyckoff)
float rejBodyMax       = input.float(0.3, "Rejection: Max Body/Range", minval=0.1, maxval=0.6, step=0.05, group=gWyckoff)

var gQuality = "Event Quality"
float strongWickRatio  = input.float(0.55, "Strong: Min Wick Ratio", minval=0.3, maxval=0.8, step=0.05, group=gQuality)
float strongCandleATR  = input.float(0.8, "Strong: Min Candle / ATR", minval=0.3, step=0.1, group=gQuality)
float strongVolFactor  = input.float(1.3, "Strong: Min Volume / Avg", minval=1.0, step=0.1, group=gQuality)
float strongSweepATR   = input.float(0.15, "Strong: Min Sweep Depth (ATR)", minval=0.0, step=0.05, group=gQuality)

// ============================================================================
// MODULE: inputs — zones
// ============================================================================

var gZones = "Zone Generation"
bool showSwingZones   = input.bool(true, "Major Swing Zones", group=gZones)
bool showSpringZones  = input.bool(true, "Spring / Upthrust Zones", group=gZones)
bool showBOSFlips     = input.bool(true, "BOS Flip Zones", group=gZones)
bool showClimaxZones  = input.bool(true, "Climax Zones", group=gZones)
float zoneWidthATR    = input.float(0.3, "Zone Width (ATR multiple)", minval=0.1, maxval=1.0, step=0.05, group=gZones)
int   maxZones        = input.int(8, "Max Visible Zones", minval=2, maxval=20, group=gZones)
int   zoneExpireBars  = input.int(200, "Zone Expire After (bars)", minval=50, maxval=500, group=gZones)
bool  showBrokenZones = input.bool(false, "Show Broken Zones", group=gZones)

// ============================================================================
// MODULE: inputs — display
// ============================================================================

var gVolume = "Volume"
int volAvgLen = input.int(20, "Volume Average Length", minval=5, group=gVolume)

var gATR = "ATR"
int atrLen = input.int(14, "ATR Length", minval=1, group=gATR)

var gAssess = "Zone Assessment"
float maxAssessDistATR = input.float(3.0, "Max Assessment Distance (ATR)", minval=1.0, step=0.5, group=gAssess)
int   approachBars     = input.int(5, "Approach Lookback Bars", minval=2, maxval=15, group=gAssess)
bool  showAssessTable  = input.bool(false, "Show Assessment Table", group=gAssess, tooltip="Assessment table (top left) — off by default")
bool  showAssessLabels = input.bool(true, "Show Assessment Labels on Zones", group=gAssess)

var gDisplay = "Display"
string displayMode  = input.string("Light", "Display Mode", options=["Light", "Debug", "Research"], group=gDisplay)
bool showSwings     = input.bool(true, "Show Swing Labels", group=gDisplay)
bool showBOS        = input.bool(true, "Show BOS Labels", group=gDisplay)
bool showFakeBreaks = input.bool(true, "Show Fake Break Labels", group=gDisplay)
bool showSpringUT   = input.bool(true, "Show Spring / Upthrust Labels", group=gDisplay)
bool showClimax     = input.bool(true, "Show Climax Labels", group=gDisplay)
bool showTest       = input.bool(true, "Show Test Labels", group=gDisplay)
bool showRejection  = input.bool(true, "Show Rejection Labels", group=gDisplay)
bool showZoneLabels = input.bool(true, "Show Zone Labels", group=gDisplay)
color supportColor  = input.color(color.new(#00e676, 0), "Support Zone Color", group=gDisplay)
color resistColor   = input.color(color.new(#ff1744, 0), "Resistance Zone Color", group=gDisplay)
color flipColor     = input.color(color.new(#ffab00, 0), "Flip Zone Color", group=gDisplay)
bool showFVG        = input.bool(true, "Show FVG", group=gDisplay, tooltip="Fair Value Gaps — semi-transparent boxes")
bool showOB         = input.bool(true, "Show Order Blocks", group=gDisplay, tooltip="Order Blocks — last opposing candle before BOS")
bool showPOC        = input.bool(true, "Show POC Level", group=gDisplay, tooltip="Point of Control — highest volume/money flow price level.\nShown as dashed line. Zones near POC get hold bonus.")

// ============================================================================
// MODULE: base calculations
// ============================================================================

float atrValue = ta.atr(atrLen)

// Echtes Handelsvolumen? Nur "tick" schließt aus; "n/a" zählt, wenn der Feed Volumen liefert (DATA_VALIDITY §2).
bool volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)

float avgVol   = volumeIsReal ? ta.sma(volume, volAvgLen) : na
float relVol   = volumeIsReal and avgVol > 0 ? volume / avgVol : 1.0
float candleRange = high - low
float bodySize    = math.abs(close - open)
float bodyToRange = candleRange > 0 ? bodySize / candleRange : 0.0
float upperWick   = high - math.max(open, close)
float lowerWick   = math.min(open, close) - low
float upperWickRatio = candleRange > 0 ? upperWick / candleRange : 0.0
float lowerWickRatio = candleRange > 0 ? lowerWick / candleRange : 0.0
float candleATR = atrValue > 0 ? candleRange / atrValue : 0.0

// Assessment context indicators
float ema20 = ta.ema(close, 20)
float ema50 = ta.ema(close, 50)
float rsi14 = ta.rsi(close, 14)
float mfi14 = volumeIsReal ? ta.mfi(hlc3, 14) : na
float priceChange10 = math.abs(close - close[10])
float pathLength10  = math.sum(math.abs(close - close[1]), 10)
float effRatio      = pathLength10 > 0 ? priceChange10 / pathLength10 : 0.0
bool  isTrendUp     = ema20 > ema50 and effRatio > 0.35
bool  isTrendDown   = ema20 < ema50 and effRatio > 0.35
bool  isRange       = effRatio <= 0.35

// ============================================================================
// MODULE: swing detection — micro + major
// ============================================================================

float pivotHi = ta.pivothigh(high, leftBars, rightBars)
float pivotLo = ta.pivotlow(low, leftBars, rightBars)
bool isSwingHigh = not na(pivotHi)
bool isSwingLow  = not na(pivotLo)

var float lastSwingHigh = na
var float lastSwingLow  = na
var int   lastSwingHighBar = na
var int   lastSwingLowBar  = na

if isSwingHigh
    lastSwingHigh := pivotHi
    lastSwingHighBar := bar_index - rightBars
if isSwingLow
    lastSwingLow := pivotLo
    lastSwingLowBar := bar_index - rightBars

float majorPivotHi = ta.pivothigh(high, majorLeftBars, majorRightBars)
float majorPivotLo = ta.pivotlow(low, majorLeftBars, majorRightBars)
bool isMajorSwingHigh = not na(majorPivotHi)
bool isMajorSwingLow  = not na(majorPivotLo)

var float lastMajorSwingHigh = na
var float lastMajorSwingLow  = na
var int   lastMajorSwingHighBar = na
var int   lastMajorSwingLowBar  = na

if isMajorSwingHigh
    lastMajorSwingHigh := majorPivotHi
    lastMajorSwingHighBar := bar_index - majorRightBars
if isMajorSwingLow
    lastMajorSwingLow := majorPivotLo
    lastMajorSwingLowBar := bar_index - majorRightBars

// ============================================================================
// MODULE: swing trend tracking — CHoCH vs BOS distinction
// ============================================================================
// swingTrend: 1 = bullish, -1 = bearish, 0 = undefined
// microSwingTrend: tracks micro structure direction
// majorSwingTrend: tracks major structure direction

var int microSwingTrend = 0
var int majorSwingTrend = 0

// ============================================================================
// MODULE: Equal Highs / Equal Lows (EQH/EQL) — liquidity pools
// ============================================================================

var float prevSwingHigh    = na
var float prevSwingLow     = na
var int   prevSwingHighBar = na
var int   prevSwingLowBar  = na

bool isEQH = false
bool isEQL = false
float eqhLevel = na
float eqlLevel = na
int   eqhFromBar = na
int   eqlFromBar = na

if isSwingHigh
    if not na(prevSwingHigh) and not na(atrValue) and atrValue > 0
        if math.abs(pivotHi - prevSwingHigh) < eqhEqlThreshold * atrValue
            isEQH := true
            eqhLevel := (pivotHi + prevSwingHigh) / 2.0
            eqhFromBar := prevSwingHighBar
    prevSwingHigh    := pivotHi
    prevSwingHighBar := bar_index - rightBars

if isSwingLow
    if not na(prevSwingLow) and not na(atrValue) and atrValue > 0
        if math.abs(pivotLo - prevSwingLow) < eqhEqlThreshold * atrValue
            isEQL := true
            eqlLevel := (pivotLo + prevSwingLow) / 2.0
            eqlFromBar := prevSwingLowBar
    prevSwingLow    := pivotLo
    prevSwingLowBar := bar_index - rightBars

// ============================================================================
// MODULE: structure-based volume profile — POC + Value Area per swing leg
// ============================================================================

// Recalculates when a new major swing completes, profiling the last leg.
int   vpRows = 15
var float pocLevel    = na
var float vaHighLevel = na
var float vaLowLevel  = na
var int   pocStartBar = na
var float[] vpBins    = array.new_float(vpRows, 0.0)

var int   prevMajorSHBar = na
var int   prevMajorSLBar = na
var float prevMajorSH    = na
var float prevMajorSL    = na

bool newMajorSH = isMajorSwingHigh and majorPivotHi != prevMajorSH
bool newMajorSL = isMajorSwingLow and majorPivotLo != prevMajorSL
bool vpTrigger  = newMajorSH or newMajorSL

if newMajorSH
    prevMajorSH    := majorPivotHi
    prevMajorSHBar := bar_index - majorRightBars
if newMajorSL
    prevMajorSL    := majorPivotLo
    prevMajorSLBar := bar_index - majorRightBars

// Volume Profile / POC has no price-only equivalent (Class C data) — the
// whole recompute is skipped when volume is not real, so pocLevel/vaHigh/
// vaLow stay `na` instead of profiling tick-count noise as if it were size.
if vpTrigger and volumeIsReal
    int legStart = na
    int legEnd   = na
    if newMajorSH and not na(prevMajorSLBar)
        legStart := prevMajorSLBar
        legEnd   := bar_index - majorRightBars
        pocStartBar := legStart
    else if newMajorSL and not na(prevMajorSHBar)
        legStart := prevMajorSHBar
        legEnd   := bar_index - majorRightBars
        pocStartBar := legStart

    if not na(legStart) and not na(legEnd) and legEnd > legStart
        int legLen = legEnd - legStart
        float legHigh = high
        float legLow  = low
        for bk = 0 to math.min(legLen, 200)
            int offset = bar_index - legEnd + bk
            if offset >= 0 and offset < 500
                legHigh := math.max(legHigh, high[offset])
                legLow  := math.min(legLow, low[offset])

        float vpStep = legHigh > legLow ? (legHigh - legLow) / vpRows : 0.0
        if vpStep > 0
            for i = 0 to vpRows - 1
                array.set(vpBins, i, 0.0)
            for bk = 0 to math.min(legLen, 200)
                int offset = bar_index - legEnd + bk
                if offset >= 0 and offset < 500
                    float bHi  = high[offset]
                    float bLo  = low[offset]
                    float bVol = nz(volume[offset])
                    if bVol > 0
                        for row = 0 to vpRows - 1
                            float rowLo = legLow + row * vpStep
                            float rowHi = rowLo + vpStep
                            if bHi >= rowLo and bLo < rowHi
                                float overlap = math.min(bHi, rowHi) - math.max(bLo, rowLo)
                                float barRng  = bHi - bLo
                                float portion = barRng > 0 ? overlap / barRng : 1.0 / vpRows
                                array.set(vpBins, row, array.get(vpBins, row) + bVol * portion)

            int   bestRow = 0
            float bestVol = 0.0
            for i = 0 to vpRows - 1
                if array.get(vpBins, i) > bestVol
                    bestVol := array.get(vpBins, i)
                    bestRow := i
            pocLevel := legLow + (bestRow + 0.5) * vpStep

            float totVol = array.sum(vpBins)
            float vaVol  = bestVol
            int   vaLo   = bestRow
            int   vaHi   = bestRow
            for _iter = 0 to vpRows - 1
                if vaVol >= totVol * 0.70
                    break
                float belowV = vaLo > 0 ? array.get(vpBins, vaLo - 1) : 0.0
                float aboveV = vaHi < vpRows - 1 ? array.get(vpBins, vaHi + 1) : 0.0
                if belowV >= aboveV and vaLo > 0
                    vaLo := vaLo - 1
                    vaVol := vaVol + belowV
                else if vaHi < vpRows - 1
                    vaHi := vaHi + 1
                    vaVol := vaVol + aboveV
                else if vaLo > 0
                    vaLo := vaLo - 1
                    vaVol := vaVol + belowV
                else
                    break
            vaHighLevel := legLow + (vaHi + 1) * vpStep
            vaLowLevel  := legLow + vaLo * vpStep

float pocDistATR  = not na(pocLevel) and atrValue > 0 ? math.abs(close - pocLevel) / atrValue : 999.0
bool  nearPOC     = pocDistATR < 0.5
bool  inValueArea = not na(vaLowLevel) and not na(vaHighLevel) ? close >= vaLowLevel and close <= vaHighLevel : false

var line pocLine   = na
var line vaHiLine  = na
var line vaLoLine  = na
if showPOC and barstate.islast and not na(pocLevel) and not na(pocStartBar)
    line.delete(pocLine)
    line.delete(vaHiLine)
    line.delete(vaLoLine)
    pocLine  := line.new(pocStartBar, pocLevel, bar_index + 5, pocLevel, color=color.new(#ffeb3b, 30), style=line.style_dashed, width=2)
    vaHiLine := line.new(pocStartBar, vaHighLevel, bar_index + 5, vaHighLevel, color=color.new(#ffeb3b, 70), style=line.style_dotted, width=1)
    vaLoLine := line.new(pocStartBar, vaLowLevel, bar_index + 5, vaLowLevel, color=color.new(#ffeb3b, 70), style=line.style_dotted, width=1)

// ============================================================================
// MODULE: break of structure — micro + major
// ============================================================================

bool microBullBOS = not na(lastSwingHigh) and close > lastSwingHigh + bosBuffer and close[1] <= lastSwingHigh + bosBuffer
bool microBearBOS = not na(lastSwingLow) and close < lastSwingLow - bosBuffer and close[1] >= lastSwingLow - bosBuffer
bool majorBullBOS = not na(lastMajorSwingHigh) and close > lastMajorSwingHigh + bosBuffer and close[1] <= lastMajorSwingHigh + bosBuffer
bool majorBearBOS = not na(lastMajorSwingLow) and close < lastMajorSwingLow - bosBuffer and close[1] >= lastMajorSwingLow - bosBuffer
bool bullishBOS = microBullBOS or majorBullBOS
bool bearishBOS = microBearBOS or majorBearBOS

// CHoCH detection: first break against the current trend = Change of Character
// BOS continuation: break in same direction as current trend
bool microBullCHoCH = microBullBOS and microSwingTrend == -1
bool microBearCHoCH = microBearBOS and microSwingTrend == 1
bool majorBullCHoCH = majorBullBOS and majorSwingTrend == -1
bool majorBearCHoCH = majorBearBOS and majorSwingTrend == 1

// Update swing trend after detecting CHoCH/BOS
if microBullBOS
    microSwingTrend := 1
if microBearBOS
    microSwingTrend := -1
if majorBullBOS
    majorSwingTrend := 1
if majorBearBOS
    majorSwingTrend := -1

bool isBullCHoCH = microBullCHoCH or majorBullCHoCH
bool isBearCHoCH = microBearCHoCH or majorBearCHoCH

// ============================================================================
// MODULE: fake breaks, spring, upthrust
// ============================================================================

bool fakeBreakout       = not na(lastSwingHigh) and high > lastSwingHigh and close <= lastSwingHigh
bool fakeBreakdown      = not na(lastSwingLow) and low < lastSwingLow and close >= lastSwingLow
bool majorFakeBreakout  = not na(lastMajorSwingHigh) and high > lastMajorSwingHigh and close <= lastMajorSwingHigh
bool majorFakeBreakdown = not na(lastMajorSwingLow) and low < lastMajorSwingLow and close >= lastMajorSwingLow

bool isSpring        = fakeBreakdown and lowerWickRatio >= minWickRatio and candleATR >= minCandleATR
bool isUpthrust      = fakeBreakout and upperWickRatio >= minWickRatio and candleATR >= minCandleATR
bool isMajorSpring   = majorFakeBreakdown and lowerWickRatio >= minWickRatio and candleATR >= minCandleATR
bool isMajorUpthrust = majorFakeBreakout and upperWickRatio >= minWickRatio and candleATR >= minCandleATR

// ============================================================================
// MODULE: rejection candle
// ============================================================================

bool isBullRejection = lowerWickRatio >= rejWickRatio and bodyToRange <= rejBodyMax and close > open
bool isBearRejection = upperWickRatio >= rejWickRatio and bodyToRange <= rejBodyMax and close < open

// ============================================================================
// MODULE: event quality grading
// ============================================================================

f_eventQuality(bool triggered, float wickR, float cATR, float rv, float sweepD, float sWickR, float sCATR, float sVolF, float sSweep) =>
    int q = 0
    if triggered
        int factors = 0
        if wickR >= sWickR
            factors += 1
        if cATR >= sCATR
            factors += 1
        if rv >= sVolF
            factors += 1
        if sweepD >= sSweep
            factors += 1
        q := factors >= 3 ? 3 : factors >= 1 ? 2 : 1
    q

f_bosQuality(bool triggered, float sweep, float rv, float sVolF) =>
    int q = 0
    if triggered
        int factors = 0
        if sweep >= 0.1
            factors += 1
        if sweep >= 0.3
            factors += 1
        if rv >= sVolF
            factors += 1
        q := factors >= 2 ? 3 : factors >= 1 ? 2 : 1
    q

f_qualityTag(int q) =>
    q == 3 ? "★" : q == 2 ? "" : q == 1 ? "~" : ""

// Spring quality
float springSweepDepth = isSpring and not na(lastSwingLow) and atrValue > 0 ? (lastSwingLow - low) / atrValue : 0.0
float majorSpringSweepDepth = isMajorSpring and not na(lastMajorSwingLow) and atrValue > 0 ? (lastMajorSwingLow - low) / atrValue : 0.0
int springQuality = f_eventQuality(isSpring, lowerWickRatio, candleATR, relVol, springSweepDepth, strongWickRatio, strongCandleATR, strongVolFactor, strongSweepATR)
int majorSpringQuality = f_eventQuality(isMajorSpring, lowerWickRatio, candleATR, relVol, majorSpringSweepDepth, strongWickRatio, strongCandleATR, strongVolFactor, strongSweepATR)

// Upthrust quality
float upthrustSweepDepth = isUpthrust and not na(lastSwingHigh) and atrValue > 0 ? (high - lastSwingHigh) / atrValue : 0.0
float majorUpthrustSweepDepth = isMajorUpthrust and not na(lastMajorSwingHigh) and atrValue > 0 ? (high - lastMajorSwingHigh) / atrValue : 0.0
int upthrustQuality = f_eventQuality(isUpthrust, upperWickRatio, candleATR, relVol, upthrustSweepDepth, strongWickRatio, strongCandleATR, strongVolFactor, strongSweepATR)
int majorUpthrustQuality = f_eventQuality(isMajorUpthrust, upperWickRatio, candleATR, relVol, majorUpthrustSweepDepth, strongWickRatio, strongCandleATR, strongVolFactor, strongSweepATR)

// BOS quality
float bullBOSSweep = microBullBOS and not na(lastSwingHigh) and atrValue > 0 ? (close - lastSwingHigh) / atrValue : 0.0
float majorBullBOSSweep = majorBullBOS and not na(lastMajorSwingHigh) and atrValue > 0 ? (close - lastMajorSwingHigh) / atrValue : 0.0
float bearBOSSweep = microBearBOS and not na(lastSwingLow) and atrValue > 0 ? (lastSwingLow - close) / atrValue : 0.0
float majorBearBOSSweep = majorBearBOS and not na(lastMajorSwingLow) and atrValue > 0 ? (lastMajorSwingLow - close) / atrValue : 0.0
int microBullBOSQuality = f_bosQuality(microBullBOS, bullBOSSweep, relVol, strongVolFactor)
int majorBullBOSQuality = f_bosQuality(majorBullBOS, majorBullBOSSweep, relVol, strongVolFactor)
int microBearBOSQuality = f_bosQuality(microBearBOS, bearBOSSweep, relVol, strongVolFactor)
int majorBearBOSQuality = f_bosQuality(majorBearBOS, majorBearBOSSweep, relVol, strongVolFactor)

// Rejection quality
int bullRejQuality = f_eventQuality(isBullRejection, lowerWickRatio, candleATR, relVol, 0.0, strongWickRatio, strongCandleATR, strongVolFactor, 0.0)
int bearRejQuality = f_eventQuality(isBearRejection, upperWickRatio, candleATR, relVol, 0.0, strongWickRatio, strongCandleATR, strongVolFactor, 0.0)

// ============================================================================
// MODULE: climax candle + sequence state machine
// ============================================================================

// Climax detection — volume confirmation degrades to pass-through (range-only
// extreme bar) when volume is not real, so the Spring/UT/CLX cascade keeps
// functioning on price data alone instead of losing the confirmation layer.
bool isClimaxCandle = candleATR >= climaxRangeATR and (volumeIsReal ? avgVol > 0 and volume >= climaxVolFactor * avgVol : true)
bool isSellingClimax = isClimaxCandle and close < open
bool isBuyingClimax  = isClimaxCandle and close > open

var int   clxState    = 0
var int   clxDirection = 0
var float clxClose    = na
var float clxHigh     = na
var float clxLow      = na
var float clxRange    = na
var float clxVolume   = na
var int   clxBar      = na
var float clxReactionExtreme = na
var int   clxTestBar  = na

if isClimaxCandle
    clxState    := 1
    clxDirection := isSellingClimax ? 1 : isBuyingClimax ? -1 : 0
    clxClose    := close
    clxHigh     := high
    clxLow      := low
    clxRange    := candleRange
    clxVolume   := volume
    clxBar      := bar_index
    clxReactionExtreme := na
    clxTestBar  := na

bool hasActiveClx = clxState >= 1 and not na(clxBar) and (bar_index - clxBar) <= testLookback * 2

if hasActiveClx and clxState == 1
    if clxDirection == 1 and close > clxClose
        clxState := 2
        clxReactionExtreme := high
    if clxDirection == -1 and close < clxClose
        clxState := 2
        clxReactionExtreme := low

if hasActiveClx and clxState == 2
    if clxDirection == 1
        clxReactionExtreme := math.max(nz(clxReactionExtreme), high)
    if clxDirection == -1
        clxReactionExtreme := math.min(nz(clxReactionExtreme, high), low)
    bool returningToZone = false
    if clxDirection == 1 and not na(clxClose) and atrValue > 0
        returningToZone := close < clxClose + 1.5 * atrValue and close > clxLow - 0.5 * atrValue
    if clxDirection == -1 and not na(clxClose) and atrValue > 0
        returningToZone := close > clxClose - 1.5 * atrValue and close < clxHigh + 0.5 * atrValue
    if returningToZone and (bar_index - clxBar) >= 2
        clxState := 3

if hasActiveClx and clxState == 3
    bool isTestBar = not na(clxRange) and candleRange <= clxRange * testRangeFactor and candleRange > 0 and (volumeIsReal ? not na(clxVolume) and volume <= clxVolume * testVolFactor : true)
    bool inZone = not na(clxClose) and atrValue > 0 and math.abs(close - clxClose) <= 1.5 * atrValue
    if isTestBar and inZone
        clxState   := 4
        clxTestBar := bar_index

if hasActiveClx and clxState == 4
    if clxDirection == 1 and (microBullBOS or majorBullBOS)
        clxState := 5
    if clxDirection == -1 and (microBearBOS or majorBearBOS)
        clxState := 5

if hasActiveClx and not na(clxBar) and (bar_index - clxBar) > testLookback * 2
    clxState := 0

bool clxDetected     = clxState >= 1
bool clxTestPending  = clxState == 3
bool clxTestValid    = clxState == 4 or clxState == 5
bool clxConfirmed    = clxState == 5
bool clxBullSequence = clxDirection == 1 and clxState >= 1
bool clxBearSequence = clxDirection == -1 and clxState >= 1

var int barsSinceLastTest = 999
barsSinceLastTest += 1
bool isTestCandle = clxState == 4 and bar_index == clxTestBar and barsSinceLastTest >= 3
if isTestCandle
    barsSinceLastTest := 0

string clxStateStr = clxState == 0 ? "—" : clxState == 1 ? "CLX" : clxState == 2 ? "REACTING" : clxState == 3 ? "TEST PENDING" : clxState == 4 ? "TEST VALID" : clxState == 5 ? "CONFIRMED" : "—"
string clxDirStr = clxDirection == 1 ? "Bull" : clxDirection == -1 ? "Bear" : "—"

// ============================================================================
// MODULE: absorption pattern detection (EVR)
// ============================================================================

float evr = candleRange > 0 ? volume / candleRange : 0.0
float evrAvg = ta.sma(evr, 14)
float evrNorm = evrAvg > 0 ? evr / evrAvg : 1.0

bool evrFalling = evrNorm < nz(evrNorm[1]) and nz(evrNorm[1]) < nz(evrNorm[2]) and nz(evrNorm[2]) < nz(evrNorm[3])
// Effort-vs-Result (evr = volume/range) has no price-only equivalent — fails
// closed (no absorption boost) when volume is not real.
bool absCondition = volumeIsReal and evrFalling and relVol > 1.2

// Absorption near Spring or Upthrust — quality boost
bool absNearSpring   = absCondition and (isSpring or isMajorSpring)
bool absNearUpthrust = absCondition and (isUpthrust or isMajorUpthrust)

// Boost event quality when absorption detected near swing event
int springQualityAdj      = absNearSpring ? math.min(3, springQuality + 1) : springQuality
int majorSpringQualityAdj = absNearSpring ? math.min(3, majorSpringQuality + 1) : majorSpringQuality
int upthrustQualityAdj      = absNearUpthrust ? math.min(3, upthrustQuality + 1) : upthrustQuality
int majorUpthrustQualityAdj = absNearUpthrust ? math.min(3, majorUpthrustQuality + 1) : majorUpthrustQuality

// EQH/EQL proximity boost: Spring near EQL or Upthrust near EQH = liquidity pool
bool springNearEQL = (isSpring or isMajorSpring) and not na(eqlLevel) and atrValue > 0 and math.abs(low - eqlLevel) < atrValue * 0.5
bool upthrustNearEQH = (isUpthrust or isMajorUpthrust) and not na(eqhLevel) and atrValue > 0 and math.abs(high - eqhLevel) < atrValue * 0.5
if springNearEQL
    springQualityAdj      := math.min(3, springQualityAdj + 1)
    majorSpringQualityAdj := math.min(3, majorSpringQualityAdj + 1)
if upthrustNearEQH
    upthrustQualityAdj      := math.min(3, upthrustQualityAdj + 1)
    majorUpthrustQualityAdj := math.min(3, majorUpthrustQualityAdj + 1)

// Absorption near any swing level (for labeling)
bool absAtSwingLevel = absCondition and (not na(lastSwingHigh) and math.abs(close - lastSwingHigh) < atrValue * 0.5 or not na(lastSwingLow) and math.abs(close - lastSwingLow) < atrValue * 0.5 or not na(lastMajorSwingHigh) and math.abs(close - lastMajorSwingHigh) < atrValue * 0.5 or not na(lastMajorSwingLow) and math.abs(close - lastMajorSwingLow) < atrValue * 0.5)

// ============================================================================
// MODULE: Fair Value Gap (FVG) detection
// ============================================================================

type FVG
    float  upper
    float  lower
    int    dir       // 1 = bullish, -1 = bearish
    int    createBar
    bool   mitigated
    box    fvgBox

var array<FVG> fvgArray = array.new<FVG>()

float fvgMinGap = atrValue * 0.3

// Bullish FVG: current low > high[2]
bool isBullFVG = low > high[2] and (low - high[2]) >= fvgMinGap
// Bearish FVG: current high < low[2]
bool isBearFVG = high < low[2] and (low[2] - high) >= fvgMinGap

if showFVG and isBullFVG
    FVG f = FVG.new()
    f.upper     := low
    f.lower     := high[2]
    f.dir       := 1
    f.createBar := bar_index
    f.mitigated := false
    f.fvgBox    := box.new(bar_index - 2, f.upper, bar_index + 20, f.lower, border_color=color.new(#00e676, 85), border_width=0, bgcolor=color.new(#00e676, 85))
    array.push(fvgArray, f)
    if array.size(fvgArray) > 10
        FVG old = array.shift(fvgArray)
        if not na(old.fvgBox)
            box.delete(old.fvgBox)

if showFVG and isBearFVG
    FVG f = FVG.new()
    f.upper     := low[2]
    f.lower     := high
    f.dir       := -1
    f.createBar := bar_index
    f.mitigated := false
    f.fvgBox    := box.new(bar_index - 2, f.upper, bar_index + 20, f.lower, border_color=color.new(#ff1744, 85), border_width=0, bgcolor=color.new(#ff1744, 85))
    array.push(fvgArray, f)
    if array.size(fvgArray) > 10
        FVG old = array.shift(fvgArray)
        if not na(old.fvgBox)
            box.delete(old.fvgBox)

// FVG mitigation check + extend boxes
var bool fvgNearSupport    = false
var bool fvgNearResistance = false
fvgNearSupport    := false
fvgNearResistance := false

if array.size(fvgArray) > 0
    for i = array.size(fvgArray) - 1 to 0
        FVG f = array.get(fvgArray, i)
        if f.mitigated
            continue
        int age = bar_index - f.createBar
        // Auto-remove after 20 bars
        if age > 20
            f.mitigated := true
            if not na(f.fvgBox)
                box.delete(f.fvgBox)
            array.remove(fvgArray, i)
            continue
        // Mitigation: price fills the gap
        bool filled = false
        if f.dir == 1 and low <= f.lower
            filled := true
        if f.dir == -1 and high >= f.upper
            filled := true
        if filled
            f.mitigated := true
            if not na(f.fvgBox)
                box.delete(f.fvgBox)
            array.remove(fvgArray, i)
            continue
        // Extend box right edge
        if not na(f.fvgBox)
            box.set_right(f.fvgBox, math.min(bar_index + 5, f.createBar + 20))
        // Track FVG near zones for assessment boost
        if f.dir == 1 and close > f.lower and close < f.upper + atrValue
            fvgNearSupport := true
        if f.dir == -1 and close < f.upper and close > f.lower - atrValue
            fvgNearResistance := true
        array.set(fvgArray, i, f)

// ============================================================================
// MODULE: Order Block (OB) detection
// ============================================================================

type OB
    float  upper
    float  lower
    int    dir        // 1 = bullish OB, -1 = bearish OB
    int    createBar
    bool   mitigated
    int    confScore
    box    obBox

var array<OB> bullOBs = array.new<OB>()
var array<OB> bearOBs = array.new<OB>()

// Bullish OB: last DOWN candle before a bullish BOS
if showOB and bullishBOS
    // Look back to find most recent bearish candle before BOS bar
    int obBar = -1
    for lb = 1 to 10
        if close[lb] < open[lb]
            obBar := lb
            break
    if obBar > 0
        OB ob = OB.new()
        ob.upper     := high[obBar]
        ob.lower     := low[obBar]
        ob.dir       := 1
        ob.createBar := bar_index - obBar
        ob.mitigated := false
        // Confluence scoring
        int cs = 0
        // Strong displacement: BOS candle range > 1.5 ATR
        cs += candleATR >= 1.5 ? 30 : 0
        // BOS confirmed
        cs += 25
        // FVG overlap check
        bool fvgOverlap = false
        if array.size(fvgArray) > 0
            for fi = 0 to array.size(fvgArray) - 1
                FVG f = array.get(fvgArray, fi)
                if f.dir == 1 and not f.mitigated and f.upper >= ob.lower and f.lower <= ob.upper
                    fvgOverlap := true
                    break
        cs += fvgOverlap ? 20 : 0
        // High volume
        cs += relVol >= 1.3 ? 15 : 0
        ob.confScore := cs
        ob.obBox     := box.new(ob.createBar, ob.upper, bar_index + 15, ob.lower, border_color=color.new(#00e676, 80), border_width=1, bgcolor=color.new(#00e676, 80))
        array.push(bullOBs, ob)
        if array.size(bullOBs) > 5
            OB old = array.shift(bullOBs)
            if not na(old.obBox)
                box.delete(old.obBox)

// Bearish OB: last UP candle before a bearish BOS
if showOB and bearishBOS
    int obBar = -1
    for lb = 1 to 10
        if close[lb] > open[lb]
            obBar := lb
            break
    if obBar > 0
        OB ob = OB.new()
        ob.upper     := high[obBar]
        ob.lower     := low[obBar]
        ob.dir       := -1
        ob.createBar := bar_index - obBar
        ob.mitigated := false
        int cs = 0
        cs += candleATR >= 1.5 ? 30 : 0
        cs += 25
        bool fvgOverlap = false
        if array.size(fvgArray) > 0
            for fi = 0 to array.size(fvgArray) - 1
                FVG f = array.get(fvgArray, fi)
                if f.dir == -1 and not f.mitigated and f.upper >= ob.lower and f.lower <= ob.upper
                    fvgOverlap := true
                    break
        cs += fvgOverlap ? 20 : 0
        cs += relVol >= 1.3 ? 15 : 0
        ob.confScore := cs
        ob.obBox     := box.new(ob.createBar, ob.upper, bar_index + 15, ob.lower, border_color=color.new(#ff1744, 80), border_width=1, bgcolor=color.new(#ff1744, 80))
        array.push(bearOBs, ob)
        if array.size(bearOBs) > 5
            OB old = array.shift(bearOBs)
            if not na(old.obBox)
                box.delete(old.obBox)

// OB mitigation check + extend boxes
var bool obNearSupport    = false
var bool obNearResistance = false
var int  obSupConfScore   = 0
var int  obResConfScore   = 0
obNearSupport    := false
obNearResistance := false
obSupConfScore   := 0
obResConfScore   := 0

if array.size(bullOBs) > 0
    for i = array.size(bullOBs) - 1 to 0
        OB ob = array.get(bullOBs, i)
        if ob.mitigated
            continue
        // Mitigated when price touches the zone
        if low <= ob.upper and high >= ob.lower
            ob.mitigated := true
            if not na(ob.obBox)
                box.delete(ob.obBox)
            array.set(bullOBs, i, ob)
            continue
        // Extend box
        if not na(ob.obBox)
            box.set_right(ob.obBox, bar_index + 5)
        // OB proximity for zone assessment
        float obDist = atrValue > 0 ? math.abs(close - (ob.upper + ob.lower) / 2.0) / atrValue : 999.0
        if obDist < 2.0
            obNearSupport := true
            obSupConfScore := math.max(obSupConfScore, ob.confScore)
        array.set(bullOBs, i, ob)

if array.size(bearOBs) > 0
    for i = array.size(bearOBs) - 1 to 0
        OB ob = array.get(bearOBs, i)
        if ob.mitigated
            continue
        if high >= ob.lower and low <= ob.upper
            ob.mitigated := true
            if not na(ob.obBox)
                box.delete(ob.obBox)
            array.set(bearOBs, i, ob)
            continue
        if not na(ob.obBox)
            box.set_right(ob.obBox, bar_index + 5)
        float obDist = atrValue > 0 ? math.abs(close - (ob.upper + ob.lower) / 2.0) / atrValue : 999.0
        if obDist < 2.0
            obNearResistance := true
            obResConfScore := math.max(obResConfScore, ob.confScore)
        array.set(bearOBs, i, ob)

// ============================================================================
// MODULE: zone management
// ============================================================================

type Zone
    int    zoneType
    float  upper
    float  lower
    float  mid
    int    direction
    int    quality
    int    createBar
    int    status
    int    touchCount
    float  score
    box    zoneBox
    label  zoneLabel
    // Assessment fields
    int    goodBounces
    int    weakBounces
    float  maxBounce
    float  lastBounce
    int    deepPenetrations
    bool   visitBounced

var array<Zone> zones = array.new<Zone>()

f_zoneTypeStr(int t) =>
    t == 1 ? "S/R Swing" : t == 2 ? "S/R Swing" : t == 3 ? "Spring" : t == 4 ? "Upthrust" : t == 5 ? "BOS Flip" : t == 6 ? "BOS Flip" : t == 7 ? "CLX Zone" : t == 8 ? "CLX Zone" : "?"

f_statusStr(int s) =>
    s == 0 ? "Fresh" : s == 1 ? "Tested" : s == 2 ? "Confirmed" : s == 3 ? "Broken" : s == 4 ? "Flipped" : "?"

f_zoneScore(int zType, int qual, int status, int age, int touches) =>
    float base = zType == 3 or zType == 4 ? 3.0 : zType == 5 or zType == 6 ? 2.5 : zType == 7 or zType == 8 ? 2.0 : 2.0
    float qBonus = qual == 3 ? 1.5 : qual == 2 ? 0.5 : 0.0
    float sBonus = status == 2 ? 1.5 : status == 1 ? 0.5 : status == 4 ? 1.0 : 0.0
    float aPenalty = age > 100 ? math.min(2.0, (age - 100) / 100.0) : 0.0
    float tBonus = math.min(1.0, touches * 0.3)
    math.max(0.0, base + qBonus + sBonus + tBonus - aPenalty)

addZone(int zType, float upper, float lower, int dir, int qual) =>
    Zone z = Zone.new()
    z.zoneType  := zType
    z.upper     := upper
    z.lower     := lower
    z.mid       := (upper + lower) / 2.0
    z.direction := dir
    z.quality   := qual
    z.createBar := bar_index
    z.status    := 0
    z.touchCount := 0
    z.score     := 0.0
    z.zoneBox   := na
    z.zoneLabel := na
    z.goodBounces := 0
    z.weakBounces := 0
    z.maxBounce   := 0.0
    z.lastBounce  := 0.0
    z.deepPenetrations := 0
    z.visitBounced := false
    array.push(zones, z)

// ── Zone creation from events ──
float halfWidth = atrValue * zoneWidthATR / 2.0

if showSwingZones and not na(majorPivotHi)
    addZone(2, majorPivotHi + halfWidth, majorPivotHi - halfWidth, -1, 2)
if showSwingZones and not na(majorPivotLo)
    addZone(1, majorPivotLo + halfWidth, majorPivotLo - halfWidth, 1, 2)

if showSpringZones and isMajorSpring and not na(lastMajorSwingLow)
    int qual = lowerWickRatio >= 0.55 and candleATR >= 0.8 ? 3 : 2
    qual := absNearSpring ? math.min(3, qual + 1) : qual
    addZone(3, math.max(low, lastMajorSwingLow), math.min(low, lastMajorSwingLow), 1, qual)
else if showSpringZones and isSpring and not na(lastSwingLow)
    int qual = lowerWickRatio >= 0.55 and candleATR >= 0.8 ? 3 : lowerWickRatio >= minWickRatio ? 2 : 1
    qual := absNearSpring ? math.min(3, qual + 1) : qual
    addZone(3, math.max(low, lastSwingLow), math.min(low, lastSwingLow), 1, qual)

if showSpringZones and isMajorUpthrust and not na(lastMajorSwingHigh)
    int qual = upperWickRatio >= 0.55 and candleATR >= 0.8 ? 3 : 2
    qual := absNearUpthrust ? math.min(3, qual + 1) : qual
    addZone(4, math.max(high, lastMajorSwingHigh), math.min(high, lastMajorSwingHigh), -1, qual)
else if showSpringZones and isUpthrust and not na(lastSwingHigh)
    int qual = upperWickRatio >= 0.55 and candleATR >= 0.8 ? 3 : upperWickRatio >= minWickRatio ? 2 : 1
    qual := absNearUpthrust ? math.min(3, qual + 1) : qual
    addZone(4, math.max(high, lastSwingHigh), math.min(high, lastSwingHigh), -1, qual)

if showBOSFlips and majorBullBOS and not na(lastMajorSwingHigh)
    addZone(5, lastMajorSwingHigh + halfWidth, lastMajorSwingHigh - halfWidth, 1, 2)
if showBOSFlips and majorBearBOS and not na(lastMajorSwingLow)
    addZone(6, lastMajorSwingLow + halfWidth, lastMajorSwingLow - halfWidth, -1, 2)

if showClimaxZones and isClimaxCandle
    int dir = close < open ? 1 : -1
    int qual = relVol >= climaxVolFactor * 1.5 ? 3 : 2
    addZone(dir == 1 ? 7 : 8, high, low, dir, qual)

// ── Zone status updates ──
int totalZones = array.size(zones)
if totalZones > 0
    for i = totalZones - 1 to 0
        Zone z = array.get(zones, i)
        int age = bar_index - z.createBar
        if age > zoneExpireBars
            if not na(z.zoneBox)
                box.delete(z.zoneBox)
            if not na(z.zoneLabel)
                label.delete(z.zoneLabel)
            array.remove(zones, i)
            continue
        bool priceInZone = high >= z.lower and low <= z.upper
        bool priceAbove  = close > z.upper
        bool priceBelow  = close < z.lower
        // The creation bar belongs to the event that built the zone — it cannot test, confirm or break it
        if age > 0
            // Touches and bounces are counted per visit (entry into the zone), not per bar inside it
            bool prevInZone = high[1] >= z.lower and low[1] <= z.upper
            bool newVisit   = priceInZone and not prevInZone
            if newVisit
                z.touchCount += 1
                z.visitBounced := false
                if z.status == 0
                    z.status := 1
            bool canBounce = (z.status == 1 or z.status == 2) and priceInZone and not z.visitBounced
            if canBounce and z.direction == 1 and close > z.mid and close > open
                z.status := 2
                z.visitBounced := true
                float bounce = atrValue > 0 ? (close - z.lower) / atrValue : 0.0
                z.lastBounce := bounce
                z.maxBounce := math.max(z.maxBounce, bounce)
                if bounce >= 0.5
                    z.goodBounces += 1
                else
                    z.weakBounces += 1
            if canBounce and z.direction == -1 and close < z.mid and close < open
                z.status := 2
                z.visitBounced := true
                float bounce = atrValue > 0 ? (z.upper - close) / atrValue : 0.0
                z.lastBounce := bounce
                z.maxBounce := math.max(z.maxBounce, bounce)
                if bounce >= 0.5
                    z.goodBounces += 1
                else
                    z.weakBounces += 1
            // Deep penetration tracking
            if z.direction == 1 and priceInZone and close < z.mid
                z.deepPenetrations += 1
            if z.direction == -1 and priceInZone and close > z.mid
                z.deepPenetrations += 1
            // Broken
            bool brokeNow = false
            if z.direction == 1 and priceBelow and close[1] >= z.lower
                z.status := 3
                brokeNow := true
            if z.direction == -1 and priceAbove and close[1] <= z.upper
                z.status := 3
                brokeNow := true
            // Flip needs a retest on a later bar than the break
            if z.status == 3 and not brokeNow and z.direction == 1 and priceInZone and close < z.mid
                z.status    := 4
                z.direction := -1
            else if z.status == 3 and not brokeNow and z.direction == -1 and priceInZone and close > z.mid
                z.status    := 4
                z.direction := 1
        z.score := f_zoneScore(z.zoneType, z.quality, z.status, age, z.touchCount)
        array.set(zones, i, z)

// ============================================================================
// MODULE: visuals — event labels
// ============================================================================

bool isDebug    = displayMode == "Debug" or displayMode == "Research"
bool isResearch = displayMode == "Research"
float offsetUnit = atrValue * 0.4

// Swing dots
if showSwings and isSwingHigh
    label.new(bar_index - rightBars, pivotHi, "·", style=label.style_label_down, color=color.new(#ef5350, 50), textcolor=color.white, size=size.tiny, tooltip="Swing High: " + str.tostring(pivotHi, "#.####"))
if showSwings and isSwingLow
    label.new(bar_index - rightBars, pivotLo, "·", style=label.style_label_up, color=color.new(#66bb6a, 50), textcolor=color.white, size=size.tiny, tooltip="Swing Low: " + str.tostring(pivotLo, "#.####"))

// Spring / Upthrust
if showSpringUT and isMajorSpring
    int q = majorSpringQuality
    label.new(bar_index, low - offsetUnit * 1.2, f_qualityTag(q) + " SPRING", style=label.style_label_up, color=q == 3 ? color.lime : q == 2 ? color.new(color.lime, 15) : color.new(color.lime, 35), textcolor=color.black, size=size.normal, tooltip="MAJOR Spring [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]\nSweep: " + str.tostring(majorSpringSweepDepth, "#.00") + " ATR | Wick: " + str.tostring(lowerWickRatio * 100, "#.0") + "% | Vol: " + str.tostring(relVol, "#.0") + "x")
else if showSpringUT and isSpring
    int q = springQuality
    label.new(bar_index, low - offsetUnit, f_qualityTag(q) + " spring", style=label.style_label_up, color=q == 3 ? color.lime : q == 2 ? color.new(color.lime, 25) : color.new(color.lime, 50), textcolor=color.black, size=size.small, tooltip="Micro Spring [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]\nSweep: " + str.tostring(springSweepDepth, "#.00") + " ATR | Wick: " + str.tostring(lowerWickRatio * 100, "#.0") + "%")

if showSpringUT and isMajorUpthrust
    int q = majorUpthrustQuality
    label.new(bar_index, high + offsetUnit * 1.2, f_qualityTag(q) + " UPTHRUST", style=label.style_label_down, color=q == 3 ? color.red : q == 2 ? color.new(color.red, 15) : color.new(color.red, 35), textcolor=color.white, size=size.normal, tooltip="MAJOR Upthrust [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]\nSweep: " + str.tostring(majorUpthrustSweepDepth, "#.00") + " ATR | Wick: " + str.tostring(upperWickRatio * 100, "#.0") + "%")
else if showSpringUT and isUpthrust
    int q = upthrustQuality
    label.new(bar_index, high + offsetUnit, f_qualityTag(q) + " upthrust", style=label.style_label_down, color=q == 3 ? color.red : q == 2 ? color.new(color.red, 25) : color.new(color.red, 50), textcolor=color.white, size=size.small, tooltip="Micro Upthrust [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]\nSweep: " + str.tostring(upthrustSweepDepth, "#.00") + " ATR | Wick: " + str.tostring(upperWickRatio * 100, "#.0") + "%")

// BOS / CHoCH
if showBOS and majorBullBOS
    int q = majorBullBOSQuality
    string bosLabel = majorBullCHoCH ? "CHoCH ↑↑" : "BOS ↑↑"
    string bosType  = majorBullCHoCH ? "MAJOR Bull CHoCH (Change of Character)" : "MAJOR Bull BOS (Continuation)"
    color  bosCol   = majorBullCHoCH ? (q == 3 ? color.new(#00bfa5, 0) : q == 2 ? color.new(#00bfa5, 10) : color.new(#00bfa5, 25)) : (q == 3 ? color.new(#00e676, 0) : q == 2 ? color.new(#00e676, 15) : color.new(#00e676, 35))
    label.new(bar_index, low - offsetUnit * 0.5, f_qualityTag(q) + " " + bosLabel, style=label.style_label_up, color=bosCol, textcolor=color.white, size=majorBullCHoCH ? size.normal : size.small, tooltip=bosType + " [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]")
else if showBOS and microBullBOS
    int q = microBullBOSQuality
    string bosLabel = microBullCHoCH ? "CHoCH ↑" : "bos ↑"
    string bosType  = microBullCHoCH ? "Micro Bull CHoCH (Change of Character)" : "Micro Bull BOS (Continuation)"
    color  bosCol   = microBullCHoCH ? (q == 3 ? color.new(#00bfa5, 10) : q == 2 ? color.new(#00bfa5, 25) : color.new(#00bfa5, 45)) : (q == 3 ? color.new(#00e676, 20) : q == 2 ? color.new(#00e676, 40) : color.new(#00e676, 60))
    label.new(bar_index, low - offsetUnit * 0.3, f_qualityTag(q) + " " + bosLabel, style=label.style_label_up, color=bosCol, textcolor=color.white, size=microBullCHoCH ? size.small : size.tiny, tooltip=bosType + " [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]")

if showBOS and majorBearBOS
    int q = majorBearBOSQuality
    string bosLabel = majorBearCHoCH ? "CHoCH ↓↓" : "BOS ↓↓"
    string bosType  = majorBearCHoCH ? "MAJOR Bear CHoCH (Change of Character)" : "MAJOR Bear BOS (Continuation)"
    color  bosCol   = majorBearCHoCH ? (q == 3 ? color.new(#d50000, 0) : q == 2 ? color.new(#d50000, 10) : color.new(#d50000, 25)) : (q == 3 ? color.new(#ff1744, 0) : q == 2 ? color.new(#ff1744, 15) : color.new(#ff1744, 35))
    label.new(bar_index, high + offsetUnit * 0.5, f_qualityTag(q) + " " + bosLabel, style=label.style_label_down, color=bosCol, textcolor=color.white, size=majorBearCHoCH ? size.normal : size.small, tooltip=bosType + " [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]")
else if showBOS and microBearBOS
    int q = microBearBOSQuality
    string bosLabel = microBearCHoCH ? "CHoCH ↓" : "bos ↓"
    string bosType  = microBearCHoCH ? "Micro Bear CHoCH (Change of Character)" : "Micro Bear BOS (Continuation)"
    color  bosCol   = microBearCHoCH ? (q == 3 ? color.new(#d50000, 10) : q == 2 ? color.new(#d50000, 25) : color.new(#d50000, 45)) : (q == 3 ? color.new(#ff1744, 20) : q == 2 ? color.new(#ff1744, 40) : color.new(#ff1744, 60))
    label.new(bar_index, high + offsetUnit * 0.3, f_qualityTag(q) + " " + bosLabel, style=label.style_label_down, color=bosCol, textcolor=color.white, size=microBearCHoCH ? size.small : size.tiny, tooltip=bosType + " [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]")

// Climax sequence
if showClimax and isClimaxCandle
    string clxType = isSellingClimax ? "Selling CLX" : isBuyingClimax ? "Buying CLX" : "CLX"
    label.new(bar_index, high + offsetUnit, clxType, style=label.style_label_down, color=color.new(#e040fb, 10), textcolor=color.white, size=size.small, tooltip="Climax [" + clxDirStr + "]\nRange: " + str.tostring(candleATR, "#.0") + "x ATR | Vol: " + str.tostring(relVol, "#.0") + "x")

if showTest and isTestCandle
    string tstDir = clxDirection == 1 ? "↑" : clxDirection == -1 ? "↓" : ""
    label.new(bar_index, clxDirection == 1 ? low - offsetUnit * 0.3 : high + offsetUnit * 0.3, "TST " + tstDir, style=clxDirection == 1 ? label.style_label_up : label.style_label_down, color=color.new(#ce93d8, 15), textcolor=color.white, size=size.small, tooltip="Test Validated [" + clxDirStr + "]")

if showClimax and clxConfirmed and clxState[1] == 4
    color confCol = clxDirection == 1 ? color.new(#00e676, 0) : color.new(#ff1744, 0)
    string confText = clxDirection == 1 ? "CLX→TST→BOS ↑" : "CLX→TST→BOS ↓"
    label.new(bar_index, clxDirection == 1 ? low - offsetUnit * 1.5 : high + offsetUnit * 1.5, confText, style=clxDirection == 1 ? label.style_label_up : label.style_label_down, color=confCol, textcolor=color.white, size=size.normal, tooltip="CONFIRMED Reversal Sequence — strongest signal")

// Fake breaks
if showFakeBreaks and fakeBreakout and not isUpthrust
    label.new(bar_index, high + offsetUnit * 0.7, "FB", style=label.style_label_down, color=color.new(#ff9800, 20), textcolor=color.white, size=size.tiny, tooltip="Fake Breakout")
if showFakeBreaks and fakeBreakdown and not isSpring
    label.new(bar_index, low - offsetUnit * 0.7, "FB", style=label.style_label_up, color=color.new(#ff9800, 20), textcolor=color.white, size=size.tiny, tooltip="Fake Breakdown")

// Rejections
if showRejection and isBullRejection and not isSpring and not isMajorSpring
    int q = bullRejQuality
    label.new(bar_index, low, f_qualityTag(q) + "rej", style=label.style_label_up, color=q == 3 ? color.new(#4db6ac, 10) : q == 2 ? color.new(#4db6ac, 30) : color.new(#4db6ac, 50), textcolor=color.white, size=size.tiny, tooltip="Bull Rejection [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]")
if showRejection and isBearRejection and not isUpthrust and not isMajorUpthrust
    int q = bearRejQuality
    label.new(bar_index, high, f_qualityTag(q) + "rej", style=label.style_label_down, color=q == 3 ? color.new(#c62828, 10) : q == 2 ? color.new(#c62828, 30) : color.new(#c62828, 50), textcolor=color.white, size=size.tiny, tooltip="Bear Rejection [" + (q == 3 ? "STRONG" : q == 2 ? "normal" : "weak") + "]")

// Absorption labels (Debug/Research only)
if isDebug and absAtSwingLevel
    label.new(bar_index, low - offsetUnit * 0.5, "ABS", style=label.style_label_up, color=color.new(color.teal, 20), textcolor=color.white, size=size.tiny, tooltip="Absorption Detected\nEVR Norm: " + str.tostring(evrNorm, "#.00") + " | RelVol: " + str.tostring(relVol, "#.0") + "x")

// EQH/EQL labels and lines (visible in Debug/Research, or when showEQHEQL is toggled on)
bool showEQ = showEQHEQL or isDebug
if showEQ and isEQH and not na(eqhLevel) and not na(eqhFromBar)
    label.new(bar_index - rightBars, eqhLevel + offsetUnit * 0.3, "EQH", style=label.style_label_down, color=color.new(#ff9800, 30), textcolor=color.white, size=size.tiny, tooltip="Equal Highs — liquidity pool\nLevel: " + str.tostring(eqhLevel, "#.####") + "\nThreshold: " + str.tostring(eqhEqlThreshold * 100, "#.0") + "% ATR")
    line.new(eqhFromBar, eqhLevel, bar_index - rightBars, eqhLevel, color=color.new(#ff9800, 40), style=line.style_dotted, width=1)
if showEQ and isEQL and not na(eqlLevel) and not na(eqlFromBar)
    label.new(bar_index - rightBars, eqlLevel - offsetUnit * 0.3, "EQL", style=label.style_label_up, color=color.new(#ff9800, 30), textcolor=color.white, size=size.tiny, tooltip="Equal Lows — liquidity pool\nLevel: " + str.tostring(eqlLevel, "#.####") + "\nThreshold: " + str.tostring(eqhEqlThreshold * 100, "#.0") + "% ATR")
    line.new(eqlFromBar, eqlLevel, bar_index - rightBars, eqlLevel, color=color.new(#ff9800, 40), style=line.style_dotted, width=1)

// Debug swing lines
plot(isDebug ? lastSwingHigh : na, "Micro Swing High", color=color.new(#ef5350, 55), style=plot.style_stepline, linewidth=1)
plot(isDebug ? lastSwingLow : na, "Micro Swing Low", color=color.new(#66bb6a, 55), style=plot.style_stepline, linewidth=1)
plot(isDebug ? lastMajorSwingHigh : na, "Major Swing High", color=color.new(#ef5350, 20), style=plot.style_stepline, linewidth=2)
plot(isDebug ? lastMajorSwingLow : na, "Major Swing Low", color=color.new(#66bb6a, 20), style=plot.style_stepline, linewidth=2)

// ============================================================================
// MODULE: visuals — zone rendering (priority-based, last bar only)
// ============================================================================

int renderCount = math.min(maxZones, array.size(zones))

if barstate.islast and array.size(zones) > 0
    for i = 0 to array.size(zones) - 1
        Zone z = array.get(zones, i)
        if not na(z.zoneBox)
            box.delete(z.zoneBox)
            z.zoneBox := na
        if not na(z.zoneLabel)
            label.delete(z.zoneLabel)
            z.zoneLabel := na
        array.set(zones, i, z)

    var array<int> rendered = array.new_int()
    array.clear(rendered)

    for r = 0 to renderCount - 1
        float bestScore = -1.0
        int bestIdx = -1
        for i = 0 to array.size(zones) - 1
            Zone z = array.get(zones, i)
            if z.score > bestScore and (z.status != 3 or showBrokenZones)
                bool alreadyDone = false
                if array.size(rendered) > 0
                    for j = 0 to array.size(rendered) - 1
                        if array.get(rendered, j) == i
                            alreadyDone := true
                            break
                if not alreadyDone
                    bestScore := z.score
                    bestIdx := i

        if bestIdx >= 0
            array.push(rendered, bestIdx)
            Zone z = array.get(zones, bestIdx)

            color baseCol = z.status == 4 ? flipColor : z.direction == 1 ? supportColor : resistColor
            color boxCol = z.status == 3 ? color.new(color.gray, 85) : z.quality == 3 ? color.new(baseCol, 75) : z.quality == 2 ? color.new(baseCol, 82) : color.new(baseCol, 88)
            color borderCol = z.status == 3 ? color.new(color.gray, 60) : z.quality == 3 ? color.new(baseCol, 30) : z.quality == 2 ? color.new(baseCol, 50) : color.new(baseCol, 65)

            z.zoneBox := box.new(z.createBar, z.upper, bar_index + 10, z.lower, border_color=borderCol, border_width=1, bgcolor=boxCol)

            if showZoneLabels
                string typeStr = f_zoneTypeStr(z.zoneType)
                string statusStr = f_statusStr(z.status)
                string qualStr = z.quality == 3 ? "★" : z.quality == 1 ? "~" : ""
                string labelText = qualStr + typeStr
                if isDebug
                    labelText := labelText + " [" + statusStr + " | " + str.tostring(z.score, "#.0") + "]"
                color lblCol = z.status == 3 ? color.new(color.gray, 40) : color.new(baseCol, 20)
                z.zoneLabel := label.new(bar_index + 5, z.mid, labelText, style=label.style_label_left, color=color.new(color.black, 100), textcolor=lblCol, size=size.small, tooltip=typeStr + "\nStatus: " + statusStr + "\nScore: " + str.tostring(z.score, "#.0") + "\nTouches: " + str.tostring(z.touchCount) + "\nQuality: " + str.tostring(z.quality) + "/3")

            array.set(zones, bestIdx, z)

// ============================================================================
// MODULE: volume validity notice
// ============================================================================

// Kurzer Hinweis unten rechts; was fehlt, steht im Data Contract oben.
var table volWarn = table.new(position.bottom_right, 1, 1)
if barstate.islast and not volumeIsReal
    table.cell(volWarn, 0, 0, "Vein S&Z · volume not available",
         bgcolor = color.new(color.orange, 20), text_color = color.white, text_size = size.small)

// ============================================================================
// MODULE: data window
// ============================================================================

int eventCode = 0
eventCode += isSwingHigh ? 1 : 0
eventCode += isSwingLow ? 2 : 0
eventCode += microBullBOS ? 4 : 0
eventCode += microBearBOS ? 8 : 0
eventCode += majorBullBOS ? 16 : 0
eventCode += majorBearBOS ? 32 : 0
eventCode += isSpring ? 64 : 0
eventCode += isUpthrust ? 128 : 0
eventCode += isMajorSpring ? 256 : 0
eventCode += isMajorUpthrust ? 512 : 0
eventCode += isClimaxCandle ? 1024 : 0
eventCode += isTestCandle ? 2048 : 0
eventCode += isBullRejection ? 4096 : 0
eventCode += isBearRejection ? 8192 : 0

eventCode += isEQH ? 16384 : 0
eventCode += isEQL ? 32768 : 0
eventCode += isBullCHoCH ? 65536 : 0
eventCode += isBearCHoCH ? 131072 : 0

plot(eventCode, "Event Code", display=display.data_window, color=color.new(color.gray, 100))
plot(springQuality, "Spring Quality (0-3)", display=display.data_window, color=color.new(color.gray, 100))
plot(upthrustQuality, "Upthrust Quality (0-3)", display=display.data_window, color=color.new(color.gray, 100))
plot(clxState, "CLX State (0-5)", display=display.data_window, color=color.new(color.gray, 100))
plot(clxDirection, "CLX Dir (1=bull,-1=bear)", display=display.data_window, color=color.new(color.gray, 100))
plot(clxConfirmed ? 1 : 0, "CLX Confirmed", display=display.data_window, color=color.new(color.gray, 100))
plot(array.size(zones), "Total Zones", display=display.data_window, color=color.new(color.gray, 100))
plot(absCondition ? 1 : 0, "Absorption", display=display.data_window, color=color.new(color.gray, 100))
plot(evrNorm, "EVR Normalized", display=display.data_window, color=color.new(color.gray, 100))
plot(isBullFVG ? 1 : isBearFVG ? -1 : 0, "FVG (1=bull,-1=bear)", display=display.data_window, color=color.new(color.gray, 100))
plot(array.size(fvgArray), "Active FVGs", display=display.data_window, color=color.new(color.gray, 100))
plot(array.size(bullOBs) + array.size(bearOBs), "Active OBs", display=display.data_window, color=color.new(color.gray, 100))
plot(pocLevel, "POC Level", display=display.data_window, color=color.new(color.gray, 100))
plot(vaHighLevel, "VA High", display=display.data_window, color=color.new(color.gray, 100))
plot(vaLowLevel, "VA Low", display=display.data_window, color=color.new(color.gray, 100))
plot(nearPOC ? 1 : 0, "Near POC", display=display.data_window, color=color.new(color.gray, 100))
plot(inValueArea ? 1 : 0, "In Value Area", display=display.data_window, color=color.new(color.gray, 100))
plot(microSwingTrend, "Micro Swing Trend (1=bull,-1=bear)", display=display.data_window, color=color.new(color.gray, 100))
plot(majorSwingTrend, "Major Swing Trend (1=bull,-1=bear)", display=display.data_window, color=color.new(color.gray, 100))
plot(isBullCHoCH ? 1 : isBearCHoCH ? -1 : 0, "CHoCH (1=bull,-1=bear)", display=display.data_window, color=color.new(color.gray, 100))
plot(isEQH ? 1 : 0, "Equal Highs", display=display.data_window, color=color.new(color.gray, 100))
plot(isEQL ? 1 : 0, "Equal Lows", display=display.data_window, color=color.new(color.gray, 100))
plot(nz(eqhLevel), "EQH Level", display=display.data_window, color=color.new(color.gray, 100))
plot(nz(eqlLevel), "EQL Level", display=display.data_window, color=color.new(color.gray, 100))

// ============================================================================
// MODULE: research table
// ============================================================================

var int swHighCount = 0
var int swLowCount = 0
var int bosUpCount = 0
var int bosDnCount = 0
var int springCount = 0
var int utCount = 0
var int climaxCount = 0
var int chochCount  = 0
var int eqhCount    = 0
var int eqlCount    = 0

if isSwingHigh
    swHighCount += 1
if isSwingLow
    swLowCount += 1
if bullishBOS
    bosUpCount += 1
if bearishBOS
    bosDnCount += 1
if isSpring or isMajorSpring
    springCount += 1
if isUpthrust or isMajorUpthrust
    utCount += 1
if isClimaxCandle
    climaxCount += 1
if isBullCHoCH or isBearCHoCH
    chochCount += 1
if isEQH
    eqhCount += 1
if isEQL
    eqlCount += 1

color tcStats = color.new(color.gray, 20)   // standard text color (dark gray)
color hdStats = color.new(color.gray, 90)   // header row background (very light gray)
color tcDim   = color.new(color.gray, 50)   // dimmed section headers

var table statsTable = na
if isResearch and barstate.islast
    // Count zone stats
    int zCount = array.size(zones)
    int supportCount = 0
    int resistCount  = 0
    if zCount > 0
        for i = 0 to zCount - 1
            Zone z = array.get(zones, i)
            if z.direction == 1
                supportCount += 1
            else
                resistCount += 1

    if na(statsTable)
        statsTable := table.new(position.top_right, 2, 18, bgcolor=color.new(color.white, 5), border_color=color.new(color.gray, 60), border_width=1, frame_color=color.new(color.gray, 40), frame_width=1)
    table.cell(statsTable, 0, 0, "Vein Structure & Zones", text_color=tcStats, text_size=size.normal, bgcolor=hdStats)
    table.cell(statsTable, 1, 0, "v0.2", text_color=color.gray, text_size=size.normal, text_halign=text.align_right, bgcolor=hdStats)
    // Events
    table.cell(statsTable, 0, 1, "─ EVENTS ─", text_color=tcDim, text_size=size.small)
    table.cell(statsTable, 1, 1, "", text_size=size.small)
    table.cell(statsTable, 0, 2, "Swing H/L", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 2, str.tostring(swHighCount) + " / " + str.tostring(swLowCount), text_color=tcStats, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 3, "BOS ↑/↓", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 3, str.tostring(bosUpCount) + " / " + str.tostring(bosDnCount), text_color=tcStats, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 4, "Springs", text_color=#00c853, text_size=size.normal)
    table.cell(statsTable, 1, 4, str.tostring(springCount), text_color=#00c853, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 5, "Upthrusts", text_color=#d50000, text_size=size.normal)
    table.cell(statsTable, 1, 5, str.tostring(utCount), text_color=#d50000, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 6, "Climax", text_color=#e040fb, text_size=size.normal)
    table.cell(statsTable, 1, 6, str.tostring(climaxCount), text_color=#e040fb, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 7, "CHoCH", text_color=#00bfa5, text_size=size.normal)
    table.cell(statsTable, 1, 7, str.tostring(chochCount), text_color=#00bfa5, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 8, "EQH / EQL", text_color=#ff9800, text_size=size.normal)
    table.cell(statsTable, 1, 8, str.tostring(eqhCount) + " / " + str.tostring(eqlCount), text_color=#ff9800, text_size=size.normal, text_halign=text.align_right)
    // Zones
    table.cell(statsTable, 0, 9, "─ ZONES ─", text_color=tcDim, text_size=size.small)
    table.cell(statsTable, 1, 9, "", text_size=size.small)
    table.cell(statsTable, 0, 10, "Total", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 10, str.tostring(zCount), text_color=tcStats, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 11, "Support", text_color=supportColor, text_size=size.normal)
    table.cell(statsTable, 1, 11, str.tostring(supportCount), text_color=supportColor, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 12, "Resistance", text_color=resistColor, text_size=size.normal)
    table.cell(statsTable, 1, 12, str.tostring(resistCount), text_color=resistColor, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 13, "Visible", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 13, str.tostring(renderCount), text_color=tcStats, text_size=size.normal, text_halign=text.align_right)
    // FVG & OB
    table.cell(statsTable, 0, 14, "─ SMC ─", text_color=tcDim, text_size=size.small)
    table.cell(statsTable, 1, 14, "", text_size=size.small)
    table.cell(statsTable, 0, 15, "Active FVGs", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 15, str.tostring(array.size(fvgArray)), text_color=tcStats, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 16, "Active OBs", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 16, str.tostring(array.size(bullOBs) + array.size(bearOBs)), text_color=tcStats, text_size=size.normal, text_halign=text.align_right)
    table.cell(statsTable, 0, 17, "Swing Trend", text_color=tcStats, text_size=size.normal)
    table.cell(statsTable, 1, 17, (majorSwingTrend == 1 ? "Bull" : majorSwingTrend == -1 ? "Bear" : "—") + " / " + (microSwingTrend == 1 ? "bull" : microSwingTrend == -1 ? "bear" : "—"), text_color=majorSwingTrend == 1 ? #00e676 : majorSwingTrend == -1 ? #ff1744 : color.gray, text_size=size.normal, text_halign=text.align_right)

// ============================================================================
// MODULE: zone assessment — nearest zone evaluation
// ============================================================================

// Find nearest support (below price) and resistance (above price)
var int nearSupIdx = -1
var int nearResIdx = -1
var float nearSupDist = 999.0
var float nearResDist = 999.0

nearSupIdx := -1
nearResIdx := -1
nearSupDist := 999.0
nearResDist := 999.0

if (showAssessLabels or showAssessTable) and array.size(zones) > 0
    for i = 0 to array.size(zones) - 1
        Zone z = array.get(zones, i)
        if z.status == 3
            continue
        float distATR = atrValue > 0 ? math.abs(close - z.mid) / atrValue : 999.0
        if distATR > maxAssessDistATR
            continue
        if z.direction == 1 and z.mid < close and distATR < nearSupDist
            nearSupDist := distATR
            nearSupIdx := i
        if z.direction == -1 and z.mid > close and distATR < nearResDist
            nearResDist := distATR
            nearResIdx := i

// Assessment functions
f_structuralStr(Zone z) =>
    float s = z.zoneType == 3 or z.zoneType == 4 ? 30.0 : z.zoneType == 5 or z.zoneType == 6 ? 25.0 : z.zoneType == 7 or z.zoneType == 8 ? 25.0 : 20.0
    s += z.quality == 3 ? 20.0 : z.quality == 2 ? 10.0 : 4.0
    s += z.status == 2 ? 20.0 : z.status == 1 ? 10.0 : z.status == 0 ? 6.0 : -25.0
    int age = bar_index - z.createBar
    s += age < 30 ? 10.0 : age < 80 ? 5.0 : age > 150 ? -5.0 : 0.0
    math.max(0.0, math.min(100.0, s))

f_histRespect(Zone z) =>
    float h = 30.0
    h += math.min(30.0, z.goodBounces * 12.0)
    h += z.maxBounce >= 1.0 ? 15.0 : z.maxBounce >= 0.5 ? 8.0 : 0.0
    h -= math.min(20.0, z.deepPenetrations * 5.0)
    h -= z.weakBounces > z.goodBounces ? 10.0 : 0.0
    h -= z.touchCount > 3 ? (z.touchCount - 3) * 5.0 : 0.0
    if z.goodBounces >= 2 and z.lastBounce < z.maxBounce * 0.5
        h -= 10.0
    math.max(0.0, math.min(100.0, h))

// Approach average range — computed in global scope (ta.* must not run conditionally)
float approachAvgRange = ta.sma(candleRange, approachBars)

f_approachQ(Zone z) =>
    float aq = 0.0
    float avgRange = approachAvgRange
    float recentRange = high - low
    if z.direction == 1
        aq += recentRange < avgRange * 0.8 ? 15.0 : 0.0
        aq += volumeIsReal and volume < avgVol * 0.9 and close < close[1] ? 10.0 : 0.0
        aq += lowerWickRatio > 0.3 and lowerWickRatio > lowerWickRatio[1] ? 10.0 : 0.0
        aq += rsi14 < 35 ? 10.0 : 0.0
        aq += volumeIsReal and mfi14 < 30 ? 8.0 : 0.0
        aq -= recentRange > avgRange * 1.2 and close < open ? 15.0 : 0.0
        aq -= volumeIsReal and volume > avgVol * 1.3 and close < open ? 12.0 : 0.0
        aq -= majorBearBOS ? 18.0 : 0.0
    else
        aq += recentRange < avgRange * 0.8 ? 15.0 : 0.0
        aq += volumeIsReal and volume < avgVol * 0.9 and close > close[1] ? 10.0 : 0.0
        aq += upperWickRatio > 0.3 and upperWickRatio > upperWickRatio[1] ? 10.0 : 0.0
        aq += rsi14 > 65 ? 10.0 : 0.0
        aq += volumeIsReal and mfi14 > 70 ? 8.0 : 0.0
        aq -= recentRange > avgRange * 1.2 and close > open ? 15.0 : 0.0
        aq -= volumeIsReal and volume > avgVol * 1.3 and close > open ? 12.0 : 0.0
        aq -= majorBullBOS ? 18.0 : 0.0
    math.max(-100.0, math.min(100.0, aq))

f_contextA(Zone z) =>
    float ca = 0.0
    if z.direction == 1
        ca += isRange ? 15.0 : isTrendDown ? -20.0 : 5.0
    else
        ca += isRange ? 15.0 : isTrendUp ? -20.0 : 5.0
    ca += effRatio < 0.25 ? 10.0 : 0.0
    math.max(-100.0, math.min(100.0, ca))

f_reachL(float distATR, Zone z) =>
    float r = distATR <= 0.5 ? 75.0 : distATR <= 1.0 ? 60.0 : distATR <= 2.0 ? 40.0 : 20.0
    if z.direction == 1 and close < close[1] and close[1] < close[2]
        r += 15.0
    if z.direction == -1 and close > close[1] and close[1] > close[2]
        r += 15.0
    if z.direction == 1 and close > close[1] and close[1] > close[2]
        r -= 15.0
    if z.direction == -1 and close < close[1] and close[1] < close[2]
        r -= 15.0
    math.max(0.0, math.min(100.0, r))

f_holdL(float structural, float respect, float aqRaw, float caRaw) =>
    float aqM = (aqRaw + 100.0) / 2.0
    float caM = (caRaw + 100.0) / 2.0
    math.max(0.0, math.min(100.0, 0.30 * structural + 0.25 * respect + 0.30 * aqM + 0.15 * caM))

f_breakL(float hold, float aqRaw, Zone z) =>
    float br = 100.0 - hold
    br += aqRaw < -30 ? 10.0 : 0.0
    br += z.touchCount > 3 ? z.touchCount * 3.0 : 0.0
    br += z.weakBounces > z.goodBounces ? 10.0 : 0.0
    if z.goodBounces >= 2 and z.lastBounce < z.maxBounce * 0.4
        br += 12.0
    math.max(0.0, math.min(100.0, br))

f_holdClass(float h) =>
    h < 31 ? "weak" : h < 51 ? "fair" : h < 71 ? "solid" : "strong"
f_reachClass(float r) =>
    r < 26 ? "low" : r < 51 ? "moderate" : r < 76 ? "elevated" : "high"
f_breakClass(float b) =>
    b < 26 ? "low" : b < 51 ? "moderate" : b < 76 ? "elevated" : "high"
f_stateCalc(float hold, float brk, float aqRaw, Zone z) =>
    brk >= 65 and aqRaw < -20 ? "BREAK LIKELY" : hold >= 65 ? "REACTION LIKELY" : hold >= 50 and brk >= 50 ? "UNDER PRESSURE" : z.touchCount > 3 and z.lastBounce < z.maxBounce * 0.5 ? "WEAKENING" : hold >= 50 ? "HOLD POSSIBLE" : "STABLE"

// Run assessment
var float supStruct = 0.0
var float supResp   = 0.0
var float supAQ     = 0.0
var float supCA     = 0.0
var float supReach  = 0.0
var float supHold   = 0.0
var float supBreak  = 0.0
var string supState = "—"
var string supType  = "—"
var bool  supOK     = false

var float resStruct = 0.0
var float resResp   = 0.0
var float resAQ     = 0.0
var float resCA     = 0.0
var float resReach  = 0.0
var float resHold   = 0.0
var float resBreak  = 0.0
var string resState = "—"
var string resType  = "—"
var bool  resOK     = false

if (showAssessLabels or showAssessTable) and nearSupIdx >= 0 and nearSupIdx < array.size(zones)
    Zone z = array.get(zones, nearSupIdx)
    supStruct := f_structuralStr(z)
    supResp   := f_histRespect(z)
    supAQ     := f_approachQ(z)
    supCA     := f_contextA(z)
    supReach  := f_reachL(nearSupDist, z)
    supHold   := f_holdL(supStruct, supResp, supAQ, supCA)
    // FVG near zone boosts hold score
    if fvgNearSupport
        supHold := math.min(100.0, supHold + 5.0)
    // OB proximity bonus (scaled by confluence score)
    if obNearSupport
        supHold := math.min(100.0, supHold + obSupConfScore * 0.1)
    // POC proximity bonus — zone near highest volume level. No price-only
    // equivalent (Class C data); pocLevel/vaHigh/vaLow stay na without real
    // volume, so the bonus is unreachable rather than comparing against na.
    if not na(pocLevel) and atrValue > 0 and math.abs(z.mid - pocLevel) / atrValue < 1.0
        supHold := math.min(100.0, supHold + 7.0)
    else if not na(vaLowLevel) and not na(vaHighLevel) and z.mid >= vaLowLevel and z.mid <= vaHighLevel
        supHold := math.min(100.0, supHold + 3.0)
    supBreak  := f_breakL(supHold, supAQ, z)
    supState  := f_stateCalc(supHold, supBreak, supAQ, z)
    supType   := f_zoneTypeStr(z.zoneType)
    supOK     := true
else
    supOK := false

if (showAssessLabels or showAssessTable) and nearResIdx >= 0 and nearResIdx < array.size(zones)
    Zone z = array.get(zones, nearResIdx)
    resStruct := f_structuralStr(z)
    resResp   := f_histRespect(z)
    resAQ     := f_approachQ(z)
    resCA     := f_contextA(z)
    resReach  := f_reachL(nearResDist, z)
    resHold   := f_holdL(resStruct, resResp, resAQ, resCA)
    // FVG near zone boosts hold score
    if fvgNearResistance
        resHold := math.min(100.0, resHold + 5.0)
    // OB proximity bonus (scaled by confluence score)
    if obNearResistance
        resHold := math.min(100.0, resHold + obResConfScore * 0.1)
    // POC proximity bonus — zone near highest volume level. No price-only
    // equivalent (Class C data); pocLevel/vaHigh/vaLow stay na without real
    // volume, so the bonus is unreachable rather than comparing against na.
    if not na(pocLevel) and atrValue > 0 and math.abs(z.mid - pocLevel) / atrValue < 1.0
        resHold := math.min(100.0, resHold + 7.0)
    else if not na(vaLowLevel) and not na(vaHighLevel) and z.mid >= vaLowLevel and z.mid <= vaHighLevel
        resHold := math.min(100.0, resHold + 3.0)
    resBreak  := f_breakL(resHold, resAQ, z)
    resState  := f_stateCalc(resHold, resBreak, resAQ, z)
    resType   := f_zoneTypeStr(z.zoneType)
    resOK     := true
else
    resOK := false

// Assessment labels on chart
var label supAssessLabel = na
var label resAssessLabel = na

if barstate.islast
    if not na(supAssessLabel)
        label.delete(supAssessLabel)
    if not na(resAssessLabel)
        label.delete(resAssessLabel)

    if showAssessLabels and supOK and nearSupIdx >= 0 and nearSupIdx < array.size(zones)
        Zone z = array.get(zones, nearSupIdx)
        string txt = "H" + str.tostring(supHold, "#") + " B" + str.tostring(supBreak, "#") + " R" + str.tostring(supReach, "#")
        color col = supHold > supBreak + 10 ? color.new(#00e676, 10) : supBreak > supHold + 10 ? color.new(#ff1744, 10) : color.new(#ffab00, 10)
        supAssessLabel := label.new(bar_index + 8, z.mid, txt, style=label.style_label_left, color=color.new(color.black, 100), textcolor=col, size=size.normal, tooltip="Support: " + supType + "\nReach: " + str.tostring(supReach, "#") + " " + f_reachClass(supReach) + "\nHold: " + str.tostring(supHold, "#") + " " + f_holdClass(supHold) + "\nBreak: " + str.tostring(supBreak, "#") + " " + f_breakClass(supBreak) + "\nState: " + supState + "\nStructure: " + str.tostring(supStruct, "#") + " | Respect: " + str.tostring(supResp, "#") + "\nApproach: " + str.tostring(supAQ, "#") + " | Context: " + str.tostring(supCA, "#"))

    if showAssessLabels and resOK and nearResIdx >= 0 and nearResIdx < array.size(zones)
        Zone z = array.get(zones, nearResIdx)
        string txt = "H" + str.tostring(resHold, "#") + " B" + str.tostring(resBreak, "#") + " R" + str.tostring(resReach, "#")
        color col = resHold > resBreak + 10 ? color.new(#ff1744, 10) : resBreak > resHold + 10 ? color.new(#00e676, 10) : color.new(#ffab00, 10)
        resAssessLabel := label.new(bar_index + 8, z.mid, txt, style=label.style_label_left, color=color.new(color.black, 100), textcolor=col, size=size.normal, tooltip="Resistance: " + resType + "\nReach: " + str.tostring(resReach, "#") + " " + f_reachClass(resReach) + "\nHold: " + str.tostring(resHold, "#") + " " + f_holdClass(resHold) + "\nBreak: " + str.tostring(resBreak, "#") + " " + f_breakClass(resBreak) + "\nState: " + resState + "\nStructure: " + str.tostring(resStruct, "#") + " | Respect: " + str.tostring(resResp, "#") + "\nApproach: " + str.tostring(resAQ, "#") + " | Context: " + str.tostring(resCA, "#"))

// Assessment table
color tcAssess = color.new(color.gray, 20)   // standard text color (dark gray)
color hdAssess = color.new(color.gray, 90)   // header row background (very light gray)
color wnAssess = #ff6f00                     // warning accent (readable on light bg)

var table assessTable = na
if showAssessTable and barstate.islast
    if na(assessTable)
        assessTable := table.new(position.top_left, 3, 12, bgcolor=color.new(color.white, 5), border_color=color.new(color.gray, 60), border_width=1, frame_color=color.new(color.gray, 40), frame_width=1)
    table.cell(assessTable, 0, 0, "Zone Assessment", text_color=tcAssess, text_size=size.normal, bgcolor=hdAssess)
    table.cell(assessTable, 1, 0, "SUPPORT", text_color=#00c853, text_size=size.normal, text_halign=text.align_center, bgcolor=hdAssess)
    table.cell(assessTable, 2, 0, "RESISTANCE", text_color=#d50000, text_size=size.normal, text_halign=text.align_center, bgcolor=hdAssess)
    table.cell(assessTable, 0, 1, "Zone", text_color=tcAssess, text_size=size.small)
    table.cell(assessTable, 1, 1, supOK ? supType : "—", text_color=supOK ? tcAssess : color.gray, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 1, resOK ? resType : "—", text_color=resOK ? tcAssess : color.gray, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 0, 2, "Distance", text_color=tcAssess, text_size=size.small)
    table.cell(assessTable, 1, 2, supOK ? str.tostring(nearSupDist, "#.1") + " ATR" : "—", text_color=tcAssess, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 2, resOK ? str.tostring(nearResDist, "#.1") + " ATR" : "—", text_color=tcAssess, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 0, 3, "Structure", text_color=tcAssess, text_size=size.small)
    table.cell(assessTable, 1, 3, supOK ? str.tostring(supStruct, "#") : "—", text_color=supOK and supStruct >= 60 ? #00c853 : supOK and supStruct >= 40 ? wnAssess : color.gray, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 3, resOK ? str.tostring(resStruct, "#") : "—", text_color=resOK and resStruct >= 60 ? #d50000 : resOK and resStruct >= 40 ? wnAssess : color.gray, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 0, 4, "Respect", text_color=tcAssess, text_size=size.small)
    table.cell(assessTable, 1, 4, supOK ? str.tostring(supResp, "#") : "—", text_color=supOK and supResp >= 60 ? #00c853 : supOK and supResp >= 40 ? wnAssess : color.gray, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 4, resOK ? str.tostring(resResp, "#") : "—", text_color=resOK and resResp >= 60 ? #d50000 : resOK and resResp >= 40 ? wnAssess : color.gray, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 0, 5, "Approach", text_color=tcAssess, text_size=size.small)
    table.cell(assessTable, 1, 5, supOK ? str.tostring(supAQ, "#") : "—", text_color=supOK and supAQ > 10 ? #00c853 : supOK and supAQ < -10 ? #d50000 : wnAssess, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 5, resOK ? str.tostring(resAQ, "#") : "—", text_color=resOK and resAQ > 10 ? #d50000 : resOK and resAQ < -10 ? #00c853 : wnAssess, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 0, 6, "Context", text_color=tcAssess, text_size=size.small)
    table.cell(assessTable, 1, 6, supOK ? str.tostring(supCA, "#") : "—", text_color=supOK and supCA > 5 ? #00c853 : supOK and supCA < -5 ? #d50000 : wnAssess, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 6, resOK ? str.tostring(resCA, "#") : "—", text_color=resOK and resCA > 5 ? #d50000 : resOK and resCA < -5 ? #00c853 : wnAssess, text_size=size.small, text_halign=text.align_center)
    // Results
    table.cell(assessTable, 0, 7, "─ RESULT ─", text_color=color.new(color.gray, 50), text_size=size.small)
    table.cell(assessTable, 1, 7, "", text_size=size.small)
    table.cell(assessTable, 2, 7, "", text_size=size.small)
    table.cell(assessTable, 0, 8, "Reach", text_color=tcAssess, text_size=size.normal)
    table.cell(assessTable, 1, 8, supOK ? str.tostring(supReach, "#") + " " + f_reachClass(supReach) : "—", text_color=supOK and supReach >= 60 ? #00c853 : tcAssess, text_size=size.normal, text_halign=text.align_center)
    table.cell(assessTable, 2, 8, resOK ? str.tostring(resReach, "#") + " " + f_reachClass(resReach) : "—", text_color=resOK and resReach >= 60 ? #d50000 : tcAssess, text_size=size.normal, text_halign=text.align_center)
    table.cell(assessTable, 0, 9, "Hold", text_color=tcAssess, text_size=size.normal)
    table.cell(assessTable, 1, 9, supOK ? str.tostring(supHold, "#") + " " + f_holdClass(supHold) : "—", text_color=supOK and supHold >= 60 ? #00c853 : supOK and supHold >= 45 ? wnAssess : color.gray, text_size=size.normal, text_halign=text.align_center)
    table.cell(assessTable, 2, 9, resOK ? str.tostring(resHold, "#") + " " + f_holdClass(resHold) : "—", text_color=resOK and resHold >= 60 ? #d50000 : resOK and resHold >= 45 ? wnAssess : color.gray, text_size=size.normal, text_halign=text.align_center)
    table.cell(assessTable, 0, 10, "Break", text_color=tcAssess, text_size=size.normal)
    table.cell(assessTable, 1, 10, supOK ? str.tostring(supBreak, "#") + " " + f_breakClass(supBreak) : "—", text_color=supOK and supBreak >= 60 ? #d50000 : supOK and supBreak >= 45 ? wnAssess : #00c853, text_size=size.normal, text_halign=text.align_center)
    table.cell(assessTable, 2, 10, resOK ? str.tostring(resBreak, "#") + " " + f_breakClass(resBreak) : "—", text_color=resOK and resBreak >= 60 ? #00c853 : resOK and resBreak >= 45 ? wnAssess : #d50000, text_size=size.normal, text_halign=text.align_center)
    table.cell(assessTable, 0, 11, "Status", text_color=tcAssess, text_size=size.normal)
    color supSCol = supState == "REACTION LIKELY" ? #00c853 : supState == "BREAK LIKELY" ? #d50000 : str.contains(supState, "PRESSURE") or str.contains(supState, "WEAKENING") ? wnAssess : tcAssess
    color resSCol = resState == "REACTION LIKELY" ? #d50000 : resState == "BREAK LIKELY" ? #00c853 : str.contains(resState, "PRESSURE") or str.contains(resState, "WEAKENING") ? wnAssess : tcAssess
    table.cell(assessTable, 1, 11, supOK ? supState : "—", text_color=supSCol, text_size=size.small, text_halign=text.align_center)
    table.cell(assessTable, 2, 11, resOK ? resState : "—", text_color=resSCol, text_size=size.small, text_halign=text.align_center)

// Assessment alerts
bool supNear = supOK and nearSupDist <= 1.0
bool resNear = resOK and nearResDist <= 1.0
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed

alertcondition(alertOk and (supNear and supReach >= 60), "Support Relevant", "VEIN-SZ · SUPPORT RELEVANT · {{ticker}} {{interval}}")
alertcondition(alertOk and (resNear and resReach >= 60), "Resistance Relevant", "VEIN-SZ · RESISTANCE RELEVANT · {{ticker}} {{interval}}")
alertcondition(alertOk and (supNear and supHold >= 60 and supBreak < 45), "Likely Support Hold", "VEIN-SZ · SUPPORT HOLD · {{ticker}} {{interval}}")
alertcondition(alertOk and (resNear and resHold >= 60 and resBreak < 45), "Likely Resist Hold", "VEIN-SZ · RESISTANCE HOLD · {{ticker}} {{interval}}")
alertcondition(alertOk and (supNear and supBreak >= 60 and supHold < 45), "Likely Support Break", "VEIN-SZ · SUPPORT BREAK · {{ticker}} {{interval}}")
alertcondition(alertOk and (resNear and resBreak >= 60 and resHold < 45), "Likely Resist Break", "VEIN-SZ · RESISTANCE BREAK · {{ticker}} {{interval}}")

Overview

Vein: Structure & Zones 4H [WavesUnchained] calculates micro and major swing pivots, break of structure (BOS), equal highs and lows, and Wyckoff heuristics including springs, upthrusts, and climax events. User settings control pivot confirmation lengths, ATR multiples for zone widths, volume thresholds, and zone retention rules. As pivot detection requires confirmed right-side bars, structural events and zones appear with a slight lag or update retroactively upon confirmation.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

SMC / ICT / price action, Support & resistance, Trend

Markets

Any symbol

Timeframes

M15-H1, H4 and above

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
Micro Pivot Left Barsint3leftBars
Micro Pivot Right Barsint3rightBars
Major Pivot Left Barsint7majorLeftBars
Major Pivot Right Barsint7majorRightBars
BOS Buffer (points)float0.0bosBuffer
EQH/EQL Threshold (ATR mult)float0.1eqhEqlThreshold
Show EQH/EQLboolfalseshowEQHEQL
Min Wick Ratio for Spring/Upthrustfloat0.35minWickRatio
Min Candle Range / ATRfloat0.5minCandleATR
Climax: Min Range / ATRfloat1.8climaxRangeATR
Climax: Min Volume / Avgfloat1.8climaxVolFactor
Test: Max Range vs Climaxfloat0.6testRangeFactor
Show all 40 settings
SettingTypeDefaultVariable
Test: Max Volume vs Climaxfloat0.7testVolFactor
Test: Lookback Barsint10testLookback
Rejection: Min Wick Ratiofloat0.5rejWickRatio
Rejection: Max Body/Rangefloat0.3rejBodyMax
Strong: Min Wick Ratiofloat0.55strongWickRatio
Strong: Min Candle / ATRfloat0.8strongCandleATR
Strong: Min Volume / Avgfloat1.3strongVolFactor
Strong: Min Sweep Depth (ATR)float0.15strongSweepATR
Major Swing ZonesbooltrueshowSwingZones
Spring / Upthrust ZonesbooltrueshowSpringZones
BOS Flip ZonesbooltrueshowBOSFlips
Climax ZonesbooltrueshowClimaxZones
Zone Width (ATR multiple)float0.3zoneWidthATR
Max Visible Zonesint8maxZones
Zone Expire After (bars)int200zoneExpireBars
Show Broken ZonesboolfalseshowBrokenZones
Volume Average Lengthint20volAvgLen
ATR Lengthint14atrLen
Max Assessment Distance (ATR)float3.0maxAssessDistATR
Approach Lookback Barsint5approachBars
Show Assessment TableboolfalseshowAssessTable
Show Assessment Labels on ZonesbooltrueshowAssessLabels
Display ModestringLightdisplayMode
Show Swing LabelsbooltrueshowSwings
Show BOS LabelsbooltrueshowBOS
Show Fake Break LabelsbooltrueshowFakeBreaks
Show Spring / Upthrust LabelsbooltrueshowSpringUT
Show Climax LabelsbooltrueshowClimax
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

This Pine Script indicator plots a ZigZag on the price chart. It identifies reversal points when price moves more than a set percentage from the last pivot. The script calculates forecasts using zigzag data and labels detected geometric patterns including harm

Indicator v4 TrendSupport & resistance
218 stars

This indicator plots market structure on the price chart by identifying swing highs and lows. It connects these points with lines to visualize trend direction and detect Break of Structure (BOS) when price breaks a prior swing in the current trend. Change of C

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Market Structure - BOS & CHoCH

Open Source Fractal

by cantolab
183 stars

This indicator implements the Fractal Model concept to plot market structure shifts, liquidity sweeps, and Change in State of Delivery (CISD) levels. It overlays custom higher-timeframe candle previews, open prices, and time markers directly on the price chart

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · MIT GitHub repository of Open Source Fractal
56 stars

BigBeluga Smart Money Concepts identifies and overlays market structure and volumetric order blocks directly on the price chart. It analyzes market swings, trend sweeps, and mapping structure using customizable pivot lengths and calculation modes. The indicato

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · None stated GitHub repository of BigBeluga - Smart Money Concepts
53 stars

Apex Trend & Liquidity Master calculates a trend cloud using customizable moving averages expanded by a volatility multiplier directly on the main price chart. It identifies dynamic support and resistance liquidity zones from swing high and low pivots, extendi

Indicator v6 TrendSupport & resistance
Updated 2 months ago · MPL-2.0 GitHub repository of Apex Trend & Liquidity Master

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