本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。
//@version=6
indicator("Luxy UT GOD - UT-BOT Forecast", shorttitle="Luxy UT God Mode", overlay=true, max_bars_back=5000, max_lines_count=500, max_labels_count=500, max_boxes_count=500)
// ================================================================================
// ATTRIBUTION & CREDITS
// ================================================================================
// This indicator implements proven trading concepts using entirely original code.
//
// CONCEPTUAL INSPIRATIONS (Reimplemented from scratch):
// ------------------------------------------------------------
// • UT Bot ATR Trailing Logic
// Original concept by @QuantNomad
// Our implementation adds: volume weighting, momentum adjustment, composite SL,
// multi-filter system (swing, %, 2-bar).
//
// LICENSE: Open Source - Feel free to learn from and improve upon this code
// Please maintain attribution when reusing components
//======================================================
//======================== GROUPS ======================
//======================================================
GROUP_UT = "================== UT Bot Settings =================="
GROUP_L1 = "🛡️ Anti-Whipsaw (Regime) ___________________________"
GROUP_FILTERS = "🎯 Signal Filters __________________________________"
GROUP_SL = "💵 Stoploss and Take Profit_________________________"
GROUP_L2 = "🔄 Adaptive Parameters _____________________________"
GROUP_L3 = "📈 MTF Confirmation ________________________________"
GROUP_CALC = "🔢 Risk Calculator _________________________________"
GROUP_PRED = "🔮 Trend Prediction ________________________________"
//======================================================
//===================== CONSTANTS ======================
//======================================================
// TP R:R Constants
const float TP1 = 1.0
const float TP1_5 = 1.5
const float TP2 = 2.0
const float TP3 = 3.0
// Asset Types
const string ASSET_AUTO = "Auto-Detect"
const string ASSET_STOCK = "Stock"
const string ASSET_CRYPTO = "Crypto"
const string ASSET_FOREX = "Forex"
const string ASSET_FUTURES = "Futures"
const string ASSET_INDEX = "Index"
const string ASSET_FUND = "Fund/ETF"
const string ASSET_CFD = "CFD"
const string ASSET_BOND = "Bond"
// Adaptive Modes
const string ADAPT_FIXED = "Fixed"
const string ADAPT_DYNAMIC = "Dynamic"
const string ADAPT_AGGRESSIVE = "Aggressive"
// MTF Timeframes
const string MTF_AUTO = "Auto (×4)"
const string MTF_5M = "5m"
const string MTF_15M = "15m"
const string MTF_30M = "30m"
const string MTF_1H = "1H"
const string MTF_4H = "4H"
const string MTF_D = "D"
const string MTF_W = "W"
//======================================================
//======================== INPUTS ======================
//======================================================
// UT Bot - Display Options
utShowLabels = input.bool(true, "Buy/Sell Labels", group=GROUP_UT, inline="ut2")
labelSizeStr = input.string("Small", "| Size", options=["Tiny", "Small", "Normal", "Large"], group=GROUP_UT, inline="ut2", tooltip = "Size of all labels on chart\n• Tiny: Minimal\n• Small: Default ⭐\n• Normal: Medium\n• Large: Easy to see")
utShowLine = input.bool(false, "UT Line", group=GROUP_UT, inline="ut3")
utShowRibbon = input.bool(false, "Ribbon Fill", group=GROUP_UT, inline="ut3", tooltip = "Buy/Sell Labels: Show BUY/SELL signals on chart\nUT Line: Plot the ATR trailing stop line\nColor Bars: Aqua=bullish, Orange=bearish\nRibbon Fill: Gradient fill between price and UT line")
utColorBars = input.bool(false, "Color Bars", group=GROUP_UT, inline="ut3")
// Status Table
i_showTable = input.bool(true, "Show Table", group=GROUP_UT, inline="tb1")
i_tablePos = input.string("Top Right", "Position", options=["Top Left", "Top Right", "Bottom Left", "Bottom Right"], group=GROUP_UT, inline="tb1")
i_tableSize = input.string("Normal", "Size", options=["Tiny", "Small", "Normal", "Large"], group=GROUP_UT, inline="tb1", tooltip = "Table text size\n• Tiny: Minimal footprint\n• Small: Balanced ⭐\n• Normal: Easy to read")
// UT Bot - Core Settings
utKey = input.float(1.2, "Sensitivity", group=GROUP_UT, step=0.1, minval=0.1, inline="ut1")
utAtrPeriod = input.int(7, "/ ATR Period", group=GROUP_UT, minval=1, inline="ut1", tooltip = "SENSITIVITY (Key × ATR):\nMultiplier for trailing stop distance\n• Higher = wider stop, fewer flips\n• Lower = tighter stop, more signals\n\nATR PERIOD:\nLookback for volatility\n\nPRESETS:\n• Scalp (1-5m): Key 0.9-1.2, ATR 5-7\n• Intraday (5-15m): Key 1.3-1.6, ATR 7-10 ⭐\n• Swing (D/W): Key 1.7-2.2, ATR 10-14")
// Convert label size string to size constant
labelSize = switch labelSizeStr
"Tiny" => size.tiny
"Small" => size.small
"Normal" => size.normal
"Large" => size.large
=> size.small
// Layer 1: Anti-Whipsaw (Regime Detection)
i_enableLayer1 = input.bool(true, "Enable Anti-Whipsaw", group=GROUP_L1, inline="L1a", tooltip = "Master switch for Anti-Whipsaw filter\n\nWhen ON: Signals filtered by ADX regime\nWhen OFF: All UT signals pass through")
i_adxPeriod = input.int(14, "ADX", minval=5, maxval=50, group=GROUP_L1, inline="L1b")
i_adxThreshold = input.int(20, "/ Thresh", minval=10, maxval=40, group=GROUP_L1, inline="L1b", tooltip = "ADX PERIOD: Lookback for calculation\n• 10-14: Responsive ⭐\n• 20+: Smoother\n\nTHRESHOLD: Min ADX for 'trending'\n• < Threshold = Choppy (signals blocked)\n• >= Threshold = Trending (signals allowed)\n• 20-25: Typical ⭐")
i_cooldownBars = input.int(3, "Cooldown", minval=0, maxval=20, group=GROUP_L1, inline="L1c")
i_confirmBars = input.int(1, "/ Confirm", minval=1, maxval=5, group=GROUP_L1, inline="L1c", tooltip = "COOLDOWN: Min bars between signals\n• 0 = Disabled\n• 2-5 = Typical ⭐\n• Prevents rapid flip-flop\n\nCONFIRM: Bars to confirm direction\n• 1 = Immediate ⭐\n• 2-3 = Conservative")
// Layer 2: Adaptive Parameters
i_assetType = input.string(ASSET_AUTO, "Asset Type", options=[ASSET_AUTO, ASSET_STOCK, ASSET_CRYPTO, ASSET_FOREX, ASSET_FUTURES, ASSET_INDEX, ASSET_FUND, ASSET_CFD, ASSET_BOND], group=GROUP_L2, inline="L2a")
i_adaptiveMode = input.string(ADAPT_DYNAMIC, "| Adaptive Mode", options=[ADAPT_FIXED, ADAPT_DYNAMIC, ADAPT_AGGRESSIVE], group=GROUP_L2, inline="L2a", tooltip = "ASSET TYPE:\nAffects sensitivity multipliers for Key Value\n\n• Auto-Detect: Identifies based on symbol ⭐\n• Stock: Standard (1.0×)\n• Crypto: Wider stops (1.5×) - high volatility\n• Forex: Tighter stops (0.8×) - low volatility\n• Futures: Moderate (1.2×)\n• Index: Slightly tighter (0.9×)\n• Fund/ETF: Slightly tighter (0.9×)\n• CFD: Standard (1.0×)\n• Bond: Much tighter (0.5×) - low volatility\n\n" + "ADAPTIVE MODE:\nHow Key Value adjusts to market volatility\n\n• Fixed: Use input values as-is (no auto-adjustment)\n• Dynamic: Auto-adjust based on volatility ⭐\n - Low vol (< 0.8×): Key × 0.85\n - Normal (0.8-1.2×): Key × 1.0\n - High vol (> 1.2×): Key × 1.15\n• Aggressive: Wider adjustments\n - Low vol: Key × 0.7\n - High vol: Key × 1.3")
// Signal Filters
i_useVolatility = input.bool(false, "High Vol Filter", group=GROUP_FILTERS, inline="L1d")
i_volLookback = input.int(50, "/ Lookback", minval=10, maxval=200, group=GROUP_FILTERS, inline="L1d")
i_volThresh = input.float(1.2, "× Avg", minval=1.0, maxval=3.0, step=0.1, group=GROUP_FILTERS, inline="L1d", tooltip = "HIGH VOLATILITY FILTER:\nDetects when current ATR is above average\n\nLOOKBACK: Bars to calculate average ATR\n• 50: Standard ⭐\n• 100+: Smoother\n\nTHRESHOLD:\n• 1.2×: Slightly elevated ⭐\n• 1.5×: High volatility\n• 2.0×: Extreme volatility\n\nWhen ON: Only allow signals in high volatility")
utUseSwingFilter= input.bool(true, "Swing Filter", group=GROUP_FILTERS, inline="sf1")
swingLen = input.int(8, "/ Lookback", group=GROUP_FILTERS, minval=1, inline="sf1", tooltip = "SWING FILTER:\nOnly signals aligned with swing structure\n\nLOOKBACK: Bars to find swing high/low\n• 5-10: Scalp/Day trading ⭐\n• 10-20: Swing trading")
utUsePctFilter = input.bool(false, "% Filter", group=GROUP_FILTERS, inline="sf2")
utPctThr = input.float(0.05, "/ Min %", group=GROUP_FILTERS, step=0.05, minval=0, inline="sf2", tooltip = "% CHANGE FILTER:\nRequire minimum bar-to-bar % change\n\nMIN %: Threshold in percent\n• 0.1-0.5%: Intraday\n• 0.3-1.0%: Swing")
utUseDelay = input.bool(false, "2-Bar Confirm", group=GROUP_FILTERS, inline="sf3", tooltip = "Require 2 consecutive bars to confirm trend direction\n\n• Reduces false signals\n• Adds slight lag\n• Good for volatile markets")
i_useVolFilter = input.bool(false, "Volume Filter", group=GROUP_FILTERS, inline="sf4")
i_volSmaLen = input.int(20, "/ SMA", minval=5, maxval=100, group=GROUP_FILTERS, inline="sf4")
i_volThreshold = input.float(1.0, "× Avg", minval=0.5, maxval=3.0, step=0.1, group=GROUP_FILTERS, inline="sf4", tooltip = "Require volume above average to confirm signals\n\nSMA: Lookback period for average volume\n• 20: Standard ⭐\n\nTHRESHOLD:\n• 0.8×: Relaxed\n• 1.0×: Above average ⭐\n• 1.5×: Strong volume required")
i_useRsiFilter = input.bool(false, "RSI Filter", group=GROUP_FILTERS, inline="sf5")
i_rsiLen = input.int(14, "/ Len", minval=5, maxval=50, group=GROUP_FILTERS, inline="sf5", tooltip = "RSI MOMENTUM FILTER:\nConfirm signals with RSI direction\n\nLEN: RSI period (14 standard ⭐)")
i_rsiOB = input.int(70, "Overbought", minval=60, maxval=90, group=GROUP_FILTERS, inline="sf9")
i_rsiOS = input.int(30, "Oversold", minval=10, maxval=40, group=GROUP_FILTERS, inline="sf9", tooltip = "Overbought: Block BUY above this\n• 70: Standard ⭐\n• 80: Relaxed\n\nOversold: Block SELL below this\n• 30: Standard ⭐\n• 20: Relaxed")
i_useHullConfirm = input.bool(false, "Hull MA", group=GROUP_FILTERS, inline="sf6")
i_hullPeriod = input.int(20, "/ Period", minval=5, maxval=100, group=GROUP_FILTERS, inline="sf6", tooltip = "HULL MA CONFIRMATION:\nRequire Hull MA direction alignment\n\nPERIOD: Hull MA lookback\n• 14-20: Responsive ⭐\n• 30-50: Smoother\n\nBUY: Price must be above Hull MA\nSELL: Price must be below Hull MA")
i_useSuperTrend = input.bool(false, "SuperTrend", group=GROUP_FILTERS, inline="sf7")
i_stAtrPeriod = input.int(10, "/ ATR", minval=5, maxval=50, group=GROUP_FILTERS, inline="sf7")
i_stMult = input.float(3.0, "×", minval=1.0, maxval=5.0, step=0.1, group=GROUP_FILTERS, inline="sf7")
i_showStLine = input.bool(false, "Line", group=GROUP_FILTERS, inline="sf7", tooltip = "SUPERTREND FILTER:\nRequire SuperTrend confirmation\n\nBUY: Only when SuperTrend is bullish\nSELL: Only when SuperTrend is bearish\n\nATR: Period for volatility (10 standard ⭐)\nMULT: Multiplier (3.0 standard ⭐)\nLINE: Show SuperTrend line on chart")
i_useFullCandle = input.bool(true, "Full Candle", group=GROUP_FILTERS, inline="sf8", tooltip = "FULL CANDLE CONFIRMATION:\nRequire entire candle above/below UT line\n\n• BUY: Low must be above trailing stop\n• SELL: High must be below trailing stop\n\nPrevents signals on candles 'straddling' the line\nReduces false signals that quickly reverse")
utShowZones = input.bool(true, "S/R Zones", group=GROUP_FILTERS, inline="sfz")
i_useZoneFilter = input.bool(false, "| Zone Filter", group=GROUP_FILTERS, inline="sfz")
i_zoneWidthMult = input.float(0.5, "/ Width×ATR", group=GROUP_FILTERS, minval=0.1, maxval=2.0, step=0.1, inline="sfz", tooltip = "SUPPORT/RESISTANCE ZONES:\nClusters recent pivot highs/lows into persistent price zones (unlike the rolling Structure line above, these stay fixed until price revisits and breaks them)\n\nS/R ZONES: Show zone boxes on chart\nZONE FILTER: Block BUY inside a resistance zone, SELL inside a support zone\nWIDTH: Zone half-width as ATR multiple\n• 0.3-0.5: Tight zones ⭐\n• 0.8-1.2: Wide zones")
i_zonePivotLen = input.int(10, "/ Pivot Strength", minval=3, maxval=50, group=GROUP_FILTERS, inline="sfz2", tooltip = "S/R PIVOT STRENGTH:\nBars each side required to confirm a swing high/low for zone clustering.\n\n• 5-7: Minor swings (more, weaker zones)\n• 10: Balanced ⭐\n• 15-20: Only major swings (fewer, stronger zones)\n\nHigher than the Divergence pivot on purpose - S/R levels should be more significant than divergence pivots.")
// SL visuals
entryShow = input.bool(true, "Show Entry Line", group=GROUP_SL, inline="SHOW")
linesLimit10 = input.bool(true, "Limit 10 bars", group=GROUP_SL, inline="SHOW", tooltip = "Limit all lines (SL, TP, Entry) to 10 bars\n• ON: Cleaner chart ⭐\n• OFF: Lines extend until next signal")
slShow = input.bool(true, "Show Stop Loss Line", group=GROUP_SL, inline="sl1")
slMethod = input.string("Smart Adaptive", "| SL Method", options=["ATR", "% Based", "Tick Based", "Swing", "Scaled ATR", "Smart Adaptive", "Safer"], group=GROUP_SL, inline="sl1", tooltip = "Stop Loss calculation method:\n\n⭐ SMART ADAPTIVE (Recommended):\n• Auto-adjusts to volatility\n• Low vol: 0.7× ATR | Med: 1.0× | High: 1.5×\n\nATR:\n• Standard ATR × Multiplier\n\n% BASED:\n• Fixed % from entry price\n• Good for high-price stocks\n\nTICK BASED:\n• Fixed number of ticks\n• Consistent dollar risk\n\nSWING:\n• Recent swing high/low\n\nSCALED ATR:\n• Dynamic multiplier by volatility\n\nSAFER:\n• Widest of ATR/Swing/% methods")
tpDisplayMode = input.string("Minimal", "TP Style", options=["Minimal", "Detailed"], group=GROUP_SL, inline="DIS")
tpFreezeOnTouch = input.bool(true, "Freeze Lines on Hit", group=GROUP_SL, inline="DIS", tooltip = "LINE DISPLAY STYLE:\n\n• Minimal: Clean labels (TP1, TP2, etc.)\n• Detailed: Shows price + % gain on label\n - Easier for trading execution\n - Example: 'TP1 $150.25 (+2.5%)'\n\nWhen ON: marks TP with ✅ when price touches it\nUseful for trade journaling and review")
slAtrLen = input.int(14, "ATR", minval=5, maxval=50, group=GROUP_SL, inline="sl3")
slAtrMult = input.float(1.5, "×", minval=0.5, maxval=3, step=0.1, group=GROUP_SL, inline="sl3", tooltip = "ATR Period & Multiplier\n• 7-10 / 1.0×: Day trading\n• 14 / 1.5×: Standard ⭐\n• 20+ / 2.0×: Swing trading")
slPctStop = input.float(5.0, "% Stop", minval=0.1, maxval=10.0, step=0.1, group=GROUP_SL, inline="sl5", tooltip = "Fixed % distance from entry\n• LONG: SL = Entry × (1 - %)\n• SHORT: SL = Entry × (1 + %)\n\n• 0.5%: Tight (scalping)\n• 1.0%: Balanced ⭐\n• 2.0%: Wide (swing)")
slTickStop = input.int(50, "Ticks", minval=5, maxval=500, step=5, group=GROUP_SL, inline="sl5", tooltip = "Fixed ticks from entry\n• Tick = minimum price movement\n• 20-50: Tight\n• 50-100: Normal ⭐\n• 100+: Wide")
slExtraTicks = input.int(0, "Buffer", minval=0, group=GROUP_SL, inline="sl6", tooltip = "Extra ticks beyond calculated SL\n• Protects against stop hunts\n• 0 = No buffer\n• 2-5 = Typical")
// TP visuals
tpShow = input.bool(true, "Show TP Lines", group=GROUP_SL, inline="tp1")
tp1On = input.bool(true, "TP×1 (1R) |", group=GROUP_SL, inline="tp2")
tp15On = input.bool(false, "TP×1.5 |", group=GROUP_SL, inline="tp2")
tp2On = input.bool(true, "TP×2 |", group=GROUP_SL, inline="tp2")
tp3On = input.bool(false, "TP×3", group=GROUP_SL, inline="tp2", tooltip = "R = Risk Units (distance from entry to SL)\n\nTP1 (1R): Conservative, 1:1 risk/reward\nTP1.5 (1.5R): Intermediate target\nTP2 (2R): Balanced, 2:1 R:R ⭐\nTP3 (3R): Aggressive, 3:1 R:R\n\nRECOMMENDED:\n• Scalping: TP1 + TP1.5\n• Day Trading: TP1 + TP2 ⭐\n• Swing: TP2 + TP3")
// Layer 3: MTF Confirmation
i_useMTF = input.bool(false, "Enable MTF", group=GROUP_L3, inline="L3a", tooltip = "Multi-Timeframe Confirmation\n\nWhen ON: Only allow signals aligned with higher timeframe trend\n• BUY: HTF must be bullish\n• SELL: HTF must be bearish\n\nReduces counter-trend entries")
i_mtfTimeframe = input.string(MTF_AUTO, "Timeframe", options=[MTF_AUTO, MTF_5M, MTF_15M, MTF_30M, MTF_1H, MTF_4H, MTF_D, MTF_W], group=GROUP_L3, inline="L3a", tooltip = "HIGHER TIMEFRAME:\n\n• Auto (×4): 4× current TF ⭐\n - 1m → 5m\n - 5m → 15m\n - 15m → 1H\n - 1H → 4H\n - 4H → D\n - D → W\n\n• Manual: Select specific timeframe")
i_statsLookback = input.int(60, "Stats Days", minval=1, maxval=365, group=GROUP_L3, inline="L3b", tooltip = "STATISTICS LOOKBACK:\n\nHow many days back to count trades for Win Rate and Avg Bars.\n\n• 5: Recent performance ⭐\n• 30: Monthly view\n• 90: Quarterly view")
// Divergence Detection
i_showDivergence = input.bool(true, "Show Divergence", group=GROUP_L3, inline="L3c", tooltip = "DIVERGENCE DETECTION:\n\nShows RSI divergences on chart\n\n• Bullish: Price ↓ + RSI ↑ (reversal up)\n• Bearish: Price ↑ + RSI ↓ (reversal down)\n\nUseful for spotting potential reversals")
i_divRsiLen = input.int(14, "RSI", minval=5, maxval=50, group=GROUP_L3, inline="L3c")
i_divLookback = input.int(5, "/ Pivot", minval=2, maxval=20, group=GROUP_L3, inline="L3c", tooltip = "RSI: Period for RSI calculation (14 standard)\n\nPIVOT: Bars to confirm pivot high/low\n• 3-5: More signals\n• 5-10: Fewer, stronger signals ⭐")
// Risk Calculator
i_showRisk = input.bool(true, "Show Risk Calc", group=GROUP_CALC, inline="L3d", tooltip = "RISK CALCULATOR:\n\nCalculates position size based on:\n• Account size\n• Risk percentage OR fixed amount\n• Stop loss distance\n\nShows how many shares/contracts to buy")
i_riskMode = input.string("% of Account", "Risk Mode", options=["% of Account", "Fixed Amount"], group=GROUP_CALC, inline="L3d", tooltip = "RISK MODE:\n\n• % of Account: Risk X% of total account\n• Fixed Amount: Risk fixed $ amount per trade\n\nFixed is useful for consistent risk across all trades")
i_accountSize = input.float(10000, "Account $", minval=100, step=1000, group=GROUP_CALC, inline="L3e", tooltip = "Your total trading account size in account currency\n\nUsed to calculate:\n• Risk amount per trade (when using %)\n• Position size\n• Max position value")
i_riskPct = input.float(1.0, "Risk %", minval=0.1, maxval=10, step=0.1, group=GROUP_CALC, inline="L3e", tooltip = "Percentage of account to risk per trade\n(Only used when Risk Mode = '% of Account')\n\n• 0.5-1%: Conservative ⭐\n• 1-2%: Moderate\n• 2-3%: Aggressive")
i_riskFixed = input.float(100, "/ Fixed $", minval=1, step=10, group=GROUP_CALC, inline="L3e", tooltip = "Fixed amount to risk per trade\n(Only used when Risk Mode = 'Fixed Amount')\n\nExamples:\n• $50: Small accounts\n• $100: Medium ⭐\n• $200+: Larger accounts")
i_accountCurrency = input.string("USD", "Currency", options=["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF"], group=GROUP_CALC, inline="L3f", tooltip = "Your account currency\n\nAffects position sizing calculations")
i_useLiveRates = input.bool(true, "Live Rates", group=GROUP_CALC, inline="L3f")
i_manualRate = input.float(1.0, "/ Manual $1=", minval=0.001, maxval=10000, step=0.01, group=GROUP_CALC, inline="L3f", tooltip = "Manual exchange rate (USD to your currency)\n\nExamples:\n• USD to ILS = 3.7\n• USD to EUR = 0.92\n• USD to JPY = 150\n\nUsed when Live Rates is OFF or unavailable")
// Trend Prediction
i_enablePrediction = input.bool(true, "Enable Prediction", group=GROUP_PRED, inline="pred1", tooltip = "TREND PREDICTION:\n\nPredicts how long the current trend may last based on historical patterns.\n\n• Gradient boxes show expected duration\n• Labels show survival probability %\n• Dashboard shows prediction stats")
i_predMode = input.string("Standard", "Mode", options=["Simple", "Standard", "Advanced"], group=GROUP_PRED, inline="pred1", tooltip = "PREDICTION MODE:\n\n• Simple: Uses median duration only\n• Standard: EWA + standard deviation ⭐\n• Advanced: Adds 5 adaptive multipliers:\n - Structure (S/R proximity)\n - Stock Type (volatility profile)\n - Flip Strength (volume + filters)\n - Error Learning (self-correction)\n - Regime (trend vs chop)")
i_predSamples = input.int(50, "History", minval=10, maxval=100, group=GROUP_PRED, inline="pred2", tooltip = "HISTORY SIZE:\n\nNumber of past trends to analyze\n\n• 10-30: Responsive to recent changes\n• 50: Balanced ⭐\n• 100: Smoother, more stable")
i_showPredBoxes = input.bool(true, "Boxes", group=GROUP_PRED, inline="pred2", tooltip = "Show gradient boxes visualizing predicted trend duration\n\n• Fades from solid to transparent\n• Position: Below (bull) or above (bear) UT line")
i_showPredLabels = input.bool(true, "Labels", group=GROUP_PRED, inline="pred2", tooltip = "Show probability labels at key points\n\n• Shows survival % at 25%, 50%, 75%, 90%, 100% of prediction\n• Weibull probability distribution")
//======================================================
//============== VAR DECLARATIONS ======================
//======================================================
// SL tracking variables
var line slLine = na
var label slLbl = na
var int slStartBar = na
var float baseSl = na
var float slPrice = na
var color slColor = na
// TP line/label variables
var line tp1Line = na
var line tp15Line = na
var line tp2Line = na
var line tp3Line = na
var label tp1Lbl = na
var label tp15Lbl = na
var label tp2Lbl = na
var label tp3Lbl = na
// TP price level variables
var float tp1Y = na
var float tp15Y = na
var float tp2Y = na
var float tp3Y = na
var float tpInitRisk = na
// Entry tracking variables
var line entryLine = na
var label entryLbl = na
var int entryBar = na
var float entryPx = na
var color entryCol = na
// Statistics tracking (Layer 3)
var int statTotalTrades = 0
var int statWins = 0
var int statLosses = 0
var int statTotalBars = 0 // Sum of bars to TP (for avg calculation)
var int statWinBars = 0 // Count of wins with bar data
var bool statTradeOpen = false
// Prediction tracking arrays
var array<int> pred_bull_durations = array.new<int>()
var array<int> pred_bear_durations = array.new<int>()
var array<int> pred_all_durations = array.new<int>()
var array<bool> pred_all_is_bullish = array.new<bool>()
// Current trend tracking for prediction
var int pred_current_bars = 0
var bool pred_is_bullish = true
// Prediction values
var float pred_avg = na
var float pred_end = na
var bool pred_using_fallback = false
// Accuracy tracking (last 10)
var array<float> pred_last10_predicted = array.new<float>()
var array<int> pred_last10_actual = array.new<int>()
var array<bool> pred_last10_bullish = array.new<bool>()
// Multiplier tracking (Advanced mode)
var array<float> pred_vol_ratios = array.new<float>()
var array<float> pred_quality_scores = array.new<float>()
var array<float> pred_error_ratios = array.new<float>()
// Prediction drawing objects
var array<box> pred_boxes = array.new<box>()
var array<label> pred_labels = array.new<label>()
// S/R Zone tracking arrays (support = pivot lows, resistance = pivot highs; kept
// in separate arrays so a resistance zone can never merge into a support zone)
var array<float> zoneResPrice = array.new<float>()
var array<int> zoneResTouch = array.new<int>()
var array<int> zoneResStartBar = array.new<int>()
var array<box> zoneResBoxes = array.new<box>()
var array<label> zoneResLabels = array.new<label>()
var array<float> zoneSupPrice = array.new<float>()
var array<int> zoneSupTouch = array.new<int>()
var array<int> zoneSupStartBar = array.new<int>()
var array<box> zoneSupBoxes = array.new<box>()
var array<label> zoneSupLabels = array.new<label>()
//======================================================
//============= LAYER 1: ANTI-WHIPSAW ==================
//======================================================
// 1.1 detectRegime() - ADX-based regime detection
// Returns: true if market is trending (ADX > threshold), false if choppy
detectRegime() =>
[diplus, diminus, adxValue] = ta.dmi(i_adxPeriod, i_adxPeriod)
bool isTrending = adxValue >= i_adxThreshold
isTrending
// 1.2 checkCooldown() - Minimum bars between signals
// Returns: true if cooldown period has passed since last signal
var int lastSignalBar = na
var int lastSignalDir = 0 // 1 = BUY, -1 = SELL, 0 = none
checkCooldown() =>
bool cooldownOk = na(lastSignalBar) or (bar_index - lastSignalBar >= i_cooldownBars)
cooldownOk
// 1.4 checkVolumeFilter() - Volume confirmation
// Returns: true if volume is above threshold (or filter is disabled)
checkVolumeFilter() =>
if not i_useVolFilter
true
else
float volSma = ta.sma(volume, i_volSmaLen)
volume >= volSma * i_volThreshold
// 1.5 checkRSIFilter() - RSI momentum confirmation
// Returns: true if RSI confirms direction (or filter is disabled)
// BUY blocked if RSI > OB (overbought), SELL blocked if RSI < OS (oversold)
checkRSIFilter(_isBuy) =>
if not i_useRsiFilter
true
else
float rsiVal = ta.rsi(close, i_rsiLen)
_isBuy ? rsiVal < i_rsiOB : rsiVal > i_rsiOS
// 1.6 Hull Moving Average function
hullMA(series float src, simple int length) =>
ta.wma(2 * ta.wma(src, length / 2) - ta.wma(src, length), math.round(math.sqrt(length)))
// 1.7 checkHullFilter() - Hull MA direction confirmation
// Returns: true if price aligns with Hull MA direction (or filter is disabled)
hullValue = i_useHullConfirm ? hullMA(close, i_hullPeriod) : close
checkHullFilter(_isBuy) =>
if not i_useHullConfirm
true
else
_isBuy ? close > hullValue : close < hullValue
// 1.8 checkVolatilityFilter() - High volatility detection
// Returns: true if current ATR is above average (or filter is disabled)
currentATR = ta.atr(14)
avgATR = ta.sma(currentATR, i_volLookback)
volatilityRatio = avgATR > 0 ? currentATR / avgATR : 1.0
isHighVol = volatilityRatio >= i_volThresh
checkVolatilityFilter() =>
if not i_useVolatility
true
else
isHighVol
// 1.9 SuperTrend Calculation
// Standard SuperTrend indicator for trend confirmation
stAtr = ta.atr(i_stAtrPeriod)
float stUp = hl2 - (i_stMult * stAtr)
float stDn = hl2 + (i_stMult * stAtr)
var float stUpperBand = na
var float stLowerBand = na
var int stDirection = 1 // 1 = bullish, -1 = bearish
// Calculate bands with proper trailing logic
stUpperBand := na(stUpperBand[1]) ? stUp : (stUp > stUpperBand[1] or close[1] < stUpperBand[1]) ? stUp : stUpperBand[1]
stLowerBand := na(stLowerBand[1]) ? stDn : (stDn < stLowerBand[1] or close[1] > stLowerBand[1]) ? stDn : stLowerBand[1]
// Determine direction
stDirection := na(stDirection[1]) ? 1 :
stDirection[1] == -1 and close > stLowerBand[1] ? 1 :
stDirection[1] == 1 and close < stUpperBand[1] ? -1 :
stDirection[1]
// SuperTrend line value
float stValue = stDirection == 1 ? stUpperBand : stLowerBand
// SuperTrend trend states
bool stBullish = stDirection == 1
bool stBearish = stDirection == -1
// SuperTrend Engine: Calculate strength (0-100)
// Strength = how far price is from SuperTrend line relative to ATR
float stDistance = math.abs(close - stValue)
float stStrength = math.min(100, (stDistance / stAtr) * 25)
// Detect SuperTrend flip (for consensus bonus)
bool stFlipBull = stDirection == 1 and stDirection[1] == -1
bool stFlipBear = stDirection == -1 and stDirection[1] == 1
// 1.10 checkSuperTrendFilter() - SuperTrend confirmation
checkSuperTrendFilter(_isBuy) =>
if not i_useSuperTrend
true
else
_isBuy ? stBullish : stBearish
// ══════════════════════════════════════════════════════════════════════════════
// 1.11 Structure Break Engine
// Detects when price breaks beyond recent swing high/low (structure break)
// ══════════════════════════════════════════════════════════════════════════════
// Find recent swing high/low using swingLen from existing Swing Filter
float sbSwingHigh = ta.highest(high, swingLen)
float sbSwingLow = ta.lowest(low, swingLen)
// Structure break = price closes beyond swing (not just wicks)
bool sbBullishBreak = close > sbSwingHigh[1] // Closed above previous swing high
bool sbBearishBreak = close < sbSwingLow[1] // Closed below previous swing low
// Structure Break Engine: Calculate strength (0-100)
float sbAtr = ta.atr(14)
float sbMid = (sbSwingHigh[1] + sbSwingLow[1]) / 2
float sbHalfRange = (sbSwingHigh[1] - sbSwingLow[1]) / 2
// Strength based on position within range OR break distance
float sbBullDistance = sbBullishBreak ? (close - sbSwingHigh[1]) : 0
float sbBearDistance = sbBearishBreak ? (sbSwingLow[1] - close) : 0
float sbBreakStrength = math.min(100, math.max(sbBullDistance, sbBearDistance) / sbAtr * 30)
float sbPositionStrength = sbHalfRange > 0 ? math.min(100, math.abs(close - sbMid) / sbHalfRange * 100) : 0
float sbStrength = sbBullishBreak or sbBearishBreak ? sbBreakStrength : sbPositionStrength
// Structure states: based on position relative to midpoint (not just close vs close[1])
bool sbBullish = close > sbMid // Above midpoint = bullish structure
bool sbBearish = close <= sbMid // At or below midpoint = bearish structure
// Layer 1 results (used to gate signals) - Note: checkConfirmation() defined after xATRTrailingStop
bool l1_regimeOk = not i_enableLayer1 or detectRegime()
bool l1_cooldownOk = i_cooldownBars == 0 or checkCooldown()
bool l1_volumeOk = checkVolumeFilter()
bool l1_volatilityOk = checkVolatilityFilter()
//======================================================
//============= LAYER 2: ADAPTIVE PARAMS ===============
//======================================================
// 2.1 detectAssetType() - Auto-detect or use manual selection
// Returns: Asset type string (uses syminfo.type for auto-detection)
detectAssetType() =>
if i_assetType != ASSET_AUTO
i_assetType
else
switch syminfo.type
"crypto" => ASSET_CRYPTO
"forex" => ASSET_FOREX
"futures" => ASSET_FUTURES
"index" => ASSET_INDEX
"fund" => ASSET_FUND
"cfd" => ASSET_CFD
"bond" => ASSET_BOND
=> ASSET_STOCK // stock or unknown → treat as stock
// 2.2 getAssetMultiplier() - Get sensitivity multiplier for asset type
// Returns: float multiplier for Key Value adjustment
getAssetMultiplier(string assetType) =>
switch assetType
ASSET_CRYPTO => 1.5 // Crypto: wider stops (high volatility)
ASSET_FOREX => 0.8 // Forex: tighter stops (low volatility)
ASSET_FUTURES => 1.2 // Futures: moderate
ASSET_INDEX => 0.9 // Index: slightly tighter (diversified, less volatile)
ASSET_FUND => 0.9 // Fund/ETF: slightly tighter (diversified)
ASSET_CFD => 1.0 // CFD: standard (mirrors underlying)
ASSET_BOND => 0.5 // Bond: much tighter (very low volatility)
ASSET_STOCK => 1.0 // Stock: standard
=> 1.0
// 2.3 getAdaptiveMultiplier() - Adjust Key based on volatility and mode
// Returns: float multiplier based on current volatility ratio
getAdaptiveMultiplier(float volRatio) =>
if i_adaptiveMode == ADAPT_FIXED
1.0
else if i_adaptiveMode == ADAPT_DYNAMIC
// Dynamic: moderate adjustments
if volRatio < 0.8
0.85 // Low volatility: tighter stops
else if volRatio > 1.2
1.15 // High volatility: wider stops
else
1.0 // Normal volatility
else // ADAPT_AGGRESSIVE
// Aggressive: wider adjustments
if volRatio < 0.8
0.7 // Low volatility: much tighter
else if volRatio > 1.2
1.3 // High volatility: much wider
else
1.0 // Normal volatility
// Layer 2 results
string detectedAsset = detectAssetType()
float assetMult = getAssetMultiplier(detectedAsset)
float adaptiveMult = getAdaptiveMultiplier(volatilityRatio)
//======================================================
//============= LAYER 3: MTF CONFIRMATION ==============
//======================================================
// 3.1 getAutoHTF() - Calculate higher timeframe (4× current)
getAutoHTF() =>
string htf = ""
if timeframe.isintraday
int mins = timeframe.multiplier
if timeframe.isseconds
mins := 1
else if timeframe.isminutes
mins := timeframe.multiplier
int htfMins = mins * 4
if htfMins < 60
htf := str.tostring(htfMins)
else if htfMins < 240
htf := str.tostring(htfMins / 60) + "H"
else if htfMins < 1440
htf := "4H"
else
htf := "D"
else if timeframe.isdaily
htf := "W"
else if timeframe.isweekly
htf := "M"
else
htf := "D"
htf
// 3.2 getSelectedHTF() - Get the selected timeframe string
getSelectedHTF() =>
if i_mtfTimeframe == MTF_AUTO
getAutoHTF()
else
switch i_mtfTimeframe
MTF_5M => "5"
MTF_15M => "15"
MTF_30M => "30"
MTF_1H => "60"
MTF_4H => "240"
MTF_D => "D"
MTF_W => "W"
=> "60"
// 3.3 Get current timeframe in minutes (for comparison)
getCurrentTFMins() =>
int mins = 0
if timeframe.isseconds
mins := 1
else if timeframe.isminutes
mins := timeframe.multiplier
else if timeframe.isdaily
mins := 1440
else if timeframe.isweekly
mins := 10080
else if timeframe.ismonthly
mins := 43200
else
mins := timeframe.multiplier
mins
int currentTFMins = getCurrentTFMins()
// 3.4 Get ALL HTF trends using EMA 9/21 crossover (tuples = 7 calls instead of 14)
[htf5mEmaFast, htf5mEmaSlow] = request.security(syminfo.tickerid, "5", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
[htf15mEmaFast, htf15mEmaSlow] = request.security(syminfo.tickerid, "15", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
[htf30mEmaFast, htf30mEmaSlow] = request.security(syminfo.tickerid, "30", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
[htf1hEmaFast, htf1hEmaSlow] = request.security(syminfo.tickerid, "60", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
[htf4hEmaFast, htf4hEmaSlow] = request.security(syminfo.tickerid, "240", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
[htfDEmaFast, htfDEmaSlow] = request.security(syminfo.tickerid, "D", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
[htfWEmaFast, htfWEmaSlow] = request.security(syminfo.tickerid, "W", [ta.ema(close, 9), ta.ema(close, 21)], lookahead=barmerge.lookahead_off)
// Calculate trend for each TF: 1 = Bull, -1 = Bear, 0 = Neutral
int htf5mTrend = htf5mEmaFast > htf5mEmaSlow ? 1 : htf5mEmaFast < htf5mEmaSlow ? -1 : 0
int htf15mTrend = htf15mEmaFast > htf15mEmaSlow ? 1 : htf15mEmaFast < htf15mEmaSlow ? -1 : 0
int htf30mTrend = htf30mEmaFast > htf30mEmaSlow ? 1 : htf30mEmaFast < htf30mEmaSlow ? -1 : 0
int htf1hTrend = htf1hEmaFast > htf1hEmaSlow ? 1 : htf1hEmaFast < htf1hEmaSlow ? -1 : 0
int htf4hTrend = htf4hEmaFast > htf4hEmaSlow ? 1 : htf4hEmaFast < htf4hEmaSlow ? -1 : 0
int htfDTrend = htfDEmaFast > htfDEmaSlow ? 1 : htfDEmaFast < htfDEmaSlow ? -1 : 0
int htfWTrend = htfWEmaFast > htfWEmaSlow ? 1 : htfWEmaFast < htfWEmaSlow ? -1 : 0
// Get selected HTF trend (for filter)
string htfTimeframe = getSelectedHTF()
int htfTrend = switch htfTimeframe
"5" => htf5mTrend
"15" => htf15mTrend
"30" => htf30mTrend
"60" => htf1hTrend
"240" => htf4hTrend
"D" => htfDTrend
"W" => htfWTrend
=> htf1hTrend
string htfTrendStr = htfTrend == 1 ? "BULL" : htfTrend == -1 ? "BEAR" : "NEUTRAL"
// Helper: trend to emoji
trendEmoji(int t) => t == 1 ? "🟢" : t == -1 ? "🔴" : "⚪"
trendStr(int t) => t == 1 ? "BULL" : t == -1 ? "BEAR" : "NEUTRAL"
// 3.5 checkMTFFilter() - Check if signal aligns with HTF trend
checkMTFFilter(_isBuy) =>
if not i_useMTF
true
else
_isBuy ? htfTrend >= 0 : htfTrend <= 0 // Allow neutral
//======================================================
//============= LAYER 3: DIVERGENCE DETECTION ==========
//======================================================
// RSI for divergence
float divRsi = ta.rsi(close, i_divRsiLen)
// Pivot detection using ta.pivothigh / ta.pivotlow
float pivotLow = ta.pivotlow(low, i_divLookback, i_divLookback)
float pivotHigh = ta.pivothigh(high, i_divLookback, i_divLookback)
float rsiPivotLow = ta.pivotlow(divRsi, i_divLookback, i_divLookback)
float rsiPivotHigh = ta.pivothigh(divRsi, i_divLookback, i_divLookback)
// Store previous pivot values for comparison
var float prevPriceLow = na
var float prevPriceHigh = na
var float prevRsiLow = na
var float prevRsiHigh = na
var int prevLowBar = na
var int prevHighBar = na
// Divergence signals
var bool bullishDiv = false
var bool bearishDiv = false
// Reset divergence flags each bar
bullishDiv := false
bearishDiv := false
// Check for Bullish Divergence (price lower low, RSI higher low)
if not na(pivotLow) and not na(rsiPivotLow)
if not na(prevPriceLow) and not na(prevRsiLow)
// Price made lower low
bool priceLowerLow = pivotLow < prevPriceLow
// RSI made higher low
bool rsiHigherLow = rsiPivotLow > prevRsiLow
// Bullish divergence confirmed
if priceLowerLow and rsiHigherLow
bullishDiv := true
// Update previous values
prevPriceLow := pivotLow
prevRsiLow := rsiPivotLow
prevLowBar := bar_index - i_divLookback
// Check for Bearish Divergence (price higher high, RSI lower high)
if not na(pivotHigh) and not na(rsiPivotHigh)
if not na(prevPriceHigh) and not na(prevRsiHigh)
// Price made higher high
bool priceHigherHigh = pivotHigh > prevPriceHigh
// RSI made lower high
bool rsiLowerHigh = rsiPivotHigh < prevRsiHigh
// Bearish divergence confirmed
if priceHigherHigh and rsiLowerHigh
bearishDiv := true
// Update previous values
prevPriceHigh := pivotHigh
prevRsiHigh := rsiPivotHigh
prevHighBar := bar_index - i_divLookback
// Track last divergence for table display
var string lastDivType = "None"
var int lastDivBar = na
if bullishDiv
lastDivType := "BULL"
lastDivBar := bar_index
if bearishDiv
lastDivType := "BEAR"
lastDivBar := bar_index
// Bars since last divergence
int barsSinceDiv = na(lastDivBar) ? 999 : bar_index - lastDivBar
//======================================================
//============= S/R ZONE ENGINE =========================
//======================================================
// Clusters recent pivot highs/lows (reused from Divergence detection above) into
// persistent zones, instead of a single rolling swing point that shifts every bar.
const int ZONE_MAX = 3
// Merge a new pivot into an existing zone (within tolerance) or add a new one.
registerZone(array<float> _prices, array<int> _touches, array<int> _startBars, float _pivotPrice, float _tolerance, int _startBar, float _currentPrice) =>
int n = array.size(_prices)
int matchIdx = -1
if n > 0
for i = 0 to n - 1
if math.abs(_pivotPrice - array.get(_prices, i)) <= _tolerance
matchIdx := i
if matchIdx >= 0
array.set(_prices, matchIdx, (array.get(_prices, matchIdx) + _pivotPrice) / 2)
array.set(_touches, matchIdx, array.get(_touches, matchIdx) + 1)
else
if n >= ZONE_MAX
// Evict the LEAST relevant zone (farthest from current price), not the
// oldest - otherwise a stale zone from before a big move lingers just
// because it hasn't been around long enough to reach the front of the queue.
int worstIdx = 0
float worstDist = math.abs(_currentPrice - array.get(_prices, 0))
for i = 1 to n - 1
float d = math.abs(_currentPrice - array.get(_prices, i))
if d > worstDist
worstDist := d
worstIdx := i
array.remove(_prices, worstIdx)
array.remove(_touches, worstIdx)
array.remove(_startBars, worstIdx)
array.push(_prices, _pivotPrice)
array.push(_touches, 1)
array.push(_startBars, _startBar)
// Is _price currently inside any zone in the list (within tolerance)? Also returns
// that zone's touch count, so callers can show S/R strength instead of a bare bool.
zoneMatch(array<float> _prices, array<int> _touches, float _tolerance, float _price) =>
bool inside = false
int touchCount = 0
if array.size(_prices) > 0
for i = 0 to array.size(_prices) - 1
if math.abs(_price - array.get(_prices, i)) <= _tolerance
inside := true
touchCount := array.get(_touches, i)
[inside, touchCount]
// Index of the single most-relevant zone on one side of price, or -1 if none.
// _wantAbove=true -> nearest zone AT-OR-ABOVE price (the ceiling overhead)
// _wantAbove=false -> nearest zone AT-OR-BELOW price (the floor underneath)
// A zone price is only drawn on the side price actually faces it, so a level that
// has been broken through (and no longer acts as S/R) simply drops off the chart.
nearestZoneIdx(array<float> _prices, float _price, bool _wantAbove) =>
int bestIdx = -1
float bestDist = 0.0
if array.size(_prices) > 0
for i = 0 to array.size(_prices) - 1
float zp = array.get(_prices, i)
// Strict side test: a resistance must be ABOVE price, a support BELOW it.
// A pivot-low cluster price has since fallen below is no longer support -
// it drops off rather than showing as a green "support" under the price.
bool onSide = _wantAbove ? zp > _price : zp < _price
float d = math.abs(_price - zp)
if onSide and (bestIdx == -1 or d < bestDist)
bestDist := d
bestIdx := i
bestIdx
// Drop zones price has clearly moved away from, every bar - not just when a new
// pivot needs their slot. Without this, a zone from before a big gap/crash keeps
// showing (e.g. an old "support" left dangling far above price after a drop) until
// a fresh pivot happens to come along and evict it. % of price, not ATR, on purpose:
// right after the same crash that makes a zone stale, ATR itself spikes too.
const float ZONE_STALE_PCT = 0.06
pruneStaleZones(array<float> _prices, array<int> _touches, array<int> _startBars, float _currentPrice, float _maxDist) =>
int idx = array.size(_prices) - 1
while idx >= 0
if math.abs(_currentPrice - array.get(_prices, idx)) > _maxDist
array.remove(_prices, idx)
array.remove(_touches, idx)
array.remove(_startBars, idx)
idx -= 1
float zoneTol = sbAtr * i_zoneWidthMult
float zoneStaleDist = close * ZONE_STALE_PCT
// Dedicated stronger pivots for zones (larger lookback than the divergence pivots -
// S/R levels should be more significant than every minor swing).
float zonePivotHigh = ta.pivothigh(high, i_zonePivotLen, i_zonePivotLen)
float zonePivotLow = ta.pivotlow(low, i_zonePivotLen, i_zonePivotLen)
pruneStaleZones(zoneResPrice, zoneResTouch, zoneResStartBar, close, zoneStaleDist)
pruneStaleZones(zoneSupPrice, zoneSupTouch, zoneSupStartBar, close, zoneStaleDist)
if not na(zonePivotHigh)
registerZone(zoneResPrice, zoneResTouch, zoneResStartBar, zonePivotHigh, zoneTol, bar_index - i_zonePivotLen, close)
if not na(zonePivotLow)
registerZone(zoneSupPrice, zoneSupTouch, zoneSupStartBar, zonePivotLow, zoneTol, bar_index - i_zonePivotLen, close)
[nearResistance, nearResTouches] = zoneMatch(zoneResPrice, zoneResTouch, zoneTol, close)
[nearSupport, nearSupTouches] = zoneMatch(zoneSupPrice, zoneSupTouch, zoneTol, close)
// checkZoneFilter() - block BUY inside a resistance zone, SELL inside a support zone
checkZoneFilter(_isBuy) =>
if not i_useZoneFilter
true
else
_isBuy ? not nearResistance : not nearSupport
//======================================================
//============= LAYER 3: RISK CALCULATOR ===============
//======================================================
// Currency symbols for display
getCurrencySymbol(string curr) =>
switch curr
"USD" => "$"
"EUR" => "€"
"GBP" => "£"
"ILS" => "₪"
"JPY" => "¥"
"CAD" => "C$"
"AUD" => "A$"
"CHF" => "Fr"
=> "$"
string currSymbol = getCurrencySymbol(i_accountCurrency)
// Get live exchange rate (USD to account currency)
string fxPair = switch i_accountCurrency
"EUR" => "EURUSD"
"GBP" => "GBPUSD"
"ILS" => "USDILS"
"JPY" => "USDJPY"
"CAD" => "USDCAD"
"AUD" => "AUDUSD"
"CHF" => "USDCHF"
=> ""
float liveRate = na
if i_useLiveRates and fxPair != "" and i_accountCurrency != "USD"
liveRate := request.security(fxPair, "D", close, lookahead=barmerge.lookahead_off)
// Default rates as fallback
float defaultRate = switch i_accountCurrency
"EUR" => 0.92
"GBP" => 0.79
"ILS" => 3.70
"JPY" => 150.0
"CAD" => 1.38
"AUD" => 1.55
"CHF" => 0.90
=> 1.0
// Calculate exchange rate (handle inverted pairs)
float exchangeRate = i_accountCurrency == "USD" ? 1.0 :
not i_useLiveRates ? i_manualRate :
na(liveRate) ? (i_manualRate != 1.0 ? i_manualRate : defaultRate) :
(i_accountCurrency == "EUR" or i_accountCurrency == "GBP" or i_accountCurrency == "AUD") ?
1/liveRate : liveRate
// Position size calculation function
calcPositionSize(float _entry, float _sl) =>
// Calculate risk amount based on mode (% of Account or Fixed Amount)
float riskAmount = i_riskMode == "Fixed Amount" ? i_riskFixed : i_accountSize * (i_riskPct / 100)
// Convert risk to symbol currency if needed
string symbolCurr = syminfo.currency
if symbolCurr != i_accountCurrency and i_accountCurrency != "USD"
riskAmount := riskAmount / exchangeRate // Convert to USD first if symbol is USD-based
float riskPerShare = math.abs(_entry - _sl)
float shares = riskPerShare > 0 ? math.floor(riskAmount / riskPerShare) : 0
float posValue = shares * _entry
float maxLoss = shares * riskPerShare
[shares, posValue, maxLoss, riskAmount]
//======================================================
//============= PREDICTION FUNCTIONS ===================
//======================================================
// EWA (Exponential Weighted Average)
ewa_avg(array<int> arr, float decay) =>
float sum_w = 0.0
float sum_v = 0.0
int n = array.size(arr)
for i = 0 to n - 1
float w = math.pow(decay, n - 1 - i)
sum_w += w
sum_v += array.get(arr, i) * w
sum_w > 0 ? sum_v / sum_w : 0.0
// EWA Standard Deviation
ewa_stdev(array<int> arr, float avg, float decay) =>
float sum_w = 0.0
float sum_sq = 0.0
int n = array.size(arr)
for i = 0 to n - 1
float w = math.pow(decay, n - 1 - i)
float diff = array.get(arr, i) - avg
sum_w += w
sum_sq += w * diff * diff
sum_w > 0 ? math.sqrt(sum_sq / sum_w) : 0.0
// Get median of array
array_median(array<int> arr) =>
int n = array.size(arr)
if n == 0
0.0
else
array<int> sorted = array.copy(arr)
array.sort(sorted)
n % 2 == 1 ? float(array.get(sorted, n/2)) : (array.get(sorted, n/2-1) + array.get(sorted, n/2)) / 2.0
// 1. Structure Multiplier (S/R proximity)
get_structure_mult() =>
float pivotHi = ta.pivothigh(high, 10, 10)
float pivotLo = ta.pivotlow(low, 10, 10)
float atr = ta.atr(14)
bool nearLevel = (not na(pivotHi) and math.abs(close - pivotHi) < atr) or
(not na(pivotLo) and math.abs(close - pivotLo) < atr)
nearLevel ? 0.7 : 1.0
// 2. Stock Type Multiplier (use existing detectedAsset)
get_stock_type_mult(string _asset) =>
switch _asset
ASSET_CRYPTO => 0.60
ASSET_FOREX => 0.80
ASSET_FUTURES => 0.90
ASSET_BOND => 1.35
=> 1.0 // Stock, Index, Fund, CFD
// 3. Flip Strength Multiplier (enhanced with filter count)
get_flip_strength_mult(bool _regimeOk, bool _volFilterOk, bool _stFilterOk, float _currentATR, float _avgATR) =>
float vol_score = math.min(40, (volume / ta.sma(volume, 20) - 1) * 40)
float atr_score = math.min(30, ((_currentATR / _avgATR) - 1) * 30)
// Filter pass count bonus
int filters_passed = 0
if _regimeOk
filters_passed += 1
if _volFilterOk
filters_passed += 1
if _stFilterOk
filters_passed += 1
float filter_score = filters_passed * 10 // 0-30 points
float total = vol_score + atr_score + filter_score
0.5 + (total / 100) * 1.5 // Range: 0.5x to 2.0x
// 4. Error Learning Multiplier
get_learning_mult() =>
int n = array.size(pred_error_ratios)
if n < 3
1.0
else
float avg_err = array.avg(pred_error_ratios)
if avg_err > 1.0
1.0 - math.min((avg_err - 1.0) * 0.5, 0.15)
else
1.0 + math.min((1.0 - avg_err) * 0.5, 0.15)
// 5. Regime Multiplier
get_regime_mult(bool _isBullish) =>
array<int> recent = _isBullish ? pred_bull_durations : pred_bear_durations
int n = array.size(recent)
if n < 6
1.0
else
float recent_avg = (array.get(recent, n-1) + array.get(recent, n-2) + array.get(recent, n-3)) / 3
float hist_avg = array.avg(recent)
float ratio = recent_avg / hist_avg
ratio >= 1.3 ? 1.2 : ratio >= 1.1 ? 1.1 : ratio <= 0.7 ? 0.8 : ratio <= 0.9 ? 0.9 : 1.0
//======================================================
//================= BASE CONDITIONS ====================
//======================================================
// Custom Momentum-Weighted Trailing System
srcUT = close
xATR = ta.atr(utAtrPeriod)
// Unique momentum-based trail calculation
var float xATRTrailingStop = na
float momentum = ta.mom(srcUT, 5) / srcUT
float volRatio = volume / ta.sma(volume, 20)
// Apply Layer 2: Asset + Adaptive multipliers
float baseKey = utKey * assetMult * adaptiveMult
float adaptiveKey = baseKey * (1 + math.abs(momentum) * 2) * math.max(0.8, math.min(1.2, volRatio))
float trailDist = xATR * adaptiveKey
if na(xATRTrailingStop)
xATRTrailingStop := srcUT
else
float targetTrail = srcUT > xATRTrailingStop ? srcUT - trailDist : srcUT + trailDist
float smoothing = 0.3
xATRTrailingStop := xATRTrailingStop * (1 - smoothing) + targetTrail * smoothing
// 1.3 checkConfirmation() - Require N consecutive bars in trend direction
// Returns: true if price has confirmed direction for i_confirmBars bars
checkConfirmation(_isBuy) =>
bool confirmed = true
if i_confirmBars > 1
for i = 1 to i_confirmBars - 1
if _isBuy
if close[i] <= xATRTrailingStop[i]
confirmed := false
break
else
if close[i] >= xATRTrailingStop[i]
confirmed := false
break
confirmed
above = ta.crossover(srcUT, xATRTrailingStop)
below = ta.crossunder(srcUT, xATRTrailingStop)
// Guard extremely small prices from exploding percentage
atrPct = srcUT != 0 ? (xATR / srcUT * 100.0) : 0.0
float atrPctSafe = math.min(atrPct, 20.0)
kUT = 0.50
thr = math.max(utPctThr, atrPctSafe * kUT)
barbuy = srcUT > xATRTrailingStop
barsell = srcUT < xATRTrailingStop
// Prediction: Detect UT Bot trend flip
bool pred_flip_to_bull = barbuy and not barbuy[1]
bool pred_flip_to_bear = barsell and not barsell[1]
bool pred_flip = pred_flip_to_bull or pred_flip_to_bear
// Save duration BEFORE reset (critical for storing completed trend)
int pred_completed_duration = pred_current_bars
// Track current trend duration (reset to 1 on flip, otherwise increment)
pred_current_bars := pred_flip ? 1 : pred_current_bars + 1
// On flip: ALWAYS store completed trend (for historical learning)
if pred_flip
// Store completed trend to arrays (ALWAYS, regardless of prediction toggle)
if pred_completed_duration > 1
if pred_is_bullish
array.push(pred_bull_durations, pred_completed_duration)
else
array.push(pred_bear_durations, pred_completed_duration)
array.push(pred_all_durations, pred_completed_duration)
array.push(pred_all_is_bullish, pred_is_bullish)
// Trim arrays to max size
if array.size(pred_all_durations) > i_predSamples
array.shift(pred_all_durations)
array.shift(pred_all_is_bullish)
if array.size(pred_bull_durations) > i_predSamples
array.shift(pred_bull_durations)
if array.size(pred_bear_durations) > i_predSamples
array.shift(pred_bear_durations)
// Store accuracy of previous prediction (only when prediction was enabled)
if i_enablePrediction and not na(pred_end) and pred_completed_duration > 1
array.push(pred_last10_predicted, pred_end)
array.push(pred_last10_actual, pred_completed_duration)
array.push(pred_last10_bullish, pred_is_bullish)
float error_ratio = pred_end > 0 ? float(pred_completed_duration) / pred_end : 1.0
array.push(pred_error_ratios, error_ratio)
if array.size(pred_last10_predicted) > 10
array.shift(pred_last10_predicted)
array.shift(pred_last10_actual)
array.shift(pred_last10_bullish)
if array.size(pred_error_ratios) > 10
array.shift(pred_error_ratios)
// Update state (AFTER storing with old direction values)
pred_is_bullish := pred_flip_to_bull
// Calculate prediction (only when enabled)
if i_enablePrediction
array<int> relevant = pred_is_bullish ? pred_bull_durations : pred_bear_durations
int samples = array.size(relevant)
if samples >= 3
pred_using_fallback := false
if i_predMode == "Simple"
float median = array_median(relevant)
pred_avg := median
pred_end := median * 2.5
else
float avg = ewa_avg(relevant, 0.9)
float stdev = ewa_stdev(relevant, avg, 0.9)
pred_avg := avg
pred_end := avg + 2.0 * stdev
if i_predMode == "Advanced"
// Apply multipliers
float total_mult = 1.0
total_mult *= get_structure_mult()
total_mult *= get_stock_type_mult(detectedAsset)
total_mult *= get_flip_strength_mult(l1_regimeOk, checkVolumeFilter(), checkSuperTrendFilter(pred_is_bullish), currentATR, avgATR)
total_mult *= get_learning_mult()
total_mult *= get_regime_mult(pred_is_bullish)
total_mult := math.max(0.4, math.min(2.0, total_mult))
pred_end *= total_mult
else
// Not enough same-direction history yet. Progressive fallback so a
// forecast ALWAYS renders (this smoothed UT line flips often, so many
// "trends" are 1 bar and never get recorded - without a fallback the
// learning arrays can sit under 3 forever and the cone never appears).
pred_using_fallback := true
int allSamples = array.size(pred_all_durations)
if allSamples >= 3
float avgAll = ewa_avg(pred_all_durations, 0.9)
float stdevAll = ewa_stdev(pred_all_durations, avgAll, 0.9)
pred_avg := avgAll
pred_end := avgAll + 2.0 * stdevAll
else if allSamples >= 1
float avgAll = ewa_avg(pred_all_durations, 0.9)
pred_avg := avgAll
pred_end := avgAll * 2.5
else
// Zero completed trends recorded yet - neutral default horizon
pred_avg := 8.0
pred_end := 16.0
// Cap the projection at the 90th percentile of ACTUAL trend lengths. On thin/
// noisy history (e.g. weekly charts) avg + 2*stdev balloons far past anything ever
// observed, pushing the strip into never-reached territory where every empirical
// survival-% is 0. Capping at p90 keeps the strip inside real history, so the % span
// from high down to ~10% instead of collapsing to 0. Only bites when the estimate
// exceeds p90 (the noisy case); tight, reliable estimates pass through unchanged.
array<int> capArr = pred_all_durations
if samples >= 3
capArr := relevant
if not na(pred_end) and array.size(capArr) >= 3
float p90dur = array.percentile_nearest_rank(capArr, 90)
if p90dur > 0
pred_end := math.min(pred_end, p90dur)
chgOk = not utUsePctFilter or (math.abs(srcUT - srcUT[1]) / srcUT[1] * 100.0 > thr)
// Momentum structure validation
float _lo = na
float _hi = na
if utUseSwingFilter
_lo := ta.lowest(srcUT, swingLen)
_hi := ta.highest(srcUT, swingLen)
swingOkBuy = not utUseSwingFilter or srcUT > _lo * 1.01
swingOkSell = not utUseSwingFilter or srcUT < _hi * 0.99
// 2-bar delay
flipUp = above
flipDn = below
utBuy = (utUseDelay ? ((bar_index > 1 ? flipUp[2] : false) and (bar_index > 0 ? barbuy[1] : false) and (bar_index > 1 ? barbuy[2] : false)) : flipUp) and chgOk and swingOkBuy
utSell = (utUseDelay ? ((bar_index > 1 ? flipDn[2] : false) and (bar_index > 0 ? barsell[1] : false) and (bar_index > 1 ? barsell[2] : false)) : flipDn) and chgOk and swingOkSell
// Full candle confirmation: require entire candle above/below UT line
bool fullCandleBuyOk = not i_useFullCandle or low > xATRTrailingStop // Entire candle above line
bool fullCandleSellOk = not i_useFullCandle or high < xATRTrailingStop // Entire candle below line
confirmRealBuy = close > xATRTrailingStop and fullCandleBuyOk
confirmRealSell = close < xATRTrailingStop and fullCandleSellOk
// Apply Layer 1 filters
bool l1_confirmBuy = checkConfirmation(true)
bool l1_confirmSell = checkConfirmation(false)
utBuy := utBuy and confirmRealBuy and l1_regimeOk and l1_cooldownOk and l1_confirmBuy and l1_volumeOk and l1_volatilityOk and checkRSIFilter(true) and checkHullFilter(true) and checkSuperTrendFilter(true) and checkMTFFilter(true) and checkZoneFilter(true)
utSell := utSell and confirmRealSell and l1_regimeOk and l1_cooldownOk and l1_confirmSell and l1_volumeOk and l1_volatilityOk and checkRSIFilter(false) and checkHullFilter(false) and checkSuperTrendFilter(false) and checkMTFFilter(false) and checkZoneFilter(false)
// Prevent consecutive same-direction signals (must alternate BUY/SELL)
utBuy := utBuy and lastSignalDir != 1
utSell := utSell and lastSignalDir != -1
// Update lastSignalBar and lastSignalDir when signal fires (ONLY on bar close to prevent race condition)
if utBuy and barstate.isconfirmed
lastSignalBar := bar_index
lastSignalDir := 1
if utSell and barstate.isconfirmed
lastSignalBar := bar_index
lastSignalDir := -1
bool utVizGate = barstate.isconfirmed or not barstate.isrealtime
// Feature ON/OFF
bool slFeatureOn = slShow and utShowLabels
bool tpFeatureOn = tpShow and utShowLabels
bool entryFeatureOn = entryShow
// Creates SL line/label if missing and returns them
ensureSLObjects(line _ln, label _lb, int _x, float _y, color _c) =>
line ln_result = _ln
label lb_result = _lb
if na(ln_result) and not na(_x) and not na(_y)
ln_result := line.new(_x, _y, _x, _y, xloc=xloc.bar_index, extend=extend.none, color=_c, width=2)
if na(lb_result) and not na(_x) and not na(_y)
lb_result := label.new(_x, _y, "Suggested Stop Loss", xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=_c, size=labelSize)
[ln_result, lb_result]
// Forward state flags used by visual gates
var bool legDone = false
var int legDoneBar = na
var bool slFrozen = false
var bool entryFrozen = false
var bool tp1Frozen = false
var bool tp15Frozen = false
var bool tp2Frozen = false
var bool tp3Frozen = false
// Visual gates (allow forced draw on touch/freeze even if "draw on close" is ON)
bool slShowVis = slFeatureOn and (utVizGate or slFrozen or legDone)
bool tpShowVis = tpFeatureOn and (utVizGate or tp1Frozen or tp15Frozen or tp2Frozen or tp3Frozen or legDone)
bool entryShowVis = entryFeatureOn and (utVizGate or entryFrozen or legDone)
// mark TP hits so labels can show ✅ even if they were off and later re-shown
var bool tp1Hit = false
var bool tp15Hit = false
var bool tp2Hit = false
var bool tp3Hit = false
// Buy/Sell label tracking (limit to 30)
const int MAX_SIGNAL_LABELS = 30
var array<label> signalLabels = array.new<label>()
// cap helper: do not extend objects past the bar where the leg ended
capX2(int x) =>
not na(legDoneBar) ? math.min(x, legDoneBar) : x
//======================================================
//====================== VISUALS =======================
//======================================================
plot(utShowLine ? xATRTrailingStop : na, title="UT Trailing Stop", color=color.orange, linewidth=2)
plot(i_showStLine ? stValue : na, title="SuperTrend", color=stBullish ? color.green : color.red, linewidth=2, style=plot.style_linebr)
// Anti-Repaint: Labels only on confirmed bars
bool utPlotGate = utShowLabels and utVizGate
// Buy/Sell labels using label.new() for dynamic size support (limited to 30)
if utPlotGate and utBuy
label newLbl = label.new(bar_index, low, "Buy", style=label.style_label_up, color=color.aqua, textcolor=color.white, size=labelSize)
array.push(signalLabels, newLbl)
if array.size(signalLabels) > MAX_SIGNAL_LABELS
label.delete(array.shift(signalLabels))
if utPlotGate and utSell
label newLbl = label.new(bar_index, high, "Sell", style=label.style_label_down, color=color.orange, textcolor=color.white, size=labelSize)
array.push(signalLabels, newLbl)
if array.size(signalLabels) > MAX_SIGNAL_LABELS
label.delete(array.shift(signalLabels))
barcolor(utColorBars ? (barbuy ? color.aqua : barsell ? color.orange : na) : na)
//======================================================
//============= RIBBON FILL (Gradient) =================
//======================================================
// Gate: only show ribbon when enabled
bool ribbon_active = utShowRibbon
// Base color based on trend direction
color ribbonBaseColor = barbuy ? #00CED1 : #FF6B6B
// Helper function for layer interpolation (16 slices for 15 layers)
get_ribbon_layer(float _start, float _end, int _step) =>
(_start * (15 - _step) + _end * _step) / 15
// Calculate layer positions from close to xATRTrailingStop
float rl1 = ribbon_active ? close : na
float rl2 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 1) : na
float rl3 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 2) : na
float rl4 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 3) : na
float rl5 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 4) : na
float rl6 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 5) : na
float rl7 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 6) : na
float rl8 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 7) : na
float rl9 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 8) : na
float rl10 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 9) : na
float rl11 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 10) : na
float rl12 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 11) : na
float rl13 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 12) : na
float rl14 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 13) : na
float rl15 = ribbon_active ? get_ribbon_layer(close, xATRTrailingStop, 14) : na
float rl16 = ribbon_active ? xATRTrailingStop : na
// Calculate gradient colors: 85% opacity (light, near price) → 30% (dark, near UT line)
color rc1 = ribbon_active ? color.new(ribbonBaseColor, 85) : na
color rc2 = ribbon_active ? color.new(ribbonBaseColor, 81) : na
color rc3 = ribbon_active ? color.new(ribbonBaseColor, 77) : na
color rc4 = ribbon_active ? color.new(ribbonBaseColor, 73) : na
color rc5 = ribbon_active ? color.new(ribbonBaseColor, 69) : na
color rc6 = ribbon_active ? color.new(ribbonBaseColor, 65) : na
color rc7 = ribbon_active ? color.new(ribbonBaseColor, 61) : na
color rc8 = ribbon_active ? color.new(ribbonBaseColor, 57) : na
color rc9 = ribbon_active ? color.new(ribbonBaseColor, 53) : na
color rc10 = ribbon_active ? color.new(ribbonBaseColor, 49) : na
color rc11 = ribbon_active ? color.new(ribbonBaseColor, 45) : na
color rc12 = ribbon_active ? color.new(ribbonBaseColor, 41) : na
color rc13 = ribbon_active ? color.new(ribbonBaseColor, 37) : na
color rc14 = ribbon_active ? color.new(ribbonBaseColor, 33) : na
color rc15 = ribbon_active ? color.new(ribbonBaseColor, 30) : na
// Plot 16 invisible lines
rp1 = plot(rl1, display=display.none, editable=false)
rp2 = plot(rl2, display=display.none, editable=false)
rp3 = plot(rl3, display=display.none, editable=false)
rp4 = plot(rl4, display=display.none, editable=false)
rp5 = plot(rl5, display=display.none, editable=false)
rp6 = plot(rl6, display=display.none, editable=false)
rp7 = plot(rl7, display=display.none, editable=false)
rp8 = plot(rl8, display=display.none, editable=false)
rp9 = plot(rl9, display=display.none, editable=false)
rp10 = plot(rl10, display=display.none, editable=false)
rp11 = plot(rl11, display=display.none, editable=false)
rp12 = plot(rl12, display=display.none, editable=false)
rp13 = plot(rl13, display=display.none, editable=false)
rp14 = plot(rl14, display=display.none, editable=false)
rp15 = plot(rl15, display=display.none, editable=false)
rp16 = plot(rl16, display=display.none, editable=false)
// Fill 15 layers with gradient
fill(rp1, rp2, ribbon_active ? rc1 : na, editable=false)
fill(rp2, rp3, ribbon_active ? rc2 : na, editable=false)
fill(rp3, rp4, ribbon_active ? rc3 : na, editable=false)
fill(rp4, rp5, ribbon_active ? rc4 : na, editable=false)
fill(rp5, rp6, ribbon_active ? rc5 : na, editable=false)
fill(rp6, rp7, ribbon_active ? rc6 : na, editable=false)
fill(rp7, rp8, ribbon_active ? rc7 : na, editable=false)
fill(rp8, rp9, ribbon_active ? rc8 : na, editable=false)
fill(rp9, rp10, ribbon_active ? rc9 : na, editable=false)
fill(rp10, rp11, ribbon_active ? rc10 : na, editable=false)
fill(rp11, rp12, ribbon_active ? rc11 : na, editable=false)
fill(rp12, rp13, ribbon_active ? rc12 : na, editable=false)
fill(rp13, rp14, ribbon_active ? rc13 : na, editable=false)
fill(rp14, rp15, ribbon_active ? rc14 : na, editable=false)
fill(rp15, rp16, ribbon_active ? rc15 : na, editable=false)
// Trend Duration Prediction (SuperDuperTrend-style): on each actual BUY/SELL signal,
// paint a gradient duration strip forward from the signal bar and keep the last 5.
// Anchored at the flip (not barstate.islast) so historical strips stay on the chart
// and the most recent one projects the CURRENT trend's expected remaining life.
int PRED_BOXES_PER_SET = 30
// Fire on every RAW trend flip (ribbon colour change), like SuperDuperTrend draws at
// every SuperTrend flip - NOT the filtered Buy/Sell signals (those are gated by MTF /
// Anti-Whipsaw / Swing / cooldown and fire rarely, so anchoring on them left the chart blank).
bool pred_draw_flip = pred_flip and utVizGate
if i_enablePrediction and pred_draw_flip
// pe/pa: the just-computed forecast for the NEW trend (never na - see fallback above)
float pe = (na(pred_end) or pred_end <= 0) ? 16.0 : pred_end
float pa = na(pred_avg) ? pe / 2.0 : pred_avg
bool peFallback = pred_using_fallback or na(pred_end) or pred_end <= 0
bool isBull = pred_is_bullish
color base_col = isBull ? color.green : color.red
// Keep ONLY the latest forecast: wipe every previous strip + label each flip
if array.size(pred_boxes) > 0
for i = 0 to array.size(pred_boxes) - 1
box.delete(array.get(pred_boxes, i))
array.clear(pred_boxes)
if array.size(pred_labels) > 0
for i = 0 to array.size(pred_labels) - 1
label.delete(array.get(pred_labels, i))
array.clear(pred_labels)
// Thin ATR-scaled strip just off the UT line (SuperDuperTrend geometry)
float predATR = ta.atr(14)
float box_height = predATR * 0.12
float box_top = na
float box_bottom = na
if isBull
box_top := xATRTrailingStop - predATR * 0.03
box_bottom := box_top - box_height
else
box_bottom := xATRTrailingStop + predATR * 0.03
box_top := box_bottom + box_height
float box_width = pe / PRED_BOXES_PER_SET
array<int> relevant = isBull ? pred_bull_durations : pred_bear_durations
array<int> survArr = array.size(relevant) >= 3 ? relevant : pred_all_durations
// 30 gradient boxes projected forward from the signal bar
if i_showPredBoxes
for i = 0 to PRED_BOXES_PER_SET - 1
int box_left = bar_index + int(i * box_width)
int box_right = bar_index + math.max(int((i + 1) * box_width), int(i * box_width) + 1)
int opacity = 5 + int((i / 29.0) * 90)
box nb = box.new(box_left, box_top, box_right, box_bottom, bgcolor=color.new(base_col, opacity), border_color=na)
array.push(pred_boxes, nb)
// Center "Trend Analysis" label with full breakdown tooltip
int header_samples = peFallback ? array.size(pred_all_durations) : array.size(relevant)
string direction_str = isBull ? "BULLISH" : "BEARISH"
string confidence_str = header_samples >= 30 ? "High" : header_samples >= 15 ? "Medium" : "Low"
string fallback_note = peFallback ? "\n⚠️ Not enough " + direction_str + "-only history yet - rough estimate, sharpens as more trends complete.\n" : ""
string header_tooltip = "📊 TREND FORECAST\n═══════════════════════\n\n🎯 Direction: " + direction_str + "\n📈 Mode: " + i_predMode + "\n\n⏱️ DURATION ESTIMATE:\n• Predicted total: " + str.tostring(pe, "#") + " bars\n• Average: " + str.tostring(pa, "#.#") + " bars\n" + fallback_note + "\n📊 DATA:\n• Sample Size: " + str.tostring(header_samples) + " trends\n• Confidence: " + confidence_str + "\n\n" + (i_predMode == "Advanced" and not peFallback ? "🔬 ADVANCED MULTIPLIERS:\n• Structure • Asset Type • Flip Strength\n• Error Learning • Regime\n\n" : "") + "💡 % labels = share of past " + direction_str + " trends that lasted at least that long.\n\n⚠️ Historical analysis, not a guarantee."
int center_bar = bar_index + int(pe / 2)
label lbl_center = label.new(center_bar, box_top, "🔮 Trend Analysis", style=isBull ? label.style_label_down : label.style_label_up, color=color.new(color.blue, 0), textcolor=color.white, size=size.small, tooltip=header_tooltip, yloc=yloc.price)
array.push(pred_labels, lbl_center)
// Survival milestone labels via INVERSE CDF (percentile positioning). Placing labels
// at fixed fractions of the strip and reading empirical % ties on sparse data (many
// "15%" in a row). Instead we place each label at a real duration percentile and label
// it with that percentile's survival level - always a distinct, declining 75/50/25/10.
if i_showPredLabels
int survN = array.size(survArr)
// survival target -> duration percentile (survival = 100 - percentile)
array<int> survPct = array.from(25, 50, 75, 90)
// Keep % labels clear of the candles. Uptrend: strip sits below price, so put
// them below the strip. Downtrend: strip sits ABOVE price, so put them below
// the candles (below the flip bar's low) instead of on the bars.
float lbl_y = isBull ? box_bottom - predATR * 0.15 : low - predATR * 0.5
for q in survPct
int surv = 100 - q
// duration at this percentile (from real history if we have enough; otherwise
// fall back to a linear position along the projected strip)
float dur = survN >= 4 ? array.percentile_nearest_rank(survArr, q) : pe * (q / 100.0)
int x = bar_index + math.max(1, int(dur))
string direction = isBull ? "BULL" : "BEAR"
string lbl_tooltip = "🎯 Trend Survival\n\n" + str.tostring(surv) + "% of past " + direction + " trends lasted at least ~" + str.tostring(int(dur)) + " bars from the flip.\n\nBased on " + str.tostring(survN) + " past trends" + (survN >= 4 ? "" : " (modeled - low history)")
label lbl_prob = label.new(x, lbl_y, str.tostring(surv) + "%", style=label.style_none, textcolor=base_col, text_formatting=text.format_bold, size=size.small, yloc=yloc.price, tooltip=lbl_tooltip)
array.push(pred_labels, lbl_prob)
// Divergence labels on chart
if i_showDivergence and bullishDiv
string bullDivTooltip = "🟢 BULLISH DIVERGENCE\n\nPrice: Lower Low ↘\nRSI: Higher Low ↗\n\nPrevious Price Low: " + str.tostring(prevPriceLow, "#.##") + "\n" + "Current Price Low: " + str.tostring(pivotLow, "#.##") + "\n" + "Previous RSI Low: " + str.tostring(prevRsiLow, "#.#") + "\n" + "Current RSI Low: " + str.tostring(rsiPivotLow, "#.#") + "\n\n" + "📈 SIGNAL: Potential reversal UP\n" + "Selling pressure weakening despite lower prices"
label.new(bar_index - i_divLookback, low[i_divLookback], "Bull\nDiv",
style=label.style_label_up, color=color.new(color.green, 30),
textcolor=color.white, size=labelSize, tooltip=bullDivTooltip)
if i_showDivergence and bearishDiv
string bearDivTooltip = "🔴 BEARISH DIVERGENCE\n\n" + "Price: Higher High ↗\n" + "RSI: Lower High ↘\n\n" + "Previous Price High: " + str.tostring(prevPriceHigh, "#.##") + "\n" + "Current Price High: " + str.tostring(pivotHigh, "#.##") + "\n" + "Previous RSI High: " + str.tostring(prevRsiHigh, "#.#") + "\n" + "Current RSI High: " + str.tostring(rsiPivotHigh, "#.#") + "\n\n" + "📉 SIGNAL: Potential reversal DOWN\n" + "Buying pressure weakening despite higher prices"
label.new(bar_index - i_divLookback, high[i_divLookback], "Bear\nDiv",
style=label.style_label_down, color=color.new(color.red, 30),
textcolor=color.white, size=labelSize, tooltip=bearDivTooltip)
// buffer used by SL logic
slBuf = xATR * 0.10
// ATR for SL calculation (separate from UT Bot ATR)
slATR = ta.atr(slAtrLen)
atrPercent = close != 0 ? (slATR / close * 100) : 1.0
// ----- SL calculators (LONG / SHORT) -----
// Comprehensive multi-method SL calculation
calcLongSL(_entry) =>
float sl = na
float tickValue = syminfo.mintick
if slMethod == "ATR"
// Standard ATR with user multiplier
sl := _entry - (slATR * slAtrMult)
else if slMethod == "% Based"
// Fixed percentage from entry
sl := _entry * (1 - slPctStop / 100)
else if slMethod == "Tick Based"
// Fixed number of ticks
sl := _entry - (slTickStop * tickValue)
else if slMethod == "Swing"
// Recent swing low
float swingFloor = ta.lowest(low, swingLen)
sl := not na(swingFloor) and swingFloor < _entry ? swingFloor : _entry - slATR * 1.5
else if slMethod == "Scaled ATR"
// Dynamic multiplier based on volatility %
float scaledMult = atrPercent > 5 ? 2.5 :
atrPercent > 3 ? 2.0 :
atrPercent > 1.5 ? 1.5 : 1.2
float atrDist = slATR * scaledMult
float minDist = _entry * 0.01 // Minimum 1% distance
sl := _entry - math.max(atrDist, minDist)
else if slMethod == "Smart Adaptive"
// Auto-adjusts to volatility level with minimum 1% floor
float smartMult = atrPercent > 3 ? 1.5 : atrPercent > 1.5 ? 1.0 : 0.7
float atrDist = slATR * smartMult
float minDist = _entry * 0.01 // Minimum 1% distance
sl := _entry - math.max(atrDist, minDist)
else if slMethod == "Safer"
// Widest of multiple methods (most conservative)
float atrSL = _entry - (slATR * slAtrMult)
float pctSL = _entry * (1 - slPctStop / 100)
float swingSL = ta.lowest(low, swingLen)
swingSL := na(swingSL) or swingSL >= _entry ? _entry - slATR * 2 : swingSL
sl := math.min(atrSL, math.min(pctSL, swingSL))
// Fallback if still na
sl := na(sl) ? _entry - slATR * 1.5 : sl
sl - slBuf
calcShortSL(_entry) =>
float sl = na
float tickValue = syminfo.mintick
if slMethod == "ATR"
sl := _entry + (slATR * slAtrMult)
else if slMethod == "% Based"
sl := _entry * (1 + slPctStop / 100)
else if slMethod == "Tick Based"
sl := _entry + (slTickStop * tickValue)
else if slMethod == "Swing"
float swingCeil = ta.highest(high, swingLen)
sl := not na(swingCeil) and swingCeil > _entry ? swingCeil : _entry + slATR * 1.5
else if slMethod == "Scaled ATR"
float scaledMult = atrPercent > 5 ? 2.5 :
atrPercent > 3 ? 2.0 :
atrPercent > 1.5 ? 1.5 : 1.2
float atrDist = slATR * scaledMult
float minDist = _entry * 0.01 // Minimum 1% distance
sl := _entry + math.max(atrDist, minDist)
else if slMethod == "Smart Adaptive"
// Auto-adjusts to volatility level with minimum 1% floor
float smartMult = atrPercent > 3 ? 1.5 : atrPercent > 1.5 ? 1.0 : 0.7
float atrDist = slATR * smartMult
float minDist = _entry * 0.01 // Minimum 1% distance
sl := _entry + math.max(atrDist, minDist)
else if slMethod == "Safer"
float atrSL = _entry + (slATR * slAtrMult)
float pctSL = _entry * (1 + slPctStop / 100)
float swingSL = ta.highest(high, swingLen)
swingSL := na(swingSL) or swingSL <= _entry ? _entry + slATR * 2 : swingSL
sl := math.max(atrSL, math.max(pctSL, swingSL))
sl := na(sl) ? _entry + slATR * 1.5 : sl
sl + slBuf
// --- Leg state (reset on every UT flip) ---
var int tradeDir = na
bool commitFlip = (utBuy or utSell) and barstate.isconfirmed
// IMPORTANT: Check if previous trade's TP was hit BEFORE resetting on new signal
// This fixes the bug where TP hit wasn't counted when a new signal fires on same bar
// NOTE: TP check runs FIRST so wins take priority over losses (consistent with main logic)
if commitFlip and not na(tradeDir) and statTradeOpen and not na(entryBar) and bar_index > entryBar
bool prevTpHit = false
if tradeDir == 1 // LONG trade - check if any TP was hit by HIGH
if tp1On and not tp1Frozen and not na(tp1Y) and high >= tp1Y
prevTpHit := true
if tp15On and not tp15Frozen and not na(tp15Y) and high >= tp15Y
prevTpHit := true
if tp2On and not tp2Frozen and not na(tp2Y) and high >= tp2Y
prevTpHit := true
if tp3On and not tp3Frozen and not na(tp3Y) and high >= tp3Y
prevTpHit := true
else // SHORT trade - check if any TP was hit by LOW
if tp1On and not tp1Frozen and not na(tp1Y) and low <= tp1Y
prevTpHit := true
if tp15On and not tp15Frozen and not na(tp15Y) and low <= tp15Y
prevTpHit := true
if tp2On and not tp2Frozen and not na(tp2Y) and low <= tp2Y
prevTpHit := true
if tp3On and not tp3Frozen and not na(tp3Y) and low <= tp3Y
prevTpHit := true
if prevTpHit
statWins += 1
int barsToTP = bar_index - entryBar
statTotalBars += barsToTP
statWinBars += 1
statTradeOpen := false
// IMPORTANT: Check if previous trade's SL was hit BEFORE resetting on new signal
// This fixes the bug where SL hit wasn't counted when a new opposite signal fires
// NOTE: SL check runs AFTER TP check - if TP was hit, statTradeOpen is already false
if commitFlip and not na(tradeDir) and not slFrozen and statTradeOpen and not na(slPrice)
float _tol = syminfo.mintick * 0.51
bool prevSlHit = (tradeDir == 1 ? low <= slPrice + _tol : high >= slPrice - _tol)
if prevSlHit
statLosses += 1
statTradeOpen := false
rtFlipAlerts = true // Always enabled - alerts are core functionality
var int lastFlipBar = na
if rtFlipAlerts and commitFlip and lastFlipBar != bar_index
lastFlipBar := bar_index
string side = utBuy ? "BUY" : "SELL"
alert("UT FLIP " + side + " | " + syminfo.ticker + " " + timeframe.period + " | Close: " + str.tostring(close), alert.freq_once_per_bar_close)
// --- build SL on UT flip ---
var float entry = na
if commitFlip
if not na(slLine)
line.delete(slLine), slLine := na
if not na(slLbl)
label.delete(slLbl), slLbl := na
// new state
slStartBar := bar_index
entry := close
baseSl := utBuy ? calcLongSL(entry) : calcShortSL(entry)
float extra = slExtraTicks * syminfo.mintick
slPrice := utBuy ? (baseSl - extra) : (baseSl + extra)
slColor := utBuy ? color.aqua : color.orange
// Sanity: keep SL on the correct side of entry (handles rare wrong-side floors)
float minFloor = xATR * 1.50 + slBuf + extra
if utBuy and slPrice >= entry
slPrice := entry - minFloor
if utSell and slPrice <= entry
slPrice := entry + minFloor
// reset leg state + TP flags + SL frozen
legDone := false
legDoneBar := na
slFrozen := false // CRITICAL: reset SL frozen flag for new trade
tp1Hit := false, tp15Hit := false, tp2Hit := false, tp3Hit := false
// Stats: new trade opened (only count if within lookback period)
bool inLookback = time >= timenow - i_statsLookback * 86400000
if inLookback
statTotalTrades += 1
statTradeOpen := inLookback
// establish trade direction and reset run trackers early
tradeDir := utBuy ? 1 : -1
// initial render
if slShowVis
slLine := line.new(slStartBar, slPrice, slStartBar, slPrice, xloc=xloc.bar_index, extend=extend.none, color=slColor, width=2)
slLbl := label.new(slStartBar, slPrice, "Suggested Stop Loss", xloc=xloc.bar_index, style=label.style_label_left, textcolor=color.white, color=slColor, size=labelSize)
// helper flags for this bar
bool inLegNow = not na(tradeDir)
int dirNow = na(tradeDir) ? 0 : tradeDir
// SL freeze-on-touch (final after all intrabar updates)
// Guard bar_index > slStartBar: never count an SL hit on the entry bar itself - that
// bar's low/high happened BEFORE entry (at the close), so it must not stop out the trade
// before it starts. Stops are only checked from the bar AFTER entry (matches TP logic).
if inLegNow and not slFrozen and not na(slPrice) and not na(slStartBar) and bar_index > slStartBar
float _tol = syminfo.mintick * 0.51
bool wickHit = (dirNow == 1 ? low <= slPrice + _tol : high >= slPrice - _tol)
if wickHit
slFrozen := true
legDone := true
legDoneBar := bar_index
entryFrozen := true
// Stats: trade lost (SL hit)
if statTradeOpen
statLosses += 1
statTradeOpen := false
// Only create SL objects if feature is enabled
if slFeatureOn
[slLine, slLbl] = ensureSLObjects(slLine, slLbl, slStartBar, slPrice, slColor)
int x2c_live = linesLimit10 ? math.min(bar_index, slStartBar + 10) : bar_index
int x2c = capX2(x2c_live)
if slShowVis and not na(slLine)
line.set_x2(slLine, x2c)
line.set_y1(slLine, slPrice)
line.set_y2(slLine, slPrice)
if slShowVis and not na(slLbl)
label.set_x(slLbl, x2c)
label.set_text(slLbl, "SL Touched ❌")
label.set_style(slLbl, label.style_label_left)
if not na(entryLine) and not na(entryBar)
int xe_live = linesLimit10 ? math.min(bar_index, entryBar + 10) : bar_index
int xe_cap = capX2(xe_live)
line.set_x2(entryLine, xe_cap)
if not na(entryLbl)
label.set_x(entryLbl, xe_cap)
label.set_style(entryLbl, label.style_label_left)
if not na(tp1Line)
line.set_x2(tp1Line, x2c)
if not tp1Frozen
tp1Frozen := true
if not na(tp1Lbl)
label.set_x(tp1Lbl, x2c)
label.set_style(tp1Lbl, label.style_label_left)
if not na(tp15Line)
line.set_x2(tp15Line, x2c)
if not tp15Frozen
tp15Frozen := true
if not na(tp15Lbl)
label.set_x(tp15Lbl, x2c)
label.set_style(tp15Lbl, label.style_label_left)
if not na(tp2Line)
line.set_x2(tp2Line, x2c)
if not tp2Frozen
tp2Frozen := true
if not na(tp2Lbl)
label.set_x(tp2Lbl, x2c)
label.set_style(tp2Lbl, label.style_label_left)
if not na(tp3Line)
line.set_x2(tp3Line, x2c)
if not tp3Frozen
tp3Frozen := true
if not na(tp3Lbl)
label.set_x(tp3Lbl, x2c)
label.set_style(tp3Lbl, label.style_label_left)
// Label X offset: push SL/TP/Entry price tags a few bars RIGHT of the last bar so
// they sit in the empty margin instead of overlapping the current candle. While the
// leg is live they follow the last bar; once done they lock near the leg's end.
int LBL_X_OFF = 3
// freeze/cap X when SL is frozen or leg is done
if slShowVis and not na(slLine) and (slFrozen or legDone)
int x2 = capX2(linesLimit10 ? math.min(bar_index, slStartBar + 10) : bar_index)
line.set_x2(slLine, x2)
if not na(slLbl)
label.set_x(slLbl, x2 + LBL_X_OFF)
// maintain SL line/label (cap X2 on legDone; do not update Y when frozen/done)
if not na(slLine) and not na(slStartBar)
int x2_any = linesLimit10 ? math.min(bar_index, slStartBar + 10) : bar_index
line.set_x1(slLine, slStartBar)
line.set_x2(slLine, capX2(x2_any))
if slShowVis and not (slFrozen or legDone) and not na(slPrice)
bool _sl_changed = na(slPrice[1]) or slPrice != slPrice[1]
if _sl_changed
line.set_y1(slLine, slPrice)
line.set_y2(slLine, slPrice)
if slShowVis and not na(slLbl)
label.set_x(slLbl, legDone ? capX2(x2_any) + LBL_X_OFF : bar_index + LBL_X_OFF)
if not (slFrozen or legDone) and not na(slPrice)
bool _sl_changed2 = na(slPrice[1]) or slPrice != slPrice[1]
if _sl_changed2
label.set_y(slLbl, slPrice)
if not slFeatureOn
if not na(slLine)
line.delete(slLine), slLine := na
if not na(slLbl)
label.delete(slLbl), slLbl := na
//======================================================
//==================== TAKE PROFITS ====================
//======================================================
clearLine(line l) =>
if not na(l)
line.delete(l)
l
clearLabel(label lb) =>
if not na(lb)
label.delete(lb)
lb
// Helper: generate TP label text based on display mode (global function)
getTpText(string _name, float _price, float _entry, bool _hit) =>
if tpDisplayMode == "Detailed" and not na(_price) and not na(_entry) and _entry != 0
float pctGain = ((_price - _entry) / _entry) * 100
string sign = pctGain >= 0 ? "+" : ""
_hit ? _name + " ✅\n" + str.tostring(_price, "$"+"#.##") + sign + str.tostring(pctGain, "#.#") + "%)" :
_name + "\n" + str.tostring(_price, "$"+"#.##") + sign + str.tostring(pctGain, "#.#") + "%)"
else
_hit ? _name + " ✅" : _name
// Build new on UT flip
if commitFlip and not na(baseSl)
// logic/state reset
clearLine(tp1Line), tp1Line := na
clearLine(tp15Line), tp15Line := na
clearLine(tp2Line), tp2Line := na
clearLine(tp3Line), tp3Line := na
clearLabel(tp1Lbl), tp1Lbl := na
clearLabel(tp15Lbl), tp15Lbl := na
clearLabel(tp2Lbl), tp2Lbl := na
clearLabel(tp3Lbl), tp3Lbl := na
tp1Y := na, tp15Y := na, tp2Y := na, tp3Y := na
tp1Frozen := false, tp15Frozen := false, tp2Frozen := false, tp3Frozen := false
tpInitRisk := math.abs(entry - baseSl)
float _pad = 0.0
_col = slColor
int _x2 = bar_index
if utBuy
tp1Y := tp1On ? entry + tpInitRisk * TP1 + _pad : na
tp15Y := tp15On ? entry + tpInitRisk * TP1_5 + _pad : na
tp2Y := tp2On ? entry + tpInitRisk * TP2 + _pad : na
tp3Y := tp3On ? entry + tpInitRisk * TP3 + _pad : na
else
tp1Y := tp1On ? entry - tpInitRisk * TP1 - _pad : na
tp15Y := tp15On ? entry - tpInitRisk * TP1_5 - _pad : na
tp2Y := tp2On ? entry - tpInitRisk * TP2 - _pad : na
tp3Y := tp3On ? entry - tpInitRisk * TP3 - _pad : na
// render (include ✅ if a hit was already recorded)
if tpShowVis
if tp1On and not na(tp1Y)
tp1Line := line.new(slStartBar, tp1Y, _x2, tp1Y, xloc=xloc.bar_index, extend=extend.none, color=_col, width=2)
tp1Lbl := label.new(_x2, tp1Y, getTpText("TP1", tp1Y, entry, tp1Hit), xloc=xloc.bar_index, style=label.style_label_right, textcolor=color.white, color=_col, size=labelSize)
if tp15On and not na(tp15Y)
tp15Line := line.new(slStartBar, tp15Y, _x2, tp15Y, xloc=xloc.bar_index, extend=extend.none, color=_col, width=2)
tp15Lbl := label.new(_x2, tp15Y, getTpText("TP1.5", tp15Y, entry, tp15Hit), xloc=xloc.bar_index, style=label.style_label_right, textcolor=color.white, color=_col, size=labelSize)
if tp2On and not na(tp2Y)
tp2Line := line.new(slStartBar, tp2Y, _x2, tp2Y, xloc=xloc.bar_index, extend=extend.none, color=_col, width=2)
tp2Lbl := label.new(_x2, tp2Y, getTpText("TP2", tp2Y, entry, tp2Hit), xloc=xloc.bar_index, style=label.style_label_right, textcolor=color.white, color=_col, size=labelSize)
if tp3On and not na(tp3Y)
tp3Line := line.new(slStartBar, tp3Y, _x2, tp3Y, xloc=xloc.bar_index, extend=extend.none, color=_col, width=2)
tp3Lbl := label.new(_x2, tp3Y, getTpText("TP3", tp3Y, entry, tp3Hit), xloc=xloc.bar_index, style=label.style_label_right, textcolor=color.white, color=_col, size=labelSize)
// Single maintenance block (RENDER ONLY)
int _x2bRaw = linesLimit10 and not na(slStartBar) ? math.min(bar_index, slStartBar + 10) : bar_index
int _x2b = capX2(_x2bRaw)
// Label x: live leg follows the last bar's right margin; done leg locks near its end
int _x2bLbl = legDone ? _x2b + LBL_X_OFF : bar_index + LBL_X_OFF
// TP1
if not na(tp1Line)
line.set_x2(tp1Line, _x2b)
if tpShowVis and not (legDone or tp1Frozen) and not na(tp1Y)
line.set_y1(tp1Line, tp1Y), line.set_y2(tp1Line, tp1Y)
if tpShowVis and not na(tp1Lbl)
label.set_x(tp1Lbl, _x2bLbl)
if not (legDone or tp1Frozen) and not na(tp1Y)
label.set_y(tp1Lbl, tp1Y)
label.set_style(tp1Lbl, label.style_label_left)
// TP1.5
if not na(tp15Line)
line.set_x2(tp15Line, _x2b)
if tpShowVis and not (legDone or tp15Frozen) and not na(tp15Y)
line.set_y1(tp15Line, tp15Y), line.set_y2(tp15Line, tp15Y)
if tpShowVis and not na(tp15Lbl)
label.set_x(tp15Lbl, _x2bLbl)
if not (legDone or tp15Frozen) and not na(tp15Y)
label.set_y(tp15Lbl, tp15Y)
label.set_style(tp15Lbl, label.style_label_left)
// TP2
if not na(tp2Line)
line.set_x2(tp2Line, _x2b)
if tpShowVis and not (legDone or tp2Frozen) and not na(tp2Y)
line.set_y1(tp2Line, tp2Y), line.set_y2(tp2Line, tp2Y)
if tpShowVis and not na(tp2Lbl)
label.set_x(tp2Lbl, _x2bLbl)
if not (legDone or tp2Frozen) and not na(tp2Y)
label.set_y(tp2Lbl, tp2Y)
label.set_style(tp2Lbl, label.style_label_left)
// TP3
if not na(tp3Line)
line.set_x2(tp3Line, _x2b)
if tpShowVis and not (legDone or tp3Frozen) and not na(tp3Y)
line.set_y1(tp3Line, tp3Y), line.set_y2(tp3Line, tp3Y)
if tpShowVis and not na(tp3Lbl)
label.set_x(tp3Lbl, _x2bLbl)
if not (legDone or tp3Frozen) and not na(tp3Y)
label.set_y(tp3Lbl, tp3Y)
label.set_style(tp3Lbl, label.style_label_left)
// --- TP freeze-on-touch (keeps working even when lines limited to 10 bars) ---
// Note: added 'not commitFlip' to prevent false triggers on signal bar (entryBar still old)
if tpFreezeOnTouch and not legDone and not commitFlip and (not na(entryBar) and bar_index > entryBar)
if tp1On and not tp1Frozen and not na(tp1Y) and ((tradeDir == 1 and high >= tp1Y) or (tradeDir == -1 and low <= tp1Y))
tp1Frozen := true, tp1Hit := true
// Stats: count WIN when TP1 is hit (first target)
if statTradeOpen and not na(entryBar)
statWins += 1
int barsToTP = bar_index - entryBar
statTotalBars += barsToTP
statWinBars += 1
statTradeOpen := false
if tpFeatureOn and not na(tp1Lbl)
label.set_text(tp1Lbl, getTpText("TP1", tp1Y, entryPx, true)), label.set_style(tp1Lbl, label.style_label_left)
if tp15On and not tp15Frozen and not na(tp15Y) and ((tradeDir == 1 and high >= tp15Y) or (tradeDir == -1 and low <= tp15Y))
tp15Frozen := true, tp15Hit := true
// Stats: count WIN if this is the first TP hit (TP1 was off or not hit yet)
if statTradeOpen and not na(entryBar)
statWins += 1
int barsToTP = bar_index - entryBar
statTotalBars += barsToTP
statWinBars += 1
statTradeOpen := false
if tpFeatureOn and not na(tp15Lbl)
label.set_text(tp15Lbl, getTpText("TP1.5", tp15Y, entryPx, true)), label.set_style(tp15Lbl, label.style_label_left)
if tp2On and not tp2Frozen and not na(tp2Y) and ((tradeDir == 1 and high >= tp2Y) or (tradeDir == -1 and low <= tp2Y))
tp2Frozen := true, tp2Hit := true
// Stats: count WIN if this is the first TP hit
if statTradeOpen and not na(entryBar)
statWins += 1
int barsToTP = bar_index - entryBar
statTotalBars += barsToTP
statWinBars += 1
statTradeOpen := false
if tpFeatureOn and not na(tp2Lbl)
label.set_text(tp2Lbl, getTpText("TP2", tp2Y, entryPx, true)), label.set_style(tp2Lbl, label.style_label_left)
if tp3On and not tp3Frozen and not na(tp3Y) and ((tradeDir == 1 and high >= tp3Y) or (tradeDir == -1 and low <= tp3Y))
tp3Frozen := true, tp3Hit := true
// Stats: count WIN if this is the first TP hit
if statTradeOpen and not na(entryBar)
statWins += 1
int barsToTP = bar_index - entryBar
statTotalBars += barsToTP
statWinBars += 1
statTradeOpen := false
if tpFeatureOn and not na(tp3Lbl)
label.set_text(tp3Lbl, getTpText("TP3", tp3Y, entryPx, true)), label.set_style(tp3Lbl, label.style_label_left)
// --- close the leg when the farthest enabled TP is touched (unified) ---
float farTP = na
// compute farthest TP level in the active direction
if tradeDir == 1
if tp1On and not na(tp1Y)
farTP := na(farTP) ? tp1Y : math.max(farTP, tp1Y)
if tp15On and not na(tp15Y)
farTP := na(farTP) ? tp15Y : math.max(farTP, tp15Y)
if tp2On and not na(tp2Y)
farTP := na(farTP) ? tp2Y : math.max(farTP, tp2Y)
if tp3On and not na(tp3Y)
farTP := na(farTP) ? tp3Y : math.max(farTP, tp3Y)
else if tradeDir == -1
if tp1On and not na(tp1Y)
farTP := na(farTP) ? tp1Y : math.min(farTP, tp1Y)
if tp15On and not na(tp15Y)
farTP := na(farTP) ? tp15Y : math.min(farTP, tp15Y)
if tp2On and not na(tp2Y)
farTP := na(farTP) ? tp2Y : math.min(farTP, tp2Y)
if tp3On and not na(tp3Y)
farTP := na(farTP) ? tp3Y : math.min(farTP, tp3Y)
// unified detection & handling (symmetric time rule: require bar after entry)
// Note: added 'not commitFlip' to prevent false triggers on signal bar
bool canCheckFar = not na(entryBar) and bar_index > entryBar and not na(farTP) and not legDone and not commitFlip
bool touchedFar = canCheckFar and ((tradeDir == 1 and high >= farTP) or (tradeDir == -1 and low <= farTP))
if touchedFar
// mark all closer/equal TPs as hit/frozen (ensures TP1/TP2 get ✅ when TP3 is hit directly)
if tradeDir == 1
if tp1On and not tp1Frozen and not na(tp1Y) and tp1Y <= farTP
tp1Frozen := true, tp1Hit := true
if tpFeatureOn and not na(tp1Lbl)
label.set_text(tp1Lbl, getTpText("TP1", tp1Y, entryPx, true)), label.set_style(tp1Lbl, label.style_label_left)
if tp15On and not tp15Frozen and not na(tp15Y) and tp15Y <= farTP
tp15Frozen := true, tp15Hit := true
if tpFeatureOn and not na(tp15Lbl)
label.set_text(tp15Lbl, getTpText("TP1.5", tp15Y, entryPx, true)), label.set_style(tp15Lbl, label.style_label_left)
if tp2On and not tp2Frozen and not na(tp2Y) and tp2Y <= farTP
tp2Frozen := true, tp2Hit := true
if tpFeatureOn and not na(tp2Lbl)
label.set_text(tp2Lbl, getTpText("TP2", tp2Y, entryPx, true)), label.set_style(tp2Lbl, label.style_label_left)
if tp3On and not tp3Frozen and not na(tp3Y) and tp3Y <= farTP
tp3Frozen := true, tp3Hit := true
if tpFeatureOn and not na(tp3Lbl)
label.set_text(tp3Lbl, getTpText("TP3", tp3Y, entryPx, true)), label.set_style(tp3Lbl, label.style_label_left)
else
if tp1On and not tp1Frozen and not na(tp1Y) and tp1Y >= farTP
tp1Frozen := true, tp1Hit := true
if tpFeatureOn and not na(tp1Lbl)
label.set_text(tp1Lbl, getTpText("TP1", tp1Y, entryPx, true)), label.set_style(tp1Lbl, label.style_label_left)
if tp15On and not tp15Frozen and not na(tp15Y) and tp15Y >= farTP
tp15Frozen := true, tp15Hit := true
if tpFeatureOn and not na(tp15Lbl)
label.set_text(tp15Lbl, getTpText("TP1.5", tp15Y, entryPx, true)), label.set_style(tp15Lbl, label.style_label_left)
if tp2On and not tp2Frozen and not na(tp2Y) and tp2Y >= farTP
tp2Frozen := true, tp2Hit := true
if tpFeatureOn and not na(tp2Lbl)
label.set_text(tp2Lbl, getTpText("TP2", tp2Y, entryPx, true)), label.set_style(tp2Lbl, label.style_label_left)
if tp3On and not tp3Frozen and not na(tp3Y) and tp3Y >= farTP
tp3Frozen := true, tp3Hit := true
if tpFeatureOn and not na(tp3Lbl)
label.set_text(tp3Lbl, getTpText("TP3", tp3Y, entryPx, true)), label.set_style(tp3Lbl, label.style_label_left)
// freeze SL/ENTRY and end leg
legDone := true
legDoneBar := bar_index
slFrozen := true
entryFrozen := true
// lock ENTRY X at freeze bar (respect 10-bar limit)
if not na(entryLine) and not na(entryBar)
bool limit10 = linesLimit10
int x2f = limit10 ? math.min(bar_index, entryBar + 10) : bar_index
line.set_x2(entryLine, x2f)
if not na(entryLbl)
label.set_x(entryLbl, x2f)
// update SL label text
if slFeatureOn and not na(slLbl)
label.set_text(slLbl, "SL Frozen"), label.set_style(slLbl, label.style_label_left)
// capture entry state on flip (logic always)
if commitFlip
if not na(entryLine)
line.delete(entryLine), entryLine := na
if not na(entryLbl)
label.delete(entryLbl), entryLbl := na
entryPx := close
entryBar := bar_index
entryCol := slColor
entryFrozen := false
// create immediately when feature is ON (independent of utVizGate)
if entryFeatureOn
entryLine := line.new(entryBar, entryPx, entryBar, entryPx,
xloc=xloc.bar_index, extend=extend.none, color=color.navy, width=2)
entryLbl := label.new(entryBar, entryPx, "ENTRY",
xloc=xloc.bar_index, style=label.style_label_right,
textcolor=color.white, color=color.navy, size=labelSize)
// render (gated)
if not na(entryLine) and not na(entryBar) and not na(entryPx)
bool limit10 = linesLimit10
int x2_live = limit10 ? math.min(bar_index, entryBar + 10) : bar_index
int x2_frozen = not na(legDoneBar) ? (limit10 ? math.min(legDoneBar, entryBar + 10) : legDoneBar) : x2_live
int x2_final = (entryFrozen or legDone) ? x2_frozen : x2_live
line.set_x2(entryLine, x2_final)
if entryShowVis and not (entryFrozen or legDone)
line.set_x1(entryLine, entryBar)
line.set_y1(entryLine, entryPx)
line.set_y2(entryLine, entryPx)
if entryShowVis
if na(entryLbl)
entryLbl := label.new(entryBar, entryPx, "ENTRY",
xloc=xloc.bar_index, style=label.style_label_right,
textcolor=color.white, color=entryCol, size=labelSize)
if not (entryFrozen or legDone)
label.set_y(entryLbl, entryPx)
label.set_x(entryLbl, (entryFrozen or legDone) ? x2_final + LBL_X_OFF : bar_index + LBL_X_OFF)
label.set_style(entryLbl, label.style_label_left)
label.set_text(entryLbl, "ENTRY")
else
if not entryShow and not na(entryLine)
line.delete(entryLine), entryLine := na
if not entryShow and not na(entryLbl)
label.delete(entryLbl), entryLbl := na
//======================================================
//======================= ALERTS =======================
//======================================================
utBuyBar = utBuy
utSellBar = utSell
alertcondition(utBuyBar, title="✅Momentum Buy Signal✅", message="✅Momentum Buy Signal✅ | {{ticker}} {{interval}} | Close: {{close}}")
alertcondition(utSellBar, title="❌Momentum Sell Signal❌", message="❌Momentum Sell Signal❌ | {{ticker}} {{interval}} | Close: {{close}}")
// Dynamic alerts - work with "Any alert() function call" in TradingView
// Note: alertcondition() only creates selectable alert types, but doesn't fire on "Any alert()"
// These dynamic calls ensure alerts work regardless of user's alert setup choice
if utBuyBar and barstate.isconfirmed
alert("✅Momentum Buy Signal✅ | " + syminfo.ticker + " " + timeframe.period + " | Close: " + str.tostring(close), alert.freq_once_per_bar_close)
if utSellBar and barstate.isconfirmed
alert("❌Momentum Sell Signal❌ | " + syminfo.ticker + " " + timeframe.period + " | Close: " + str.tostring(close), alert.freq_once_per_bar_close)
//======================================================
//=================== STATUS TABLE =====================
//======================================================
// Table position
tablePos = switch i_tablePos
"Top Left" => position.top_left
"Top Right" => position.top_right
"Bottom Left" => position.bottom_left
"Bottom Right" => position.bottom_right
=> position.top_right
// Table size
tableSize = switch i_tableSize
"Tiny" => size.tiny
"Small" => size.small
"Normal" => size.normal
"Large" => size.large
=> size.small
// Colors
color tblBg = color.new(#1a1a2e, 5)
color tblBorder = color.new(#4a4a6a, 20)
color txtWhite = color.white
color colGreen = #00d4aa
color colRed = #ff6b6b
color colYellow = #ffd93d
// Calculate current values for display
[_diplus, _diminus, _adxVal] = ta.dmi(i_adxPeriod, i_adxPeriod)
float _rsiVal = ta.rsi(close, i_rsiLen)
float _volSma = ta.sma(volume, i_volSmaLen)
float _volRatio = _volSma > 0 ? volume / _volSma : 1.0
// Filter status checks (for display - recalculate to show current state)
bool _regimePass = i_enableLayer1 ? _adxVal >= i_adxThreshold : true
bool _cooldownPass = l1_cooldownOk
bool _volumePass = i_useVolFilter ? _volRatio >= i_volThreshold : true
bool _rsiPassBuy = i_useRsiFilter ? _rsiVal < i_rsiOB : true
bool _rsiPassSell = i_useRsiFilter ? _rsiVal > i_rsiOS : true
bool _swingPassBuy = swingOkBuy
bool _swingPassSell= swingOkSell
// Signal state
string sigState = barbuy ? "BULLISH" : barsell ? "BEARISH" : "NEUTRAL"
string sigEmoji = barbuy ? "🟢" : barsell ? "🔴" : "⚪"
color sigColor = barbuy ? colGreen : barsell ? colRed : txtWhite
// Asset type icon and color
string assetIcon = switch detectedAsset
ASSET_CRYPTO => "₿"
ASSET_FOREX => "💱"
ASSET_FUTURES => "📈"
ASSET_INDEX => "📉"
ASSET_FUND => "🏦"
ASSET_CFD => "📄"
ASSET_BOND => "🔒"
ASSET_STOCK => "📊"
=> "📊"
color assetCol = switch detectedAsset
ASSET_CRYPTO => color.new(#f7931a, 0)
ASSET_FOREX => color.new(#4caf50, 0)
ASSET_FUTURES => color.new(#2196f3, 0)
ASSET_INDEX => color.new(#9c27b0, 0)
ASSET_FUND => color.new(#00bcd4, 0)
ASSET_CFD => color.new(#ff9800, 0)
ASSET_BOND => color.new(#607d8b, 0)
=> txtWhite
// ══════════════════════════════════════════════════════════════════════════════
// CONFIDENCE SCORE CALCULATION
// ══════════════════════════════════════════════════════════════════════════════
// Progress bar function (10 blocks)
get_progress_bar(float pct) =>
int filled = math.round(pct / 10)
string bar = ""
for i = 0 to 9
bar := bar + (i < filled ? "█" : "░")
bar
// Calculate confidence score (0-100) based on engine alignment
// Weights: UT Bot 26%, SuperTrend 21%, Structure 18%, ADX 13%, MTF 8%, Volume 10%, Divergence 4%
float conf_ut_score = barbuy or barsell ? 26.0 : 13.0 // Clear direction = full points
// SuperTrend: direction alignment + strength bonus
bool conf_st_aligned = (barbuy and stBullish) or (barsell and stBearish)
float conf_st_score = conf_st_aligned ? 12.0 + (stStrength / 100.0 * 9.0) : 4.0 // Max 21
// Structure Break: direction alignment + strength bonus
bool conf_sb_aligned = (barbuy and sbBullish) or (barsell and sbBearish)
float conf_sb_score = conf_sb_aligned ? 11.0 + (sbStrength / 100.0 * 7.0) : 4.0 // Max 18
// ADX Regime: trending = full points
float conf_adx_score = l1_regimeOk ? 13.0 : 4.0
// MTF: alignment with current direction
bool conf_mtf_aligned = (barbuy and htfTrend >= 0) or (barsell and htfTrend <= 0)
float conf_mtf_score = conf_mtf_aligned ? 8.0 : 2.0
// Volume: higher volume = more conviction
float conf_vol_score = _volRatio >= 2.0 ? 10.0 : _volRatio >= 1.5 ? 8.0 : _volRatio >= 1.0 ? 6.0 : 3.0
// Divergence: recent aligned divergence = bonus, recent opposing divergence = penalty, stale/none = neutral
bool conf_div_recent = barsSinceDiv <= i_divLookback * 4
bool conf_div_aligned = conf_div_recent and ((barbuy and lastDivType == "BULL") or (barsell and lastDivType == "BEAR"))
bool conf_div_opposed = conf_div_recent and ((barbuy and lastDivType == "BEAR") or (barsell and lastDivType == "BULL"))
float conf_div_score = conf_div_aligned ? 4.0 : conf_div_opposed ? 0.0 : 2.0 // Max 4
// Total confidence (0-100)
float confidenceScore = conf_ut_score + conf_st_score + conf_sb_score + conf_adx_score + conf_mtf_score + conf_vol_score + conf_div_score
confidenceScore := math.min(100.0, math.max(0.0, confidenceScore))
// Progress bar and display
string confidenceBar = get_progress_bar(confidenceScore)
color confidenceColor = confidenceScore >= 80 ? color.green : confidenceScore >= 60 ? color.lime : confidenceScore >= 40 ? color.orange : color.red
string confidenceEmoji = confidenceScore >= 80 ? "🔥" : confidenceScore >= 60 ? "🟢" : confidenceScore >= 40 ? "⚖️" : confidenceScore >= 20 ? "⚠️" : "❌"
// S/R Zone boxes (drawn once on the last bar, same as the table - cheap, no per-bar redraw)
if utShowZones and barstate.islast
if array.size(zoneResBoxes) > 0
for i = 0 to array.size(zoneResBoxes) - 1
box.delete(array.get(zoneResBoxes, i))
array.clear(zoneResBoxes)
if array.size(zoneResLabels) > 0
for i = 0 to array.size(zoneResLabels) - 1
label.delete(array.get(zoneResLabels, i))
array.clear(zoneResLabels)
if array.size(zoneSupBoxes) > 0
for i = 0 to array.size(zoneSupBoxes) - 1
box.delete(array.get(zoneSupBoxes, i))
array.clear(zoneSupBoxes)
if array.size(zoneSupLabels) > 0
for i = 0 to array.size(zoneSupLabels) - 1
label.delete(array.get(zoneSupLabels, i))
array.clear(zoneSupLabels)
// Draw ONLY the single most-relevant zone on each side (nearest to price) - the
// others are tracked for the filter but drawing all 3+3 just clutters the chart.
int resIdx = nearestZoneIdx(zoneResPrice, close, true)
if resIdx >= 0
float zPrice = array.get(zoneResPrice, resIdx)
int zStart = array.get(zoneResStartBar, resIdx)
int zTouch = array.get(zoneResTouch, resIdx)
box b = box.new(zStart, zPrice + zoneTol, bar_index + 20, zPrice - zoneTol, bgcolor=color.new(color.red, 88), border_color=color.new(color.red, 40), border_width=1)
array.push(zoneResBoxes, b)
string resTooltip = "Resistance Zone (nearest)\n\nPrice: " + str.tostring(zPrice, format.mintick) + "\nTouches: " + str.tostring(zTouch) + " (pivot highs clustered here)\nZone width: ±" + str.tostring(zoneTol, "#.##") + " (" + str.tostring(i_zoneWidthMult, "0.0") + "× ATR)\n\n" + (i_useZoneFilter ? "FILTER ON: blocks BUY while price is inside this zone" : "Display only (Zone Filter is OFF)")
label resLbl = label.new(bar_index + 20, zPrice, "R " + str.tostring(zPrice, format.mintick) + " (" + str.tostring(zTouch) + "×)", xloc=xloc.bar_index, style=label.style_label_left, color=color.new(color.red, 20), textcolor=color.white, size=size.small, tooltip=resTooltip)
array.push(zoneResLabels, resLbl)
int supIdx = nearestZoneIdx(zoneSupPrice, close, false)
if supIdx >= 0
float zPrice = array.get(zoneSupPrice, supIdx)
int zStart = array.get(zoneSupStartBar, supIdx)
int zTouch = array.get(zoneSupTouch, supIdx)
box b = box.new(zStart, zPrice + zoneTol, bar_index + 20, zPrice - zoneTol, bgcolor=color.new(color.green, 88), border_color=color.new(color.green, 40), border_width=1)
array.push(zoneSupBoxes, b)
string supTooltip = "Support Zone (nearest)\n\nPrice: " + str.tostring(zPrice, format.mintick) + "\nTouches: " + str.tostring(zTouch) + " (pivot lows clustered here)\nZone width: ±" + str.tostring(zoneTol, "#.##") + " (" + str.tostring(i_zoneWidthMult, "0.0") + "× ATR)\n\n" + (i_useZoneFilter ? "FILTER ON: blocks SELL while price is inside this zone" : "Display only (Zone Filter is OFF)")
label supLbl = label.new(bar_index + 20, zPrice, "S " + str.tostring(zPrice, format.mintick) + " (" + str.tostring(zTouch) + "×)", xloc=xloc.bar_index, style=label.style_label_left, color=color.new(color.green, 20), textcolor=color.white, size=size.small, tooltip=supTooltip)
array.push(zoneSupLabels, supLbl)
// Build table
var table statusTbl = na
color colNavy = color.new(#000080, 0)
if i_showTable and barstate.islast
// Delete old table
if not na(statusTbl)
table.delete(statusTbl)
// Create new table (3 columns, dynamic rows)
statusTbl := table.new(tablePos, 3, 25, bgcolor=tblBg, border_width=1, border_color=tblBorder, frame_width=2, frame_color=tblBorder)
int row = 0
// Row 0: Header (Navy background, merged, yellow text)
table.cell(statusTbl, 0, row, "UT GOD MODE", text_color=colYellow, text_size=tableSize, text_halign=text.align_center, bgcolor=colNavy)
table.cell(statusTbl, 1, row, "", bgcolor=colNavy)
table.cell(statusTbl, 2, row, "", bgcolor=colNavy)
table.merge_cells(statusTbl, 0, row, 2, row)
row += 1
// Row 1: Ticker | Asset Type (Navy background, yellow text)
table.cell(statusTbl, 0, row, "Asset: " + syminfo.ticker, text_color=colYellow, text_size=tableSize, text_halign=text.align_left, bgcolor=colNavy,
tooltip="Symbol: " + syminfo.ticker + "\nExchange: " + syminfo.prefix)
table.cell(statusTbl, 1, row, detectedAsset + " (" + str.tostring(assetMult, "0.0") + "×)", text_color=colYellow, text_size=tableSize, text_halign=text.align_left, bgcolor=colNavy,
tooltip="Asset: " + detectedAsset + "\nMultiplier: " + str.tostring(assetMult, "0.0") + "×\n\nMULTIPLIERS BY TYPE:\n• Crypto: 1.5× (high volatility)\n• Futures: 1.2× (moderate)\n• Stock: 1.0× (standard)\n• CFD: 1.0× (mirrors underlying)\n• Index: 0.9× (diversified)\n• Fund/ETF: 0.9× (diversified)\n• Forex: 0.8× (low volatility)\n• Bond: 0.5× (very stable)\n\nYou can override auto-detection in settings")
table.cell(statusTbl, 2, row, assetIcon, text_color=colYellow, text_size=tableSize, text_halign=text.align_center, bgcolor=colNavy)
row += 1
// Row 2: Confidence Score with Progress Bar
string confPctDisplay = str.tostring(confidenceScore, "#") + "%"
string confVolStatus = _volRatio >= 2.0 ? "🔥" : _volRatio >= 1.5 ? "✅" : _volRatio >= 1.0 ? "🟡" : "❌"
string confDivStatus = conf_div_aligned ? "✅" : conf_div_opposed ? "❌" : "➖"
string confTooltip = "🎯 CONFIDENCE SCORE\n\nWeighted combination of all engines:\n\n" +
"• UT Bot Direction: " + str.tostring(conf_ut_score, "#") + "/26\n" +
"• SuperTrend: " + str.tostring(conf_st_score, "#.#") + "/21 " + (conf_st_aligned ? "✅" : "❌") + "\n" +
"• Structure: " + str.tostring(conf_sb_score, "#.#") + "/18 " + (conf_sb_aligned ? "✅" : "❌") + "\n" +
"• ADX Regime: " + str.tostring(conf_adx_score, "#") + "/13 " + (l1_regimeOk ? "✅" : "❌") + "\n" +
"• MTF Align: " + str.tostring(conf_mtf_score, "#") + "/8 " + (conf_mtf_aligned ? "✅" : "❌") + "\n" +
"• Volume: " + str.tostring(conf_vol_score, "#") + "/10 " + confVolStatus + " (" + str.tostring(_volRatio, "0.0") + "×)\n\n" +
"• Divergence: " + str.tostring(conf_div_score, "#") + "/4 " + confDivStatus + " (" + lastDivType + ")\n\n" +
"TOTAL: " + str.tostring(confidenceScore, "#") + "/100\n\n" +
"🔥 80+ = Strong signal\n🟢 60+ = Good\n⚖️ 40+ = Weak\n⚠️ <40 = Very weak"
table.cell(statusTbl, 0, row, "🎯 Confidence", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Overall signal confidence based on engine alignment")
table.cell(statusTbl, 1, row, confidenceBar + " " + confPctDisplay, text_color=confidenceColor, text_size=tableSize, text_halign=text.align_center,
tooltip=confTooltip)
table.cell(statusTbl, 2, row, confidenceEmoji, text_color=color.white, text_size=tableSize, text_halign=text.align_center,
tooltip=confTooltip)
row += 1
// Row 3: Adaptive Mode
string adaptIcon = i_adaptiveMode == ADAPT_FIXED ? "📊" : i_adaptiveMode == ADAPT_DYNAMIC ? "🔄" : "⚡"
color adaptCol = i_adaptiveMode == ADAPT_FIXED ? txtWhite : i_adaptiveMode == ADAPT_DYNAMIC ? colGreen : colYellow
table.cell(statusTbl, 0, row, "Adaptive", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Adaptive Mode\nHow Key Value auto-adjusts to volatility\n• Fixed: No adjustment\n• Dynamic: Moderate (±15%)\n• Aggressive: Wide (±30%)")
table.cell(statusTbl, 1, row, i_adaptiveMode + " (" + str.tostring(adaptiveMult, "0.00") + "×)", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Mode: " + i_adaptiveMode + "\nCurrent multiplier: " + str.tostring(adaptiveMult, "0.00") + "×\nVolatility ratio: " + str.tostring(volatilityRatio, "0.00") + "×\n\n• Fixed: Always 1.0×\n• Dynamic:\n - Low vol (<0.8): 0.85×\n - Normal: 1.0×\n - High vol (>1.2): 1.15×\n• Aggressive:\n - Low vol: 0.7×\n - High vol: 1.3×")
table.cell(statusTbl, 2, row, adaptIcon, text_color=adaptCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Row 3: Signal Status
table.cell(statusTbl, 0, row, "Signal", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Current trend direction based on UT Bot\n• 🟢 BULLISH: Price above trailing stop\n• 🔴 BEARISH: Price below trailing stop")
table.cell(statusTbl, 1, row, sigState, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left)
table.cell(statusTbl, 2, row, sigEmoji, text_color=sigColor, text_size=tableSize, text_halign=text.align_center)
row += 1
// Statistics Row (always show - after Signal Status)
// Win rate = wins / total trades (not wins / (wins + losses))
float winRate = statTotalTrades > 0 ? (statWins / float(statTotalTrades)) * 100 : 0.0
float avgBars = statWinBars > 0 ? statTotalBars / float(statWinBars) : 0.0
color statCol = statTotalTrades == 0 ? txtWhite : winRate >= 50 ? colGreen : winRate >= 30 ? colYellow : colRed
string statEmoji = statTotalTrades == 0 ? "📊" : winRate >= 50 ? "📈" : winRate >= 30 ? "📊" : "📉"
string statsLabel = "Stats (" + str.tostring(i_statsLookback) + "d)"
string statsValue = statTotalTrades == 0 ? "No trades" : str.tostring(winRate, "0.0") + "% | " + str.tostring(avgBars, "0.0") + " bars"
table.cell(statusTbl, 0, row, statsLabel, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Trade Statistics (Last " + str.tostring(i_statsLookback) + " days)\n\nWin Rate: TP1 hit before SL\nAvg Bars: Average bars to reach TP1")
table.cell(statusTbl, 1, row, statsValue, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="STATISTICS (Last " + str.tostring(i_statsLookback) + " days):\n\nTotal Trades: " + str.tostring(statTotalTrades) + "\nWins (TP1): " + str.tostring(statWins) + "\nLosses (SL): " + str.tostring(statLosses) + "\n\nWin Rate: " + str.tostring(winRate, "0.0") + "%\nAvg Bars to TP1: " + str.tostring(avgBars, "0.0"))
table.cell(statusTbl, 2, row, statEmoji, text_color=statCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// MTF Row (always show - displays all higher timeframe trends)
string mtfEmojis = ""
string mtfTooltip = "HIGHER TIMEFRAME TRENDS:\n(EMA 9/21 crossover)\n\n"
// Only show TFs higher than current (currentTFMins defined in Layer 3)
if currentTFMins < 5
mtfEmojis := mtfEmojis + trendEmoji(htf5mTrend)
mtfTooltip := mtfTooltip + "5 m = " + trendEmoji(htf5mTrend) + " " + trendStr(htf5mTrend) + "\n"
if currentTFMins < 15
mtfEmojis := mtfEmojis + trendEmoji(htf15mTrend)
mtfTooltip := mtfTooltip + "15m = " + trendEmoji(htf15mTrend) + " " + trendStr(htf15mTrend) + "\n"
if currentTFMins < 30
mtfEmojis := mtfEmojis + trendEmoji(htf30mTrend)
mtfTooltip := mtfTooltip + "30m = " + trendEmoji(htf30mTrend) + " " + trendStr(htf30mTrend) + "\n"
if currentTFMins < 60
mtfEmojis := mtfEmojis + trendEmoji(htf1hTrend)
mtfTooltip := mtfTooltip + "1 H = " + trendEmoji(htf1hTrend) + " " + trendStr(htf1hTrend) + "\n"
if currentTFMins < 240
mtfEmojis := mtfEmojis + trendEmoji(htf4hTrend)
mtfTooltip := mtfTooltip + "4 H = " + trendEmoji(htf4hTrend) + " " + trendStr(htf4hTrend) + "\n"
if currentTFMins < 1440
mtfEmojis := mtfEmojis + trendEmoji(htfDTrend)
mtfTooltip := mtfTooltip + "Day = " + trendEmoji(htfDTrend) + " " + trendStr(htfDTrend) + "\n"
if currentTFMins < 10080
mtfEmojis := mtfEmojis + trendEmoji(htfWTrend)
mtfTooltip := mtfTooltip + "Wk = " + trendEmoji(htfWTrend) + " " + trendStr(htfWTrend) + "\n"
// Add filter status if MTF filter is enabled
bool mtfAligned = barbuy ? (htfTrend >= 0) : barsell ? (htfTrend <= 0) : true
string filterTF = i_mtfTimeframe == MTF_AUTO ? "Auto (" + htfTimeframe + ")" : i_mtfTimeframe
if i_useMTF
mtfTooltip := mtfTooltip + "\n───────────────\nFILTER: ON (" + filterTF + ")\nStatus: " + (mtfAligned ? "✅ Signal allowed" : "❌ Signal blocked")
else
mtfTooltip := mtfTooltip + "\n───────────────\nFILTER: OFF (display only)"
// MTF summary emoji: if filter ON show alignment, if OFF show selected HTF trend
string mtfSummaryEmoji = i_useMTF ? (mtfAligned ? "✅" : "❌") : trendEmoji(htfTrend)
color mtfSummaryCol = i_useMTF ? (mtfAligned ? colGreen : colRed) : (htfTrend == 1 ? colGreen : htfTrend == -1 ? colRed : txtWhite)
table.cell(statusTbl, 0, row, "Multi TF", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Multi-Timeframe Trends\n\nEmoji row (left to right):\nLow TF → High TF\n\n🟢 = BULL (uptrend)\n🔴 = BEAR (downtrend)\n⚪ = NEUTRAL\n\nFilter: " + (i_useMTF ? "ON" : "OFF"))
table.cell(statusTbl, 1, row, mtfEmojis, text_color=txtWhite, text_size=tableSize, text_halign=text.align_center,
tooltip=mtfTooltip)
table.cell(statusTbl, 2, row, mtfSummaryEmoji, text_color=mtfSummaryCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Divergence Row (always show)
string divStatus = lastDivType == "BULL" ? "BULL ↗" : lastDivType == "BEAR" ? "BEAR ↘" : "None"
string divBarsAgo = barsSinceDiv < 999 ? str.tostring(barsSinceDiv) + " bars ago" : "—"
color divCol = lastDivType == "BULL" ? colGreen : lastDivType == "BEAR" ? colRed : txtWhite
string divEmoji = lastDivType == "BULL" ? "🟢" : lastDivType == "BEAR" ? "🔴" : "⚪"
table.cell(statusTbl, 0, row, "Divergence", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="RSI Divergence Detection\n\n• BULL: Price ↓ + RSI ↑ (potential reversal up)\n• BEAR: Price ↑ + RSI ↓ (potential reversal down)\n\nRSI Period: " + str.tostring(i_divRsiLen) + "\nPivot Lookback: " + str.tostring(i_divLookback) + "\n\nFeeds the Confidence Score above (up to 4 pts, bonus if aligned with signal, penalty if opposed)")
table.cell(statusTbl, 1, row, divStatus + " | " + divBarsAgo, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Last Divergence: " + lastDivType + "\nBars ago: " + str.tostring(barsSinceDiv) + "\n\n• Bullish divergence often signals bottom\n• Bearish divergence often signals top")
table.cell(statusTbl, 2, row, divEmoji, text_color=divCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Prediction Row (if enabled)
if i_enablePrediction
int predSamplesAll = array.size(pred_all_durations)
int predBullCount = array.size(pred_bull_durations)
int predBearCount = array.size(pred_bear_durations)
// Check if current direction has enough samples (need 3 of SAME type)
int currentDirSamples = pred_is_bullish ? predBullCount : predBearCount
string currentDir = pred_is_bullish ? "BULL" : "BEAR"
bool canPredict = not na(pred_end)
string predPrefix = pred_using_fallback ? "~" : ""
string predStatus = not canPredict ? "Collecting " + currentDir + "..." : predPrefix + str.tostring(int(pred_avg)) + " avg / " + str.tostring(int(pred_end)) + " max bars"
string predEmoji = not canPredict ? "⏳" : pred_using_fallback ? "🔮❓" : "🔮"
color predCol = not canPredict ? colYellow : pred_using_fallback ? colYellow : colGreen
// Calculate success rate from last 10
int successes = 0
int total = math.min(10, math.min(array.size(pred_last10_actual), array.size(pred_last10_predicted)))
if total > 0
for i = 0 to total - 1
if array.get(pred_last10_actual, i) <= int(array.get(pred_last10_predicted, i))
successes += 1
float successRate = total > 0 ? (float(successes) / float(total)) * 100 : 0
string predTooltip = "🔮 Trend Duration Prediction\n\nMode: " + i_predMode +
"\nCurrent direction: " + currentDir +
"\n\n📊 Historical Data:" +
"\n• Bull trends: " + str.tostring(predBullCount) + (pred_is_bullish ? " ← need 3+" : "") +
"\n• Bear trends: " + str.tostring(predBearCount) + (not pred_is_bullish ? " ← need 3+" : "") +
"\n• Total: " + str.tostring(predSamplesAll) +
"\n\n" + (currentDirSamples >= 3 ? "✅ Ready to predict " + currentDir + " trends" :
pred_using_fallback ? "⚠️ Using combined bull+bear estimate (only " + str.tostring(currentDirSamples) + "/3 " + currentDir + " trends so far)" :
"⏳ Need " + str.tostring(3 - currentDirSamples) + " more " + currentDir + " trends") +
"\n\nSuccess Rate: " + str.tostring(successRate, "0.0") + "% (last 10)"
string avgStr = na(pred_avg) ? "—" : str.tostring(int(pred_avg))
string maxStr = na(pred_end) ? "—" : str.tostring(int(pred_end))
string cellTooltip = "Current " + currentDir + " trend: " + str.tostring(pred_current_bars) + " bars\n\n" +
"Predicted Avg: " + avgStr + " bars\n" +
"Predicted Max: " + maxStr + " bars" +
(pred_using_fallback ? "\n\n⚠️ Estimate based on combined bull+bear history (not enough " + currentDir + "-only samples yet)" : "")
if not canPredict
cellTooltip := "Collecting " + currentDir + " data...\n\n" +
"Need 3+ completed " + currentDir + " trends\n" +
"to predict " + currentDir + " duration.\n\n" +
"Current: " + str.tostring(currentDirSamples) + "/3 " + currentDir + " trends\n\n" +
"Current trend: " + str.tostring(pred_current_bars) + " bars"
table.cell(statusTbl, 0, row, "Prediction", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip=predTooltip)
table.cell(statusTbl, 1, row, predStatus, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip=cellTooltip)
table.cell(statusTbl, 2, row, predEmoji, text_color=predCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Risk Calculator Row (always show if enabled)
if i_showRisk
// Recalculate position size with current values
float tblEntry = not na(entryPx) ? entryPx : close
float tblSL = not na(slPrice) ? slPrice : close - slATR * 1.5
float tblRiskPerShare = math.abs(tblEntry - tblSL)
// Use risk mode (% of Account or Fixed Amount)
float tblRiskAmount = i_riskMode == "Fixed Amount" ? i_riskFixed : i_accountSize * (i_riskPct / 100)
float tblShares = tblRiskPerShare > 0 ? math.floor(tblRiskAmount / tblRiskPerShare) : 0
float tblPosValue = tblShares * tblEntry
float tblMaxLoss = tblShares * tblRiskPerShare
string sharesText = str.tostring(tblShares, "#") + " shares"
// Display text based on risk mode
string riskText = i_riskMode == "Fixed Amount" ?
"R: " + currSymbol + str.tostring(tblRiskAmount, "#") :
"R: " + str.tostring(i_riskPct, "0.0") + "%"
color riskCol = tblShares > 0 ? colGreen : colYellow
// Tooltip based on risk mode
string riskTooltip = i_riskMode == "Fixed Amount" ?
"Risk Calculator\n\nMode: Fixed Amount\nRisk: " + currSymbol + str.tostring(i_riskFixed, "#") + " per trade\n\nBased on current Entry/SL distance" :
"Risk Calculator\n\nMode: % of Account\nAccount: " + currSymbol + str.tostring(i_accountSize, "#,###") + "\nRisk: " + str.tostring(i_riskPct, "0.0") + "% = " + currSymbol + str.tostring(tblRiskAmount, "#") + "\n\nBased on current Entry/SL distance"
table.cell(statusTbl, 0, row, "Position", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip=riskTooltip)
table.cell(statusTbl, 1, row, sharesText + " | " + riskText, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="POSITION SIZING:\n\nEntry: " + str.tostring(tblEntry, "#.##") + "\nStop Loss: " + str.tostring(tblSL, "#.##") + "\nRisk/Share: $" + str.tostring(tblRiskPerShare, "#.##") + "\n\nShares: " + str.tostring(tblShares, "#") + "\nPosition Value: $" + str.tostring(tblPosValue, "#,###") + "\nMax Loss: " + currSymbol + str.tostring(tblMaxLoss, "#") + "\n\nCurrency: " + i_accountCurrency + "\nExchange Rate: " + str.tostring(exchangeRate, "0.00"))
table.cell(statusTbl, 2, row, "🧮", text_color=riskCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// ADX Regime (always show if Layer 1 enabled)
if i_enableLayer1
string adxTxt = "Strength: " + str.tostring(_adxVal, "0.0")
string adxEmoji = _regimePass ? "✅" : "❌"
color adxCol = _regimePass ? colGreen : colRed
table.cell(statusTbl, 0, row, "ADX Regime", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="ADX (Average Directional Index)\nMeasures trend strength in the market.\nHigh value = strong trend, Low value = sideways market.")
table.cell(statusTbl, 1, row, adxTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="ADX = " + adxTxt + " | Threshold = " + str.tostring(i_adxThreshold) + "\n• ✅ Trending: ADX ≥ " + str.tostring(i_adxThreshold) + "\n• ❌ Choppy: ADX < " + str.tostring(i_adxThreshold) + " (signals blocked)")
table.cell(statusTbl, 2, row, adxEmoji, text_color=adxCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Cooldown
if i_cooldownBars > 0
int barsSince = na(lastSignalBar) ? 999 : bar_index - lastSignalBar
string cdTxt = _cooldownPass ? str.tostring(barsSince) + " bars" : str.tostring(barsSince) + "/" + str.tostring(i_cooldownBars)
string cdEmoji = _cooldownPass ? "✅" : "❌"
color cdCol = _cooldownPass ? colGreen : colYellow
table.cell(statusTbl, 0, row, "Cooldown", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Cooldown Period\nPrevents rapid consecutive signals.\nRequires minimum bars between signals.")
table.cell(statusTbl, 1, row, "Period: " + cdTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Bars since last signal: " + str.tostring(barsSince) + "\nRequired: " + str.tostring(i_cooldownBars) + " bars\n• ✅ Cooldown passed\n• ❌ Still in cooldown (signals blocked)")
table.cell(statusTbl, 2, row, cdEmoji, text_color=cdCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// High Volatility Filter
if i_useVolatility
string volRatioTxt = str.tostring(volatilityRatio, "0.00") + "×"
string highVolEmoji = isHighVol ? "✅" : "❌"
color highVolCol = isHighVol ? colGreen : colRed
table.cell(statusTbl, 0, row, "High Vol Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="High Volatility Filter\nDetects when current ATR is above average.\nOnly allows signals in volatile market conditions.")
table.cell(statusTbl, 1, row, volRatioTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="ATR ratio: " + volRatioTxt + " average\nThreshold: " + str.tostring(i_volThresh, "0.0") + "×\n• ✅ High volatility (signals allowed)\n• ❌ Low volatility (signals blocked)")
table.cell(statusTbl, 2, row, highVolEmoji, text_color=highVolCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Swing Filter
if utUseSwingFilter
bool swingOk = barbuy ? _swingPassBuy : _swingPassSell
string swEmoji = swingOk ? "✅" : "❌"
color swCol = swingOk ? colGreen : colRed
table.cell(statusTbl, 0, row, "Swing Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Swing Structure Filter\nChecks price alignment with recent swing high/low.\nPrevents entries against local trend structure.")
table.cell(statusTbl, 1, row, str.tostring(swingLen) + " bars", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Swing structure check (Lookback: " + str.tostring(swingLen) + " bars)\n• ✅ Price aligned with swing\n• ❌ Price against swing structure")
table.cell(statusTbl, 2, row, swEmoji, text_color=swCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Full Candle Filter
if i_useFullCandle
bool fullCandleOk = barbuy ? fullCandleBuyOk : fullCandleSellOk
string fcEmoji = fullCandleOk ? "✅" : "❌"
color fcCol = fullCandleOk ? colGreen : colRed
string fcStatus = fullCandleOk ? "Clear" : "Straddling"
table.cell(statusTbl, 0, row, "Full Candle", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Full Candle Confirmation\nRequires entire candle above/below UT line.\n\n• BUY: Low must be above trailing stop\n• SELL: High must be below trailing stop\n\nPrevents signals on candles 'straddling' the line")
table.cell(statusTbl, 1, row, fcStatus, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Full Candle Check:\n• Clear: Entire candle is on one side of UT line\n• Straddling: Candle crosses the UT line\n\n" + (barbuy ? "BUY requires: Low > UT Line" : "SELL requires: High < UT Line"))
table.cell(statusTbl, 2, row, fcEmoji, text_color=fcCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Volume Filter
if i_useVolFilter
string volTxt = str.tostring(_volRatio, "0.00") + "×"
string volEmoji = _volumePass ? "✅" : "❌"
color volCol = _volumePass ? colGreen : colRed
table.cell(statusTbl, 0, row, "Volume Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Volume Filter\nRequires above-average volume to confirm signals.\nHigher volume = stronger confirmation of move.")
table.cell(statusTbl, 1, row, volTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Volume ratio: " + volTxt + " average\nThreshold: " + str.tostring(i_volThreshold, "0.0") + "×\n• ✅ Volume above threshold\n• ❌ Low volume (signal blocked)")
table.cell(statusTbl, 2, row, volEmoji, text_color=volCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// RSI Filter
if i_useRsiFilter
string rsiTxt = str.tostring(_rsiVal, "0.0")
bool rsiOk = barbuy ? _rsiPassBuy : _rsiPassSell
string rsiEmoji = rsiOk ? "✅" : "❌"
color rsiCol = rsiOk ? colGreen : colRed
table.cell(statusTbl, 0, row, "RSI Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="RSI (Relative Strength Index)\nMeasures momentum and identifies overbought/oversold.\nBlocks buys in OB zone and sells in OS zone.")
table.cell(statusTbl, 1, row, rsiTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="RSI = " + rsiTxt + "\nOverbought: " + str.tostring(i_rsiOB) + " | Oversold: " + str.tostring(i_rsiOS) + "\n• ✅ RSI confirms direction\n• ❌ RSI blocks signal (OB/OS zone)")
table.cell(statusTbl, 2, row, rsiEmoji, text_color=rsiCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Hull MA Filter
if i_useHullConfirm
bool hullOk = barbuy ? checkHullFilter(true) : checkHullFilter(false)
string hullEmoji = hullOk ? "✅" : "❌"
color hullCol = hullOk ? colGreen : colRed
table.cell(statusTbl, 0, row, "Hull MA Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Hull Moving Average\nFast moving average with reduced lag.\nRequires price above/below Hull MA to confirm direction.")
table.cell(statusTbl, 1, row, str.tostring(i_hullPeriod) + " period", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Hull MA Confirmation (Period: " + str.tostring(i_hullPeriod) + ")\n• ✅ Price aligned with Hull MA\n• ❌ Price against Hull MA (signal blocked)")
table.cell(statusTbl, 2, row, hullEmoji, text_color=hullCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// SuperTrend Filter (Engine with Strength)
if i_useSuperTrend
bool stOk = barbuy ? stBullish : stBearish
string stEmoji = stOk ? "✅" : "❌"
color stCol = stOk ? colGreen : colRed
string stTrend = stBullish ? "BULL 🟢" : "BEAR 🔴"
string stStrengthTxt = str.tostring(stStrength, "0") + "%"
table.cell(statusTbl, 0, row, "SuperTrend", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="SuperTrend Engine\nClassic trend-following indicator with strength scoring.\n\nStrength = Distance from ST line ÷ ATR × 25\nHigher = Stronger trend confirmation")
table.cell(statusTbl, 1, row, stTrend + " | " + stStrengthTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="SuperTrend: " + (stBullish ? "BULLISH" : "BEARISH") + "\nStrength: " + stStrengthTxt + "\n\nATR Period: " + str.tostring(i_stAtrPeriod) + "\nMultiplier: " + str.tostring(i_stMult, "0.0") + "×\n\n• ✅ Signal aligned with SuperTrend\n• ❌ Signal against SuperTrend (blocked)")
table.cell(statusTbl, 2, row, stEmoji, text_color=stCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// Structure Break Engine (always show - part of consensus)
bool sbOk = barbuy ? sbBullish : sbBearish
string sbEmoji = sbOk ? "✅" : "❌"
color sbCol = sbOk ? colGreen : colRed
string sbTrend = sbBullishBreak ? "BREAK ↑" : sbBearishBreak ? "BREAK ↓" : sbBullish ? "BULL" : "BEAR"
string sbStrengthTxt = str.tostring(sbStrength, "0") + "%"
table.cell(statusTbl, 0, row, "Structure", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Structure Analysis\n\nBREAK ↑ = Closed above swing high\nBREAK ↓ = Closed below swing low\nBULL = Above midpoint of range\nBEAR = Below midpoint of range\n\nStrength = Position within range (0-100%)")
table.cell(statusTbl, 1, row, sbTrend + " | " + sbStrengthTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Structure: " + sbTrend + "\nStrength: " + sbStrengthTxt + "\n\nSwing High: " + str.tostring(sbSwingHigh, "#.##") + "\nSwing Low: " + str.tostring(sbSwingLow, "#.##") + "\nMidpoint: " + str.tostring(sbMid, "#.##") + "\nLookback: " + str.tostring(swingLen) + " bars\n\n• ✅ Structure supports direction\n• ❌ Structure opposes direction")
table.cell(statusTbl, 2, row, sbEmoji, text_color=sbCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// S/R Zones (persistent pivot-clustered zones, separate from the rolling Structure line above)
if utShowZones
string zoneStatus = nearResistance and nearSupport ? "BOTH ⚠️" : nearResistance ? "RESISTANCE (" + str.tostring(nearResTouches) + "×)" : nearSupport ? "SUPPORT (" + str.tostring(nearSupTouches) + "×)" : "Clear"
string zoneEmoji = nearResistance ? "🔴" : nearSupport ? "🟢" : "⚪"
color zoneCol = nearResistance ? colRed : nearSupport ? colGreen : txtWhite
string zoneTooltip = "Support/Resistance Zones\n\nClusters significant pivot highs/lows (pivot strength: " + str.tostring(i_zonePivotLen) + " bars each side) into persistent zones. Only the nearest resistance ABOVE and nearest support BELOW price are drawn.\n\nZone width: ±" + str.tostring(zoneTol, "#.##") + " (" + str.tostring(i_zoneWidthMult, "0.0") + "× ATR)\nResistance zones tracked: " + str.tostring(array.size(zoneResPrice)) + "\nSupport zones tracked: " + str.tostring(array.size(zoneSupPrice)) + "\n\n" + (i_useZoneFilter ? "FILTER: ON - blocks BUY inside resistance / SELL inside support" : "FILTER: OFF (display only)")
table.cell(statusTbl, 0, row, "S/R Zones", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip=zoneTooltip)
table.cell(statusTbl, 1, row, zoneStatus, text_color=zoneCol, text_size=tableSize, text_halign=text.align_left,
tooltip=zoneTooltip)
table.cell(statusTbl, 2, row, zoneEmoji, text_color=zoneCol, text_size=tableSize, text_halign=text.align_center)
row += 1
// 2-Bar Confirm
if utUseDelay
table.cell(statusTbl, 0, row, "2-Bar Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="2-Bar Confirmation\nRequires 2 consecutive bars in same direction.\nAdds slight lag but reduces false signals.")
table.cell(statusTbl, 1, row, "ON", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="2-Bar Confirmation active\n• Requires 2 consecutive bars to confirm trend\n• Adds lag but reduces false signals")
table.cell(statusTbl, 2, row, "⚡", text_color=colYellow, text_size=tableSize, text_halign=text.align_center)
row += 1
// % Filter
if utUsePctFilter
float pctChg = math.abs(close - close[1]) / close[1] * 100
string pctTxt = str.tostring(pctChg, "0.00") + "%"
bool pctOk = pctChg > thr
string pctEmoji = pctOk ? "✅" : "❌"
color pctCol = pctOk ? colGreen : colRed
table.cell(statusTbl, 0, row, "% Change Filter", text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Percent Change Filter\nRequires minimum price change between bars.\nFilters out insignificant small movements.")
table.cell(statusTbl, 1, row, pctTxt, text_color=txtWhite, text_size=tableSize, text_halign=text.align_left,
tooltip="Bar change: " + pctTxt + "\nThreshold: " + str.tostring(thr, "0.00") + "%\n• ✅ Sufficient movement\n• ❌ Too small (signal blocked)")
table.cell(statusTbl, 2, row, pctEmoji, text_color=pctCol, text_size=tableSize, text_halign=text.align_center)
row += 1
//END 16/07/2026 - Fix: SL no longer marked 'touched' on the entry bar itself (guard bar_index > slStartBar) - that bar's range predates entry