Luxy BIG beautiful Dynamic ORB - v5

orenluxy · study · 2387 行 · 点赞 6,230 · TradingView 原页

本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。

Pine Script

//@version=6
indicator("Luxy BIG beautiful Dynamic ORB", shorttitle = "Luxy Big Beautiful ORB", overlay=true, max_labels_count=500, max_lines_count=500, max_boxes_count=300)
 
// ===================================================================================================
// ====================================== DISCLAIMER & INFO =========================================
// ===================================================================================================
//
// DISCLAIMER: This indicator is for educational and informational purposes only.
// It does not constitute financial advice. Trading involves substantial risk of loss.
// Past performance does not guarantee future results. Always do your own research.
//
// DESCRIPTION:
// Dynamic Opening Range Breakout (ORB) indicator with multi-stage ranges (5/15/30/60 min).
// Identifies breakouts, retests, and failed breaks with advanced filtering options.
// Includes position sizing, risk management, and higher timeframe bias analysis.
//
// FEATURES:
// • Multi-stage ORB levels (5, 15, 30, 60 minutes)
// • Breakout and retest detection with cycle counting
// • Volume, trend, and HTF bias filters
// • Position sizing with risk management
// • Target/stop-loss visualization
// • Real-time dashboard with trade parameters
//
// BEST PRACTICES:
// • Recommended for liquid stocks with >1M daily volume
// • Best on 5-minute charts during regular trading hours
// • Paper trade first to understand the signals
// • Use with proper risk management (1-2% risk per trade)
// • Combine with overall market analysis
//
// LIMITATIONS:
// • May produce false signals in choppy/ranging markets
// • Requires clean data feed for accurate ORB calculation
// • Not suitable for all market conditions
// • Performance varies by instrument and timeframe
//
// CREDITS:
// • Based on Opening Range Breakout concepts (Toby Crabel, Mark Fisher and more)
// • Enhanced with modern technical analysis techniques
// • Original implementation by ORENLuxy
//
// VERSION: 3.0
// AUTHOR: ORENLuxy
// LAST UPDATED: October 2025
//
// ===================================================================================================
// ================================================ GROUPS ===========================================
// ===================================================================================================
 
grp_colors   = "🎨 ORB COLORS ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_session  = "⏰ SESSION SETTINGS ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_breakout = "⚡ BREAKOUT DETECTION ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_targets  = "🎯 TARGETS & RISK ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_lines    = "📍 TP/SL LINES ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_pullback = "↩️ PULLBACK FILTER ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_volume   = "📊 VOLUME FILTER ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_trend    = "📈 TREND FILTER ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_dash     = "📊 DASHBOARD ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_htf      = "📈 HIGHER TIMEFRAME ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
grp_alerts   = "🔔 ALERTS ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
 
// ===================================================================================================
// ============================================ CONSTANTS ============================================
// ===================================================================================================
 
// Label positioning offsets (as percentage multipliers)
LABEL_OFFSET_ABOVE_PRICE = 1.012
LABEL_OFFSET_BELOW_PRICE = 0.988 
LABEL_OFFSET_BARS = 2      
 
// Failed breakout detection window
MAX_BARS_FAILED_BREAK = 5  // Maximum bars to consider a retest as "failed break" instead of committed breakout
 
// ORB Core
showEdgeLabels = input.bool(true, "Show Edge Labels", inline = "core", group=grp_colors, tooltip="Display ORB stage labels at the edge of price levels\n\nShows which ORB period is active (5min/15min/30min/60min)\nLabels follow price as chart scrolls\n\nON = Labels visible\nOFF = Clean chart")
showBG = input.bool(true, "Background", group=grp_colors, inline = "core")
fillTransparency = input.int(95, "Transparency", inline="core", minval=0, maxval=100, step=5, group=grp_colors, tooltip="Background fill transparency for ORB range\n\n0 = Fully opaque (solid color)\n50 = Semi-transparent\n95 = Nearly invisible (recommended)\n100 = Completely invisible")
enableORB5Signals = input.bool(true, "Enable ORB 5", group=grp_colors, inline="5-15", tooltip="Enable/Disable ORB stages\n\nORB 5: First 5 minutes - fastest signals, most noise\nORB 15: First 15 minutes - balanced signals\nORB 30: First 30 minutes - slower, more reliable\nORB 60: First 60 minutes - slowest, most confirmed\n\nDisable stages you don't want to trade")
orb5LineColor = input.color(color.green, "", inline = "5-15",  group=grp_colors)
enableORB15Signals = input.bool(true, "Enable ORB15", group=grp_colors, inline="5-15")
orb15LineColor = input.color(color.yellow, "", inline = "5-15",group=grp_colors)
enableORB30Signals = input.bool(true, "Enable ORB30", group=grp_colors, inline="30-60")
orb30LineColor = input.color(color.blue, "", inline = "30-60", group=grp_colors)
enableORB60Signals = input.bool(true, "Enable ORB60", group=grp_colors, inline="30-60")
orb60LineColor = input.color(color.purple, "", inline = "30-60", group=grp_colors)
showMidLine = input.bool(true, "Mid-Range Line", group=grp_colors, tooltip="Display a thin line at the midpoint between ORB High and Low\n\nUSEFUL FOR:\n• Visual center reference\n• Mean reversion trading\n• Support/resistance level\n• Range symmetry analysis\n\nThe line uses the same color as the ORB boundaries but thinner\n\nON = Midpoint line visible\nOFF = Only High/Low lines")
 
sessionMode = input.string("Auto-Detect", "Session Mode", options=["Auto-Detect", "New-York", "London", "Tokyo", "Sydney", "Frankfurt", "Custom"], group=grp_session, tooltip="Define trading session for ORB calculation\n\nAUTO-DETECT: Uses symbol's native hours (recommended)\n  • Stocks: Uses exchange hours automatically\n  • Crypto: 24/7\n\nNEW YORK: 09:30-16:00 EST (NYSE/NASDAQ)\nLONDON: 08:00-16:30 GMT (LSE)\nTOKYO: 09:00-15:00 JST (TSE)\nSYDNEY: 10:00-16:00 AEST (ASX)\nFRANKFURT: 09:00-17:30 CET (XETRA)\nCUSTOM: Define your own session\n\nORB resets at session start")
customSession = input.session("0930-1600", "Custom Session Hours", group=grp_session, tooltip="Custom trading session definition\n\nONLY ACTIVE when Session Mode = Custom\n\nFORMAT: HHMM-HHMM (24-hour)\n\nEXAMPLES:\n• 0930-1600 = NYSE regular hours\n• 0400-2000 = Extended trading\n• 0000-2359 = Full 24 hours\n\nTIMEZONE: Chart timezone")
 
// Define session string with proper format: "HHMM-HHMM:DAYS"
// DAYS format: 1=Sunday, 2=Monday, 3=Tuesday, 4=Wednesday, 5=Thursday, 6=Friday, 7=Saturday
// Most markets: 23456 = Monday-Friday
string tradingSession = "0930-1600:23456"  // Default: NYSE hours, Mon-Fri
 
if sessionMode == "Auto-Detect"
    if syminfo.type == "stock"
        // Stocks: Regular market hours, weekdays only
        tradingSession := "0930-1600:23456"
    else if syminfo.type == "crypto"
        // Crypto: 24/7 trading
        tradingSession := "0000-2359:1234567"
    else if syminfo.type == "forex"
        // Forex: 24-hour weekday trading (Sunday evening to Friday evening)
        tradingSession := "1700-1700:12345"
    else
        // Other assets: assume 24/7
        tradingSession := "0000-2359:1234567"
        
else if sessionMode == "New-York"
    // NYSE/NASDAQ regular hours: 09:30-16:00 EST, Monday-Friday
    tradingSession := "0930-1600:23456"
    
else if sessionMode == "London"
    // LSE hours: 08:00-16:30 GMT, Monday-Friday
    tradingSession := "0800-1630:23456"
    
else if sessionMode == "Tokyo"
    // TSE hours: 09:00-15:00 JST, Monday-Friday
    tradingSession := "0900-1500:23456"
    
else if sessionMode == "Sydney"
    // ASX hours: 10:00-16:00 AEST, Monday-Friday
    tradingSession := "1000-1600:23456"
    
else if sessionMode == "Frankfurt"
    // XETRA hours: 09:00-17:30 CET, Monday-Friday
    tradingSession := "0900-1730:23456"
    
else if sessionMode == "Custom"
    // Custom session: Check if user provided days format
    // If not, add weekdays by default
    if str.contains(customSession, ":")
        // User provided full format with days
        tradingSession := customSession
    else
        // User only provided time, add weekdays
        tradingSession := customSession + ":23456"
 
// Breakout Detection
enableBreakout = input.bool(true, "Enable Breakout Detection", group=grp_breakout, tooltip="Detect and label breakouts above ORB High or below ORB Low\n\nON: Shows breakout labels when price exits ORB range\nOFF: Only displays ORB levels without signals\n\nBreakouts detected on CLOSE (no repaint)")
enableRetest = input.bool(true, "Show Retest Labels", group=grp_breakout, inline="retest", tooltip="Show labels when price returns to ORB after breakout\n\nRETEST = Price broke out, then came back to test ORB level\n\nUSEFUL FOR:\n• Re-entry opportunities\n• Confirming support/resistance\n• Failed breakout detection\n\nON: Shows retest labels\nOFF: Only shows initial breakout")
labelSize = input.string("Small", "| Label Size", options=["Tiny", "Small", "Normal", "Large", "Huge"], inline = "retest", group=grp_breakout, tooltip="Visual size for ALL labels (breakout, retest, failed)\n\nTINY: Minimal space, cluttered on small screens\nSMALL: Clean and readable (recommended)\nNORMAL: Good for presentations\nLARGE: High visibility\nHUGE: Maximum visibility")
enableFVGFilter = input.bool(false, "Enable FVG Filter", group=grp_breakout, tooltip="Fair Value Gap Filter\n\nRequires breakout through FVG for confirmation\n\nFVG = Price gap from strong momentum\nON: Only signals with FVG\nOFF: All signals")
fvgProximity = input.float(2.0, "FVG Proximity", minval=0.5, maxval=5.0, step=0.5, group=grp_breakout, tooltip="How close to FVG?\n\n1.0 = Must be IN the FVG\n2.0 = Within 2x FVG size (recommended)\n3.0 = Within 3x FVG size (very permissive)")
showFVG = input.bool(true, "Show FVG Boxes", group=grp_breakout, inline="fvg")
fvgTransparency = input.int(90, "Transparency", minval=0, maxval=100, step=5, inline="fvg", group=grp_breakout)
maxFVGtoKeep = input.int(5, "Max FVG", minval=1, maxval=20, inline="fvg", group=grp_breakout)
fvgBullColor = input.color(color.lime, "Bullish FVG", inline="fvgcolor", group=grp_breakout)
fvgBearColor = input.color(color.orange, "Bearish FVG", inline="fvgcolor", group=grp_breakout)
signalMode = input.string("Track Cycles", "Signal Mode", options=["First Only", "Track Cycles"], group=grp_breakout, inline="mode", tooltip="How many breakout signals to show per day\n\nFIRST ONLY:\n• Shows only the FIRST breakout in each direction\n• One UP signal, one DOWN signal max per day\n• Clean chart, less noise\n• Good for: Swing traders, position traders\n\nTRACK CYCLES:\n• Tracks Breakout → Retest → Re-breakout cycles\n• Multiple signals as price oscillates\n• Detects momentum returns\n• Good for: Active traders, scalpers\n\nRECOMMENDED: Track Cycles")
maxCycles = input.int(6, "Max Cycles", minval=1, maxval=10, group=grp_breakout, inline="mode", tooltip="Maximum cycles to track when Signal Mode = Track Cycles\n\nCYCLE = Breakout → Retest → Re-breakout sequence\n\n1-2 cycles: Clean chart, fewer signals\n4-6 cycles: Balanced (recommended)\n8-10 cycles: Very active, may clutter chart\n\nIGNORED when Signal Mode = First Only")
breakoutBuffer = input.float(0.2, "Breakout Buffer (%)", minval=0, maxval=5, step=0.1, group=grp_breakout, inline="buffer", tooltip="Extra distance required beyond ORB level for breakout\n\nPREVENTS: False breakouts from tiny penetrations\n\nHOW IT WORKS:\n• UP: Price must close ABOVE (ORB High + buffer%)\n• DOWN: Price must close BELOW (ORB Low - buffer%)\n\nRECOMMENDATIONS:\n• 0.0% = Exact level (sensitive, more signals)\n• 0.1% = Tiny buffer (recommended)\n• 0.3% = Moderate filter\n• 0.5%+ = Conservative (fewer signals)\n\nHigher = fewer but stronger breakouts")
retestBuffer = input.float(0.0, "Retest Buffer (%)", minval=0, maxval=5, step=0.1, group=grp_breakout, inline="buffer", tooltip="Tolerance for retest detection\n\nALLOWS: Price to slightly penetrate ORB during retest\n\nHOW IT WORKS:\n• After UP breakout: Retest if price returns to ORB High ± buffer\n• After DOWN breakout: Retest if price returns to ORB Low ± buffer\n\nRECOMMENDATIONS:\n• 0.0% = Must touch exact level (strict)\n• 0.1-0.2% = Slight tolerance (recommended)\n• 0.3-0.5% = Generous tolerance\n\nHigher = more retests detected")
minRetestDistancePct = input.float(0.5, "Min Distance for Retest (%)", minval=0.5, maxval=10, step=0.5, group=grp_breakout, tooltip="How far price must travel away from ORB before retest is valid\n\nPREVENTS: Labeling tiny bounces as 'retests'\n\nEXAMPLE: ORB High = $10.00, Distance = 2%\n• Price breaks to $10.20 (2% away) ✓ Valid\n• Price returns to $10.00 → RETEST LABEL\n• If only went to $10.05 (0.5% away) ✗ Not valid\n\nRECOMMENDATIONS:\n• 0.5% = Very sensitive (many retests)\n• 2.0% = Balanced (filters noise) ⭐\n• 3.0%+ = Only strong moves\n\nHigher = fewer but more meaningful retests")
minBarsOutside = input.int(2, "Min Bars Outside ORB", minval=1, maxval=10, group=grp_breakout, tooltip="How many bars price must stay outside ORB for 'committed' breakout\n\nPREVENTS: Labeling quick failures as breakouts\n\nEXAMPLE: Min Bars = 2\n• Bar 1: Price breaks above ORB High\n• Bar 2: Still above ORB High ✓\n• Bar 3: Returns inside → Now can show retest\n\nIf returned on Bar 2 → Shows 'FAILED BREAK' instead\n\nRECOMMENDATIONS:\n• 1 bar = Aggressive (more signals, more failures)\n• 2 bars = Balanced (recommended) ⭐\n• 3-4 bars = Conservative (only strong moves)\n\nHigher = fewer but stronger breakouts")
minPullbackPct = input.float(0.3, "Min Pullback for Re-break (%)", minval=0.1, maxval=2, step=0.1, group=grp_breakout, tooltip="How far price must pull back before detecting new breakout\n\nPREVENTS: Too many signals from small wiggles\n\nEXAMPLE: Last breakout at $10.00\n• Price reaches $10.50\n• Must pull back to $10.35 (0.3% = $0.15) before new breakout counted\n\nRECOMMENDED:\n• 0.3% = Balanced (recommended) ⭐\n• 0.5% = Less sensitive\n• 1.0% = Very selective")
 
// Pullback Filter
enablePullbackFilter = input.bool(false, "Enable Pullback Filter", group=grp_pullback, tooltip="Require pullback confirmation before showing breakout signal\n\nWHY: Filters weak breakouts that reverse immediately\nHOW: Waits for price to pull back slightly, then continue breakout direction\n\nWORKFLOW:\n1. Price breaks above/below ORB\n2. Wait for pullback (small dip/rise)\n3. Price continues in breakout direction\n4. NOW show signal ✅\n\nBENEFITS:\n• Better entry prices\n• Fewer false breakouts\n• Improved risk/reward\n• Confirmation of strength\n\nDRAWBACKS:\n• Misses fast runners\n• Slower signals\n• More conservative\n\nON: Wait for pullback confirmation\nOFF: Immediate signals (current behavior)\n\nRECOMMENDED: Try OFF first, enable if too many false signals")
pullbackPercent = input.float(0.3, "Pullback Required (%)", minval=0.1, maxval=2.0, step=0.1, group=grp_pullback, inline="pb", tooltip="How much pullback required for confirmation\n\nEXAMPLE: ORB High = $100, Pullback = 0.5%\n• Breaks to $100.60 (above ORB)\n• Pulls back to $100.10 (0.5% dip) ✓\n• Continues to $100.70 → SIGNAL!\n\nSMALLER % = More signals, faster\nLARGER % = Fewer signals, stronger\n\nRECOMMENDED:\n• 0.3% = Sensitive (active trading) ⭐\n• 0.5% = Balanced\n• 1.0% = Conservative (swing trading)")
pullbackTimeout = input.int(10, "Timeout (bars)", minval=3, maxval=30, group=grp_pullback, inline="pb", tooltip="Max bars to wait for pullback\n\nIf pullback doesn't happen within this time:\n→ Shows signal anyway (prevents missing strong moves)\n\nEXAMPLE: Timeout = 10 bars\n• Breakout occurs\n• Wait 10 bars for pullback\n• No pullback? → Show signal at bar 10\n• Pullback at bar 5? → Show signal when continues\n\nSHORTER = Less waiting, more immediate signals\nLONGER = More patient, better confirmation\n\nRECOMMENDED:\n• 5 bars = Quick (5min chart = 25 min)\n• 10 bars = Balanced (5min chart = 50 min) ⭐\n• 15 bars = Patient (5min chart = 75 min)")
 
