Indicator · Pine Script v6 · On the chart

SPY 0DTE Scalper - 5m

by chris-c-thomas

This overlay indicator scores up to ten equal-weight conditions on the five-minute chart and marks a signal when the total meets a configurable threshold.

View on GitHub

GitHub stars14on the repository
Pine versionv6current
Lines of code1,133
Settings40inputs
LicenceMIT
Last update6 months ago8 Apr 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

1,133 lines of Pine Script v6 from scripts/spy-0dte-scalper/spy-0dte-scalper-5min.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

spy-0dte-scalper-5min.pine
// ============================================================================
// SPY 0DTE Scalper
// Timeframe: 5-minute
// Version: 1.0.0
// Author: Chris Thomas
// ============================================================================
// Purpose-built overlay for scalping SPY 0DTE options on the 5-minute chart.
// Targets swing-scalp trades with 5-25 minute hold times. The 5-minute chart
// produces ~78 RTH bars per session, so the signal engine is calibrated for
// higher conviction per signal with lower total signal count compared to the
// 1-minute variant.
//
// Uses a scoring engine (up to 10 equal-weight conditions) rather than the
// 1-minute's strict AND-gate. Each condition contributes +1; signals fire
// when the cumulative score meets a configurable minimum threshold. This
// allows partial confluence to still generate signals when enough independent
// conditions align. The indicator does not auto-trade.
//
// Key differences from 1-minute variant:
//   - Scoring engine replaces AND-gate logic
//   - EMA ribbon uses 9/15/21 (tighter fast/mid spacing)
//   - Multi-timeframe layer pulls 1-min RSI, EMA trend, and VWAP cross
//   - TICK integrated as scoring condition (requires >500 / <-500)
//   - Volume Footprint delta available as optional scoring condition (#10)
//   - Opening range default 15 minutes (3 bars)
//
// v1.1.0 CPU optimization:
//   - TICK request moved from "1" to chart TF (eliminates 5x LTF bar mult)
//   - max_bars_back reduced 5000 → 2000 (~25 sessions retained)
//
// Components:
//   - EMA ribbon (9/15/21) with dynamic trend cloud fill
//   - Session-anchored VWAP with standard deviation bands
//   - RSI momentum (scoring condition)
//   - ADX / DMI trend strength (scoring condition)
//   - ATR realized volatility
//   - TTM Squeeze detection (BB/KC compression, scoring condition)
//   - NYSE TICK Index breadth (6-tier, scoring condition #9)
//   - Volume Footprint via request.footprint() (dashboard + optional
//     scoring condition #10). Requires TradingView Premium/Ultimate.
//   - Multi-timeframe confirmation: 1-min RSI, EMA trend, VWAP cross
//   - Candle pattern detection (engulfing, strong close, hammer, shooting star)
//   - Key levels: Prior Day H/L/C, Pre-Market H/L, Opening Range, HOD/LOD
//   - Level proximity detection for scoring
//   - Regime classifier (BULLISH / BEARISH / RANGING / NO TRADE)
//   - VWAP cross markers (diamond shapes)
//   - Signal cooldown (configurable bar count)
//   - 21-row heads-up dashboard
//   - Dual alert system: 9 alertcondition + 4 dynamic alert() calls
// ============================================================================

// ============================================================================
// TradingView Pine Script v6
// ============================================================================
//@version=6
indicator(
     title     = "SPY 0DTE Scalper - 5m",
     shorttitle = "SPY-0-5m",
     overlay   = true,
     max_labels_count  = 500,
     max_lines_count   = 500,
     max_boxes_count   = 200,
     max_bars_back     = 2000
 )

// ============================================================================
// CONSTANTS
// ============================================================================
var string GP_EMA     = "EMA Ribbon"
var string GP_VWAP    = "VWAP"
var string GP_RSI     = "RSI"
var string GP_ADX     = "ADX"
var string GP_ATR     = "ATR"
var string GP_SQUEEZE = "TTM Squeeze"
var string GP_TICK    = "NYSE TICK"
var string GP_LEVELS  = "Key Levels"
var string GP_SESSION = "Session & Time"
var string GP_SIGNALS = "Signal Logic"
var string GP_MTF     = "Multi-Timeframe"
var string GP_DASH    = "Dashboard"

