//@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.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Detect Tweezers
bool
true
input_enable_tweezers
Lookback bars
int
3
tweezer_lookback
Level tolerance (%)
float
0.01
tweezer_tolerance
Require long body on 1st candle
bool
false
tweezer_require_long_body
Require trend context
bool
false
tweezer_require_trend
Check wick similarity
bool
true
tweezer_check_wicks
Max wick ratio
float
2.0
tweezer_wick_tolerance
Min RelVolume for 2nd candle
float
0.8
tweezer_min_volume
Tweezer Weight
float
0.8
wTweezer
Kangaroo Weight
float
1.2
wKangaroo
Detect Kangaroo Tail
bool
true
input_enable_kangaroo
Min wick/body ratio
float
1.8
kt_wickBodyMin
Show all 40 settings
Setting
Type
Default
Variable
Max body as frac of range
float
0.50
kt_bodyFracMax
Body position tolerance (0-1)
float
0.3
kt_bodyPosition
Require trend context
bool
false
kt_requireTrend
Min RelVolume for rejection
float
0.9
kt_min_volume
Extend (bars)
int
200
zoneExtendBars
Lifetime (bars)
int
400
zoneTTL
Invalidate on close-through
bool
true
invalidateOnClose
Only first retest can signal
bool
true
retestOnceOnly
Max overlap to merge zones (%)
float
0.5
zoneOverlapPct
Merge overlapping zones (vs skip)
bool
true
zoneMergeEnabled
EMA length
int
200
emaLen
HTF for regime gate
timeframe
240
trendTF
Factor
float
3.0
stFactor
ATR Length
int
10
stAtrLen
ATR Length
int
10
mostLen
Multiplier
float
2.0
mostMult
TF1
timeframe
15
stackTF1
TF2
timeframe
60
stackTF2
TF3
timeframe
240
stackTF3
Trend Filter Type
string
EMA
trendFilterType
ST scope
string
Local
gateSTscope
ST HTF
timeframe
240
gateSTtf
MOST scope
string
Local
gateMOSTscope
MOST HTF
timeframe
240
gateMOSTtf
TF Sensitivity
float
1.0
tfSensitivity
Auto-detect from chart TF
bool
true
tfAutoDetect
Enable Auto-Threshold Tuning
bool
true
autoTune
ATR length
int
14
atrLen
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 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
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