// Volume Filter
enableVolumeFilter = input.bool(false, "Enable Volume Filter", group=grp_volume, inline="enable", tooltip="Require volume confirmation for breakout signals\n\nWHY: High volume breakouts are more reliable\nWHAT: Compares current volume to moving average\n\nON: Only shows breakouts with sufficient volume\nOFF: Shows all breakouts regardless of volume\n\nRECOMMENDED: ON for stocks, OFF for crypto")
volumeMaLength = input.int(20, "| MA Length", minval=1, group=grp_volume, inline="enable", tooltip="Volume moving average period for comparison\n\nSHORTER = More sensitive to recent volume spikes\nLONGER = Smoother average, less reactive\n\nRECOMMENDATIONS:\n• 10 periods: Very responsive\n• 20 periods: Balanced (recommended) ⭐\n• 50 periods: Long-term average\n\nCHART TIMEFRAME affects calculation:\n• 5min chart, MA=20 → Last 100 minutes\n• 1hour chart, MA=20 → Last 20 hours")
volumeMultiplier = input.float(1.5, "Min Volume ×", minval=0.5, step=0.1, group=grp_volume, inline="mult", tooltip="Minimum volume multiplier required for breakout\n\nCURRENT VOLUME must be ≥ (Volume MA × Multiplier)\n\nEXAMPLE: Volume MA = 1M shares, Multiplier = 1.5\n• Breakout with 1.6M volume ✓ Valid\n• Breakout with 1.2M volume ✗ Rejected\n\nRECOMMENDATIONS:\n• 1.0× = Any above-average volume\n• 1.5× = Moderate requirement (recommended) ⭐\n• 2.0× = Strong requirement (fewer signals)\n• 2.5×+ = Very selective\n\nHigher = fewer but higher-quality signals")
strongVolumeMultiplier = input.float(2.0, "Strong ×", minval=1.0, step=0.1, group=grp_volume, inline="mult", tooltip="Strong volume override threshold\n\nSPECIAL RULE: If volume exceeds this multiplier:\n→ BYPASSES all other filters (trend, etc.)\n→ Shows breakout immediately\n\nRATIONALE: Massive volume = institutional activity\n\nEXAMPLE: Volume MA = 1M, Strong = 2.0×\n• Breakout with 2.5M volume → Shows even if trend filter says NO\n\nRECOMMENDATIONS:\n• 2.0× = Balanced (recommended) ⭐\n• 2.5× = More selective\n• 3.0× = Only exceptional volume\n\nMUST BE ≥ Min Volume Multiplier")
 
// Trend Filter
enableTrendFilter = input.bool(false, "Enable Trend Filter", group=grp_trend, inline = "FILTER", tooltip="Only show breakouts that align with broader trend\n\nWHY: Trend-aligned breakouts have higher success rate\nWHAT: Checks if price is above/below trend indicator\n\nRULES:\n• UP breakout: Only if price ABOVE trend\n• DOWN breakout: Only if price BELOW trend\n\nON: Filters against-trend breakouts\nOFF: Shows all breakouts\n\nRECOMMENDED: ON for conservative trading")
trendMode = input.string("VWAP", "| Trend Mode",inline = "FILTER", options=["VWAP", "EMA", "Custom EMA", "SuperTrend", "VWAP+EMA", "VWAP+SuperTrend"], group=grp_trend, tooltip="Method to determine trend direction\n\nVWAP (Volume-Weighted Average Price):\n• Accounts for volume in price calculation\n• Good for: Intraday trading\n• Above VWAP = Bullish, Below = Bearish\n\nEMA (12-period):\n• Fast exponential moving average\n• Responsive to recent price action\n• Good for: Quick trends\n\nCUSTOM EMA:\n• Your own EMA period\n• Longer = Smoother trend\n• Good for: Swing trading (50-200 period)\n\nSUPERTREND:\n• ATR-based trailing stop\n• Clear trend direction\n• Good for: Strong trends\n\nVWAP+EMA:\n• BOTH must agree\n• More conservative\n• Fewer but higher quality signals\n\nVWAP+SUPERTREND:\n• BOTH must agree\n• Most conservative\n• Best signal quality")
 
customEmaLength = input.int(50, "Custom EMA Length", minval=1, group=grp_trend, inline="ema", tooltip="EMA period when Trend Mode = Custom EMA\n\nSHORTER = More reactive, more signals\nLONGER = Smoother, fewer signals\n\nRECOMMENDATIONS:\n• 20-30: Intraday trading\n• 50: Swing trading (recommended) ⭐\n• 100-200: Position trading\n\nIGNORED for other trend modes")
supertrendPeriod = input.int(10, "SuperTrend Period", minval=1, group=grp_trend, inline="st", tooltip="ATR period for SuperTrend calculation\n\nSHORTER = More sensitive, tighter stops\nLONGER = Smoother, wider stops\n\nRECOMMENDATIONS:\n• 7: Aggressive\n• 10: Balanced (recommended) ⭐\n• 14: Conservative\n\nONLY USED when Trend Mode includes SuperTrend")
supertrendMult = input.float(3.0, "Mult", minval=0.1, step=0.1, group=grp_trend, inline="st", tooltip="ATR multiplier for SuperTrend bands\n\nLOWER = Tighter bands, more trend changes\nHIGHER = Wider bands, fewer changes\n\nRECOMMENDATIONS:\n• 2.0: Sensitive (more signals)\n• 3.0: Balanced (recommended) ⭐\n• 4.0+: Conservative (fewer signals)\n\nONLY USED when Trend Mode includes SuperTrend")
 
// ===================================================================================================
// ========================================= TARGETS & RISK MANAGEMENT ================================
// ===================================================================================================
 
// Target System
enableTargets = input.bool(true, "Enable Targets & Stop-Loss", group=grp_targets, tooltip="Calculate and display profit targets (TP) and stop-loss (SL)\n\nWHEN ENABLED:\n• Shows TP/SL lines on chart during breakout\n• Displays values in dashboard\n• Calculates Risk/Reward ratio\n\nWHEN DISABLED:\n• Only shows ORB levels\n• No trade management\n\nRECOMMENDED: ON for active trading")
 
showTP1 = input.bool(true, "TP1 (1R)", group=grp_targets, inline="show", tooltip="Select which Take Profit levels to display\n\nR = RISK UNITS (1R = distance from entry to stop-loss)\n\nTP1 (1R): Conservative target, 1:1 risk/reward\nTP1.5 (1.5R): Intermediate target\nTP2 (2R): Balanced target, 2:1 risk/reward\nTP3 (3R): Aggressive target, 3:1 risk/reward\n\nRECOMMENDED COMBINATIONS:\n• Scalping: TP1 + TP1.5\n• Day Trading: TP1 + TP2 ⭐\n• Swing Trading: TP1 + TP2 + TP3\n\nLines and labels stop updating when last enabled TP is hit")
showTP1_5 = input.bool(false, "TP1.5 (1.5R)", group=grp_targets, inline="show")
showTP2 = input.bool(true, "TP2 (2R)", group=grp_targets, inline="show")
showTP3 = input.bool(false, "TP3 (3R)", group=grp_targets, inline="show")
 
stopMode = input.string("ATR", "Stop Method", options=["ATR", "ORB %", "Swing", "Safer"], group=grp_targets, inline="tgt", tooltip="How to calculate Stop Loss placement\n\nATR (Recommended):\n• Based on stock volatility\n• Volatile stocks get wider stops\n• Formula: SL = Entry ± (ATR × 1.5)\n• Prevents premature stop-outs\n• Good for: All trading styles\n\nORB %:\n• Fixed percentage beyond ORB level\n• Simple and predictable\n• Formula: SL = ORB_Level ± (ORB_Range × 20%)\n• Good for: Beginners, consistent trading\n\nSWING:\n• Placed at recent swing high/low\n• Technical support/resistance\n• Respects chart structure\n• May be far from entry\n• Good for: Technical traders\n\nSAFER:\n• Widest stop of all methods\n• Lowest chance of stop-out\n• Largest potential loss\n• Formula: max(ATR×2.5, ORB×50%, Swing)\n• Good for: Risk-averse traders, volatile markets")
atrLength = input.int(14, "ATR", minval=5, maxval=50, group=grp_targets, inline="atr2", tooltip="ATR calculation period\n\nATR = Average True Range = How much price moves\n\nSHORTER = More reactive to recent volatility\nLONGER = Smoother, stable average\n\nRECOMMENDATIONS:\n• 7-10: Day trading, responsive\n• 14: Standard, balanced (recommended) ⭐\n• 20-30: Swing trading, smooth\n\nUSED FOR: ATR-based targets and stops")
atrMultiplier = input.float(1.5, "×", minval=0.5, maxval=3, step=0.1, group=grp_targets, inline="atr2", tooltip="ATR multiplier for stop-loss distance\n\nSL = Entry ± (ATR × Multiplier)\n\nLOWER = Tighter stop, less risk, more stop-outs\nHIGHER = Wider stop, more risk, fewer stop-outs\n\nRECOMMENDATIONS:\n• 1.0×: Tight stop (aggressive)\n• 1.5×: Balanced (recommended) ⭐\n• 2.0×: Loose stop (conservative)\n• 2.5×+: Very loose (volatile stocks)\n\nONLY AFFECTS: ATR stop method")
stopOrbFraction = input.float(20, "ORB Stop %", minval=10, maxval=50, step=5, group=grp_targets, inline="stop", tooltip="Stop distance as percentage of ORB range\n\nONLY USED when Stop Method = ORB %\n\nFORMULA:\n• LONG: SL = ORB_Low - (ORB_Range × 20%)\n• SHORT: SL = ORB_High + (ORB_Range × 20%)\n\nEXAMPLE: ORB Range = $1.00, Fraction = 20%\n• SL placed $0.20 beyond ORB boundary\n\nRECOMMENDATIONS:\n• 10-15%: Tight (more stop-outs)\n• 20%: Balanced (recommended) ⭐\n• 30-40%: Loose (fewer stop-outs)\n• 50%: Very loose")
swingBars = input.int(3, "Swing Bars", minval=1, maxval=10, group=grp_targets, inline="stop", tooltip="Lookback period to find swing high/low\n\nONLY USED when Stop Method = Swing\n\nSWING = Local high/low point (reversal point)\n\nSHORTER = Recent swing (closer SL)\nLONGER = Deeper swing (farther SL)\n\nEXAMPLE: Swing Bars = 3\n• Looks back 3 bars\n• Finds lowest low (for LONG) or highest high (for SHORT)\n• Places SL there\n\nRECOMMENDATIONS:\n• 2: Very recent swing (aggressive)\n• 3: Recent swing (recommended) ⭐\n• 5: Moderate swing\n• 7-10: Deep swing (conservative)")
showTPSLLines = input.bool(true, "Show TP/SL Lines on Chart", group=grp_lines, tooltip="Display Entry, TP, and SL as horizontal lines on chart\n\nWHEN BREAKOUT OCCURS:\n• Lines appear instantly\n• Extend forward as chart progresses\n• Update with each new bar\n\nLINES FREEZE (stop updating) when:\n• Last TP is hit ✓\n• Stop Loss is hit ✗\n• End of Day (if enabled)\n\nCOLORS:\n• Entry: Cyan (LONG) / Orange (SHORT)\n• TP: Green when hit, cyan/orange before\n• SL: Red always\n\nON: Shows lines (recommended)\nOFF: Only dashboard display")
lineDisplayMode = input.string("Detailed", "Label Format", options=["Short", "Detailed"], group=grp_lines, inline="format", tooltip="Label text format on TP/SL lines\n\nSHORT:\n• Minimal text: 'ENTRY', 'TP1', 'SL'\n• Clean and compact\n• Good for: Small screens, many lines\n\nDETAILED:\n• Full info: 'ENTRY: $150.50', 'TP1: $152.20 +1.1%'\n• Shows price and percentage\n• Good for: Analysis, larger screens (recommended) ⭐\n\nAFTER HIT:\n• TP: '✅ TP1: $152.20'\n• SL: '❌ SL: $148.50'")
freezeOnEOD = input.bool(true, "Freeze Lines at End of Day", group=grp_lines, inline="freeze", tooltip="Stop updating lines when trading session ends\n\nON (Recommended):\n• Lines freeze at session close\n• Keeps intraday setup visible\n• Clean daily reset\n• Good for: ORB strategy (intraday)\n\nOFF:\n• Lines continue extending until hit\n• May cross multiple sessions\n• Good for: Swing trading, 24/7 markets\n\nNOTE: Lines also freeze when SL hit or last TP hit")
 
// ===================================================================================================
// ============================================ DASHBOARD ============================================
// ===================================================================================================
 
showDashboard = input.bool(true, "Show Info Panel", group=grp_dash, inline="theme")
dashTheme = input.string("Dark", "| Theme", options=["Dark", "Light"], group=grp_dash, inline="theme", tooltip="Dashboard color scheme\n\nDARK (Recommended):\n• Light text on dark background\n• Matches dark chart themes\n• Less eye strain\n• Professional look\n\nLIGHT:\n• Dark text on light background\n• Matches light chart themes\n• Good for presentations\n• High contrast")
dashPos = input.string("Bottom Left", "Position", options=["Top Left", "Top Right", "Bottom Left", "Bottom Right", "Middle Right"], group=grp_dash, inline="pos", tooltip="Dashboard placement on chart\n\nTOP LEFT: Good for price action top-right\nTOP RIGHT: Good for price action top-left\nBOTTOM LEFT: Classic placement (recommended) ⭐\nBOTTOM RIGHT: Avoid volume bars\nMIDDLE RIGHT: Floating position\n\nChoose based on where price usually is")
dashSize = input.string("Auto", "Text Size", options=["Auto", "Small", "Normal", "Large"], group=grp_dash, inline="pos", tooltip="Text size inside dashboard\n\nAUTO: Adapts to chart zoom (recommended) ⭐\nSMALL: Compact, more screen space\nNORMAL: Readable\nLARGE: Presentations, poor eyesight")
showStage = input.bool(true, "Stage", group=grp_dash, inline="rows1", tooltip="Display current ORB stage\n\nSHOWS: Which timeframe is active\n• ORB 5 (5 minutes)\n• ORB 15 (15 minutes)\n• ORB 30 (30 minutes)\n• ORB 60 (60 minutes)\n\nUseful for knowing which ORB you're trading")
showRange = input.bool(true, "Range", group=grp_dash, inline="rows1", tooltip="Display ORB range size\n\nSHOWS:\n• High-Low difference (dollars)\n• Range as percentage\n\nEXAMPLE: '$0.50 (2.5%)'\n\nUseful for assessing if range is significant")
showStatus = input.bool(true, "Status", group=grp_dash, inline="rows1", tooltip="Display breakout status\n\nSTATES:\n• 🔵 INSIDE RANGE: Price within ORB\n• 🟢 BREAK UP: Above ORB High\n• 🔴 BREAK DOWN: Below ORB Low\n• 🟠 RETEST: Returned to ORB after breakout\n\nQuick visual confirmation of position")
showVol = input.bool(true, "Volume", group=grp_dash, inline="rows2", tooltip="Display volume status\n\nONLY SHOWN when Volume Filter enabled\n\nSHOWS:\n• Current volume vs average\n• Multiplier (e.g., '1.8x')\n• ✅ if above threshold\n• ❌ if below threshold\n\nUseful for confirming breakout strength")
showTrend = input.bool(true, "Trend", group=grp_dash, inline="rows2", tooltip="Display trend direction\n\nONLY SHOWN when Trend Filter enabled\n\nSHOWS:\n• Trend direction (UP/DOWN/NEUTRAL)\n• Method used (VWAP/EMA/etc.)\n• ✅ Aligned / ❌ Against trend\n\nUseful for context")
showRisk = input.bool(true, "Risk/Reward", group=grp_dash, inline="rows2", tooltip="Display Risk/Reward ratio\n\nSHOWS: Ratio of potential profit to risk\n\nFORMAT: '1:2.5' means:\n• Risk: $100\n• Reward: $250\n\nINDICATORS:\n• ✅ Ratio ≥ 2.0 (good)\n• ⚠️ Ratio 1.5-2.0 (acceptable)\n• ❌ Ratio < 1.5 (poor)\n\nBased on highest enabled TP")
 
