Indicator · Pine Script v6 · On the chart

GOD Conviction Engine

by Oluwacutyp· code credited to OMNI-GOD ∞ [UNIFIED MASTER SUITE — Gold & BTC Adaptive]

A large all-in-one overlay aimed at gold and Bitcoin charts.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code3,696
Settings40inputs
LicenceUnlicense
Last update2 weeks ago10 Oct 2026

Add It to Your TradingView Chart

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

Source Code

3,696 lines of Pine Script v6 from OMNI-GOD-UNIFIED.pine (15).txt, published under Unlicense. Keep the author's credit and licence lines when you reuse it.

OMNI-GOD-UNIFIED.pine (15).txt
// This source code is subject to the terms of the Mozilla Public License 2.0
// © OMNI-GOD ∞ [UNIFIED MASTER SUITE — Gold & BTC Adaptive]
// Merged from: OMNI-TITAN v10 GOD EDITION + OMNI-GOD3 HYBRID v9 QUANTUM + kim-TITAN v9 ASCENSION
//
// ── BUGS FIXED IN THIS MERGE (vs the 3 originals) ─────────────────────────────
// 1. `abs()` / `round()` are NOT Pine functions → replaced with math.abs / math.round
// 2. Function defined INSIDE an if-block (drawFieldForZone) → moved to global scope
// 3. `lookahead=barmerge.lookahead_on` removed (Pine v6 removed that parameter;
//    the non-repaint pattern is now request.security(sym, "D", high[1]))
// 4. NFP detection used dayofweek == 6 (Saturday) → fixed to 5 (Friday)
// 5. Elastic EMA length built with float math fed to ta.ema(int) → pure int bands
// 6. Breaker arrays grew unbounded → capped at 8
// 7. Radar (top-right) and Analysis table (top-right) overlapped → Analysis moved top-left
// 8. float→int implicit conversions (color transparency, lengths) eliminated
// 9. TP2+SL same-bar double exit race (if/if) → if/else-if exit ladder
// 10. Dark pool levels now persist (var) instead of resetting each bar
// ═══════════════════════════════════════════════════════════════════════════════

//@version=6
indicator("GOD Conviction Engine (automatic)", shorttitle="GOD CE", overlay=true, max_labels_count=500, max_boxes_count=500, max_lines_count=500, max_bars_back=500)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║                    SECTION 1: ENUMS & INPUTS                      ║
// ╚═══════════════════════════════════════════════════════════════════╝

enum RiskProfile
    Aggressive = "Aggressive (65% Confluence)"
    Standard   = "Standard (70% Confluence)"
    Strict     = "Strict (80% Confluence)"

string GRP_UI     = "━━━ VISUAL TOGGLES ━━━"
string GRP_CORE   = "━━━ CORE ENGINE & BASELINE ━━━"
string GRP_MACRO  = "━━━ MACRO CORRELATION (DXY / NDX) ━━━"
string GRP_ELAST  = "━━━ ELASTIC EMA & REGIME ━━━"
string GRP_LIQ    = "━━━ LIQUIDITY, ASIAN & SESSIONS ━━━"
string GRP_SMC    = "━━━ SMC QUANTUM ZONES & TIERS ━━━"
string GRP_IND    = "━━━ MACD / RSI / OSCILLATORS ━━━"
string GRP_ADV    = "━━━ WYCKOFF / DARK POOL / ASOC ━━━"
string GRP_RISK   = "━━━ RISK, SIZING & HYDRA TRAIL ━━━"
string GRP_ALERT  = "━━━ ALERTS & WEBHOOKS ━━━"
string GRP_LAYOUT = "━━━ LAYOUT (TABLE PLACEMENT) ━━━"
string GRP_PATTERN = "━━━ PATTERN — H&S + ICHIMOKU ━━━"
string GRP_ROUTE   = "━━━ STRATEGY ROUTER ━━━"

// ── Visual Toggles ──
bool showEmas      = input.bool(true,  "Show Trend EMAs",                   group=GRP_UI)
bool showHma       = input.bool(true,  "Show HMA 50 Baseline",              group=GRP_UI)
bool showElastic   = input.bool(true,  "Show Elastic EMA",                  group=GRP_UI)
bool showOB        = input.bool(true,  "Show Order Blocks (OB)",            group=GRP_UI)
bool showFVG       = input.bool(true,  "Show Fair Value Gaps (FVG)",        group=GRP_UI)
bool showBreakers  = input.bool(true,  "Show Breaker Blocks (RBS/SBR)",     group=GRP_UI)
bool showSweeps    = input.bool(true,  "Show Liquidity Sweeps",             group=GRP_UI)
bool showAsian     = input.bool(true,  "Show Asian Range Box (00-07 UTC)",  group=GRP_UI)
bool showLevels    = input.bool(true,  "Show Key OHLC & Pivot Lines",       group=GRP_UI)
bool showPD        = input.bool(true,  "Show PD Array Bands",               group=GRP_UI)
bool showVwap      = input.bool(true,  "Show VWAP Line",                    group=GRP_UI)
bool showMktStr    = input.bool(true,  "Show Structure (BOS/CHoCH/Swings)", group=GRP_UI)
bool showQuantum   = input.bool(true,  "Show Quantum Field Gravity",        group=GRP_UI)
bool showDarkPool  = input.bool(true,  "Show Dark Pool Levels",             group=GRP_UI)
bool showLiqHeat   = input.bool(true,  "Show Liquidity Heat Projection",    group=GRP_UI)
bool showHeatmap   = input.bool(true,  "Show Confluence Heatmap",           group=GRP_UI)
bool showNarrative = input.bool(true,  "Show Narrative Voice Label",        group=GRP_UI)
bool showHUD       = input.bool(true,  "Show Execution HUD (position: Layout group)", group=GRP_UI)
bool showRadar     = input.bool(true,  "Show Confluence Radar (position: Layout group)", group=GRP_UI)
bool showAnalysis  = input.bool(true,  "Show Analysis Table (position: Layout group)", group=GRP_UI)
bool showRegime    = input.bool(true,  "Show Regime Label On Chart",               group=GRP_UI)
bool showKillzones = input.bool(false, "Show Session Backgrounds",          group=GRP_UI)

// ── Core Engine ──
int  fastLen     = input.int(9,   "Fast EMA",                group=GRP_CORE, minval=1)
int  medLen      = input.int(21,  "Medium EMA",              group=GRP_CORE, minval=1)
int  slowLen     = input.int(50,  "Slow EMA",                group=GRP_CORE, minval=1)
int  trendLen    = input.int(200, "Trend EMA",               group=GRP_CORE, minval=1)
int  hmaLen      = input.int(50,  "Non-Lag Baseline (HMA)",  group=GRP_CORE, minval=5)
int  atrLen      = input.int(14,  "ATR Length",              group=GRP_CORE, minval=1)
int  adxLen      = input.int(14,  "ADX Length",              group=GRP_CORE, minval=1)
int  adxMin      = input.int(18,  "ADX Min Strength",        group=GRP_CORE, minval=5)
int  zLen        = input.int(20,  "Z-Score Window",          group=GRP_CORE, minval=5)
int  volLen      = input.int(20,  "Volume SMA Length",       group=GRP_CORE, minval=5)
bool useVolImb   = input.bool(true,  "Volume Imbalance (2.0x SMA)",        group=GRP_CORE)
bool useVolConf  = input.bool(false, "Require Volume Confirmation",        group=GRP_CORE)
bool useKillzone = input.bool(false, "Killzone Session Filter (London+NY)",group=GRP_CORE)
bool useBtcWeekend = input.bool(true, "BTC Weekend Thin-Liquidity Filter", group=GRP_CORE, tooltip="Sat/Sun: +3% stricter thresholds, 15% wider SL buffers (BTC only)")

// ── Pattern recognition — Head & Shoulders + Ichimoku ──
bool showIchimoku = input.bool(true, "Ichimoku: Show Clouds (Daily + TF)", group=GRP_PATTERN, tooltip="Classic 9/26/52. Cloud = the Kumo surrounding the current bar")
bool useIchimoku  = input.bool(true, "Ichimoku: Score Cloud Context", group=GRP_PATTERN)
bool showHs       = input.bool(true, "H&S: Show Pattern Labels & Neckline", group=GRP_PATTERN)
bool useHs        = input.bool(true, "H&S: Score Neckline Breaks", group=GRP_PATTERN)
int  hsAtrTol     = input.int(2, "H&S Shoulder Tolerance (×ATR)", group=GRP_PATTERN, minval=1, maxval=5)

// ── Macro Correlation ──
bool   useMacro    = input.bool(true,   "Enable Macro Engine",         group=GRP_MACRO)
bool   autoMacro   = input.bool(true,   "Auto-Detect Macro Asset",     group=GRP_MACRO, tooltip="DXY for Gold (inverse), NDX for BTC (direct)")
string manualMacro = input.string("TVC:DXY", "Manual Macro Symbol",   group=GRP_MACRO)

// ── Elastic Trends & Regime ──
bool  useElastic   = input.bool(true,  "Enable Elastic EMA (Hurst-driven)", group=GRP_ELAST)
int   minElastic   = input.int(5,   "Min Elastic Length", group=GRP_ELAST, minval=3, maxval=20)
int   maxElastic   = input.int(55,  "Max Elastic Length", group=GRP_ELAST, minval=10, maxval=100)
float regTrendTh   = input.float(0.20, "Trend Slope Threshold", group=GRP_ELAST, step=0.05, minval=0.05)
int   regimeLen    = input.int(20, "Regime Lookback (BB/Slope)", group=GRP_ELAST, minval=10, maxval=50)

// ── Liquidity, Asian & Sessions ──
int  swingLen   = input.int(10, "Swing Pivot Length", group=GRP_LIQ, minval=3, maxval=30)
float eqTolerance = input.float(0.30, "EQH/EQL ATR Tolerance", group=GRP_LIQ, step=0.05, minval=0.05)
bool  useWeekOpen = input.bool(true, "Use Weekly Open Level", group=GRP_LIQ)

// ── SMC Quantum Zones & Tiers ──
int   maxOBs       = input.int(5,   "Max Active Order Blocks",   group=GRP_SMC, minval=1, maxval=10)
int   maxFVGs      = input.int(5,   "Max Active FVGs",           group=GRP_SMC, minval=1, maxval=10)
int   fvgMinPct    = input.int(20,  "FVG Min Size (% ATR)",      group=GRP_SMC, minval=5, maxval=100)
int   zoneMaxAge   = input.int(120, "Zone Max Age (bars)",       group=GRP_SMC, minval=20, maxval=500)
float qGravity     = input.float(0.50, "Quantum Gravity Steepness", group=GRP_SMC, step=0.1, minval=0.1, maxval=2.0)
float qCorePct     = input.float(0.35, "Zone Core Probability (0-1)", group=GRP_SMC, step=0.05, minval=0.1, maxval=0.9, tooltip="Central band of a zone with highest reaction probability. Core taps score higher than shadow taps.")
bool  useLiqHier   = input.bool(true, "Enable Liquidity Tiers",       group=GRP_SMC)
int   maxLiqTier   = input.int(4, "Max Tier Level", group=GRP_SMC, minval=1, maxval=4)
bool  showTierTags = input.bool(true, "Show Tier Badges on Zones",    group=GRP_SMC)

// ── Oscillators ──
int macdFast = input.int(12, "MACD Fast",   group=GRP_IND, minval=1)
int macdSlow = input.int(26, "MACD Slow",   group=GRP_IND, minval=1)
int macdSig  = input.int(9,  "MACD Signal", group=GRP_IND, minval=1)
int rsiLen   = input.int(14, "RSI Length",     group=GRP_IND, minval=2)
int rsiOBLvl = input.int(70, "RSI Overbought", group=GRP_IND, minval=55, maxval=90)
int rsiOSLvl = input.int(30, "RSI Oversold",   group=GRP_IND, minval=10, maxval=45)

// ── Wyckoff / Dark Pool / ASOC ──
bool useWyckoff   = input.bool(true, "Enable Wyckoff Phase Detector",  group=GRP_ADV)
bool useVolDiv    = input.bool(true, "Enable Volume Divergence",       group=GRP_ADV)
bool useDarkPool  = input.bool(true, "Calculate Dark Pool Levels",     group=GRP_ADV)
bool useASOC      = input.bool(true, "Enable ASOC Self-Optimization",  group=GRP_ADV)
int  asocWindow   = input.int(20, "ASOC Sample Window (Trades)", group=GRP_ADV, minval=5, maxval=50)
float optStep     = input.float(1.0, "ASOC Adjust Step (%)", group=GRP_ADV, minval=0.1, step=0.1, tooltip="Hybrid self-opt: per completed sample, WR<45% tightens threshold by this, WR>70% loosens it")
bool  useFactorGate = input.bool(true, "Require Min Distinct Factors", group=GRP_ADV, tooltip="Blocks entries where one dominant factor inflates the score")
int   minFactors    = input.int(4, "Min Distinct Factors to Enter", group=GRP_ADV, minval=2, maxval=10)

// ── Risk & Hydra Execution ──
RiskProfile userRisk   = input.enum(RiskProfile.Standard, "Execution Strictness", group=GRP_RISK)
float slMult           = input.float(1.0, "SL ATR Buffer Multiplier",  group=GRP_RISK, minval=0.1, step=0.1)
float trailATR         = input.float(1.2, "Hydra Trail ATR Distance",  group=GRP_RISK, minval=0.2, step=0.1, tooltip="Phase-1 trail: how many ATR below the bar's close the stop sits once a trade is +0.3R. 0.6 = old very-tight behavior, 1.2–1.5 = one bar of room")
float tp1RR            = input.float(1.5, "TP1 Risk:Reward",           group=GRP_RISK, minval=0.5, step=0.25)
float tp2RR            = input.float(3.0, "TP2 Risk:Reward",           group=GRP_RISK, minval=1.0, step=0.5)
bool  usePVS           = input.bool(true, "PVS Mode: TP2 partial, TP3 runner exit", group=GRP_RISK)
float accBalance       = input.float(10000.0, "Account Balance ($)",   group=GRP_RISK, minval=100.0)
float riskPct          = input.float(1.0, "Risk % Per Trade",          group=GRP_RISK, minval=0.1, maxval=10.0, step=0.1)
float tickValue        = input.float(1.0, "Tick/Point Value",          group=GRP_RISK, minval=0.0, step=0.1)
bool  useHydra         = input.bool(true, "Enable Hydra 4-Phase Trailing Stop", group=GRP_RISK)
bool  moveBEat50       = input.bool(true, "Move to BE at 0.5R Gain",   group=GRP_RISK)
bool  useProfitLock    = input.bool(true, "Profit-Lock Trail (0.5R retrace)", group=GRP_RISK, tooltip="After +1.5R, locks profit if price retraces 0.5R from the peak")
float beOffsetR        = input.float(0.10, "TP1 BE Lock Offset (R)", group=GRP_RISK, step=0.05, minval=0.0, tooltip="At TP1 the stop moves to entry + this many R (0 = plain BE)")
bool  useSpikeShield   = input.bool(true, "Spike Shield (no entries on vol spikes)", group=GRP_RISK)

// ── Strategy Router (dynamic playbook selection) ──
bool  useRouter  = input.bool(true, "Enable Strategy Router", group=GRP_ROUTE, tooltip="ON: router picks the playbook per market state — MOMENTUM / MEAN-REV / POST-SPIKE / BREAKOUT / FLAT. OFF: legacy single-strategy score mode")
int   modeDwell  = input.int(2, "Mode switch dwell (bars)", group=GRP_ROUTE, minval=1, maxval=10, tooltip="Candidate mode must persist this many confirmed bars before the router switches")
bool  alertOnMode = input.bool(false, "Alert on strategy-mode change", group=GRP_ROUTE)
bool  showGeoLines = input.bool(false, "Show geometry lines (support/resistance trendlines)", group=GRP_ROUTE, tooltip="Optional overlay of the detected trendlines. Off by default — the geometry drives decisions, not decoration")
float spikeAtrMult     = input.float(2.2, "Spike Shield ATR Multiple", group=GRP_RISK, step=0.1, minval=1.0)

// ── Alerts ──
bool alertOnSignal = input.bool(true, "Alert: New Setup Signal",  group=GRP_ALERT)
bool alertOnEntry  = input.bool(true, "Alert: Order Execution",   group=GRP_ALERT)
bool alertOnZone   = input.bool(true, "Alert: Zone Entry/Tap",    group=GRP_ALERT)
bool alertOnExit   = input.bool(true, "Alert: TP / BE / SL Exit", group=GRP_ALERT)

// ── Layout — dynamic table placement ──
string hudPos      = input.string("bottom_right", "Execution HUD Position",    options=["bottom_right", "bottom_left", "top_right", "top_left"], group=GRP_LAYOUT)
string radarPos    = input.string("top_right",    "Confluence Radar Position", options=["bottom_right", "bottom_left", "top_right", "top_left"], group=GRP_LAYOUT)
string analysisPos = input.string("top_left",     "Analysis Table Position",   options=["bottom_right", "bottom_left", "top_right", "top_left"], group=GRP_LAYOUT)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║        SECTION 2: SYMBOL DETECTION & ADAPTIVE PARAMETERS         ║
// ╚═══════════════════════════════════════════════════════════════════╝

string tickerID = str.upper(syminfo.ticker)
bool isGold = str.contains(tickerID, "XAU") or str.contains(tickerID, "GOLD")
bool isBTC  = str.contains(tickerID, "BTC")

string symbolMode = isGold ? "GOLD 🥇" : isBTC ? "BTC ₿" : "STANDARD"

float symImpulse = isGold ? 0.85 : isBTC ? 1.60 : 1.0
float symFvgScl  = isGold ? 0.75 : isBTC ? 1.50 : 1.0
float symSlBuf   = isGold ? 1.00 : isBTC ? 1.40 : 1.0
float symOhlcW   = isGold ? 2.00 : isBTC ? 0.60 : 1.0
float symSweepW  = isGold ? 1.00 : isBTC ? 1.40 : 1.0

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 3: SESSION / KILLZONE & GOLD TIME AWARENESS          ║
// ╚═══════════════════════════════════════════════════════════════════╝

int utcHour     = hour(time, "UTC")
int utcDay      = dayofweek(time, "UTC")
int utcDayMonth = dayofmonth(time, "UTC")

bool inAsian     = utcHour >= 0  and utcHour < 7
bool inLondon    = utcHour >= 7  and utcHour < 12
bool inLondonKZ  = utcHour >= 7  and utcHour < 10
bool inNY        = utcHour >= 12 and utcHour < 17
bool inNYKZ      = utcHour >= 12 and utcHour < 15
bool inNYClose   = utcHour >= 17 and utcHour < 20
bool inKillzone  = inLondonKZ or inNYKZ
bool sessionOk   = not useKillzone or not timeframe.isintraday or inKillzone

// London Fix window (15:00 UTC) — manipulation warning for Gold
bool inLondonFix = isGold and utcHour == 15
// NFP: first Friday of the month (dayofweek: 0=Sunday … 5=Friday)
bool isNfpDay    = isGold and utcDay == 5 and utcDayMonth <= 7
bool cautionMode = isNfpDay or inLondonFix
// BTC trades 24/7 — Saturday/Sunday liquidity is thin: wider stops, stricter entries
bool btcWeekend = isBTC and (utcDay == 0 or utcDay == 6)

bgcolor(showKillzones and timeframe.isintraday and inAsian   ? color.new(color.purple, 94) : na, title="Asian Session")
bgcolor(showKillzones and timeframe.isintraday and inLondon  ? color.new(color.blue,   92) : na, title="London Session")
bgcolor(showKillzones and timeframe.isintraday and inNY      ? color.new(color.orange, 92) : na, title="NY Session")
bgcolor(showKillzones and timeframe.isintraday and inNYClose ? color.new(color.gray,   94) : na, title="NY Close")

// ╔═══════════════════════════════════════════════════════════════════╗
// ║         SECTION 4: TIMEFRAME INTELLIGENCE & ADAPTIVE HTF         ║
// ╚═══════════════════════════════════════════════════════════════════╝

int tfMult = timeframe.multiplier
bool isScalp    = timeframe.isintraday and tfMult <= 5
bool isIntraday = timeframe.isintraday and tfMult > 5 and tfMult <= 60

string tfLabel  = isScalp ? "SCALP" : isIntraday ? "INTRADAY" : "SWING"
string htf      = isScalp ? "60" : isIntraday ? "240" : "W"
float  regAtrMult = isScalp ? 0.8 : isIntraday ? 1.2 : 2.0

string biasTF1 = isScalp ? "15"  : isIntraday ? "60"  : "240"
string biasTF2 = isScalp ? "60"  : isIntraday ? "240" : "1D"
string biasTF3 = isScalp ? "240" : isIntraday ? "1D"  : "1W"

string tf1Label = isScalp ? "15m" : isIntraday ? "1H"    : "4H"
string tf2Label = isScalp ? "1H"  : isIntraday ? "4H"    : "Daily"
string tf3Label = isScalp ? "4H"  : isIntraday ? "Daily" : "Weekly"

// ╔═══════════════════════════════════════════════════════════════════╗
// ║              SECTION 5: PURE HELPER FUNCTIONS                    ║
// ╚═══════════════════════════════════════════════════════════════════╝

// Sigmoid activation for quantum field gravity
f_sigmoid(float x, float steepness) =>
    1.0 / (1.0 + math.exp(-steepness * x))

// Map a layout input to a Pine position constant
f_pos(string p) =>
    p == "top_left" ? position.top_left : p == "top_right" ? position.top_right : p == "bottom_left" ? position.bottom_left : position.bottom_right

// Hurst-driven elastic EMA length — pure INT math (ta.ema needs int length)
// Trending (high Hurst) → short, responsive EMA · Choppy (low Hurst) → long, smooth EMA
f_elasticLen(float hVal) =>
    int el = useElastic ? (hVal > 0.66 ? minElastic : hVal > 0.34 ? (minElastic + maxElastic) / 2 : maxElastic) : fastLen
    el

// Multi-TF bias payload
f_biasData() =>
    float bE50  = ta.ema(close, 50)
    float bE200 = ta.ema(close, 200)
    bool  bBull = close > bE50 and bE50 > bE200
    bool  bBear = close < bE50 and bE50 < bE200
    float bDist = (close - bE50) / math.max(bE50, 0.001) * 100.0
    [bBull, bBear, bDist]

// HTF impulse / order block payload
f_obData() =>
    float oAtr  = ta.rma(ta.tr, atrLen)
    bool  oBull = (close - open) > oAtr * 1.5 and close > high[1] and close[1] < open[1]
    bool  oBear = (open - close) > oAtr * 1.5 and close < low[1] and close[1] > open[1]
    [oBull, oBear, high[1], low[1]]

// Macro payload
f_macroData() =>
    float mC    = close
    float mE50  = ta.ema(close, 50)
    float mE200 = ta.ema(close, 200)
    [mC, mE50, mE200]

// Radar row helper
f_radar_row(table tbl, int row, string factor, string status, bool isPass) =>
    table.cell(tbl, 0, row, factor,  bgcolor=color.rgb(10, 10, 10), text_color=color.gray,  text_size=size.small)
    table.cell(tbl, 1, row, status,  bgcolor=isPass ? color.new(color.green, 75) : color.new(color.red, 75), text_color=color.white, text_size=size.small)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║        SECTION 6: MACRO CORRELATION ENGINE (DXY / NDX)           ║
// ═══════════════════════════════════════════════════════════════════╝

string activeMacroSym = autoMacro ? (isGold ? "TVC:DXY" : isBTC ? "NASDAQ:NDX" : manualMacro) : manualMacro
string macroName = str.contains(activeMacroSym, "DXY") ? "DXY" : str.contains(activeMacroSym, "NDX") ? "NDX" : "MACRO"

[macroClose, macroEma50, macroEma200] = request.security(activeMacroSym, timeframe.period, f_macroData(), ignore_invalid_symbol=true)
bool macroValid = useMacro and not na(macroClose) and not na(macroEma50)

