Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Source Code


// This Pine Script® code is subject to the Terms of Use at https://www.tradingview.com/pine-script-docs/concepts/licensing/
// © InstitutionalFlowScalper

//@version=6
indicator("Institutional Flow Scalper [IFS] v4", shorttitle="IFS v4", overlay=true, max_lines_count=500, max_labels_count=500, max_boxes_count=500)

// ─────────────────────────────────────────────────────────────────────────────
// ██ INPUTS
// ─────────────────────────────────────────────────────────────────────────────

// ── Signal Engine ──
grpSignal = "Signal Engine"
sensitivity     = input.string("Medium", "Sensitivity", options=["Low", "Medium", "High", "Adaptive"], group=grpSignal, tooltip="Adaptive mode auto-adjusts based on current volatility regime")
minConfidence   = input.int(30, "Min Confidence Score (0-100)", minval=0, maxval=100, group=grpSignal)
showSignals     = input.bool(true, "Show Entry Signals", group=grpSignal)
showExits       = input.bool(true, "Show Exit Signals", group=grpSignal)

// ── Volume Delta (CVD) ──
grpCVD = "Volume Delta Engine"
cvdLength       = input.int(14, "CVD Lookback", minval=5, maxval=50, group=grpCVD)
cvdSmoothing    = input.int(5, "CVD Smoothing", minval=1, maxval=20, group=grpCVD)
showCVDDiv      = input.bool(true, "Show CVD Divergences", group=grpCVD)

// ── Liquidity Sweep ──
grpLiq = "Liquidity Sweep Detection"
swingLookback   = input.int(10, "Swing Point Lookback", minval=3, maxval=30, group=grpLiq)
sweepThreshold  = input.float(1.5, "Volume Spike Threshold (x avg)", minval=1.0, maxval=5.0, step=0.1, group=grpLiq)
showSweeps      = input.bool(true, "Show Liquidity Sweeps", group=grpLiq)

// ── VWAP Institutional ──
grpVWAP = "Institutional VWAP"
showVWAP        = input.bool(true, "Show VWAP", group=grpVWAP)
showVWAPBands   = input.bool(true, "Show VWAP Bands (1σ, 2σ)", group=grpVWAP)
vwapBandMult1   = input.float(1.0, "Band 1 Multiplier", minval=0.5, maxval=3.0, step=0.1, group=grpVWAP)
vwapBandMult2   = input.float(2.0, "Band 2 Multiplier", minval=1.0, maxval=5.0, step=0.1, group=grpVWAP)
pocLength       = input.int(20, "POC Lookback", minval=10, maxval=50, group=grpVWAP)

// ── Absorption Detection ──
grpAbsorb = "Order Absorption"
absorbVolMult   = input.float(2.0, "Absorption Volume Multiple", minval=1.5, maxval=5.0, step=0.1, group=grpAbsorb)
absorbBodyRatio = input.float(0.3, "Max Body/Range Ratio", minval=0.1, maxval=0.5, step=0.05, group=grpAbsorb)
showAbsorption  = input.bool(true, "Show Absorption Zones", group=grpAbsorb)

// ── Anti-Chop Filter ──
grpChop = "Anti-Chop Filter"
chopLength      = input.int(14, "Chop Index Length", minval=5, maxval=30, group=grpChop)
chopThreshold   = input.float(65.0, "Chop Threshold", minval=50.0, maxval=75.0, step=0.1, group=grpChop, tooltip="Above this value = choppy market, signals disabled")
showChopZone    = input.bool(true, "Show Chop Zone Background", group=grpChop)

// ── Session Awareness ──
grpSession = "Session Awareness"
showSessionBG   = input.bool(false, "Show Session Background", group=grpSession)
nySession       = input.session("0930-1100", "NY Morning Session", group=grpSession)
pmSession       = input.session("1400-1600", "NY Afternoon Session", group=grpSession)
ldnSession      = input.session("0300-0500", "London Session", group=grpSession)

// ── Risk Management ──
grpRisk = "Risk Management"
atrLength       = input.int(14, "ATR Length", minval=5, maxval=30, group=grpRisk)
atrMultSL       = input.float(1.5, "Stop Loss (ATR Multiple)", minval=0.5, maxval=5.0, step=0.1, group=grpRisk)
atrMultTP      = input.float(1.0, "Take Profit (ATR Multiple)", minval=0.5, maxval=5.0, step=0.1, group=grpRisk)
showSLTP        = input.bool(true, "Show SL/TP Levels", group=grpRisk)
sltp_extend     = input.int(30, "SL/TP Line Length (bars)", minval=10, maxval=100, group=grpRisk)