enablePosSizing = input.bool(true, "Calculate Position Size", group=grp_dash, tooltip="Calculate shares to buy based on risk management\n\nCALCULATES:\n• Max shares to buy\n• Position dollar value\n• Risk amount\n• TP profit projections\n• TP allocation splits\n\nWHY:\n• Consistent risk per trade\n• Proper position sizing\n• Prevents over-leveraging\n\nSHOWS: Extra section in dashboard\n\nON: Shows position sizing\nOFF: Only price levels")
riskMode = input.string("$ Amount", "Risk", options=["$ Amount", "% of Account"], group=grp_dash, inline="risk", tooltip="How to define your risk per trade\n\n$ AMOUNT:\n• Risk fixed dollar amount\n• Simple and direct\n• Example: '$150 per trade'\n• Good for: Consistent trading\n\n% OF ACCOUNT:\n• Risk percentage of total account\n• Scales with account size\n• Example: '1% of $25,000 = $250'\n• Good for: Account growth\n\nRECOMMENDED: % of Account")
fixedRisk = input.float(150, "$", minval=10, maxval=10000, group=grp_dash, inline="risk", tooltip="Fixed dollar amount to risk per trade\n\nONLY USED when Risk Mode = $ Amount\n\nEXAMPLE: Fixed Risk = $150\n• Entry: $10.00\n• Stop: $9.50 (50¢ risk per share)\n• Position: 300 shares ($150 ÷ $0.50)\n• Total position value: $3,000\n\nRECOMMENDATIONS:\n• $50-100: Small accounts\n• $150-300: Medium accounts ⭐\n• $500+: Large accounts")
accountSize = input.float(25000, "Account $", minval=1000, group=grp_dash, inline="acct", tooltip="Total trading account size\n\nUSED FOR: Calculating risk percentage\n\nEXAMPLE: Account = $25,000, Risk = 1%\n• Risk per trade = $250\n\nUPDATE THIS:\n• When account grows/shrinks significantly\n• For accurate position sizing\n• Monthly review recommended")
riskPct = input.float(0.5, "Risk %", minval=0.1, maxval=5, step=0.1, group=grp_dash, inline="acct", tooltip="Percentage of account to risk per trade\n\nONLY USED when Risk Mode = % of Account\n\nRISK LEVELS:\n• 0.25-0.5%: Very conservative, slow growth\n• 0.5-1%: Balanced (recommended) ⭐\n• 1-2%: Aggressive, faster growth\n• 2-3%: Very aggressive, high risk\n• 3%+: Extreme risk, not recommended\n\nEXAMPLE: Account = $25,000, Risk = 1%\n• Risk per trade = $250\n• Entry: $10, Stop: $9.50 (50¢ risk)\n• Position: 500 shares\n\nRECOMMENDED: 0.5-1% for most traders")
maxPositionPct = input.float(25.0, "Max Position %", minval=5, maxval=100, step=5, group=grp_dash, inline="poslimit", tooltip="Maximum position size as % of account\n\nPREVENTS OVER-LEVERAGE when stop is very tight\n\nEXAMPLE: Account = $10,000, Max = 25%\n• Maximum position value = $2,500\n• If stock = $100, max 25 shares\n• Even if risk calculation allows more\n\nRECOMMENDED LEVELS:\n• 10-15%: Very conservative\n• 20-25%: Balanced ⭐\n• 30-50%: Aggressive\n• 50%+: Very aggressive\n\nThis protects against:\n• Tight stops causing huge positions\n• Slippage on large positions\n• Liquidity issues")
 
// ===================================================================================================
// ======================================= HIGHER TIMEFRAME FILTER ===================================
// ===================================================================================================
 
enableHTF = input.bool(true, "Check Daily Trend", group=grp_htf, tooltip="Check higher timeframe trend before taking breakouts\n\nWHY: Breakouts WITH higher timeframe trend have higher success rate\n\nWHAT IT DOES:\n• Checks daily/4H/weekly trend\n• Shows warning if breakout against trend\n• Displays HTF bias in dashboard\n\nDOES NOT BLOCK signals - only warns you\n\nON: Shows HTF context (recommended for swing traders)\nOFF: Focuses only on intraday ORB")
htfTF = input.timeframe("D", "Timeframe", group=grp_htf, inline="htf", tooltip="Higher timeframe to analyze for trend\n\nD (DAILY) - Recommended:\n• Best for intraday ORB trading\n• Shows overall market direction\n• Most relevant for stock traders\n\n240 (4-HOUR):\n• Shorter-term trend\n• Good for crypto (24/7 markets)\n• More reactive\n\nW (WEEKLY):\n• Long-term trend\n• Best for swing trading\n• Slower changes\n\nRULE: HTF should be 4-10× your chart timeframe")
htfMethod = input.string("Price vs MA", "Method", options=["Price vs MA", "Candle Direction"], group=grp_htf, inline="htf", tooltip="How to determine HTF trend\n\nPRICE VS MA (Recommended):\n• Compares HTF close to moving average\n• Above MA = Bullish\n• Below MA = Bearish\n• Smooth and reliable\n• Less prone to whipsaws\n\nCANDLE DIRECTION:\n• Based on HTF candle color\n• Green candle = Bullish\n• Red candle = Bearish\n• More reactive\n• Can change quickly\n\nRECOMMENDED: Price vs MA for stability")
htfEMA = input.int(20, "MA", minval=5, maxval=200, group=grp_htf, inline="htf2", tooltip="Moving average period for HTF trend\n\nONLY USED when Method = Price vs MA\n\nSHORTER = More reactive, frequent changes\nLONGER = Smoother, stable trend\n\nRECOMMENDATIONS:\n• 10-20: Responsive (short-term trend)\n• 20-50: Balanced (recommended) ⭐\n• 50-100: Smooth (intermediate trend)\n• 100-200: Very smooth (long-term trend)\n\nCOMMON CHOICES:\n• Daily TF + 20 EMA: Classic\n• Daily TF + 50 EMA: Swing trading\n• Weekly TF + 20 EMA: Position trading")
htfMinStrength = input.float(2.0, "Min Distance %", minval=0.0, maxval=10.0, step=0.5, group=grp_htf, inline="htf3", tooltip="Minimum distance from MA to consider trend valid\n\nPrice must be at least X% away from MA\n\nLOWER = Accept weak trends\nHIGHER = Only strong trends\n\nRECOMMENDATIONS:\n• 0-1%: Very permissive\n• 1-2%: Balanced ⭐\n• 2-3%: Conservative\n• 3%+: Very selective\n\nHELPS AVOID:\n• Choppy/sideways markets\n• Weak trend signals\n• False breakouts")
 
// ===================================================================================================
// ============================================= ALERTS ==============================================
// ===================================================================================================
 
enableAlerts = input.bool(true, "Enable Alerts", group=grp_alerts, tooltip="Master switch for all alert types\n\nON: Alerts enabled (if sub-types checked)\nOFF: No alerts sent (overrides all)\n\nTO USE ALERTS:\n1. Enable this switch\n2. Check desired alert types below\n3. Click 'Create Alert' button\n4. Select this indicator\n5. Choose 'Any alert() function call'\n\nALERTS FIRE on bar close (no repaint)")
alertBreakouts = input.bool(true, "Breakout Alerts", group=grp_alerts, inline="brk", tooltip="Get notified on ORB breakouts\n\nTRIGGERS:\n• Price closes above ORB High\n• Price closes below ORB Low\n\nALERT MESSAGE INCLUDES:\n• Direction (UP/DOWN)\n• Symbol\n• Price\n• ORB stage\n• Cycle number (if tracking)\n\nEXAMPLE:\n'🔼 BREAKOUT UP #2 - AAPL @ $150.25 | ORB15 H: $150.00'\n\nRECOMMENDED: ON for active trading")
alertRetests = input.bool(true, "Retest Alerts", group=grp_alerts, inline="brk", tooltip="Get notified when price retests ORB after breakout\n\nTRIGGERS:\n• Price broke out\n• Moved away (min distance)\n• Returns to ORB level\n\nWHY USEFUL:\n• Potential re-entry opportunity\n• Confirms support/resistance\n• Trend continuation signal\n\nALERT MESSAGE INCLUDES:\n• Direction\n• Symbol\n• Price\n• Retest cycle number\n\nEXAMPLE:\n'🔁 RETEST UP #1 - AAPL @ $150.10 | Back to ORB15 range'\n\nRECOMMENDED: ON if using retest strategy")
alertFailedBreaks = input.bool(true, "Failed Break Alerts", group=grp_alerts, inline="fail", tooltip="Get notified when breakout fails quickly\n\nTRIGGERS:\n• Price broke out of ORB\n• Returns inside ORB within few bars\n• Before becoming 'committed breakout'\n\nWHY USEFUL:\n• Warns of false breakout\n• Potential reversal signal\n• Exit signal if already in trade\n\nALERT MESSAGE INCLUDES:\n• Direction that failed\n• Symbol\n• Current price\n\nEXAMPLE:\n'⚠️ FAILED BREAK UP - AAPL @ $149.80 | Returned to ORB30'\n\nRECOMMENDED: ON to avoid bad trades")
alertStageComplete = input.bool(false, "Stage Complete Alerts", group=grp_alerts, inline="fail", tooltip="Get notified when each ORB stage finishes forming\n\nTRIGGERS:\n• After 5 minutes: ORB5 complete\n• After 15 minutes: ORB15 complete\n• After 30 minutes: ORB30 complete\n• After 60 minutes: ORB60 complete\n\nWHY USEFUL:\n• Know when ORB is 'locked in'\n• Start watching for breakouts\n• Trading session milestones\n\nALERT MESSAGE INCLUDES:\n• Stage name\n• Symbol\n• Range high/low\n• Range percentage\n\nEXAMPLE:\n'✅ ORB15 COMPLETE - AAPL | Range: $149.50 - $150.20 (0.5%)'\n\nRECOMMENDED: OFF (creates many alerts)")
 
// ===================================================================================================
// ========================================= INPUT VALIDATION ========================================
// ===================================================================================================
 
// Validate breakout settings
if breakoutBuffer < 0 or breakoutBuffer > 5
    runtime.error("❌ Breakout Buffer must be between 0-5%. Current value: " + str.tostring(breakoutBuffer))
 
if minRetestDistancePct < 0.5
    runtime.error("❌ Min Retest Distance too low. Must be >= 0.5% to prevent false signals. Current: " + str.tostring(minRetestDistancePct))
 
if minBarsOutside < 1
    runtime.error("❌ Min Bars Outside must be >= 1. Current: " + str.tostring(minBarsOutside))
 
if maxCycles < 1 or maxCycles > 10
    runtime.error("❌ Max Cycles must be between 1-10. Current: " + str.tostring(maxCycles))
 
// Validate volume settings
if enableVolumeFilter
    if volumeMultiplier < 0.5
        runtime.error("❌ Volume Multiplier too low. Must be >= 0.5x. Current: " + str.tostring(volumeMultiplier))
    
    if strongVolumeMultiplier < volumeMultiplier
        runtime.error("❌ Strong Volume Multiplier must be >= Min Volume Multiplier. Strong: " + str.tostring(strongVolumeMultiplier) + ", Min: " + str.tostring(volumeMultiplier))
    
    if volumeMaLength < 1
        runtime.error("❌ Volume MA Length must be >= 1. Current: " + str.tostring(volumeMaLength))
 
// Validate target settings
if enableTargets
    if atrLength < 5 or atrLength > 50
        runtime.error("❌ ATR Length must be between 5-50. Current: " + str.tostring(atrLength))
    
    if atrMultiplier < 0.5 or atrMultiplier > 5
        runtime.error("❌ ATR Multiplier must be between 0.5-5. Current: " + str.tostring(atrMultiplier))
    
    if stopOrbFraction < 10 or stopOrbFraction > 100
        runtime.error("❌ ORB Stop % must be between 10-100%. Current: " + str.tostring(stopOrbFraction))
    
    if swingBars < 1 or swingBars > 20
        runtime.error("❌ Swing Bars must be between 1-20. Current: " + str.tostring(swingBars))
 
// Validate position sizing
if enablePosSizing
    if riskMode == "$ Amount" and fixedRisk < 10
        runtime.error("❌ Fixed Risk too low. Must be >= $10. Current: $" + str.tostring(fixedRisk))
    
    if riskMode == "% of Account"
        if riskPct < 0.1 or riskPct > 10
            runtime.error("❌ Risk % must be between 0.1-10%. Current: " + str.tostring(riskPct) + "%")
        
        if accountSize < 100
            runtime.error("❌ Account Size too low. Must be >= $100. Current: $" + str.tostring(accountSize))
 
// Validate visual settings
if fillTransparency < 0 or fillTransparency > 100
    runtime.error("❌ Fill Transparency must be between 0-100%. Current: " + str.tostring(fillTransparency))
 
// Validate HTF settings
if enableHTF
    if htfEMA < 5 or htfEMA > 200
        runtime.error("❌ HTF EMA period must be between 5-200. Current: " + str.tostring(htfEMA))
 
// Validate trend filter settings
if enableTrendFilter
    if customEmaLength < 1 or customEmaLength > 500
        runtime.error("❌ Custom EMA Length must be between 1-500. Current: " + str.tostring(customEmaLength))
    
    if supertrendPeriod < 1 or supertrendPeriod > 50
        runtime.error("❌ SuperTrend Period must be between 1-50. Current: " + str.tostring(supertrendPeriod))
    
    if supertrendMult < 0.1 or supertrendMult > 10
        runtime.error("❌ SuperTrend Multiplier must be between 0.1-10. Current: " + str.tostring(supertrendMult))
 
// Logical validation: TP selections
if enableTargets
    int tpCount = 0
    if showTP1
        tpCount += 1
    if showTP1_5
        tpCount += 1
    if showTP2
        tpCount += 1
    if showTP3
        tpCount += 1
    
    if tpCount == 0
        runtime.error("❌ Enable at least one Target (TP1, TP2, etc.) when 'Enable Targets' is ON")
 
// ===================================================================================================
// ============================================== VARIABLES ==========================================
// ===================================================================================================
 
var int stage = 0
var int buildingStage = 1
var label labH = na
var label labL = na
var color cachedLineColor = na
var color cachedFillColor = na
var float lastLabelH = na
var float lastLabelL = na
var int lastLabelStage = na
var string cachedStageName = na
 
// Breakout tracking - PER STAGE (not global)
var bool hadBreakUp = false
var bool hadBreakDown = false
var int breakUpBar = na
var int breakDownBar = na
var bool shownBreakUpLabel = false
var bool shownBreakDownLabel = false
 
// Track if there was EVER a breakout today (for dashboard display)
var bool everHadBreakUp = false
var bool everHadBreakDown = false
var float breakoutEntryPrice = na
 
// Track which stage had breakout (to avoid duplicate signals on same stage)
var int lastBreakUpStage = 0
var int lastBreakDownStage = 0
 
// Pullback filter
var bool awaiting_pullback_up = false
var bool awaiting_pullback_down = false
var int pullback_wait_bar_up = na
var int pullback_wait_bar_down = na
var float pullback_high_price = na
var float pullback_low_price = na
var bool had_pullback_up = false
var bool had_pullback_down = false
 
// Cycle tracking
var int cyclesUp = 0
var int cyclesDown = 0
var int retestCyclesUp = 0 
var int retestCyclesDown = 0
 
// Alert message strings
var string alertMsgBreakUp = ""
var string alertMsgBreakDown = ""
var string alertMsgRetestUp = ""
var string alertMsgRetestDown = ""
var string alertMsgFailedUp = ""
var string alertMsgFailedDown = ""
var string alertMsgStageComplete = ""
 
// Track alert triggers for alertcondition (global scope)
bool alertBreakUpTriggered = false
bool alertBreakDownTriggered = false
bool alertRetestUpTriggered = false
bool alertRetestDownTriggered = false
bool alertFailedUpTriggered = false
bool alertFailedDownTriggered = false
bool alertStageCompleteTriggered = false
 
// Track bars outside ORB after breakout
var int barsOutsideAfterBreakUp = 0
var int barsOutsideAfterBreakDown = 0
 
// Save label references for potential failed break updates
var label lastBreakUpLabel = na
var label lastBreakDownLabel = na
 
// Track breakout bar index for failed break detection
var int breakUpBarIndex = 0
var int breakDownBarIndex = 0
 
// Label history tracking for cleanup
var array<label> breakoutLabels = array.new<label>()
var int maxLabelsToKeep = 10
 
var array<line> tpslLines = array.new<line>()
var array<box> orbBoxes = array.new<box>()
var int maxLinesToKeep = 20
var int maxBoxesToKeep = 10
 
 
// Dashboard optimization
var int lastDashUpdateBar = -1
 
// Cached calculations for performance
var float cachedATR = na
var int cachedATRBar = -1
var float cachedVolumeMA = na
var int cachedVolumeMABar = -1
 
// FVG tracking
var array<box> fvgBoxes = array.new<box>()
var array<float> fvgTops = array.new<float>()
var array<float> fvgBottoms = array.new<float>()
var array<bool> fvgIsBullish = array.new<bool>()
var array<bool> fvgIsActive = array.new<bool>()
var array<int> fvgStartBar = array.new<int>()
 
// Retest tracking
var bool hadRetestUp = false
var bool hadRetestDown = false
var int retestUpBar = na
var int retestDownBar = na
 
// Track if price went far enough for valid retest
var bool wentFarEnoughUp = false
var bool wentFarEnoughDown = false
 
// Session timing variables
var int sessionStartBar = na
var float sessionStartTime = na
 
// Trend calculation
var float trendVWAP = na
var float trendEMA12 = na
var float trendEMACustom = na
var float trendSTUp = na
var float trendSTDown = na
var int trendSTDirection = 0
 
// Volume tracking
var float volumeMA = na
 
// HTF Bias tracking
var bool htfBullish = false
var bool htfBearish = false
var string htfBiasText = ""
 