// Inverse for Gold: DXY DOWN = GOLD BULLISH. Direct for BTC: NDX UP = BTC BULLISH.
bool isInverseMacro = isGold or (not isBTC and str.contains(activeMacroSym, "DXY"))
bool macroBullish = macroValid and (isInverseMacro ? macroClose < macroEma50 and macroEma50 < macroEma200 : macroClose > macroEma50 and macroEma50 > macroEma200)
bool macroBearish = macroValid and (isInverseMacro ? macroClose > macroEma50 and macroEma50 > macroEma200 : macroClose < macroEma50 and macroEma50 < macroEma200)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 7: CORE ENGINE, VOLATILITY & 4-STATE REGIME          ║
// ╚═══════════════════════════════════════════════════════════════════╝

float atr    = ta.rma(ta.tr, atrLen)
float atrAvg = ta.sma(atr, 20)
float atrRatio = atr / math.max(atrAvg, 0.001)
bool lowVol  = atrRatio < 0.70
bool highVol = atrRatio > 1.50

float emaF   = ta.ema(close, fastLen)
float emaM   = ta.ema(close, medLen)
float emaS   = ta.ema(close, slowLen)
float emaT   = ta.ema(close, trendLen)
float hmaBase = ta.hma(close, hmaLen)

[_diP, _diM, adxLine] = ta.dmi(adxLen, adxLen)
bool adxOk = adxLine > adxMin

float volSma       = ta.sma(volume, volLen)
bool  volImbalance = useVolImb ? volume > volSma * 2.0 : volume > volSma * 1.2
bool  volStrong    = volume > volSma * 1.2
bool  volOk        = not useVolConf or volStrong

float zBase  = ta.sma(close, zLen)
float zDev   = ta.stdev(close, zLen)
float zScore = zDev > 0 ? (close - zBase) / zDev : 0.0

bool bullStack = emaF > emaM and emaM > emaS and close > emaT and close > hmaBase
bool bearStack = emaF < emaM and emaM < emaS and close < emaT and close < hmaBase

// ── Regime detection (Hurst persistence + BB width + ADX + slope) ──
int regimeSlopeLen = math.max(regimeLen / 2, 5)
float bbMid   = ta.sma(close, regimeLen)
float bbDev   = ta.stdev(close, regimeLen)
float bbWidth = 2 * bbDev / math.max(bbMid, 0.001)

float hurst = math.log(ta.atr(10) / math.max(ta.atr(20), 0.0000001)) / math.log(2)
float hurstNorm = math.max(0.0, math.min(1.0, (hurst + 1.0) / 2.0))

float slope = ta.linreg(close, regimeSlopeLen, 0) - ta.linreg(close, regimeSlopeLen, 1)
float slopeNorm = slope / math.max(atr, 0.001)

bool highADX    = adxLine > 25
bool lowADX     = adxLine < 18
bool wideBB     = bbWidth > ta.sma(bbWidth, regimeLen) * 1.3
bool narrowBB   = bbWidth < ta.sma(bbWidth, regimeLen) * 0.7
bool priceAbove = close > emaS
bool priceBelow = close < emaS

string regimeStr = "◆ RANGING"
color  regimeCol = color.new(color.yellow, 50)
if highADX and priceAbove and slopeNorm > regTrendTh
    regimeStr := "▲ TRENDING BULL"
    regimeCol := color.new(color.green, 50)
else if highADX and priceBelow and slopeNorm < -regTrendTh
    regimeStr := "▼ TRENDING BEAR"
    regimeCol := color.new(color.red, 50)
else if highVol and wideBB and not highADX
    regimeStr := "⚡ VOLATILE"
    regimeCol := color.new(color.orange, 50)
else if lowVol and narrowBB and lowADX
    regimeStr := "🌀 CHOPPY"
    regimeCol := color.new(color.gray, 50)
else if highVol and highADX and (priceAbove or priceBelow) and (slopeNorm > 0.3 or slopeNorm < -0.3)
    regimeStr := "🚀 BREAKOUT"
    regimeCol := color.new(color.blue, 50)
else if (ta.cross(close, emaS) or ta.cross(close, emaT)) and math.abs(slopeNorm) < 0.1
    regimeStr := "↺ REVERSAL"
    regimeCol := color.new(color.purple, 50)

bool regimeVolatile = regimeStr == "⚡ VOLATILE" or regimeStr == "🚀 BREAKOUT"
string volRegimeStr = lowVol ? "LOW VOL 🤏 (Chop)" : highVol ? "HIGH VOL ⚡ (Expand)" : "NORMAL VOL"

if showRegime
    var label regimeLabel = na
    if barstate.islast
        if not na(regimeLabel)
            label.delete(regimeLabel)
        regimeLabel := label.new(bar_index, high + atr * 1.5, regimeStr, color=regimeCol, textcolor=color.white, style=label.style_label_down, size=size.small)

// ── Elastic EMA (Hurst-driven) ──
// Pine v6: ta.ema() requires a SIMPLE int length. A runtime (series) length is
// implemented as an explicit EMA recursion with a dynamic smoothing alpha:
//   alpha = 2 / (len + 1) ;  ema = alpha*close + (1-alpha)*ema[1]
int elasticLen = f_elasticLen(hurstNorm)
var float emaElastic = na
float elAlpha = 2.0 / (elasticLen + 1.0)
emaElastic := na(emaElastic[1]) ? close : elAlpha * close + (1.0 - elAlpha) * emaElastic[1]

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 8: PD ARRAY & KEY INSTITUTIONAL LEVELS               ║
// ╚═══════════════════════════════════════════════════════════════════╝

float rangeH  = ta.highest(high, 50)
float rangeL  = ta.lowest(low,  50)
float rangeEQ = (rangeH + rangeL) * 0.5
bool  inPremium  = close > rangeEQ
bool  inDiscount = close < rangeEQ

// Pine v6: no lookahead parameter — high[1] on "D" is the non-repaint previous-day high
float pdHigh  = request.security(syminfo.tickerid, "D", high[1])
float pdLow   = request.security(syminfo.tickerid, "D", low[1])
float pdClose = request.security(syminfo.tickerid, "D", close[1])
float pwHigh  = request.security(syminfo.tickerid, "W", high[1])
float pwLow   = request.security(syminfo.tickerid, "W", low[1])
// Current week's open — key institutional reference (especially BTC)
float weekOpen = request.security(syminfo.tickerid, "W", open)

float dailyPivot = not na(pdHigh) and not na(pdLow) and not na(pdClose) ? (pdHigh + pdLow + pdClose) / 3.0 : na
bool abovePivot = not na(dailyPivot) and close >= dailyPivot

float recentSwingLow  = ta.lowest(low,  5)
float recentSwingHigh = ta.highest(high, 5)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║              SECTION 9: MULTI-TIMEFRAME BIAS ENGINE              ║
// ╚═══════════════════════════════════════════════════════════════════╝

[tf1Bull, tf1Bear, tf1Dist] = request.security(syminfo.tickerid, biasTF1, f_biasData())
[tf2Bull, tf2Bear, tf2Dist] = request.security(syminfo.tickerid, biasTF2, f_biasData())
[tf3Bull, tf3Bear, tf3Dist] = request.security(syminfo.tickerid, biasTF3, f_biasData())

int biasScore = 0
if tf1Bull
    biasScore += 1
if tf2Bull
    biasScore += 1
if tf3Bull
    biasScore += 1
if tf1Bear
    biasScore -= 1
if tf2Bear
    biasScore -= 1
if tf3Bear
    biasScore -= 1

string overallBias = biasScore >= 2 ? "BULLISH" : biasScore <= -2 ? "BEARISH" : "MIXED"
color  biasBg = biasScore >= 2 ? color.new(color.green, 45) : biasScore <= -2 ? color.new(color.red, 45) : color.new(color.orange, 48)
string biasEmoji = biasScore >= 2 ? "▲ BULLISH" : biasScore <= -2 ? "▼ BEARISH" : "◆ MIXED"

string tf1Str = (tf1Bull ? "▲ BULL " : tf1Bear ? "▼ BEAR " : "◆ FLAT ") + str.tostring(nz(tf1Dist), "#.##") + "% vs EMA50"
string tf2Str = (tf2Bull ? "▲ BULL " : tf2Bear ? "▼ BEAR " : "◆ FLAT ") + str.tostring(nz(tf2Dist), "#.##") + "% vs EMA50"
string tf3Str = (tf3Bull ? "▲ BULL " : tf3Bear ? "▼ BEAR " : "◆ FLAT ") + str.tostring(nz(tf3Dist), "#.##") + "% vs EMA50"
color tf1Col = tf1Bull ? color.new(color.green, 55) : tf1Bear ? color.new(color.red, 55) : color.new(color.orange, 55)
color tf2Col = tf2Bull ? color.new(color.green, 55) : tf2Bear ? color.new(color.red, 55) : color.new(color.orange, 55)
color tf3Col = tf3Bull ? color.new(color.green, 55) : tf3Bear ? color.new(color.red, 55) : color.new(color.orange, 55)

// ── HTF Pivots ──
float htfPvH = request.security(syminfo.tickerid, biasTF2, ta.pivothigh(high, swingLen, swingLen))
float htfPvL = request.security(syminfo.tickerid, biasTF2, ta.pivotlow(low,  swingLen, swingLen))
var float lastHtfPH = na
var float lastHtfPL = na
if not na(htfPvH)
    lastHtfPH := htfPvH
if not na(htfPvL)
    lastHtfPL := htfPvL

// ── HTF Order Block feed ──
[htfBullImp, htfBearImp, htfObH, htfObL] = request.security(syminfo.tickerid, htf, f_obData())
bool isNewHtfBar = ta.change(time(htf)) != 0

// ╔═══════════════════════════════════════════════════════════════════╗
// ║        SECTION 10: ASIAN RANGE & LIQUIDITY GEOMETRY              ║
// ╚═══════════════════════════════════════════════════════════════════╝

var float asianHigh = na
var float asianLow  = na
var int   asianStart = na
var box   asianBox  = na

if inAsian and not inAsian[1]
    asianHigh := high
    asianLow  := low
    asianStart := bar_index
    if showAsian and timeframe.isintraday
        if not na(asianBox)
            box.delete(asianBox)
        asianBox := box.new(bar_index, high, bar_index + 40, low, border_color=color.new(color.purple, 50), bgcolor=color.new(color.purple, 92), border_style=line.style_dashed)
else if inAsian
    asianHigh := math.max(nz(asianHigh, high), high)
    asianLow  := math.min(nz(asianLow, low), low)
    if showAsian and timeframe.isintraday and not na(asianBox)
        box.set_top(asianBox, asianHigh)
        box.set_bottom(asianBox, asianLow)
        box.set_right(asianBox, bar_index + 20)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 11: MARKET STRUCTURE — BOS / CHoCH & SWINGS          ║
// ╚═══════════════════════════════════════════════════════════════════╝

float ph = ta.pivothigh(high, swingLen, swingLen)
float pl = ta.pivotlow(low,  swingLen, swingLen)

var float lastPH  = na
var float lastPH2 = na
var float lastPL  = na
var float lastPL2 = na
var int   structState = 0

if not na(ph)
    lastPH2 := lastPH
    lastPH  := ph
if not na(pl)
    lastPL2 := lastPL
    lastPL  := pl

// BOS / CHoCH with displacement confirmation
bool bullBOS   = not na(lastPL2) and not na(lastPH) and close > lastPH and (close - open) > atr * 0.3
bool bearBOS   = not na(lastPH2) and not na(lastPL) and close < lastPL and (open - close) > atr * 0.3
bool bullCHoCH = not na(lastPL2) and not na(lastPH2) and close > lastPH2 and (close - open) > atr * 0.3
bool bearCHoCH = not na(lastPH2) and not na(lastPL2) and close < lastPL2 and (open - close) > atr * 0.3

if bullBOS or bullCHoCH
    structState := 1
if bearBOS or bearCHoCH
    structState := -1

bool isHH = not na(lastPH) and not na(lastPH2) and lastPH > lastPH2
bool isHL = not na(lastPL) and not na(lastPL2) and lastPL > lastPL2
bool isLH = not na(lastPH) and not na(lastPH2) and lastPH < lastPH2
bool isLL = not na(lastPL) and not na(lastPL2) and lastPL < lastPL2

bool bullStruct = structState == 1
bool bearStruct = structState == -1

string structStr = "◆ RANGING"
if bullStruct
    structStr := "▲ BULLISH (BOS Confirmed)"
else if bearStruct
    structStr := "▼ BEARISH (BOS Confirmed)"
else if bullCHoCH
    structStr := "▲ CHoCH Bullish"
else if bearCHoCH
    structStr := "▼ CHoCH Bearish"
else if isHH
    structStr := "▲ HH — awaiting HL"
else if isHL
    structStr := "▲ HL — awaiting HH"
else if isLH
    structStr := "▼ LH — awaiting LL"
else if isLL
    structStr := "▼ LL — awaiting LH"

color structCol = bullStruct ? color.new(color.green, 50) : bearStruct ? color.new(color.red, 50) : color.new(color.orange, 55)

// ── EQH / EQL — equal highs/lows = stacked stop-liquidity pools ──
bool isEQL  = not na(lastPL) and not na(lastPL2) and math.abs(lastPL - lastPL2) <= atr * eqTolerance
bool isEQH  = not na(lastPH) and not na(lastPH2) and math.abs(lastPH - lastPH2) <= atr * eqTolerance
float eqlLevel = (lastPL + lastPL2) / 2
float eqhLevel = (lastPH + lastPH2) / 2
bool nearEQL = isEQL and math.abs(close - eqlLevel) <= atr * 0.5
bool nearEQH = isEQH and math.abs(close - eqhLevel) <= atr * 0.5

if showMktStr and barstate.isconfirmed
    if isHH
        label.new(bar_index[swingLen], lastPH + atr * 0.4, "HH", textcolor=color.new(color.lime, 20), style=label.style_none, size=size.tiny)
    if isHL
        label.new(bar_index[swingLen], lastPL - atr * 0.4, "HL", textcolor=color.new(color.lime, 20), style=label.style_none, size=size.tiny)
    if isLH
        label.new(bar_index[swingLen], lastPH + atr * 0.4, "LH", textcolor=color.new(color.red, 20), style=label.style_none, size=size.tiny)
    if isLL
        label.new(bar_index[swingLen], lastPL - atr * 0.4, "LL", textcolor=color.new(color.red, 20), style=label.style_none, size=size.tiny)
    if bullBOS
        label.new(bar_index, high + atr * 0.6, "BOS ▲", textcolor=color.new(color.teal, 10), style=label.style_none, size=size.tiny)
    if bearBOS
        label.new(bar_index, low - atr * 0.6, "BOS ▼", textcolor=color.new(color.maroon, 10), style=label.style_none, size=size.tiny)

// ── Liquidity Sweeps (swing + Asian range) ──
bool bslSweep    = not na(lastPH) and high > lastPH and close < lastPH and close < open
bool sslSweep    = not na(lastPL) and low  < lastPL and close > lastPL and close > open
bool sweepAsianH = not na(asianHigh) and high > asianHigh and close < asianHigh and close < open
bool sweepAsianL = not na(asianLow)  and low  < asianLow  and close > asianLow  and close > open

bool validBuySweep  = sslSweep or sweepAsianL
bool validSellSweep = bslSweep or sweepAsianH

if showSweeps and barstate.isconfirmed
    if validBuySweep
        label.new(bar_index, low - atr * 0.6, "SSL ⚡", color=color.new(color.green, 8), textcolor=color.white, style=label.style_label_up, size=size.small)
    if validSellSweep
        label.new(bar_index, high + atr * 0.6, "BSL ⚡", color=color.new(color.red, 8), textcolor=color.white, style=label.style_label_down, size=size.small)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 12: OSCILLATORS — MACD / RSI / VWAP ENGINES         ║
// ╚═══════════════════════════════════════════════════════════════════╝

[macdLine, macdSignal, macdHist] = ta.macd(close, macdFast, macdSlow, macdSig)
bool macdAbove  = macdLine > macdSignal
bool macdBelow  = macdLine < macdSignal
bool macdCross  = ta.crossover(macdLine, macdSignal)
bool macdDeadX  = ta.crossunder(macdLine, macdSignal)
bool macdHistUp = macdHist > macdHist[1]
bool bullDiv    = low[1] < low[5]  and macdLine[1] > macdLine[5] and low[1]  == ta.lowest(low, 5)
bool bearDiv    = high[1] > high[5] and macdLine[1] < macdLine[5] and high[1] == ta.highest(high, 5)

string macdStr = "◆ NEUTRAL"
if macdCross
    macdStr := "▲ BULL CROSS ⚡"
else if macdDeadX
    macdStr := "▼ BEAR CROSS ⚡"
else if bullDiv
    macdStr := "▲ BULL DIVERGENCE"
else if bearDiv
    macdStr := "▼ BEAR DIVERGENCE"
else if macdAbove and macdHistUp
    macdStr := "▲ BULL STRONG"
else if macdAbove
    macdStr := "▲ BULL"
else if macdBelow and not macdHistUp
    macdStr := "▼ BEAR STRONG"
else if macdBelow
    macdStr := "▼ BEAR"

color macdDispCol = color.new(color.orange, 48)
if macdAbove or macdCross or bullDiv
    macdDispCol := color.new(color.green, 45)
else if macdBelow or macdDeadX or bearDiv
    macdDispCol := color.new(color.red, 45)

float rsiVal = ta.rsi(close, rsiLen)
bool  rsiOB  = rsiVal >= rsiOBLvl
bool  rsiOS  = rsiVal <= rsiOSLvl
bool  rsiBullRange = rsiVal > 50 and rsiVal < rsiOBLvl
bool  rsiBearRange = rsiVal < 50 and rsiVal > rsiOSLvl
bool  rsiBullDiv = low[1]  < low[5]  and rsiVal[1] > rsiVal[5] and low[1]  == ta.lowest(low,  5)
bool  rsiBearDiv = high[1] > high[5] and rsiVal[1] < rsiVal[5] and high[1] == ta.highest(high, 5)

string rsiStr = str.tostring(math.round(rsiVal)) + " — "
if rsiOB
    rsiStr := rsiStr + "OVERBOUGHT ⚠"
else if rsiOS
    rsiStr := rsiStr + "OVERSOLD ⚠"
else if rsiBullDiv
    rsiStr := rsiStr + "BULL DIVERGENCE"
else if rsiBearDiv
    rsiStr := rsiStr + "BEAR DIVERGENCE"
else if rsiBullRange
    rsiStr := rsiStr + "BULLISH RANGE"
else if rsiBearRange
    rsiStr := rsiStr + "BEARISH RANGE"
else
    rsiStr := rsiStr + "NEUTRAL"

color rsiDispCol = color.new(color.orange, 55)
if rsiOS or rsiBullDiv
    rsiDispCol := color.new(color.green, 45)
else if rsiOB or rsiBearDiv
    rsiDispCol := color.new(color.red, 45)
else if rsiBullRange
    rsiDispCol := color.new(color.green, 58)
else if rsiBearRange
    rsiDispCol := color.new(color.red, 58)

float vwapVal   = ta.vwap(hlc3)
bool  aboveVwap = close > vwapVal
bool  vwapCross = ta.crossover(close, vwapVal)
bool  vwapDeadX = ta.crossunder(close, vwapVal)

string vwapStr = "—"
color  vwapCol = color.rgb(22, 22, 22)
if timeframe.isintraday
    if vwapCross
        vwapStr := "▲ RECLAIMED VWAP"
        vwapCol := color.new(color.green, 40)
    else if vwapDeadX
        vwapStr := "▼ LOST VWAP"
        vwapCol := color.new(color.red, 40)
    else if aboveVwap
        vwapStr := "▲ ABOVE VWAP"
        vwapCol := color.new(color.green, 55)
    else
        vwapStr := "▼ BELOW VWAP"
        vwapCol := color.new(color.red, 55)
else
    vwapStr := "N/A (Daily+)"
    vwapCol := color.rgb(22, 22, 22)

// ── Volume value band (intraday) — VWAP of highs/lows = institutional value area ──
float vwapHigh = ta.vwap(high)
float vwapLow  = ta.vwap(low)
float oiLevel  = (vwapHigh + vwapLow) / 2
bool aboveValue = timeframe.isintraday and close > vwapHigh
bool belowValue = timeframe.isintraday and close < vwapLow

// ╔═══════════════════════════════════════════════════════════════════╗
// ║   PATTERN ENGINES — ICHIMOKU KINKO HYO + HEAD & SHOULDERS        ║
// ╚═══════════════════════════════════════════════════════════════════╝

// ── Ichimoku (classic 9/26/52). Current cloud = spans projected from 26 bars ago ──
f_ichimoku() =>
    float iTk  = (ta.highest(high, 9) + ta.lowest(low, 9)) / 2
    float iKj  = (ta.highest(high, 26) + ta.lowest(low, 26)) / 2
    float iSpA = (iTk + iKj) / 2
    float iSpB = (ta.highest(high, 52) + ta.lowest(low, 52)) / 2
    [math.max(iSpA[26], iSpB[26]), math.min(iSpA[26], iSpB[26]), iTk, iKj]

[ichiTopC, ichiBotC, ichiTk, ichiKj]   = f_ichimoku()
[dayKumoTop, dayKumoBot, dayTk, dayKj] = request.security(syminfo.tickerid, "D", f_ichimoku())

bool tkKjGold  = ta.crossover(ichiTk, ichiKj)
bool tkKjDeath = ta.crossunder(ichiTk, ichiKj)

string dayKumoState = na(dayKumoTop) ? "N/A" : close > dayKumoTop ? "▲ ABOVE CLOUD" : close < dayKumoBot ? "▼ BELOW CLOUD" : "◆ IN CLOUD"
string tfKumoState  = na(ichiTopC) ? "N/A" : close > ichiTopC ? "▲ ABOVE" : close < ichiBotC ? "▼ BELOW" : "◆ IN"

// ── Head & Shoulders state machine — pivot-confirmed, non-repainting, PURE (no drawing — callable in security) ──
// state: 0 none · 1 LS+HEAD confirmed · 2 complete (neckline watched) · 3 neckline broken
f_hs_top() =>
    var int   tState = 0
    var float tLS = na
    var float tHead = na
    var float tRS = na
    var int   tLSbar = 0
    var int   tHeadBar = 0
    var int   tRSbar = 0
    var float tNL1 = na
    var int   tNL1bar = 0
    var float tNL2 = na
    var int   tNL2bar = 0
    var int   tBrokeBar = 0
    var array<float> phV = array.new_float()
    var array<int>   phB = array.new_int()

    float ph = ta.pivothigh(high, swingLen, swingLen)
    if not na(ph)
        array.unshift(phV, ph)
        array.unshift(phB, bar_index - swingLen)
        if array.size(phV) > 4
            array.pop(phV)
            array.pop(phB)

    float nlNow = na
    bool lsNow = false
    bool rsNow = false
    bool brkNow = false
    bool rstNow = false

    // Invalidation & expiry
    bool resetNow = false
    if tState == 1
        if not na(ph) and ph > tHead
            resetNow := true
        else if bar_index - tHeadBar > 150
            resetNow := true
    if tState == 2
        if close > tHead
            resetNow := true
        else if bar_index - tRSbar > 120
            resetNow := true
    if tState == 3 and bar_index - tBrokeBar > 10
        resetNow := true
    if resetNow
        tState := 0
        tLS := na
        tHead := na
        tRS := na
        tNL1 := na
        tNL2 := na
        tBrokeBar := 0
        rstNow := true

    // 0 → 1 : new head forms above a prior swing (left shoulder)
    if tState == 0 and array.size(phV) >= 2
        float headC = array.get(phV, 0)
        float lsC   = array.get(phV, 1)
        if headC > lsC + atr * 0.5
            tState := 1
            tLS := lsC
            tLSbar := array.get(phB, 1)
            tHead := headC
            tHeadBar := array.get(phB, 0)
            int kA = bar_index - tHeadBar
            int kB = bar_index - tLSbar
            tNL1 := low[kA]
            tNL1bar := tHeadBar
            for kk = kA + 1 to kB - 1
                if low[kk] < tNL1
                    tNL1 := low[kk]
                    tNL1bar := bar_index - kk
            lsNow := true

    // 1 → 2 : right shoulder below head, close to left shoulder
    if tState == 1 and not na(ph) and ph < tHead - atr * 0.5
        if math.abs(ph - tLS) <= atr * hsAtrTol
            tState := 2
            tRS := ph
            tRSbar := bar_index - swingLen
            int kA2 = bar_index - tRSbar
            int kB2 = bar_index - tHeadBar
            tNL2 := low[kA2]
            tNL2bar := tRSbar
            for kk = kA2 + 1 to kB2 - 1
                if low[kk] < tNL2
                    tNL2 := low[kk]
                    tNL2bar := bar_index - kk
            rsNow := true

    // 2 / 3 : live neckline + break watch
    if tState == 2 or tState == 3
        float nlSlope = (tNL2 - tNL1) / math.max(tNL2bar - tNL1bar, 1)
        nlNow := tNL2 + nlSlope * (bar_index - tNL2bar)
        if tState == 2 and close < nlNow
            tState := 3
            tBrokeBar := bar_index
            brkNow := true

    [tState, nlNow, tBrokeBar, tLS, tHead, tRS, tLSbar, tHeadBar, tRSbar, tNL1, tNL1bar, lsNow, rsNow, brkNow, rstNow]

