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