// Targets & Stops tracking
var line lineTP1 = na
var line lineTP1_5 = na
var line lineTP2 = na
var line lineTP3 = na
var line lineSL = na
var label labelTP1 = na
var label labelTP2 = na
var label labelSL = na
var float currentEntry = na
var float currentSL = na
var float currentTP1 = na
var float currentTP2 = na
var bool activeTradeUp = false
var bool activeTradeDown = false
 
// TP/SL Lines state (new system)
var line orbEntryLine = na
var label orbEntryLabel = na
var float orbEntryPrice = na
var int orbEntryBar = na
var color orbLineColor = na
var line orbSLLine = na
var label orbSLLabel = na
var float orbSLPrice = na
var line orbTP1Line = na
var label orbTP1Label = na
var float orbTP1Price = na
var line orbTP1_5Line = na
var label orbTP1_5Label = na
var float orbTP1_5Price = na
var line orbTP2Line = na
var label orbTP2Label = na
var float orbTP2Price = na
var line orbTP3Line = na
var label orbTP3Label = na
var float orbTP3Price = na
var bool orbLinesFrozen = false
var bool orbTP1Hit = false
var bool orbTP1_5Hit = false
var bool orbTP2Hit = false
var bool orbTP3Hit = false
var bool orbSLHit = false
var int orbTradeDirection = 0
 
// Position sizing
var float maxShares = na
var float positionValue = na
var float maxLoss = na
var float projectedProfit = na
 
// Dashboard table
var table dashTable = na
 
// ===================================================================================================
// ============================================ ALERT FUNCTIONS ======================================
// ===================================================================================================
 
// Send alert with custom message
sendAlert(string message) =>
    if enableAlerts
        alert(message, alert.freq_once_per_bar_close)
 
// ===================================================================================================
// ========================================== VALIDATION FUNCTIONS ===================================
// ===================================================================================================
 
// Volume validation
hasVolumeConfirmation(volMA, mult, strongMult) =>
    if na(volMA) or volMA == 0
        true
    else
        currentVol = volume
        isStrongVolume = strongMult > 0 and currentVol >= volMA * strongMult
        isNormalVolume = currentVol >= volMA * mult
        isStrongVolume or isNormalVolume
 
// Stage validation - checks if current stage signals are enabled
isStageAllowed(currentStage, enableORB5, enableORB15, enableORB30, enableORB60) =>
    bool allowed = false
    
    if currentStage == 1
        allowed := enableORB5
    else if currentStage == 2
        allowed := enableORB15 or enableORB5
    else if currentStage == 3
        allowed := enableORB30 or enableORB15 or enableORB5
    else if currentStage == 4
        allowed := enableORB60 or enableORB30 or enableORB15 or enableORB5
    
    allowed
 
// ===================================================================================================
// =========================================== TARGET & STOP FUNCTIONS ================================
// ===================================================================================================
 
// Calculate Take Profit levels as fixed Risk:Reward multiples
// TP = Entry ± (Risk × Multiplier), where Risk = |Entry - SL|
calculateTargets(entry, sl, isBullish) =>
    float risk = math.abs(entry - sl)
    
    // Define fixed Risk:Reward multipliers
    // TP1 = 1R (1:1), TP1.5 = 1.5R (1:1.5), TP2 = 2R (1:2), TP3 = 3R (1:3)
    float tp1 = na
    float tp1_5 = na
    float tp2 = na
    float tp3 = na
    
    if isBullish
        tp1 := entry + (risk * 1.0)
        tp1_5 := entry + (risk * 1.5)
        tp2 := entry + (risk * 2.0)
        tp3 := entry + (risk * 3.0)
    else
        tp1 := entry - (risk * 1.0)
        tp1_5 := entry - (risk * 1.5)
        tp2 := entry - (risk * 2.0)
        tp3 := entry - (risk * 3.0)
    
    [tp1, tp1_5, tp2, tp3]
 
calculateStopLoss(entry, orbHigh, orbLow, orbRange, atr, mode, isBullish) =>
    float sl = na
    float orbFraction = stopOrbFraction / 100
    
    // ATR fallback if missing or zero
    float validATR = na(atr) or atr == 0 ? orbRange * 0.5 : atr
    
    // Minimum stop distance (0.5% of entry or 30% of ORB range, whichever is larger)
    float minStopDistance = math.max(entry * 0.005, orbRange * 0.3)
    
    if isBullish
        if mode == "ATR"
            sl := entry - (validATR * atrMultiplier)
        else if mode == "ORB %"
            sl := orbLow - (orbRange * orbFraction)
        else if mode == "Swing"
            sl := ta.lowest(low, swingBars)
        else if mode == "Safer"
            float atrSL = entry - (validATR * atrMultiplier)
            float swingSL = ta.lowest(low, swingBars)
            sl := math.min(atrSL, swingSL)
        
        // Validations for LONG
        sl := math.max(sl, entry * 0.5)  // Prevent negative or extreme stops
        sl := math.min(sl, entry - minStopDistance)  // Ensure minimum distance
        
    else
        if mode == "ATR"
            sl := entry + (validATR * atrMultiplier)
        else if mode == "ORB %"
            sl := orbHigh + (orbRange * orbFraction)
        else if mode == "Swing"
            sl := ta.highest(high, swingBars)
        else if mode == "Safer"
            float atrSL = entry + (validATR * atrMultiplier)
            float swingSL = ta.highest(high, swingBars)
            sl := math.max(atrSL, swingSL)
        
        // Validations for SHORT
        sl := math.min(sl, entry * 1.5)  // Prevent extreme stops
        sl := math.max(sl, entry + minStopDistance)  // Ensure minimum distance
    
    sl
 
calculatePositionSize(entry, sl) =>
    float riskAmount = 0.0
    
    if riskMode == "% of Account"
        riskAmount := accountSize * (riskPct / 100)
    else
        riskAmount := fixedRisk
    
    float riskPerShare = math.abs(entry - sl)
    
    // Calculate shares based on risk
    float sharesFromRisk = riskPerShare > 0 ? math.floor(riskAmount / riskPerShare) : 0
    
    // Calculate maximum shares based on position limit
    float maxPositionValue = accountSize * (maxPositionPct / 100)
    float maxSharesFromLimit = entry > 0 ? math.floor(maxPositionValue / entry) : 0
    
    // Use the smaller of the two limits
    float shares = math.min(sharesFromRisk, maxSharesFromLimit)
    float posValue = shares * entry
    float maxLossCalc = shares * riskPerShare
    
    [shares, posValue, maxLossCalc, riskAmount]
 
calculateTPSplits(show1, show1_5, show2, show3) =>
    int activeCount = 0
    if show1
        activeCount += 1
    if show1_5
        activeCount += 1
    if show2
        activeCount += 1
    if show3
        activeCount += 1
    
    float split1 = 0.0
    float split1_5 = 0.0
    float split2 = 0.0
    float split3 = 0.0
    
    if activeCount == 1
        if show1
            split1 := 1.0
        else if show1_5
            split1_5 := 1.0
        else if show2
            split2 := 1.0
        else if show3
            split3 := 1.0
    else if activeCount == 2
        float half = 0.5
        if show1
            split1 := half
            if show1_5
                split1_5 := half
            else if show2
                split2 := half
            else if show3
                split3 := half
        else if show1_5
            split1_5 := half
            if show2
                split2 := half
            else if show3
                split3 := half
        else if show2
            split2 := half
            if show3
                split3 := half
        else if show3
            split3 := half
    else if activeCount >= 3
        float remaining = 0.5
        float remainder = remaining / (activeCount - 1)
        
        bool firstAssigned = false
        if show1
            split1 := firstAssigned ? remainder : 0.5
            firstAssigned := true
        if show1_5
            split1_5 := firstAssigned ? remainder : 0.5
            firstAssigned := true
        if show2
            split2 := firstAssigned ? remainder : 0.5
            firstAssigned := true
        if show3
            split3 := firstAssigned ? remainder : 0.5
            firstAssigned := true
    
    [split1, split1_5, split2, split3]
 
checkHTFBias() =>
    bool bullish = false
    bool bearish = false
    string biasText = ""
    float strength = 0.0
    
    if enableHTF
        [htfClose, htfOpen, htfMA] = request.security(syminfo.tickerid, htfTF, [close, open, ta.ema(close, htfEMA)], barmerge.gaps_off, barmerge.lookahead_off)
        
        if htfMethod == "Price vs MA"
            // Calculate distance percentage from MA
            float distancePct = htfMA > 0 ? ((htfClose - htfMA) / htfMA) * 100 : 0
            strength := math.abs(distancePct)
            
            // Check if strong enough to consider
            bool strongEnough = enableHTF ? strength >= htfMinStrength : false
            
            bullish := htfClose > htfMA and strongEnough
            bearish := htfClose < htfMA and strongEnough
            
        else if htfMethod == "Candle Direction"
            // Calculate candle body size
            float bodySize = math.abs(htfClose - htfOpen)
            float candleRange = htfClose > 0 ? (bodySize / htfClose) * 100 : 0
            strength := candleRange
            
            bullish := htfClose > htfOpen
            bearish := htfClose < htfOpen
        
        if bullish
            biasText := strength > htfMinStrength * 2 ? "✅✅ Strong Aligned" : "✅ Aligned"
        else if bearish
            biasText := strength > htfMinStrength * 2 ? "⚠️ Strong Counter ⚠️" : "⚠️ Counter-Trend"
        else
            biasText := "➖ Neutral/Weak"
    
    [bullish, bearish, biasText, strength]
 
// ===================================================================================================
// ============================================== ORB CORE ===========================================
// ===================================================================================================
 
var float orb5H = na
var float orb5L = na
var float orb15H = na
var float orb15L = na
var float orb30H = na
var float orb30L = na
var float orb60H = na
var float orb60L = na
 
// ===================================================================================================
// ============================================== FUNCTIONS ==========================================
// ===================================================================================================
 
getBodyHigh() => math.max(open, close)
getBodyLow() => math.min(open, close)
bodyClosedAbove(level) => getBodyLow() > level
bodyClosedBelow(level) => getBodyHigh() < level
 
currentTF_minutes = timeframe.in_seconds() / 60
needsConfirmation = currentTF_minutes <= 5
 
stageName(s) => s == 0 ? "Building" : s == 1 ? "ORB 5" : s == 2 ? "ORB 15" : s == 3 ? "ORB 30" : "ORB 60"
 
getBuffer(level, pct) =>
    level * (pct / 100)
 
priceRetestFromAbove(level, buffer) =>
    getBodyLow() < level + buffer
 
priceRetestFromBelow(level, buffer) =>
    getBodyHigh() > level - buffer
 
getLabelSize(sizeStr) =>
    sizeStr == "Tiny" ? size.tiny : sizeStr == "Small" ? size.small : sizeStr == "Normal" ? size.normal : sizeStr == "Large" ? size.large : size.huge
 
calcSuperTrend(atrPeriod, multiplier) =>
    atr = ta.atr(atrPeriod)
    hl2Val = hl2
    
    basicUpperBand = hl2Val + multiplier * atr
    basicLowerBand = hl2Val - multiplier * atr
    
    var float finalUpperBand = na
    var float finalLowerBand = na
    var int trendDir = 1
    
    finalUpperBand := na(finalUpperBand[1]) or basicUpperBand < finalUpperBand[1] or close[1] > finalUpperBand[1] ? basicUpperBand : finalUpperBand[1]
    finalLowerBand := na(finalLowerBand[1]) or basicLowerBand > finalLowerBand[1] or close[1] < finalLowerBand[1] ? basicLowerBand : finalLowerBand[1]
    
    trendDir := na(trendDir[1]) ? 1 : close > finalUpperBand[1] ? 1 : close < finalLowerBand[1] ? -1 : trendDir[1]
    
    [finalLowerBand, finalUpperBand, trendDir]
 
isTrendUp(mode, closePrice, vwapVal, ema12Val, emaCustomVal, stDir) =>
    switch mode
        "VWAP" => closePrice > vwapVal
        "EMA" => closePrice > ema12Val
        "Custom EMA" => closePrice > emaCustomVal
        "SuperTrend" => stDir == 1
        "VWAP+EMA" => closePrice > vwapVal and closePrice > ema12Val
        "VWAP+SuperTrend" => closePrice > vwapVal and stDir == 1
        => true
 
isTrendDown(mode, closePrice, vwapVal, ema12Val, emaCustomVal, stDir) =>
    switch mode
        "VWAP" => closePrice < vwapVal
        "EMA" => closePrice < ema12Val
        "Custom EMA" => closePrice < emaCustomVal
        "SuperTrend" => stDir == -1
        "VWAP+EMA" => closePrice < vwapVal and closePrice < ema12Val
        "VWAP+SuperTrend" => closePrice < vwapVal and stDir == -1
        => true
 
// ===================================================================================================
// ====================================== CLEANUP FUNCTIONS ==========================================
// ===================================================================================================
 
// Clean up old lines to prevent memory issues
cleanupLine(lineObject) =>
    if not na(lineObject)
        array.push(tpslLines, lineObject)
        if array.size(tpslLines) > maxLinesToKeep
            oldLine = array.shift(tpslLines)
            if not na(oldLine)
                line.delete(oldLine)
 
// Clean up old labels to prevent memory issues
cleanupLabel(labelObject) =>
    if not na(labelObject)
        array.push(breakoutLabels, labelObject)
        if array.size(breakoutLabels) > maxLabelsToKeep
            oldLabel = array.shift(breakoutLabels)
            if not na(oldLabel)
                label.delete(oldLabel)
 
// Clean up old boxes to prevent memory issues
cleanupBox(boxObject) =>
    if not na(boxObject)
        array.push(orbBoxes, boxObject)
        if array.size(orbBoxes) > maxBoxesToKeep
            oldBox = array.shift(orbBoxes)
            if not na(oldBox)
                box.delete(oldBox)
 
// ===================================================================================================
// ===================================== FVG DETECTION FUNCTIONS =====================================
// ===================================================================================================
 
// Detect Bullish FVG (gap below)
detectBullishFVG() =>
    bool hasFVG = false
    float fvgTop = na
    float fvgBottom = na
    
    if bar_index >= 2
        if high[2] < low[0]
            hasFVG := true
            fvgTop := low[0]
            fvgBottom := high[2]
    
    [hasFVG, fvgTop, fvgBottom]
 
// Detect Bearish FVG (gap above)
detectBearishFVG() =>
    bool hasFVG = false
    float fvgTop = na
    float fvgBottom = na
    
    if bar_index >= 2
        if low[2] > high[0]
            hasFVG := true
            fvgTop := low[2]
            fvgBottom := high[0]
    
    [hasFVG, fvgTop, fvgBottom]
 
// Check if level is NEAR FVG (more permissive)
hasValidFVGNearLevel(level, isBullish) =>
    bool foundFVG = false
    
    if array.size(fvgTops) > 0
        for i = 0 to array.size(fvgTops) - 1
            if array.get(fvgIsBullish, i) == isBullish
                fvgTop = array.get(fvgTops, i)
                fvgBottom = array.get(fvgBottoms, i)
                fvgSize = fvgTop - fvgBottom
                
                // Use user-defined proximity
                expandedTop = fvgTop + (fvgSize * fvgProximity)
                expandedBottom = fvgBottom - (fvgSize * fvgProximity)
                
                if level >= expandedBottom and level <= expandedTop
                    foundFVG := true
                    break
    
    foundFVG
 
// Clean up old FVG
cleanupOldFVG() =>
    while array.size(fvgBoxes) > maxFVGtoKeep
        oldBox = array.shift(fvgBoxes)
        array.shift(fvgTops)
        array.shift(fvgBottoms)
        array.shift(fvgIsBullish)
        array.shift(fvgIsActive)
        array.shift(fvgStartBar)
        if not na(oldBox)
            box.delete(oldBox)
 
// ===================================================================================================
// ========================================= NEW DAY/SESSION RESET ===================================
// ===================================================================================================
 
// Check if we're in the selected trading session
inSession = not na(time(timeframe.period, tradingSession))
 
// Detect new session or new day
isNewSession = inSession and not inSession[1]
 
// For 24/7 markets, detect session reset by time gap or explicit session change
bool is24_7Market = syminfo.type == "crypto" or (syminfo.type == "forex" and tradingSession == "0000-2359:1234567")
 
// Detect day change differently for different market types
isNewDay = false
if is24_7Market
    // For 24/7 markets: detect gap larger than normal bar spacing (e.g., weekend gap)
    timeSinceLastBar = time - time[1]
    normalBarTime = timeframe.in_seconds() * 1000  // Normal bar duration in ms
    isNewDay := timeSinceLastBar > (normalBarTime * 10)  // Gap 10x larger than normal = new session
else
    // For regular markets: use day change
    isNewDay := ta.change(dayofweek) != 0
 
// Reset on new session OR significant time gap
shouldReset = isNewSession or isNewDay
 