// Mirror — inverse Head & Shoulders (bullish) — pure
f_hs_bottom() =>
    var int   bState = 0
    var float bLS = na
    var float bHead = na
    var float bRS = na
    var int   bLSbar = 0
    var int   bHeadBar = 0
    var int   bRSbar = 0
    var float bNL1 = na
    var int   bNL1bar = 0
    var float bNL2 = na
    var int   bNL2bar = 0
    var int   bBrokeBar = 0
    var array<float> plV = array.new_float()
    var array<int>   plB = array.new_int()

    float pl = ta.pivotlow(low, swingLen, swingLen)
    if not na(pl)
        array.unshift(plV, pl)
        array.unshift(plB, bar_index - swingLen)
        if array.size(plV) > 4
            array.pop(plV)
            array.pop(plB)

    float nlNow = na
    bool lsNow = false
    bool rsNow = false
    bool brkNow = false
    bool rstNow = false

    // Invalidation & expiry
    bool resetNow = false
    if bState == 1
        if not na(pl) and pl < bHead
            resetNow := true
        else if bar_index - bHeadBar > 150
            resetNow := true
    if bState == 2
        if close < bHead
            resetNow := true
        else if bar_index - bRSbar > 120
            resetNow := true
    if bState == 3 and bar_index - bBrokeBar > 10
        resetNow := true
    if resetNow
        bState := 0
        bLS := na
        bHead := na
        bRS := na
        bNL1 := na
        bNL2 := na
        bBrokeBar := 0
        rstNow := true

    // 0 → 1 : new head forms below a prior swing (left shoulder)
    if bState == 0 and array.size(plV) >= 2
        float headC = array.get(plV, 0)
        float lsC   = array.get(plV, 1)
        if headC < lsC - atr * 0.5
            bState := 1
            bLS := lsC
            bLSbar := array.get(plB, 1)
            bHead := headC
            bHeadBar := array.get(plB, 0)
            int kA = bar_index - bHeadBar
            int kB = bar_index - bLSbar
            bNL1 := high[kA]
            bNL1bar := bHeadBar
            for kk = kA + 1 to kB - 1
                if high[kk] > bNL1
                    bNL1 := high[kk]
                    bNL1bar := bar_index - kk
            lsNow := true

    // 1 → 2 : right shoulder above head, close to left shoulder
    if bState == 1 and not na(pl) and pl > bHead + atr * 0.5
        if math.abs(pl - bLS) <= atr * hsAtrTol
            bState := 2
            bRS := pl
            bRSbar := bar_index - swingLen
            int kA2 = bar_index - bRSbar
            int kB2 = bar_index - bHeadBar
            bNL2 := high[kA2]
            bNL2bar := bRSbar
            for kk = kA2 + 1 to kB2 - 1
                if high[kk] > bNL2
                    bNL2 := high[kk]
                    bNL2bar := bar_index - kk
            rsNow := true

    // 2 / 3 : live neckline + break watch
    if bState == 2 or bState == 3
        float nlSlope = (bNL2 - bNL1) / math.max(bNL2bar - bNL1bar, 1)
        nlNow := bNL2 + nlSlope * (bar_index - bNL2bar)
        if bState == 2 and close > nlNow
            bState := 3
            bBrokeBar := bar_index
            brkNow := true

    [bState, nlNow, bBrokeBar, bLS, bHead, bRS, bLSbar, bHeadBar, bRSbar, bNL1, bNL1bar, lsNow, rsNow, brkNow, rstNow]

// H&S stage text for tables
f_hsStage(int st, float nl, string brokeGlyph) =>
    st == 1 ? "LS+Head forming" : st == 2 ? "NL " + str.tostring(nl, "#.##") : "BROKEN " + brokeGlyph

[hsT_state, hsT_nl, hsT_broke, hsT_ls, hsT_head, hsT_rs, hsT_lsB, hsT_headB, hsT_rsB, hsT_nl1, hsT_nl1B, hsT_lsNow, hsT_rsNow, hsT_brkNow, hsT_rstNow] = f_hs_top()
[hsB_state, hsB_nl, hsB_broke, hsB_ls, hsB_head, hsB_rs, hsB_lsB, hsB_headB, hsB_rsB, hsB_nl1, hsB_nl1B, hsB_lsNow, hsB_rsNow, hsB_brkNow, hsB_rstNow] = f_hs_bottom()
[hsD_Tst, hsD_Tnl, hsD_Tbrk, hsD_Tls, hsD_Thead, hsD_Trs, hsD_TlsB, hsD_TheadB, hsD_TrsB, hsD_Tnl1, hsD_Tnl1B, hsD_TlsNow, hsD_TheadNow, hsD_TbrkNow, hsD_TrstNow] = request.security(syminfo.tickerid, "D", f_hs_top())
[hsD_Bst, hsD_Bnl, hsD_Bbrk, hsD_Bls, hsD_Bhead, hsD_Brs, hsD_BlsB, hsD_BheadB, hsD_BrsB, hsD_Bnl1, hsD_Bnl1B, hsD_BlsNow, hsD_BheadNow, hsD_BbrkNow, hsD_BrstNow] = request.security(syminfo.tickerid, "D", f_hs_bottom())

// ── H&S alerts (global scope — security expressions cannot alert) ──
if alertOnSignal
    if hsT_brkNow
        alert("⚠ H&S TOP — Neckline broken | " + syminfo.ticker, alert.freq_once_per_bar)
    if hsB_brkNow
        alert("✔ H&S BOTTOM — Neckline broken | " + syminfo.ticker, alert.freq_once_per_bar)
    if hsD_TbrkNow
        alert("⚠ DAILY H&S TOP — Neckline broken | " + syminfo.ticker, alert.freq_once_per_bar)
    if hsD_BbrkNow
        alert("✔ DAILY H&S BOTTOM — Neckline broken | " + syminfo.ticker, alert.freq_once_per_bar)

f_drawHsVisuals() =>
    // ── H&S visuals — native TF (event-driven LS/HEAD/RS labels + live neckline) ──
    var label nLsT = na
    var label nHeadT = na
    var label nRsT = na
    var line  nNlT = na
    var label nLsB = na
    var label nHeadB = na
    var label nRsB = na
    var line  nNlB = na

    if hsT_rstNow
        if not na(nLsT)
            label.delete(nLsT)
        if not na(nHeadT)
            label.delete(nHeadT)
        if not na(nRsT)
            label.delete(nRsT)
        if not na(nNlT)
            line.delete(nNlT)
        nLsT := na
        nHeadT := na
        nRsT := na
        nNlT := na
    if hsB_rstNow
        if not na(nLsB)
            label.delete(nLsB)
        if not na(nHeadB)
            label.delete(nHeadB)
        if not na(nRsB)
            label.delete(nRsB)
        if not na(nNlB)
            line.delete(nNlB)
        nLsB := na
        nHeadB := na
        nRsB := na
        nNlB := na

    if showHs
        if hsT_lsNow
            if not na(nLsT)
                label.delete(nLsT)
            if not na(nHeadT)
                label.delete(nHeadT)
            nLsT := label.new(hsT_lsB, hsT_ls, "LS", style=label.style_label_up, color=color.new(color.orange, 10), textcolor=color.white, size=size.tiny)
            nHeadT := label.new(hsT_headB, hsT_head, "HEAD", style=label.style_label_up, color=color.new(color.red, 10), textcolor=color.white, size=size.tiny)
        if hsT_rsNow
            nRsT := label.new(hsT_rsB, hsT_rs, "RS", style=label.style_label_up, color=color.new(color.orange, 10), textcolor=color.white, size=size.tiny)
        if hsB_lsNow
            if not na(nLsB)
                label.delete(nLsB)
            if not na(nHeadB)
                label.delete(nHeadB)
            nLsB := label.new(hsB_lsB, hsB_ls, "LS", style=label.style_label_down, color=color.new(color.teal, 10), textcolor=color.white, size=size.tiny)
            nHeadB := label.new(hsB_headB, hsB_head, "HEAD", style=label.style_label_down, color=color.new(color.green, 10), textcolor=color.white, size=size.tiny)
        if hsB_rsNow
            nRsB := label.new(hsB_rsB, hsB_rs, "RS", style=label.style_label_down, color=color.new(color.teal, 10), textcolor=color.white, size=size.tiny)
        if hsT_state == 2 or hsT_state == 3
            if na(nNlT)
                nNlT := line.new(hsT_nl1B, hsT_nl1, bar_index, hsT_nl, color=color.new(color.yellow, 0), style=line.style_dashed, width=2)
            else
                line.set_xy2(nNlT, bar_index, hsT_nl)
        if hsB_state == 2 or hsB_state == 3
            if na(nNlB)
                nNlB := line.new(hsB_nl1B, hsB_nl1, bar_index, hsB_nl, color=color.new(color.yellow, 0), style=line.style_dashed, width=2)
            else
                line.set_xy2(nNlB, bar_index, hsB_nl)

    // ── H&S visuals — daily pattern (series-based neckline + status tag) ──
    var line  dNlT = na
    var line  dNlB = na
    var label dTag = na
    if showHs and barstate.islast
        if not na(dNlT)
            line.delete(dNlT)
        dNlT := na
        if not na(dNlB)
            line.delete(dNlB)
        dNlB := na
        if not na(dTag)
            label.delete(dTag)
        dTag := na
        if hsD_Tst >= 2 and not na(hsD_Tnl) and not na(hsD_Tnl[40])
            dNlT := line.new(bar_index - 40, hsD_Tnl[40], bar_index + 10, hsD_Tnl, color=color.new(color.yellow, 0), style=line.style_dashed, width=2)
        if hsD_Bst >= 2 and not na(hsD_Bnl) and not na(hsD_Bnl[40])
            dNlB := line.new(bar_index - 40, hsD_Bnl[40], bar_index + 10, hsD_Bnl, color=color.new(color.yellow, 0), style=line.style_dashed, width=2)
        if hsD_Tst >= 1 or hsD_Bst >= 1
            string dTxt = ""
            if hsD_Tst > 0
                dTxt := "DAILY H&S TOP " + (hsD_Tst == 3 ? "BROKEN ✖" : "forming")
            if hsD_Bst > 0
                dTxt := dTxt + (dTxt == "" ? "" : " · ") + "DAILY H&S BOT " + (hsD_Bst == 3 ? "BROKEN ✔" : "forming")
            dTag := label.new(bar_index, high, dTxt, style=label.style_label_down, color=color.new(color.yellow, 25), textcolor=color.black, size=size.tiny)

f_drawHsVisuals()

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 13: INSTITUTIONAL FOOTPRINT & DARK POOL LEVELS       ║
// ╚═══════════════════════════════════════════════════════════════════╝

float delta    = (close - open) * volume / math.max(high - low, 0.001)
float cumDelta = ta.cum(delta)
float deltaAvg = ta.sma(cumDelta, 50)
float deltaZ   = (cumDelta - deltaAvg) / math.max(ta.stdev(cumDelta, 50), 0.001)

bool aggressiveBuy  = delta > 0 and volume > volSma * 1.5
bool aggressiveSell = delta < 0 and volume > volSma * 1.5
bool absorption     = math.abs(close - open) < atr * 0.2 and volume > volSma * 1.3

string footprintStr = "Neutral"
color  footprintCol = color.rgb(22, 22, 22)
if absorption
    footprintStr := "Absorption ⚡ (Reversal)"
    footprintCol := color.new(color.purple, 50)
else if aggressiveBuy
    footprintStr := "Aggressive Buying 🟢"
    footprintCol := color.new(color.green, 45)
else if aggressiveSell
    footprintStr := "Aggressive Selling 🔴"
    footprintCol := color.new(color.red, 45)
else if deltaZ > 1.5
    footprintStr := "Accumulation 📈"
    footprintCol := color.new(color.blue, 45)
else if deltaZ < -1.5
    footprintStr := "Distribution 📉"
    footprintCol := color.new(color.orange, 45)

// Dark pool levels — persist (var) so proximity scoring works between extremes
var float dpBuyLevel  = na
var float dpSellLevel = na
if useDarkPool
    if deltaZ > 1.8
        dpBuyLevel := close
    if deltaZ < -1.8
        dpSellLevel := close

var line dpLineBuy  = na
var line dpLineSell = na
if showDarkPool and barstate.islast
    if not na(dpLineBuy)
        line.delete(dpLineBuy)
    if not na(dpLineSell)
        line.delete(dpLineSell)
    if not na(dpBuyLevel)
        dpLineBuy := line.new(bar_index - 30, dpBuyLevel, bar_index + 15, dpBuyLevel, color=color.new(color.blue, 40), style=line.style_dashed, width=2)
    if not na(dpSellLevel)
        dpLineSell := line.new(bar_index - 30, dpSellLevel, bar_index + 15, dpSellLevel, color=color.new(color.orange, 40), style=line.style_dashed, width=2)

// ── Sentiment vector (-1 .. +1) ──
float sentiment = 0.0
if rsiVal > 50
    sentiment += 0.2
else if rsiVal < 50
    sentiment -= 0.2
if macdAbove
    sentiment += 0.2
else if macdBelow
    sentiment -= 0.2
if adxLine > 25 and close > emaS
    sentiment += 0.3
else if adxLine > 25 and close < emaS
    sentiment -= 0.3
if bullStruct
    sentiment += 0.3
else if bearStruct
    sentiment -= 0.3
sentiment := math.max(-1.0, math.min(1.0, sentiment))

string sentStr = "Neutral"
color  sentCol = color.rgb(30, 30, 30)
if sentiment > 0.5
    sentStr := "Extreme Greed 🤑"
    sentCol := color.new(color.green, 45)
else if sentiment > 0.2
    sentStr := "Bullish"
    sentCol := color.new(color.green, 65)
else if sentiment < -0.5
    sentStr := "Extreme Fear 😨"
    sentCol := color.new(color.red, 45)
else if sentiment < -0.2
    sentStr := "Bearish"
    sentCol := color.new(color.red, 65)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║        SECTION 14: WYCKOFF PHASE & VOLUME DIVERGENCE            ║
// ╚═══════════════════════════════════════════════════════════════════╝

bool volBullDiv = useVolDiv and low[1] < low[5]  and volume[1] < volume[5] and low[1]  == ta.lowest(low, 5)
bool volBearDiv = useVolDiv and high[1] > high[5] and volume[1] < volume[5] and high[1] == ta.highest(high, 5)

bool wyckSpring = useWyckoff and not na(lastPL2) and low < lastPL2 and close > lastPL2 and volume > volSma * 1.3 and inDiscount
bool wyckSOS    = useWyckoff and close > rangeH[1] and volume > volSma * 1.5 and bullStruct
bool wyckUTAD   = useWyckoff and high > rangeH and close < rangeEQ and volume < volSma * 0.8 and inPremium
bool wyckLPSY   = useWyckoff and not na(lastPH) and high < lastPH and volume < volSma * 0.8 and bearStruct

string wyckStr = "N/A"
color  wyckCol = color.gray
if wyckSpring
    wyckStr := "ACCUMULATION: Spring"
    wyckCol := color.new(color.green, 50)
else if wyckSOS
    wyckStr := "MARKUP: SOS"
    wyckCol := color.new(color.teal, 50)
else if wyckUTAD
    wyckStr := "DISTRIBUTION: UTAD"
    wyckCol := color.new(color.orange, 50)
else if wyckLPSY
    wyckStr := "DISTRIBUTION: LPSY"
    wyckCol := color.new(color.red, 50)
else if inDiscount and lowVol
    wyckStr := "ACCUMULATION: Building"
    wyckCol := color.new(color.blue, 60)
else if inPremium and lowVol
    wyckStr := "DISTRIBUTION: Building"
    wyckCol := color.new(color.maroon, 60)
else if bullStruct
    wyckStr := "MARKUP: Phase"
    wyckCol := color.new(color.green, 70)
else if bearStruct
    wyckStr := "MARKDOWN: Phase"
    wyckCol := color.new(color.red, 70)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║        SECTION 15: CANDLESTICK PATTERN ENGINE                   ║
// ╚═══════════════════════════════════════════════════════════════════╝

float candleBody = math.abs(close - open)
float upperWick  = high - math.max(close, open)
float lowerWick  = math.min(close, open) - low
float totalRange = high - low

bool bullPin = totalRange > atr * 0.3 and lowerWick > candleBody * 2.0 and lowerWick > upperWick * 2.0
bool bearPin = totalRange > atr * 0.3 and upperWick > candleBody * 2.0 and upperWick > lowerWick * 2.0
bool bullEngulf = close > open and close[1] < open[1] and close > open[1] and open < close[1] and candleBody > math.abs(close[1] - open[1])
bool bearEngulf = close < open and close[1] > open[1] and close < open[1] and open > close[1] and candleBody > math.abs(close[1] - open[1])
bool insideBar   = high[1] < high[2] and low[1] > low[2]
bool ibBullBreak = insideBar[1] and close > high[1]
bool ibBearBreak = insideBar[1] and close < low[1]
bool doji        = totalRange > atr * 0.2 and candleBody < atr * 0.08

string candleStr = "No Pattern"
if bullEngulf or ibBullBreak
    candleStr := "▲ Bull Engulf / IB Break"
else if bullPin
    candleStr := "▲ Bullish Pin Bar"
else if bearEngulf or ibBearBreak
    candleStr := "▼ Bear Engulf / IB Break"
else if bearPin
    candleStr := "▼ Bearish Pin Bar"
else if doji
    candleStr := "◆ Doji (Indecision)"

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 16: SMC QUANTUM ZONES — OB / FVG / BREAKERS         ║
// ╚═══════════════════════════════════════════════════════════════════╝

// ── Liquidity tier classifier (Retail → CTA → Inst → Central Bank) ──
f_liqTier(float level) =>
    int tier = 0
    if useLiqHier
        float dist = math.abs(close - level) / math.max(atr, 0.001)
        if dist < 0.3
            tier := 1
        if (not na(pdHigh) and math.abs(level - pdHigh) < atr * 0.5) or (not na(pdLow) and math.abs(level - pdLow) < atr * 0.5)
            tier := 2
        if (not na(pwHigh) and math.abs(level - pwHigh) < atr * 0.8) or (not na(pwLow) and math.abs(level - pwLow) < atr * 0.8)
            tier := 3
        if math.abs(level - math.round(level / 50.0) * 50.0) < 0.05 or math.abs(level - math.round(level / 1000.0) * 1000.0) < 0.05
            tier := 4
        if tier > maxLiqTier
            tier := maxLiqTier
    tier

f_tierString(int t) =>
    t == 4 ? "🟥 CB" : t == 3 ? "🟧 Inst" : t == 2 ? "🟩 CTA" : t == 1 ? "🟦 Retail" : "—"

// ── Zone arrays ──
var array<float> bObH    = array.new<float>()
var array<float> bObL    = array.new<float>()
var array<int>   bObBar  = array.new<int>()
var array<bool>  bObOk   = array.new<bool>()
var array<box>   bObBox  = array.new<box>()
var array<float> bObStr  = array.new<float>()
var array<int>   bObTouch = array.new<int>()

var array<float> rObH    = array.new<float>()
var array<float> rObL    = array.new<float>()
var array<int>   rObBar  = array.new<int>()
var array<bool>  rObOk   = array.new<bool>()
var array<box>   rObBox  = array.new<box>()
var array<float> rObStr  = array.new<float>()
var array<int>   rObTouch = array.new<int>()

var array<float> bFvgH   = array.new<float>()
var array<float> bFvgL   = array.new<float>()
var array<int>   bFvgBar = array.new<int>()
var array<bool>  bFvgOk  = array.new<bool>()
var array<box>   bFvgBox = array.new<box>()
var array<float> bFvgStr = array.new<float>()

var array<float> rFvgH   = array.new<float>()
var array<float> rFvgL   = array.new<float>()
var array<int>   rFvgBar = array.new<int>()
var array<bool>  rFvgOk  = array.new<bool>()
var array<box>   rFvgBox = array.new<box>()
var array<float> rFvgStr = array.new<float>()

var array<float> rbsH    = array.new<float>()
var array<float> rbsL    = array.new<float>()
var array<bool>  rbsOk   = array.new<bool>()
var array<box>   rbsBox  = array.new<box>()
var array<int>   rbsState = array.new<int>()

var array<float> sbrH    = array.new<float>()
var array<float> sbrL    = array.new<float>()
var array<bool>  sbrOk   = array.new<bool>()
var array<box>   sbrBox  = array.new<box>()
var array<int>   sbrState = array.new<int>()

var array<line> zoneLines = array.new<line>()

// ── Zone adders ──
addOB(array<float> H, array<float> L, array<int> Bar, array<bool> Ok, array<box> Bx, array<float> Str, array<int> Touch,
      float top, float bot, int bIdx, float strength, color borderC, color bgC, int bw) =>
    if array.size(H) >= maxOBs
        if array.size(Bx) > 0
            box.delete(array.shift(Bx))
        array.shift(H)
        array.shift(L)
        array.shift(Bar)
        array.shift(Ok)
        array.shift(Str)
        array.shift(Touch)
    array.push(H, top)
    array.push(L, bot)
    array.push(Bar, bIdx)
    array.push(Ok, true)
    array.push(Str, strength)
    array.push(Touch, 0)
    int tier = f_liqTier(top)
    if showOB
        array.push(Bx, box.new(bIdx, top, bIdx + 40, bot, border_color=borderC, bgcolor=bgC, border_width=bw))
        if showTierTags and tier > 0
            label.new(bIdx + 20, (top + bot) / 2, f_tierString(tier), color=color.new(color.gray, 50), textcolor=color.white, style=label.style_label_center, size=size.tiny)

addFVG(array<float> H, array<float> L, array<int> Bar, array<bool> Ok, array<box> Bx, array<float> Str,
       float top, float bot, int bIdx, color bgC) =>
    if array.size(H) >= maxFVGs
        if array.size(Bx) > 0
            box.delete(array.shift(Bx))
        array.shift(H)
        array.shift(L)
        array.shift(Bar)
        array.shift(Ok)
        array.shift(Str)
    array.push(H, top)
    array.push(L, bot)
    array.push(Bar, bIdx)
    array.push(Ok, true)
    array.push(Str, 70.0)
    if showFVG
        array.push(Bx, box.new(bIdx, top, bIdx + 40, bot, border_color=na, bgcolor=bgC))

// Quantum field line drawer (global scope — Pine forbids functions inside control blocks)
drawFieldForZone(array<float> H, array<float> L, array<bool> Ok, array<float> Str, color col) =>
    if array.size(H) > 0
        for i = 0 to array.size(H) - 1
            if array.get(Ok, i)
                float top = array.get(H, i)
                float bot = array.get(L, i)
                float strV = array.get(Str, i) / 100.0
                float mid = (top + bot) / 2.0
                float width = atr * 0.5 * (1.0 + strV)
                for j = -2 to 2
                    float offset = (j / 2.0) * width
                    float y1 = mid + offset - atr * 0.1
                    float y2 = mid + offset + atr * 0.1
                    line l = line.new(bar_index - 5, y1, bar_index + 5, y2, color=color.new(col, 30), width=1, style=line.style_dotted)
                    array.push(zoneLines, l)