// ── Dashboard ──
grpDash = "Dashboard"
showDashboard   = input.bool(true, "Show Dashboard", group=grpDash)
dashPosition    = input.string("Top Right", "Dashboard Position", options=["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group=grpDash)
dashSize        = input.string("Small", "Dashboard Size", options=["Tiny", "Small", "Normal"], group=grpDash)

// ── Visual Style ──
grpVis = "Visual Style"
bullColor       = input.color(color.new(#00E676, 0), "Bull Color", group=grpVis)
bearColor       = input.color(color.new(#FF1744, 0), "Bear Color", group=grpVis)
neutralColor    = input.color(color.new(#FFD740, 0), "Neutral/Warning Color", group=grpVis)
vwapColor       = input.color(color.new(#2196F3, 0), "VWAP Color", group=grpVis)
entryColor      = input.color(color.new(#FFFFFF, 0), "Entry Line Color", group=grpVis)
slColor         = input.color(color.new(#FF1744, 0), "Stop Loss Color", group=grpVis)
tpColor        = input.color(color.new(#00E676, 0), "TP Color", group=grpVis)

// ─────────────────────────────────────────────────────────────────────────────
// ██ CORE CALCULATIONS
// ─────────────────────────────────────────────────────────────────────────────

atrVal = ta.atr(atrLength)

atrPctRank = ta.percentrank(atrVal, 100)
adaptiveMult = switch sensitivity
    "Low"      => 0.7
    "High"     => 1.3
    "Adaptive" => atrPctRank > 70 ? 0.7 : atrPctRank < 30 ? 1.3 : 1.0
    => 1.0

// ─────────────────────────────────────────────────────────────────────────────
// ██ PILLAR 1: SYNTHETIC VOLUME DELTA (CVD)
// ─────────────────────────────────────────────────────────────────────────────

barRange    = high - low
bodySize    = math.abs(close - open)
upperWick   = high - math.max(close, open)
lowerWick   = math.min(close, open) - low

closePosition = barRange > 0 ? (close - low) / barRange : 0.5
buyVolume     = volume * closePosition
sellVolume    = volume * (1.0 - closePosition)
volumeDelta   = buyVolume - sellVolume

var float cvd = 0.0
cvd := cvd + volumeDelta

var float sessionCVD = 0.0
isNewSession = ta.change(time("D")) != 0
sessionCVD := isNewSession ? volumeDelta : sessionCVD + volumeDelta

cvdSmoothed = ta.ema(sessionCVD, cvdSmoothing)
cvdRateOfChange = ta.roc(cvdSmoothed, cvdLength)

// CVD Divergence
pivotLowPrice  = ta.pivotlow(low, swingLookback, swingLookback)
pivotHighPrice = ta.pivotlow(-high, swingLookback, swingLookback)

var float prevPivotLowPrice  = na
var float prevPivotLowCVD    = na
var float prevPivotHighPrice = na
var float prevPivotHighCVD   = na

bullishCVDDiv = false
bearishCVDDiv = false

if not na(pivotLowPrice)
    currLowCVD = cvdSmoothed[swingLookback]
    if not na(prevPivotLowPrice) and not na(prevPivotLowCVD)
        if pivotLowPrice < prevPivotLowPrice and currLowCVD > prevPivotLowCVD
            bullishCVDDiv := true
    prevPivotLowPrice := pivotLowPrice
    prevPivotLowCVD   := currLowCVD

if not na(pivotHighPrice)
    currHighPrice = -pivotHighPrice
    currHighCVD   = cvdSmoothed[swingLookback]
    if not na(prevPivotHighPrice) and not na(prevPivotHighCVD)
        if currHighPrice > prevPivotHighPrice and currHighCVD < prevPivotHighCVD
            bearishCVDDiv := true
    prevPivotHighPrice := currHighPrice
    prevPivotHighCVD   := currHighCVD

// ─────────────────────────────────────────────────────────────────────────────
// ██ PILLAR 2: LIQUIDITY SWEEP DETECTION
// ─────────────────────────────────────────────────────────────────────────────

swingHigh = ta.pivothigh(high, swingLookback, 1)
swingLow  = ta.pivotlow(low, swingLookback, 1)

var float recentSwingHigh = na
var float recentSwingLow  = na

if not na(swingHigh)
    recentSwingHigh := swingHigh
if not na(swingLow)
    recentSwingLow := swingLow

avgVolume   = ta.sma(volume, 20)
volumeSpike = volume > avgVolume * sweepThreshold

bearishSweep = not na(recentSwingHigh) and high > recentSwingHigh and close < recentSwingHigh and close < open and volumeSpike
bullishSweep = not na(recentSwingLow) and low < recentSwingLow and close > recentSwingLow and close > open and volumeSpike

// ─────────────────────────────────────────────────────────────────────────────
// ██ PILLAR 3: MICRO MOMENTUM SHIFT
// ─────────────────────────────────────────────────────────────────────────────

priceROC   = ta.roc(close, cvdLength)
cvdROC     = ta.roc(cvdSmoothed, cvdLength)

momentumBullish = priceROC < 0 and cvdROC > 0
momentumBearish = priceROC > 0 and cvdROC < 0

pressureIndex = ta.ema(volumeDelta / math.max(volume, 1) * 100, cvdSmoothing)

// ─────────────────────────────────────────────────────────────────────────────
// ██ ORDER ABSORPTION
// ─────────────────────────────────────────────────────────────────────────────

isAbsorption      = volume > avgVolume * absorbVolMult and barRange > 0 and (bodySize / barRange) < absorbBodyRatio
bullishAbsorption = isAbsorption and close > open
bearishAbsorption = isAbsorption and close < open

// ─────────────────────────────────────────────────────────────────────────────
// ██ INSTITUTIONAL VWAP
// ─────────────────────────────────────────────────────────────────────────────

var float sumPV  = 0.0
var float sumVol = 0.0
var float sumPV2 = 0.0

if isNewSession
    sumPV  := 0.0
    sumVol := 0.0
    sumPV2 := 0.0

typicalPrice = (high + low + close) / 3.0
sumPV  += typicalPrice * volume
sumVol += volume
sumPV2 += typicalPrice * typicalPrice * volume

vwapVal    = sumVol > 0 ? sumPV / sumVol : close
vwapVar    = sumVol > 0 ? sumPV2 / sumVol - vwapVal * vwapVal : 0.0
vwapStdDev = math.sqrt(math.max(vwapVar, 0))

vwapUpper1 = vwapVal + vwapStdDev * vwapBandMult1
vwapLower1 = vwapVal - vwapStdDev * vwapBandMult1
vwapUpper2 = vwapVal + vwapStdDev * vwapBandMult2
vwapLower2 = vwapVal - vwapStdDev * vwapBandMult2

vwapDistance = vwapStdDev > 0 ? (close - vwapVal) / vwapStdDev : 0.0

// ── POC ──
var float pocPrice = na
pocSumPV  = ta.sma(typicalPrice * volume, pocLength) * pocLength
pocSumVol = ta.sma(volume, pocLength) * pocLength
pocPrice := pocSumVol > 0 ? pocSumPV / pocSumVol : close

// ─────────────────────────────────────────────────────────────────────────────
// ██ CHOPPINESS INDEX
// ─────────────────────────────────────────────────────────────────────────────

highestHigh = ta.highest(high, chopLength)
lowestLow   = ta.lowest(low, chopLength)
atrSum      = ta.sma(atrVal, chopLength) * chopLength
rangeHL     = highestHigh - lowestLow

chopIndex = rangeHL > 0 ? 100.0 * math.log10(atrSum / rangeHL) / math.log10(chopLength) : 50.0
isChoppy  = chopIndex > chopThreshold

// ─────────────────────────────────────────────────────────────────────────────
// ██ SESSION AWARENESS
// ─────────────────────────────────────────────────────────────────────────────

inNYMorning  = not na(time(timeframe.period, nySession, "America/New_York"))
inNYAfternoon = not na(time(timeframe.period, pmSession, "America/New_York"))
inLondon     = not na(time(timeframe.period, ldnSession, "America/New_York"))
inKillZone   = inNYMorning or inNYAfternoon or inLondon
sessionMult  = inKillZone ? 1.2 : 1.0

// ─────────────────────────────────────────────────────────────────────────────
// ██ CONFIDENCE SCORE (0-100)
// ─────────────────────────────────────────────────────────────────────────────

deltaScore   = math.min(math.abs(pressureIndex) / 2.0, 20.0)
cvdDivScore  = (bullishCVDDiv or bearishCVDDiv) ? 20.0 : 0.0
sweepScore   = (bullishSweep or bearishSweep) ? 20.0 : 0.0
momScore     = math.min(math.abs(cvdROC - priceROC), 20.0)
vwapScore    = math.abs(vwapDistance) > 1.5 ? 8.0 : math.abs(vwapDistance) > 1.0 ? 5.0 : 2.0
absorbScore  = isAbsorption ? 6.0 : 0.0
contextScore = math.min(vwapScore + absorbScore + 6.0, 20.0)

rawConfidence = deltaScore + cvdDivScore + sweepScore + momScore + contextScore
confidence    = math.min(rawConfidence * adaptiveMult * sessionMult, 100.0)

// ─────────────────────────────────────────────────────────────────────────────
// ██ SIGNAL GENERATION (v3 - Enhanced for Scalping Frequency)
// ─────────────────────────────────────────────────────────────────────────────

// ── Additional Common Pillars ──
// EMA trend alignment (fast crosses slow)
emaFast = ta.ema(close, 9)
emaSlow = ta.ema(close, 21)
emaBullish = emaFast > emaSlow
emaBearish = emaFast < emaSlow
emaCrossUp   = ta.crossover(emaFast, emaSlow)
emaCrossDown = ta.crossunder(emaFast, emaSlow)

// Volume spike (current bar volume > 1.3x average)
volStrong = volume > avgVolume * 1.3

// Strong candle (body > 60% of range, directional conviction)
strongBullCandle = barRange > 0 and (close - open) / barRange > 0.6 and close > open and volStrong
strongBearCandle = barRange > 0 and (open - close) / barRange > 0.6 and close < open and volStrong

// Price rejection from VWAP bands (mean reversion signal)
vwapBounceUp   = low <= vwapLower1 and close > vwapLower1 and close > open
vwapBounceDown = high >= vwapUpper1 and close < vwapUpper1 and close < open

// VWAP cross
vwapBullish = close > vwapVal and close[1] <= vwapVal
vwapBearish = close < vwapVal and close[1] >= vwapVal

// ── Pillar Count (now with 8 possible pillars, need 2) ──
bullPillarCount = 0
bullPillarCount += volumeDelta > 0 and deltaScore > 3 ? 1 : 0    // 1. Strong buy delta
bullPillarCount += bullishCVDDiv or momentumBullish ? 1 : 0        // 2. CVD divergence or momentum shift
bullPillarCount += bullishSweep ? 1 : 0                            // 3. Liquidity sweep
bullPillarCount += bullishAbsorption ? 1 : 0                       // 4. Order absorption
bullPillarCount += vwapBullish or vwapBounceUp ? 1 : 0             // 5. VWAP cross or bounce
bullPillarCount += emaCrossUp or (emaBullish and strongBullCandle) ? 1 : 0  // 6. EMA alignment + strong candle
bullPillarCount += close > pocPrice and close[1] <= pocPrice ? 1 : 0  // 7. POC breakout

bearPillarCount = 0
bearPillarCount += volumeDelta < 0 and deltaScore > 3 ? 1 : 0
bearPillarCount += bearishCVDDiv or momentumBearish ? 1 : 0
bearPillarCount += bearishSweep ? 1 : 0
bearPillarCount += bearishAbsorption ? 1 : 0
bearPillarCount += vwapBearish or vwapBounceDown ? 1 : 0
bearPillarCount += emaCrossDown or (emaBearish and strongBearCandle) ? 1 : 0
bearPillarCount += close < pocPrice and close[1] >= pocPrice ? 1 : 0

// ── Bias ──
bullishBias = (volumeDelta > 0 ? 1 : 0) + (momentumBullish ? 1 : 0) + (bullishCVDDiv ? 2 : 0) + (bullishSweep ? 2 : 0) + (bullishAbsorption ? 1 : 0) + (close > vwapVal ? 1 : 0) + (emaBullish ? 1 : 0)
bearishBias = (volumeDelta < 0 ? 1 : 0) + (momentumBearish ? 1 : 0) + (bearishCVDDiv ? 2 : 0) + (bearishSweep ? 2 : 0) + (bearishAbsorption ? 1 : 0) + (close < vwapVal ? 1 : 0) + (emaBearish ? 1 : 0)

passesConfidence = confidence >= minConfidence
passesChop       = not isChoppy
passesSession    = inKillZone

longSignal  = bullishBias > bearishBias and bullPillarCount >= 2 and passesConfidence and passesChop and passesSession and barstate.isconfirmed
shortSignal = bearishBias > bullishBias and bearPillarCount >= 2 and passesConfidence and passesChop and passesSession and barstate.isconfirmed

var int lastSignalBar = 0
signalCooldown = bar_index - lastSignalBar >= 3

// Position state declared here so it can be used in entry conditions
// State: 0=flat, 1=long, -1=short
var int   posState    = 0
var float posEntry    = na
var float posSL       = na
var float posTP      = na
var int   posEntryBar = na

// CRITICAL: Only allow new entry when position is FLAT (previous trade closed by TP or SL)
longEntry  = longSignal and signalCooldown and posState == 0
shortEntry = shortSignal and signalCooldown and posState == 0

if longEntry or shortEntry
    lastSignalBar := bar_index

// ─────────────────────────────────────────────────────────────────────────────
// ██ POSITION TRACKER
// ─────────────────────────────────────────────────────────────────────────────

// ── Open Long ──
if longEntry
    posState    := 1
    posEntry    := close
    posSL       := close - atrVal * atrMultSL
    posTP      := close + atrVal * atrMultTP
    posEntryBar := bar_index

// ── Open Short ──
if shortEntry
    posState    := -1
    posEntry    := close
    posSL       := close + atrVal * atrMultSL
    posTP      := close - atrVal * atrMultTP
    posEntryBar := bar_index

// ── Exit Conditions ──

longSLHit   = posState == 1 and not na(posSL) and low <= posSL
shortSLHit  = posState == -1 and not na(posSL) and high >= posSL

longTPHit   = posState == 1 and not na(posTP) and high >= posTP
shortTPHit  = posState == -1 and not na(posTP) and low <= posTP

// Momentum exit (only before TP reached)
longMomExit  = posState == 1 and momentumBearish and pressureIndex < -5
shortMomExit = posState == -1 and momentumBullish and pressureIndex > 5

longExit  = longSLHit or longTPHit or longMomExit
shortExit = shortSLHit or shortTPHit or shortMomExit

// Determine exit reason
var string exitReason = ""
if longExit and posState == 1
    if longTPHit
        exitReason := "TP HIT ✓"
    else if longSLHit
        exitReason := "SL HIT ✗"
    else
        exitReason := "MOM EXIT"
    posState   := 0
    posEntry   := na
    posSL      := na
    posTP     := na

if shortExit and posState == -1
    if shortTPHit
        exitReason := "TP HIT ✓"
    else if shortSLHit
        exitReason := "SL HIT ✗"
    else
        exitReason := "MOM EXIT"
    posState   := 0
    posEntry   := na
    posSL      := na
    posTP     := na

// ─────────────────────────────────────────────────────────────────────────────
// ██ PLOTTING - VWAP & OVERLAYS
// ─────────────────────────────────────────────────────────────────────────────

plot(showVWAP ? vwapVal : na, "VWAP", vwapColor, linewidth=2)
plot(emaFast, "EMA 9", color.new(#FF9800, 40), linewidth=1)
plot(emaSlow, "EMA 21", color.new(#9C27B0, 40), linewidth=1)
p_vu1 = plot(showVWAPBands ? vwapUpper1 : na, "VWAP +1σ", color.new(vwapColor, 60))
p_vl1 = plot(showVWAPBands ? vwapLower1 : na, "VWAP -1σ", color.new(vwapColor, 60))
p_vu2 = plot(showVWAPBands ? vwapUpper2 : na, "VWAP +2σ", color.new(vwapColor, 80), style=plot.style_circles)
p_vl2 = plot(showVWAPBands ? vwapLower2 : na, "VWAP -2σ", color.new(vwapColor, 80), style=plot.style_circles)
fill(p_vu1, p_vl1, color.new(vwapColor, 92))
plot(showVWAP ? pocPrice : na, "Dynamic POC", color.new(#FF9800, 30), style=plot.style_cross)

bgcolor(showChopZone and isChoppy ? color.new(neutralColor, 90) : na, title="Chop Zone")
bgcolor(showSessionBG and inNYMorning ? color.new(#2196F3, 93) : na, title="NY Morning")
bgcolor(showSessionBG and inNYAfternoon ? color.new(#4CAF50, 93) : na, title="NY Afternoon")
bgcolor(showSessionBG and inLondon ? color.new(#9C27B0, 93) : na, title="London")

// ─────────────────────────────────────────────────────────────────────────────
// ██ PLOTTING - ENTRIES & EXITS (Today only to keep chart clean)
// ─────────────────────────────────────────────────────────────────────────────

// Only show labels/lines/boxes from today's session
isToday = ta.change(time("D")) == 0 and time >= timenow - 86400000

plotshape(showSignals and longEntry, "Long Entry", shape.labelup, location.belowbar, bullColor, size=size.large, text="LONG", textcolor=color.white)
plotshape(showSignals and shortEntry, "Short Entry", shape.labeldown, location.abovebar, bearColor, size=size.large, text="SHORT", textcolor=color.white)

barcolor(longEntry ? bullColor : shortEntry ? bearColor : longExit ? bearColor : shortExit ? bullColor : na)

// Exit labels (today only)
if showExits and longExit and isToday
    color exitCol = exitReason == "TP HIT ✓" ? color.new(tpColor, 10) : exitReason == "SL HIT ✗" ? color.new(bearColor, 10) : color.new(neutralColor, 10)
    label.new(bar_index, high, exitReason, color=exitCol, textcolor=color.white, style=label.style_label_down, size=size.normal)

if showExits and shortExit and isToday
    color exitCol = exitReason == "TP HIT ✓" ? color.new(tpColor, 10) : exitReason == "SL HIT ✗" ? color.new(bearColor, 10) : color.new(neutralColor, 10)
    label.new(bar_index, low, exitReason, color=exitCol, textcolor=color.white, style=label.style_label_up, size=size.normal)

// CVD Divergence (today only)
if showCVDDiv and bullishCVDDiv and isToday
    label.new(bar_index - swingLookback, low[swingLookback], "Bull\nDiv", color=color.new(bullColor, 20), textcolor=color.white, style=label.style_label_up, size=size.small)
if showCVDDiv and bearishCVDDiv and isToday
    label.new(bar_index - swingLookback, high[swingLookback], "Bear\nDiv", color=color.new(bearColor, 20), textcolor=color.white, style=label.style_label_down, size=size.small)

// Sweeps (today only)
if showSweeps and bullishSweep and isToday
    label.new(bar_index, low, "SWEEP ▲", color=color.new(bullColor, 10), textcolor=color.white, style=label.style_label_up, size=size.normal)
if showSweeps and bearishSweep and isToday
    label.new(bar_index, high, "SWEEP ▼", color=color.new(bearColor, 10), textcolor=color.white, style=label.style_label_down, size=size.normal)

// Absorption (plotshape handles all bars but these are small diamonds, keeping them)
plotshape(showAbsorption and bullishAbsorption and isToday, "Bull Absorb", shape.diamond, location.belowbar, color.new(bullColor, 30), size=size.tiny)
plotshape(showAbsorption and bearishAbsorption and isToday, "Bear Absorb", shape.diamond, location.abovebar, color.new(bearColor, 30), size=size.tiny)

// ─────────────────────────────────────────────────────────────────────────────
// ██ PLOTTING - SL/TP/ENTRY VISUALS (ALL TRADES - HISTORICAL)
// ─────────────────────────────────────────────────────────────────────────────

// Instead of showing all historical trades, only show today's to keep chart clean

if showSLTP and (longEntry or shortEntry) and isToday
    float ep  = close
    float slP = na
    float tpP = na

    if longEntry
        slP := ep - atrVal * atrMultSL
        tpP := ep + atrVal * atrMultTP
    else
        slP := ep + atrVal * atrMultSL
        tpP := ep - atrVal * atrMultTP

    int bS = bar_index
    int bE = bar_index + sltp_extend

    // Entry line
    line.new(bS, ep, bE, ep, color=entryColor, style=line.style_solid, width=2)
    label.new(bE + 1, ep, "ENTRY " + str.tostring(ep, "#.##"), color=color.new(entryColor, 20), textcolor=color.new(#000000, 0), style=label.style_label_left, size=size.normal)

    // SL line
    line.new(bS, slP, bE, slP, color=slColor, style=line.style_solid, width=2)
    label.new(bE + 1, slP, "SL " + str.tostring(slP, "#.##") + "\n" + str.tostring(math.abs(ep - slP), "#.##") + " pts", color=color.new(slColor, 20), textcolor=color.white, style=label.style_label_left, size=size.normal)

    // TP line
    line.new(bS, tpP, bE, tpP, color=tpColor, style=line.style_solid, width=2)
    label.new(bE + 1, tpP, "TP " + str.tostring(tpP, "#.##") + "\n" + str.tostring(math.abs(tpP - ep), "#.##") + " pts", color=color.new(tpColor, 20), textcolor=color.white, style=label.style_label_left, size=size.normal)


    // Colored zones
    if longEntry
        box.new(bS, ep, bE, slP, border_color=color.new(slColor, 80), bgcolor=color.new(slColor, 88), border_width=0)
        box.new(bS, tpP, bE, ep, border_color=color.new(tpColor, 80), bgcolor=color.new(tpColor, 88), border_width=0)
    else
        box.new(bS, slP, bE, ep, border_color=color.new(slColor, 80), bgcolor=color.new(slColor, 88), border_width=0)
        box.new(bS, ep, bE, tpP, border_color=color.new(tpColor, 80), bgcolor=color.new(tpColor, 88), border_width=0)

    // Confidence badge
    string dirText = longEntry ? "▲ LONG ENTRY" : "▼ SHORT ENTRY"
    string pText   = ""
    if longEntry
        pText := (volumeDelta > 0 and deltaScore > 3 ? "Delta " : "") + (bullishCVDDiv or momentumBullish ? "Mom " : "") + (bullishSweep ? "Sweep " : "") + (bullishAbsorption ? "Absorb " : "") + (vwapBullish or vwapBounceUp ? "VWAP " : "") + (emaCrossUp or (emaBullish and strongBullCandle) ? "EMA " : "")
    else
        pText := (volumeDelta < 0 and deltaScore > 3 ? "Delta " : "") + (bearishCVDDiv or momentumBearish ? "Mom " : "") + (bearishSweep ? "Sweep " : "") + (bearishAbsorption ? "Absorb " : "") + (vwapBearish or vwapBounceDown ? "VWAP " : "") + (emaCrossDown or (emaBearish and strongBearCandle) ? "EMA " : "")

    color cBg = confidence >= 80 ? color.new(bullColor, 10) : confidence >= 65 ? color.new(#2196F3, 10) : color.new(neutralColor, 10)

    if longEntry
        label.new(bS, low - atrVal * 0.5, dirText + "\nConfidence: " + str.tostring(math.round(confidence)) + "%\nPillars: " + pText, color=cBg, textcolor=color.white, style=label.style_label_up, size=size.normal)
    else
        label.new(bS, high + atrVal * 0.5, dirText + "\nConfidence: " + str.tostring(math.round(confidence)) + "%\nPillars: " + pText, color=cBg, textcolor=color.white, style=label.style_label_down, size=size.normal)


// ─────────────────────────────────────────────────────────────────────────────
// ██ DASHBOARD
// ─────────────────────────────────────────────────────────────────────────────

if showDashboard and barstate.islast
    var table dash = na

    tblPos = switch dashPosition
        "Top Left"     => position.top_left
        "Top Right"    => position.top_right
        "Bottom Left"  => position.bottom_left
        "Bottom Right" => position.bottom_right
        => position.top_right

    tblSize = switch dashSize
        "Tiny"   => size.tiny
        "Small"  => size.small
        "Normal" => size.normal
        => size.small

    dash := table.new(tblPos, 2, 10, bgcolor=color.new(#1a1a2e, 10), border_width=1, border_color=color.new(#333355, 0), frame_width=2, frame_color=color.new(#333355, 0))

    table.cell(dash, 0, 0, "IFS v4", text_color=color.white, text_size=tblSize, bgcolor=color.new(#16213e, 0), text_formatting=text.format_bold)
    table.cell(dash, 1, 0, "Dashboard", text_color=color.new(color.white, 40), text_size=tblSize, bgcolor=color.new(#16213e, 0))

    confColor = confidence >= 80 ? bullColor : confidence >= 60 ? neutralColor : bearColor
    table.cell(dash, 0, 1, "Confidence", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 1, str.tostring(math.round(confidence)) + "%", text_color=confColor, text_size=tblSize, text_formatting=text.format_bold)

    deltaCol = volumeDelta > 0 ? bullColor : bearColor
    table.cell(dash, 0, 2, "Vol Delta", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 2, volumeDelta > 0 ? "BUY ▲" : "SELL ▼", text_color=deltaCol, text_size=tblSize)

    pressCol = pressureIndex > 5 ? bullColor : pressureIndex < -5 ? bearColor : neutralColor
    table.cell(dash, 0, 3, "Pressure", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 3, str.tostring(math.round(pressureIndex, 1)), text_color=pressCol, text_size=tblSize)

    vwapDistCol = math.abs(vwapDistance) > 2 ? bearColor : math.abs(vwapDistance) > 1 ? neutralColor : bullColor
    table.cell(dash, 0, 4, "VWAP Dist", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 4, (vwapDistance > 0 ? "+" : "") + str.tostring(math.round(vwapDistance, 2)) + "σ", text_color=vwapDistCol, text_size=tblSize)

    table.cell(dash, 0, 5, "ATR", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 5, str.tostring(math.round(atrVal, 2)), text_color=color.new(color.white, 50), text_size=tblSize)

    sessStr = inNYMorning ? "NY AM 🔥" : inNYAfternoon ? "NY PM 🔥" : inLondon ? "LDN 🔥" : "OFF-HOURS"
    sessCol = inKillZone ? bullColor : color.new(color.white, 50)
    table.cell(dash, 0, 6, "Session", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 6, sessStr, text_color=sessCol, text_size=tblSize)

    chopCol = isChoppy ? bearColor : bullColor
    table.cell(dash, 0, 7, "Chop Idx", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 7, str.tostring(math.round(chopIndex, 1)), text_color=chopCol, text_size=tblSize)

    biasCol = bullishBias > bearishBias ? bullColor : bearishBias > bullishBias ? bearColor : neutralColor
    table.cell(dash, 0, 8, "Bias", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 8, bullishBias > bearishBias ? "BULLISH" : bearishBias > bullishBias ? "BEARISH" : "NEUTRAL", text_color=biasCol, text_size=tblSize, text_formatting=text.format_bold)

    // Position status row
    posStr = posState == 1 ? "LONG ACTIVE" : posState == -1 ? "SHORT ACTIVE" : "FLAT"
    posCol = posState == 1 ? bullColor : posState == -1 ? bearColor : color.new(color.white, 50)
    table.cell(dash, 0, 9, "Position", text_color=color.new(color.white, 30), text_size=tblSize)
    table.cell(dash, 1, 9, posStr, text_color=posCol, text_size=tblSize, text_formatting=text.format_bold)

// ─────────────────────────────────────────────────────────────────────────────
// ██ ALERTS
// ─────────────────────────────────────────────────────────────────────────────

alertcondition(longEntry, "IFS Long Entry", "IFS: LONG signal | Confidence: {{plot_0}}% | Entry: {{close}}")
alertcondition(shortEntry, "IFS Short Entry", "IFS: SHORT signal | Confidence: {{plot_0}}% | Entry: {{close}}")
alertcondition(longExit, "IFS Long Exit", "IFS: Exit LONG")
alertcondition(shortExit, "IFS Short Exit", "IFS: Exit SHORT")
alertcondition(bullishSweep, "Bullish Sweep", "IFS: Bullish liquidity sweep")
alertcondition(bearishSweep, "Bearish Sweep", "IFS: Bearish liquidity sweep")
alertcondition(isAbsorption, "Absorption", "IFS: Order absorption detected")

// ─────────────────────────────────────────────────────────────────────────────
// ██ DATA WINDOW
// ─────────────────────────────────────────────────────────────────────────────

plot(confidence, "Confidence Score", display=display.data_window, color=color.new(color.white, 100))
plot(pressureIndex, "Pressure Index", display=display.data_window, color=color.new(color.white, 100))
plot(sessionCVD, "Session CVD", display=display.data_window, color=color.new(color.white, 100))
plot(chopIndex, "Chop Index", display=display.data_window, color=color.new(color.white, 100))
plot(vwapDistance, "VWAP Distance", display=display.data_window, color=color.new(color.white, 100))