if shouldReset
    cyclesUp := 0
    cyclesDown := 0
    retestCyclesUp := 0
    retestCyclesDown := 0
    barsOutsideAfterBreakUp := 0
    barsOutsideAfterBreakDown := 0
    wentFarEnoughUp := false
    wentFarEnoughDown := false
 
    orb5H := na
    orb5L := na
    orb15H := na
    orb15L := na
    orb30H := na
    orb30L := na
    orb60H := na
    orb60L := na
    
    // Reset session timing
    sessionStartBar := na
    sessionStartTime := na
    
    stage := 0
    buildingStage := 1
    
    cachedLineColor := na
    cachedFillColor := na
    cachedStageName := na
    
    lastLabelH := na
    lastLabelL := na
    lastLabelStage := na
    
    if not na(labH)
        label.delete(labH)
        labH := na
    if not na(labL)
        label.delete(labL)
        labL := na
 
    hadBreakUp := false
    hadBreakDown := false
    breakUpBar := na
    breakDownBar := na
    shownBreakUpLabel := false
    shownBreakDownLabel := false        
    everHadBreakUp := false
    everHadBreakDown := false
    breakoutEntryPrice := na
    hadRetestUp := false
    hadRetestDown := false
    retestUpBar := na
    retestDownBar := na
    awaiting_pullback_up := false
    awaiting_pullback_down := false
    pullback_wait_bar_up := na
    pullback_wait_bar_down := na
    pullback_high_price := na
    pullback_low_price := na
    had_pullback_up := false
    had_pullback_down := false
 
// Reset TP/SL Lines state
    if not na(orbEntryLine)
        line.delete(orbEntryLine)
        orbEntryLine := na
    if not na(orbEntryLabel)
        label.delete(orbEntryLabel)
        orbEntryLabel := na
    if not na(orbSLLine)
        line.delete(orbSLLine)
        orbSLLine := na
    if not na(orbSLLabel)
        label.delete(orbSLLabel)
        orbSLLabel := na
    if not na(orbTP1Line)
        line.delete(orbTP1Line)
        orbTP1Line := na
    if not na(orbTP1Label)
        label.delete(orbTP1Label)
        orbTP1Label := na
    if not na(orbTP1_5Line)
        line.delete(orbTP1_5Line)
        orbTP1_5Line := na
    if not na(orbTP1_5Label)
        label.delete(orbTP1_5Label)
        orbTP1_5Label := na
    if not na(orbTP2Line)
        line.delete(orbTP2Line)
        orbTP2Line := na
    if not na(orbTP2Label)
        label.delete(orbTP2Label)
        orbTP2Label := na
    if not na(orbTP3Line)
        line.delete(orbTP3Line)
        orbTP3Line := na
    if not na(orbTP3Label)
        label.delete(orbTP3Label)
        orbTP3Label := na
    
    orbEntryPrice := na
    orbEntryBar := na
    orbLineColor := na
    orbSLPrice := na
    orbTP1Price := na
    orbTP1_5Price := na
    orbTP2Price := na
    orbTP3Price := na
    orbLinesFrozen := false
    orbTP1Hit := false
    orbTP1_5Hit := false
    orbTP2Hit := false
    orbTP3Hit := false
    orbSLHit := false
    orbTradeDirection := 0
 
    lastDashUpdateBar := bar_index - 100
 
// ===================================================================================================
// ========================================= TIME CALCULATION ========================================
// ===================================================================================================
 
// FALLBACK: If indicator was added mid-session, sessionStartTime will be na
// In this case, initialize it on the first bar we're inside the session
if inSession and na(sessionStartTime)
    sessionStartBar := bar_index
    sessionStartTime := time
 
// Track session start time and bar when new session is detected
if isNewSession
    sessionStartBar := bar_index
    sessionStartTime := time
 
// Calculate minutes elapsed since session start
float minsFromOpen = -1.0
 
if not na(sessionStartTime) and inSession
    // Calculate minutes elapsed since session started
    minsFromOpen := (time - sessionStartTime) / 60000
    
    // Safety cap: prevent extreme values during overnight/weekend gaps
    if minsFromOpen > 480
        minsFromOpen := 480
    
    // Additional safety: if negative (shouldn't happen but just in case)
    if minsFromOpen < 0
        minsFromOpen := 0
 
// ===================================================================================================
// ===================================== GLOBAL CACHE (PERFORMANCE) ==================================
// ===================================================================================================
 
if barstate.isconfirmed or barstate.islast
    // Cache ATR (used in: dashboard, TP/SL lines, breakout labels)
    // Only calculate ATR when actually needed
    bool needATR = everHadBreakUp or everHadBreakDown or showDashboard or not na(orb5H)
    if needATR and bar_index != cachedATRBar
        cachedATR := ta.atr(atrLength)
        cachedATRBar := bar_index
    
    // Cache Volume MA (used in: volume filter, breakout detection)
    if enableVolumeFilter and bar_index != cachedVolumeMABar
        cachedVolumeMA := ta.sma(volume, volumeMaLength)
        cachedVolumeMABar := bar_index
 
// ===================================================================================================
// ======================================= ORB LEVEL BUILDING ========================================
// ===================================================================================================
 
// ORB is an INTRADAY strategy - requires minute-based timeframes
currentTF_seconds = timeframe.in_seconds()
isHTF = currentTF_seconds >= 86400  // Daily or higher (86400 sec = 1 day)
 
// This tracks the high and low during the opening range period
if not isHTF and minsFromOpen >= 0 and minsFromOpen < 480 and barstate.isconfirmed
    
    // Build ORB 5-minute range (first 5 minutes)
    if minsFromOpen < 5
        orb5H := na(orb5H) ? high : math.max(orb5H, high)
        orb5L := na(orb5L) ? low : math.min(orb5L, low)
    
    // Build ORB 15-minute range (first 15 minutes)
    if minsFromOpen < 15
        orb15H := na(orb15H) ? high : math.max(orb15H, high)
        orb15L := na(orb15L) ? low : math.min(orb15L, low)
    
    // Build ORB 30-minute range (first 30 minutes)
    if minsFromOpen < 30
        orb30H := na(orb30H) ? high : math.max(orb30H, high)
        orb30L := na(orb30L) ? low : math.min(orb30L, low)
    
    // Build ORB 60-minute range (first 60 minutes)
    if minsFromOpen < 60
        orb60H := na(orb60H) ? high : math.max(orb60H, high)
        orb60L := na(orb60L) ? low : math.min(orb60L, low)
 
// ===================================================================================================
// ===================================== STAGE & COLOR MANAGEMENT ====================================
// ===================================================================================================
 
// Calculate potential new stage (always calculated)
newStage = minsFromOpen >= 60 ? 4 : minsFromOpen >= 30 ? 3 : minsFromOpen >= 15 ? 2 : minsFromOpen >= 5 ? 1 : 0
 
// Initialize stageChanged (will be updated only on confirmed bars)
stageChanged = false
 
// Only update stage on confirmed bars to prevent repainting
if barstate.isconfirmed or barstate.islast
    stageChanged := newStage != stage
    colorNeedsUpdate = stageChanged or na(cachedLineColor)
     
    // Update stage (needed for correct actH/actL calculation)
    if colorNeedsUpdate
        stage := newStage
        buildingStage := stage + 1
        baseColor = buildingStage == 1 ? orb5LineColor : buildingStage == 2 ? orb15LineColor : buildingStage == 3 ? orb30LineColor : orb60LineColor
        cachedLineColor := baseColor
        cachedFillColor := color.new(baseColor, fillTransparency)
        cachedStageName := stageName(buildingStage)
        
        // Reset breakout flags when moving to new stage
        if stageChanged and stage > 0
            // Reset tracking for new stage (but keep lastBreakUpStage to prevent double signals)
            hadBreakUp := false
            hadBreakDown := false
            hadRetestUp := false
            hadRetestDown := false
            wentFarEnoughUp := false
            wentFarEnoughDown := false
            barsOutsideAfterBreakUp := 0
            barsOutsideAfterBreakDown := 0
 
// Display range (what to SHOW on chart - based on current stage)
float displayH = stage == 0 ? orb5H : stage == 1 ? orb15H : stage == 2 ? orb30H : orb60H
float displayL = stage == 0 ? orb5L : stage == 1 ? orb15L : stage == 2 ? orb30L : orb60L
haveDisplayRange = not na(displayH) and not na(displayL)
 
// Check if price is back inside ORB range - if so, reset breakout flags
// This allows the dashboard to hide trade info when price is no longer outside
if stageChanged and stage > 0 and not na(displayH) and not na(displayL)
    priceBackInside = close <= displayH and close >= displayL
    if priceBackInside
        hadBreakUp := false
        hadBreakDown := false
 
// Breakout range (what to MEASURE for breakouts - completed ORB from PREVIOUS stage)
float breakoutH = stage == 1 ? orb5H : stage == 2 ? orb15H : stage == 3 ? orb30H : stage == 4 ? orb60H : na
float breakoutL = stage == 1 ? orb5L : stage == 2 ? orb15L : stage == 3 ? orb30L : stage == 4 ? orb60L : na
haveBreakoutRange = not na(breakoutH) and not na(breakoutL)
 
// Keep actH/actL for backward compatibility with other parts of code
buildingStage := stage + 1
actH = breakoutH
actL = breakoutL
haveRange = haveBreakoutRange
 
if stageChanged and stage > 0 and alertStageComplete and inSession
    orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
    rangeSize = actH - actL
    rangePct = (rangeSize / actL) * 100
    alertMsgStageComplete := "✅ " + orbStageName + " COMPLETE - " + syminfo.ticker + " | Range: $" + str.tostring(actH, "#.##") + " - $" + str.tostring(actL, "#.##") + " (" + str.tostring(rangePct, "#.#") + "%)"
    sendAlert(alertMsgStageComplete)
    alertStageCompleteTriggered := true
    
// ===================================================================================================
// ========================================== ORB VISUALS ============================================
// ===================================================================================================
 
pH = plot(not isHTF and haveDisplayRange ? displayH : na, "ORB High", color=cachedLineColor, linewidth=1, style=plot.style_linebr)
pL = plot(not isHTF and haveDisplayRange ? displayL : na, "ORB Low", color=cachedLineColor, linewidth=1, style=plot.style_linebr)
pMid = plot(not isHTF and haveDisplayRange and showMidLine ? (displayH + displayL) / 2 : na, "ORB Mid", color=color.new(cachedLineColor, 30), linewidth=1, style=plot.style_line)
bgCol = not isHTF and showBG and haveDisplayRange ? cachedFillColor : na
fill(pH, pL, color=bgCol)
 
// ===================================================================================================
// ======================================= TREND CALCULATION ========================================
// ===================================================================================================
 
// PERFORMANCE: Only calculate trends when filter is enabled
if enableTrendFilter
    // VWAP (expensive - only if mode requires it)
    if str.contains(trendMode, "VWAP")
        trendVWAP := ta.vwap(close)
    
    // EMAs (always needed for EMA modes)
    if trendMode == "EMA" or trendMode == "VWAP+EMA"
        trendEMA12 := ta.ema(close, 12)
    
    if trendMode == "Custom EMA"
        trendEMACustom := ta.ema(close, customEmaLength)
    
    // SuperTrend (expensive - only if mode requires it)
    // Cache result for performance (only recalculate on new bar)
    if str.contains(trendMode, "SuperTrend")
        if barstate.isnew or na(trendSTUp)
            [stLower, stUpper, stDir] = calcSuperTrend(supertrendPeriod, supertrendMult)
            trendSTUp := stLower
            trendSTDown := stUpper
            trendSTDirection := stDir
 
// ===================================================================================================
// ====================================== BREAKOUT DETECTION =========================================
// ===================================================================================================
 
stageAllowed = isStageAllowed(stage, enableORB5Signals, enableORB15Signals, enableORB30Signals, enableORB60Signals)
canDetectBreakout = not isHTF and enableBreakout and stage > 0 and haveBreakoutRange and barstate.isconfirmed and stageAllowed
// Volume MA is now cached globally (see GLOBAL CACHE section above)
volumeMA := cachedVolumeMA
 
// Detect and display FVG (only after ORB is established)
[bullFVG, bullTop, bullBottom] = detectBullishFVG()
[bearFVG, bearTop, bearBottom] = detectBearishFVG()
 
// Create Bullish FVG (only after at least ORB5 is complete)
if bullFVG and showFVG and stage > 0
    fvgBox = box.new(left=bar_index-2, top=bullTop, right=bar_index, bottom=bullBottom,
                     border_color=color.new(fvgBullColor, 0),
                     bgcolor=color.new(fvgBullColor, fvgTransparency),
                     border_width=1,
                     extend=extend.none)  // Changed from extend.right to extend.none
    array.push(fvgBoxes, fvgBox)
    array.push(fvgTops, bullTop)
    array.push(fvgBottoms, bullBottom)
    array.push(fvgIsBullish, true)
    array.push(fvgIsActive, true)
    array.push(fvgStartBar, bar_index-2)
    cleanupOldFVG()
 
// Create Bearish FVG (only after at least ORB5 is complete)
if bearFVG and showFVG and stage > 0
    fvgBox = box.new(left=bar_index-2, top=bearTop, right=bar_index, bottom=bearBottom,
                     border_color=color.new(fvgBearColor, 0),
                     bgcolor=color.new(fvgBearColor, fvgTransparency),
                     border_width=1,
                     extend=extend.none)  // Changed from extend.right to extend.none
    array.push(fvgBoxes, fvgBox)
    array.push(fvgTops, bearTop)
    array.push(fvgBottoms, bearBottom)
    array.push(fvgIsBullish, false)
    array.push(fvgIsActive, true)
    array.push(fvgStartBar, bar_index-2)
    cleanupOldFVG()
 
// Update existing FVG boxes - extend to current bar and close when filled
if showFVG and array.size(fvgBoxes) > 0
    for i = 0 to array.size(fvgBoxes) - 1
        if array.get(fvgIsActive, i)
            fvgTop = array.get(fvgTops, i)
            fvgBottom = array.get(fvgBottoms, i)
            isBull = array.get(fvgIsBullish, i)
            startBar = array.get(fvgStartBar, i)
            currentBox = array.get(fvgBoxes, i)
            boxColor = isBull ? fvgBullColor : fvgBearColor
            
            // Check if FVG is filled (price closed through it)
            fvgFilled = false
            if isBull
                // Bullish FVG filled when price closes below bottom
                fvgFilled := close < fvgBottom
            else
                // Bearish FVG filled when price closes above top
                fvgFilled := close > fvgTop
            
            // Update box to current bar (extends it)
            if not na(currentBox)
                // Update existing box instead of creating new one
                if fvgFilled
                    // FVG is filled - update to final state
                    box.set_right(currentBox, bar_index)
                    box.set_border_style(currentBox, line.style_dashed)
                    array.set(fvgIsActive, i, false)
                else
                    // FVG still active - just extend right edge
                    box.set_right(currentBox, bar_index)
 
