//@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.
Micro and major swing detection with quality grading
Auto support and resistance zones from swings and springs
Climax sequence state machine and rejection candles
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Micro Pivot Left Bars
int
3
leftBars
Micro Pivot Right Bars
int
3
rightBars
Major Pivot Left Bars
int
7
majorLeftBars
Major Pivot Right Bars
int
7
majorRightBars
BOS Buffer (points)
float
0.0
bosBuffer
EQH/EQL Threshold (ATR mult)
float
0.1
eqhEqlThreshold
Show EQH/EQL
bool
false
showEQHEQL
Min Wick Ratio for Spring/Upthrust
float
0.35
minWickRatio
Min Candle Range / ATR
float
0.5
minCandleATR
Climax: Min Range / ATR
float
1.8
climaxRangeATR
Climax: Min Volume / Avg
float
1.8
climaxVolFactor
Test: Max Range vs Climax
float
0.6
testRangeFactor
Show all 40 settings
Setting
Type
Default
Variable
Test: Max Volume vs Climax
float
0.7
testVolFactor
Test: Lookback Bars
int
10
testLookback
Rejection: Min Wick Ratio
float
0.5
rejWickRatio
Rejection: Max Body/Range
float
0.3
rejBodyMax
Strong: Min Wick Ratio
float
0.55
strongWickRatio
Strong: Min Candle / ATR
float
0.8
strongCandleATR
Strong: Min Volume / Avg
float
1.3
strongVolFactor
Strong: Min Sweep Depth (ATR)
float
0.15
strongSweepATR
Major Swing Zones
bool
true
showSwingZones
Spring / Upthrust Zones
bool
true
showSpringZones
BOS Flip Zones
bool
true
showBOSFlips
Climax Zones
bool
true
showClimaxZones
Zone Width (ATR multiple)
float
0.3
zoneWidthATR
Max Visible Zones
int
8
maxZones
Zone Expire After (bars)
int
200
zoneExpireBars
Show Broken Zones
bool
false
showBrokenZones
Volume Average Length
int
20
volAvgLen
ATR Length
int
14
atrLen
Max Assessment Distance (ATR)
float
3.0
maxAssessDistATR
Approach Lookback Bars
int
5
approachBars
Show Assessment Table
bool
false
showAssessTable
Show Assessment Labels on Zones
bool
true
showAssessLabels
Display Mode
string
Light
displayMode
Show Swing Labels
bool
true
showSwings
Show BOS Labels
bool
true
showBOS
Show Fake Break Labels
bool
true
showFakeBreaks
Show Spring / Upthrust Labels
bool
true
showSpringUT
Show Climax Labels
bool
true
showClimax
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.
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
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
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
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
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
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