Indicator · Pine Script v6 · On the chart

Tweezer & Kangaroo Zones [WavesUnchained]

by casoon

This overlay indicator detects tweezer tops and bottoms and kangaroo-tail pin bars, then draws supply and demand zones from those patterns.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code1,409
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,409 lines of Pine Script v6 from indicators/market_structure/tweezer_kangaroo_zones/tweezer_kangaroo_zones.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

tweezer_kangaroo_zones.pine
//@version=6
// ============================================================================
// Tweezer & Kangaroo Zones [WavesUnchained]
// Version: 3.2.1
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Modular pattern detection with zone scoring, structure context, and confluence analysis
//
// Features:
//   ✓ Tweezer pattern detection with TF sensitivity
//   ✓ Kangaroo tail (pin bar) detection
//   ✓ Dynamic supply/demand zones
//   ✓ 5-component zone scoring (0-100)
//   ✓ Zone freshness decay & Top-N filtering
//   ✓ Structure context (HH/HL/LH/LL swing detection)
//   ✓ Pattern weighting (Tweezer vs Kangaroo)
//   ✓ Zone merge on overlap
//   ✓ Session-aware profiles (RTH/ETH)
//   ✓ HTF Stack panel with 3 timeframes
//   ✓ Confluence scoring system (8 components)
//   ✓ Supertrend & MOST integration
//   ✓ Performance metrics & evaluation
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   real trade volume only — pattern volume checks and
//                         confluence Relative Volume component degrade to neutral
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
//
// ============================================================================
indicator("Tweezer & Kangaroo Zones [WavesUnchained]",
     shorttitle = "TKZ",
     overlay = true,
     max_boxes_count = 500,
     max_labels_count = 500,
     max_lines_count = 500)

// Module code injected by build system below

// ============================================================================
// INLINE MODULES (Combined at build time)
// ============================================================================

// ============================================================================
// MODULE: inputs
// ============================================================================

//==================== Inputs ====================//

// Pattern Detection
input_enable_tweezers = input.bool(true, "Detect Tweezers", group="Pattern Detection")
tweezer_lookback = input.int(3, "Lookback bars", minval=1, maxval=5, group="Pattern Detection")
tweezer_tolerance = input.float(0.01, "Level tolerance (%)", step=0.001, group="Pattern Detection")
tweezer_require_long_body = input.bool(false, "Require long body on 1st candle", group="Pattern Detection")
tweezer_require_trend = input.bool(false, "Require trend context", group="Pattern Detection")
tweezer_check_wicks = input.bool(true, "Check wick similarity", group="Pattern Detection")
tweezer_wick_tolerance = input.float(2.0, "Max wick ratio", step=0.5, minval=1.1, group="Pattern Detection")
tweezer_min_volume = input.float(0.8, "Min RelVolume for 2nd candle", step=0.1, group="Pattern Detection")

// Pattern Weights (Kangaroo typically stronger due to clear rejection wick)
wTweezer    = input.float(0.8, "Tweezer Weight", minval=0.5, maxval=1.5, step=0.1, group="Pattern Detection")
wKangaroo   = input.float(1.2, "Kangaroo Weight", minval=0.5, maxval=1.5, step=0.1, group="Pattern Detection")

input_enable_kangaroo = input.bool(true, "Detect Kangaroo Tail", group="Pattern Detection")
kt_wickBodyMin = input.float(1.8, "Min wick/body ratio", group="Pattern Detection")
kt_bodyFracMax = input.float(0.50, "Max body as frac of range", group="Pattern Detection")
kt_bodyPosition = input.float(0.3, "Body position tolerance (0-1)", group="Pattern Detection")
kt_requireTrend = input.bool(false, "Require trend context", group="Pattern Detection")
kt_min_volume = input.float(0.9, "Min RelVolume for rejection", step=0.1, group="Pattern Detection")

// Zone Management
zoneExtendBars  = input.int(200, "Extend (bars)", minval=10, group="Zone Management")
zoneTTL         = input.int(400, "Lifetime (bars)", group="Zone Management")
invalidateOnClose = input.bool(true, "Invalidate on close-through", group="Zone Management")
retestOnceOnly  = input.bool(true, "Only first retest can signal", group="Zone Management")
zoneOverlapPct  = input.float(0.5, "Max overlap to merge zones (%)", step=0.1, group="Zone Management")
zoneMergeEnabled = input.bool(true, "Merge overlapping zones (vs skip)", group="Zone Management")

// Trend Detection
emaLen      = input.int(200, "EMA length", group="Trend Detection")
trendTF     = input.timeframe("240", "HTF for regime gate", group="Trend Detection")

// Supertrend
stFactor    = input.float(3.0, "Factor", step=0.1, group="Supertrend")
stAtrLen    = input.int(10, "ATR Length", group="Supertrend")

// MOST
mostLen     = input.int(10, "ATR Length", group="MOST")
mostMult    = input.float(2.0, "Multiplier", group="MOST")

// HTF Stack
stackTF1    = input.timeframe("15", "TF1", group="HTF Stack")
stackTF2    = input.timeframe("60", "TF2", group="HTF Stack")
stackTF3    = input.timeframe("240", "TF3", group="HTF Stack")

// Trend Gate
trendFilterType = input.string("EMA", "Trend Filter Type", options=["None", "EMA", "Supertrend", "MOST"], group="Trend Gate")
gateSTscope     = input.string("Local", "ST scope", options=["Local","HTF","Both"], group="Trend Gate")
gateSTtf        = input.timeframe("240", "ST HTF", group="Trend Gate")
gateMOSTscope   = input.string("Local", "MOST scope", options=["Local","HTF","Both"], group="Trend Gate")
gateMOSTtf      = input.timeframe("240", "MOST HTF", group="Trend Gate")

// TF Sensitivity (adjusts pattern detection based on timeframe)
tfSensitivity   = input.float(1.0, "TF Sensitivity", minval=0.5, maxval=2.0, step=0.1, group="TF Sensitivity", tooltip="Adjusts pattern thresholds for timeframe.\n< 1.0 = Stricter (low TF)\n1.0 = Balanced (mid TF)\n> 1.0 = Looser (high TF)")
tfAutoDetect    = input.bool(true, "Auto-detect from chart TF", group="TF Sensitivity", tooltip="Automatically set sensitivity based on chart timeframe")

// Auto-Tuning
autoTune        = input.bool(true, "Enable Auto-Threshold Tuning", group="Auto-Tuning")
atrLen          = input.int(14, "ATR length", group="Auto-Tuning")
chopLen         = input.int(14, "Choppiness length", minval=2, group="Auto-Tuning")
volLen          = input.int(20, "Volume SMA length", group="Auto-Tuning")

// Session Filter (simplified)
useSession      = input.bool(false, "Enable session filter", group="Session Filter")
sessionRTH      = input.session("0930-1600", "RTH Session", group="Session Filter")
resetZonesDaily = input.bool(false, "Reset zones daily", group="Session Filter")

// Fixed session values (sensible defaults)
sessionETH      = "0000-2359"
profileMode     = "Both"
signalOnlyInSess = true
rthTTLmult      = 1.0
ethTTLmult      = 0.7
rthRvSpike      = 1.30
ethRvSpike      = 1.50
rthZoneThinATR  = 0.6
ethZoneThinATR  = 0.5
rthChopSoft     = 55.0
ethChopSoft     = 60.0
rthChopHard     = 62.0
ethChopHard     = 65.0

// Signal Scoring (simplified)
minScoreBuy  = input.float(4.5, "Min Score BUY", group="Signal Scoring")
minScoreSell = input.float(5.5, "Min Score SELL", group="Signal Scoring")

// Fixed score weights (sensible defaults)
useRegime    = true
useStack     = true
useStruct    = true
useRelVol    = true
useChopAdv   = true
useZoneThin  = true
useST        = false
useMOST      = false
wRegime     = 2.0
wStack      = 3.0
wStruct     = 1.0
wRelVol     = 1.0
wChop       = 1.0
wZoneThin   = 1.0
wSupertrend = 1.0
wMOST       = 1.0
wPattern    = 1.5

// ST/MOST scope uses Trend Gate settings
stScope  = gateSTscope
stTF     = gateSTtf
mostScope = gateMOSTscope
mostTF    = gateMOSTtf

// Fixed label thresholds
labelStrongTh = 8.0
labelMidTh    = 6.0

// Structure Context
useStructContext  = input.bool(true, "Enable Structure Context", group="Structure Context")
structLookback    = input.int(20, "Swing Lookback", minval=5, maxval=50, group="Structure Context")
structBonus       = input.float(10.0, "Structure Bonus (0-20)", minval=0, maxval=20, group="Structure Context")
requireAfterSwing = input.bool(false, "Only after swing point", group="Structure Context")

// Zone Scoring (5-Component System)
useZoneScore      = input.bool(true, "Enable Zone Scoring", group="Zone Scoring")
zoneScoreMin      = input.float(50.0, "Min Zone Score (0-100)", minval=0, maxval=100, group="Zone Scoring")
zoneScoreTopN     = input.int(5, "Show Top N Zones", minval=1, maxval=20, group="Zone Scoring")
zoneHalfLife      = input.int(80, "Freshness Half-Life (bars)", minval=10, maxval=500, group="Zone Scoring")
zoneMaxTouches    = input.int(4, "Max Touches before weak", minval=1, maxval=10, group="Zone Scoring")
zoneLookback      = input.int(100, "Lookback for extremes", minval=20, maxval=500, group="Zone Scoring")