// ── Impulse detection & zone creation ──
float localImpThresh = atr * regAtrMult * symImpulse * 1.2
bool  bullImpulse    = (close - open) > localImpThresh and close > high[1] and close[1] < open[1]
bool  bearImpulse    = (open - close) > localImpThresh and close < low[1] and close[1] > open[1]

if htfBullImp and isNewHtfBar
    addOB(bObH, bObL, bObBar, bObOk, bObBox, bObStr, bObTouch, htfObH, htfObL, bar_index, 100.0, color.new(color.lime, 5), color.new(color.lime, 72), 2)
else if bullImpulse
    float bStr = math.min((close - open) / atr * 25.0 + 50.0, 100.0)
    addOB(bObH, bObL, bObBar, bObOk, bObBox, bObStr, bObTouch, high[1], low[1], bar_index[1], bStr, color.new(color.lime, 30), color.new(color.lime, 88), 1)

if htfBearImp and isNewHtfBar
    addOB(rObH, rObL, rObBar, rObOk, rObBox, rObStr, rObTouch, htfObH, htfObL, bar_index, 100.0, color.new(color.red, 5), color.new(color.red, 72), 2)
else if bearImpulse
    float rStr = math.min((open - close) / atr * 25.0 + 50.0, 100.0)
    addOB(rObH, rObL, rObBar, rObOk, rObBox, rObStr, rObTouch, high[1], low[1], bar_index[1], rStr, color.new(color.red, 30), color.new(color.red, 88), 1)

// ── Breaker blocks (RBS/SBR), capped at 8 ──
if not na(ph) and close > ph
    float half = atr * 0.5
    if array.size(rbsH) >= 8
        if array.size(rbsBox) > 0
            box.delete(array.shift(rbsBox))
        array.shift(rbsH)
        array.shift(rbsL)
        array.shift(rbsOk)
        array.shift(rbsState)
    array.push(rbsH, ph)
    array.push(rbsL, ph - half)
    array.push(rbsOk, true)
    array.push(rbsState, 0)
    if showBreakers
        array.push(rbsBox, box.new(bar_index[swingLen], ph, bar_index + 40, ph - half, border_color=color.new(color.blue, 35), bgcolor=color.new(color.blue, 88), border_width=1))

if not na(pl) and close < pl
    float half = atr * 0.5
    if array.size(sbrH) >= 8
        if array.size(sbrBox) > 0
            box.delete(array.shift(sbrBox))
        array.shift(sbrH)
        array.shift(sbrL)
        array.shift(sbrOk)
        array.shift(sbrState)
    array.push(sbrH, pl + half)
    array.push(sbrL, pl)
    array.push(sbrOk, true)
    array.push(sbrState, 0)
    if showBreakers
        array.push(sbrBox, box.new(bar_index[swingLen], pl + half, bar_index + 40, pl, border_color=color.new(color.orange, 35), bgcolor=color.new(color.orange, 88), border_width=1))

// ── FVG detection ──
float fvgMin  = atr * (fvgMinPct / 100.0) * symFvgScl
bool  bFvgNew = low > high[2] and (low - high[2]) >= fvgMin
bool  rFvgNew = high < low[2] and (low[2] - high) >= fvgMin

if bFvgNew
    addFVG(bFvgH, bFvgL, bFvgBar, bFvgOk, bFvgBox, bFvgStr, low, high[2], bar_index[1], color.new(color.teal, 83))
if rFvgNew
    addFVG(rFvgH, rFvgL, rFvgBar, rFvgOk, rFvgBox, rFvgStr, low[2], high, bar_index[1], color.new(color.fuchsia, 83))

// ── OB hit detection, touch decay & quantum gravity ──
bool  bObHit = false
bool  bObCoreTap = false
float bObNearTop = na
float bObNearBot = na
float bObMinDist = 1e10
float bObZoneGravity = 0.0
if array.size(bObH) > 0
    for i = 0 to array.size(bObH) - 1
        if array.get(bObOk, i)
            float t  = array.get(bObH, i)
            float b  = array.get(bObL, i)
            int   cb = array.get(bObBar, i)
            if showOB and i < array.size(bObBox)
                box.set_right(array.get(bObBox, i), bar_index + 40)
            if low <= t and close >= b
                bObHit := true
                // Quantum core band — central probability zone of the OB
                float hz = t - b
                if hz > 0
                    float coreBot = b + hz * (1.0 - qCorePct) / 2.0
                    float coreTop = b + hz * (1.0 + qCorePct) / 2.0
                    if close >= coreBot and close <= coreTop
                        bObCoreTap := true
                        if showOB and i < array.size(bObBox)
                            box.set_bgcolor(array.get(bObBox, i), color.new(color.lime, 45))
                int touches = array.get(bObTouch, i) + 1
                array.set(bObTouch, i, touches)
                if touches >= 3
                    array.set(bObStr, i, array.get(bObStr, i) * 0.7)
            float dist = math.abs(close - t) / math.max(atr, 0.001)
            bObZoneGravity += f_sigmoid(-dist, qGravity) * (array.get(bObStr, i) / 100.0)
            if close < b or (bar_index - cb) > zoneMaxAge
                array.set(bObOk, i, false)
                if showOB and i < array.size(bObBox)
                    box.set_bgcolor(array.get(bObBox, i), color.new(color.gray, 94))
                    box.set_border_color(array.get(bObBox, i), na)
            float d = math.abs(close - t)
            if d < bObMinDist
                bObMinDist := d
                bObNearTop := t
                bObNearBot := b

bool  rObHit = false
bool  rObCoreTap = false
float rObNearTop = na
float rObNearBot = na
float rObMinDist = 1e10
float rObZoneGravity = 0.0
if array.size(rObH) > 0
    for i = 0 to array.size(rObH) - 1
        if array.get(rObOk, i)
            float t  = array.get(rObH, i)
            float b  = array.get(rObL, i)
            int   cb = array.get(rObBar, i)
            if showOB and i < array.size(rObBox)
                box.set_right(array.get(rObBox, i), bar_index + 40)
            if high >= b and close <= t
                rObHit := true
                // Quantum core band — central probability zone of the OB
                float hz = t - b
                if hz > 0
                    float coreBot = b + hz * (1.0 - qCorePct) / 2.0
                    float coreTop = b + hz * (1.0 + qCorePct) / 2.0
                    if close >= coreBot and close <= coreTop
                        rObCoreTap := true
                        if showOB and i < array.size(rObBox)
                            box.set_bgcolor(array.get(rObBox, i), color.new(color.red, 45))
                int touches = array.get(rObTouch, i) + 1
                array.set(rObTouch, i, touches)
                if touches >= 3
                    array.set(rObStr, i, array.get(rObStr, i) * 0.7)
            float dist = math.abs(close - b) / math.max(atr, 0.001)
            rObZoneGravity += f_sigmoid(-dist, qGravity) * (array.get(rObStr, i) / 100.0)
            if close > t or (bar_index - cb) > zoneMaxAge
                array.set(rObOk, i, false)
                if showOB and i < array.size(rObBox)
                    box.set_bgcolor(array.get(rObBox, i), color.new(color.gray, 94))
                    box.set_border_color(array.get(rObBox, i), na)
            float d = math.abs(close - b)
            if d < rObMinDist
                rObMinDist := d
                rObNearTop := t
                rObNearBot := b

// ── FVG hit detection & nearest tracking ──
bool  bFvgHit = false
float bFvgNearTop = na
float bFvgNearBot = na
float bFvgMinDist = 1e10
if array.size(bFvgH) > 0
    for i = 0 to array.size(bFvgH) - 1
        if array.get(bFvgOk, i)
            float t  = array.get(bFvgH, i)
            float b  = array.get(bFvgL, i)
            int   cb = array.get(bFvgBar, i)
            if showFVG and i < array.size(bFvgBox)
                box.set_right(array.get(bFvgBox, i), bar_index + 40)
            if low <= t and close >= b
                bFvgHit := true
            if close < b or (bar_index - cb) > zoneMaxAge
                array.set(bFvgOk, i, false)
                if showFVG and i < array.size(bFvgBox)
                    box.set_bgcolor(array.get(bFvgBox, i), color.new(color.gray, 94))
            float d = math.abs(close - t)
            if d < bFvgMinDist
                bFvgMinDist := d
                bFvgNearTop := t
                bFvgNearBot := b

bool  rFvgHit = false
float rFvgNearTop = na
float rFvgNearBot = na
float rFvgMinDist = 1e10
if array.size(rFvgH) > 0
    for i = 0 to array.size(rFvgH) - 1
        if array.get(rFvgOk, i)
            float t  = array.get(rFvgH, i)
            float b  = array.get(rFvgL, i)
            int   cb = array.get(rFvgBar, i)
            if showFVG and i < array.size(rFvgBox)
                box.set_right(array.get(rFvgBox, i), bar_index + 40)
            if high >= b and close <= t
                rFvgHit := true
            if close > t or (bar_index - cb) > zoneMaxAge
                array.set(rFvgOk, i, false)
                if showFVG and i < array.size(rFvgBox)
                    box.set_bgcolor(array.get(rFvgBox, i), color.new(color.gray, 94))
            float d = math.abs(close - t)
            if d < rFvgMinDist
                rFvgMinDist := d
                rFvgNearTop := t
                rFvgNearBot := b

// ── Breaker hit detection ──
bool rbsHit = false
float rbsNearTop = na
if array.size(rbsH) > 0
    for i = 0 to array.size(rbsH) - 1
        if array.get(rbsOk, i)
            float t = array.get(rbsH, i)
            float b = array.get(rbsL, i)
            if showBreakers and i < array.size(rbsBox)
                box.set_right(array.get(rbsBox, i), bar_index + 40)
            if low <= t and close >= b
                rbsHit := true
                rbsNearTop := t
                array.set(rbsState, i, 1)
            if close < b or (bar_index - 10) > zoneMaxAge
                array.set(rbsOk, i, false)
                if showBreakers and i < array.size(rbsBox)
                    box.set_bgcolor(array.get(rbsBox, i), color.new(color.gray, 94))
                    box.set_border_color(array.get(rbsBox, i), na)

bool sbrHit = false
float sbrNearTop = na
if array.size(sbrH) > 0
    for i = 0 to array.size(sbrH) - 1
        if array.get(sbrOk, i)
            float t = array.get(sbrH, i)
            float b = array.get(sbrL, i)
            if showBreakers and i < array.size(sbrBox)
                box.set_right(array.get(sbrBox, i), bar_index + 40)
            if high >= b and close <= t
                sbrHit := true
                sbrNearTop := t
                array.set(sbrState, i, 1)
            if close > t or (bar_index - 10) > zoneMaxAge
                array.set(sbrOk, i, false)
                if showBreakers and i < array.size(sbrBox)
                    box.set_bgcolor(array.get(sbrBox, i), color.new(color.gray, 94))
                    box.set_border_color(array.get(sbrBox, i), na)

// ── Quantum field gravity visual ──
if showQuantum
    if array.size(zoneLines) > 0
        for i = 0 to array.size(zoneLines) - 1
            line.delete(array.get(zoneLines, i))
        array.clear(zoneLines)
    drawFieldForZone(bObH, bObL, bObOk, bObStr, color.green)
    drawFieldForZone(rObH, rObL, rObOk, rObStr, color.red)
    drawFieldForZone(bFvgH, bFvgL, bFvgOk, bFvgStr, color.teal)
    drawFieldForZone(rFvgH, rFvgL, rFvgOk, rFvgStr, color.fuchsia)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 17: ASOC SELF-OPTIMIZATION & DYNAMIC THRESHOLD       ║
// ╚═══════════════════════════════════════════════════════════════════╝

var array<bool> asocResults = array.new_bool(20, true)
// Hybrid ASOC state [0]=persistent offset(%) [1]=wins in sample [2]=trades in sample
var array<float> adaptState = array.new_float(3, 0.0)

// ── SELF-MEASURING MODES — per-mode track record feeds back into the router ──
// Losing modes get stricter entry thresholds; modes <35% WR after 8+ trades are bypassed
var int    tradeMode  = 0
var array<float> modePerf = array.new<float>(12)   // per mode: [trades, wins]
f_modePerfAdd(int m, bool win) =>
    int base = math.min(5, math.max(0, m)) * 2
    array.set(modePerf, base, array.get(modePerf, base) + 1.0)
    array.set(modePerf, base + 1, array.get(modePerf, base + 1) + (win ? 1.0 : 0.0))
f_modePerfN(int m) =>
    array.get(modePerf, math.min(5, math.max(0, m)) * 2)
f_modePerfWr(int m) =>
    float n = f_modePerfN(m)
    n >= 5 ? array.get(modePerf, math.min(5, math.max(0, m)) * 2 + 1) / n : 0.5
f_modeThreshAdj(int m) =>
    f_modePerfN(m) >= 5 ? math.max(-10.0, math.min(10.0, (0.5 - f_modePerfWr(m)) * 40.0)) : 0.0
f_modeViable(int m) =>
    float n = f_modePerfN(m)
    n < 8 or f_modePerfWr(m) >= 0.35

// HYBRID SELF-OPT — v10 ASOC rolling results + HYBRID v9 step-tuner, merged.
// Every asocWindow closed trades: WR < 45% → threshold +optStep (stricter);
// WR > 70% → threshold -optStep (looser). Offset persists until another sample flips it.
f_record_trade(bool isWin) =>
    f_modePerfAdd(tradeMode, isWin)
    if useASOC
        array.unshift(asocResults, isWin)
        if array.size(asocResults) > asocWindow
            array.pop(asocResults)
        float off = array.get(adaptState, 0)
        float wins = array.get(adaptState, 1)
        float trades = array.get(adaptState, 2)
        trades := trades + 1.0
        if isWin
            wins := wins + 1.0
        if trades >= asocWindow
            float wRate = wins / trades * 100.0
            if wRate < 45.0
                off := math.min(off + optStep, 10.0)
            else if wRate > 70.0
                off := math.max(off - optStep, -10.0)
            trades := 0.0
            wins := 0.0
        array.set(adaptState, 0, off)
        array.set(adaptState, 1, wins)
        array.set(adaptState, 2, trades)

float asocWins = 0.0
for i = 0 to array.size(asocResults) - 1
    if array.get(asocResults, i)
        asocWins += 1.0
float asocWinRate = asocWins / math.max(array.size(asocResults), 1) * 100.0
float adaptOffset = array.get(adaptState, 0)
float adaptTrades = array.get(adaptState, 2)

// Base threshold by risk profile → regime-adjusted → hybrid ASOC offset (clamp 55–92)
float baseThresh = userRisk == RiskProfile.Aggressive ? 65.0 : userRisk == RiskProfile.Strict ? 80.0 : 70.0
float threshAdj = 0.0
if regimeStr == "▲ TRENDING BULL" or regimeStr == "▼ TRENDING BEAR"
    threshAdj := -5.0
else if regimeStr == "🌀 CHOPPY"
    threshAdj := 8.0
else if regimeStr == "🚀 BREAKOUT"
    threshAdj := -3.0
else if regimeStr == "↺ REVERSAL"
    threshAdj := 2.0
float confThreshold = math.max(55.0, math.min(92.0, baseThresh + threshAdj))

float dynThreshold = math.max(55.0, math.min(92.0, confThreshold + (useASOC ? adaptOffset : 0.0)))

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 18: 20-FACTOR CONFLUENCE SCORING ENGINE             ║
// ═══════════════════════════════════════════════════════════════════╝

float buyScore  = 0.0
float sellScore = 0.0
string buyWhy   = "Awaiting Setup..."
string sellWhy  = "Awaiting Setup..."

bool atBuyZone  = bObHit or bFvgHit or rbsHit
bool atSellZone = rObHit or rFvgHit or sbrHit

// 1. Macro alignment (max 20)
if useMacro and macroValid
    if macroBullish
        buyScore += 20.0
        buyWhy := "Macro Aligned"
    else if macroBearish
        buyScore -= 25.0
    if macroBearish
        sellScore += 20.0
        sellWhy := "Macro Aligned"
    else if macroBullish
        sellScore -= 25.0

// 2. Trend stack & HMA baseline (max 18)
if bullStack
    buyScore += 12.0
if close > hmaBase
    buyScore += 6.0
if bearStack
    sellScore += 12.0
if close < hmaBase
    sellScore += 6.0

// 3. Elastic EMA (max 6)
if close > emaElastic
    buyScore += 6.0
else
    sellScore += 6.0

// 4. Multi-TF bias (max 15)
if tf3Bull
    buyScore += 8.0
if tf2Bull
    buyScore += 4.0
if tf1Bull
    buyScore += 3.0
if tf3Bear
    sellScore += 8.0
if tf2Bear
    sellScore += 4.0
if tf1Bear
    sellScore += 3.0

// 5. Liquidity sweeps & Asian range (max 25, symbol-weighted)
if validBuySweep and volImbalance
    buyScore += 25.0 * symSweepW
    buyWhy := "SSL Sweep + Vol Imbalance"
else if validBuySweep
    buyScore += 18.0 * symSweepW
    buyWhy := "SSL Sweep"
if validSellSweep and volImbalance
    sellScore += 25.0 * symSweepW
    sellWhy := "BSL Sweep + Vol Imbalance"
else if validSellSweep
    sellScore += 18.0 * symSweepW
    sellWhy := "BSL Sweep"

// 6. SMC quantum zones — OB gravity (max 22). Core-band taps score above shadow taps.
if bObHit or bObZoneGravity > 0.4
    float qBase = bObHit ? (bObCoreTap ? 18.0 : 12.0) : 8.0
    float qBonus = math.min(22.0, qBase + bObZoneGravity * 10.0)
    buyScore += qBonus
    buyWhy := buyWhy == "Awaiting Setup..." ? (bObCoreTap ? "Bull OB Core Tap" : "Bull OB Gravity") : buyWhy + " + OB"
if rObHit or rObZoneGravity > 0.4
    float qBase = rObHit ? (rObCoreTap ? 18.0 : 12.0) : 8.0
    float qBonus = math.min(22.0, qBase + rObZoneGravity * 10.0)
    sellScore += qBonus
    sellWhy := sellWhy == "Awaiting Setup..." ? (rObCoreTap ? "Bear OB Core Tap" : "Bear OB Gravity") : sellWhy + " + OB"

// 6b. FVG hits (max 10)
if bFvgHit and inDiscount
    buyScore += 10.0
    buyWhy := buyWhy == "Awaiting Setup..." ? "Bull FVG" : buyWhy + " + FVG"
else if bFvgHit
    buyScore += 6.0
if rFvgHit and inPremium
    sellScore += 10.0
    sellWhy := sellWhy == "Awaiting Setup..." ? "Bear FVG" : sellWhy + " + FVG"
else if rFvgHit
    sellScore += 6.0

// 7. Breaker blocks (max 10)
if rbsHit and inDiscount
    buyScore += 10.0
    buyWhy := buyWhy == "Awaiting Setup..." ? "RBS Breaker" : buyWhy + " + RBS"
else if rbsHit
    buyScore += 6.0
if sbrHit and inPremium
    sellScore += 10.0
    sellWhy := sellWhy == "Awaiting Setup..." ? "SBR Breaker" : sellWhy + " + SBR"
else if sbrHit
    sellScore += 6.0

// 8. Premium / Discount equilibrium (max 8)
if inDiscount
    buyScore += 8.0
if inPremium
    sellScore += 8.0

// 9. ADX trend strength (max 6)
if adxOk
    buyScore += 6.0
    sellScore += 6.0

// 10. MACD & divergence (max 8)
if macdAbove
    buyScore += 4.0
if macdBelow
    sellScore += 4.0
if macdCross or bullDiv
    buyScore += 4.0
if macdDeadX or bearDiv
    sellScore += 4.0

// 11. RSI & extreme reversals (max 8)
if rsiOS and inDiscount
    buyScore += 8.0
if rsiOB and inPremium
    sellScore += 8.0
if rsiBullDiv
    buyScore += 4.0
if rsiBearDiv
    sellScore += 4.0

// 12. Dark pool proximity (max 6)
if not na(dpBuyLevel) and math.abs(close - dpBuyLevel) < atr * 0.6
    buyScore += 6.0
if not na(dpSellLevel) and math.abs(close - dpSellLevel) < atr * 0.6
    sellScore += 6.0

// 13. Market structure (max 8)
if bullStruct
    buyScore += 8.0
if bearStruct
    sellScore += 8.0

// 14. VWAP (intraday, max 5)
if timeframe.isintraday
    if aboveVwap
        buyScore += 5.0
    else
        sellScore += 5.0

// 15. Wyckoff & volume divergence (max 8)
if volBullDiv and inDiscount
    buyScore += 4.0
if volBearDiv and inPremium
    sellScore += 4.0
if wyckSpring
    buyScore += 4.0
if wyckUTAD
    sellScore += 4.0

// 16. Z-score extremes (max 10)
if zScore <= -1.8
    buyScore += 10.0
if zScore >= 1.8
    sellScore += 10.0

// 17. OHLC confluence at institutional levels (symbol-weighted)
float ohlcBuf = atr * 0.4
float ohlcPts = 5.0 * symOhlcW
if not na(pdLow) and math.abs(low - pdLow) < ohlcBuf
    buyScore += ohlcPts
if not na(pdHigh) and math.abs(high - pdHigh) < ohlcBuf
    sellScore += ohlcPts
if not na(pwLow) and math.abs(low - pwLow) < ohlcBuf
    buyScore += ohlcPts * 0.5
if not na(pwHigh) and math.abs(high - pwHigh) < ohlcBuf
    sellScore += ohlcPts * 0.5
if not na(dailyPivot) and math.abs(close - dailyPivot) < ohlcBuf
    buyScore += 3.0 * symOhlcW
    sellScore += 3.0 * symOhlcW

// 18. Institutional footprint (max 8)
if aggressiveBuy and inDiscount
    buyScore += 5.0
if aggressiveSell and inPremium
    sellScore += 5.0
if absorption and atBuyZone
    buyScore += 3.0
if absorption and atSellZone
    sellScore += 3.0

// 19. Sentiment vector — contrarian overlay (max 6)
if sentiment < -0.4 and atBuyZone
    buyScore += 6.0
if sentiment > 0.4 and atSellZone
    sellScore += 6.0

// 20. Candle pattern at zone (max 5)
if (bullPin or bullEngulf or ibBullBreak) and atBuyZone
    buyScore += 5.0
if (bearPin or bearEngulf or ibBearBreak) and atSellZone
    sellScore += 5.0

// 21. EQH/EQL stacked liquidity pools (max 4)
if nearEQL and inDiscount
    buyScore += 4.0
    buyWhy := buyWhy == "Awaiting Setup..." ? "EQL Liquidity Pool" : buyWhy + " + EQL"
if nearEQH and inPremium
    sellScore += 4.0
    sellWhy := sellWhy == "Awaiting Setup..." ? "EQH Liquidity Pool" : sellWhy + " + EQH"

// 22. Weekly open reference — two-sided (max 3)
if useWeekOpen and not na(weekOpen) and math.abs(close - weekOpen) < atr * 0.4
    buyScore += 3.0
    sellScore += 3.0

// 23. Volume value band — institutional value area (intraday, max 2)
if timeframe.isintraday
    if belowValue and inDiscount
        buyScore += 2.0
    if aboveValue and inPremium
        sellScore += 2.0

// 24. Daily Ichimoku cloud context (max 5)
if useIchimoku and not na(dayKumoTop)
    if close > dayKumoTop
        buyScore += 3.0
        buyWhy := buyWhy == "Awaiting Setup..." ? "Daily Above Kumo" : buyWhy + " + Kumo"
    else if close < dayKumoBot
        sellScore += 3.0
        sellWhy := sellWhy == "Awaiting Setup..." ? "Daily Below Kumo" : sellWhy + " + Kumo"
    if math.abs(close - dayKumoTop) <= atr * 0.3 and close >= dayKumoTop
        buyScore += 2.0
    if math.abs(close - dayKumoBot) <= atr * 0.3 and close <= dayKumoBot
        sellScore += 2.0

// 25. TK/KJ cross on current TF (max 3)
if useIchimoku
    if tkKjGold
        buyScore += 3.0
        buyWhy := buyWhy == "Awaiting Setup..." ? "TK/KJ Golden Cross" : buyWhy + " + TKX"
    if tkKjDeath
        sellScore += 3.0
        sellWhy := sellWhy == "Awaiting Setup..." ? "TK/KJ Death Cross" : sellWhy + " + TKX"