// NO-REPAINT: Using barstate.isconfirmed ensures signals only trigger on confirmed bar close
if canDetectBreakout and barstate.isconfirmed
    bufferUp = getBuffer(actH, breakoutBuffer)
    bufferDown = getBuffer(actL, breakoutBuffer)
    
    breakUpNow = false
    
    // Track if price was above ORB on previous bar
    wasAboveORB = close[1] > actH + bufferUp
    isAboveORB = close > actH + bufferUp
    
    // Calculate minimum pullback required
    minPullback = actH * (minPullbackPct / 100)
    hadSignificantPullback = not hadBreakUp or (hadBreakUp and low <= actH + bufferUp + minPullback)
    
    // Breakout occurs when: crosses up AND (first on stage OR had pullback)
    priceCrossedUp = not wasAboveORB and isAboveORB
    isFirstBreakOnStage = lastBreakUpStage != stage and isAboveORB
    
    if (priceCrossedUp and hadSignificantPullback) or isFirstBreakOnStage
        volumeOK = enableVolumeFilter ? hasVolumeConfirmation(volumeMA, volumeMultiplier, strongVolumeMultiplier) : true
        trendOK = enableTrendFilter ? isTrendUp(trendMode, close, trendVWAP, trendEMA12, trendEMACustom, trendSTDirection) : true
        fvgOK = enableFVGFilter ? hasValidFVGNearLevel(actH, true) : true
        
        breakUpNow := volumeOK and trendOK and fvgOK
    
    if breakUpNow
        everHadBreakUp := true
        breakoutEntryPrice := actH
        lastBreakUpStage := stage
        breakUpBar := bar_index
        breakUpBarIndex := bar_index
        hadBreakUp := true
        hadRetestUp := false
        barsOutsideAfterBreakUp := 0
        wentFarEnoughUp := false
 
    breakDownNow = false
    
    // Track if price was below ORB on previous bar
    wasBelowORB = close[1] < actL - bufferDown
    isBelowORB = close < actL - bufferDown
    
    // Calculate minimum pullback required
    minPullback := actL * (minPullbackPct / 100)
    hadSignificantPullback := not hadBreakDown or (hadBreakDown and high >= actL - bufferDown - minPullback)
    
    // Breakout occurs when: crosses down AND (first on stage OR had pullback)
    priceCrossedDown = not wasBelowORB and isBelowORB
    isFirstBreakOnStageDown = lastBreakDownStage != stage and isBelowORB
    
    if (priceCrossedDown and hadSignificantPullback) or isFirstBreakOnStageDown
        volumeOK = enableVolumeFilter ? hasVolumeConfirmation(volumeMA, volumeMultiplier, strongVolumeMultiplier) : true
        trendOK = enableTrendFilter ? isTrendDown(trendMode, close, trendVWAP, trendEMA12, trendEMACustom, trendSTDirection) : true
        fvgOK = enableFVGFilter ? hasValidFVGNearLevel(actL, false) : true
        
        breakDownNow := volumeOK and trendOK and fvgOK
    
    if breakDownNow
        everHadBreakDown := true
        breakoutEntryPrice := actL
        lastBreakDownStage := stage
        breakDownBar := bar_index
        breakDownBarIndex := bar_index
        hadBreakDown := true
        hadRetestDown := false
        barsOutsideAfterBreakDown := 0
        wentFarEnoughDown := false
    
    // ===================================================================================================
    // =================================== PULLBACK FILTER STATE MACHINE =================================
    // ===================================================================================================
    
    if enablePullbackFilter
        if breakUpNow and not awaiting_pullback_up
            awaiting_pullback_up := true
            pullback_wait_bar_up := bar_index
            pullback_high_price := high
            had_pullback_up := false
            breakUpNow := false
        
        if awaiting_pullback_up
            bars_waiting = bar_index - pullback_wait_bar_up
            pullback_amount = pullback_high_price * (pullbackPercent / 100)
            
            if low <= pullback_high_price - pullback_amount
                had_pullback_up := true
            
            if had_pullback_up and close > pullback_high_price
                breakUpNow := true
                awaiting_pullback_up := false
            
            else if bars_waiting >= pullbackTimeout
                breakUpNow := true
                awaiting_pullback_up := false
        
        if breakDownNow and not awaiting_pullback_down
            awaiting_pullback_down := true
            pullback_wait_bar_down := bar_index
            pullback_low_price := low
            had_pullback_down := false
            breakDownNow := false
        
        if awaiting_pullback_down
            bars_waiting = bar_index - pullback_wait_bar_down
            pullback_amount = pullback_low_price * (pullbackPercent / 100)
            
            if high >= pullback_low_price + pullback_amount
                had_pullback_down := true
            
            if had_pullback_down and close < pullback_low_price
                breakDownNow := true
                awaiting_pullback_down := false
            
            else if bars_waiting >= pullbackTimeout
                breakDownNow := true
                awaiting_pullback_down := false
    
    if breakUpNow
        showLabel = false
        if signalMode == "First Only"
            showLabel := not shownBreakUpLabel
        else if signalMode == "Track Cycles"
            showLabel := cyclesUp < maxCycles
            if showLabel
                cyclesUp += 1
        
        if showLabel
            volRatio = volumeMA > 0 ? volume / volumeMA : 0
            orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
            cycleText = signalMode == "Track Cycles" ? " #" + str.tostring(cyclesUp) : ""
            labelText = enableVolumeFilter ? str.format("🔼 BREAK UP:\n{0}{1}\nVol: {2}x", orbStageName, cycleText, str.tostring(volRatio, "#.#")) : str.format("🔼 BREAK UP:\n{0}{1}", orbStageName, cycleText)
            
            lastBreakUpLabel := label.new(bar_index, high * LABEL_OFFSET_ABOVE_PRICE, labelText,
                      style=label.style_label_down,
                      color=color.new(color.green, 0),
                      textcolor=color.white,
                      size=getLabelSize(labelSize))
            cleanupLabel(lastBreakUpLabel)
            
            shownBreakUpLabel := true
            
            // Send breakout alert
            if alertBreakouts
                orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                alertMsgBreakUp := "🔼 BREAKOUT UP " + cycleText + " - " + syminfo.ticker + " @ $" + str.tostring(close, "#.##") + " | " + orbStageName + " H: $" + str.tostring(actH, "#.##")
                sendAlert(alertMsgBreakUp)
                alertBreakUpTriggered := true
            // Create TP/SL Lines for LONG breakout
            if showTPSLLines and enableTargets
                // Delete old lines first
                if not na(orbEntryLine)
                    line.delete(orbEntryLine)
                    orbEntryLine := na
                if not na(orbEntryLabel)
                    label.delete(orbEntryLabel)
                    orbEntryLabel := na
                if not na(orbSLLine)
                    line.delete(orbSLLine)
                    orbSLLine := na
                if not na(orbSLLabel)
                    label.delete(orbSLLabel)
                    orbSLLabel := na
                if not na(orbTP1Line)
                    line.delete(orbTP1Line)
                    orbTP1Line := na
                if not na(orbTP1Label)
                    label.delete(orbTP1Label)
                    orbTP1Label := na
                if not na(orbTP1_5Line)
                    line.delete(orbTP1_5Line)
                    orbTP1_5Line := na
                if not na(orbTP1_5Label)
                    label.delete(orbTP1_5Label)
                    orbTP1_5Label := na
                if not na(orbTP2Line)
                    line.delete(orbTP2Line)
                    orbTP2Line := na
                if not na(orbTP2Label)
                    label.delete(orbTP2Label)
                    orbTP2Label := na
                if not na(orbTP3Line)
                    line.delete(orbTP3Line)
                    orbTP3Line := na
                if not na(orbTP3Label)
                    label.delete(orbTP3Label)
                    orbTP3Label := na
                
                // Calculate targets - SL first, then TPs (TPs are calculated as multiples of Risk)
                entry = actH
                // ATR is now cached globally (see GLOBAL CACHE section above)
                atr = cachedATR
                sl = calculateStopLoss(entry, actH, actL, actH - actL, atr, stopMode, true)  // ✅ true = LONG
                [tp1, tp1_5, tp2, tp3] = calculateTargets(entry, sl, true)
                
                // Store state
                orbEntryPrice := entry
                orbEntryBar := bar_index
                orbLineColor := color.new(color.aqua, 0)
                orbSLPrice := sl
                orbTP1Price := tp1
                orbTP1_5Price := tp1_5
                orbTP2Price := tp2
                orbTP3Price := tp3
                orbLinesFrozen := false
                orbTP1Hit := false
                orbTP1_5Hit := false
                orbTP2Hit := false
                orbTP3Hit := false
                orbSLHit := false
                orbTradeDirection := 1
                
                // Create ENTRY line
                orbEntryLine := line.new(orbEntryBar, orbEntryPrice, orbEntryBar, orbEntryPrice, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                cleanupLine(orbEntryLine)
                string entryText = lineDisplayMode == "Short" ? "ENTRY" : str.format("ENTRY: ${0}", str.tostring(orbEntryPrice, "#.##"))
                orbEntryLabel := label.new(orbEntryBar, orbEntryPrice, entryText, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create SL line
                orbSLLine := line.new(orbEntryBar, orbSLPrice, orbEntryBar, orbSLPrice, xloc=xloc.bar_index, extend=extend.none, color=color.new(color.red, 0), width=1, style=line.style_solid)
                cleanupLine(orbSLLine)
                string slText = lineDisplayMode == "Short" ? "SL" : str.format("SL: ${0}", str.tostring(orbSLPrice, "#.##"))
                orbSLLabel := label.new(orbEntryBar, orbSLPrice, slText, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=color.new(color.red, 0), size=getLabelSize(labelSize))
                
                // Create TP1 line
                if showTP1
                    orbTP1Line := line.new(orbEntryBar, orbTP1Price, orbEntryBar, orbTP1Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP1Line)
                    float tp1Pct = ((orbTP1Price - entry) / entry) * 100
                    string tp1Text = lineDisplayMode == "Short" ? "TP1" : str.format("TP1: ${0} +{1}%", str.tostring(orbTP1Price, "#.##"), str.tostring(tp1Pct, "#.#"))
                    orbTP1Label := label.new(orbEntryBar, orbTP1Price, tp1Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create TP1.5 line
                if showTP1_5
                    orbTP1_5Line := line.new(orbEntryBar, orbTP1_5Price, orbEntryBar, orbTP1_5Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP1_5Line)
                    float tp1_5Pct = ((orbTP1_5Price - entry) / entry) * 100
                    string tp1_5Text = lineDisplayMode == "Short" ? "TP1.5" : str.format("TP1.5: ${0} +{1}%", str.tostring(orbTP1_5Price, "#.##"), str.tostring(tp1_5Pct, "#.#"))
                    orbTP1_5Label := label.new(orbEntryBar, orbTP1_5Price, tp1_5Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create TP2 line
                if showTP2
                    orbTP2Line := line.new(orbEntryBar, orbTP2Price, orbEntryBar, orbTP2Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP2Line)
                    float tp2Pct = ((orbTP2Price - entry) / entry) * 100
                    string tp2Text = lineDisplayMode == "Short" ? "TP2" : str.format("TP2: ${0} +{1}%", str.tostring(orbTP2Price, "#.##"), str.tostring(tp2Pct, "#.#"))
                    orbTP2Label := label.new(orbEntryBar, orbTP2Price, tp2Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create TP3 line
                if showTP3
                    orbTP3Line := line.new(orbEntryBar, orbTP3Price, orbEntryBar, orbTP3Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP3Line)
                    float tp3Pct = ((orbTP3Price - entry) / entry) * 100
                    string tp3Text = lineDisplayMode == "Short" ? "TP3" : str.format("TP3: ${0} +{1}%", str.tostring(orbTP3Price, "#.##"), str.tostring(tp3Pct, "#.#"))
                    orbTP3Label := label.new(orbEntryBar, orbTP3Price, tp3Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))    
        else
            lastBreakUpLabel := label.new(bar_index, high *LABEL_OFFSET_ABOVE_PRICE , "",
                      style=label.style_label_down,
                      color=color.new(color.green, 100),
                      textcolor=color.white,
                      size=size.tiny)
    
    if breakDownNow
        showLabel = false
        if signalMode == "First Only"
            showLabel := not shownBreakDownLabel
        else if signalMode == "Track Cycles"
            showLabel := cyclesDown < maxCycles
            if showLabel
                cyclesDown += 1
        
        if showLabel
            volRatio = volumeMA > 0 ? volume / volumeMA : 0
            orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
            cycleText = signalMode == "Track Cycles" ? " #" + str.tostring(cyclesDown) : ""
            labelText = enableVolumeFilter ? str.format("🔽 BREAK DOWN:\n{0}{1}\nVol: {2}x", orbStageName, cycleText, str.tostring(volRatio, "#.#")) : str.format("🔽 BREAK DOWN:\n{0}{1}", orbStageName, cycleText)
            
            lastBreakDownLabel := label.new(bar_index, low * LABEL_OFFSET_BELOW_PRICE, labelText,
                      style=label.style_label_up, 
                      color=color.new(color.red, 0), 
                      textcolor=color.white, 
                      size=getLabelSize(labelSize))
            cleanupLabel(lastBreakDownLabel)
            
            shownBreakDownLabel := true
            
            // Send breakout alert
            if alertBreakouts
                orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                alertMsgBreakDown := "🔽 BREAKOUT DOWN " + cycleText + " - " + syminfo.ticker + " @ $" + str.tostring(close, "#.##") + " | " + orbStageName + " L: $" + str.tostring(actL, "#.##")
                sendAlert(alertMsgBreakDown)
                alertBreakDownTriggered := true
            
            // Create TP/SL Lines for SHORT breakout
            if showTPSLLines and enableTargets
                // Delete old lines first
                if not na(orbEntryLine)
                    line.delete(orbEntryLine)
                    orbEntryLine := na
                if not na(orbEntryLabel)
                    label.delete(orbEntryLabel)
                    orbEntryLabel := na
                if not na(orbSLLine)
                    line.delete(orbSLLine)
                    orbSLLine := na
                if not na(orbSLLabel)
                    label.delete(orbSLLabel)
                    orbSLLabel := na
                if not na(orbTP1Line)
                    line.delete(orbTP1Line)
                    orbTP1Line := na
                if not na(orbTP1Label)
                    label.delete(orbTP1Label)
                    orbTP1Label := na
                if not na(orbTP1_5Line)
                    line.delete(orbTP1_5Line)
                    orbTP1_5Line := na
                if not na(orbTP1_5Label)
                    label.delete(orbTP1_5Label)
                    orbTP1_5Label := na
                if not na(orbTP2Line)
                    line.delete(orbTP2Line)
                    orbTP2Line := na
                if not na(orbTP2Label)
                    label.delete(orbTP2Label)
                    orbTP2Label := na
                if not na(orbTP3Line)
                    line.delete(orbTP3Line)
                    orbTP3Line := na
                if not na(orbTP3Label)
                    label.delete(orbTP3Label)
                    orbTP3Label := na
                
                // Calculate targets - SL first, then TPs (TPs are calculated as multiples of Risk)
                entry = actL
                // ATR is now cached globally (see GLOBAL CACHE section above)
                atr = cachedATR
                sl = calculateStopLoss(entry, actH, actL, actH - actL, atr, stopMode, false)
                [tp1, tp1_5, tp2, tp3] = calculateTargets(entry, sl, false)
                
                // Store state
                orbEntryPrice := entry
                orbEntryBar := bar_index
                orbLineColor := color.new(color.orange, 0)
                orbSLPrice := sl
                orbTP1Price := tp1
                orbTP1_5Price := tp1_5
                orbTP2Price := tp2
                orbTP3Price := tp3
                orbLinesFrozen := false
                orbTP1Hit := false
                orbTP1_5Hit := false
                orbTP2Hit := false
                orbTP3Hit := false
                orbSLHit := false
                orbTradeDirection := -1
                
                // Create ENTRY line
                orbEntryLine := line.new(orbEntryBar, orbEntryPrice, orbEntryBar, orbEntryPrice, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                cleanupLine(orbEntryLine)
                string entryText = lineDisplayMode == "Short" ? "ENTRY" : str.format("ENTRY: ${0}", str.tostring(orbEntryPrice, "#.##"))
                orbEntryLabel := label.new(orbEntryBar, orbEntryPrice, entryText, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create SL line
                orbSLLine := line.new(orbEntryBar, orbSLPrice, orbEntryBar, orbSLPrice, xloc=xloc.bar_index, extend=extend.none, color=color.new(color.red, 0), width=1, style=line.style_solid)
                cleanupLine(orbSLLine)
                string slText = lineDisplayMode == "Short" ? "SL" : str.format("SL: ${0}", str.tostring(orbSLPrice, "#.##"))
                orbSLLabel := label.new(orbEntryBar, orbSLPrice, slText, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=color.new(color.red, 0), size=getLabelSize(labelSize))
                
                // Create TP1 line
                if showTP1
                    orbTP1Line := line.new(orbEntryBar, orbTP1Price, orbEntryBar, orbTP1Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP1Line)
                    float tp1Pct = ((entry - orbTP1Price) / entry) * 100
                    string tp1Text = lineDisplayMode == "Short" ? "TP1" : str.format("TP1: ${0} +{1}%", str.tostring(orbTP1Price, "#.##"), str.tostring(tp1Pct, "#.#"))
                    orbTP1Label := label.new(orbEntryBar, orbTP1Price, tp1Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create TP1.5 line
                if showTP1_5
                    orbTP1_5Line := line.new(orbEntryBar, orbTP1_5Price, orbEntryBar, orbTP1_5Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP1_5Line)
                    float tp1_5Pct = ((entry - orbTP1_5Price) / entry) * 100
                    string tp1_5Text = lineDisplayMode == "Short" ? "TP1.5" : str.format("TP1.5: ${0} +{1}%", str.tostring(orbTP1_5Price, "#.##"), str.tostring(tp1_5Pct, "#.#"))
                    orbTP1_5Label := label.new(orbEntryBar, orbTP1_5Price, tp1_5Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create TP2 line
                if showTP2
                    orbTP2Line := line.new(orbEntryBar, orbTP2Price, orbEntryBar, orbTP2Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP2Line)
                    float tp2Pct = ((entry - orbTP2Price) / entry) * 100
                    string tp2Text = lineDisplayMode == "Short" ? "TP2" : str.format("TP2: ${0} +{1}%", str.tostring(orbTP2Price, "#.##"), str.tostring(tp2Pct, "#.#"))
                    orbTP2Label := label.new(orbEntryBar, orbTP2Price, tp2Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
                
                // Create TP3 line
                if showTP3
                    orbTP3Line := line.new(orbEntryBar, orbTP3Price, orbEntryBar, orbTP3Price, xloc=xloc.bar_index, extend=extend.none, color=orbLineColor, width=1, style=line.style_solid)
                    cleanupLine(orbTP3Line)
                    float tp3Pct = ((entry - orbTP3Price) / entry) * 100
                    string tp3Text = lineDisplayMode == "Short" ? "TP3" : str.format("TP3: ${0} +{1}%", str.tostring(orbTP3Price, "#.##"), str.tostring(tp3Pct, "#.#"))
                    orbTP3Label := label.new(orbEntryBar, orbTP3Price, tp3Text, xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=orbLineColor, size=getLabelSize(labelSize))
 
        else
            lastBreakDownLabel := label.new(bar_index, low * LABEL_OFFSET_BELOW_PRICE, "",
                      style=label.style_label_up,
                      color=color.new(color.red, 100),
                      textcolor=color.white,
                      size=size.tiny)
 
// ===================================================================================================
// ====================================== BARS OUTSIDE TRACKING ======================================
// ===================================================================================================
 
// Track how many bars price stays outside ORB after breakout
// This determines "committed breakout" vs "failed break"
// Only count bars AFTER the breakout bar (not the breakout bar itself)
if not isHTF and haveRange and barstate.isconfirmed
    
    // Count bars above ORB after breakup
    if hadBreakUp and bar_index > breakUpBarIndex
        if close > actH
            barsOutsideAfterBreakUp += 1
            
            // Check if price went far enough for valid retest
            minRetestDistance = actH * (minRetestDistancePct / 100)
            if close > actH + minRetestDistance
                wentFarEnoughUp := true  
    
    // Count bars below ORB after breakdown
    if hadBreakDown and bar_index > breakDownBarIndex
        if close < actL
            barsOutsideAfterBreakDown += 1
            
            // Check if price went far enough for valid retest
            minRetestDistance = actL * (minRetestDistancePct / 100)
            if close < actL - minRetestDistance
                wentFarEnoughDown := true 
 
// ===================================================================================================
// ======================================= RETEST DETECTION ==========================================
// ===================================================================================================
 
// NO-REPAINT: Retest detection uses confirmed bars to prevent false signals
if not isHTF and enableRetest and haveRange and barstate.isconfirmed
        
    retestBufferCalc = getBuffer(actH, retestBuffer)
        
    if hadBreakUp and not hadRetestUp
        // Use the stored flag instead of checking current bar only
        wentFarEnough = wentFarEnoughUp
        
        isCommittedBreakout = barsOutsideAfterBreakUp >= minBarsOutside
        
        priceBackInside = close < actH + retestBufferCalc and close > actL - retestBufferCalc
        
        barsSinceBreak = bar_index - breakUpBarIndex
        isFailedBreak = everHadBreakUp and barsSinceBreak > 0 and barsSinceBreak <= MAX_BARS_FAILED_BREAK and priceBackInside and not isCommittedBreakout
        if isFailedBreak
            if not na(lastBreakUpLabel)
                label.set_text(lastBreakUpLabel, "⚠️ FAILED BREAK")
                label.set_color(lastBreakUpLabel, color.new(color.orange, 0))
            
            // Send failed break alert
            if alertFailedBreaks
                orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                alertMsgFailedUp := "⚠️ FAILED BREAK UP - " + syminfo.ticker + " @ $" + str.tostring(close, "#.##") + " | " + orbStageName + " - Price back inside range after " + str.tostring(barsSinceBreak) + " bars"
                sendAlert(alertMsgFailedUp)
                alertFailedUpTriggered := true
 
            if cyclesUp > 0
                cyclesUp -= 1
            
            hadBreakUp := false
            barsOutsideAfterBreakUp := 0
        
        else if wentFarEnough and priceBackInside and isCommittedBreakout
            hadRetestUp := true
            retestUpBar := bar_index
            barsOutsideAfterBreakUp := 0  // Reset - price back inside after committed breakout
            
            if signalMode == "Track Cycles"
                hadBreakUp := false
                lastBreakUpStage := 0
 
            showRetestLabel = true
            if signalMode == "Track Cycles"
                totalCyclesUp = cyclesUp + retestCyclesUp
                showRetestLabel := totalCyclesUp < maxCycles
            
            if showRetestLabel
                retestCyclesUp += 1
                orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                retestText = str.format("🔁 RETEST:\n{0} #{1}", orbStageName, str.tostring(retestCyclesUp))
                
                retestLabel = label.new(bar_index, high * LABEL_OFFSET_ABOVE_PRICE, retestText,
                          style=label.style_label_down,
                          color=color.new(color.orange, 20),
                          textcolor=color.white,
                          size=getLabelSize(labelSize))
                cleanupLabel(retestLabel)
                
                // Send retest alert
                if alertRetests
                    orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                    alertMsgRetestUp := "🔁 RETEST UP #" + str.tostring(retestCyclesUp) + " - " + syminfo.ticker + " @ $" + str.tostring(close, "#.##") + " | Back to " + orbStageName + " range"
                    sendAlert(alertMsgRetestUp)
                    alertRetestUpTriggered := true
                    
    if hadBreakDown and not hadRetestDown
        // Use the stored flag instead of checking current bar only
        wentLowEnough = wentFarEnoughDown
        
        isCommittedBreakout = barsOutsideAfterBreakDown >= minBarsOutside
        
        priceBackInside = close > actL - retestBufferCalc and close < actH + retestBufferCalc
        
        barsSinceBreak = bar_index - breakDownBarIndex
        isFailedBreak = everHadBreakDown and barsSinceBreak > 0 and barsSinceBreak <= MAX_BARS_FAILED_BREAK and priceBackInside and not isCommittedBreakout
        if isFailedBreak
            if not na(lastBreakDownLabel)
                label.set_text(lastBreakDownLabel, "⚠️ FAILED BREAK")
                label.set_color(lastBreakDownLabel, color.new(color.orange, 0))
            
            // Send failed break alert
            if alertFailedBreaks
                orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                alertMsgFailedDown := "⚠️ FAILED BREAK DOWN - " + syminfo.ticker + " @ $" + str.tostring(close, "#.##") + " | " + orbStageName + " - Price back inside range after " + str.tostring(barsSinceBreak) + " bars"
                sendAlert(alertMsgFailedDown)
                alertFailedDownTriggered := true
 
            if cyclesDown > 0
                cyclesDown -= 1
            
            hadBreakDown := false
            barsOutsideAfterBreakDown := 0
        
        else if wentLowEnough and priceBackInside and isCommittedBreakout
            hadRetestDown := true
            retestDownBar := bar_index
            barsOutsideAfterBreakDown := 0  // Reset - price back inside after committed breakout
            
            if signalMode == "Track Cycles"
                hadBreakDown := false
                lastBreakDownStage := 0
            
            showRetestLabel = true
            if signalMode == "Track Cycles"
                totalCyclesDown = cyclesDown + retestCyclesDown
                showRetestLabel := totalCyclesDown < maxCycles
            
            if showRetestLabel
                retestCyclesDown += 1
                orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                retestText = str.format("🔁 RETEST:\n{0} #{1}", orbStageName, str.tostring(retestCyclesDown))
                
                retestLabel = label.new(bar_index, low * LABEL_OFFSET_BELOW_PRICE, retestText,
                          style=label.style_label_up,
                          color=color.new(color.orange, 20),
                          textcolor=color.white,
                          size=getLabelSize(labelSize))
                cleanupLabel(retestLabel)
                
                // Send retest alert
                if alertRetests
                    orbStageName = stage == 1 ? "ORB5" : stage == 2 ? "ORB15" : stage == 3 ? "ORB30" : "ORB60"
                    alertMsgRetestDown := "🔁 RETEST DOWN #" + str.tostring(retestCyclesDown) + " - " + syminfo.ticker + " @ $" + str.tostring(close, "#.##") + " | Back to " + orbStageName + " range"
                    sendAlert(alertMsgRetestDown)
                    alertRetestDownTriggered := true
 
// ===================================================================================================
// ========================================== EDGE LABELS ============================================
// ===================================================================================================
 
if not isHTF and showEdgeLabels and haveDisplayRange
 
    stageChanged = stage != lastLabelStage
    priceChangedH = displayH != lastLabelH
    priceChangedL = displayL != lastLabelL
    labelsExist = not na(labH) and not na(labL)
    needsUpdate = stageChanged or priceChangedH or priceChangedL or not labelsExist
    
    if needsUpdate
        labelColor = color.new(cachedLineColor, 0)
        
        if na(labH)
            labH := label.new(bar_index + LABEL_OFFSET_BARS, displayH, cachedStageName, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=labelColor, textcolor=color.white, size=getLabelSize(labelSize))
        else
            label.set_y(labH, displayH)
            if stageChanged
                label.set_text(labH, cachedStageName)
                label.set_color(labH, labelColor)
        
        if na(labL)
            labL := label.new(bar_index + LABEL_OFFSET_BARS, displayL, cachedStageName, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=labelColor, textcolor=color.white, size=getLabelSize(labelSize))
        else
            label.set_y(labL, displayL)
            if stageChanged
                label.set_text(labL, cachedStageName)
                label.set_color(labL, labelColor)
        
        lastLabelH := displayH
        lastLabelL := displayL
        lastLabelStage := stage
    
       // PERFORMANCE: Only update label position on last bar (visual update)
    if barstate.islast
        if not na(labH)
            label.set_x(labH, bar_index + LABEL_OFFSET_BARS)
        if not na(labL)
            label.set_x(labL, bar_index + LABEL_OFFSET_BARS)
 
 // ===================================================================================================
// ================================ TP/SL LINES UPDATE LOOP ==========================================
// ===================================================================================================
 
// Only extend lines when not frozen
// Only extend lines on last bar (visual update)
if not orbLinesFrozen and barstate.islast
    if not na(orbEntryLine)
        line.set_x2(orbEntryLine, bar_index)
    if not na(orbEntryLabel)
        label.set_x(orbEntryLabel, bar_index)
 
    if not na(orbSLLine)
        line.set_x2(orbSLLine, bar_index)
    if not na(orbSLLabel)
        label.set_x(orbSLLabel, bar_index)
 
    if not na(orbTP1Line)
        line.set_x2(orbTP1Line, bar_index)
    if not na(orbTP1Label)
        label.set_x(orbTP1Label, bar_index)
 
    if not na(orbTP1_5Line)
        line.set_x2(orbTP1_5Line, bar_index)
    if not na(orbTP1_5Label)
        label.set_x(orbTP1_5Label, bar_index)
 
    if not na(orbTP2Line)
        line.set_x2(orbTP2Line, bar_index)
    if not na(orbTP2Label)
        label.set_x(orbTP2Label, bar_index)
 
    if not na(orbTP3Line)
        line.set_x2(orbTP3Line, bar_index)
    if not na(orbTP3Label)
        label.set_x(orbTP3Label, bar_index)
 
// Check for hits and freeze (only if not already frozen)
if not orbLinesFrozen and not na(orbEntryPrice) and (barstate.isconfirmed or barstate.islast)
    
    if orbTradeDirection == 1  // LONG
        
        // TP1 Hit
        if not orbTP1Hit and not na(orbTP1Price) and showTP1 and high >= orbTP1Price
            orbTP1Hit := true
            if not na(orbTP1Label)
                string tp1Text = lineDisplayMode == "Short" ? "✅ TP1" : str.format("✅ TP1: ${0}", str.tostring(orbTP1Price, "#.##"))
                label.set_text(orbTP1Label, tp1Text)
                label.set_color(orbTP1Label, color.new(color.green, 0))
            if not na(orbTP1Line)
                line.set_color(orbTP1Line, color.new(color.green, 0))
        
        // TP1.5 Hit
        if not orbTP1_5Hit and not na(orbTP1_5Price) and showTP1_5 and high >= orbTP1_5Price
            orbTP1_5Hit := true
            if not na(orbTP1_5Label)
                string tp1_5Text = lineDisplayMode == "Short" ? "✅ TP1.5" : str.format("✅ TP1.5: ${0}", str.tostring(orbTP1_5Price, "#.##"))
                label.set_text(orbTP1_5Label, tp1_5Text)
                label.set_color(orbTP1_5Label, color.new(color.green, 0))
            if not na(orbTP1_5Line)
                line.set_color(orbTP1_5Line, color.new(color.green, 0))
        
        // TP2 Hit
        if not orbTP2Hit and not na(orbTP2Price) and showTP2 and high >= orbTP2Price
            orbTP2Hit := true
            if not na(orbTP2Label)
                string tp2Text = lineDisplayMode == "Short" ? "✅ TP2" : str.format("✅ TP2: ${0}", str.tostring(orbTP2Price, "#.##"))
                label.set_text(orbTP2Label, tp2Text)
                label.set_color(orbTP2Label, color.new(color.green, 0))
            if not na(orbTP2Line)
                line.set_color(orbTP2Line, color.new(color.green, 0))
        
        // TP3 Hit
        if not orbTP3Hit and not na(orbTP3Price) and showTP3 and high >= orbTP3Price
            orbTP3Hit := true
            if not na(orbTP3Label)
                string tp3Text = lineDisplayMode == "Short" ? "✅ TP3" : str.format("✅ TP3: ${0}", str.tostring(orbTP3Price, "#.##"))
                label.set_text(orbTP3Label, tp3Text)
                label.set_color(orbTP3Label, color.new(color.green, 0))
            if not na(orbTP3Line)
                line.set_color(orbTP3Line, color.new(color.green, 0))
        
        // Check if last TP hit - FREEZE (LONG)
        lastTPHit = false
        if showTP3 and orbTP3Hit
            lastTPHit := true
        else if not showTP3 and showTP2 and orbTP2Hit
            lastTPHit := true
        else if not showTP3 and not showTP2 and showTP1_5 and orbTP1_5Hit
            lastTPHit := true
        else if not showTP3 and not showTP2 and not showTP1_5 and showTP1 and orbTP1Hit
            lastTPHit := true
        
        if lastTPHit
            orbLinesFrozen := true
            everHadBreakUp := false  // Clear dashboard when last TP hit
        
        // SL Hit - FREEZE
        if not orbSLHit and not na(orbSLPrice) and low <= orbSLPrice
            orbSLHit := true
            orbLinesFrozen := true
            everHadBreakUp := false  // Clear dashboard when SL hit
            if not na(orbSLLabel)
                string slText = lineDisplayMode == "Short" ? "❌ SL" : str.format("❌ SL: ${0}", str.tostring(orbSLPrice, "#.##"))
                label.set_text(orbSLLabel, slText)
    
    else if orbTradeDirection == -1  // SHORT
        
        // TP1 Hit
        if not orbTP1Hit and not na(orbTP1Price) and showTP1 and low <= orbTP1Price
            orbTP1Hit := true
            if not na(orbTP1Label)
                string tp1Text = lineDisplayMode == "Short" ? "✅ TP1" : str.format("✅ TP1: ${0}", str.tostring(orbTP1Price, "#.##"))
                label.set_text(orbTP1Label, tp1Text)
                label.set_color(orbTP1Label, color.new(color.green, 0))
            if not na(orbTP1Line)
                line.set_color(orbTP1Line, color.new(color.green, 0))
        
        // TP1.5 Hit
        if not orbTP1_5Hit and not na(orbTP1_5Price) and showTP1_5 and low <= orbTP1_5Price
            orbTP1_5Hit := true
            if not na(orbTP1_5Label)
                string tp1_5Text = lineDisplayMode == "Short" ? "✅ TP1.5" : str.format("✅ TP1.5: ${0}", str.tostring(orbTP1_5Price, "#.##"))
                label.set_text(orbTP1_5Label, tp1_5Text)
                label.set_color(orbTP1_5Label, color.new(color.green, 0))
            if not na(orbTP1_5Line)
                line.set_color(orbTP1_5Line, color.new(color.green, 0))
        
        // TP2 Hit
        if not orbTP2Hit and not na(orbTP2Price) and showTP2 and low <= orbTP2Price
            orbTP2Hit := true
            if not na(orbTP2Label)
                string tp2Text = lineDisplayMode == "Short" ? "✅ TP2" : str.format("✅ TP2: ${0}", str.tostring(orbTP2Price, "#.##"))
                label.set_text(orbTP2Label, tp2Text)
                label.set_color(orbTP2Label, color.new(color.green, 0))
            if not na(orbTP2Line)
                line.set_color(orbTP2Line, color.new(color.green, 0))
        
        // TP3 Hit
        if not orbTP3Hit and not na(orbTP3Price) and showTP3 and low <= orbTP3Price
            orbTP3Hit := true
            if not na(orbTP3Label)
                string tp3Text = lineDisplayMode == "Short" ? "✅ TP3" : str.format("✅ TP3: ${0}", str.tostring(orbTP3Price, "#.##"))
                label.set_text(orbTP3Label, tp3Text)
                label.set_color(orbTP3Label, color.new(color.green, 0))
            if not na(orbTP3Line)
                line.set_color(orbTP3Line, color.new(color.green, 0))
        
        // Check if last TP hit - FREEZE (SHORT)
        lastTPHit = false
        if showTP3 and orbTP3Hit
            lastTPHit := true
        else if not showTP3 and showTP2 and orbTP2Hit
            lastTPHit := true
        else if not showTP3 and not showTP2 and showTP1_5 and orbTP1_5Hit
            lastTPHit := true
        else if not showTP3 and not showTP2 and not showTP1_5 and showTP1 and orbTP1Hit
            lastTPHit := true
        
        if lastTPHit
            orbLinesFrozen := true
            everHadBreakDown := false  // Clear dashboard when last TP hit
        
        // SL Hit - FREEZE
        if not orbSLHit and not na(orbSLPrice) and high >= orbSLPrice
            orbSLHit := true
            orbLinesFrozen := true
            everHadBreakDown := false  // Clear dashboard when SL hit
            if not na(orbSLLabel)
                string slText = lineDisplayMode == "Short" ? "❌ SL" : str.format("❌ SL: ${0}", str.tostring(orbSLPrice, "#.##"))
                label.set_text(orbSLLabel, slText)
    
    // EOD Freeze
    if freezeOnEOD and not inSession and inSession[1]
        orbLinesFrozen := true
 
// ===================================================================================================
// ============================================ DASHBOARD ============================================
// ===================================================================================================
 
// Calculate HTF bias only when HTF bar changes (uses request.security - expensive)
var string cachedHTFBiasText = ""
var int lastHTFCheckBar = -1
var bool cachedHTFBullish = false
var bool cachedHTFBearish = false
var float cachedHTFStrength = 0.0
 
// Detect when new HTF bar appears
bool newHTFBar = barstate.isconfirmed ? ta.change(time(htfTF)) != 0 : false
 
if enableHTF and (newHTFBar or na(cachedHTFBiasText)) and barstate.islast
    [htfBullishCalc, htfBearishCalc, htfBiasTextTemp, htfStrengthTemp] = checkHTFBias()
    cachedHTFBiasText := htfBiasTextTemp
    cachedHTFBullish := htfBullishCalc
    cachedHTFBearish := htfBearishCalc
    lastHTFCheckBar := bar_index
 
// Use cached values for HTF variables
htfBullish := cachedHTFBullish  
htfBearish := cachedHTFBearish
htfBiasText := cachedHTFBiasText
 
// Update dashboard only on confirmed bars or significant changes (performance optimization)
// Only show dashboard during session or if ORB was built today
bool hasActiveORB = inSession or not na(orb5H)
 
// Update dashboard ONLY on last bar
if showDashboard and hasActiveORB and barstate.islast
    lastDashUpdateBar := bar_index
    
    // Determine table position
    tablePos = dashPos == "Top Left" ? position.top_left : dashPos == "Top Right" ? position.top_right : dashPos == "Bottom Left" ? position.bottom_left : dashPos == "Bottom Right" ? position.bottom_right : position.middle_right
    
    // Determine text size
    txtSize = dashSize == "Small" ? size.small : dashSize == "Normal" ? size.normal : dashSize == "Large" ? size.large : size.auto
    
    // Determine colors based on theme
    bool isDarkTheme = dashTheme == "Dark"
    color bgColor = isDarkTheme ? color.new(color.black , 0) : color.new(color.white, 10)
    color txtColor = isDarkTheme ? color.white : color.black
    color headerColor = isDarkTheme ? color.new(color.aqua, 30) : color.new(color.silver, 30)
    
    // Create table if it doesn't exist
    if na(dashTable)
        dashTable := table.new(tablePos, 2, 25, bgcolor=bgColor, border_width=0, frame_width=1)
    else
        table.clear(dashTable, 0, 0, 1, 24)
        table.set_position(dashTable, tablePos)
    
    // Row counter (starting from 1 to avoid merge_cells bug in row 0)
    int row = 0
    
    // Header - Dynamic with Status
    string headerTitle = "📊 ORB DASHBOARD"
    color headerBgColor = headerColor
    string headerTooltip = "ORB Dashboard\n\nShows Opening Range Breakout status and trade setup.\n\n⚪ Default = Price in range\n⬆️ Green = Breakout UP (long signal)\n⬇️ Red = Breakout DOWN (short signal)\n\nThe dashboard shows the active breakout trade parameters including entry, targets, and stop loss."
 
    if everHadBreakUp
        headerTitle := "📊 ORB DASHBOARD ⬆️"
        headerBgColor := color.new(color.green, 10)
    else if everHadBreakDown
        headerTitle := "📊 ORB DASHBOARD ⬇️"
        headerBgColor := color.new(color.red, 10)
 
    // Create header with proper merge
    table.cell(dashTable, 0, row, headerTitle, text_color=txtColor, text_size=txtSize, text_halign=text.align_center, bgcolor=headerBgColor, tooltip=headerTooltip)
    table.cell(dashTable, 1, row, "", text_halign=text.align_center, bgcolor=headerBgColor)
    table.merge_cells(dashTable, 0, row, 1, row)
    row += 1
    
    // HTF Warning (if on high timeframe)
    if isHTF
        htfWarningText = "⚠️ You're on " + timeframe.period + " TF - Switch to LTF"
        table.cell(dashTable, 0, row, htfWarningText, text_color=color.white, text_size=txtSize, bgcolor=color.new(color.orange, 0), text_halign=text.align_center)
        table.cell(dashTable, 1, row, "", bgcolor=color.new(color.orange, 0))
        table.merge_cells(dashTable, 0, row, 1, row)
        row += 1
    
    // Range info (only show after first ORB completes)
    if showRange and not isHTF and stage > 0
        orbRange = actH - actL
        rangePct = (orbRange / actL) * 100
        rangeTooltip = "ORB Range Size\n\nShows the width of the Opening Range in both dollars and percentage.\n\n• Large range (>1.5%) = High volatility\n• Medium range (0.5-1.5%) = Normal volatility\n• Small range (<0.5%) = Low volatility\n\nLarger ranges may require wider stops and smaller position sizes."
        
        table.cell(dashTable, 0, row, "Range:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=rangeTooltip)
        table.cell(dashTable, 1, row, str.format("${0} ({1}% width)", str.tostring(orbRange, "#.##"), str.tostring(rangePct, "#.#")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
        row += 1
    
    // Volume
    if showVol and enableVolumeFilter and not isHTF
        volRatio = volumeMA > 0 ? volume / volumeMA : 0
        volStatus = volRatio >= strongVolumeMultiplier ? "✅ Strong" : volRatio >= volumeMultiplier ? "✅ Good" : "⚠️ Weak"
        table.cell(dashTable, 0, row, "Volume:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left)
        table.cell(dashTable, 1, row, str.format("{0}× {1}", str.tostring(volRatio, "#.#"), volStatus), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
        row += 1
    
    // Trend
    if showTrend and enableTrendFilter and not isHTF
        trendStatus = "➖ Neutral"
        if isTrendUp(trendMode, close, trendVWAP, trendEMA12, trendEMACustom, trendSTDirection)
            trendStatus := "✅ Bullish"
        else if isTrendDown(trendMode, close, trendVWAP, trendEMA12, trendEMACustom, trendSTDirection)
            trendStatus := "⚠️ Bearish"
        
        table.cell(dashTable, 0, row, "Trend:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left)
        table.cell(dashTable, 1, row, trendStatus, text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
        row += 1
    
    // HTF Bias (use cached values updated earlier)
    if enableHTF
        htfTooltip = "Higher Timeframe Bias\n\nShows if the broader market trend supports your breakout direction.\n\n✅ Aligned: HTF trend matches breakout direction (good)\n⚠️ Counter-Trend: HTF trend opposes breakout (risky)\n➖ Neutral: No clear HTF trend\n\nBreakouts WITH HTF trend have higher success rates."
        table.cell(dashTable, 0, row, "HTF Bias:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=htfTooltip)
        table.cell(dashTable, 1, row, cachedHTFBiasText, text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
        row += 1
    
    // Trade info section (only if breakout happened AND targets enabled)
    if enableTargets and (everHadBreakUp or everHadBreakDown)
        
        // Blank row for visual separation
        table.cell(dashTable, 0, row, "", text_color=txtColor, text_size=size.tiny)
        table.cell(dashTable, 1, row, "", text_color=txtColor, text_size=size.tiny)
        row += 1
        
        /// Calculate trade parameters - SL first, then TPs (TPs are calculated as multiples of Risk)
        isBullish = everHadBreakUp
        entry = na(breakoutEntryPrice) ? (isBullish ? actH : actL) : breakoutEntryPrice
        
        // ATR is now cached globally (see GLOBAL CACHE section above)
        float atrValue = cachedATR
        float orbRange = actH - actL
        atr = na(atrValue) ? orbRange : atrValue
        
        sl = calculateStopLoss(entry, actH, actL, orbRange, atr, stopMode, isBullish)
        [tp1, tp1_5, tp2, tp3] = calculateTargets(entry, sl, isBullish)
        
        // Position Sizing (before entry)
        if enablePosSizing
            // Calculate position size
            [shares, posValue, maxLossCalc, riskAmount] = calculatePositionSize(entry, sl)
            
            // Position header  
            table.cell(dashTable, 0, row, "Position Size:", text_color=txtColor, text_size=txtSize, text_halign=text.align_center, bgcolor=headerColor)
            table.cell(dashTable, 1, row, "", bgcolor=headerColor)
            table.merge_cells(dashTable, 0, row, 1, row)
            row += 1
            
            // Max shares
            sharesTooltip = "Maximum Position Size\n\nNumber of shares you can buy while staying within your risk limits.\n\nCALCULATED BY:\nRisk Amount ÷ Distance to Stop Loss\n\nEXAMPLE:\n• Risk: $150\n• Entry: $10.00\n• Stop: $9.50 (50¢ distance)\n• Max Shares: 300 shares ($150 ÷ $0.50)\n• Position Value: $3,000\n\nThis ensures consistent risk management across all trades."
            table.cell(dashTable, 0, row, "Max Shares:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=sharesTooltip)
            table.cell(dashTable, 1, row, str.format("{0} (${1})", str.tostring(shares, "#"), str.tostring(posValue, "#,###")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
            row += 1
            
            // Risk amount
            riskPctOfAcct = (riskAmount / accountSize) * 100
            riskTooltip = "Risk Amount\n\nTotal dollar amount you'll lose if stop loss is hit.\n\nSHOWS:\n• Dollar amount at risk\n• Percentage of total account\n\nRECOMMENDED RISK LEVELS:\n• 0.5-1% of account (conservative)\n• 1-2% of account (balanced)\n• 2-3% of account (aggressive)\n\nNEVER risk more than 3% on a single trade.\n\nThis is your MAXIMUM LOSS - stick to it!"
            table.cell(dashTable, 0, row, "Risk:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=riskTooltip)
            table.cell(dashTable, 1, row, str.format("${0} ({1}%)", str.tostring(riskAmount, "#"), str.tostring(riskPctOfAcct, "#.#")), text_color=color.new(color.orange, 0), text_size=txtSize, text_halign=text.align_right)
            row += 1
            
            // Blank row after position size
            table.cell(dashTable, 0, row, "", text_color=txtColor, text_size=size.tiny, height=0.5)
            table.cell(dashTable, 1, row, "", text_color=txtColor, text_size=size.tiny, height=0.5)
            row += 1
 
        // Entry
        entryTooltip = "Entry Price\n\nYour suggested entry point for the trade.\n\nFOR LONG: ORB High level (where breakout occurred)\nFOR SHORT: ORB Low level (where breakdown occurred)\n\nThis is where you would buy/sell to enter the position.\n\nNOTE: Market entry may differ slightly due to slippage."
        table.cell(dashTable, 0, row, "Entry:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=entryTooltip)
        table.cell(dashTable, 1, row, str.format("${0}", str.tostring(entry, "#.##")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
        row += 1
        
        // Stop Loss
        slDist = math.abs(entry - sl)
        slPct = (slDist / entry) * 100
        stopTooltip = "Stop Loss\n\nYour exit price if trade goes against you. Limits maximum loss per trade.\n\nCALCULATED BY: " + stopMode + " method\n\nDISTANCE: Shows $ distance and % from entry\n\nIMPORTANT: Always use a stop loss to protect your capital. Never hold a losing position hoping it will recover."
        table.cell(dashTable, 0, row, "Stop:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=stopTooltip)
        table.cell(dashTable, 1, row, str.format("${0} ({1}%)", str.tostring(sl, "#.##"), str.tostring(slPct, "#.#")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
        row += 1
        
        // Targets (only enabled ones)
        if showTP1
            tp1Dist = math.abs(tp1 - entry)
            tp1Pct = (tp1Dist / entry) * 100
            tp1Label = orbTP1Hit ? "✅ TP1:" : "TP1:"
            tp1Tooltip = "Take Profit 1 (1R)\n\nFirst profit target at 1× Risk distance.\n\n1R = Risk/Reward ratio of 1:1\n\nIf you risk $100, TP1 gives you $100 profit.\n\nSTRATEGY:\n• Conservative: Exit full position at TP1\n• Balanced: Take 50% profit, hold rest for TP2\n• Aggressive: Hold through for higher targets\n\n✅ = Target hit"
            table.cell(dashTable, 0, row, tp1Label, text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=tp1Tooltip)
            table.cell(dashTable, 1, row, str.format("${0} (+{1}%)", str.tostring(tp1, "#.##"), str.tostring(tp1Pct, "#.#")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
            row += 1
        
        if showTP1_5
            tp1_5Dist = math.abs(tp1_5 - entry)
            tp1_5Pct = (tp1_5Dist / entry) * 100
            tp1_5Label = orbTP1_5Hit ? "✅ TP1.5:" : "TP1.5:"
            tp1_5Tooltip = "Take Profit 1.5 (1.5R)\n\nIntermediate target at 1.5× Risk distance.\n\n1.5R = Risk/Reward ratio of 1:1.5\n\nIf you risk $100, TP1.5 gives you $150 profit.\n\nUSEFUL FOR:\n• Scaling out of positions gradually\n• Capturing middle-ground profits\n• Adjusting to market volatility\n\n✅ = Target hit"
            table.cell(dashTable, 0, row, tp1_5Label, text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=tp1_5Tooltip)
            table.cell(dashTable, 1, row, str.format("${0} (+{1}%)", str.tostring(tp1_5, "#.##"), str.tostring(tp1_5Pct, "#.#")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
            row += 1
        
        if showTP2
            tp2Dist = math.abs(tp2 - entry)
            tp2Pct = (tp2Dist / entry) * 100
            tp2Label = orbTP2Hit ? "✅ TP2:" : "TP2:"
            tp2Tooltip = "Take Profit 2 (2R)\n\nMain profit target at 2× Risk distance.\n\n2R = Risk/Reward ratio of 1:2\n\nIf you risk $100, TP2 gives you $200 profit.\n\nThis is the STANDARD target for most ORB traders.\n\nSTRATEGY:\n• Exit remaining position at TP2\n• Move stop to breakeven after TP1 hit\n• Let runners go to TP3 if strong momentum\n\n✅ = Target hit"
            table.cell(dashTable, 0, row, tp2Label, text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=tp2Tooltip)
            table.cell(dashTable, 1, row, str.format("${0} (+{1}%)", str.tostring(tp2, "#.##"), str.tostring(tp2Pct, "#.#")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
            row += 1
        
        if showTP3
            tp3Dist = math.abs(tp3 - entry)
            tp3Pct = (tp3Dist / entry) * 100
            tp3Label = orbTP3Hit ? "✅ TP3:" : "TP3:"
            tp3Tooltip = "Take Profit 3 (3R)\n\nExtended profit target at 3× Risk distance.\n\n3R = Risk/Reward ratio of 1:3\n\nIf you risk $100, TP3 gives you $300 profit.\n\nONLY FOR:\n• Strong trending days\n• High momentum breakouts\n• Extended runners\n\nMOST TRADES won't reach TP3.\n\nSTRATEGY: Trail stop or use for 'lottery tickets'\n\n✅ = Target hit"
            table.cell(dashTable, 0, row, tp3Label, text_color=txtColor, text_size=txtSize, text_halign=text.align_left, tooltip=tp3Tooltip)
            table.cell(dashTable, 1, row, str.format("${0} (+{1}%)", str.tostring(tp3, "#.##"), str.tostring(tp3Pct, "#.#")), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
            row += 1
 
        // Risk/Reward
        if showRisk
            riskDist = math.abs(entry - sl)
            rewardDist = showTP2 ? math.abs(tp2 - entry) : showTP1 ? math.abs(tp1 - entry) : showTP1_5 ? math.abs(tp1_5 - entry) : showTP3 ? math.abs(tp3 - entry) : math.abs(tp2 - entry)
            rrRatio = riskDist > 0 ? rewardDist / riskDist : 0
            rrStatus = rrRatio >= 2.0 ? "✅" : rrRatio >= 1.5 ? "⚠️" : "❌"
            
            table.cell(dashTable, 0, row, "R/R Ratio:", text_color=txtColor, text_size=txtSize, text_halign=text.align_left)
            table.cell(dashTable, 1, row, str.format("1:{0} {1}", str.tostring(rrRatio, "#.#"), rrStatus), text_color=txtColor, text_size=txtSize, text_halign=text.align_right)
            row += 1
        
 
// ===================================================================================================
// ====================================== INDIVIDUAL ALERTS =========================================
// ===================================================================================================
// These create separate alert conditions that users can select individually
// when creating alerts. This gives more control than the generic "Any alert() function call"
 
alertcondition(alertBreakUpTriggered and enableAlerts and alertBreakouts, 
               title="🔼 ORB Breakout UP", 
               message="{{ticker}} - Breakout ABOVE ORB High @ ${{close}}")
 
alertcondition(alertBreakDownTriggered and enableAlerts and alertBreakouts, 
               title="🔽 ORB Breakout DOWN", 
               message="{{ticker}} - Breakout BELOW ORB Low @ ${{close}}")
 
alertcondition(alertRetestUpTriggered and enableAlerts and alertRetests, 
               title="🔁 ORB Retest UP", 
               message="{{ticker}} - Retesting ORB after breakout UP @ ${{close}}")
 
alertcondition(alertRetestDownTriggered and enableAlerts and alertRetests, 
               title="🔁 ORB Retest DOWN", 
               message="{{ticker}} - Retesting ORB after breakout DOWN @ ${{close}}")
 
alertcondition(alertFailedUpTriggered and enableAlerts and alertFailedBreaks, 
               title="⚠️ ORB Failed Break UP", 
               message="{{ticker}} - Failed breakout UP, back inside ORB @ ${{close}}")
 
alertcondition(alertFailedDownTriggered and enableAlerts and alertFailedBreaks, 
               title="⚠️ ORB Failed Break DOWN", 
               message="{{ticker}} - Failed breakout DOWN, back inside ORB @ ${{close}}")
 
alertcondition(alertStageCompleteTriggered and enableAlerts and alertStageComplete, 
               title="✅ ORB Stage Complete", 
               message="{{ticker}} - ORB Stage completed") 
 
// END OF CODE

← 返回列表