// Zone Classification & Labels
showZoneLabels    = input.bool(true, "Show Zone Labels", group="Zone Labels")
showTooltips      = input.bool(true, "Show Tooltips", group="Zone Labels")
showOnlyAB        = input.bool(false, "Show only A/B Zones", group="Zone Labels")
gradeAMin         = input.float(75.0, "Grade A: Min Score", minval=50, maxval=100, group="Zone Labels")
gradeBMin         = input.float(55.0, "Grade B: Min Score", minval=30, maxval=80, group="Zone Labels")
reversalBonus     = input.float(15.0, "Reversal Bonus", minval=0, maxval=30, group="Zone Labels")
continuationPenalty = input.float(10.0, "Continuation Penalty", minval=0, maxval=30, group="Zone Labels")

// Panels
useHTFPanel     = input.bool(false, "Show HTF Stack Panel", group="Panels")

signalRequireTrend = false

// Visuals
colSupply   = input.color(color.new(#FF4444, 85), "Supply zone", group="Colors")
colDemand   = input.color(color.new(#44FF88, 85), "Demand zone", group="Colors")
colActiveBrd= input.color(color.new(#FFFFFF, 30), "Active border", group="Colors")
colDeadBrd  = input.color(color.new(#808080, 70), "Inactive border", group="Colors")
zoneStyle   = input.string("Solid", "Zone Style", options=["Solid", "Dashed", "Dotted"], group="Colors")

// Debug Logging (for pattern tuning)
enableDebugLog    = input.bool(false, "Enable Debug Logging", group="Debug Logging")
logStartDate      = input.time(timestamp("2025-08-20 00:00"), "Log Start Date", group="Debug Logging")
logTweezerChecks  = input.bool(true, "Log Tweezer Checks", group="Debug Logging")
logKangarooChecks = input.bool(true, "Log Kangaroo Checks", group="Debug Logging")
logZoneCreation   = input.bool(true, "Log Zone Creation", group="Debug Logging")
logFailedPatterns = input.bool(true, "Log Failed Pattern Checks", group="Debug Logging")


// ============================================================================
// MODULE: helpers
// ============================================================================

//==================== Helpers ====================//

range_val  = high - low
uWick  = high - math.max(open, close)
lWick  = math.min(open, close) - low
pct(d) => nz(d) / nz(close, 1)

// Candle body helpers
bodyHi = math.max(close, open)
bodyLo = math.min(close, open)
body = bodyHi - bodyLo
bodyAvg = ta.ema(body, 14)
longBody = body > bodyAvg
whiteBody = open < close
blackBody = open > close

// Volume Analysis
// Real trade volume only (DATA_VALIDITY.md §2/§7) - tick/n/a volume is not a
// participation signal. volCheck/useRelVol degrade to neutral below when this
// is false, instead of gating pattern detection on a meaningless tick count.
volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)
volAvg = ta.sma(volume, volLen)
relVol = volume / math.max(1, volAvg)  // Relative Volume: current / average

atr = ta.atr(atrLen)
atrPct = atr / math.max(1e-6, close)

//==================== TF Sensitivity Calculation ====================//
// Auto-detect TF multiplier based on chart timeframe
// Returns: 0.7 for low TF, 1.0 for mid TF, 1.3-1.5 for high TF
f_get_tf_minutes() =>
    tfStr = timeframe.period
    multiplier = timeframe.multiplier
    if str.contains(tfStr, "S")
        multiplier / 60.0  // Seconds to minutes
    else if str.contains(tfStr, "D")
        multiplier * 1440.0  // Days to minutes
    else if str.contains(tfStr, "W")
        multiplier * 10080.0  // Weeks to minutes
    else if str.contains(tfStr, "M")
        multiplier * 43200.0  // Months to minutes (approx)
    else
        float(multiplier)  // Already in minutes

tfMinutes = f_get_tf_minutes()

// Calculate auto TF sensitivity
// 1-15M = 0.7, 30M-2H = 1.0, 4H+ = 1.3-1.5
f_calc_tf_sensitivity(mins) =>
    if mins <= 15
        0.7
    else if mins <= 120
        1.0
    else if mins <= 240
        1.3
    else
        1.5

tfSensitivityEff = tfAutoDetect ? f_calc_tf_sensitivity(tfMinutes) : tfSensitivity

// Apply TF sensitivity to pattern thresholds
// Higher sensitivity = looser thresholds (more patterns detected)
tweezerToleranceEff = tweezer_tolerance * tfSensitivityEff
ktWickBodyMinBase   = kt_wickBodyMin / tfSensitivityEff  // Lower = looser
ktBodyFracMaxBase   = kt_bodyFracMax * tfSensitivityEff  // Higher = looser

// Choppiness Index (0 trend → 100 chop)
f_chop(len) =>
    var float trSum = 0.0
    trSum := 0.0
    for i = 0 to len - 1
        trSum += ta.tr[i]
    hh = ta.highest(high, len)
    ll = ta.lowest(low, len)
    rng = math.max(1e-6, hh - ll)
    100 * math.log10(trSum / rng) / math.log10(len)
chop = f_chop(chopLen)

// Sessions
inRTH = useSession ? not na(time(timeframe.period, sessionRTH)) : true
inETH = useSession ? not na(time(timeframe.period, sessionETH)) : true
activeRTH = profileMode == "RTH only" or (profileMode=="Both" and inRTH and not inETH)
activeETH = profileMode == "ETH only" or (profileMode=="Both" and inETH and not inRTH)
useRTH = activeRTH or (profileMode=="Both" and (inRTH and inETH or (not inRTH and not inETH)))
inSession = useSession ? (profileMode=="Both" ? (inRTH or inETH) : (profileMode=="RTH only" ? inRTH : inETH)) : true
newDay = ta.change(time("D")) != 0

// Effective per-session thresholds
effRvSpike     = useRTH ? rthRvSpike : ethRvSpike
effZoneThinATR = useRTH ? rthZoneThinATR : ethZoneThinATR
effChopSoft    = useRTH ? rthChopSoft : ethChopSoft
effChopHard    = useRTH ? rthChopHard : ethChopHard
zoneTTLAdj     = useRTH ? zoneTTL * rthTTLmult : zoneTTL * ethTTLmult

// Auto-tuned thresholds with TF sensitivity + chop adjustment
var float ktBodyFracEff  = ktBodyFracMaxBase
var float ktWickBodyEff  = ktWickBodyMinBase

if autoTune
    // Auto-tune Kangaroo thresholds based on market conditions (chop)
    ktBodyFracEff := ktBodyFracMaxBase * (1 - 0.5 * math.max(0, (chop - 50) / 50))
    ktWickBodyEff := ktWickBodyMinBase * (1 + 0.25 * math.max(0, (chop - 50) / 50))
else
    // No chop adjustment, just use TF-adjusted values
    ktBodyFracEff := ktBodyFracMaxBase
    ktWickBodyEff := ktWickBodyMinBase

// Logging helper
shouldLog = enableDebugLog and time >= logStartDate


// ============================================================================
// MODULE: structure_context
// ============================================================================

//==================== Structure Context (Swing Detection) ====================//

// Swing High/Low Detection using pivots
swingHighBar = ta.pivothigh(high, structLookback, structLookback)
swingLowBar = ta.pivotlow(low, structLookback, structLookback)

// Track last swing values for structure detection
var float lastSwingHigh = na
var float lastSwingLow = na
var float prevSwingHigh = na
var float prevSwingLow = na

if not na(swingHighBar)
    prevSwingHigh := lastSwingHigh
    lastSwingHigh := swingHighBar

if not na(swingLowBar)
    prevSwingLow := lastSwingLow
    lastSwingLow := swingLowBar

// Structure Detection: HH/HL = Bullish, LH/LL = Bearish
isHH = not na(lastSwingHigh) and not na(prevSwingHigh) and lastSwingHigh > prevSwingHigh
isHL = not na(lastSwingLow) and not na(prevSwingLow) and lastSwingLow > prevSwingLow
isLH = not na(lastSwingHigh) and not na(prevSwingHigh) and lastSwingHigh < prevSwingHigh
isLL = not na(lastSwingLow) and not na(prevSwingLow) and lastSwingLow < prevSwingLow

// Bullish structure: HH + HL (uptrend)
bullishStructure = isHH or isHL
// Bearish structure: LH + LL (downtrend)
bearishStructure = isLH or isLL

// Check if pattern is near recent swing (within structLookback bars)
barsFromSwingHigh = bar_index - ta.valuewhen(not na(swingHighBar), bar_index, 0)
barsFromSwingLow = bar_index - ta.valuewhen(not na(swingLowBar), bar_index, 0)
nearSwingHigh = barsFromSwingHigh <= structLookback
nearSwingLow = barsFromSwingLow <= structLookback


// ============================================================================
// MODULE: pattern_detection
// ============================================================================

//==================== Trend Gate (needed for pattern detection) ====================//

ema      = ta.ema(close, emaLen)
emaUp    = close > ema
regUpHTF = request.security(syminfo.tickerid, trendTF, close > ta.ema(close, emaLen), lookahead=barmerge.lookahead_off)
emaGateUp  = emaUp and regUpHTF
emaGateDn  = not emaUp and not regUpHTF

// Trend detection helpers
downTrend = close < ema
upTrend = close > ema

//==================== Tweezer Detection ====================//
// Improved Tweezer Detection with:
// - Flexible lookback (not just consecutive)
// - Wick similarity check
// - Volume validation
// - Pattern strength score
// - Optional body size requirements

var bool tTop = false
var int tTopBack = na
var float tTopStrength = na  // Pattern strength 0-1
var bool tBot = false
var int tBotBack = na
var float tBotStrength = na  // Pattern strength 0-1

if input_enable_tweezers and inSession
    tTop := false
    tTopBack := na
    tTopStrength := na
    tBot := false
    tBotBack := na
    tBotStrength := na

    // Tweezer Top: Search for 2 candles with similar highs
    // 1st candle: Bullish (Uptrend continuation)
    // 2nd candle: Bearish (Reversal)
    trendOKTop = tweezer_require_trend ? upTrend : true
    if trendOKTop
        for i = 1 to tweezer_lookback
            if whiteBody[i] and blackBody  // 1st green, 2nd red
                // Level Match: Highs should be very similar
                levelDiff = math.abs(high - high[i]) / high
                levelMatch = levelDiff <= tweezerToleranceEff

                // Optional: Body Size Check
                bodyCheck = tweezer_require_long_body ? (longBody[i] and body > bodyAvg * 0.5) : true

                // Optional: Wick Similarity Check
                wickCheck = true
                wickSimilarity = 1.0
                if tweezer_check_wicks
                    wick1 = high[i] - bodyHi[i]
                    wick2 = high - bodyHi
                    wickRatio = math.max(wick1, wick2) / math.max(1e-6, math.min(wick1, wick2))
                    wickCheck := wickRatio <= tweezer_wick_tolerance
                    wickSimilarity := 1.0 - math.min(1.0, (wickRatio - 1.0) / (tweezer_wick_tolerance - 1.0))

                // Volume Check: 2nd candle should have good volume
                volCheck = not volumeIsReal or relVol >= tweezer_min_volume

                if levelMatch and bodyCheck and wickCheck and volCheck
                    levelSimilarity = 1.0 - math.min(1.0, levelDiff / tweezerToleranceEff)
                    strength = (levelSimilarity * 0.6) + (wickSimilarity * 0.4)

                    tTop := true
                    tTopBack := i
                    tTopStrength := strength

                    if shouldLog and logTweezerChecks
                        log.info(str.format("TWEEZER_TOP_FOUND | {0,date,yyyy-MM-dd HH:mm} | lookback={1} | levelDiff={2,number,#.####} | wickRatio={3,number,#.##} | relVol={4,number,#.##} | strength={5,number,#.##}",
                            time, i, levelDiff, tweezer_check_wicks ? (math.max(high[i] - bodyHi[i], high - bodyHi) / math.max(1e-6, math.min(high[i] - bodyHi[i], high - bodyHi))) : 0.0, relVol, strength))
                    break
                else
                    if shouldLog and logTweezerChecks and logFailedPatterns and i == 1
                        log.info(str.format("TWEEZER_TOP_FAIL | {0,date,yyyy-MM-dd HH:mm} | levelMatch={1} (diff={2,number,#.####} vs tol={3,number,#.####}) | bodyCheck={4} | wickCheck={5} | volCheck={6} (rv={7,number,#.##} vs min={8,number,#.##})",
                            time, levelMatch, levelDiff, tweezerToleranceEff, bodyCheck, wickCheck, volCheck, relVol, tweezer_min_volume))

    // Tweezer Bottom: Search for 2 candles with similar lows
    trendOKBot = tweezer_require_trend ? downTrend : true
    if trendOKBot
        for i = 1 to tweezer_lookback
            if blackBody[i] and whiteBody  // 1st red, 2nd green
                levelDiff = math.abs(low - low[i]) / low
                levelMatch = levelDiff <= tweezerToleranceEff

                bodyCheck = tweezer_require_long_body ? (longBody[i] and body > bodyAvg * 0.5) : true

                wickCheck = true
                wickSimilarity = 1.0
                if tweezer_check_wicks
                    wick1 = bodyLo[i] - low[i]
                    wick2 = bodyLo - low
                    wickRatio = math.max(wick1, wick2) / math.max(1e-6, math.min(wick1, wick2))
                    wickCheck := wickRatio <= tweezer_wick_tolerance
                    wickSimilarity := 1.0 - math.min(1.0, (wickRatio - 1.0) / (tweezer_wick_tolerance - 1.0))

                volCheck = not volumeIsReal or relVol >= tweezer_min_volume

                if levelMatch and bodyCheck and wickCheck and volCheck
                    levelSimilarity = 1.0 - math.min(1.0, levelDiff / tweezerToleranceEff)
                    strength = (levelSimilarity * 0.6) + (wickSimilarity * 0.4)

                    tBot := true
                    tBotBack := i
                    tBotStrength := strength

                    if shouldLog and logTweezerChecks
                        log.info(str.format("TWEEZER_BOT_FOUND | {0,date,yyyy-MM-dd HH:mm} | lookback={1} | levelDiff={2,number,#.####} | wickRatio={3,number,#.##} | relVol={4,number,#.##} | strength={5,number,#.##}",
                            time, i, levelDiff, tweezer_check_wicks ? (math.max(bodyLo[i] - low[i], bodyLo - low) / math.max(1e-6, math.min(bodyLo[i] - low[i], bodyLo - low))) : 0.0, relVol, strength))
                    break
                else
                    if shouldLog and logTweezerChecks and logFailedPatterns and i == 1
                        log.info(str.format("TWEEZER_BOT_FAIL | {0,date,yyyy-MM-dd HH:mm} | levelMatch={1} (diff={2,number,#.####} vs tol={3,number,#.####}) | bodyCheck={4} | wickCheck={5} | volCheck={6} (rv={7,number,#.##} vs min={8,number,#.##})",
                            time, levelMatch, levelDiff, tweezerToleranceEff, bodyCheck, wickCheck, volCheck, relVol, tweezer_min_volume))

else
    tTop := false
    tTopBack := na
    tTopStrength := na
    tBot := false
    tBotBack := na
    tBotStrength := na

//==================== Kangaroo Tail (Pin Bar) Detection ====================//
// Classic pin bar detection WITHOUT pivot dependency
// + Volume validation for real rejections
// + Rejection strength score (based on wick length)

var bool ktBear = false
var float ktBearStrength = na
var bool ktBull = false
var float ktBullStrength = na

if input_enable_kangaroo and inSession
    wickBodyRatioBear = uWick / math.max(1e-6, body)
    wickBodyRatioBull = lWick / math.max(1e-6, body)
    bodyFrac = body / math.max(1e-6, range_val)

    bodyPositionFromBottom = (bodyLo - low) / math.max(1e-6, range_val)
    bodyPositionFromTop = (high - bodyHi) / math.max(1e-6, range_val)

    volCheck = not volumeIsReal or relVol >= kt_min_volume

    // Bearish Kangaroo: Rejection from above
    bearBasic = wickBodyRatioBear >= ktWickBodyEff and bodyFrac <= ktBodyFracEff
    bearBodyPos = bodyPositionFromBottom <= kt_bodyPosition
    bearTrend = kt_requireTrend ? upTrend : true

    if bearBasic and bearBodyPos and bearTrend and volCheck
        wickPct = uWick / math.max(1e-6, range_val)
        strength = math.min(1.0, (wickPct - 0.5) / 0.4)

        ktBear := true
        ktBearStrength := strength

        if shouldLog and logKangarooChecks
            log.info(str.format("KANGAROO_BEAR_FOUND | {0,date,yyyy-MM-dd HH:mm} | wickBodyRatio={1,number,#.##} (min={2,number,#.##}) | bodyFrac={3,number,#.##} (max={4,number,#.##}) | bodyPos={5,number,#.##} (max={6,number,#.##}) | relVol={7,number,#.##} | strength={8,number,#.##}",
                time, wickBodyRatioBear, ktWickBodyEff, bodyFrac, ktBodyFracEff, bodyPositionFromBottom, kt_bodyPosition, relVol, strength))
    else
        ktBear := false
        ktBearStrength := na

        if shouldLog and logKangarooChecks and logFailedPatterns
            log.info(str.format("KANGAROO_BEAR_FAIL | {0,date,yyyy-MM-dd HH:mm} | bearBasic={1} (wickBody={2,number,#.##} vs min={3,number,#.##}, bodyFrac={4,number,#.##} vs max={5,number,#.##}) | bearBodyPos={6} (pos={7,number,#.##} vs max={8,number,#.##}) | bearTrend={9} | volCheck={10} (rv={11,number,#.##} vs min={12,number,#.##})",
                time, bearBasic, wickBodyRatioBear, ktWickBodyEff, bodyFrac, ktBodyFracEff, bearBodyPos, bodyPositionFromBottom, kt_bodyPosition, bearTrend, volCheck, relVol, kt_min_volume))

    // Bullish Kangaroo: Rejection from below
    bullBasic = wickBodyRatioBull >= ktWickBodyEff and bodyFrac <= ktBodyFracEff
    bullBodyPos = bodyPositionFromTop <= kt_bodyPosition
    bullTrend = kt_requireTrend ? downTrend : true

    if bullBasic and bullBodyPos and bullTrend and volCheck
        wickPct = lWick / math.max(1e-6, range_val)
        strength = math.min(1.0, (wickPct - 0.5) / 0.4)

        ktBull := true
        ktBullStrength := strength

        if shouldLog and logKangarooChecks
            log.info(str.format("KANGAROO_BULL_FOUND | {0,date,yyyy-MM-dd HH:mm} | wickBodyRatio={1,number,#.##} (min={2,number,#.##}) | bodyFrac={3,number,#.##} (max={4,number,#.##}) | bodyPos={5,number,#.##} (max={6,number,#.##}) | relVol={7,number,#.##} | strength={8,number,#.##}",
                time, wickBodyRatioBull, ktWickBodyEff, bodyFrac, ktBodyFracEff, bodyPositionFromTop, kt_bodyPosition, relVol, strength))
    else
        ktBull := false
        ktBullStrength := na

        if shouldLog and logKangarooChecks and logFailedPatterns
            log.info(str.format("KANGAROO_BULL_FAIL | {0,date,yyyy-MM-dd HH:mm} | bullBasic={1} (wickBody={2,number,#.##} vs min={3,number,#.##}, bodyFrac={4,number,#.##} vs max={5,number,#.##}) | bullBodyPos={6} (pos={7,number,#.##} vs max={8,number,#.##}) | bullTrend={9} | volCheck={10} (rv={11,number,#.##} vs min={12,number,#.##})",
                time, bullBasic, wickBodyRatioBull, ktWickBodyEff, bodyFrac, ktBodyFracEff, bullBodyPos, bodyPositionFromTop, kt_bodyPosition, bullTrend, volCheck, relVol, kt_min_volume))
else
    ktBear := false
    ktBearStrength := na
    ktBull := false
    ktBullStrength := na


// ============================================================================
// MODULE: zone_management
// ============================================================================

//==================== Zone Model ====================//

var box[]  boxes     = array.new<box>()
var float[] zTop     = array.new<float>()
var float[] zBot     = array.new<float>()
var bool[] zSupply   = array.new<bool>()
var int[]  zBornBar  = array.new<int>()
var bool[] zActive   = array.new<bool>()
var bool[] zRetested = array.new<bool>()
// Zone Scoring Arrays
var float[] zScore     = array.new<float>()  // Total score (0-100)
var float[] zRejection = array.new<float>()  // Rejection quality (0-1)
var float[] zExtreme   = array.new<float>()  // Extreme proximity (0-1)
var float[] zImpulse   = array.new<float>()  // Impulse before (0-1)
var int[]   zTouches   = array.new<int>()    // Touch counter
var int[]   zLastTouchBar = array.new<int>() // Debounce: last touch bar
var string[] zPatternType = array.new<string>()  // "T" for Tweezer, "K" for Kangaroo
var float[] zScoreMult  = array.new<float>()     // Pattern weight (× height factor at creation) kept for score updates
var float[] zStructPts  = array.new<float>()     // Structure bonus kept for score updates
var int[]   zPatternBar = array.new<int>()       // Last bar a pattern created/reinforced the zone
var string[] zZoneType = array.new<string>()     // "R" for Reversal, "C" for Continuation
var string[] zGrade = array.new<string>()        // "A", "B", "C" Ranking
var label[] zLabels = array.new<label>()         // Zone labels with tooltips

//==================== Zone Score Functions ====================//

// Lookback extremes for extreme proximity calculation
lookbackHigh = ta.highest(high, zoneLookback)
lookbackLow = ta.lowest(low, zoneLookback)
lookbackRange = lookbackHigh - lookbackLow

// Rejection Quality (0-1)
f_calc_rejection(_isSupply, _top, _bot, _patternType, _patternStrength) =>
    rng = high - low
    if rng <= 0
        0.5
    else
        float baseline = 0.0
        if _isSupply
            wickPct = uWick / rng
            closePos = 1.0 - (close - low) / rng
            baseline := math.min(1.0, (wickPct * 0.6) + (closePos * 0.4))
        else
            wickPct = lWick / rng
            closePos = (close - low) / rng
            baseline := math.min(1.0, (wickPct * 0.6) + (closePos * 0.4))

        if not na(_patternStrength) and _patternStrength > 0
            math.max(baseline, _patternStrength)
        else
            baseline

// Extreme Proximity (0-1)
f_calc_extreme(_isSupply, _top, _bot) =>
    if lookbackRange <= 0
        0.5
    else
        float globalScore = 0.0
        if _isSupply
            distToHigh = lookbackHigh - _top
            globalScore := 1.0 - math.min(1.0, distToHigh / (lookbackRange * 0.5))
        else
            distToLow = _bot - lookbackLow
            globalScore := 1.0 - math.min(1.0, distToLow / (lookbackRange * 0.5))

        float localScore = 0.0
        if _isSupply and not na(lastSwingHigh)
            distToSwing = math.abs(lastSwingHigh - _top)
            swingRange = atr * 3
            localScore := 1.0 - math.min(1.0, distToSwing / swingRange)
        else if not _isSupply and not na(lastSwingLow)
            distToSwing = math.abs(_bot - lastSwingLow)
            swingRange = atr * 3
            localScore := 1.0 - math.min(1.0, distToSwing / swingRange)

        math.max(globalScore, localScore)

// Impulse Before (0-1)
f_calc_impulse(_isSupply) =>
    atrNorm = atr * 2
    float moveInto = 0.0
    if _isSupply
        moveInto := math.max(0.0, nz(high[1] - high[6]))
    else
        moveInto := math.max(0.0, nz(low[6] - low[1]))
    math.min(1.0, moveInto / math.max(1e-6, atrNorm))

// Freshness (0-1)
f_calc_fresh(_bornBar) =>
    age = bar_index - _bornBar
    math.pow(0.5, age / float(zoneHalfLife))

// Cleanliness (0-1)
f_calc_clean(_touches) =>
    1.0 - math.min(1.0, float(_touches) / float(zoneMaxTouches))

// Total Score (0-100)
f_calc_zone_score(_rejection, _extreme, _impulse, _fresh, _clean) =>
    30.0 * _rejection + 25.0 * _extreme + 20.0 * _impulse + 15.0 * _fresh + 10.0 * _clean

// Alpha for score-based visualization
f_get_zone_alpha(_score) =>
    baseAlpha = 85
    scoreBonus = math.min(30.0, _score * 0.3)
    int(baseAlpha - scoreBonus)

// Zone Type: Reversal vs Continuation
f_classify_zone_type(_isSupply, _extreme, _impulse) =>
    isReversal = _extreme >= 0.6 and _impulse >= 0.5
    isReversal ? "R" : "C"

// Calculate grade: A/B/C
f_calc_grade(_score, _touches) =>
    if _score >= gradeAMin and _touches <= 1
        "A"
    else if _score >= gradeBMin
        "B"
    else
        "C"

// Build tooltip for zone
f_build_tooltip(_isSupply, _zoneType, _patternType, _grade, _score, _rejection, _extreme, _impulse, _fresh, _clean, _touches, _age, _top, _bot) =>
    zoneDir = _isSupply ? "Supply (S)" : "Demand (D)"
    zoneTypeStr = _zoneType == "R" ? "Reversal" : "Continuation"
    patternStr = _patternType == "K" ? "Kangaroo (K)" : "Tweezer (T)"
    gradeStr = _grade == "A" ? "A = Premium" : _grade == "B" ? "B = Good" : "C = Context"

    header = zoneDir + " Zone\n"
    header += "Type: " + zoneTypeStr + "\n"
    header += "Pattern: " + patternStr + "\n"
    header += "Grade: " + gradeStr + "\n"
    header += "Score: " + str.tostring(_score, "#.0") + "/100\n"
    header += "─────────────────\n"

    breakdown = "SCORE BREAKDOWN\n"
    breakdown += "Rejection: " + str.tostring(_rejection, "#.00") + "\n"
    breakdown += "Extreme: " + str.tostring(_extreme, "#.00") + "\n"
    breakdown += "Impulse: " + str.tostring(_impulse, "#.00") + "\n"
    breakdown += "Fresh: " + str.tostring(_fresh, "#.00") + " (" + str.tostring(_age) + " bars)\n"
    breakdown += "Clean: " + str.tostring(_clean, "#.00") + " (" + str.tostring(_touches) + " touches)\n"
    breakdown += "─────────────────\n"

    zoneHeight = math.abs(_top - _bot)
    heightATR = zoneHeight / atr
    zoneInfo = "ZONE INFO\n"
    zoneInfo += "Height: " + str.tostring(heightATR, "#.00") + " ATR\n"
    zoneInfo += "Top: " + str.tostring(_top, "#.00") + "\n"
    zoneInfo += "Bottom: " + str.tostring(_bot, "#.00") + "\n"
    zoneInfo += "─────────────────\n"

    tradingHint = "TRADING\n"
    if _grade == "A"
        tradingHint += "★ Best: First retest\n"
    else if _grade == "B"
        tradingHint += "◆ Good: Wait for confirmation\n"
    else
        tradingHint += "○ Context only\n"

    if _isSupply
        tradingHint += "Invalidation: Close > " + str.tostring(_top, "#.00") + "\n"
    else
        tradingHint += "Invalidation: Close < " + str.tostring(_bot, "#.00") + "\n"

    legend = "─────────────────\n"
    legend += "LABEL: Grade Dir Pattern Score\n"
    legend += "A/B/C = Quality Grade\n"
    legend += "S/D = Supply/Demand\n"
    legend += "K/T = Kangaroo/Tweezer"

    header + breakdown + zoneInfo + tradingHint + legend

// Label text for zone (short)
f_build_label_text(_grade, _isSupply, _patternType, _score) =>
    dir = _isSupply ? "S" : "D"
    _grade + " " + dir + " " + _patternType + " " + str.tostring(_score, "#")

// Label color based on grade
f_get_label_color(_grade, _isSupply) =>
    if _grade == "A"
        _isSupply ? color.new(#E53935, 0) : color.new(#00897B, 0)
    else if _grade == "B"
        _isSupply ? color.new(#EF5350, 15) : color.new(#26A69A, 15)
    else
        _isSupply ? color.new(#E57373, 30) : color.new(#4DB6AC, 30)

//==================== Zone Helper Functions ====================//

f_reset_daily() =>
    if array.size(boxes) > 0
        for i = 0 to array.size(boxes) - 1
            if array.get(zActive, i)
                array.set(zActive, i, false)
                b = array.get(boxes, i)
                box.set_border_color(b, colDeadBrd)

if resetZonesDaily and newDay
    f_reset_daily()

// Cluster-Limit: No new zone if same direction exists nearby
f_is_too_close(_top, _bot, _isSupply) =>
    tooClose = false
    midNew = (_top + _bot) * 0.5
    if array.size(boxes) > 0
        for i = 0 to array.size(boxes) - 1
            if array.get(zActive, i) and array.get(zSupply, i) == _isSupply
                midExist = (array.get(zTop, i) + array.get(zBot, i)) * 0.5
                if math.abs(midNew - midExist) < atr * 0.8
                    tooClose := true
                    break
    tooClose

// Find overlapping zone - returns index or -1
f_find_overlapping_zone(_top, _bot, _isSupply) =>
    overlapIdx = -1
    if array.size(boxes) > 0
        for i = 0 to array.size(boxes) - 1
            if array.get(zActive, i) and array.get(zSupply, i) == _isSupply
                existTop = array.get(zTop, i)
                existBot = array.get(zBot, i)
                hasOverlap = _bot < existTop and _top > existBot
                if hasOverlap
                    overlapTop = math.min(_top, existTop)
                    overlapBot = math.max(_bot, existBot)
                    overlapHeight = overlapTop - overlapBot
                    newHeight = _top - _bot
                    existHeight = existTop - existBot
                    overlapPct = overlapHeight / math.min(newHeight, existHeight)
                    if overlapPct >= zoneOverlapPct
                        overlapIdx := i
                        break
    overlapIdx

// Merge new zone into existing
f_merge_zone(_idx, _newTop, _newBot, _newRejection, _newExtreme, _newImpulse, _patternType) =>
    existTop = array.get(zTop, _idx)
    existBot = array.get(zBot, _idx)
    mergedTop = math.max(existTop, _newTop)
    mergedBot = math.min(existBot, _newBot)

    array.set(zTop, _idx, mergedTop)
    array.set(zBot, _idx, mergedBot)

    existRejection = array.get(zRejection, _idx)
    existExtreme = array.get(zExtreme, _idx)
    existImpulse = array.get(zImpulse, _idx)

    array.set(zRejection, _idx, math.max(existRejection, _newRejection))
    array.set(zExtreme, _idx, math.max(existExtreme, _newExtreme))
    array.set(zImpulse, _idx, math.max(existImpulse, _newImpulse))

    bornBar = array.get(zBornBar, _idx)
    touches = array.get(zTouches, _idx)
    isSup = array.get(zSupply, _idx)

    structScore = 0.0
    if useStructContext
        if isSup and (bearishStructure or nearSwingHigh)
            structScore := structBonus
        else if not isSup and (bullishStructure or nearSwingLow)
            structScore := structBonus

    patternMult = _patternType == "K" ? wKangaroo : wTweezer

    baseScore = f_calc_zone_score( array.get(zRejection, _idx), array.get(zExtreme, _idx), array.get(zImpulse, _idx), f_calc_fresh(bornBar), f_calc_clean(touches) )
    newScore = baseScore * patternMult + structScore
    array.set(zScore, _idx, newScore)
    array.set(zScoreMult, _idx, patternMult)
    array.set(zStructPts, _idx, structScore)
    array.set(zPatternBar, _idx, bar_index)

    b = array.get(boxes, _idx)
    box.set_top(b, mergedTop)
    box.set_bottom(b, mergedBot)

    alpha = f_get_zone_alpha(newScore)
    newBgColor = isSup ? color.new(color.from_gradient(newScore, 0, 100, #FF4444, #FF0000), alpha) : color.new(color.from_gradient(newScore, 0, 100, #44FF88, #00FF00), alpha)
    box.set_bgcolor(b, newBgColor)

//==================== Zone Creation ====================//

f_new_zone(_isSupply, _top, _bot, _patternType, _patternStrength) =>
    if f_is_too_close(_top, _bot, _isSupply)
        if shouldLog and logZoneCreation
            log.info(str.format("ZONE_REJECTED_CLUSTER | {0,date,yyyy-MM-dd HH:mm} | type={1} | pattern={2} | top={3,number,#.##} | bot={4,number,#.##} | reason=too_close_to_existing",
                time, _isSupply ? "SUPPLY" : "DEMAND", _patternType, _top, _bot))
        false
    else
        rejection = f_calc_rejection(_isSupply, _top, _bot, _patternType, _patternStrength)
        extreme = f_calc_extreme(_isSupply, _top, _bot)
        impulse = f_calc_impulse(_isSupply)

        overlapIdx = f_find_overlapping_zone(_top, _bot, _isSupply)

        if overlapIdx >= 0
            if zoneMergeEnabled
                f_merge_zone(overlapIdx, _top, _bot, rejection, extreme, impulse, _patternType)
                if shouldLog and logZoneCreation
                    log.info(str.format("ZONE_MERGED | {0,date,yyyy-MM-dd HH:mm} | type={1} | pattern={2} | top={3,number,#.##} | bot={4,number,#.##} | mergedWith=idx{5}",
                        time, _isSupply ? "SUPPLY" : "DEMAND", _patternType, _top, _bot, overlapIdx))
                true
            else
                if shouldLog and logZoneCreation
                    log.info(str.format("ZONE_REJECTED_OVERLAP | {0,date,yyyy-MM-dd HH:mm} | type={1} | pattern={2} | top={3,number,#.##} | bot={4,number,#.##} | reason=overlap_merge_disabled",
                        time, _isSupply ? "SUPPLY" : "DEMAND", _patternType, _top, _bot))
                false
        else
            structScore = 0.0
            if useStructContext
                if _isSupply and (bearishStructure or nearSwingHigh)
                    structScore := structBonus
                else if not _isSupply and (bullishStructure or nearSwingLow)
                    structScore := structBonus

            patternMult = _patternType == "K" ? wKangaroo : wTweezer

            zoneH = math.abs(_top - _bot)
            hPenalty = math.min(1.0, zoneH / (atr * 1.2))
            hScore = 1.0 - hPenalty

            baseScore = f_calc_zone_score(rejection, extreme, impulse, 1.0, 1.0)
            baseScore := baseScore * (0.7 + 0.3 * hScore)
            initScore = baseScore * patternMult + structScore

            if useZoneScore and initScore < zoneScoreMin
                if shouldLog and logZoneCreation
                    log.info(str.format("ZONE_REJECTED_SCORE | {0,date,yyyy-MM-dd HH:mm} | type={1} | pattern={2} | top={3,number,#.##} | bot={4,number,#.##} | score={5,number,#.##} | minScore={6,number,#.##} | rejection={7,number,#.##} | extreme={8,number,#.##} | impulse={9,number,#.##}",
                        time, _isSupply ? "SUPPLY" : "DEMAND", _patternType, _top, _bot, initScore, zoneScoreMin, rejection, extreme, impulse))
                false
            else
                borderStyle = zoneStyle == "Dashed" ? line.style_dashed : zoneStyle == "Dotted" ? line.style_dotted : line.style_solid
                borderWidth = 2

                alpha = useZoneScore ? f_get_zone_alpha(initScore) : 85
                bgColor = _isSupply ? color.new(color.from_gradient(initScore, 0, 100, #FF4444, #FF0000), alpha) : color.new(color.from_gradient(initScore, 0, 100, #44FF88, #00FF00), alpha)

                b = box.new(bar_index, _top, bar_index+zoneExtendBars, _bot, border_color=colActiveBrd, border_width=borderWidth, border_style=borderStyle, bgcolor=bgColor)
                array.push(boxes, b)
                array.push(zTop, _top)
                array.push(zBot, _bot)
                array.push(zSupply, _isSupply)
                array.push(zBornBar, bar_index)
                array.push(zActive, true)
                array.push(zRetested, false)
                array.push(zScore, initScore)
                array.push(zRejection, rejection)
                array.push(zExtreme, extreme)
                array.push(zImpulse, impulse)
                array.push(zTouches, 0)
                array.push(zLastTouchBar, bar_index)
                array.push(zPatternType, _patternType)
                array.push(zScoreMult, patternMult * (0.7 + 0.3 * hScore))
                array.push(zStructPts, structScore)
                array.push(zPatternBar, bar_index)

                zType = f_classify_zone_type(_isSupply, extreme, impulse)
                adjustedScore = zType == "R" ? initScore + reversalBonus : initScore - continuationPenalty
                adjustedScore := math.max(0, math.min(100, adjustedScore))
                array.set(zScore, array.size(zScore) - 1, adjustedScore)

                grade = f_calc_grade(adjustedScore, 0)
                array.push(zZoneType, zType)
                array.push(zGrade, grade)

                if shouldLog and logZoneCreation
                    log.info(str.format("ZONE_CREATED | {0,date,yyyy-MM-dd HH:mm} | type={1} | pattern={2} | grade={3} | zoneType={4} | score={5,number,#.##} | top={6,number,#.##} | bot={7,number,#.##} | rejection={8,number,#.##} | extreme={9,number,#.##} | impulse={10,number,#.##} | structBonus={11,number,#.##}",
                        time, _isSupply ? "SUPPLY" : "DEMAND", _patternType, grade, zType, adjustedScore, _top, _bot, rejection, extreme, impulse, structScore))

                if showZoneLabels and (not showOnlyAB or grade != "C")
                    lblText = f_build_label_text(grade, _isSupply, _patternType, adjustedScore)
                    lblY = _isSupply ? _top : _bot

                    lblStyle = _isSupply ? label.style_label_down : label.style_label_up
                    lblColor = f_get_label_color(grade, _isSupply)

                    tooltipText = showTooltips ? f_build_tooltip(_isSupply, zType, _patternType, grade, adjustedScore, rejection, extreme, impulse, 1.0, 1.0, 0, 0, _top, _bot) : ""

                    lbl = label.new(bar_index, lblY, lblText, style=lblStyle, color=lblColor, textcolor=color.white, size=size.small, tooltip=tooltipText)
                    array.push(zLabels, lbl)
                else
                    array.push(zLabels, na)

                true

//==================== Zone Update ====================//

f_update_zones() =>
    if array.size(boxes) > 0
        for i = 0 to array.size(boxes) - 1
            if array.get(zActive, i)
                b = array.get(boxes, i)
                box.set_right(b, bar_index+1)
                if bar_index - array.get(zBornBar, i) > zoneTTLAdj
                    array.set(zActive, i, false)
                    box.set_border_color(b, colDeadBrd)
                isSup = array.get(zSupply, i)
                top_  = array.get(zTop, i)
                bot_  = array.get(zBot, i)
                invalid = invalidateOnClose and ((isSup and close > top_) or (not isSup and close < bot_))
                if invalid
                    array.set(zActive, i, false)
                    box.set_border_color(b, colDeadBrd)

                if useZoneScore and array.get(zActive, i)
                    wickInZone = (high >= bot_ and low <= top_)
                    closeOutside = isSup ? (close < bot_) : (close > top_)
                    closeInside = (close >= bot_ and close <= top_)
                    potentialTouch = wickInZone and (closeOutside or closeInside)
                    lastTouchBar = array.get(zLastTouchBar, i)
                    debounceOK = (bar_index - lastTouchBar) >= 3

                    if potentialTouch and debounceOK
                        currTouches = array.get(zTouches, i)
                        array.set(zTouches, i, currTouches + 1)
                        array.set(zLastTouchBar, i, bar_index)

                    bornBar = array.get(zBornBar, i)
                    touches = array.get(zTouches, i)
                    rejection = array.get(zRejection, i)
                    extreme = array.get(zExtreme, i)
                    impulse = array.get(zImpulse, i)
                    fresh = f_calc_fresh(bornBar)
                    clean = f_calc_clean(touches)
                    // Same composition as creation/merge: pattern weight (and the
                    // creation height factor) and the structure bonus are kept,
                    // only freshness and cleanliness change over time.
                    baseNewScore = f_calc_zone_score(rejection, extreme, impulse, fresh, clean) * array.get(zScoreMult, i) + array.get(zStructPts, i)

                    zType = array.get(zZoneType, i)
                    newScore = zType == "R" ? baseNewScore + reversalBonus : baseNewScore - continuationPenalty
                    newScore := math.max(0, math.min(100, newScore))
                    array.set(zScore, i, newScore)

                    newGrade = f_calc_grade(newScore, touches)
                    oldGrade = array.get(zGrade, i)
                    array.set(zGrade, i, newGrade)

                    alpha = f_get_zone_alpha(newScore)
                    newBgColor = isSup ? color.new(color.from_gradient(newScore, 0, 100, #FF4444, #FF0000), alpha) : color.new(color.from_gradient(newScore, 0, 100, #44FF88, #00FF00), alpha)
                    box.set_bgcolor(b, newBgColor)

                    if showZoneLabels and oldGrade != newGrade
                        lbl = array.get(zLabels, i)
                        if not na(lbl)
                            patternType = array.get(zPatternType, i)
                            age = bar_index - bornBar
                            lblText = f_build_label_text(newGrade, isSup, patternType, newScore)
                            label.set_text(lbl, lblText)
                            label.set_color(lbl, f_get_label_color(newGrade, isSup))

                            if showTooltips
                                tooltipText = f_build_tooltip(isSup, zType, patternType, newGrade, newScore, rejection, extreme, impulse, fresh, clean, touches, age, top_, bot_)
                                label.set_tooltip(lbl, tooltipText)

// Top-N Filtering
f_apply_topn_filter() =>
    if useZoneScore and array.size(boxes) > zoneScoreTopN
        int[] activeIndices = array.new<int>()
        float[] activeScores = array.new<float>()

        for i = 0 to array.size(boxes) - 1
            if array.get(zActive, i)
                array.push(activeIndices, i)
                array.push(activeScores, array.get(zScore, i))

        if array.size(activeIndices) > zoneScoreTopN
            for pass = 0 to zoneScoreTopN - 1
                maxIdx = pass
                for j = pass + 1 to array.size(activeScores) - 1
                    if array.get(activeScores, j) > array.get(activeScores, maxIdx)
                        maxIdx := j
                if maxIdx != pass
                    tempScore = array.get(activeScores, pass)
                    tempIdx = array.get(activeIndices, pass)
                    array.set(activeScores, pass, array.get(activeScores, maxIdx))
                    array.set(activeIndices, pass, array.get(activeIndices, maxIdx))
                    array.set(activeScores, maxIdx, tempScore)
                    array.set(activeIndices, maxIdx, tempIdx)

            for i = zoneScoreTopN to array.size(activeIndices) - 1
                idx = array.get(activeIndices, i)
                b = array.get(boxes, idx)
                box.set_border_color(b, colDeadBrd)
                isSup = array.get(zSupply, idx)
                dimColor = isSup ? color.new(#FF4444, 95) : color.new(#44FF88, 95)
                box.set_bgcolor(b, dimColor)

            for i = 0 to zoneScoreTopN - 1
                idx = array.get(activeIndices, i)
                b = array.get(boxes, idx)
                box.set_border_color(b, colActiveBrd)

//==================== Zone Creation from Patterns ====================//

supplyStructOK = not requireAfterSwing or nearSwingHigh
demandStructOK = not requireAfterSwing or nearSwingLow

var bool tTopZoneCreated = false
var bool tBotZoneCreated = false
var bool ktBearZoneCreated = false
var bool ktBullZoneCreated = false

tTopZoneCreated := false
tBotZoneCreated := false
ktBearZoneCreated := false
ktBullZoneCreated := false

if tTop and not na(close) and not na(high) and supplyStructOK
    other = tTopBack
    zT = math.max(high, high[other])
    zB = math.max(bodyHi, bodyHi[other])
    tTopZoneCreated := f_new_zone(true, zT, zB, "T", tTopStrength)

if tBot and not na(close) and not na(low) and demandStructOK
    other = tBotBack
    zT = math.min(bodyLo, bodyLo[other])
    zB = math.min(low, low[other])
    tBotZoneCreated := f_new_zone(false, zT, zB, "T", tBotStrength)

if ktBear and not na(close) and not na(high) and supplyStructOK
    zT = high
    zB = math.max(open, close)
    ktBearZoneCreated := f_new_zone(true, zT, zB, "K", ktBearStrength)

if ktBull and not na(close) and not na(low) and demandStructOK
    zT = math.min(open, close)
    zB = low
    ktBullZoneCreated := f_new_zone(false, zT, zB, "K", ktBullStrength)

f_update_zones()
f_apply_topn_filter()


// ============================================================================
// MODULE: trend_gate
// ============================================================================

//==================== Trend Gate Module ====================//
// Supertrend, MOST, EMA Gate, Confluence Score

//==================== Supertrend & MOST Functions ====================//

// Unified Supertrend direction function
f_st_dir() =>
    [st_line, st_dir] = ta.supertrend(stFactor, stAtrLen)
    close >= st_line ? 1 : -1

// MOST (Moving Average Support/Resistance)
f_most_dir(len, mult) =>
    a = ta.atr(len)
    var float longStop = na
    var float shortStop = na
    var int dir = 1
    longStop := na(longStop) ? close - mult*a : math.max(longStop, close - mult*a)
    shortStop := na(shortStop) ? close + mult*a : math.min(shortStop, close + mult*a)
    if dir == 1 and close < longStop
        dir := -1
        shortStop := close + mult*a
    else if dir == -1 and close > shortStop
        dir := 1
        longStop := close - mult*a
    dir

//==================== Score Components ====================//

// Supertrend directions (for scoring)
stDirLocal = useST ? (stScope == "Local" or stScope == "Both" ? f_st_dir() : 0) : 0
stDirHTF = useST and (stScope == "HTF" or stScope == "Both") ? request.security(syminfo.tickerid, stTF, f_st_dir(), lookahead = barmerge.lookahead_off) : 0
mostDirLocal = useMOST ? (mostScope == "Local" or mostScope == "Both" ? f_most_dir(mostLen, mostMult) : 0) : 0
mostDirHTF = useMOST and (mostScope == "HTF" or mostScope == "Both") ? request.security(syminfo.tickerid, mostTF, f_most_dir(mostLen, mostMult), lookahead = barmerge.lookahead_off) : 0

//==================== Gate Components ====================//

gateUseEMA  = trendFilterType == "EMA"
gateUseST   = trendFilterType == "Supertrend"
gateUseMOST = trendFilterType == "MOST"

// Supertrend Gate
stGateLocal = gateUseST and (gateSTscope == "Local" or gateSTscope == "Both") ? f_st_dir() : 0
stGateHTF = gateUseST and (gateSTscope == "HTF" or gateSTscope == "Both") ? request.security(syminfo.tickerid, gateSTtf, f_st_dir(), lookahead = barmerge.lookahead_off) : 0

// MOST Gate
mostGateLocal = gateUseMOST and (gateMOSTscope == "Local" or gateMOSTscope == "Both") ? f_most_dir(mostLen, mostMult) : 0
mostGateHTF = gateUseMOST and (gateMOSTscope == "HTF" or gateMOSTscope == "Both") ? request.security(syminfo.tickerid, gateMOSTtf, f_most_dir(mostLen, mostMult), lookahead = barmerge.lookahead_off) : 0

// Gate direction flags
stGateUp = gateUseST ? (gateSTscope == "Local" ? stGateLocal > 0 : gateSTscope == "HTF" ? stGateHTF > 0 : (stGateLocal > 0 and stGateHTF > 0)) : true
stGateDn = gateUseST ? (gateSTscope == "Local" ? stGateLocal < 0 : gateSTscope == "HTF" ? stGateHTF < 0 : (stGateLocal < 0 and stGateHTF < 0)) : true
mostGateUp = gateUseMOST ? (gateMOSTscope == "Local" ? mostGateLocal > 0 : gateMOSTscope == "HTF" ? mostGateHTF > 0 : (mostGateLocal > 0 and mostGateHTF > 0)) : true
mostGateDn = gateUseMOST ? (gateMOSTscope == "Local" ? mostGateLocal < 0 : gateMOSTscope == "HTF" ? mostGateHTF < 0 : (mostGateLocal < 0 and mostGateHTF < 0)) : true

// Final trend gate (combines EMA, ST, MOST)
trendUp = (not gateUseEMA or emaGateUp) and stGateUp and mostGateUp
trendDn = (not gateUseEMA or emaGateDn) and stGateDn and mostGateDn

//==================== Confluence Score (weighted + normalized) ====================//

float baseScore = 0.0
float baseMax = 0.0

// Regime component
if useRegime
    baseScore += (trendUp or trendDn) ? wRegime : 0
    baseMax += wRegime

// HTF Stack component
f_tfUp(tf, len) => request.security(syminfo.tickerid, tf, close > ta.ema(close, len), lookahead = barmerge.lookahead_off)
s1 = f_tfUp(stackTF1, emaLen)
s2 = f_tfUp(stackTF2, emaLen)
s3 = f_tfUp(stackTF3, emaLen)
stackPts = (s1 ? 1 : 0) + (s2 ? 1 : 0) + (s3 ? 1 : 0)
if useStack
    baseScore += wStack * stackPts / 3.0
    baseMax += wStack

// Structure component (added in signal section)
if useStruct
    baseMax += wStruct

// Relative Volume component
// Dropped from both baseScore and baseMax (not just scored 0) when volume
// isn't real, so this behaves the same as any other disabled component
// instead of systematically lowering the achievable score on CFDs.
rv = volume / math.max(1.0, ta.sma(volume, 20))
if useRelVol and volumeIsReal
    baseScore += rv >= effRvSpike ? wRelVol : 0
    baseMax += wRelVol

// Choppiness component
if useChopAdv
    baseScore += chop <= effChopSoft ? wChop : 0
    baseMax += wChop

// Zone Thin component (added in signal section)
if useZoneThin
    baseMax += wZoneThin

// Helper for zone thin scoring
zoneThinPts(_top, _bot) => (math.abs(_top - _bot) / atr) <= effZoneThinATR ? wZoneThin : 0

// Supertrend/MOST in score
if useST
    baseMax += wSupertrend
if useMOST
    baseMax += wMOST

// Pattern Strength component
float patternStrength = na(tTopStrength) ? (na(tBotStrength) ? (na(ktBearStrength) ? (na(ktBullStrength) ? na : ktBullStrength) : ktBearStrength) : tBotStrength) : tTopStrength
if not na(patternStrength)
    baseScore += patternStrength * wPattern
    baseMax += wPattern

//==================== Retest Signals ====================//

var bool buySig = false
var bool sellSig = false
var float buyNorm = na
var float sellNorm = na

buySig := false
sellSig := false

hardBlocked = chop >= effChopHard
softGate = chop < effChopSoft or (trendUp or trendDn)

//==================== Signal Generation ====================//

// Hoisted out of the zone loop — ta.* calls inside a loop execute multiple times
// per bar and corrupt their rolling series
float lowestLow5Prev   = ta.lowest(low, 5)[1]
float highestHigh5Prev = ta.highest(high, 5)[1]

if array.size(boxes) > 0
    for i = 0 to array.size(boxes) - 1
        if array.get(zActive, i)
            isSup = array.get(zSupply, i)
            top_ = array.get(zTop, i)
            bot_ = array.get(zBot, i)
            wasRet = array.get(zRetested, i)
            // A zone created or reinforced by this bar's own pattern is not
            // being retested — the pattern bar always lies inside it.
            patternBarItself = array.get(zPatternBar, i) == bar_index
            inside = high >= bot_ and low <= top_ and not patternBarItself
            trendCheckBuy = signalRequireTrend ? trendUp : true
            trendCheckSell = signalRequireTrend ? trendDn : true
            if inside and (not wasRet or not retestOnceOnly) and not hardBlocked and softGate and (inSession or not signalOnlyInSess)
                // Buy signal (demand zone)
                if not isSup and trendCheckBuy and (not useStruct or (low > lowestLow5Prev))
                    stPtsBuy = useST ? ((stScope == "Local" ? stDirLocal > 0 : stScope == "HTF" ? stDirHTF > 0 : (stDirLocal > 0 and stDirHTF > 0)) ? wSupertrend : 0) : 0
                    mostPtsBuy = useMOST ? ((mostScope == "Local" ? mostDirLocal > 0 : mostScope == "HTF" ? mostDirHTF > 0 : (mostDirLocal > 0 and mostDirHTF > 0)) ? wMOST : 0) : 0
                    sideAdd = (useStruct ? wStruct : 0) + (useZoneThin ? zoneThinPts(top_, bot_) : 0) + stPtsBuy + mostPtsBuy
                    sc = baseScore + sideAdd
                    scMax = baseMax
                    norm = scMax > 0 ? 10.0 * sc / scMax : na
                    if sc >= minScoreBuy
                        buySig := true
                        buyNorm := norm
                        array.set(zRetested, i, true)
                // Sell signal (supply zone)
                if isSup and trendCheckSell and (not useStruct or (high < highestHigh5Prev))
                    stPtsSell = useST ? ((stScope == "Local" ? stDirLocal < 0 : stScope == "HTF" ? stDirHTF < 0 : (stDirLocal < 0 and stDirHTF < 0)) ? wSupertrend : 0) : 0
                    mostPtsSell = useMOST ? ((mostScope == "Local" ? mostDirLocal < 0 : mostScope == "HTF" ? mostDirHTF < 0 : (mostDirLocal < 0 and mostDirHTF < 0)) ? wMOST : 0) : 0
                    sideAdd = (useStruct ? wStruct : 0) + (useZoneThin ? zoneThinPts(top_, bot_) : 0) + stPtsSell + mostPtsSell
                    sc = baseScore + sideAdd
                    scMax = baseMax
                    norm = scMax > 0 ? 10.0 * sc / scMax : na
                    if sc >= minScoreSell
                        sellSig := true
                        sellNorm := norm
                        array.set(zRetested, i, true)

// Binary filters
bool binBuyFilter = (baseScore >= minScoreBuy)
bool binSellFilter = (baseScore >= minScoreSell)


// ============================================================================
// MODULE: visualization
// ============================================================================

//==================== Visualization Module ====================//
// Pattern Labels, HTF Panel, Metrics Panel, Alerts, Exports

//==================== Pattern Labels ====================//

// Pattern label colors based on score strength
getPatternColor(isBullish) =>
    score = baseScore
    isStrong = not na(score) and score >= labelStrongTh
    isMid = not na(score) and score >= labelMidTh
    if isBullish
        isStrong ? color.new(#006400, 0) : isMid ? color.new(#228B22, 0) : color.new(#32CD32, 0)
    else
        isStrong ? color.new(color.red, 0) : isMid ? color.new(color.red, 30) : color.new(color.red, 60)

// Pattern labels only when no zone was created (plotshape for simple positioning)
plotshape(tTop and not tTopZoneCreated, title="Tweezer Top (no zone)", style=shape.labeldown, text="T", location=location.abovebar, color=getPatternColor(false), textcolor=color.white, size=size.tiny)
plotshape(tBot and not tBotZoneCreated, title="Tweezer Bottom (no zone)", style=shape.labelup, text="T", location=location.belowbar, color=getPatternColor(true), textcolor=color.white, size=size.tiny)
plotshape(ktBear and not ktBearZoneCreated, title="Kangaroo Bear (no zone)", style=shape.labeldown, text="K", location=location.abovebar, color=getPatternColor(false), textcolor=color.white, size=size.tiny)
plotshape(ktBull and not ktBullZoneCreated, title="Kangaroo Bull (no zone)", style=shape.labelup, text="K", location=location.belowbar, color=getPatternColor(true), textcolor=color.white, size=size.tiny)

//==================== Hidden Exports ====================//

// Hidden exports for libs / request.security consumers
plot(buySig ? 1 : 0, title="buySignal", display=display.none)
plot(sellSig ? 1 : 0, title="sellSignal", display=display.none)
plot(trendUp ? 1 : 0, title="regimeUp", display=display.none)
plot(trendDn ? 1 : 0, title="regimeDown", display=display.none)
plot(chop, title="choppiness", display=display.none)
plot(baseScore, title="confluenceScoreRaw", display=display.none)
plot(buyNorm, title="buyScoreNorm0_10", display=display.none)
plot(sellNorm, title="sellScoreNorm0_10", display=display.none)
plot(binBuyFilter ? 1 : 0, title="binaryBuyFilter", display=display.none)
plot(binSellFilter ? 1 : 0, title="binarySellFilter", display=display.none)

//==================== Zone Score Helper ====================//

// Get top zone scores for panel display
f_get_top_scores() =>
    float[] topScores = array.new<float>(3, 0.0)
    if useZoneScore and array.size(zScore) > 0
        // Find top 3 scores from active zones
        float[] scores = array.new<float>()
        for i = 0 to array.size(boxes) - 1
            if array.get(zActive, i)
                array.push(scores, array.get(zScore, i))
        // Sort descending
        if array.size(scores) > 0
            array.sort(scores, order.descending)
            for i = 0 to math.min(2, array.size(scores) - 1)
                array.set(topScores, i, array.get(scores, i))
    topScores

// Zone Score exports
topScoresExport = f_get_top_scores()
plot(array.get(topScoresExport, 0), title="topZoneScore1", display=display.none)
plot(array.get(topScoresExport, 1), title="topZoneScore2", display=display.none)
plot(array.get(topScoresExport, 2), title="topZoneScore3", display=display.none)

//==================== Alerts ====================//

bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed

alertcondition(alertOk and (buySig), "Buy (zone retest)", "TKZ · BUY ZONE RETEST LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (sellSig), "Sell (zone retest)", "TKZ · SELL ZONE RETEST SHORT · {{ticker}} {{interval}}")

//==================== HTF Stack Panel ====================//

// Light-theme table style (works on both TradingView themes)
color tc = color.new(color.gray, 20)   // standard text color
color hd = color.new(color.gray, 90)   // header row background

var table stack = na
if useHTFPanel and na(stack)
    stack := table.new(position.top_right, 2, 14,
         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)

f_dot(ok) => ok ? "●" : "○"
f_col(ok) => ok ? #00c853 : #d50000

if useHTFPanel and barstate.islast
    table.cell(stack, 0, 0, "HTF Stack", text_color=tc, text_size=size.small, bgcolor=hd)
    table.cell(stack, 1, 0, "", text_color=tc, text_size=size.small, bgcolor=hd)
    table.cell(stack, 0, 1, str.format("{0} {1}", timeframe.period, f_dot(close > ema)), text_color=f_col(close > ema), text_size=size.small)
    table.cell(stack, 0, 2, str.format("{0} {1}", stackTF1, f_dot(s1)), text_color=f_col(s1), text_size=size.small)
    table.cell(stack, 0, 3, str.format("{0} {1}", stackTF2, f_dot(s2)), text_color=f_col(s2), text_size=size.small)
    table.cell(stack, 0, 4, str.format("{0} {1}", stackTF3, f_dot(s3)), text_color=f_col(s3), text_size=size.small)
    table.cell(stack, 1, 1, str.format("CHOP: {0,number,#0.0} (soft/hard {1}/{2})", chop, effChopSoft, effChopHard), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 2, str.format("ATR%: {0,number,#0.2}%", atrPct * 100), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 3, str.format("Sess: {0}", inSession ? (useRTH ? "RTH" : "ETH") : "OFF"), text_color=inSession ? #00c853 : #d50000, text_size=size.small)
    table.cell(stack, 1, 4, str.format("RV≥ {0,number,#0.00}×  Thin<{1,number,#0.00}×ATR", effRvSpike, effZoneThinATR), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 5, str.format("Raw: {0,number,#0.00}", baseScore), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 6, str.format("BUY N: {0}", na(buyNorm) ? "—" : str.format("{0,number,#0.0}", buyNorm)), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 7, str.format("SELL N: {0}", na(sellNorm) ? "—" : str.format("{0,number,#0.0}", sellNorm)), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 8, str.format("Gate: EMA={0} ST={1} MOST={2}", gateUseEMA ? "✔" : "✖", gateUseST ? "✔" : "✖", gateUseMOST ? "✔" : "✖"), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 9, str.format("Min: B{0} / S{1}", minScoreBuy, minScoreSell), text_color=tc, text_size=size.small)
    table.cell(stack, 1, 10, str.format("Filters: B={0} S={1}", binBuyFilter ? "ON" : "OFF", binSellFilter ? "ON" : "OFF"), text_color=(binBuyFilter or binSellFilter) ? #00c853 : #d50000, text_size=size.small)
    // Zone Scores
    if useZoneScore
        topScores = f_get_top_scores()
        activeCount = 0
        if array.size(boxes) > 0
            for i = 0 to array.size(boxes) - 1
                if array.get(zActive, i)
                    activeCount += 1
        table.cell(stack, 0, 11, "Zone Scores", text_color=tc, text_size=size.small, bgcolor=hd)
        table.cell(stack, 1, 11, str.format("Active: {0} | TopN: {1}", activeCount, zoneScoreTopN), text_color=tc, text_size=size.small)
        table.cell(stack, 0, 12, str.format("Top 3: {0,number,#0.0}", array.get(topScores, 0)), text_color=#ff6f00, text_size=size.small)
        table.cell(stack, 1, 12, str.format("{0,number,#0.0} | {1,number,#0.0}", array.get(topScores, 1), array.get(topScores, 2)), text_color=tc, text_size=size.small)
        table.cell(stack, 0, 13, str.format("MinScore: {0,number,#0.0}", zoneScoreMin), text_color=tc, text_size=size.small)
        table.cell(stack, 1, 13, str.format("Decay: {0} bars", zoneHalfLife), text_color=tc, text_size=size.small)

Overview

Tweezer & Kangaroo Zones [WavesUnchained] scores each zone, decays freshness, merges overlapping zones, and can invalidate a zone after a close-through or after the first retest. Optional trend context uses an EMA, Supertrend, and MOST on a higher timeframe, with relative-volume filters that stay neutral when volume is not meaningful. A structure read of swing highs and lows and a multi-timeframe stack panel add confluence. It does not place trades.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

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

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

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
Detect Tweezersbooltrueinput_enable_tweezers
Lookback barsint3tweezer_lookback
Level tolerance (%)float0.01tweezer_tolerance
Require long body on 1st candleboolfalsetweezer_require_long_body
Require trend contextboolfalsetweezer_require_trend
Check wick similaritybooltruetweezer_check_wicks
Max wick ratiofloat2.0tweezer_wick_tolerance
Min RelVolume for 2nd candlefloat0.8tweezer_min_volume
Tweezer Weightfloat0.8wTweezer
Kangaroo Weightfloat1.2wKangaroo
Detect Kangaroo Tailbooltrueinput_enable_kangaroo
Min wick/body ratiofloat1.8kt_wickBodyMin
Show all 40 settings
SettingTypeDefaultVariable
Max body as frac of rangefloat0.50kt_bodyFracMax
Body position tolerance (0-1)float0.3kt_bodyPosition
Require trend contextboolfalsekt_requireTrend
Min RelVolume for rejectionfloat0.9kt_min_volume
Extend (bars)int200zoneExtendBars
Lifetime (bars)int400zoneTTL
Invalidate on close-throughbooltrueinvalidateOnClose
Only first retest can signalbooltrueretestOnceOnly
Max overlap to merge zones (%)float0.5zoneOverlapPct
Merge overlapping zones (vs skip)booltruezoneMergeEnabled
EMA lengthint200emaLen
HTF for regime gatetimeframe240trendTF
Factorfloat3.0stFactor
ATR Lengthint10stAtrLen
ATR Lengthint10mostLen
Multiplierfloat2.0mostMult
TF1timeframe15stackTF1
TF2timeframe60stackTF2
TF3timeframe240stackTF3
Trend Filter TypestringEMAtrendFilterType
ST scopestringLocalgateSTscope
ST HTFtimeframe240gateSTtf
MOST scopestringLocalgateMOSTscope
MOST HTFtimeframe240gateMOSTtf
TF Sensitivityfloat1.0tfSensitivity
Auto-detect from chart TFbooltruetfAutoDetect
Enable Auto-Threshold TuningbooltrueautoTune
ATR lengthint14atrLen
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

7 stars

This indicator plots Smart Money Concepts and price action components directly on the price chart. It highlights bullish and bearish order blocks, multi-timeframe pivot points, market structure swings with key Fibonacci retracement levels (0.618 and 0.786), zo

Indicator v5 SMC / ICT / price actionSupport & resistance Repaints
Updated 3 months ago · None stated GitHub repository of Cryptokazancev Strategy Pack
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

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