// 26. H&S neckline breaks (max 8)
if useHs
    if hsT_state == 3 and bar_index - hsT_broke <= 5
        sellScore += 8.0
        sellWhy := sellWhy == "Awaiting Setup..." ? "H&S Neckline Break" : sellWhy + " + H&S"
    if hsB_state == 3 and bar_index - hsB_broke <= 5
        buyScore += 8.0
        buyWhy := buyWhy == "Awaiting Setup..." ? "H&S Bottom Break" : buyWhy + " + H&S"
    if hsD_Tst == 3
        sellScore += 6.0
        sellWhy := sellWhy == "Awaiting Setup..." ? "Daily H&S Break" : sellWhy + " + dH&S"
    if hsD_Bst == 3
        buyScore += 6.0
        buyWhy := buyWhy == "Awaiting Setup..." ? "Daily H&S Bottom" : buyWhy + " + dH&S"

buyScore  := math.max(0.0, math.min(100.0, buyScore))
sellScore := math.max(0.0, math.min(100.0, sellScore))

// ── Distinct factor families — entry-quality gate (blocks 1-factor blowups) ──
int buyFactors = 0
if useMacro and macroValid and macroBullish
    buyFactors += 1
if bullStack
    buyFactors += 1
if close > hmaBase
    buyFactors += 1
if close > emaElastic
    buyFactors += 1
if tf3Bull
    buyFactors += 1
if tf2Bull
    buyFactors += 1
if tf1Bull
    buyFactors += 1
if validBuySweep
    buyFactors += 1
if bObHit or bObZoneGravity > 0.4
    buyFactors += 1
if bFvgHit
    buyFactors += 1
if rbsHit
    buyFactors += 1
if inDiscount
    buyFactors += 1
if adxOk
    buyFactors += 1
if macdAbove
    buyFactors += 1
if macdCross or bullDiv
    buyFactors += 1
if rsiOS and inDiscount
    buyFactors += 1
if rsiBullDiv
    buyFactors += 1
if not na(dpBuyLevel) and math.abs(close - dpBuyLevel) < atr * 0.6
    buyFactors += 1
if bullStruct
    buyFactors += 1
if timeframe.isintraday and aboveVwap
    buyFactors += 1
if (volBullDiv and inDiscount) or wyckSpring
    buyFactors += 1
if zScore <= -1.8
    buyFactors += 1
if (not na(pdLow) and math.abs(low - pdLow) < ohlcBuf) or (not na(pwLow) and math.abs(low - pwLow) < ohlcBuf) or (not na(dailyPivot) and math.abs(close - dailyPivot) < ohlcBuf)
    buyFactors += 1
if (aggressiveBuy and inDiscount) or (absorption and atBuyZone)
    buyFactors += 1
if sentiment < -0.4 and atBuyZone
    buyFactors += 1
if (bullPin or bullEngulf or ibBullBreak) and atBuyZone
    buyFactors += 1
if nearEQL and inDiscount
    buyFactors += 1
if useWeekOpen and not na(weekOpen) and math.abs(close - weekOpen) < atr * 0.4
    buyFactors += 1
if timeframe.isintraday and belowValue and inDiscount
    buyFactors += 1
if useIchimoku and not na(dayKumoTop) and close > dayKumoTop
    buyFactors += 1
if useIchimoku and tkKjGold
    buyFactors += 1
if useHs and hsB_state == 3 and bar_index - hsB_broke <= 5
    buyFactors += 1
if useHs and hsD_Bst == 3
    buyFactors += 1

int sellFactors = 0
if useMacro and macroValid and macroBearish
    sellFactors += 1
if bearStack
    sellFactors += 1
if close < hmaBase
    sellFactors += 1
if close < emaElastic
    sellFactors += 1
if tf3Bear
    sellFactors += 1
if tf2Bear
    sellFactors += 1
if tf1Bear
    sellFactors += 1
if validSellSweep
    sellFactors += 1
if rObHit or rObZoneGravity > 0.4
    sellFactors += 1
if rFvgHit
    sellFactors += 1
if sbrHit
    sellFactors += 1
if inPremium
    sellFactors += 1
if adxOk
    sellFactors += 1
if macdBelow
    sellFactors += 1
if macdDeadX or bearDiv
    sellFactors += 1
if rsiOB and inPremium
    sellFactors += 1
if rsiBearDiv
    sellFactors += 1
if not na(dpSellLevel) and math.abs(close - dpSellLevel) < atr * 0.6
    sellFactors += 1
if bearStruct
    sellFactors += 1
if timeframe.isintraday and not aboveVwap
    sellFactors += 1
if (volBearDiv and inPremium) or wyckUTAD
    sellFactors += 1
if zScore >= 1.8
    sellFactors += 1
if (not na(pdHigh) and math.abs(high - pdHigh) < ohlcBuf) or (not na(pwHigh) and math.abs(high - pwHigh) < ohlcBuf) or (not na(dailyPivot) and math.abs(close - dailyPivot) < ohlcBuf)
    sellFactors += 1
if (aggressiveSell and inPremium) or (absorption and atSellZone)
    sellFactors += 1
if sentiment > 0.4 and atSellZone
    sellFactors += 1
if (bearPin or bearEngulf or ibBearBreak) and atSellZone
    sellFactors += 1
if nearEQH and inPremium
    sellFactors += 1
if useWeekOpen and not na(weekOpen) and math.abs(close - weekOpen) < atr * 0.4
    sellFactors += 1
if timeframe.isintraday and aboveValue and inPremium
    sellFactors += 1
if useIchimoku and not na(dayKumoBot) and close < dayKumoBot
    sellFactors += 1
if useIchimoku and tkKjDeath
    sellFactors += 1
if useHs and hsT_state == 3 and bar_index - hsT_broke <= 5
    sellFactors += 1
if useHs and hsD_Tst == 3
    sellFactors += 1

bool buyGateOk  = not useFactorGate or buyFactors >= minFactors
bool sellGateOk = not useFactorGate or sellFactors >= minFactors

getGrade(float score, float thresh) =>
    float diff = score - thresh
    diff >= 20.0 ? "A+ GOD" : diff >= 10.0 ? "A APEX" : diff >= 0.0 ? "B PRIME" : "C WEAK"

// ── Counter-bias & regime threshold gates ──
bool strongBear = biasScore == -3
bool strongBull = biasScore == 3
float buyThresh  = dynThreshold
float sellThresh = dynThreshold
if userRisk != RiskProfile.Aggressive
    if strongBear
        buyThresh := math.min(buyThresh + 8.0, 95.0)
    if strongBull
        sellThresh := math.min(sellThresh + 8.0, 95.0)
    if regimeStr == "🌀 CHOPPY" or lowVol
        buyThresh := math.min(buyThresh + 5.0, 95.0)
        sellThresh := math.min(sellThresh + 5.0, 95.0)
if regimeVolatile
    buyThresh := math.max(buyThresh - 5.0, 50.0)
    sellThresh := math.max(sellThresh - 5.0, 50.0)
if useBtcWeekend and btcWeekend
    buyThresh := math.min(buyThresh + 3.0, 95.0)
    sellThresh := math.min(sellThresh + 3.0, 95.0)

bool isCounterBiasBuy  = overallBias == "BEARISH" and buyScore >= buyThresh
bool isCounterBiasSell = overallBias == "BULLISH" and sellScore >= sellThresh

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 19: TRADE STATE MACHINE, SIZING & HYDRA TRAIL       ║
// ╚═══════════════════════════════════════════════════════════════════╝

var int    tradeState = 0
var float  entryPx    = na
var float  slPx       = na
var float  tp1Px      = na
var float  tp2Px      = na
var float  tp3Px      = na
var float  riskR      = na
var bool   tp1Done    = false
var bool   beActive   = false
var int    tradeBars  = 0
var int    hydraPhase = 0
var float  maxProfitR = 0.0
var string catalyst   = "—"
var bool   isCounterBias = false
var float  peakConv    = 0.0
var int    convLowBars = 0
var bool   convSoft    = false
var box    tpViz      = na
var box    slViz      = na
var line   entryViz   = na

// Position sizing
float slDist = math.abs(nz(entryPx, close) - nz(slPx, close))
float riskDollars = accBalance * (riskPct / 100.0)
float posUnits = slDist > 0 ? riskDollars / (slDist * math.max(tickValue, 0.001)) : 0.0

bool canTrade = not cautionMode or userRisk == RiskProfile.Aggressive
float dynSlBuf = (regimeVolatile ? slMult * 1.5 : slMult) * (useBtcWeekend and btcWeekend ? 1.15 : 1.0)
bool spikeBlocked = useSpikeShield and atrRatio > spikeAtrMult

// ╔══════════════════════════════════════════════════════════════════╗
// ║  V-REVERSAL DETECTOR — spike → reclaim (the gold "V")           ║
// ║  vBot/vTop = aggressive V-reversal · vBotBreak/vTopBreak =      ║
// ║  V-then-breakout (close reclaims the pre-spike extreme)         ║
// ╚══════════════════════════════════════════════════════════════════╝
int    vLook     = 12
float  vSpikeA   = 2.5
int    vWin      = 3
float  volC      = volSma > 0 ? volume / volSma : 1.0
float  vPreHigh  = ta.highest(high, vLook + vWin)
float  vPreLow   = ta.lowest(low, vLook + vWin)
float  vSpikeBot = ta.lowest(low, vLook)
float  vSpikeTop = ta.highest(high, vLook)
float  vDrop     = vPreHigh - vSpikeBot
float  vPump     = vSpikeTop - vPreLow
int    barsBot   = ta.barssince(low <= vSpikeBot)
int    barsTop   = ta.barssince(high >= vSpikeTop)
bool   vBotTrig  = vDrop >= vSpikeA * atr and barsBot >= 1 and barsBot <= vWin and close - vSpikeBot >= 0.5 * vDrop and close > open and volC >= 1.2
bool   vTopTrig  = vPump >= vSpikeA * atr and barsTop >= 1 and barsTop <= vWin and vSpikeTop - close >= 0.5 * vPump and close < open and volC >= 1.2
bool   vBotBreak = vDrop >= vSpikeA * atr and barsBot >= 1 and barsBot <= vWin and close > vPreHigh and volC >= 1.2
bool   vTopBreak = vPump >= vSpikeA * atr and barsTop >= 1 and barsTop <= vWin and close < vPreLow and volC >= 1.2

// ── SESSION SENSE — instrument-aware clock (UTC exchange time) ──
bool isWeekend     = isBTC and (dayofweek(time) == 0 or dayofweek(time) == 6)
bool isGoldWeekday = not isBTC and dayofweek(time) >= 1 and dayofweek(time) <= 5
bool isAsia        = isGoldWeekday and hour(time) < 6
bool isLondon      = isGoldWeekday and hour(time) >= 7 and hour(time) < 12
bool isGoldActive  = isGoldWeekday and hour(time) >= 7 and hour(time) < 17

// ╔══════════════════════════════════════════════════════════════════╗
// ║  LINE GEOMETRY — trendlines · triangles · wedges · ranges       ║
// ║  swing pivots → least-squares lines → pattern + breakout        ║
// ╚══════════════════════════════════════════════════════════════════╝
// Least-squares fit over the last `cnt` pivots. x = bars since oldest fit pivot.
// Returns [slope per bar, value at oldest pivot, mean |residual|]
f_fitLine(array<float> P, array<int> B, int cnt) =>
    int start = array.size(P) - cnt
    float sx = 0.0
    float sy = 0.0
    float sxy = 0.0
    float sxx = 0.0
    int x0 = array.get(B, start)
    for i = start to array.size(P) - 1
        float x = array.get(B, i) - x0
        float y = array.get(P, i)
        sx += x
        sy += y
        sxy += x * y
        sxx += x * x
    float slope = 0.0
    float b0 = 0.0
    float denom = cnt * sxx - sx * sx
    if denom > 0
        slope := (cnt * sxy - sx * sy) / denom
        b0 := (sy - slope * sx) / cnt
    float resid = 0.0
    for i = start to array.size(P) - 1
        float x = array.get(B, i) - x0
        resid += math.abs(array.get(P, i) - (slope * x + b0))
    [slope, b0, resid / cnt]

var array<float> geoPlP = array.new<float>()
var array<int>   geoPlB = array.new<int>()
var array<float> geoPhP = array.new<float>()
var array<int>   geoPhB = array.new<int>()
float pivHi = ta.pivothigh(3, 3)
float pivLo = ta.pivotlow(3, 3)
if barstate.isconfirmed
    if not na(pivHi)
        array.push(geoPhP, pivHi)
        array.push(geoPhB, bar_index - 3)
        if array.size(geoPhP) > 8
            array.shift(geoPhP)
            array.shift(geoPhB)
    if not na(pivLo)
        array.push(geoPlP, pivLo)
        array.push(geoPlB, bar_index - 3)
        if array.size(geoPlP) > 8
            array.shift(geoPlP)
            array.shift(geoPlB)

int PAT_NONE  = 0
int PAT_TRI   = 1
int PAT_WDOW  = 2
int PAT_WUP   = 3
int PAT_ATRI  = 4
int PAT_DTRI  = 5
int PAT_RANGE = 6

bool  hasSup = array.size(geoPlP) >= 3
bool  hasRes = array.size(geoPhP) >= 3
float supSlope = na
float supNow   = na
float supResid = na
if hasSup
    int scnt = math.min(4, array.size(geoPlP))
    [s0, b0, r0] = f_fitLine(geoPlP, geoPlB, scnt)
    supSlope := s0
    supNow   := s0 * (bar_index - array.get(geoPlB, array.size(geoPlP) - scnt)) + b0
    supResid := r0
bool supClean = hasSup and supResid < atr * 0.75
bool supTrend = supClean and supSlope > atr * 0.05
bool supTest  = hasSup and low <= supNow and close > supNow

float resSlope = na
float resNow   = na
float resResid = na
if hasRes
    int rcnt = math.min(4, array.size(geoPhP))
    [s1, b1, r1] = f_fitLine(geoPhP, geoPhB, rcnt)
    resSlope := s1
    resNow   := s1 * (bar_index - array.get(geoPhP, array.size(geoPhP) - rcnt)) + b1
    resResid := r1
bool resClean   = hasRes and resResid < atr * 0.75
bool resTrendUp = resClean and resSlope > atr * 0.05
bool resTest    = hasRes and high >= resNow and close < resNow

float gapNow   = hasSup and hasRes and supClean and resClean ? resNow - supNow : na
float gapSlope = hasSup and hasRes ? resSlope - supSlope : na
float barsApex = not na(gapNow) and not na(gapSlope) and gapSlope < 0 and gapNow > 0 ? gapNow / -gapSlope : na

int pattern = PAT_NONE
if hasSup and hasRes and supClean and resClean and gapNow > atr * 0.5
    bool supRise = supSlope > atr * 0.02
    bool supFall = supSlope < -atr * 0.02
    bool resRise = resSlope > atr * 0.02
    bool resFall = resSlope < -atr * 0.02
    if not (supRise or supFall) and not (resRise or resFall)
        pattern := PAT_RANGE
    else if gapSlope < -atr * 0.01 and not na(barsApex) and barsApex >= 5 and barsApex <= 150
        if not (resRise or resFall) and supRise
            pattern := PAT_ATRI
        else if not (supRise or supFall) and resFall
            pattern := PAT_DTRI
        else if resFall and supRise
            pattern := PAT_TRI
        else if resFall and supFall
            pattern := PAT_WDOW
        else if resRise and supRise
            pattern := PAT_WUP
string patternName = "—"
if pattern == PAT_TRI
    patternName := "SYM TRIANGLE"
else if pattern == PAT_WDOW
    patternName := "FALLING WEDGE"
else if pattern == PAT_WUP
    patternName := "RISING WEDGE"
else if pattern == PAT_ATRI
    patternName := "ASCENDING TRI"
else if pattern == PAT_DTRI
    patternName := "DESCENDING TRI"
else if pattern == PAT_RANGE
    patternName := "RANGE BOX"

bool patBreakLong  = pattern != PAT_NONE and close > resNow and close[1] < resNow - resSlope and volume > volSma
bool patBreakShort = pattern != PAT_NONE and close < supNow and close[1] > supNow - supSlope and volume > volSma

// ── ASIA RANGE BREAK — gold consolidates Asia (00–06 UTC), breaks in London ──
var float asiaHi  = na
var float asiaLo  = na
var int   asiaDay = 1
int dNow = dayofmonth(time)
if dNow != asiaDay
    asiaHi := na
    asiaLo := na
    asiaDay := dNow
if isAsia
    asiaHi := na(asiaHi) ? high : math.max(asiaHi, high)
    asiaLo := na(asiaLo) ? low : math.min(asiaLo, low)
bool asiaValid      = isGoldWeekday and not na(asiaHi) and asiaHi > asiaLo
bool asiaBreakLong  = asiaValid and isLondon and close > asiaHi and close[1] <= asiaHi and volume > volSma
bool asiaBreakShort = asiaValid and isLondon and close < asiaLo and close[1] >= asiaLo and volume > volSma

// Optional geometry overlay (off by default)
var line geoSupL = na
var line geoResL = na
if showGeoLines
    if hasSup and supClean
        if na(geoSupL)
            geoSupL := line.new(bar_index - 20, supNow - supSlope * 20, bar_index, supNow, color=color.new(color.green, 25), width=1)
        else
            line.set_xy1(geoSupL, bar_index - 20, supNow - supSlope * 20)
            line.set_xy2(geoSupL, bar_index, supNow)
    else if not na(geoSupL)
        line.delete(geoSupL)
        geoSupL := na
    if hasRes and resClean
        if na(geoResL)
            geoResL := line.new(bar_index - 20, resNow - resSlope * 20, bar_index, resNow, color=color.new(color.red, 25), width=1)
        else
            line.set_xy1(geoResL, bar_index - 20, resNow - resSlope * 20)
            line.set_xy2(geoResL, bar_index, resNow)
    else if not na(geoResL)
        line.delete(geoResL)
        geoResL := na

// ╔══════════════════════════════════════════════════════════════════╗
// ║  STRATEGY ROUTER — picks the playbook per market state          ║
// ║  0 FLAT · 1 MOMENTUM · 2 MEAN-REV · 3 POST-SPIKE · 4 BREAKOUT   ║
// ╚══════════════════════════════════════════════════════════════════╝
int M_FLAT = 0
int M_MOM  = 1
int M_MEAN = 2
int M_POST = 3
int M_BRK  = 4
int M_LEG  = 5  // legacy single-strategy (useRouter off)

// Per-mode playbook rules. Returns [allowLong, allowShort, allowCounter, tp1R, tp2R, tp3R, slCapAtr, runnerAtr, threshOff]
f_modeRules(int m, float t1, float t2, string rg) =>
    bool aL = true
    bool aS = true
    bool ctr = true
    float f1 = t1
    float f2 = t2
    float f3 = t2 + 1.5
    float cap = 2.0
    float run = 1.2
    float off = 0.0
    if m == M_MOM
        ctr := false
        aL := rg == "▲ TRENDING BULL"
        aS := rg == "▼ TRENDING BEAR"
    else if m == M_MEAN
        f1 := math.min(f1, 0.75)
        f2 := math.min(f2, 1.5)
        f3 := f2 + 1.0
        cap := 1.5
        run := 0.8
        off := -5.0
    else if m == M_POST
        f1 := math.min(f1, 0.5)
        f2 := math.min(f2, 1.0)
        f3 := f2 + 0.75
        cap := 1.25
        run := 1.0
        off := -5.0
    else if m == M_BRK
        f1 := math.min(f1, 1.0)
        f2 := math.min(f2, 2.0)
        f3 := f2 + 1.0
        cap := 1.5
        run := 1.0
        off := -10.0
    [aL, aS, ctr, f1, f2, f3, cap, run, off]

float range30   = ta.highest(high, 30) - ta.lowest(low, 30)
bool  squeeze30 = range30 < atr * 6.0
bool  adxUp     = adxLine > nz(adxLine[3])

int candMode = M_FLAT
if useRouter
    if not (volOk and sessionOk and canTrade)
        candMode := M_FLAT
    else if atrRatio > spikeAtrMult
        candMode := M_POST
    else if vBotTrig or vTopTrig or vBotBreak or vTopBreak
        candMode := M_POST
    else if patBreakLong or patBreakShort
        candMode := M_BRK
    else if asiaBreakLong or asiaBreakShort
        candMode := M_BRK
    else if (squeeze30 and adxUp) and f_modeViable(M_BRK)
        candMode := M_BRK
    else if (regimeStr == "▲ TRENDING BULL" or regimeStr == "▼ TRENDING BEAR") and adxOk and f_modeViable(M_MOM)
        candMode := M_MOM
    else if f_modeViable(M_MEAN)
        candMode := M_MEAN
    else
        candMode := M_LEG
else
    candMode := volOk and sessionOk and canTrade ? M_LEG : M_FLAT

var int mode = 0
var int modeCand = 0
var int modeHold = 0
if barstate.isconfirmed
    modeHold := candMode == modeCand ? modeHold + 1 : 1
    if candMode != modeCand
        modeCand := candMode
    if modeHold >= modeDwell and modeCand != mode
        mode := modeCand

string modeName = "LEGACY"
if mode == M_FLAT
    modeName := "FLAT (no-trade)"
else if mode == M_MOM
    modeName := "MOMENTUM"
else if mode == M_MEAN
    modeName := "MEAN-REV"
else if mode == M_POST
    modeName := "POST-SPIKE"
else if mode == M_BRK
    modeName := "BREAKOUT"

// Playbook gate: does the active mode's setup agree with this side right now?
f_modeSignal(int m, bool isLong) =>
    bool ok = true
    if m == M_MOM
        float val = timeframe.isintraday ? vwapVal : hmaBase
        ok := isLong ? (low[1] <= val + atr * 0.75 or low[2] <= val + atr * 0.75 or low[3] <= val + atr * 0.75) : (high[1] >= val - atr * 0.75 or high[2] >= val - atr * 0.75 or high[3] >= val - atr * 0.75)
    else if m == M_MEAN
        ok := isLong ? (inDiscount and (validBuySweep or atBuyZone or rsiOS)) : (inPremium and (validSellSweep or atSellZone or rsiOB))
    else if m == M_POST
        ok := isLong ? (vBotTrig or vBotBreak) : (vTopTrig or vTopBreak)
    else if m == M_BRK
        ok := isLong ? ((close > ta.highest(high, 30)[1] and volume > volSma) or patBreakLong or asiaBreakLong) : ((close < ta.lowest(low, 30)[1] and volume > volSma) or patBreakShort or asiaBreakShort)
    ok

if useRouter and alertOnMode and barstate.isconfirmed and mode != mode[1]
    alert("🧭 MODE → " + modeName + " | " + syminfo.ticker, alert.freq_once_per_bar)

// ── GOD CONVICTION ENGINE — live directional confluence (0..100) ──
// (uses existing pdHigh/pdLow prev-day levels declared above)
f_conviction(bool isLong) =>
    float c = 50.0
    if isGoldActive
        c += 3.0
    if isWeekend
        c -= 5.0
    if isLong
        if regimeStr == "▲ TRENDING BULL"
            c += isBTC ? 14.0 : 12.0
        if regimeStr == "▼ TRENDING BEAR"
            c -= 12.0
        if close > (timeframe.isintraday ? vwapVal : hmaBase)
            c += 8.0
        else
            c -= 5.0
    else
        if regimeStr == "▼ TRENDING BEAR"
            c += isBTC ? 14.0 : 12.0
        if regimeStr == "▲ TRENDING BULL"
            c -= 12.0
        if close < (timeframe.isintraday ? vwapVal : hmaBase)
            c += 8.0
        else
            c -= 5.0
    if adxOk and adxLine > nz(adxLine[3])
        c += 8.0
    if isLong
        if emaF > emaS
            c += 8.0
        else
            c -= 8.0
    else
        if emaF < emaS
            c += 8.0
        else
            c -= 8.0
    if isLong
        if macroBullish
            c += 8.0
        if macroBearish
            c -= 8.0
    else
        if macroBearish
            c += 8.0
        if macroBullish
            c -= 8.0
    if isLong
        if validBuySweep or vBotTrig
            c += 8.0
        if validSellSweep
            c -= 10.0
    else
        if validSellSweep or vTopTrig
            c += 8.0
        if validBuySweep
            c -= 10.0
    if isLong
        if hsB_state >= 2
            c += 8.0
        if hsT_state >= 2
            c -= 8.0
    else
        if hsT_state >= 2
            c += 8.0
        if hsB_state >= 2
            c -= 8.0
    if isLong
        if rsiOS
            c += 5.0
        if rsiOB
            c -= 5.0
    else
        if rsiOB
            c += 5.0
        if rsiOS
            c -= 5.0
    if isLong
        if supTrend and supTest
            c += 8.0
        if patBreakLong
            c += 8.0
        if asiaBreakLong
            c += 8.0
        if supTrend and close < supNow
            c -= 10.0
        if resClean and resSlope < 0 and high >= resNow and close < resNow
            c -= 8.0
        if pattern == PAT_WDOW
            c += 5.0
        if pattern == PAT_WUP
            c -= 5.0
    else
        if resTrendUp and resTest
            c += 8.0
        if patBreakShort
            c += 8.0
        if asiaBreakShort
            c += 8.0
        if resTrendUp and close > resNow
            c -= 10.0
        if supClean and supSlope > 0 and low <= supNow and close > supNow
            c -= 8.0
        if pattern == PAT_WUP
            c += 5.0
        if pattern == PAT_WDOW
            c -= 5.0
    float rndStep = isBTC ? 500.0 : 100.0
    float rnd = math.round(close / rndStep) * rndStep
    if math.abs(close - rnd) < atr * 0.5
        if isLong and close < rnd
            c -= 5.0
        if isLong and close > rnd
            c += 5.0
        if not isLong and close > rnd
            c -= 5.0
        if not isLong and close < rnd
            c += 5.0
    if not na(pdHigh) and not na(pdLow)
        if isLong and close > pdHigh
            c += 5.0
        if isLong and close < pdLow
            c -= 5.0
        if not isLong and close < pdLow
            c += 5.0
        if not isLong and close > pdHigh
            c -= 5.0
    if not na(pwHigh) and not na(pwLow)
        if isLong and close > pwHigh
            c += 5.0
        if isLong and close < pwLow
            c -= 5.0
        if not isLong and close < pwLow
            c += 5.0
        if not isLong and close > pwHigh
            c -= 5.0
    math.max(0.0, math.min(100.0, c))

float convNow = tradeState == 1 ? f_conviction(true) : tradeState == -1 ? f_conviction(false) : f_conviction(buyScore >= sellScore)

bool goLong  = false
bool goShort = false

[modeAL, modeAS, modeCTR, modeT1, modeT2, modeT3, modeCap, modeRun, modeOff] = f_modeRules(useRouter ? mode : M_LEG, tp1RR, tp2RR, regimeStr)
float modeThreshB = math.max(math.min(buyThresh + modeOff + f_modeThreshAdj(mode), 95.0), 50.0)
float modeThreshS = math.max(math.min(sellThresh + modeOff + f_modeThreshAdj(mode), 95.0), 50.0)
var float posRunAtr = 1.2

if tradeState == 0 and barstate.isconfirmed and volOk and sessionOk and canTrade
    bool modeOpen  = useRouter ? mode > M_FLAT : true
    bool playLong  = useRouter ? f_modeSignal(mode, true) : true
    bool playShort = useRouter ? f_modeSignal(mode, false) : true
    bool spikeOk2  = (useRouter and mode == M_POST) or not spikeBlocked
    bool buyOk  = modeAL and modeOpen and playLong  and spikeOk2 and buyScore  >= modeThreshB and buyGateOk
    bool sellOk = modeAS and modeOpen and playShort and spikeOk2 and sellScore >= modeThreshS and sellGateOk
    if not modeCTR
        if isCounterBiasBuy
            buyOk := false
        if isCounterBiasSell
            sellOk := false
    if buyOk and (not sellOk or buyScore >= sellScore + 5.0)
        goLong := true
    else if sellOk and (not buyOk or sellScore >= buyScore + 5.0)
        goShort := true

// ── LONG ENTRY ──
if goLong
    tradeState := 1
    entryPx := close
    slPx := math.max(recentSwingLow - atr * dynSlBuf * symSlBuf, close - atr * modeCap)
    riskR := entryPx - slPx
    tp1Px := entryPx + riskR * modeT1
    tp2Px := entryPx + riskR * modeT2
    tp3Px := usePVS ? entryPx + riskR * modeT3 : na
    posRunAtr := modeRun
    peakConv := convNow
    convLowBars := 0
    convSoft := false
    tp1Done := false
    beActive := false
    tradeBars := 0
    hydraPhase := 0
    maxProfitR := 0.0
    catalyst := buyWhy
    tradeMode := mode
    isCounterBias := isCounterBiasBuy
    if not na(tpViz)
        box.delete(tpViz)
    if not na(slViz)
        box.delete(slViz)
    if not na(entryViz)
        line.delete(entryViz)
    tpViz := box.new(bar_index, usePVS ? tp3Px : tp2Px, bar_index + 1, entryPx, bgcolor=color.new(color.teal, 88), border_color=color.new(color.teal, 35))
    slViz := box.new(bar_index, entryPx, bar_index + 1, slPx, bgcolor=color.new(color.red, 88), border_color=color.new(color.red, 35))
    entryViz := line.new(bar_index - 3, entryPx, bar_index + 1, entryPx, color=color.new(color.white, 40), style=line.style_dashed)
    string grNote = "\n[" + getGrade(buyScore, buyThresh) + "]"
    string cbNote = isCounterBiasBuy ? "\n⚡ Counter-Bias" : ""
    string tp3Str = usePVS ? "\nTP3: " + str.tostring(tp3Px, "#.##") : ""
    label.new(bar_index, slPx - atr * 1.5,
        text="🟢 LONG\nUnits: " + str.tostring(posUnits, "#.##") + "\nEntry: " + str.tostring(entryPx, "#.##") + "\nSL: " + str.tostring(slPx, "#.##") + "\nTP1: " + str.tostring(tp1Px, "#.##") + "\nTP2: " + str.tostring(tp2Px, "#.##") + tp3Str + "\n" + buyWhy + grNote + cbNote + "\nScore: " + str.tostring(buyScore, "#") + "%\n🧭 " + modeName,
        color=color.rgb(8, 38, 18), textcolor=color.white, style=label.style_label_up, size=size.normal)
    if alertOnEntry
        alert("🟢 LONG | " + syminfo.ticker + (isCounterBiasBuy ? " ⚡COUNTER" : "") + " | Entry: " + str.tostring(entryPx, "#.##") + " | Units: " + str.tostring(posUnits, "#.##") + " | " + buyWhy, alert.freq_once_per_bar)

// ── SHORT ENTRY ──
if goShort
    tradeState := -1
    entryPx := close
    slPx := math.min(recentSwingHigh + atr * dynSlBuf * symSlBuf, close + atr * modeCap)
    riskR := slPx - entryPx
    tp1Px := entryPx - riskR * modeT1
    tp2Px := entryPx - riskR * modeT2
    tp3Px := usePVS ? entryPx - riskR * modeT3 : na
    posRunAtr := modeRun
    peakConv := convNow
    convLowBars := 0
    convSoft := false
    tp1Done := false
    beActive := false
    tradeBars := 0
    hydraPhase := 0
    maxProfitR := 0.0
    catalyst := sellWhy
    tradeMode := mode
    isCounterBias := isCounterBiasSell
    if not na(tpViz)
        box.delete(tpViz)
    if not na(slViz)
        box.delete(slViz)
    if not na(entryViz)
        line.delete(entryViz)
    tpViz := box.new(bar_index, entryPx, bar_index + 1, usePVS ? tp3Px : tp2Px, bgcolor=color.new(color.teal, 88), border_color=color.new(color.teal, 35))
    slViz := box.new(bar_index, slPx, bar_index + 1, entryPx, bgcolor=color.new(color.red, 88), border_color=color.new(color.red, 35))
    entryViz := line.new(bar_index - 3, entryPx, bar_index + 1, entryPx, color=color.new(color.white, 40), style=line.style_dashed)
    string grNote = "\n[" + getGrade(sellScore, sellThresh) + "]"
    string cbNote = isCounterBiasSell ? "\n⚡ Counter-Bias" : ""
    string tp3Str = usePVS ? "\nTP3: " + str.tostring(tp3Px, "#.##") : ""
    label.new(bar_index, slPx + atr * 1.5,
        text="🔴 SHORT\nUnits: " + str.tostring(posUnits, "#.##") + "\nEntry: " + str.tostring(entryPx, "#.##") + "\nSL: " + str.tostring(slPx, "#.##") + "\nTP1: " + str.tostring(tp1Px, "#.##") + "\nTP2: " + str.tostring(tp2Px, "#.##") + tp3Str + "\n" + sellWhy + grNote + cbNote + "\nScore: " + str.tostring(sellScore, "#") + "%\n🧭 " + modeName,
        color=color.rgb(38, 8, 8), textcolor=color.white, style=label.style_label_down, size=size.normal)
    if alertOnEntry
        alert("🔴 SHORT | " + syminfo.ticker + (isCounterBiasSell ? " ⚡COUNTER" : "") + " | Entry: " + str.tostring(entryPx, "#.##") + " | Units: " + str.tostring(posUnits, "#.##") + " | " + sellWhy, alert.freq_once_per_bar)

// ── GOD CONVICTION — LONG (live soft/hard invalidation + stop adaptation) ──
if tradeState == 1 and barstate.isconfirmed
    if convNow > peakConv
        peakConv := convNow
    convLowBars := convNow < 20.0 ? convLowBars + 1 : 0
    if convNow < 40.0
        float softTarget = entryPx - 0.5 * riskR
        if softTarget > slPx
            slPx := softTarget
            convSoft := true
            if not na(slViz)
                box.set_bottom(slViz, slPx)
    if peakConv - convNow >= 20.0 and close > entryPx
        float midPx = (entryPx + close) / 2
        if midPx > slPx
            slPx := midPx
            if not na(slViz)
                box.set_bottom(slViz, slPx)
    if convLowBars >= 2
        tradeState := 0
        f_record_trade(beActive)
        label.new(bar_index, close, "❄ INVALIDATED — Conv " + str.tostring(convNow, "#"), color=color.new(color.purple, 10), textcolor=color.white, style=label.style_label_up, size=size.small)
        if alertOnExit
            alert("❄ CONVICTION EXIT | LONG | " + syminfo.ticker + " @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)

// ── LONG POSITION MANAGEMENT ──
string exitNote = ""

if tradeState == 1 and barstate.isconfirmed
    tradeBars += 1
    float curR = (close - entryPx) / math.max(riskR, 0.001)
    if curR > maxProfitR
        maxProfitR := curR
    // HYDRA 4-phase trailing
    if useHydra and riskR > 0
        if not tp1Done
            if curR >= 0.3
                float tPrice = close - atr * trailATR
                if tPrice > slPx
                    slPx := tPrice
                    hydraPhase := 1
        else
            if not beActive
                beActive := true
                slPx := entryPx
                hydraPhase := 2
            else
                if curR >= 1.5
                    float runner = close - atr * slMult * posRunAtr
                    if runner > slPx
                        slPx := runner
                        hydraPhase := 3
                    // Profit lock: after +1.5R, never give back more than 0.5R from the peak
                    if useProfitLock and maxProfitR >= 1.5
                        float lockPx = entryPx + (maxProfitR - 0.5) * riskR
                        if lockPx > slPx
                            slPx := lockPx
                else if moveBEat50 and curR >= 0.5 and slPx < entryPx
                    slPx := entryPx
                    hydraPhase := 2
    if not na(tpViz)
        box.set_right(tpViz, bar_index + 1)
    if not na(slViz)
        box.set_right(slViz, bar_index + 1)
        box.set_bottom(slViz, slPx)
    if not na(entryViz)
        line.set_x2(entryViz, bar_index + 1)
    // Intelligent BE: pre-TP1 defense at +0.75R on strong conviction
    if not tp1Done and close - entryPx >= riskR * 0.75 and convNow >= 60.0 and slPx < entryPx
        slPx := entryPx + 0.1 * riskR
        beActive := true
        if not na(slViz)
            box.set_bottom(slViz, slPx)
    if not tp1Done and high >= tp1Px
        tp1Done := true
        beActive := true
        if convNow >= 70.0
            slPx := entryPx + 0.25 * riskR
        else if convNow >= 50.0
            slPx := entryPx + beOffsetR * riskR
        else
            slPx := entryPx
        if not na(slViz)
            box.set_bottom(slViz, slPx)
        label.new(bar_index, tp1Px + atr * 0.3, "✅ TP1 + BE Lock", color=color.new(color.teal, 20), textcolor=color.white, style=label.style_label_down, size=size.tiny)
    // Exit ladder — if/else-if so TP and SL can never both fire on one bar
    if usePVS
        if high >= tp3Px
            tradeState := 0
            exitNote := "TP3 HIT 🚀"
            f_record_trade(true)
            label.new(bar_index, tp3Px + atr * 0.5, "🎯 TP3 HIT", color=color.new(color.teal, 10), textcolor=color.white, style=label.style_label_down, size=size.normal)
            if alertOnExit
                alert("🚀 TP3 | " + syminfo.ticker + " LONG @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)
        else if low <= slPx
            tradeState := 0
            exitNote := beActive ? "STOPPED AT BE 🛡️" : "STOP LOSS ❌"
            f_record_trade(beActive)
            label.new(bar_index, slPx - atr * 0.5, exitNote, color=color.new(color.red, 10), textcolor=color.white, style=label.style_label_up, size=size.normal)
            if alertOnExit
                alert(exitNote + " | " + syminfo.ticker + " LONG @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)
        else if high >= tp2Px
            label.new(bar_index, tp2Px + atr * 0.3, "✅ TP2 (runner on)", color=color.new(color.teal, 20), textcolor=color.white, style=label.style_label_down, size=size.tiny)
    else
        if high >= tp2Px
            tradeState := 0
            exitNote := "TP2 HIT ✅"
            f_record_trade(true)
            label.new(bar_index, tp2Px + atr * 0.5, "🎯 TP2 HIT", color=color.new(color.teal, 10), textcolor=color.white, style=label.style_label_down, size=size.normal)
            if alertOnExit
                alert("🎯 TP2 | " + syminfo.ticker + " LONG @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)
        else if low <= slPx
            tradeState := 0
            exitNote := beActive ? "STOPPED AT BE 🛡️" : "STOP LOSS ❌"
            f_record_trade(beActive)
            label.new(bar_index, slPx - atr * 0.5, exitNote, color=color.new(color.red, 10), textcolor=color.white, style=label.style_label_up, size=size.normal)
            if alertOnExit
                alert(exitNote + " | " + syminfo.ticker + " LONG @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)

// ── GOD CONVICTION — SHORT (live soft/hard invalidation + stop adaptation) ──
if tradeState == -1 and barstate.isconfirmed
    if convNow > peakConv
        peakConv := convNow
    convLowBars := convNow < 20.0 ? convLowBars + 1 : 0
    if convNow < 40.0
        float softTarget = entryPx + 0.5 * riskR
        if softTarget < slPx
            slPx := softTarget
            convSoft := true
            if not na(slViz)
                box.set_top(slViz, slPx)
    if peakConv - convNow >= 20.0 and close < entryPx
        float midPx = (entryPx + close) / 2
        if midPx < slPx
            slPx := midPx
            if not na(slViz)
                box.set_top(slViz, slPx)
    if convLowBars >= 2
        tradeState := 0
        f_record_trade(beActive)
        label.new(bar_index, close, "❄ INVALIDATED — Conv " + str.tostring(convNow, "#"), color=color.new(color.purple, 10), textcolor=color.white, style=label.style_label_down, size=size.small)
        if alertOnExit
            alert("❄ CONVICTION EXIT | SHORT | " + syminfo.ticker + " @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)

// ── SHORT POSITION MANAGEMENT ──
if tradeState == -1 and barstate.isconfirmed
    tradeBars += 1
    float curR = (entryPx - close) / math.max(riskR, 0.001)
    if curR > maxProfitR
        maxProfitR := curR
    // HYDRA 4-phase trailing
    if useHydra and riskR > 0
        if not tp1Done
            if curR >= 0.3
                float tPrice = close + atr * trailATR
                if tPrice < slPx
                    slPx := tPrice
                    hydraPhase := 1
        else
            if not beActive
                beActive := true
                slPx := entryPx
                hydraPhase := 2
            else
                if curR >= 1.5
                    float runner = close + atr * slMult * posRunAtr
                    if runner < slPx
                        slPx := runner
                        hydraPhase := 3
                    // Profit lock: after +1.5R, never give back more than 0.5R from the peak
                    if useProfitLock and maxProfitR >= 1.5
                        float lockPx = entryPx - (maxProfitR - 0.5) * riskR
                        if lockPx < slPx
                            slPx := lockPx
                else if moveBEat50 and curR >= 0.5 and slPx > entryPx
                    slPx := entryPx
                    hydraPhase := 2
    if not na(tpViz)
        box.set_right(tpViz, bar_index + 1)
    if not na(slViz)
        box.set_right(slViz, bar_index + 1)
        box.set_top(slViz, slPx)
    if not na(entryViz)
        line.set_x2(entryViz, bar_index + 1)
    // Intelligent BE: pre-TP1 defense at -0.75R on strong conviction
    if not tp1Done and entryPx - close >= riskR * 0.75 and convNow >= 60.0 and slPx > entryPx
        slPx := entryPx - 0.1 * riskR
        beActive := true
        if not na(slViz)
            box.set_top(slViz, slPx)
    if not tp1Done and low <= tp1Px
        tp1Done := true
        beActive := true
        if convNow >= 70.0
            slPx := entryPx - 0.25 * riskR
        else if convNow >= 50.0
            slPx := entryPx - beOffsetR * riskR
        else
            slPx := entryPx
        if not na(slViz)
            box.set_top(slViz, slPx)
        label.new(bar_index, tp1Px - atr * 0.3, "✅ TP1 + BE Lock", color=color.new(color.teal, 20), textcolor=color.white, style=label.style_label_up, size=size.tiny)
    if usePVS
        if low <= tp3Px
            tradeState := 0
            exitNote := "TP3 HIT 🚀"
            f_record_trade(true)
            label.new(bar_index, tp3Px - atr * 0.5, "🎯 TP3 HIT", color=color.new(color.teal, 10), textcolor=color.white, style=label.style_label_up, size=size.normal)
            if alertOnExit
                alert("🚀 TP3 | " + syminfo.ticker + " SHORT @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)
        else if high >= slPx
            tradeState := 0
            exitNote := beActive ? "STOPPED AT BE 🛡️" : "STOP LOSS ❌"
            f_record_trade(beActive)
            label.new(bar_index, slPx + atr * 0.5, exitNote, color=color.new(color.red, 10), textcolor=color.white, style=label.style_label_down, size=size.normal)
            if alertOnExit
                alert(exitNote + " | " + syminfo.ticker + " SHORT @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)
        else if low <= tp2Px
            label.new(bar_index, tp2Px - atr * 0.3, "✅ TP2 (runner on)", color=color.new(color.teal, 20), textcolor=color.white, style=label.style_label_up, size=size.tiny)
    else
        if low <= tp2Px
            tradeState := 0
            exitNote := "TP2 HIT ✅"
            f_record_trade(true)
            label.new(bar_index, tp2Px - atr * 0.5, "🎯 TP2 HIT", color=color.new(color.teal, 10), textcolor=color.white, style=label.style_label_up, size=size.normal)
            if alertOnExit
                alert("🎯 TP2 | " + syminfo.ticker + " SHORT @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)
        else if high >= slPx
            tradeState := 0
            exitNote := beActive ? "STOPPED AT BE 🛡️" : "STOP LOSS ❌"
            f_record_trade(beActive)
            label.new(bar_index, slPx + atr * 0.5, exitNote, color=color.new(color.red, 10), textcolor=color.white, style=label.style_label_down, size=size.normal)
            if alertOnExit
                alert(exitNote + " | " + syminfo.ticker + " SHORT @ " + str.tostring(close, "#.##"), alert.freq_once_per_bar)

// ── Pending signals (post-state-machine) ──
bool pendingBuy  = modeAL and buyScore >= modeThreshB and buyGateOk and not spikeBlocked and tradeState == 0 and volOk and sessionOk and canTrade
bool pendingSell = modeAS and sellScore >= modeThreshS and sellGateOk and not spikeBlocked and tradeState == 0 and volOk and sessionOk and canTrade

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 20: SIGNAL & ZONE ALERTS                             ║
// ╚═══════════════════════════════════════════════════════════════════╝

if alertOnSignal and barstate.isconfirmed
    if pendingBuy
        alert("⚠ BUY | " + syminfo.ticker + " [" + symbolMode + "] | " + str.tostring(buyScore, "#") + "% | " + buyWhy + " | Regime: " + regimeStr, alert.freq_once_per_bar)
    if pendingSell
        alert("⚠ SELL | " + syminfo.ticker + " [" + symbolMode + "] | " + str.tostring(sellScore, "#") + "% | " + sellWhy + " | Regime: " + regimeStr, alert.freq_once_per_bar)

if alertOnZone and tradeState == 0 and barstate.isconfirmed
    if bObHit or bFvgHit or rbsHit
        alert("📍 BUY ZONE | " + syminfo.ticker, alert.freq_once_per_bar)
    if rObHit or rFvgHit or sbrHit
        alert("📍 SELL ZONE | " + syminfo.ticker, alert.freq_once_per_bar)

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 21: S&R CLUSTER WEIGHT ENGINE                        ║
// ╚═══════════════════════════════════════════════════════════════════╝

srClusterWeight(float lev) =>
    float cd = atr * 0.65
    int w = 0
    if not na(pdHigh) and math.abs(lev - pdHigh) <= cd
        w += 3
    if not na(pdLow) and math.abs(lev - pdLow) <= cd
        w += 3
    if not na(pwHigh) and math.abs(lev - pwHigh) <= cd
        w += 3
    if not na(pwLow) and math.abs(lev - pwLow) <= cd
        w += 3
    if not na(dailyPivot) and math.abs(lev - dailyPivot) <= cd
        w += 3
    if not na(bObNearTop) and math.abs(lev - bObNearTop) <= cd
        w += 2
    if not na(bObNearBot) and math.abs(lev - bObNearBot) <= cd
        w += 2
    if not na(rObNearTop) and math.abs(lev - rObNearTop) <= cd
        w += 2
    if not na(rObNearBot) and math.abs(lev - rObNearBot) <= cd
        w += 2
    if not na(bFvgNearTop) and math.abs(lev - bFvgNearTop) <= cd
        w += 2
    if not na(bFvgNearBot) and math.abs(lev - bFvgNearBot) <= cd
        w += 2
    if not na(rFvgNearTop) and math.abs(lev - rFvgNearTop) <= cd
        w += 2
    if not na(rFvgNearBot) and math.abs(lev - rFvgNearBot) <= cd
        w += 2
    if not na(lastPH) and math.abs(lev - lastPH) <= cd
        w += 2
    if not na(lastPL) and math.abs(lev - lastPL) <= cd
        w += 2
    if not na(lastHtfPH) and math.abs(lev - lastHtfPH) <= cd
        w += 2
    if not na(lastHtfPL) and math.abs(lev - lastHtfPL) <= cd
        w += 2
    if not na(rbsNearTop) and math.abs(lev - rbsNearTop) <= cd
        w += 2
    if not na(sbrNearTop) and math.abs(lev - sbrNearTop) <= cd
        w += 2
    if useWeekOpen and not na(weekOpen) and math.abs(lev - weekOpen) <= cd
        w += 2
    if timeframe.isintraday and not na(oiLevel) and math.abs(lev - oiLevel) <= cd
        w += 2
    if math.abs(lev - rangeH) <= cd
        w += 1
    if math.abs(lev - rangeL) <= cd
        w += 1
    w

srStars(int w) =>
    w >= 5 ? "★★★" : w >= 3 ? "★★" : "★"

srBgLabel(int w, bool isSup) =>
    isSup ? (w >= 5 ? color.rgb(8, 45, 18) : w >= 3 ? color.rgb(10, 28, 14) : color.rgb(10, 18, 12)) : (w >= 5 ? color.rgb(45, 8, 8) : w >= 3 ? color.rgb(28, 10, 10) : color.rgb(18, 10, 10))

srBgValue(int w, bool isSup) =>
    isSup ? (w >= 5 ? color.rgb(12, 60, 24) : w >= 3 ? color.rgb(14, 38, 18) : color.rgb(14, 24, 16)) : (w >= 5 ? color.rgb(60, 12, 12) : w >= 3 ? color.rgb(38, 14, 14) : color.rgb(24, 14, 14))

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 22: EXECUTION HUD (BOTTOM-RIGHT)                     ║
// ╚═══════════════════════════════════════════════════════════════════╝

f_drawHud() =>
    if showHUD
        color DR = color.rgb(10, 10, 10)
        color MR = color.rgb(18, 18, 18)
        color TC = color.new(color.white, 8)
        color LC = color.new(color.gray, 15)

        var table execHUD = na
        var string hudPosUsed = ""
        if na(execHUD) or hudPos != hudPosUsed
            if not na(execHUD)
                table.delete(execHUD)
            execHUD := table.new(f_pos(hudPos), 2, 26, border_width=1, border_color=color.new(color.gray, 65), frame_color=color.new(color.gray, 55), frame_width=1)
            hudPosUsed := hudPos

        if barstate.islast
            color stateBg = color.rgb(15, 15, 15)
            string stateStr = "● SCANNING"
            if tradeState == 1
                stateBg := color.rgb(0, 52, 20)
                stateStr := "▲ LONG ACTIVE"
            else if tradeState == -1
                stateBg := color.rgb(52, 0, 0)
                stateStr := "▼ SHORT ACTIVE"
            else if pendingBuy
                stateBg := color.rgb(0, 38, 14)
                stateStr := "⚠ BUY SIGNAL"
            else if pendingSell
                stateBg := color.rgb(38, 0, 0)
                stateStr := "⚠ SELL SIGNAL"
            else if exitNote != ""
                stateStr := "LAST: " + exitNote

            string liveRR = "—"
            color rrCol = MR
            if tradeState != 0 and not na(riskR) and riskR > 0
                float pnl = tradeState == 1 ? close - entryPx : entryPx - close
                float rr = pnl / riskR
                liveRR := (rr >= 0 ? "+" : "") + str.tostring(rr, "#.##") + "R | " + str.tostring(tradeBars) + "b"
                rrCol := rr >= 0 ? color.new(color.green, 50) : color.new(color.red, 50)

            string hydraStr = "—"
            if tradeState != 0
                hydraStr := hydraPhase == 0 ? "Phase 0: Initial" : hydraPhase == 1 ? "Phase 1: Tight Trail" : hydraPhase == 2 ? "Phase 2: BE Locked" : "Phase 3: Runner"
            string tp1Str = "—"
            if tradeState != 0
                if tp1Done
                    tp1Str := "✅ Secured (BE On)"
                else
                    tp1Str := "⏳ En Route"

            string sessionStr = "Off-Session"
            color sessionCol = color.rgb(22, 22, 22)
            if not timeframe.isintraday
                sessionStr := "Daily+ (N/A)"
                sessionCol := color.new(color.gray, 60)
            else if inLondon
                sessionStr := "🔵 London (07-12)"
                sessionCol := color.new(color.blue, 52)
            else if inNY
                sessionStr := "🟠 New York (12-17)"
                sessionCol := color.new(color.orange, 52)
            else if inAsian
                sessionStr := "🟣 Asian (00-07)"
                sessionCol := color.new(color.purple, 60)

            string cautionStr = cautionMode ? "⚠ CAUTION MODE" : spikeBlocked ? "🛡 SPIKE SHIELD" : (useBtcWeekend and btcWeekend) ? "🌙 THIN (BTC WE)" : "✅ Normal"

            string macroHudStr = "N/A"
            color macroHudCol = color.rgb(22, 22, 22)
            if macroValid
                if macroBullish
                    macroHudStr := isInverseMacro ? "✅ Favors Longs" : "✅ Favors Longs"
                    macroHudCol := color.new(color.green, 70)
                else if macroBearish
                    macroHudStr := "✅ Favors Shorts"
                    macroHudCol := color.new(color.red, 70)
                else
                    macroHudStr := "◆ Neutral"

            string zoneStr = "Between Zones"
            color zoneCol = color.rgb(22, 22, 22)
            if atBuyZone
                zoneStr := "⚡ IN BUY ZONE"
                zoneCol := color.new(color.green, 50)
            else if atSellZone
                zoneStr := "⚡ IN SELL ZONE"
                zoneCol := color.new(color.red, 50)
            string buyZoneStr = "No Zone"
            if not na(bObNearTop)
                buyZoneStr := "OB @ " + str.tostring(bObNearTop, "#.##")
            else if not na(bFvgNearTop)
                buyZoneStr := "FVG @ " + str.tostring(bFvgNearTop, "#.##")
            else if not na(rbsNearTop)
                buyZoneStr := "RBS @ " + str.tostring(rbsNearTop, "#.##")
            string sellZoneStr = "No Zone"
            if not na(rObNearTop) and rObNearTop > close
                sellZoneStr := "OB @ " + str.tostring(rObNearTop, "#.##")
            else if not na(rFvgNearTop) and rFvgNearTop > close
                sellZoneStr := "FVG @ " + str.tostring(rFvgNearTop, "#.##")
            else if not na(sbrNearTop) and sbrNearTop > close
                sellZoneStr := "SBR @ " + str.tostring(sbrNearTop, "#.##")

            string liqStr = "No Sweep"
            color liqCol = MR
            if sslSweep
                liqStr := "▲ SSL SWEPT"
                liqCol := color.new(color.green, 45)
            else if bslSweep
                liqStr := "▼ BSL SWEPT"
                liqCol := color.new(color.red, 45)

            string pdStr = "EQUILIBRIUM ↔"
            if inPremium
                pdStr := "PREMIUM ↑"
            else if inDiscount
                pdStr := "DISCOUNT ↓"

            string biasAgreStr = "—"
            color biasAgreCol = color.rgb(22, 22, 22)
            if tradeState == 1
                if overallBias == "BULLISH"
                    biasAgreStr := "✅ ALIGNED"
                    biasAgreCol := color.new(color.green, 50)
                else if overallBias == "BEARISH"
                    biasAgreStr := "⚡ COUNTER"
                    biasAgreCol := color.new(color.orange, 45)
                else
                    biasAgreStr := "◆ MIXED"
            else if tradeState == -1
                if overallBias == "BEARISH"
                    biasAgreStr := "✅ ALIGNED"
                    biasAgreCol := color.new(color.green, 50)
                else if overallBias == "BULLISH"
                    biasAgreStr := "⚡ COUNTER"
                    biasAgreCol := color.new(color.orange, 45)
                else
                    biasAgreStr := "◆ MIXED"

            string catStr = "Awaiting..."
            if tradeState != 0
                catStr := catalyst
            else if pendingBuy
                catStr := buyWhy
            else if pendingSell
                catStr := sellWhy

            string confStr = "B:" + str.tostring(buyScore, "#") + "% (" + str.tostring(buyFactors) + "F) S:" + str.tostring(sellScore, "#") + "% (" + str.tostring(sellFactors) + "F)"
            color confBg = MR
            if buyScore >= buyThresh
                confBg := color.new(color.green, 40)
            else if sellScore >= sellThresh
                confBg := color.new(color.red, 40)

            string gradeStr = tradeState != 0 ? getGrade(tradeState == 1 ? buyScore : sellScore, dynThreshold) : "—"
            string asocStr = useASOC ? str.tostring(asocWinRate, "#") + "% WR | Adj " + (adaptOffset >= 0 ? "+" : "") + str.tostring(adaptOffset, "#.#") + "% | " + str.tostring(adaptTrades, "#") + "/" + str.tostring(asocWindow) + " tr" : "Off"

            table.cell(execHUD, 0, 0, "OMNI-GOD ∞ UNIFIED", bgcolor=color.rgb(5, 5, 5), text_color=TC, text_size=size.small)
            table.cell(execHUD, 1, 0, stateStr, bgcolor=stateBg, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 1, "Mode / Timeframe", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 1, symbolMode + " | " + tfLabel, bgcolor=MR, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 2, "Regime", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 2, regimeStr + " | " + volRegimeStr, bgcolor=regimeCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 3, "Session / Caution", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 3, sessionStr + " | " + cautionStr, bgcolor=sessionCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 4, "Macro (" + macroName + ")", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 4, macroHudStr, bgcolor=macroHudCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 5, "Overall Bias", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 5, biasEmoji, bgcolor=biasBg, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 6, "Market Structure", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 6, structStr, bgcolor=structCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 7, "Wyckoff Phase", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 7, wyckStr, bgcolor=wyckCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 8, "MACD", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 8, macdStr, bgcolor=macdDispCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 9, "RSI (" + str.tostring(rsiLen) + ")", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 9, rsiStr, bgcolor=rsiDispCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 10, "VWAP", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 10, vwapStr, bgcolor=vwapCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 11, "Sentiment", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 11, sentStr, bgcolor=sentCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 12, "Footprint", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 12, footprintStr, bgcolor=footprintCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 13, "Candle", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 13, candleStr, bgcolor=MR, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 14, "Liquidity / PD", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 14, liqStr + " | " + pdStr + (nearEQL ? " | ⭑EQL Pool" : nearEQH ? " | ⭑EQH Pool" : ""), bgcolor=liqCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 15, "Zone Status", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 15, zoneStr + " | B:" + buyZoneStr + " S:" + sellZoneStr, bgcolor=zoneCol, text_color=TC, text_size=size.tiny)
            table.cell(execHUD, 0, 16, "Confluence", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 16, confStr, bgcolor=confBg, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 17, "ASOC Threshold", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 17, asocStr, bgcolor=MR, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 18, "Position Sizing", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 18, str.tostring(posUnits, "#.##") + " units ($" + str.tostring(riskDollars, "#") + " risk)", bgcolor=MR, text_color=color.teal, text_size=size.small)
            table.cell(execHUD, 0, 19, "Live R:R | TP1", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 19, liveRR + " | Max +" + str.tostring(maxProfitR, "#.##") + "R | " + tp1Str, bgcolor=rrCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 20, "Hydra Trail", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 20, hydraStr, bgcolor=MR, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 21, "Bias Agreement", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 21, biasAgreStr, bgcolor=biasAgreCol, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 22, "Setup Grade", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 22, gradeStr, bgcolor=MR, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 23, "Catalyst", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 23, catStr, bgcolor=MR, text_color=TC, text_size=size.tiny)
            string asianStr = "N/A"
            if not na(asianHigh) and not na(asianLow)
                asianStr := str.tostring(asianLow, "#.##") + " - " + str.tostring(asianHigh, "#.##") + " | " + str.tostring(math.abs(asianHigh - asianLow) / math.max(atr, 0.001), "#.##") + " ATR | " + str.tostring(bar_index - nz(asianStart, bar_index)) + "b"
            string valueStr = "N/A (Daily+)"
            if timeframe.isintraday
                valueStr := belowValue ? "BELOW BAND ⬇ (Value Low)" : aboveValue ? "ABOVE BAND ⬆ (Value High)" : "IN VALUE BAND ⇄"
            table.cell(execHUD, 0, 24, "Asian Range", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 24, asianStr, bgcolor=MR, text_color=TC, text_size=size.small)
            table.cell(execHUD, 0, 25, "Vol Value (VWAP)", bgcolor=DR, text_color=LC, text_size=size.small)
            table.cell(execHUD, 1, 25, valueStr, bgcolor=MR, text_color=TC, text_size=size.small)

f_drawHud()

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 23: CONFLUENCE RADAR (TOP-RIGHT)                     ║
// ═══════════════════════════════════════════════════════════════════╝

f_drawRadar() =>
    if showRadar
        var table radarTbl = na
        var string radarPosUsed = ""
        if na(radarTbl) or radarPos != radarPosUsed
            if not na(radarTbl)
                table.delete(radarTbl)
            radarTbl := table.new(f_pos(radarPos), 2, 13, border_width=1, border_color=color.new(color.gray, 65), frame_color=color.new(color.gray, 55), frame_width=1)
            radarPosUsed := radarPos
        if barstate.islast
            f_radar_row(radarTbl, 0, "━━ CONFLUENCE RADAR ━━", "━━ " + symbolMode + " ━━", true)
            f_radar_row(radarTbl, 1, "Macro (" + macroName + ")", macroBullish ? "Favors Longs" : macroBearish ? "Favors Shorts" : "Neutral", macroBullish or macroBearish)
            f_radar_row(radarTbl, 2, "Trend Stack (EMA)", bullStack ? "Bullish" : bearStack ? "Bearish" : "Mixed", bullStack or bearStack)
            f_radar_row(radarTbl, 3, "Baseline (HMA 50)", close > hmaBase ? "Above" : "Below", true)
            f_radar_row(radarTbl, 4, "Elastic EMA (" + str.tostring(elasticLen) + ")", close > emaElastic ? "Bullish" : "Bearish", true)
            f_radar_row(radarTbl, 5, "Multi-TF Bias", overallBias, overallBias != "MIXED")
            f_radar_row(radarTbl, 6, "Liquidity / EQ Pool", validBuySweep or validSellSweep ? "Swept ⚡" : nearEQL or nearEQH ? "EQ Pool 🎯" : "None", validBuySweep or validSellSweep or nearEQL or nearEQH)
            f_radar_row(radarTbl, 7, "SMC Zone / Breaker", atBuyZone or atSellZone ? "Tap" : "None", atBuyZone or atSellZone)
            f_radar_row(radarTbl, 8, "ADX Strength", adxOk ? "Strong" : "Weak", adxOk)
            f_radar_row(radarTbl, 9, "Wyckoff Phase", wyckStr, wyckStr != "N/A")
            f_radar_row(radarTbl, 10, "Score vs Threshold", (buyScore >= dynThreshold or sellScore >= dynThreshold) ? "EXECUTE" : "WAIT", buyScore >= dynThreshold or sellScore >= dynThreshold)
            f_radar_row(radarTbl, 11, "Ichimoku (Daily)", dayKumoState, not na(dayKumoTop))
            f_radar_row(radarTbl, 12, "H&S Pattern", hsT_state == 3 ? "TOP BROKEN ✖" : hsB_state == 3 ? "BOTTOM BROKEN ✔" : hsT_state >= 1 or hsB_state >= 1 ? "Forming" : hsD_Tst == 3 ? "DAILY TOP ✖" : hsD_Bst == 3 ? "DAILY BOTTOM ✔" : "None", hsT_state >= 1 or hsB_state >= 1 or hsD_Tst >= 1 or hsD_Bst >= 1)

f_drawRadar()

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 24: ANALYSIS TABLE (TOP-LEFT)                        ║
// ╚═══════════════════════════════════════════════════════════════════╝

f_drawAnalysis() =>
    if showAnalysis
        color HDR = color.rgb(5, 5, 5)
        color DR2 = color.rgb(10, 10, 10)
        color MR2 = color.rgb(18, 18, 18)
        color TC2 = color.new(color.white, 8)
        color LC2 = color.new(color.gray, 15)
        color CUR_BG = color.rgb(18, 18, 35)
        color PIV_BG = color.rgb(30, 28, 8)

        var table atbl = na
        var string atblPosUsed = ""
        if na(atbl) or analysisPos != atblPosUsed
            if not na(atbl)
                table.delete(atbl)
            atbl := table.new(f_pos(analysisPos), 2, 26, border_width=1, border_color=color.new(color.gray, 65), frame_color=color.new(color.gray, 55), frame_width=1)
            atblPosUsed := analysisPos

        if barstate.islast
            // ── Resistance / Support candidates ──
            array<float> resCands = array.from(
                not na(pdHigh)      and pdHigh      > close ? pdHigh      : na,
                not na(pwHigh)      and pwHigh      > close ? pwHigh      : na,
                not na(dailyPivot)  and dailyPivot  > close ? dailyPivot  : na,
                not na(lastPH)      and lastPH      > close ? lastPH      : na,
                not na(lastHtfPH)   and lastHtfPH   > close ? lastHtfPH   : na,
                not na(rObNearBot)  and rObNearBot  > close ? rObNearBot  : na,
                not na(rFvgNearTop) and rFvgNearTop > close ? rFvgNearTop : na,
                not na(sbrNearTop)  and sbrNearTop  > close ? sbrNearTop  : na,
                useWeekOpen and not na(weekOpen) and weekOpen > close ? weekOpen : na,
                timeframe.isintraday and not na(oiLevel) and oiLevel > close ? oiLevel : na,
                not na(rangeH)      and rangeH      > close ? rangeH      : na)

            array<float> supCands = array.from(
                not na(pdLow)       and pdLow       < close ? pdLow       : na,
                not na(pwLow)       and pwLow       < close ? pwLow       : na,
                not na(dailyPivot)  and dailyPivot  < close ? dailyPivot  : na,
                not na(lastPL)      and lastPL      < close ? lastPL      : na,
                not na(lastHtfPL)   and lastHtfPL   < close ? lastHtfPL   : na,
                not na(bObNearTop)  and bObNearTop  < close ? bObNearTop  : na,
                not na(bFvgNearTop) and bFvgNearTop < close ? bFvgNearTop : na,
                not na(rbsNearTop)  and rbsNearTop  < close ? rbsNearTop  : na,
                useWeekOpen and not na(weekOpen) and weekOpen < close ? weekOpen : na,
                timeframe.isintraday and not na(oiLevel) and oiLevel < close ? oiLevel : na,
                not na(rangeL)      and rangeL      < close ? rangeL      : na)

            // ── 3 nearest resistances ──
            float res1 = na
            float res2 = na
            float res3 = na
            int resW1 = 0
            int resW2 = 0
            int resW3 = 0
            if array.size(resCands) > 0
                for i = 0 to array.size(resCands) - 1
                    float lev = array.get(resCands, i)
                    if not na(lev)
                        int w = srClusterWeight(lev)
                        if na(res1) or lev < res1
                            res3 := res2
                            resW3 := resW2
                            res2 := res1
                            resW2 := resW1
                            res1 := lev
                            resW1 := w
                        else if (na(res2) or lev < res2) and math.abs(lev - res1) > atr * 0.4
                            res3 := res2
                            resW3 := resW2
                            res2 := lev
                            resW2 := w
                        else if (na(res3) or lev < res3) and not na(res2) and math.abs(lev - res2) > atr * 0.4
                            res3 := lev
                            resW3 := w

            // ── 3 nearest supports ──
            float sup1 = na
            float sup2 = na
            float sup3 = na
            int supW1 = 0
            int supW2 = 0
            int supW3 = 0
            if array.size(supCands) > 0
                for i = 0 to array.size(supCands) - 1
                    float lev = array.get(supCands, i)
                    if not na(lev)
                        int w = srClusterWeight(lev)
                        if na(sup1) or lev > sup1
                            sup3 := sup2
                            supW3 := supW2
                            sup2 := sup1
                            supW2 := supW1
                            sup1 := lev
                            supW1 := w
                        else if (na(sup2) or lev > sup2) and math.abs(lev - sup1) > atr * 0.4
                            sup3 := sup2
                            supW3 := supW2
                            sup2 := lev
                            supW2 := w
                        else if (na(sup3) or lev > sup3) and not na(sup2) and math.abs(lev - sup2) > atr * 0.4
                            sup3 := lev
                            supW3 := w

            bool res1IsPivot = not na(res1) and not na(dailyPivot) and math.abs(res1 - dailyPivot) < atr * 0.6
            bool res2IsPivot = not na(res2) and not na(dailyPivot) and math.abs(res2 - dailyPivot) < atr * 0.6
            bool res3IsPivot = not na(res3) and not na(dailyPivot) and math.abs(res3 - dailyPivot) < atr * 0.6
            bool sup1IsPivot = not na(sup1) and not na(dailyPivot) and math.abs(sup1 - dailyPivot) < atr * 0.6
            bool sup2IsPivot = not na(sup2) and not na(dailyPivot) and math.abs(sup2 - dailyPivot) < atr * 0.6
            bool sup3IsPivot = not na(sup3) and not na(dailyPivot) and math.abs(sup3 - dailyPivot) < atr * 0.6

            string res3LStr = not na(res3) ? srStars(resW3) + " Resistance" : "—"
            string res3VStr = not na(res3) ? str.tostring(res3, "#.##") + (res3IsPivot ? " ⭕" : "") : "—"
            string res2LStr = not na(res2) ? srStars(resW2) + " Resistance" : "—"
            string res2VStr = not na(res2) ? str.tostring(res2, "#.##") + (res2IsPivot ? " ⭕" : "") : "—"
            string res1LStr = not na(res1) ? srStars(resW1) + " Resistance" + (res1IsPivot ? " ⭕" : "") : "—"
            string res1VStr = not na(res1) ? str.tostring(res1, "#.##") : "—"
            string sup1LStr = not na(sup1) ? srStars(supW1) + " Support" + (sup1IsPivot ? " ⭕" : "") : "—"
            string sup1VStr = not na(sup1) ? str.tostring(sup1, "#.##") : "—"
            string sup2LStr = not na(sup2) ? srStars(supW2) + " Support" : "—"
            string sup2VStr = not na(sup2) ? str.tostring(sup2, "#.##") : "—"
            string sup3LStr = not na(sup3) ? srStars(supW3) + " Support" : "—"
            string sup3VStr = not na(sup3) ? str.tostring(sup3, "#.##") + (sup3IsPivot ? " ⭕" : "") : "—"

            string lastStr = "◆ LAST " + str.tostring(close, "#.##")

            string prefLine = "—"
            string altLine = "—"
            string comment = "—"
            if overallBias == "BEARISH"
                if not na(res1)
                    prefLine := "Downside prevails below " + str.tostring(res1, "#.##")
                    if not na(sup1) and not na(sup2)
                        prefLine := prefLine + ", targets " + str.tostring(sup1, "#.##") + " & " + str.tostring(sup2, "#.##")
                    else if not na(sup1)
                        prefLine := prefLine + ", target " + str.tostring(sup1, "#.##")
                else
                    prefLine := "Bearish — sell rallies to resistance"
                if not na(res1) and not na(res2)
                    altLine := "Above " + str.tostring(res1, "#.##") + ", look for " + str.tostring(res2, "#.##")
                else
                    altLine := "Break above resistance → bearish invalidated"
            else if overallBias == "BULLISH"
                if not na(sup1)
                    prefLine := "Upside prevails above " + str.tostring(sup1, "#.##")
                    if not na(res1) and not na(res2)
                        prefLine := prefLine + ", targets " + str.tostring(res1, "#.##") + " & " + str.tostring(res2, "#.##")
                    else if not na(res1)
                        prefLine := prefLine + ", target " + str.tostring(res1, "#.##")
                else
                    prefLine := "Bullish — buy dips to support"
                if not na(sup1) and not na(sup2)
                    altLine := "Below " + str.tostring(sup1, "#.##") + ", look for " + str.tostring(sup2, "#.##")
                else
                    altLine := "Break below support → bullish invalidated"
            else
                string sStr = not na(sup1) ? str.tostring(sup1, "#.##") : "?"
                string rStr = not na(res1) ? str.tostring(res1, "#.##") : "?"
                prefLine := "Ranging between " + sStr + " and " + rStr
                if not na(res1)
                    altLine := "Break " + rStr + " bullish | Break " + sStr + " bearish"
                else
                    altLine := "Await directional breakout"

            // ── AUTO-GENERATED BRIEFING — assembles itself from live state ──
            string cDir = overallBias == "BULLISH" ? "BULLISH bias" : overallBias == "BEARISH" ? "BEARISH bias" : "MIXED bias"
            cDir := cDir + " (" + str.tostring(math.abs(biasScore)) + "/3 TF" + (math.abs(biasScore) == 1 ? "" : "s") + " aligned)"
            string cConf = "Conf B " + str.tostring(buyScore, "#") + "%/" + str.tostring(buyFactors) + "F vs S " + str.tostring(sellScore, "#") + "%/" + str.tostring(sellFactors) + "F"
            string cDriver = "none above threshold yet"
            if buyScore >= dynThreshold
                cDriver := buyWhy
            else if sellScore >= dynThreshold
                cDriver := sellWhy
            string cCloud = na(dayKumoTop) ? "" : close > dayKumoTop ? " | Above daily Kumo" : close < dayKumoBot ? " | Below daily Kumo" : " | In daily Kumo (chop)"
            string cPat = ""
            if hsT_state >= 1
                cPat := "H&S TOP " + (hsT_state == 3 ? "BROKEN ✖" : "forming · NL " + str.tostring(hsT_nl, "#.##"))
            if hsB_state >= 1
                cPat := cPat + (cPat == "" ? "" : " · ") + "H&S BOT " + (hsB_state == 3 ? "BROKEN ✔" : "forming · NL " + str.tostring(hsB_nl, "#.##"))
            if hsD_Tst == 3
                cPat := cPat + (cPat == "" ? "" : " · ") + "dH&S TOP broken"
            if hsD_Bst == 3
                cPat := cPat + (cPat == "" ? "" : " · ") + "dH&S BOT broken"
            if cPat == "" and nearEQL
                cPat := "EQL pool at " + str.tostring(eqlLevel, "#.##")
            if cPat == "" and nearEQH
                cPat := "EQH pool at " + str.tostring(eqhLevel, "#.##")
            string cRisk = ""
            if cautionMode
                cRisk := cRisk + " ⚠Caution"
            if spikeBlocked
                cRisk := cRisk + " 🛡Spike"
            if not adxOk
                cRisk := cRisk + " weak-mom"
            if useBtcWeekend and btcWeekend
                cRisk := cRisk + " thin-wknd"
            if isCounterBias
                cRisk := cRisk + " counter-bias"
            if lowVol
                cRisk := cRisk + " low-vol"
            string cLevels = "R " + (not na(res1) ? str.tostring(res1, "#.##") : "?") + " | S " + (not na(sup1) ? str.tostring(sup1, "#.##") : "?")
            comment := cDir + " | " + cConf + " | Driver: " + cDriver + cCloud
            string cRead = cLevels + " | " + (cPat == "" ? "no active pattern" : cPat) + cRisk

            color res3LBG = srBgLabel(resW3, false)
            color res3VBG = srBgValue(resW3, false)
            color res2LBG = srBgLabel(resW2, false)
            color res2VBG = srBgValue(resW2, false)
            color res1LBG = srBgLabel(resW1, false)
            color res1VBG = srBgValue(resW1, false)
            color sup1LBG = srBgLabel(supW1, true)
            color sup1VBG = srBgValue(supW1, true)
            color sup2LBG = srBgLabel(supW2, true)
            color sup2VBG = srBgValue(supW2, true)
            color sup3LBG = srBgLabel(supW3, true)
            color sup3VBG = srBgValue(supW3, true)
            if res1IsPivot
                res1LBG := PIV_BG
                res1VBG := PIV_BG
            if sup1IsPivot
                sup1LBG := PIV_BG
                sup1VBG := PIV_BG

            string alignStr = "DIVERGING (0/3)"
            int absScore = math.abs(biasScore)
            if absScore == 3
                alignStr := "STRONG (3/3)"
            else if absScore == 2
                alignStr := "ALIGNED (2/3)"
            else if absScore == 1
                alignStr := "WEAK (1/3)"

            table.cell(atbl, 0, 0, "ANALYSIS " + tfLabel, bgcolor=HDR, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 0, symbolMode, bgcolor=HDR, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 1, "Intraday (" + tf1Label + ")", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 1, tf1Str, bgcolor=tf1Col, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 2, "Short Term (" + tf2Label + ")", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 2, tf2Str, bgcolor=tf2Col, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 3, "Medium Term (" + tf3Label + ")", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 3, tf3Str, bgcolor=tf3Col, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 4, "Alignment", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 4, alignStr, bgcolor=MR2, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 5, "MACD", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 5, macdStr, bgcolor=macdDispCol, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 6, "RSI (" + str.tostring(rsiLen) + ")", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 6, rsiStr, bgcolor=rsiDispCol, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 7, "VWAP", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 7, vwapStr, bgcolor=vwapCol, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 8, "Wyckoff", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 8, wyckStr, bgcolor=wyckCol, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 9, "── LEVELS ──", bgcolor=HDR, text_color=color.new(color.gray, 60), text_size=size.tiny)
            table.cell(atbl, 1, 9, "── ⭕ = Daily Pivot ──", bgcolor=HDR, text_color=color.new(color.gray, 60), text_size=size.tiny)
            table.cell(atbl, 0, 10, res3LStr, bgcolor=res3LBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 10, res3VStr, bgcolor=res3VBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 11, res2LStr, bgcolor=res2LBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 11, res2VStr, bgcolor=res2VBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 12, res1LStr, bgcolor=res1LBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 12, res1VStr, bgcolor=res1VBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 13, lastStr, bgcolor=CUR_BG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 13, abovePivot ? "▲ Above Pivot" : "▼ Below Pivot", bgcolor=abovePivot ? color.new(color.green, 55) : color.new(color.red, 55), text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 14, sup1LStr, bgcolor=sup1LBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 14, sup1VStr, bgcolor=sup1VBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 15, sup2LStr, bgcolor=sup2LBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 15, sup2VStr, bgcolor=sup2VBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 16, sup3LStr, bgcolor=sup3LBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 1, 16, sup3VStr, bgcolor=sup3VBG, text_color=TC2, text_size=size.small)
            table.cell(atbl, 0, 17, "── ANALYSIS ──", bgcolor=HDR, text_color=color.new(color.gray, 60), text_size=size.tiny)
            table.cell(atbl, 1, 17, "────────────────", bgcolor=HDR, text_color=color.new(color.gray, 60), text_size=size.tiny)
            table.cell(atbl, 0, 18, "Our Preference", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 18, prefLine, bgcolor=biasBg, text_color=TC2, text_size=size.tiny)
            table.cell(atbl, 0, 19, "Alternative", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 19, altLine, bgcolor=MR2, text_color=TC2, text_size=size.tiny)
            table.cell(atbl, 0, 20, "Comment", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 20, comment + " | Th: " + str.tostring(dynThreshold, "#") + "%", bgcolor=MR2, text_color=TC2, text_size=size.tiny)
            string hsTFLine = "—"
            if hsT_state > 0 or hsB_state > 0
                hsTFLine := (hsT_state > 0 ? "TOP " + f_hsStage(hsT_state, hsT_nl, "✖") : "") + (hsB_state > 0 ? " · BOT " + f_hsStage(hsB_state, hsB_nl, "✔") : "")
            string hsDFLine = "—"
            if hsD_Tst > 0 or hsD_Bst > 0
                hsDFLine := (hsD_Tst > 0 ? "TOP " + f_hsStage(hsD_Tst, hsD_Tnl, "✖") : "") + (hsD_Bst > 0 ? " · BOT " + f_hsStage(hsD_Bst, hsD_Bnl, "✔") : "")
            table.cell(atbl, 0, 21, "Ichimoku (Daily)", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 21, dayKumoState + " | " + str.tostring(dayKumoBot, "#.##") + "–" + str.tostring(dayKumoTop, "#.##"), bgcolor=MR2, text_color=TC2, text_size=size.tiny)
            table.cell(atbl, 0, 22, "Ichimoku (TF)", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 22, tfKumoState + " | TK" + (ichiTk > ichiKj ? ">" : "<") + "KJ" + (tkKjGold ? " ✚X" : tkKjDeath ? " ✖X" : ""), bgcolor=MR2, text_color=TC2, text_size=size.tiny)
            table.cell(atbl, 0, 23, "H&S (Daily)", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 23, hsDFLine, bgcolor=MR2, text_color=TC2, text_size=size.tiny)
            table.cell(atbl, 0, 24, "H&S (TF)", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 24, hsTFLine, bgcolor=MR2, text_color=TC2, text_size=size.tiny)
            table.cell(atbl, 0, 25, "Auto-Read", bgcolor=DR2, text_color=LC2, text_size=size.small)
            table.cell(atbl, 1, 25, cRead, bgcolor=MR2, text_color=TC2, text_size=size.tiny)

f_drawAnalysis()

// ╔═══════════════════════════════════════════════════════════════════╗
// ║     SECTION 25: PLOTS, LINES, HEATMAP & NARRATIVE                ║
// ╚═══════════════════════════════════════════════════════════════════╝

plot(showEmas ? emaF : na, "Fast EMA", color=color.new(color.aqua, 15), linewidth=1)
plot(showEmas ? emaM : na, "Med EMA", color=color.new(color.orange, 15), linewidth=2)
plot(showEmas ? emaS : na, "Slow EMA", color=color.new(color.red, 15), linewidth=2)
plot(showEmas ? emaT : na, "Trend EMA", color=color.new(color.white, 55), linewidth=1)
plot(showHma ? hmaBase : na, "HMA Baseline", color=color.new(color.purple, 20), linewidth=2)
plot(showElastic and useElastic ? emaElastic : na, "Elastic EMA", color=color.new(color.cyan, 30), linewidth=2, style=plot.style_dotted)
plot(showVwap and timeframe.isintraday ? vwapVal : na, "VWAP", color=color.new(color.white, 22), linewidth=2)

plot(showPD ? rangeH : na, "Range High", color=color.new(color.red, 72), style=plot.style_stepline)
plot(showPD ? rangeL : na, "Range Low", color=color.new(color.green, 72), style=plot.style_stepline)
plot(showPD ? rangeEQ : na, "Range EQ", color=color.new(color.gray, 78), style=plot.style_stepline)

// Confluence heatmap background (tiered opacity — int-safe)
// NOTE: bgcolor() is only allowed at global scope, so the color is resolved inline here
float heatHgtB = (buyScore - buyThresh) / math.max(100.0 - buyThresh, 1.0)
float heatHgtS = (sellScore - sellThresh) / math.max(100.0 - sellThresh, 1.0)
color heatColor = showHeatmap and tradeState == 0 and buyScore >= buyThresh ? (heatHgtB >= 0.6 ? color.new(color.green, 60) : heatHgtB >= 0.3 ? color.new(color.green, 75) : color.new(color.green, 88)) : showHeatmap and tradeState == 0 and sellScore >= sellThresh ? (heatHgtS >= 0.6 ? color.new(color.red, 60) : heatHgtS >= 0.3 ? color.new(color.red, 75) : color.new(color.red, 88)) : na
f_drawClouds() =>
    // ── Ichimoku clouds — daily + current TF ──
    var line     cldDT = na
    var line     cldDB = na
    var linefill cldDF = na
    var line     cldCT = na
    var line     cldCB = na
    var linefill cldCF = na
    if showIchimoku and barstate.islast
        if not na(cldDT)
            line.delete(cldDT)
        if not na(cldDB)
            line.delete(cldDB)
        if not na(cldDF)
            linefill.delete(cldDF)
        if not na(cldCT)
            line.delete(cldCT)
        if not na(cldCB)
            line.delete(cldCB)
        if not na(cldCF)
            linefill.delete(cldCF)
        if not na(dayKumoTop) and not na(dayKumoBot)
            cldDT := line.new(bar_index - 60, dayKumoTop[60], bar_index + 30, dayKumoTop, color=color.new(color.gray, 25), style=line.style_solid, width=1)
            cldDB := line.new(bar_index - 60, dayKumoBot[60], bar_index + 30, dayKumoBot, color=color.new(color.gray, 25), style=line.style_solid, width=1)
            cldDF := linefill.new(cldDT, cldDB, color.new(color.gray, 85))
        if not na(ichiTopC) and not na(ichiBotC)
            cldCT := line.new(bar_index - 60, ichiTopC[60], bar_index + 30, ichiTopC, color=color.new(color.aqua, 20), style=line.style_solid, width=1)
            cldCB := line.new(bar_index - 60, ichiBotC[60], bar_index + 30, ichiBotC, color=color.new(color.aqua, 20), style=line.style_solid, width=1)
            cldCF := linefill.new(cldCT, cldCB, color.new(color.aqua, 88))

f_drawClouds()

bgcolor(heatColor, title="Confluence Heatmap")

f_drawLevels() =>
    // Key OHLC & pivot lines (redrawn on last bar)
    var line _pdH = na
    var line _pdL = na
    var line _pwH = na
    var line _pwL = na
    var line _pvL = na
    var line _wkO = na
    if showLevels and barstate.islast
        if not na(_pdH)
            line.delete(_pdH)
        if not na(_pdL)
            line.delete(_pdL)
        if not na(_pwH)
            line.delete(_pwH)
        if not na(_pwL)
            line.delete(_pwL)
        if not na(_pvL)
            line.delete(_pvL)
        if not na(_wkO)
            line.delete(_wkO)
        if not na(pdHigh)
            _pdH := line.new(bar_index - 30, pdHigh, bar_index + 15, pdHigh, color=color.new(color.orange, 35), style=line.style_dashed, width=1)
        if not na(pdLow)
            _pdL := line.new(bar_index - 30, pdLow, bar_index + 15, pdLow, color=color.new(color.orange, 35), style=line.style_dashed, width=1)
        if not na(pwHigh)
            _pwH := line.new(bar_index - 30, pwHigh, bar_index + 15, pwHigh, color=color.new(color.yellow, 50), style=line.style_dotted, width=1)
        if not na(pwLow)
            _pwL := line.new(bar_index - 30, pwLow, bar_index + 15, pwLow, color=color.new(color.yellow, 50), style=line.style_dotted, width=1)
        if not na(dailyPivot)
            _pvL := line.new(bar_index - 30, dailyPivot, bar_index + 15, dailyPivot, color=color.new(color.white, 38), style=line.style_dotted, width=2)
        if useWeekOpen and not na(weekOpen)
            _wkO := line.new(bar_index - 30, weekOpen, bar_index + 15, weekOpen, color=color.new(color.green, 35), style=line.style_dotted, width=1)

f_drawLevels()

f_drawLiqHeat() =>
    // Liquidity heat projection above/below last swings
    var box liqAboveBox = na
    var box liqBelowBox = na
    if showLiqHeat and barstate.islast
        if not na(liqAboveBox)
            box.delete(liqAboveBox)
        if not na(liqBelowBox)
            box.delete(liqBelowBox)
        if not na(lastPH)
            liqAboveBox := box.new(bar_index - 5, lastPH, bar_index + 20, lastPH + atr * 0.6, bgcolor=color.new(color.red, 92), border_color=na)
        if not na(lastPL)
            liqBelowBox := box.new(bar_index - 5, lastPL - atr * 0.6, bar_index + 20, lastPL, bgcolor=color.new(color.green, 92), border_color=na)

f_drawLiqHeat()

f_drawSweeps() =>
    // Sweep reference lines
    if showSweeps and barstate.isconfirmed
        if bslSweep
            line.new(bar_index - swingLen, lastPH, bar_index, lastPH, color=color.new(color.red, 40), style=line.style_dashed)
        if sslSweep
            line.new(bar_index - swingLen, lastPL, bar_index, lastPL, color=color.new(color.green, 40), style=line.style_dashed)

f_drawSweeps()

f_drawNarrative() =>
    // ── NARRATIVE VOICE ──
    if showNarrative and barstate.islast
        string convStr = str.tostring(convNow, "#") + (convNow > nz(convNow[1]) ? " ▲" : convNow < nz(convNow[1]) ? " ▼" : " ·")
        string perfTag = ""
        if f_modePerfN(mode) >= 5
            perfTag := " " + str.tostring(f_modePerfWr(mode) * 100, "#") + "% WR (" + str.tostring(f_modePerfN(mode), "#") + ")"
        string narrative = "🧭 " + modeName + perfTag + " · " + symbolMode + " · Conv " + convStr
        if tradeState == 1
            string rStr = riskR > 0 ? str.tostring((close - entryPx) / riskR, "#.##") + "R" : "—"
            narrative := narrative + " · 🚀 LONG " + (close >= entryPx ? "+" : "") + rStr + " · " + (tp1Done ? (slPx > entryPx ? "BE + Profit Lock" : "BE Locked") : "En Route") + (convSoft ? " · ⚠ Soft Invalidation" : "") + (isCounterBias ? " · ⚡ C-Bias" : "") + " · " + catalyst
        else if tradeState == -1
            string rStr = riskR > 0 ? str.tostring((entryPx - close) / riskR, "#.##") + "R" : "—"
            narrative := narrative + " · 🚀 SHORT " + (close <= entryPx ? "+" : "") + rStr + " · " + (tp1Done ? (slPx < entryPx ? "BE + Profit Lock" : "BE Locked") : "En Route") + (convSoft ? " · ⚠ Soft Invalidation" : "") + (isCounterBias ? " · ⚡ C-Bias" : "") + " · " + catalyst
        else
            string ctx = "Equilibrium"
            if cautionMode
                ctx := "⚠ " + (isNfpDay ? "NFP Day" : "London Fix")
            else if spikeBlocked
                ctx := "🛡 Spike Shield"
            else if vBotTrig
                ctx := "⚡ V-bottom " + str.tostring(nz(barsBot)) + "b ago"
            else if vTopTrig
                ctx := "⚡ V-top " + str.tostring(nz(barsTop)) + "b ago"
            else if pattern != PAT_NONE
                ctx := "📐 " + patternName + (not na(barsApex) ? " · apex ~" + str.tostring(barsApex, "#") + "b" : "")
            else if isWeekend
                ctx := "🌙 WEEKEND · thin flow"
            else if asiaBreakLong
                ctx := "🌏 ASIA RANGE BREAK ↑"
            else if asiaBreakShort
                ctx := "🌏 ASIA RANGE BREAK ↓"
            else if useHs and ((hsT_state == 3 and bar_index - hsT_broke <= 5) or (hsB_state == 3 and bar_index - hsB_broke <= 5))
                ctx := "⚠ H&S " + (hsT_state == 3 ? "TOP BROKEN" : "BOTTOM BROKEN")
            else if pendingBuy
                ctx := "🟢 BUY READY " + str.tostring(buyScore, "#") + "% · " + buyWhy
            else if pendingSell
                ctx := "🔴 SELL READY " + str.tostring(sellScore, "#") + "% · " + sellWhy
            else if squeeze30
                ctx := "🟡 Compression — breakout watch"
            else if regimeVolatile
                ctx := "🟣 Volatile expansion"
            else if inPremium
                ctx := "Premium zone — favor shorts"
            else if inDiscount
                ctx := "Discount zone — favor longs"
            narrative := narrative + " · " + ctx

        var label narLabel = na
        if not na(narLabel)
            label.delete(narLabel)
        narrative := "🧭 " + modeName + " · " + narrative
        narLabel := label.new(bar_index + 1, close, narrative, color=color.rgb(8, 8, 12), textcolor=color.white, style=label.style_label_left, size=size.small, textalign=text.align_left)

f_drawNarrative()

// ╔═══════════════════════════════════════════════════════════════════╗
// ║                          END OF SCRIPT                            ║
// ╚═══════════════════════════════════════════════════════════════════╝

Overview

GOD Conviction Engine combines trend EMAs, an HMA baseline, an elastic EMA, order blocks, fair value gaps, breaker blocks, liquidity sweeps, an Asian range box, key levels, VWAP, market structure and several tables including a confluence radar and execution panel. Risk profiles set a confluence threshold, and ADX, ATR and EMA lengths are adjustable. Most features can be toggled, and the dense output may be heavy to read.

Details

Published by

Oluwacutyp on GitHub

Code credit

OMNI-GOD ∞ [UNIFIED MASTER SUITE — Gold & BTC Adaptive], as written in the script's header

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

SMC / ICT / price action, Trend, Sessions & time, Screeners & dashboards

Markets

Gold, Crypto

Timeframes

Any timeframe

Risk Notes

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

Settings

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

SettingTypeDefaultVariable
Show Trend EMAsbooltrueshowEmas
Show HMA 50 BaselinebooltrueshowHma
Show Elastic EMAbooltrueshowElastic
Show Order Blocks (OB)booltrueshowOB
Show Fair Value Gaps (FVG)booltrueshowFVG
Show Breaker Blocks (RBS/SBR)booltrueshowBreakers
Show Liquidity SweepsbooltrueshowSweeps
Show Asian Range Box (00-07 UTC)booltrueshowAsian
Show Key OHLC & Pivot LinesbooltrueshowLevels
Show PD Array BandsbooltrueshowPD
Show VWAP LinebooltrueshowVwap
Show Structure (BOS/CHoCH/Swings)booltrueshowMktStr
Show all 40 settings
SettingTypeDefaultVariable
Show Quantum Field GravitybooltrueshowQuantum
Show Dark Pool LevelsbooltrueshowDarkPool
Show Liquidity Heat ProjectionbooltrueshowLiqHeat
Show Confluence HeatmapbooltrueshowHeatmap
Show Narrative Voice LabelbooltrueshowNarrative
Show Execution HUD (position: Layout group)booltrueshowHUD
Show Confluence Radar (position: Layout group)booltrueshowRadar
Show Analysis Table (position: Layout group)booltrueshowAnalysis
Show Regime Label On ChartbooltrueshowRegime
Show Session BackgroundsboolfalseshowKillzones
Fast EMAint9fastLen
Medium EMAint21medLen
Slow EMAint50slowLen
Trend EMAint200trendLen
Non-Lag Baseline (HMA)int50hmaLen
ATR Lengthint14atrLen
ADX Lengthint14adxLen
ADX Min Strengthint18adxMin
Z-Score Windowint20zLen
Volume SMA Lengthint20volLen
Volume Imbalance (2.0x SMA)booltrueuseVolImb
Require Volume ConfirmationboolfalseuseVolConf
Killzone Session Filter (London+NY)boolfalseuseKillzone
BTC Weekend Thin-Liquidity FilterbooltrueuseBtcWeekend
Ichimoku: Show Clouds (Daily + TF)booltrueshowIchimoku
Ichimoku: Score Cloud ContextbooltrueuseIchimoku
H&S: Show Pattern Labels & NecklinebooltrueshowHs
H&S: Score Neckline BreaksbooltrueuseHs
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

This indicator overlays a market structure dashboard on the price chart. It reads up to seven user-selected timeframes, each with an on/off switch and a trend weight, and shows columns for EMA trend, swing highs and lows, structure, order blocks, and fair valu

Indicator v6 Screeners & dashboardsSMC / ICT / price action
Updated 3 months ago · None stated GitHub repository of Market Structure Dashboard | Flux Charts

Auric Sentinel

by arslanahmad429
1 stars

A multi-layer overlay that combines an EMA ribbon with a higher-timeframe EMA and price context, swing-based order blocks, a premium and discount zone, session and volatility filters, and a risk table. Settings choose the EMA lengths, swing bars, invalidation

Indicator v6 SMC / ICT / price actionTrend
Updated 4 months ago · None stated GitHub repository of Auric Sentinel

Jigsa God Mode Pro

by jigsabersisa
1 stars

This indicator overlays a 50 and 200 EMA trend filter, RSI confirmation and pivot-based zones on the price chart, and prints buy and sell signals with projected entry, ATR-based stop loss and risk to reward take profit lines. Optional layers include liquidity

Indicator v5 SMC / ICT / price actionTrend
Updated 6 months ago · GPL-3.0 GitHub repository of Jigsa God Mode Pro
1 stars

This overlay indicator maps smart-money market structure on the price chart. It labels swing points as higher highs, higher lows, lower highs, and lower lows, and draws Break of Structure and Change of Character lines when structure breaks. A multi-timeframe t

Indicator v6 SMC / ICT / price actionScreeners & dashboards
Updated 4 months ago · None stated GitHub repository of SMC Market Structure + MTF
257 stars

This indicator overlays an intraday SPX and ES chart with a midnight session range, regular-trading-hours opening levels, and a VWAP projection box. It draws the midnight high, mid, and low, flags edge rejections within an absolute tolerance, and plots optiona

Indicator v6 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of SPX Midnight Box + Snag Logic

Golden Bullet Strategy

by TradersPost
218 stars

The Golden Bullet Strategy is an overlay signal indicator based on Smart Money Concepts and time-based liquidity models. It identifies liquidity sweeps of a designated reference session range during a specified hunt window. It then confirms Market Structure Sh

Indicator v6 SMC / ICT / price actionSessions & time
Updated 3 months ago · None stated GitHub repository of Golden Bullet Strategy

More indicators

Trade Your Charts With a Regulated Broker

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

Compare brokers