// Colors — vibrant, dark-theme optimized
var color C_VWAP         = #FFFFFF
var color C_VWAP_BAND    = #00BCD4
var color C_EMA_FAST     = #FFD600
var color C_EMA_MID      = #FF9100
var color C_EMA_SLOW     = #FF1744
var color C_CLOUD_BULL   = color.new(#00E676, 85)
var color C_CLOUD_BEAR   = color.new(#FF1744, 85)
var color C_CLOUD_MIXED  = color.new(#FFD600, 90)
var color C_PM_HIGH      = #FF5252
var color C_PM_LOW       = #69F0AE
var color C_PDH          = #FF6E40
var color C_PDL          = #64FFDA
var color C_PDC          = #B0BEC5
var color C_OR_HIGH      = #E040FB
var color C_OR_LOW       = #B388FF
var color C_CALLS        = #00E676
var color C_PUTS         = #FF1744
var color C_BG_CALLS     = color.new(#00E676, 92)
var color C_BG_PUTS      = color.new(#FF1744, 92)
var color C_DASH_BG      = color.new(#1A1A2E, 5)
var color C_DASH_HEADER  = #7C4DFF
var color C_DASH_TEXT     = #E0E0E0
var color C_GREEN        = #00E676
var color C_RED          = #FF1744
var color C_YELLOW       = #FFD600
var color C_ORANGE       = #FF9100
var color C_GRAY         = #616161
var color C_NEUTRAL      = #B0BEC5

// Footprint colors
var string GP_FOOTPRINT  = "Volume Footprint"
var color  C_FP_POC      = #E040FB
var color  C_FP_DELTA_P  = #00E676
var color  C_FP_DELTA_N  = #FF1744

// ============================================================================
// INPUTS
// ============================================================================

// --- EMA Ribbon ---
i_emaFastLen   = input.int(9,    "Fast EMA Length",         minval=2, group=GP_EMA)
i_emaMidLen    = input.int(15,   "Mid EMA Length",          minval=2, group=GP_EMA)
i_emaSlowLen   = input.int(21,   "Slow EMA Length",         minval=2, group=GP_EMA)
i_showEmaCloud = input.bool(true, "Show EMA Cloud Fill",    group=GP_EMA)

// --- VWAP ---
i_showVwap      = input.bool(true,  "Show VWAP",              group=GP_VWAP)
i_showVwapBands = input.bool(true,  "Show VWAP +/- 1 StdDev", group=GP_VWAP)
i_vwapBandMult  = input.float(1.0,  "VWAP Band Multiplier",   minval=0.5, maxval=3.0, step=0.25, group=GP_VWAP)

// --- RSI ---
i_rsiLen = input.int(10, "RSI Length",       minval=2, group=GP_RSI)
i_rsiOB  = input.int(65, "RSI Overbought",  minval=50, maxval=95, group=GP_RSI)
i_rsiOS  = input.int(35, "RSI Oversold",    minval=5,  maxval=50, group=GP_RSI)

// --- ADX ---
i_adxLen     = input.int(14, "ADX Length",                    minval=2, group=GP_ADX)
i_adxNoTrend = input.int(18, "ADX No-Trend Threshold",       minval=5,  maxval=30, group=GP_ADX)
i_adxTrend   = input.int(25, "ADX Trending Threshold",       minval=15, maxval=50, group=GP_ADX)
i_adxStrong  = input.int(40, "ADX Strong Trend Threshold",   minval=25, maxval=60, group=GP_ADX)

// --- ATR ---
i_atrLen = input.int(14, "ATR Length", minval=2, group=GP_ATR)

// --- TTM Squeeze ---
i_useSqueeze   = input.bool(true,  "Enable TTM Squeeze Detection",    group=GP_SQUEEZE,
                     tooltip="Detects Bollinger Band compression inside Keltner Channels — volatility squeeze precedes breakouts.")
i_sqzBBLen     = input.int(20,     "BB Length",                        minval=5, maxval=50, group=GP_SQUEEZE)
i_sqzBBMult    = input.float(2.0,  "BB Multiplier",                   minval=0.5, maxval=4.0, step=0.25, group=GP_SQUEEZE)
i_sqzKCLen     = input.int(20,     "KC Length",                        minval=5, maxval=50, group=GP_SQUEEZE)
i_sqzKCMult    = input.float(1.5,  "KC Multiplier",                   minval=0.5, maxval=4.0, step=0.25, group=GP_SQUEEZE)

// --- NYSE TICK ---
i_useTick      = input.bool(true,   "Enable NYSE TICK Index",          group=GP_TICK,
                     tooltip="Pulls NYSE TICK at chart timeframe for market breadth confirmation. Requires RTH data.")
i_tickSymbol   = input.string("USI:TICK", "TICK Symbol",               group=GP_TICK,
                     options=["USI:TICK", "USI:TICK.NQ"])
i_tickBull     = input.int(500,     "TICK Bullish Threshold",          minval=100, maxval=1000, step=50, group=GP_TICK)
i_tickBear     = input.int(-500,    "TICK Bearish Threshold",          minval=-1000, maxval=-100, step=50, group=GP_TICK)
i_tickExtreme  = input.int(800,     "TICK Extreme Threshold",          minval=500, maxval=1500, step=50, group=GP_TICK)

// --- Volume Footprint ---
i_useFootprint   = input.bool(true,   "Enable Volume Footprint",         group=GP_FOOTPRINT,
                     tooltip="Requests volume footprint data for the current bar. Requires TradingView Premium or Ultimate plan. Returns buy/sell volume, delta, POC, VAH, VAL. NOTE: Footprint data repaints by design — real-time values may differ slightly from historical recalculations.")
i_fpTicksPerRow  = input.int(25,      "Ticks Per Footprint Row",         minval=1, maxval=500, group=GP_FOOTPRINT,
                     tooltip="Number of price ticks per row. For SPY ($0.01 mintick), 25 = $0.25 per row. Lower = more granular but noisier.")
i_fpVaPct        = input.int(70,      "Value Area Percentage",           minval=50, maxval=95, step=5, group=GP_FOOTPRINT)
i_fpShowPOC      = input.bool(true,   "Show POC Level",                  group=GP_FOOTPRINT,
                     tooltip="Plots the Point of Control (highest-volume price row) as a step line on the chart.")
i_fpUseScoring   = input.bool(false,  "Use Delta in Scoring (#10)",      group=GP_FOOTPRINT,
                     tooltip="When ON, positive delta adds 1 point for CALLS, negative delta adds 1 point for PUTS. This is a REPAINTING condition — disabled by default for signal integrity. Max score becomes 10.")

// --- Key Levels ---
i_showPM     = input.bool(true, "Show Pre-Market High/Low",     group=GP_LEVELS)
i_showPD     = input.bool(true, "Show Prior Day H/L/C",         group=GP_LEVELS)
i_showOR     = input.bool(true, "Show Opening Range",            group=GP_LEVELS)
i_orMinutes  = input.int(15,    "Opening Range Minutes (rounded up to nearest 5-min bar)", minval=5, maxval=60, step=5, group=GP_LEVELS)
i_showHODLOD = input.bool(true, "Show Current Session HOD/LOD",  group=GP_LEVELS)

// --- Session ---
i_pmSession  = input.session("0400-0930", "Pre-Market Session",    group=GP_SESSION)
i_rthSession = input.session("0930-1600", "Regular Trading Hours",  group=GP_SESSION)
i_tz         = input.string("America/New_York", "Timezone",         group=GP_SESSION,
                 options=["America/New_York", "America/Chicago", "America/Los_Angeles", "UTC"])

// --- Signal Logic ---
i_showSignals   = input.bool(true,  "Show CALLS / PUTS Signals",         group=GP_SIGNALS)
i_cooldownBars  = input.int(3,      "Signal Cooldown (bars)",             minval=1, maxval=20, group=GP_SIGNALS)
i_signalStart   = input.session("0945-1545", "Signal Time Window",       group=GP_SIGNALS)
i_requireADX    = input.bool(true,  "Require ADX Above No-Trend",        group=GP_SIGNALS)
i_showVwapCross = input.bool(true,  "Show VWAP Cross Markers",           group=GP_SIGNALS)
i_vwapExtFilter = input.float(2.0,  "Max VWAP Distance (ATR multiples)", minval=0.5, maxval=5.0, step=0.25, group=GP_SIGNALS)
i_minScore      = input.int(4,      "Minimum Signal Score",              minval=2, maxval=10, group=GP_SIGNALS,
                     tooltip="Minimum conditions for signal. Core: 5. Enhancements: MTF trend (#6), level proximity (#7), squeeze fired (#8), TICK confirms (#9), FP delta (#10 opt-in). Max: 10 with footprint.")

// --- Multi-Timeframe ---
i_useMTF        = input.bool(true,  "Enable 1-Min MTF Confirmation",     group=GP_MTF,
                     tooltip="Pulls 1-minute RSI and EMA trend via request.security for an additional confirmation layer.")
i_mtfRsiLen     = input.int(14,     "1-Min RSI Length",                   minval=2, group=GP_MTF)
i_mtfEmaFast    = input.int(8,      "1-Min Fast EMA Length",             minval=2, group=GP_MTF)
i_mtfEmaSlow    = input.int(21,     "1-Min Slow EMA Length",             minval=2, group=GP_MTF)

// --- Dashboard ---
i_showDash = input.bool(true,     "Show Dashboard",      group=GP_DASH)
i_dashSize = input.string("small", "Dashboard Text Size", options=["tiny", "small", "normal", "large"], group=GP_DASH)

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

// Map dashboard size string to size constant
getDashSize(string s) =>
    switch s
        "tiny"  => size.tiny
        "small" => size.small
        "large" => size.large
        => size.normal

// Check if current bar falls within a given session
inSession(string sess) =>
    not na(time(timeframe.period, sess, i_tz))

// Format a float to 2 decimal places
f2(float val) =>
    str.tostring(val, "#.##")

// Return green for positive, red for negative
posnegColor(float val) =>
    val >= 0 ? C_GREEN : C_RED

// ============================================================================
// CORE INDICATOR CALCULATIONS
// ============================================================================

// --- EMA Ribbon ---
emaFast = ta.ema(close, i_emaFastLen)
emaMid  = ta.ema(close, i_emaMidLen)
emaSlow = ta.ema(close, i_emaSlowLen)

bool emaBullish = emaFast > emaMid and emaMid > emaSlow
bool emaBearish = emaFast < emaMid and emaMid < emaSlow

color emaCloudColor = emaBullish ? C_CLOUD_BULL : emaBearish ? C_CLOUD_BEAR : C_CLOUD_MIXED

// --- VWAP + Standard Deviation Bands ---
float vwapValue  = ta.vwap(hlc3)
float vwapCumVol = ta.cum(volume)
float vwapCumPV  = ta.cum(hlc3 * volume)
float vwapCumPV2 = ta.cum(hlc3 * hlc3 * volume)
float vwapMean   = vwapCumPV / vwapCumVol
float vwapVar    = math.max(vwapCumPV2 / vwapCumVol - vwapMean * vwapMean, 0)
float vwapStdev  = math.sqrt(vwapVar)
float vwapUpper  = vwapValue + vwapStdev * i_vwapBandMult
float vwapLower  = vwapValue - vwapStdev * i_vwapBandMult
float vwapDist   = close - vwapValue

// --- RSI ---
float rsiValue = ta.rsi(close, i_rsiLen)

getRsiLabel() =>
    if rsiValue > i_rsiOB
        "OVERBOUGHT"
    else if rsiValue < i_rsiOS
        "OVERSOLD"
    else
        "NEUTRAL"

getRsiColor() =>
    if rsiValue > i_rsiOB
        C_RED
    else if rsiValue < i_rsiOS
        C_GREEN
    else
        C_NEUTRAL

// --- ADX with DI+/DI- ---
[diPlus, diMinus, adxValue] = ta.dmi(i_adxLen, i_adxLen)

getAdxLabel() =>
    if adxValue >= i_adxStrong
        "STRONG TREND"
    else if adxValue >= i_adxTrend
        "TRENDING"
    else if adxValue >= i_adxNoTrend
        "WEAK/RANGING"
    else
        "NO TREND"

getAdxColor() =>
    if adxValue >= i_adxTrend
        C_GREEN
    else if adxValue >= i_adxNoTrend
        C_YELLOW
    else
        C_RED

// --- ATR ---
float atrValue = ta.atr(i_atrLen)

// ============================================================================
// TTM SQUEEZE DETECTION
// ============================================================================

// Bollinger Bands
float sqzBBBasis = ta.sma(close, i_sqzBBLen)
float sqzBBDev   = ta.stdev(close, i_sqzBBLen) * i_sqzBBMult
float sqzBBUpper = sqzBBBasis + sqzBBDev
float sqzBBLower = sqzBBBasis - sqzBBDev

// Keltner Channels
float sqzKCBasis = ta.ema(close, i_sqzKCLen)
float sqzKCRange = ta.atr(i_sqzKCLen) * i_sqzKCMult
float sqzKCUpper = sqzKCBasis + sqzKCRange
float sqzKCLower = sqzKCBasis - sqzKCRange

// Squeeze state: BB inside KC = squeeze ON (compression)
bool squeezeOn    = sqzBBUpper < sqzKCUpper and sqzBBLower > sqzKCLower
bool squeezeFired = not squeezeOn and squeezeOn[1]

// Track bars since last squeeze fired for recency context
var int sqzBarsSinceFire = 999
if squeezeFired
    sqzBarsSinceFire := 0
else
    sqzBarsSinceFire += 1

// Squeeze momentum histogram (linear regression of midline delta)
float sqzMomSource = close - math.avg(sqzBBBasis, sqzKCBasis)
float sqzMomentum  = ta.linreg(sqzMomSource, i_sqzBBLen, 0)

// ============================================================================
// NYSE TICK INDEX
// ============================================================================

// Pull TICK at chart timeframe to avoid LTF bar multiplication.
// On 5-min chart, this returns the TICK close at each 5-min bar boundary —
// functionally equivalent for threshold-based scoring.
float tickValue = i_useTick ? request.security(i_tickSymbol, timeframe.period, close, lookahead=barmerge.lookahead_off) : na

// ============================================================================
// VOLUME FOOTPRINT DATA
// ============================================================================
// Dashboard + optional scoring condition (#10).
// request.footprint() counts toward the 40-call request.*() budget.
// Total request.*() calls after tuple consolidation: 4
//   1x chart TF (TICK), 1x "1" (MTF tuple), 1x "D" (daily tuple), 1x footprint
// CPU optimization v1.1.0: TICK moved from "1" to chart TF, eliminating 5x
//   LTF bar multiplication. max_bars_back reduced 5000 → 2000.

footprint fpData = i_useFootprint ? request.footprint(i_fpTicksPerRow, i_fpVaPct) : na

// Bar-level metrics
float fpBuyVol   = not na(fpData) ? fpData.buy_volume()  : na
float fpSellVol  = not na(fpData) ? fpData.sell_volume()  : na
float fpDelta    = not na(fpData) ? fpData.delta()        : na

// POC midpoint price
float fpPocPrice = na
if not na(fpData)
    volume_row pocRow = fpData.poc()
    if not na(pocRow)
        fpPocPrice := (pocRow.up_price() + pocRow.down_price()) / 2.0

// Derived
float fpTotalVol = nz(fpBuyVol) + nz(fpSellVol)
float fpDeltaPct = fpTotalVol > 0 ? (fpDelta / fpTotalVol) * 100.0 : na
bool  fpAvailable = not na(fpData)

// Delta strength classification for dashboard
fpDeltaLabel() =>
    if na(fpDelta)
        "N/A"
    else if math.abs(nz(fpDeltaPct)) > 40
        fpDelta > 0 ? "STRONG BUY" : "STRONG SELL"
    else if math.abs(nz(fpDeltaPct)) > 20
        fpDelta > 0 ? "BUY LEAN" : "SELL LEAN"
    else
        "BALANCED"

fpDeltaColor() =>
    if na(fpDelta)
        C_GRAY
    else if math.abs(nz(fpDeltaPct)) > 40
        fpDelta > 0 ? C_FP_DELTA_P : C_FP_DELTA_N
    else if math.abs(nz(fpDeltaPct)) > 20
        fpDelta > 0 ? color.new(C_FP_DELTA_P, 30) : color.new(C_FP_DELTA_N, 30)
    else
        C_NEUTRAL

// ============================================================================
// MULTI-TIMEFRAME (1-MIN) DATA
// ============================================================================

// Consolidated into a single request.security() tuple — 1 pass through 1-min data instead of 5
// NOTE: mtfVwapCrossUp/Dn were previously pulled but never used in scoring, dashboard, or alerts — removed as dead code.
[_mtfRsi, _mtfEmaFastVal, _mtfEmaSlowVal] = request.security(
     syminfo.tickerid, "1",
     [ta.rsi(close, i_mtfRsiLen), ta.ema(close, i_mtfEmaFast), ta.ema(close, i_mtfEmaSlow)],
     lookahead=barmerge.lookahead_off)

float mtfRsi         = i_useMTF ? _mtfRsi         : na
float mtfEmaFastVal  = i_useMTF ? _mtfEmaFastVal  : na
float mtfEmaSlowVal  = i_useMTF ? _mtfEmaSlowVal  : na
bool  mtfBullish     = not na(mtfEmaFastVal) and mtfEmaFastVal > mtfEmaSlowVal
bool  mtfBearish     = not na(mtfEmaFastVal) and mtfEmaFastVal < mtfEmaSlowVal

getMtfTrendLabel() =>
    if mtfBullish
        "BULLISH"
    else if mtfBearish
        "BEARISH"
    else
        "MIXED"

getMtfTrendColor() =>
    if mtfBullish
        C_GREEN
    else if mtfBearish
        C_RED
    else
        C_YELLOW

getMtfRsiColor() =>
    if not na(mtfRsi) and mtfRsi > i_rsiOB
        C_RED
    else if not na(mtfRsi) and mtfRsi < i_rsiOS
        C_GREEN
    else
        C_NEUTRAL

getMtfRsiLabel() =>
    if not na(mtfRsi) and mtfRsi > i_rsiOB
        "OVERBOUGHT"
    else if not na(mtfRsi) and mtfRsi < i_rsiOS
        "OVERSOLD"
    else
        "NEUTRAL"

// ============================================================================
// SESSION DETECTION & KEY LEVELS
// ============================================================================

bool isRTH    = inSession(i_rthSession)
bool isPM     = inSession(i_pmSession)
bool rthStart = isRTH and not isRTH[1]

// --- Pre-Market High / Low ---
var float pmHigh = na
var float pmLow  = na

if isPM
    if na(pmHigh) or not isPM[1]
        pmHigh := high
        pmLow  := low
    else
        pmHigh := math.max(pmHigh, high)
        pmLow  := math.min(pmLow, low)

// --- Prior Day High / Low / Close + Current Day Open ---
// Consolidated into a single request.security() tuple — 1 pass through daily data instead of 4
[pdHigh, pdLow, pdClose, dayOpen] = request.security(
     syminfo.tickerid, "D",
     [high[1], low[1], close[1], open],
     lookahead=barmerge.lookahead_on)

// --- Opening Range ---
// On 5-min chart, each bar = 5 minutes. OR bar count = ceil(orMinutes / 5).
var float orHigh     = na
var float orLow      = na
var bool  orComplete = false
var int   orBars     = 0
int orBarsNeeded     = math.ceil(i_orMinutes / 5)

if rthStart
    orHigh     := high
    orLow      := low
    orComplete := false
    orBars     := 1
else if isRTH and not orComplete
    orBars += 1
    orHigh := math.max(orHigh, high)
    orLow  := math.min(orLow, low)
    if orBars >= orBarsNeeded
        orComplete := true

// --- Current Session HOD / LOD ---
var float sessionHigh = na
var float sessionLow  = na

if rthStart
    sessionHigh := high
    sessionLow  := low
else if isRTH
    sessionHigh := math.max(nz(sessionHigh, high), high)
    sessionLow  := math.min(nz(sessionLow, low), low)

// ============================================================================
// REGIME / MODE ENGINE
// ============================================================================

getMode() =>
    if adxValue < i_adxNoTrend and math.abs(vwapDist) < atrValue * 0.5
        "NO TRADE"
    else if adxValue < i_adxNoTrend
        "RANGING"
    else if adxValue >= i_adxNoTrend and emaBullish and close > vwapValue
        "BULLISH"
    else if adxValue >= i_adxNoTrend and emaBearish and close < vwapValue
        "BEARISH"
    else if adxValue >= i_adxTrend
        "TRANSITION"
    else
        "RANGING"

string currentMode = getMode()

getModeColor() =>
    switch currentMode
        "BULLISH"    => C_GREEN
        "BEARISH"    => C_RED
        "RANGING"    => C_YELLOW
        "TRANSITION" => C_ORANGE
        "NO TRADE"   => C_GRAY
        => C_NEUTRAL

// ============================================================================
// SIGNAL LOGIC
// ============================================================================

// --- Shared filter conditions ---
bool inSignalWindow = inSession(i_signalStart)
bool adxFilter      = not i_requireADX or adxValue >= i_adxNoTrend
bool vwapExtended   = math.abs(vwapDist) > atrValue * i_vwapExtFilter
bool barConfirmed   = barstate.isconfirmed

// --- Cooldown tracking ---
var int barsSinceSignal = 999
barsSinceSignal += 1

// --- Candle pattern detection (5-min optimized) ---

// Engulfing patterns (unchanged from 1-min)
bool bullishEngulfing = close > open and close > high[1] and open <= low[1]
bool bearishEngulfing = close < open and close < low[1]  and open >= high[1]

// Strong close — body > 55% of range (raised from 50% for 5-min)
bool strongBullClose  = close > open and (close - open) > (high - low) * 0.55
bool strongBearClose  = close < open and (open - close) > (high - low) * 0.55

// Hammer / Shooting star — tail > 1.8x body (relaxed from 2.0 for 5-min)
bool hammer           = close > open and (open - low)   > (close - open) * 1.8 and (high - close) < (close - open) * 0.5
bool shootingStar     = close < open and (high - open)  > (open - close) * 1.8 and (close - low)  < (open - close) * 0.5

// Inside bar breakout (NEW for 5-min) — prior bar is inside bar, current bar breaks out
bool priorInsideBar    = high[1] < high[2] and low[1] > low[2]
bool insideBarBreakUp  = priorInsideBar and close > high[1] and close > open
bool insideBarBreakDn  = priorInsideBar and close < low[1]  and close < open

// 3-bar momentum (NEW for 5-min) — 3 consecutive directional closes
bool threeBarMomentumUp = close > close[1] and close[1] > close[2] and close[2] > close[3]
bool threeBarMomentumDn = close < close[1] and close[1] < close[2] and close[2] < close[3]

// --- Level proximity detection (within 1.0 ATR) ---
bool nearSupport    = (not na(pmLow)  and math.abs(close - pmLow)  < atrValue * 1.0) or
                      (not na(orLow)  and orComplete and math.abs(close - orLow)  < atrValue * 1.0) or
                      (not na(pdLow)  and math.abs(close - pdLow)  < atrValue * 1.0) or
                      (not na(sessionLow) and math.abs(close - sessionLow) < atrValue * 1.0)

bool nearResistance = (not na(pmHigh) and math.abs(close - pmHigh) < atrValue * 1.0) or
                      (not na(orHigh) and orComplete and math.abs(close - orHigh) < atrValue * 1.0) or
                      (not na(pdHigh) and math.abs(close - pdHigh) < atrValue * 1.0) or
                      (not na(sessionHigh) and math.abs(close - sessionHigh) < atrValue * 1.0)

// --- CALLS (bullish entry) scoring ---
bool callsGate    = barConfirmed and inSignalWindow and adxFilter and not vwapExtended and barsSinceSignal >= i_cooldownBars

// Core conditions (1-5)
bool callsVwapOk  = close > vwapValue or (close > vwapValue - atrValue * 0.3 and close[1] < vwapValue)
bool callsEmaOk   = emaFast > emaSlow or (close > emaSlow and close[1] < emaSlow)
bool callsRsiOk   = rsiValue > i_rsiOS and rsiValue < i_rsiOB
bool callsCandle  = bullishEngulfing or strongBullClose or hammer or insideBarBreakUp or threeBarMomentumUp
bool callsAdxOk   = adxValue >= i_adxNoTrend

// Enhancement conditions (6-9, optional)
bool callsMtfOk   = i_useMTF and not na(mtfEmaFastVal) and mtfBullish
bool callsLevelOk = nearSupport
bool callsSqzOk   = i_useSqueeze and (squeezeFired or sqzBarsSinceFire <= 3)
bool callsTickOk  = i_useTick and not na(tickValue) and tickValue > i_tickBull

// Score calculation
int callsScore = 0
if callsVwapOk
    callsScore += 1
if callsEmaOk
    callsScore += 1
if callsRsiOk
    callsScore += 1
if callsCandle
    callsScore += 1
if callsAdxOk
    callsScore += 1
if callsMtfOk
    callsScore += 1
if callsLevelOk
    callsScore += 1
if callsSqzOk
    callsScore += 1
if callsTickOk
    callsScore += 1

// Enhancement #10: Footprint Delta alignment (optional, repainting)
bool callsFpDeltaOk = i_useFootprint and i_fpUseScoring and not na(fpDelta) and fpDelta > 0
if callsFpDeltaOk
    callsScore += 1

bool callsSignal = callsGate and callsScore >= i_minScore

// --- PUTS (bearish entry) scoring ---
bool putsGate    = barConfirmed and inSignalWindow and adxFilter and not vwapExtended and barsSinceSignal >= i_cooldownBars

// Core conditions (1-5)
bool putsVwapOk  = close < vwapValue or (close < vwapValue + atrValue * 0.3 and close[1] > vwapValue)
bool putsEmaOk   = emaFast < emaSlow or (close < emaSlow and close[1] > emaSlow)
bool putsRsiOk   = rsiValue > i_rsiOS and rsiValue < i_rsiOB
bool putsCandle  = bearishEngulfing or strongBearClose or shootingStar or insideBarBreakDn or threeBarMomentumDn
bool putsAdxOk   = adxValue >= i_adxNoTrend

// Enhancement conditions (6-9, optional)
bool putsMtfOk   = i_useMTF and not na(mtfEmaFastVal) and mtfBearish
bool putsLevelOk = nearResistance
bool putsSqzOk   = i_useSqueeze and (squeezeFired or sqzBarsSinceFire <= 3)
bool putsTickOk  = i_useTick and not na(tickValue) and tickValue < i_tickBear

// Score calculation
int putsScore = 0
if putsVwapOk
    putsScore += 1
if putsEmaOk
    putsScore += 1
if putsRsiOk
    putsScore += 1
if putsCandle
    putsScore += 1
if putsAdxOk
    putsScore += 1
if putsMtfOk
    putsScore += 1
if putsLevelOk
    putsScore += 1
if putsSqzOk
    putsScore += 1
if putsTickOk
    putsScore += 1

// Enhancement #10: Footprint Delta alignment (optional, repainting)
bool putsFpDeltaOk = i_useFootprint and i_fpUseScoring and not na(fpDelta) and fpDelta < 0
if putsFpDeltaOk
    putsScore += 1

bool putsSignal = putsGate and putsScore >= i_minScore

// Reset cooldown on signal fire
if callsSignal or putsSignal
    barsSinceSignal := 0

// --- VWAP cross detection ---
bool vwapCrossUp   = ta.crossover(close, vwapValue)
bool vwapCrossDown = ta.crossunder(close, vwapValue)

// ============================================================================
// PLOTS — EMA RIBBON
// ============================================================================

pFast = plot(emaFast, "EMA Fast", C_EMA_FAST, linewidth=1)
pMid  = plot(emaMid,  "EMA Mid",  C_EMA_MID,  linewidth=1)
pSlow = plot(emaSlow, "EMA Slow", C_EMA_SLOW, linewidth=1)

fill(pFast, pSlow, color=i_showEmaCloud ? emaCloudColor : color.new(color.white, 100), title="EMA Cloud")

// ============================================================================
// PLOTS — VWAP + BANDS
// ============================================================================

plot(i_showVwap     ? vwapValue : na, "VWAP",       C_VWAP,      linewidth=2)
plot(i_showVwapBands ? vwapUpper : na, "VWAP +1 SD", C_VWAP_BAND, linewidth=1, style=plot.style_circles)
plot(i_showVwapBands ? vwapLower : na, "VWAP -1 SD", C_VWAP_BAND, linewidth=1, style=plot.style_circles)

// --- Footprint POC Level ---
plot(i_useFootprint and i_fpShowPOC ? fpPocPrice : na,
     "Footprint POC", C_FP_POC, linewidth=2, style=plot.style_stepline)

// ============================================================================
// KEY LEVELS — HORIZONTAL LINES WITH LABELS
// ============================================================================

// --- Pre-Market High / Low ---
var line  pmHighLine = na
var line  pmLowLine  = na
var label pmHighLbl  = na
var label pmLowLbl   = na

if barstate.islast and i_showPM and not na(pmHigh) and isRTH
    if not na(pmHighLine)
        line.delete(pmHighLine)
        line.delete(pmLowLine)
        label.delete(pmHighLbl)
        label.delete(pmLowLbl)

    pmHighLine := line.new(bar_index - 60, pmHigh, bar_index + 5, pmHigh,
         color=C_PM_HIGH, style=line.style_dashed, width=1)
    pmLowLine  := line.new(bar_index - 60, pmLow, bar_index + 5, pmLow,
         color=C_PM_LOW, style=line.style_dashed, width=1)
    pmHighLbl  := label.new(bar_index + 7, pmHigh, "PM HIGH  " + f2(pmHigh),
         style=label.style_label_left, color=color.new(C_PM_HIGH, 80),
         textcolor=C_PM_HIGH, size=size.small)
    pmLowLbl   := label.new(bar_index + 7, pmLow, "PM LOW  " + f2(pmLow),
         style=label.style_label_left, color=color.new(C_PM_LOW, 80),
         textcolor=C_PM_LOW, size=size.small)

// --- Prior Day High / Low / Close ---
var line  pdhLine = na
var line  pdlLine = na
var line  pdcLine = na
var label pdhLbl  = na
var label pdlLbl  = na
var label pdcLbl  = na

if barstate.islast and i_showPD and not na(pdHigh)
    if not na(pdhLine)
        line.delete(pdhLine)
        line.delete(pdlLine)
        line.delete(pdcLine)
        label.delete(pdhLbl)
        label.delete(pdlLbl)
        label.delete(pdcLbl)

    pdhLine := line.new(bar_index - 60, pdHigh, bar_index + 5, pdHigh,
         color=C_PDH, style=line.style_dotted, width=1)
    pdlLine := line.new(bar_index - 60, pdLow, bar_index + 5, pdLow,
         color=C_PDL, style=line.style_dotted, width=1)
    pdcLine := line.new(bar_index - 60, pdClose, bar_index + 5, pdClose,
         color=C_PDC, style=line.style_dotted, width=1)
    pdhLbl  := label.new(bar_index + 7, pdHigh, "PDH  " + f2(pdHigh),
         style=label.style_label_left, color=color.new(C_PDH, 80),
         textcolor=C_PDH, size=size.small)
    pdlLbl  := label.new(bar_index + 7, pdLow, "PDL  " + f2(pdLow),
         style=label.style_label_left, color=color.new(C_PDL, 80),
         textcolor=C_PDL, size=size.small)
    pdcLbl  := label.new(bar_index + 7, pdClose, "PDC  " + f2(pdClose),
         style=label.style_label_left, color=color.new(C_PDC, 80),
         textcolor=C_PDC, size=size.small)

// --- Opening Range ---
var line  orHighLine = na
var line  orLowLine  = na
var label orHighLbl  = na
var label orLowLbl   = na

if barstate.islast and i_showOR and orComplete and not na(orHigh)
    if not na(orHighLine)
        line.delete(orHighLine)
        line.delete(orLowLine)
        label.delete(orHighLbl)
        label.delete(orLowLbl)

    orHighLine := line.new(bar_index - 60, orHigh, bar_index + 5, orHigh,
         color=C_OR_HIGH, style=line.style_dashed, width=1)
    orLowLine  := line.new(bar_index - 60, orLow, bar_index + 5, orLow,
         color=C_OR_LOW, style=line.style_dashed, width=1)
    orHighLbl  := label.new(bar_index + 7, orHigh, "OR HIGH  " + f2(orHigh),
         style=label.style_label_left, color=color.new(C_OR_HIGH, 80),
         textcolor=C_OR_HIGH, size=size.small)
    orLowLbl   := label.new(bar_index + 7, orLow, "OR LOW  " + f2(orLow),
         style=label.style_label_left, color=color.new(C_OR_LOW, 80),
         textcolor=C_OR_LOW, size=size.small)

// --- Current Session HOD / LOD ---
var line  hodLine = na
var line  lodLine = na
var label hodLbl  = na
var label lodLbl  = na

if barstate.islast and i_showHODLOD and isRTH and not na(sessionHigh)
    if not na(hodLine)
        line.delete(hodLine)
        line.delete(lodLine)
        label.delete(hodLbl)
        label.delete(lodLbl)

    hodLine := line.new(bar_index - 60, sessionHigh, bar_index + 5, sessionHigh,
         color=C_GREEN, style=line.style_solid, width=1)
    lodLine := line.new(bar_index - 60, sessionLow, bar_index + 5, sessionLow,
         color=C_RED, style=line.style_solid, width=1)
    hodLbl  := label.new(bar_index + 7, sessionHigh, "HOD  " + f2(sessionHigh),
         style=label.style_label_left, color=color.new(C_GREEN, 80),
         textcolor=C_GREEN, size=size.small)
    lodLbl  := label.new(bar_index + 7, sessionLow, "LOD  " + f2(sessionLow),
         style=label.style_label_left, color=color.new(C_RED, 80),
         textcolor=C_RED, size=size.small)

// ============================================================================
// SIGNAL VISUAL CUES
// ============================================================================

// Background highlight on signal bars
bgcolor(i_showSignals and callsSignal ? C_BG_CALLS : na, title="CALLS Background Flash")
bgcolor(i_showSignals and putsSignal  ? C_BG_PUTS  : na, title="PUTS Background Flash")

// Arrow shapes for fast scanning
plotshape(i_showSignals and callsSignal, title="CALLS Arrow", style=shape.triangleup,
     location=location.belowbar, color=C_CALLS, size=size.tiny)
plotshape(i_showSignals and putsSignal,  title="PUTS Arrow",  style=shape.triangledown,
     location=location.abovebar, color=C_PUTS,  size=size.tiny)

// Detailed text labels with hover tooltip
if i_showSignals and callsSignal
    string sqzTip  = i_useSqueeze ? "\nSqueeze: " + (squeezeOn ? "ON" : squeezeFired ? "FIRED" : "OFF") : ""
    string tickTip = i_useTick and not na(tickValue) ? "\nTICK: " + str.tostring(math.round(tickValue)) : ""
    string tip = "Score: " + str.tostring(callsScore) + "/" + str.tostring(i_minScore) + "+" +
         "\nRSI: " + f2(rsiValue) + "  |  ADX: " + f2(adxValue) +
         "\nVWAP Dist: " + f2(vwapDist) + "  |  Mode: " + currentMode +
         "\nATR: " + f2(atrValue) + "  |  EMA: " + (emaBullish ? "BULL" : emaBearish ? "BEAR" : "MIXED") +
         (i_useMTF ? "\n1m Trend: " + getMtfTrendLabel() + "  |  1m RSI: " + f2(nz(mtfRsi)) : "") +
         sqzTip + tickTip
    label.new(bar_index, low, "CALLS",
         style=label.style_label_up, color=C_CALLS,
         textcolor=color.black, size=size.small, tooltip=tip)

if i_showSignals and putsSignal
    string sqzTip  = i_useSqueeze ? "\nSqueeze: " + (squeezeOn ? "ON" : squeezeFired ? "FIRED" : "OFF") : ""
    string tickTip = i_useTick and not na(tickValue) ? "\nTICK: " + str.tostring(math.round(tickValue)) : ""
    string tip = "Score: " + str.tostring(putsScore) + "/" + str.tostring(i_minScore) + "+" +
         "\nRSI: " + f2(rsiValue) + "  |  ADX: " + f2(adxValue) +
         "\nVWAP Dist: " + f2(vwapDist) + "  |  Mode: " + currentMode +
         "\nATR: " + f2(atrValue) + "  |  EMA: " + (emaBullish ? "BULL" : emaBearish ? "BEAR" : "MIXED") +
         (i_useMTF ? "\n1m Trend: " + getMtfTrendLabel() + "  |  1m RSI: " + f2(nz(mtfRsi)) : "") +
         sqzTip + tickTip
    label.new(bar_index, high, "PUTS",
         style=label.style_label_down, color=C_PUTS,
         textcolor=color.white, size=size.small, tooltip=tip)

// VWAP cross markers
plotshape(i_showVwapCross and vwapCrossUp   and barConfirmed, title="VWAP Cross Bullish",
     style=shape.diamond, location=location.belowbar, color=#76FF03, size=size.tiny)
plotshape(i_showVwapCross and vwapCrossDown and barConfirmed, title="VWAP Cross Bearish",
     style=shape.diamond, location=location.abovebar, color=C_PUTS,  size=size.tiny)

// ============================================================================
// DASHBOARD TABLE
// ============================================================================

var table dash = table.new(position.bottom_right, 3, 21,
     bgcolor    = C_DASH_BG,
     border_width = 0,
     border_color = color.new(#FFFFFF, 85),
     frame_width  = 1,
     frame_color  = color.new(#FFFFFF, 80))

// dayOpen already fetched in the consolidated daily tuple above
float avgVol  = ta.sma(volume, 20)

if barstate.islast and i_showDash
    string sz = i_dashSize

    cellSize = getDashSize(sz)

    // --- Row 0: Header ---
    table.cell(dash, 0, 0, "  SPY 0DTE Scalper - 5m  ", text_color=#FFFFFF,
         bgcolor=C_DASH_HEADER, text_size=cellSize, text_halign=text.align_center)
    table.cell(dash, 1, 0, "", bgcolor=C_DASH_HEADER, text_size=cellSize)
    table.cell(dash, 2, 0, "", bgcolor=C_DASH_HEADER, text_size=cellSize)
    table.merge_cells(dash, 0, 0, 2, 0)

    // --- Row 1: Price ---
    color priceClr = close >= open ? C_GREEN : C_RED
    string dayType = close >= open ? "Green Day" : "Red Day"
    table.cell(dash, 0, 1, "Price",    text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 1, f2(close),  text_color=priceClr,    text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 1, dayType,    text_color=priceClr,    text_size=cellSize, text_halign=text.align_right)

    // --- Row 2: Mode ---
    string mAdvice = currentMode == "NO TRADE" ? "AVOID" : currentMode == "RANGING" ? "CAUTION" : "ACTIVE"
    table.cell(dash, 0, 2, "Mode",        text_color=C_DASH_TEXT,  text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 2, currentMode,   text_color=getModeColor(), text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 2, mAdvice,       text_color=getModeColor(), text_size=cellSize, text_halign=text.align_right)

    // --- Row 3: RSI ---
    table.cell(dash, 0, 3, "RSI (" + str.tostring(i_rsiLen) + ")",
         text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 3, f2(rsiValue),  text_color=getRsiColor(), text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 3, getRsiLabel(),  text_color=getRsiColor(), text_size=cellSize, text_halign=text.align_right)

    // --- Row 4: ADX ---
    table.cell(dash, 0, 4, "ADX",            text_color=C_DASH_TEXT,  text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 4, f2(adxValue),     text_color=getAdxColor(), text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 4, getAdxLabel(),    text_color=getAdxColor(), text_size=cellSize, text_halign=text.align_right)

    // --- Row 5: VWAP Distance ---
    float absVD  = math.abs(vwapDist)
    color vdClr  = absVD > atrValue ? C_RED : absVD > atrValue * 0.5 ? C_YELLOW : C_GREEN
    string vdLbl = absVD > atrValue ? "EXTENDED" : "NEAR VWAP"
    table.cell(dash, 0, 5, "VWAP Dist",   text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 5, f2(vwapDist),  text_color=vdClr,       text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 5, vdLbl,         text_color=vdClr,       text_size=cellSize, text_halign=text.align_right)

    // --- Row 6: PM High ---
    if not na(pmHigh)
        float pmhD   = close - pmHigh
        string pmhLbl = close < pmHigh ? "RESISTANCE" : "CLEARED"
        color pmhClr  = close < pmHigh ? C_RED : C_GREEN
        table.cell(dash, 0, 6, "PM High",   text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 6, f2(pmhD),    text_color=pmhClr,     text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 6, pmhLbl,      text_color=pmhClr,     text_size=cellSize, text_halign=text.align_right)
    else
        table.cell(dash, 0, 6, "PM High", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 6, "N/A",     text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 6, "",         text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)

    // --- Row 7: PM Low ---
    if not na(pmLow)
        float pmlD   = close - pmLow
        string pmlLbl = close > pmLow ? "SUPPORT" : "BROKEN"
        color pmlClr  = close > pmLow ? C_GREEN : C_RED
        table.cell(dash, 0, 7, "PM Low",    text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 7, f2(pmlD),    text_color=pmlClr,     text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 7, pmlLbl,      text_color=pmlClr,     text_size=cellSize, text_halign=text.align_right)
    else
        table.cell(dash, 0, 7, "PM Low",  text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 7, "N/A",     text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 7, "",         text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)

    // --- Row 8: ATR ---
    table.cell(dash, 0, 8, "ATR (" + str.tostring(i_atrLen) + ")",
         text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 8, f2(atrValue),  text_color=C_NEUTRAL, text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 8, "",            text_size=cellSize)

    // --- Row 9: EMA Trend ---
    string emaTrStr = emaBullish ? "BULLISH" : emaBearish ? "BEARISH" : "MIXED"
    color emaTrClr  = emaBullish ? C_GREEN : emaBearish ? C_RED : C_YELLOW
    table.cell(dash, 0, 9, "EMA Trend",   text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 9, emaTrStr,      text_color=emaTrClr,    text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 9, "",            text_size=cellSize)

    // --- Row 10: Last Signal ---
    string sigStr  = callsSignal ? "CALLS" : putsSignal ? "PUTS" : "—"
    color sigClr   = callsSignal ? C_GREEN : putsSignal ? C_RED : C_GRAY
    string sigScore = callsSignal ? str.tostring(callsScore) + "/9" : putsSignal ? str.tostring(putsScore) + "/9" : ""
    table.cell(dash, 0, 10, "Signal",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 10, sigStr,    text_color=sigClr,      text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 10, sigScore,  text_color=sigClr,      text_size=cellSize, text_halign=text.align_right)

    // --- Row 11: Day Change % ---
    float dayChg  = not na(dayOpen) and dayOpen != 0 ? ((close - dayOpen) / dayOpen) * 100 : 0.0
    string dcStr  = (dayChg >= 0 ? "+" : "") + f2(dayChg) + "%"
    table.cell(dash, 0, 11, "Day Chg",  text_color=C_DASH_TEXT,         text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 11, dcStr,      text_color=posnegColor(dayChg), text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 11, "",         text_size=cellSize)

    // --- Row 12: Relative Volume ---
    float rVol    = avgVol > 0 ? volume / avgVol : 0.0
    string rvStr  = f2(rVol) + "x"
    color rvClr   = rVol > 1.5 ? C_GREEN : rVol > 1.0 ? C_YELLOW : C_GRAY
    string rvLbl  = rVol > 1.5 ? "SPIKE" : rVol > 1.0 ? "ABOVE AVG" : "BELOW AVG"
    table.cell(dash, 0, 12, "Rel Vol",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 12, rvStr,      text_color=rvClr,       text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 12, rvLbl,      text_color=rvClr,       text_size=cellSize, text_halign=text.align_right)

    // --- Row 13: DI+/DI- Spread ---
    float diSpread  = diPlus - diMinus
    string diStr    = "+" + f2(diPlus) + " / -" + f2(diMinus)
    color diClr     = diSpread > 0 ? C_GREEN : C_RED
    string diLbl    = diSpread > 0 ? "BULLS" : "BEARS"
    table.cell(dash, 0, 13, "DI Spread", text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
    table.cell(dash, 1, 13, diStr,       text_color=diClr,       text_size=cellSize, text_halign=text.align_right)
    table.cell(dash, 2, 13, diLbl,       text_color=diClr,       text_size=cellSize, text_halign=text.align_right)

    // --- Row 14: 1m Trend (MTF) ---
    if i_useMTF
        table.cell(dash, 0, 14, "1m Trend",          text_color=C_DASH_TEXT,       text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 14, getMtfTrendLabel(),   text_color=getMtfTrendColor(), text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 14, "",                   text_size=cellSize)
    else
        table.cell(dash, 0, 14, "1m Trend", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 14, "OFF",      text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 14, "",          text_size=cellSize)

    // --- Row 15: 1m RSI (MTF) ---
    if i_useMTF and not na(mtfRsi)
        table.cell(dash, 0, 15, "1m RSI",            text_color=C_DASH_TEXT,   text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 15, f2(mtfRsi),          text_color=getMtfRsiColor(), text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 15, getMtfRsiLabel(),     text_color=getMtfRsiColor(), text_size=cellSize, text_halign=text.align_right)
    else
        table.cell(dash, 0, 15, "1m RSI",  text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 15, i_useMTF ? "N/A" : "OFF", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 15, "",         text_size=cellSize)

    // --- Row 16: TTM Squeeze ---
    if i_useSqueeze
        string sqzStr = squeezeOn ? "ON" : squeezeFired ? "FIRED" : "OFF"
        color sqzClr  = squeezeOn ? C_ORANGE : squeezeFired ? C_GREEN : sqzBarsSinceFire <= 3 ? C_GREEN : C_GRAY
        string sqzLbl = squeezeOn ? "BUILDING" : squeezeFired ? "BREAKOUT" : sqzBarsSinceFire <= 3 ? str.tostring(sqzBarsSinceFire) + " bars ago" : "NORMAL"
        table.cell(dash, 0, 16, "Squeeze",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 16, sqzStr,     text_color=sqzClr,      text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 16, sqzLbl,     text_color=sqzClr,      text_size=cellSize, text_halign=text.align_right)
    else
        table.cell(dash, 0, 16, "Squeeze",  text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 16, "OFF",      text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 16, "",          text_size=cellSize)

    // --- Row 17: NYSE TICK ---
    if i_useTick and not na(tickValue)
        float tv     = tickValue
        int   tvRnd  = math.round(tv)
        string tvStr = str.tostring(tvRnd)
        color tvClr  = tv > i_tickExtreme ? C_GREEN : tv > i_tickBull ? C_GREEN : tv > 0 ? C_NEUTRAL : tv < -i_tickExtreme ? C_RED : tv < i_tickBear ? C_RED : tv < 0 ? C_NEUTRAL : C_GRAY
        string tvLbl = tv > i_tickExtreme ? "EXTREME BULL" : tv > i_tickBull ? "BULLISH" : tv > 0 ? "MILD BULL" : tv < -i_tickExtreme ? "EXTREME BEAR" : tv < i_tickBear ? "BEARISH" : tv < 0 ? "MILD BEAR" : "FLAT"
        table.cell(dash, 0, 17, "TICK",    text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 17, tvStr,     text_color=tvClr,       text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 17, tvLbl,     text_color=tvClr,       text_size=cellSize, text_halign=text.align_right)
    else
        table.cell(dash, 0, 17, "TICK",    text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 17, i_useTick ? "N/A" : "OFF", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 17, "",         text_size=cellSize)

    // --- Row 18: Footprint Status ---
    if i_useFootprint
        string fpStat = fpAvailable ? "ACTIVE" + (i_fpUseScoring ? " | #10" : " | INFO") : "N/A"
        table.cell(dash, 0, 18, "FP Status", text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 18, fpStat,      text_color=fpAvailable ? C_GREEN : C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 18, "",           text_size=cellSize)
    else
        table.cell(dash, 0, 18, "FP Status", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 18, "OFF",       text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 18, "",           text_size=cellSize)

    // --- Row 19: Footprint Delta ---
    if i_useFootprint and fpAvailable
        string fpDStr = fpDeltaLabel() + " | " + str.tostring(math.round(nz(fpDelta))) + " (" + f2(nz(fpDeltaPct)) + "%)"
        table.cell(dash, 0, 19, "FP Delta",  text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 19, fpDStr,      text_color=fpDeltaColor(), text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 19, "",           text_size=cellSize)
    else
        table.cell(dash, 0, 19, "FP Delta",  text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 19, i_useFootprint ? "N/A" : "OFF", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 19, "",           text_size=cellSize)

    // --- Row 20: Footprint POC ---
    if i_useFootprint and not na(fpPocPrice)
        table.cell(dash, 0, 20, "FP POC",    text_color=C_DASH_TEXT, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 20, f2(fpPocPrice), text_color=C_FP_POC, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 20, "",           text_size=cellSize)
    else
        table.cell(dash, 0, 20, "FP POC",    text_color=C_GRAY, text_size=cellSize, text_halign=text.align_left)
        table.cell(dash, 1, 20, i_useFootprint ? "N/A" : "OFF", text_color=C_GRAY, text_size=cellSize, text_halign=text.align_right)
        table.cell(dash, 2, 20, "",           text_size=cellSize)

// ============================================================================
// ALERTS — STANDARD (alertcondition) + DYNAMIC (alert)
// ============================================================================

// alertcondition() — appears in TradingView "Create Alert" dropdown
alertcondition(callsSignal, title="0DTE-5m: CALLS Signal",
     message="CALLS signal fired on SPY (5m) — bullish confluence detected")
alertcondition(putsSignal,  title="0DTE-5m: PUTS Signal",
     message="PUTS signal fired on SPY (5m) — bearish confluence detected")
alertcondition(vwapCrossUp, title="0DTE-5m: VWAP Cross Bullish",
     message="SPY price crossed above VWAP (5m)")
alertcondition(vwapCrossDown, title="0DTE-5m: VWAP Cross Bearish",
     message="SPY price crossed below VWAP (5m)")
alertcondition(currentMode != currentMode[1], title="0DTE-5m: Regime Mode Change",
     message="SPY regime mode has changed (5m)")
alertcondition(i_useSqueeze and squeezeFired, title="0DTE-5m: Squeeze Fired",
     message="TTM Squeeze has fired (5m) — volatility expansion detected")

// alert() — dynamic messages with interpolated context
if callsSignal
    string sqzCtx  = i_useSqueeze ? " | Sqz: " + (squeezeOn ? "ON" : "OFF") : ""
    string tickCtx = i_useTick and not na(tickValue) ? " | TICK: " + str.tostring(math.round(tickValue)) : ""
    string fpCtx   = i_useFootprint and not na(fpDelta) ? " | FP: " + fpDeltaLabel() + " (d:" + str.tostring(math.round(fpDelta)) + ")" + (not na(fpPocPrice) ? " POC:" + f2(fpPocPrice) : "") : ""
    int maxScoreDisplay = 9 + (i_useFootprint and i_fpUseScoring ? 1 : 0)
    alert("0DTE-5m CALLS | SPY " + f2(close) +
         " | Score " + str.tostring(callsScore) + "/" + str.tostring(maxScoreDisplay) +
         " | RSI " + f2(rsiValue) +
         " | ADX " + f2(adxValue) +
         " | VWAP Dist " + f2(vwapDist) +
         " | Mode: " + currentMode +
         (i_useMTF ? " | 1m: " + getMtfTrendLabel() : "") +
         sqzCtx + tickCtx + fpCtx,
         alert.freq_once_per_bar_close)

if putsSignal
    string sqzCtx  = i_useSqueeze ? " | Sqz: " + (squeezeOn ? "ON" : "OFF") : ""
    string tickCtx = i_useTick and not na(tickValue) ? " | TICK: " + str.tostring(math.round(tickValue)) : ""
    string fpCtx   = i_useFootprint and not na(fpDelta) ? " | FP: " + fpDeltaLabel() + " (d:" + str.tostring(math.round(fpDelta)) + ")" + (not na(fpPocPrice) ? " POC:" + f2(fpPocPrice) : "") : ""
    int maxScoreDisplay = 9 + (i_useFootprint and i_fpUseScoring ? 1 : 0)
    alert("0DTE-5m PUTS | SPY " + f2(close) +
         " | Score " + str.tostring(putsScore) + "/" + str.tostring(maxScoreDisplay) +
         " | RSI " + f2(rsiValue) +
         " | ADX " + f2(adxValue) +
         " | VWAP Dist " + f2(vwapDist) +
         " | Mode: " + currentMode +
         (i_useMTF ? " | 1m: " + getMtfTrendLabel() : "") +
         sqzCtx + tickCtx + fpCtx,
         alert.freq_once_per_bar_close)

if currentMode != currentMode[1]
    alert("0DTE-5m MODE SHIFT | " + str.tostring(currentMode[1]) + " -> " + currentMode +
         " | SPY " + f2(close),
         alert.freq_once_per_bar)

if i_useSqueeze and squeezeFired
    alert("0DTE-5m SQUEEZE FIRED | SPY " + f2(close) +
         " | Momentum: " + (sqzMomentum > 0 ? "BULLISH" : "BEARISH") +
         " | ADX " + f2(adxValue) +
         " | Mode: " + currentMode,
         alert.freq_once_per_bar_close)

// Footprint delta alert conditions
alertcondition(
     i_useFootprint and not na(fpDelta) and fpDelta > 0 and nz(fpDeltaPct) > 30,
     title="0DTE-5m: Strong Positive FP Delta (>30%)",
     message="SPY 5m: Strong positive footprint delta — aggressive buying dominant")
alertcondition(
     i_useFootprint and not na(fpDelta) and fpDelta < 0 and nz(fpDeltaPct) < -30,
     title="0DTE-5m: Strong Negative FP Delta (>30%)",
     message="SPY 5m: Strong negative footprint delta — aggressive selling dominant")

// ============================================================================
// END OF SCRIPT
// ============================================================================

Overview

SPY 0DTE Scalper - 5m plots a 9/15/21 EMA ribbon with optional cloud fill, session-anchored VWAP and standard-deviation bands, and key levels such as prior day, pre-market, opening range, and session high and low. RSI, ADX, ATR, TTM squeeze, NYSE TICK, optional volume footprint, one-minute confirmation, and candle patterns feed the score. A regime label and dashboard summarize bias. It does not place trades or set exits.

Details

Published by

chris-c-thomas on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Scalping, Moving averages, Oscillators & momentum, Support & resistance

Markets

Stocks

Timeframes

M1-M5

Risk Notes

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

Topics

financeoptionspinescriptpinescript-indicatorspinescript-strategiestradingtradingvewtradingview-chart-analysischarting

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Fast EMA Lengthint9i_emaFastLen
Mid EMA Lengthint15i_emaMidLen
Slow EMA Lengthint21i_emaSlowLen
Show EMA Cloud Fillbooltruei_showEmaCloud
Show VWAPbooltruei_showVwap
Show VWAP +/- 1 StdDevbooltruei_showVwapBands
VWAP Band Multiplierfloat1.0i_vwapBandMult
RSI Lengthint10i_rsiLen
RSI Overboughtint65i_rsiOB
RSI Oversoldint35i_rsiOS
ADX Lengthint14i_adxLen
ADX No-Trend Thresholdint18i_adxNoTrend
Show all 40 settings
SettingTypeDefaultVariable
ADX Trending Thresholdint25i_adxTrend
ADX Strong Trend Thresholdint40i_adxStrong
ATR Lengthint14i_atrLen
Enable TTM Squeeze Detectionbooltruei_useSqueeze
BB Lengthint20i_sqzBBLen
BB Multiplierfloat2.0i_sqzBBMult
KC Lengthint20i_sqzKCLen
KC Multiplierfloat1.5i_sqzKCMult
Enable NYSE TICK Indexbooltruei_useTick
TICK SymbolstringUSI:TICKi_tickSymbol
TICK Bullish Thresholdint500i_tickBull
TICK Bearish Thresholdint-500i_tickBear
TICK Extreme Thresholdint800i_tickExtreme
Enable Volume Footprintbooltruei_useFootprint
Ticks Per Footprint Rowint25i_fpTicksPerRow
Value Area Percentageint70i_fpVaPct
Show POC Levelbooltruei_fpShowPOC
Use Delta in Scoring (#10)boolfalsei_fpUseScoring
Show Pre-Market High/Lowbooltruei_showPM
Show Prior Day H/L/Cbooltruei_showPD
Show Opening Rangebooltruei_showOR
Opening Range Minutes (rounded up to nearest 5-min bar)int15i_orMinutes
Show Current Session HOD/LODbooltruei_showHODLOD
Pre-Market Sessionsession0400-0930i_pmSession
Regular Trading Hourssession0930-1600i_rthSession
TimezonestringAmerica/New_Yorki_tz
Show CALLS / PUTS Signalsbooltruei_showSignals
Signal Cooldown (bars)int3i_cooldownBars
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

This indicator scans NSE-listed stocks for momentum signals using Supertrend, multiple EMAs, MACD, and stochastic oscillator. It supports continuous or flip signal modes and displays matching setups in a customizable table at the bottom left of the chart. The

Indicator v5 Oscillators & momentumMoving averages
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup: Vijay Thakare Option Buying Scalping Setup Scanner

PivotHighLow

by Alorse
330 stars

This indicator calculates pivot high and pivot low points on the price chart based on configurable left and right bar counts. Confirmed pivot highs and lows are plotted with negative bar offsets directly on the price chart using line and cross styles. Addition

Indicator v4 Support & resistanceOscillators & momentum Repaints
Updated 2 months ago · None stated GitHub repository of PivotHighLow

Hedge Scalp

by nikita-doronin
30 stars

The Hedge Scalp indicator calculates an Exponential Moving Average (EMA) and generates four long grid levels and one hedged short grid level at user-defined percentage distances below the EMA. It models grid entry execution and average entry take-profit levels

Indicator v5 Moving averagesSupport & resistance
Updated 2 years ago · Custom licence GitHub repository of Hedge Scalp
23 stars

This indicator collection calculates and plots previous daily high and low price levels on the chart to identify potential supply and demand zones. It includes real-time intraday high and low calculations, RSI divergence detection across a customizable period

Indicator v4 Support & resistanceDivergence
Updated 3 years ago · None stated GitHub repository of Daily High and Low Strategy

BTE Acc AO SMIO

by f-cksociety
11 stars

This script provides an indicator-based backtesting engine framework that simulates trade entries, exits, and filtering directly on the price chart. It evaluates selected entry rules and multi-layered filters, including moving average trends, volume increases,

Indicator v5 TrendOscillators & momentum
Updated 4 years ago · MPL-2.0 GitHub repository of BTE Acc AO SMIO

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers