Indicator · Pine Script v6 · On the chart

SMC Structure & Expectation [WavesUnchained]

by casoon

This indicator plots a four-layer Smart Money Concepts framework directly on the price chart.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code2,113
Settings40inputs
LicenceMIT
Last updateyesterday27 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

2,113 lines of Pine Script v6 from indicators/market_structure/smc_structure_expectation/smc_structure_expectation.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

smc_structure_expectation.pine
//@version=6
// ============================================================================
// SMC Structure & Expectation [WavesUnchained]
// Version: 1.1.6
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Context & Confirmation Tool: Market Structure + Expectation + BOS Validation + Professional Order Block System
//
// Features:
//   ✓ Layer 1: Objective Market Structure (HH/HL/LH/LL based on confirmed swings)
//   ✓ Layer 2: Logical Expectation/Bias (Expect HL, LH, Continuation, Failure)
//   ✓ Layer 3: Selective Confirmation (BOS + Professional Order Block System)
//   ✓ Layer 4: Optional Warnings (Trendline breaks, Early Top/Bottom)
//   ✓ Professional OB Filters: BOS-only, Displacement (1.2 ATR), Location (Premium/Discount), Max 1 OB per direction, TTL, Mitigation tracking
//   ✓ No signal spam - Context over quantity
//   ✓ Timeframe-adaptive settings (1H optimized for NatGas/Commodities)
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   real trade volume only — OB bonus/filter degrades to neutral
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
//
// ============================================================================
indicator("SMC Structure & Expectation [WavesUnchained]", shorttitle="SMC-SE", overlay=true, max_labels_count=100, max_boxes_count=50, max_lines_count=300)

// ═══════════════════════════════════════════════════════════════════════════
// SMC STRUCTURE & EXPECTATION - 4-LAYER FRAMEWORK
// ═══════════════════════════════════════════════════════════════════════════
// Version: 1.1.0
// Author: WavesUnchained
//
// ARCHITECTURE: Strict Layer Separation
//
// Layer 1: Market Structure (HH/HL/LH/LL)
//   ✓ Objective structure detection from confirmed swings
//   ✓ No predictions, only descriptions
//   ✓ Foundation for all other layers
//
// Layer 2: Expectation/Bias
//   ✓ Logical hypotheses derived from structure
//   ✓ "Expect HL", "Expect LH", "Expect Continuation"
//   ✓ Expectation ≠ Signal (can be wrong)
//
// Layer 3: Confirmation (BOS + Order Blocks)
//   ✓ BOS: Break of Structure (pivot-based)
//   ✓ Professional OB System with 8 filters:
//     1. BOS-only (no OB without BOS)
//     2. Displacement (min 1.2 ATR)
//     3. Location (Premium/Discount zones)
//     4. Range limits (0.3-1.0 ATR)
//     5. Max 1 OB per direction (replace logic)
//     6. FVG overlap (optional)
//     7. Mitigation tracking (30% penetration)
//     8. TTL (80 bars default)
//
// Layer 4: Warnings (Optional, Subtle)
//   ✓ Trendline breaks
//   ✓ Early top/bottom signals
//   ✓ NO alerts on warnings
//   ✓ Visual only, does NOT change expectation
//
// PHILOSOPHY:
// - Context over quantity
// - Quality over signals
// - No signal spam
// - This is a TOOL, not an algorithm
//
// ═══════════════════════════════════════════════════════════════════════════

// NOTE: Modules will be combined by build system
// Individual module imports shown here for reference only

// ============================================================================
// INLINE MODULES (Combined at build time)
// ============================================================================

// ============================================================================
// MODULE: inputs
// ============================================================================

// ==== INPUTS MODULE ====
// User configuration for SMC Structure & Expectation

// ==== GROUP: DEBUG & LOGGING ====
bool enableDebugLog = input.bool(false, "Enable Debug Logging", group="Debug", tooltip="Log structure events, BOS triggers, OB creation/rejection to Pine Logs. Useful for debugging.")
string logLevel = input.string("Smart", "Log Level", options=["Smart", "HIGH Only", "MED+", "All"], group="Debug", tooltip="Smart = Priority-based filtering, HIGH Only = >80 score, MED+ = >50, All = everything")
bool logStructure = input.bool(true, "Log Structure Events", group="Debug")
bool logExpectation = input.bool(true, "Log Expectation Changes", group="Debug")
bool logBOS = input.bool(true, "Log BOS Events", group="Debug")
bool logOB = input.bool(true, "Log OB Lifecycle", group="Debug")
bool logRejectReasons = input.bool(false, "Log OB Reject Reasons", group="Debug", tooltip="Show why OBs were rejected (useful for tuning filters)")

// ==== GROUP: BAR COLORING ====
string barColorMode = input.string("BOS + OB Priority", "Bar Color Mode", options=["Off", "Structure", "Expectation", "BOS + OB Priority", "Multi-Layer", "Alignment"], group="Bar Coloring", tooltip="Off = No coloring | Structure = Bullish/Bearish/Neutral | Expectation = Continuation zones | BOS + OB Priority = HIGH/MED/LOW priority | Multi-Layer = Combined | Alignment = Internal/External alignment")

// ==== GROUP: LAYER TOGGLES ====
bool showLayer1 = input.bool(true, "Show Layer 1 - Market Structure", group="Layer Control")
bool showLayer2 = input.bool(true, "Show Layer 2 - Expectation", group="Layer Control")
bool showLayer3 = input.bool(true, "Show Layer 3 - Confirmation (BOS + OB)", group="Layer Control")
bool showLayer4 = input.bool(true, "Show Layer 4 - Warnings", group="Layer Control")

// ==== GROUP: LAYER 1 - MARKET STRUCTURE ====
int structureLookback = input.int(5, "External Pivot Lookback (Major Structure)", minval=2, maxval=20, group="Layer 1: Structure", tooltip="Major swings for overall bias")
int internalLookback = input.int(3, "Internal Pivot Lookback (Entry Timing)", minval=2, maxval=10, group="Layer 1: Structure", tooltip="Smaller swings for entry timing. Lower = faster signals")
bool enableInternalStructure = input.bool(true, "Enable Internal Structure", group="Layer 1: Structure", tooltip="Track smaller timeframe structure for alignment checks")
bool showStructureLabels = input.bool(true, "Show HH/HL/LH/LL Labels", group="Layer 1: Structure")
bool showSwingLines = input.bool(true, "Show Swing High/Low Lines", group="Layer 1: Structure")
color structureColorBull = input.color(color.new(color.green, 0), "Bullish Structure Color", group="Layer 1: Structure")
color structureColorBear = input.color(color.new(color.red, 0), "Bearish Structure Color", group="Layer 1: Structure")

// ==== GROUP: LAYER 2 - EXPECTATION ====
bool showExpectationLabel = input.bool(true, "Show Expectation Label", group="Layer 2: Expectation")
bool showExpectationZone = input.bool(true, "Show Expected HL/LH Zones", group="Layer 2: Expectation")
int expectationExpiry = input.int(30, "Expectation Expiry (bars)", minval=10, maxval=100, group="Layer 2: Expectation")
color expectationColorBull = input.color(color.new(color.green, 70), "Bullish Expectation Zone", group="Layer 2: Expectation")
color expectationColorBear = input.color(color.new(color.red, 70), "Bearish Expectation Zone", group="Layer 2: Expectation")

// ==== GROUP: LAYER 3 - CONFIRMATION (BOS + OB) ====
bool showBOSLabels = input.bool(true, "Show BOS Labels", group="Layer 3: Confirmation")
string bosMode = input.string("Close-cross", "BOS Mode", options=["Close-cross", "High/Low-cross"], group="Layer 3: Confirmation", tooltip="Close-cross = conservative (less fakeouts), High/Low-cross = aggressive (faster signals)")
bool requireConfirm = input.bool(true, "Aggro requires close confirmation", group="Layer 3: Confirmation", tooltip="High/Low-cross mode: require close confirmation within N bars to avoid wick fakeouts")
int confirmBars = input.int(2, "Aggro confirm bars", minval=1, maxval=5, group="Layer 3: Confirmation", tooltip="Bars to wait for close confirmation in High/Low-cross mode")
bool enableOrderBlocks = input.bool(true, "Enable Order Block Detection", group="Layer 3: Confirmation")

// OB Filter Settings
// Timeframe-adaptive defaults: 1H/4H window=3 minDisp=1.0, 15m window=4 minDisp=0.9, 5m window=5 minDisp=0.8
bool useAutoDisplacement = input.bool(true, "Auto-Adjust Displacement Settings", group="Layer 3: Confirmation", tooltip="Automatically adjusts window and min displacement based on timeframe")

// Calculate auto defaults based on timeframe
int autoWindow = timeframe.in_seconds(timeframe.period) >= 3600 ? 3 : timeframe.in_seconds(timeframe.period) >= 900 ? 4 : 5
float autoMinDisp = timeframe.in_seconds(timeframe.period) >= 3600 ? 1.0 : timeframe.in_seconds(timeframe.period) >= 900 ? 0.9 : 0.8

float minDisplacement = useAutoDisplacement ? autoMinDisp : input.float(1.0, "Min Displacement (ATR multiples)", minval=0.5, maxval=5.0, step=0.1, group="Layer 3: Confirmation", tooltip="Lower = more OBs. 1.0 is balanced, 1.2+ very selective")
int displacementWindow = useAutoDisplacement ? autoWindow : input.int(3, "Displacement Lookback (bars)", minval=1, maxval=10, group="Layer 3: Confirmation")
float minOBRange = input.float(0.3, "Min OB Range (ATR)", minval=0.1, maxval=2.0, step=0.1, group="Layer 3: Confirmation")

// Size Filter Mode
string sizeFilterMode = input.string("Score Penalty", "Size Filter Mode", options=["Hard Limit", "Score Penalty", "Disabled"], group="Layer 3: Confirmation", tooltip="Hard Limit = Reject oversized OBs | Score Penalty = Allow but reduce score | Disabled = No size filtering")

// Timeframe-adaptive maxOBRange with manual override option
bool useAutoMaxRange = input.bool(true, "Auto-Adjust Max OB Range", group="Layer 3: Confirmation", tooltip="Automatically adjusts based on timeframe: Intraday=1.3, Daily=1.0, Weekly+=1.5")
float manualMaxOBRange = input.float(1.0, "Manual Max OB Range (ATR)", minval=0.5, maxval=3.0, step=0.1, group="Layer 3: Confirmation", tooltip="Only used if Auto-Adjust is OFF")

// Calculate actual maxOBRange based on settings
float maxOBRange = useAutoMaxRange ? (timeframe.isweekly or timeframe.ismonthly ? 1.5 : timeframe.isdaily ? 1.0 : timeframe.isintraday ? 1.3 : 1.0) : manualMaxOBRange
bool requireFVG = input.bool(false, "Require FVG Overlap", group="Layer 3: Confirmation")
float obLocationThreshold = input.float(0.58, "Premium/Discount Threshold", minval=0.5, maxval=0.8, step=0.01, group="Layer 3: Confirmation", tooltip="Lower = more OBs allowed. 0.58 = NatGas default, 0.55+ strict")
int obTTL = input.int(100, "OB Time-To-Live (bars)", minval=20, maxval=300, group="Layer 3: Confirmation")
float mitigationPenetration = input.float(0.25, "Mitigation Penetration (%)", minval=0.1, maxval=1.0, step=0.05, group="Layer 3: Confirmation")
bool useVolumFilter = input.bool(false, "Require Volume > SMA(20)", group="Layer 3: Confirmation", tooltip="Default OFF - volume data often unreliable on CFDs/illiquid markets")
float volumeFloor = input.float(0.6, "Volume Floor (x SMA)", minval=0.3, maxval=1.5, step=0.1, group="Layer 3: Confirmation", tooltip="Only used if Volume Filter is ON. Soft floor instead of hard >1.0")

color obColorBull = input.color(color.new(color.green, 75), "Bullish OB Color", group="Layer 3: Confirmation")
color obColorBear = input.color(color.new(color.red, 75), "Bearish OB Color", group="Layer 3: Confirmation")

// ==== GROUP: LAYER 4 - WARNINGS ====
bool enableSwingLevelBreak = input.bool(true, "Enable Swing Level Break Warning", group="Layer 4: Warnings")
bool enableEarlyTop = input.bool(true, "Enable Early Top Warning", group="Layer 4: Warnings")
bool enableEarlyBottom = input.bool(true, "Enable Early Bottom Warning", group="Layer 4: Warnings")
float wickThreshold = input.float(0.5, "Wick Threshold (%) for Early Signals", minval=0.3, maxval=0.8, step=0.1, group="Layer 4: Warnings")
int rsiLength = input.int(14, "RSI Length", minval=5, maxval=50, group="Layer 4: Warnings")
int rsiOverbought = input.int(70, "RSI Overbought", minval=60, maxval=90, group="Layer 4: Warnings")
int rsiOversold = input.int(30, "RSI Oversold", minval=10, maxval=40, group="Layer 4: Warnings")
int warningCooldown = input.int(30, "Warning Cooldown (bars)", minval=10, maxval=100, group="Layer 4: Warnings")

// ==== GROUP: DISPLAY & DASHBOARD ====
string dashPosition = input.string("Off", "Dashboard Position", options=["Top Left", "Top Right", "Bottom Left", "Bottom Right", "Off"], group="Display")
bool showDashboard = dashPosition != "Off"
bool minimalMode = input.bool(false, "Minimal Mode (hide most labels)", group="Display")

// Reject Heatmap
bool showRejectHeatmap = input.bool(false, "Show Reject Heatmap", group="Display", tooltip="Shows detailed OB rejection statistics with heat visualization")
string heatmapCorner = input.string("Bottom Right", "Heatmap Position", options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group="Display")

// Pending BOS Visualization
bool showPendingViz = input.bool(true, "Show Pending BOS Visuals", group="Display", tooltip="Shows pending BOS state with level line and countdown in Aggro mode with confirmation")
string pendingLineStyle = input.string("Dotted", "Pending Line Style", options=["Solid", "Dashed", "Dotted"], group="Display")

// ==== GROUP: ALERTS ====
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool onlyHighPriorityAlerts = input.bool(true, "Only HIGH Priority OB Alerts (>80)", group="Alerts", tooltip="Filter alerts to only HIGH priority setups. While on, the standard 'Bullish/Bearish Confirmation' alerts (priority 80 or below) never fire — use the 'HIGH Priority' alerts instead.")

// ==== GROUP: TECHNICAL SETTINGS ====
int atrLength = input.int(14, "ATR Length", minval=5, maxval=50, group="Technical")
int volumeLength = input.int(20, "Volume SMA Length", minval=10, maxval=100, group="Technical")

// ==== GROUP: JMA OVERLAY ====
bool showJMA = input.bool(true, "Show JMA Lines", group="JMA Overlay")
int jmaLen1 = input.int(20, "JMA 1 Length", minval=5, maxval=100, group="JMA Overlay")
int jmaLen2 = input.int(75, "JMA 2 Length", minval=5, maxval=200, group="JMA Overlay")
int jmaLen3 = input.int(200, "JMA 3 Length", minval=5, maxval=500, group="JMA Overlay")
color jmaColor1 = input.color(color.new(color.red, 0), "JMA 1 Color (20)", group="JMA Overlay")
color jmaColor2 = input.color(color.new(color.blue, 0), "JMA 2 Color (75)", group="JMA Overlay")
color jmaColor3 = input.color(color.new(color.green, 0), "JMA 3 Color (200)", group="JMA Overlay")


// ============================================================================
// MODULE: layer1_structure
// ============================================================================

// ==== LAYER 1: MARKET STRUCTURE ====
// Objective market structure detection: HH/HL/LH/LL based on confirmed swings
// Dual-layer structure: External (major swings) + Internal (timing)

// === PIVOT DETECTION (Foundation) ===
// External Structure (Major Swings)
float pivotHigh = ta.pivothigh(high, structureLookback, structureLookback)
float pivotLow = ta.pivotlow(low, structureLookback, structureLookback)

// Internal Structure (Entry Timing)
float internalPivotHigh = enableInternalStructure ? ta.pivothigh(high, internalLookback, internalLookback) : na
float internalPivotLow = enableInternalStructure ? ta.pivotlow(low, internalLookback, internalLookback) : na

// === SWING STATE TRACKING (EXTERNAL) ===
var float lastConfirmedSwingHigh = na
var float lastConfirmedSwingLow = na
var int lastSwingHighBar = na
var int lastSwingLowBar = na

// === STRUCTURE STATE (EXTERNAL - Descriptive, not predictive) ===
var string currentStructure = "Neutral"  // "HH/HL" or "LH/LL" or "Neutral"

// === SWING TYPE CLASSIFICATION (EXTERNAL) ===
var string lastSwingType = ""  // "HH", "HL", "LH", "LL" (for display/labels)
var string lastHighType = ""   // "HH" or "LH" (separate tracking for highs)
var string lastLowType = ""    // "HL" or "LL" (separate tracking for lows)

// === INTERNAL STRUCTURE STATE ===
var float internalLastSwingHigh = na
var float internalLastSwingLow = na
var string internalStructure = "Neutral"  // "HH/HL" or "LH/LL" or "Neutral"
var string internalHighType = ""  // "HH" or "LH"
var string internalLowType = ""   // "HL" or "LL"

// Swings as they stood before this bar's pivots are processed — the opposite
// swing preceding a newly confirmed pivot (used for BOS leg strength)
float swingHighBeforeUpdate = lastConfirmedSwingHigh
float swingLowBeforeUpdate = lastConfirmedSwingLow

// === UPDATE STRUCTURE ON NEW PIVOTS ===
// CRITICAL FIX: Use pivotHigh/pivotLow price and bar_index - structureLookback
if not na(pivotHigh)
    float previousHigh = lastConfirmedSwingHigh

    // Classify this high
    if na(previousHigh)
        lastSwingType := "First High"
        lastHighType := "First High"
        currentStructure := "Neutral"
    else if pivotHigh > previousHigh
        lastSwingType := "HH"
        lastHighType := "HH"
        // HH can occur in both bullish and bearish structures
        // Don't change structure based on highs alone
    else if pivotHigh < previousHigh
        lastSwingType := "LH"
        lastHighType := "LH"
        // LH suggests potential bearish structure
        if currentStructure != "LH/LL"
            currentStructure := "LH/LL"

    // Store confirmed swing - CRITICAL: Use pivot price and offset bar
    lastConfirmedSwingHigh := pivotHigh
    lastSwingHighBar := bar_index - structureLookback

    // DEBUG LOG
    if enableDebugLog and logStructure
        log.info(str.format("STRUCTURE: {0} @ {1} | Pivot: {2} | Bar: {3}",
            lastSwingType, syminfo.ticker, str.tostring(pivotHigh, format.mintick),
            str.tostring(lastSwingHighBar)))

if not na(pivotLow)
    float previousLow = lastConfirmedSwingLow

    // Classify this low
    if na(previousLow)
        lastSwingType := "First Low"
        lastLowType := "First Low"
        currentStructure := "Neutral"
    else if pivotLow > previousLow
        lastSwingType := "HL"
        lastLowType := "HL"
        // HL suggests potential bullish structure
        if currentStructure != "HH/HL"
            currentStructure := "HH/HL"
    else if pivotLow < previousLow
        lastSwingType := "LL"
        lastLowType := "LL"
        // LL can occur in both bullish and bearish structures
        // Don't change structure based on lows alone

    // Store confirmed swing - CRITICAL: Use pivot price and offset bar
    lastConfirmedSwingLow := pivotLow
    lastSwingLowBar := bar_index - structureLookback

    // DEBUG LOG
    if enableDebugLog and logStructure
        log.info(str.format("STRUCTURE: {0} @ {1} | Pivot: {2} | Bar: {3}",
            lastSwingType, syminfo.ticker, str.tostring(pivotLow, format.mintick),
            str.tostring(lastSwingLowBar)))

// === INTERNAL STRUCTURE UPDATE ===
if enableInternalStructure
    if not na(internalPivotHigh)
        float prevIntHigh = internalLastSwingHigh

        if not na(prevIntHigh)
            if internalPivotHigh > prevIntHigh
                internalHighType := "HH"
                // Don't change structure on highs alone
            else
                internalHighType := "LH"
                if internalStructure != "LH/LL"
                    internalStructure := "LH/LL"

        internalLastSwingHigh := internalPivotHigh

    if not na(internalPivotLow)
        float prevIntLow = internalLastSwingLow

        if not na(prevIntLow)
            if internalPivotLow > prevIntLow
                internalLowType := "HL"
                if internalStructure != "HH/HL"
                    internalStructure := "HH/HL"
            else
                internalLowType := "LL"
                // Don't change structure on lows alone

        internalLastSwingLow := internalPivotLow

// === STRUCTURE ALIGNMENT CHECK ===
bool structureAligned = false
bool structureConflict = false

if enableInternalStructure
    bool extBullish = currentStructure == "HH/HL"
    bool extBearish = currentStructure == "LH/LL"
    bool intBullish = internalStructure == "HH/HL"
    bool intBearish = internalStructure == "LH/LL"

    structureAligned := (extBullish and intBullish) or (extBearish and intBearish)
    structureConflict := (extBullish and intBearish) or (extBearish and intBullish)

// === STRUCTURE DESCRIPTION (For Layer 2) ===
bool isBullishStructure = currentStructure == "HH/HL"
bool isBearishStructure = currentStructure == "LH/LL"
bool isNeutralStructure = currentStructure == "Neutral"

// === EXPORT VARIABLES FOR OTHER LAYERS ===
// These variables are used by Layer 2 (Expectation) and Layer 3 (Confirmation)


// ============================================================================
// MODULE: layer2_expectation
// ============================================================================

// ==== LAYER 2: EXPECTATION / BIAS ====
// Derive logical expectations from market structure
// Expectation is a hypothesis, not a signal

// === EXPECTATION STATES ===
var string currentExpectation = "None"  // "Expect HL", "Expect LH", "Expect Continuation", "Expect Failure"
var float expectationPrice = na         // Reference price for expectation
var int expectationBar = na             // Bar where expectation was set
var bool expectationActive = false

// === EXPECTATION TYPES ===
bool expectHL = false           // Expecting Higher Low (bullish reversal pattern)
bool expectLH = false           // Expecting Lower High (bearish reversal pattern)
bool expectContinuation = false // Expecting trend continuation
bool expectFailure = false      // Expecting structure failure

// === DERIVE EXPECTATION FROM STRUCTURE ===

// When we see HH in bullish structure → expect HL next
if not na(pivotHigh) and lastSwingType == "HH" and isBullishStructure
    if not expectationActive or currentExpectation != "Expect HL"
        currentExpectation := "Expect HL"
        expectationPrice := lastConfirmedSwingLow  // Reference: previous swing low
        expectationBar := bar_index
        expectationActive := true
        expectHL := true
        expectLH := false
        expectContinuation := false
        expectFailure := false

        // DEBUG LOG
        if enableDebugLog and logExpectation
            log.info(str.format("EXPECTATION: Expect HL | Ref: {0} | Structure: Bullish",
                str.tostring(expectationPrice, format.mintick)))

// When we see LL in bearish structure → expect LH next
if not na(pivotLow) and lastSwingType == "LL" and isBearishStructure
    if not expectationActive or currentExpectation != "Expect LH"
        currentExpectation := "Expect LH"
        expectationPrice := lastConfirmedSwingHigh  // Reference: previous swing high
        expectationBar := bar_index
        expectationActive := true
        expectHL := false
        expectLH := true
        expectContinuation := false
        expectFailure := false

        // DEBUG LOG
        if enableDebugLog and logExpectation
            log.info(str.format("EXPECTATION: Expect LH | Ref: {0} | Structure: Bearish",
                str.tostring(expectationPrice, format.mintick)))

// When we see HL in bullish structure → expect continuation (HH next)
if not na(pivotLow) and lastSwingType == "HL" and isBullishStructure
    if not expectationActive or currentExpectation != "Expect Continuation"
        currentExpectation := "Expect Continuation"
        expectationPrice := lastConfirmedSwingHigh  // Need to break above this
        expectationBar := bar_index
        expectationActive := true
        expectHL := false
        expectLH := false
        expectContinuation := true
        expectFailure := false

// When we see LH in bearish structure → expect continuation (LL next)
if not na(pivotHigh) and lastSwingType == "LH" and isBearishStructure
    if not expectationActive or currentExpectation != "Expect Continuation"
        currentExpectation := "Expect Continuation"
        expectationPrice := lastConfirmedSwingLow  // Need to break below this
        expectationBar := bar_index
        expectationActive := true
        expectHL := false
        expectLH := false
        expectContinuation := true
        expectFailure := false

// === EXPECTATION EXPIRY ===
// Expectation changes only through structure break, but we add timeout for clean UX
bool expectationExpired = false

if expectationActive and not na(expectationBar)
    int barsSinceExpectation = bar_index - expectationBar

    // Expire if too many bars passed without confirmation
    if barsSinceExpectation > expectationExpiry
        expectationActive := false
        currentExpectation := "None"
        expectationPrice := na
        expectationExpired := true

// Flags mirror the persistent expectation state on every bar — set only inside the
// derivation blocks above they were one-bar pulses, so BOS / invalidation / bar color
// could only see them on the bar the expectation was set.
expectHL           := expectationActive and currentExpectation == "Expect HL"
expectLH           := expectationActive and currentExpectation == "Expect LH"
expectContinuation := expectationActive and currentExpectation == "Expect Continuation"
expectFailure      := expectationActive and currentExpectation == "Expect Failure"

// === EXPECTATION INVALIDATION ===
// Expectation is invalidated by structure break in opposite direction

// Invalidate "Expect HL" if price breaks below reference low
if expectHL and not na(expectationPrice)
    if close < expectationPrice
        currentExpectation := "Expect Failure"
        expectHL := false
        expectFailure := true

// Invalidate "Expect LH" if price breaks above reference high
if expectLH and not na(expectationPrice)
    if close > expectationPrice
        currentExpectation := "Expect Failure"
        expectLH := false
        expectFailure := true

// === EXPORT FOR LAYER 3 ===
// Layer 3 (Confirmation) uses these expectations to filter BOS and OB signals


// ============================================================================
// MODULE: layer3_confirmation
// ============================================================================

// ==== LAYER 3: CONFIRMATION ====
// Selective BOS + Professional Order Block System
// Only confirms signals that align with expectation

// === TECHNICAL INDICATORS ===
float atr = ta.atr(atrLength)
float volumeSMA = ta.sma(volume, volumeLength)

// Real trade volume only — tick/n/a volume carries no participation signal
// (DATA_VALIDITY.md §2/§7). relVol degrades to na when not real, which makes
// every relVol-based bonus tier below fall through to +0 (na comparisons are
// false in Pine) instead of scoring a fabricated tick-count signal.
bool volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)

// === BOS DETECTION (Major Swings Only) ===
// BOS only on confirmed structure swings (HH/HL/LH/LL), not every pivot
// This prevents "mini-BOS" spam in sideways/chop

var float bosSwingHigh = na
var float bosSwingLow = na

// Opposite swing preceding each BOS level (start of the impulse leg)
var float bosPriorLow = na
var float bosPriorHigh = na

// Store bar indices at BOS trigger time (for Range-Mismatch check in OB creation)
var int bosTriggerHighBar = na
var int bosTriggerLowBar = na

// Relative Leg Strength (Impulse vs Correction ratio)
var float currentLegStrength = na

// === INTELLIGENT LOGGING SYSTEM ===
// Priority-based log filtering to reduce noise

// Check if log should be shown based on priority and log level
shouldLog(float priority) =>
    bool show = false

    if logLevel == "All"
        show := true
    else if logLevel == "MED+"
        show := priority >= 50
    else if logLevel == "HIGH Only"
        show := priority >= 80
    else  // "Smart" mode
        // Smart: Always show HIGH, show MED if detail enabled, LOW only in full debug
        show := priority >= 80 or (priority >= 50 and enableDebugLog) or (priority < 50 and enableDebugLog and logOB)

    show

// Generate compact context summary for logs
getContextSummary(float priority, float legStrength, bool aligned, bool conflict) =>
    string priorityTag = priority > 80 ? "HIGH" : priority > 50 ? "MED" : "LOW"
    string alignTag = enableInternalStructure ? (aligned ? "ALN" : conflict ? "CNF" : "NEU") : ""
    string strengthTag = na(legStrength) ? "" : legStrength > 1.5 ? "STR" : legStrength < 1.0 ? "WEK" : ""

    string summary = priorityTag
    if alignTag != ""
        summary := summary + " " + alignTag
    if strengthTag != ""
        summary := summary + " " + strengthTag

    summary

// Calculate full potential score (used for reject logging)
// Simulates complete scoring as if setup passed all filters
calculatePotentialScore(float disp, float relVol, bool hasFVG, bool isBull, float obBottom, float obTop, float rangeHigh, float rangeLow) =>
    float score = 15.0  // Base score

    // Displacement bonus
    if disp >= 2.0
        score += 30.0
    else if disp >= 1.5
        score += 20.0
    else
        score += 10.0

    // FVG bonus
    if hasFVG
        score += 20.0

    // Volume bonus
    if relVol > 2.0
        score += 15.0
    else if relVol > 1.5
        score += 10.0
    else if relVol > 1.0
        score += 5.0

    // Location bonus (if BOS range exists)
    if not na(rangeHigh) and not na(rangeLow)
        float eq = (rangeHigh + rangeLow) / 2
        if isBull
            if obBottom < rangeLow + (rangeHigh - rangeLow) * 0.38
                score += 20.0
            else if obBottom < eq
                score += 10.0
        else
            if obTop > rangeLow + (rangeHigh - rangeLow) * 0.62
                score += 20.0
            else if obTop > eq
                score += 10.0

    score

// === OB PRIORITY SCORE CALCULATION ===
// Combines all context attributes into priority score (0-100)
// MUST be defined before OB creation
calculateOBPriority(float baseScore) =>
    float priority = baseScore  // Start with base score (displacement, location, etc.)

    // Structure Alignment: +30 if aligned, -15 if conflict
    if enableInternalStructure
        if structureAligned
            priority += 30
        else if structureConflict
            priority -= 15

    // Leg Strength: +25 for STRONG, +15 for normal, 0 for weak
    if not na(currentLegStrength)
        if currentLegStrength > 1.5
            priority += 25
        else if currentLegStrength > 1.0
            priority += 15

    // Cap at 100
    priority := math.min(priority, 100)
    priority

// Update BOS levels on MAJOR swings (structure confirmation)
// Track swing points from Layer 1 structure, not every pivot
// CRITICAL: Use separate lastHighType/lastLowType to avoid overwrite issues
if not na(pivotHigh) and (lastHighType == "HH" or lastHighType == "LH")
    bosSwingHigh := pivotHigh
    bosPriorLow := swingLowBeforeUpdate

if not na(pivotLow) and (lastLowType == "HL" or lastLowType == "LL")
    bosSwingLow := pivotLow
    bosPriorHigh := swingHighBeforeUpdate

// === BOS CONTEXT RANGE (For accurate Premium/Discount calculation) ===
// Store the range that belongs to THIS specific BOS
var float bosRangeHigh = na
var float bosRangeLow = na

// === BOS LOGIC ===
bool bullishBOS = false
bool bearishBOS = false
int bosBar = na

// BOS Rejection Counter (declared early for use in BOS blocks)
int rejectRangeMismatch = 0
rejectRangeMismatch := nz(rejectRangeMismatch[1], 0)

// === AGGRO-BOS PENDING STATE MACHINE ===
// When High/Low-cross + requireConfirm enabled: wait for close confirmation within N bars
var bool pendingBull = false
var bool pendingBear = false
var int pendingUntilBull = na
var int pendingUntilBear = na
var float pendingLevelBull = na
var float pendingLevelBear = na
var int pendingSwingBarBull = na
var int pendingSwingBarBear = na

// BOS Event detection: Conservative (Close-cross) vs Aggressive (High/Low-cross)
bool bullBOSEvent = false
bool bearBOSEvent = false
bool aggroMode = bosMode == "High/Low-cross"

// Cross events computed unconditionally at global scope — ta.crossover/crossunder
// inside conditional branches would build inconsistent history
bool crossCloseOverHigh = ta.crossover(close, bosSwingHigh)
bool crossHighOverHigh  = ta.crossover(high, bosSwingHigh)
bool crossCloseUnderLow = ta.crossunder(close, bosSwingLow)
bool crossLowUnderLow   = ta.crossunder(low, bosSwingLow)

// === BULLISH BOS EVENT ===
if not na(bosSwingHigh)
    if bosMode == "Close-cross"
        // Conservative: Close-cross triggers immediate BOS
        if crossCloseOverHigh
            bullBOSEvent := true
            bosTriggerHighBar := lastSwingHighBar
    else
        // Aggressive: High-cross
        if requireConfirm
            // With confirmation: Enter pending state on wick cross
            if crossHighOverHigh and not pendingBull
                pendingBull := true
                pendingUntilBull := bar_index + confirmBars
                pendingLevelBull := bosSwingHigh
                pendingSwingBarBull := lastSwingHighBar

                if enableDebugLog and logBOS
                    log.info(str.format("BOS PENDING (BULL): Wick crossed {0}, waiting for close confirmation within {1} bars",
                        str.tostring(bosSwingHigh, format.mintick), str.tostring(confirmBars)))
        else
            // Without confirmation: High-cross triggers immediate BOS
            if crossHighOverHigh
                bullBOSEvent := true
                bosTriggerHighBar := lastSwingHighBar

// === BEARISH BOS EVENT ===
if not na(bosSwingLow)
    if bosMode == "Close-cross"
        // Conservative: Close-cross triggers immediate BOS
        if crossCloseUnderLow
            bearBOSEvent := true
            bosTriggerLowBar := lastSwingLowBar
    else
        // Aggressive: Low-cross
        if requireConfirm
            // With confirmation: Enter pending state on wick cross
            if crossLowUnderLow and not pendingBear
                pendingBear := true
                pendingUntilBear := bar_index + confirmBars
                pendingLevelBear := bosSwingLow
                pendingSwingBarBear := lastSwingLowBar

                if enableDebugLog and logBOS
                    log.info(str.format("BOS PENDING (BEAR): Wick crossed {0}, waiting for close confirmation within {1} bars",
                        str.tostring(bosSwingLow, format.mintick), str.tostring(confirmBars)))
        else
            // Without confirmation: Low-cross triggers immediate BOS
            if crossLowUnderLow
                bearBOSEvent := true
                bosTriggerLowBar := lastSwingLowBar

// === PENDING CONFIRMATION CHECK ===
// Check if close confirms pending BOS within timeout window
if pendingBull
    // Check timeout
    if bar_index > pendingUntilBull
        pendingBull := false
        pendingUntilBull := na
        pendingLevelBull := na
        pendingSwingBarBull := na

        if enableDebugLog and logBOS
            log.info("BOS PENDING TIMEOUT (BULL): No close confirmation within window, fake BOS")

    // Check close confirmation
    else if close > pendingLevelBull
        bullBOSEvent := true
        pendingBull := false
        pendingUntilBull := na

        // Calculate preliminary priority for log filtering (use base score 50 as estimate)
        float prelimPriority = calculateOBPriority(50)

        if enableDebugLog and logBOS and shouldLog(prelimPriority)
            string context = getContextSummary(prelimPriority, currentLegStrength, structureAligned, structureConflict)
            log.info(str.format("BOS CONFIRMED (BULL): {0} | Close {1} > Level {2} | Strength: {3}",
                context,
                str.tostring(close, format.mintick), str.tostring(pendingLevelBull, format.mintick),
                str.tostring(currentLegStrength, "#.##")))

        // Store trigger bar for OB creation
        bosTriggerHighBar := pendingSwingBarBull

        pendingLevelBull := na
        pendingSwingBarBull := na

if pendingBear
    // Check timeout
    if bar_index > pendingUntilBear
        pendingBear := false
        pendingUntilBear := na
        pendingLevelBear := na
        pendingSwingBarBear := na

        if enableDebugLog and logBOS
            log.info("BOS PENDING TIMEOUT (BEAR): No close confirmation within window, fake BOS")

    // Check close confirmation
    else if close < pendingLevelBear
        bearBOSEvent := true
        pendingBear := false
        pendingUntilBear := na

        // Calculate preliminary priority for log filtering (use base score 50 as estimate)
        float prelimPriority = calculateOBPriority(50)

        if enableDebugLog and logBOS and shouldLog(prelimPriority)
            string context = getContextSummary(prelimPriority, currentLegStrength, structureAligned, structureConflict)
            log.info(str.format("BOS CONFIRMED (BEAR): {0} | Close {1} < Level {2} | Strength: {3}",
                context,
                str.tostring(close, format.mintick), str.tostring(pendingLevelBear, format.mintick),
                str.tostring(currentLegStrength, "#.##")))

        // Store trigger bar for OB creation
        bosTriggerLowBar := pendingSwingBarBear

        pendingLevelBear := na
        pendingSwingBarBear := na

// Bullish BOS: Event-based with range validation
if bullBOSEvent
    // Only valid if we expect bullish continuation or HL formation
    if (expectHL or expectContinuation) and isBullishStructure
        // Range-Mismatch-Guard: HL must come after HH
        bool rangeOkBull = not na(lastSwingLowBar) and not na(bosTriggerHighBar) and (lastSwingLowBar > bosTriggerHighBar)

        if rangeOkBull
            bullishBOS := true
            bosBar := bar_index

            // Set BOS context range: HH that was broken + current HL
            bosRangeHigh := bosSwingHigh
            bosRangeLow := lastConfirmedSwingLow

            // Relative Leg Strength: impulse (prior low -> broken high)
            // vs correction (broken high -> HL), in both BOS modes
            float impulseRange = bosSwingHigh - bosPriorLow
            float correctionRange = bosSwingHigh - lastConfirmedSwingLow
            currentLegStrength := not na(impulseRange) and correctionRange > 0 ? impulseRange / correctionRange : na

            // DEBUG LOG
            if enableDebugLog and logBOS
                string reason = expectHL ? "Expect HL" : "Expect Continuation"
                log.info(str.format("BOS: Bullish @ {0} | Level: {1} | Mode: {2} | Reason: {3}",
                    str.tostring(close, format.mintick), str.tostring(bosSwingHigh, format.mintick), bosMode, reason))

            // Consume BOS level (prevent re-trigger)
            bosSwingHigh := na
        else
            rejectRangeMismatch += 1
            if enableDebugLog and logBOS
                log.info("BOS REJECTED: Range mismatch (HL must come after HH)")

// Bearish BOS: Event-based with range validation
if bearBOSEvent
    // Only valid if we expect bearish continuation or LH formation
    if (expectLH or expectContinuation) and isBearishStructure
        // Range-Mismatch-Guard: LH must come after LL
        bool rangeOkBear = not na(lastSwingHighBar) and not na(bosTriggerLowBar) and (lastSwingHighBar > bosTriggerLowBar)

        if rangeOkBear
            bearishBOS := true
            bosBar := bar_index

            // Set BOS context range: LL that was broken + current LH
            bosRangeLow := bosSwingLow
            bosRangeHigh := lastConfirmedSwingHigh

            // Relative Leg Strength: impulse (prior high -> broken low)
            // vs correction (broken low -> LH), in both BOS modes
            float impulseRange = bosPriorHigh - bosSwingLow
            float correctionRange = lastConfirmedSwingHigh - bosSwingLow
            currentLegStrength := not na(impulseRange) and correctionRange > 0 ? impulseRange / correctionRange : na

            // DEBUG LOG
            if enableDebugLog and logBOS
                string reason = expectLH ? "Expect LH" : "Expect Continuation"
                log.info(str.format("BOS: Bearish @ {0} | Level: {1} | Mode: {2} | Reason: {3}",
                    str.tostring(close, format.mintick), str.tostring(bosSwingLow, format.mintick), bosMode, reason))

            // Consume BOS level (prevent re-trigger)
            bosSwingLow := na
        else
            rejectRangeMismatch += 1
            if enableDebugLog and logBOS
                log.info("BOS REJECTED: Range mismatch (LH must come after LL)")

// === DISPLACEMENT CALCULATION ===
// FIX 2: Calculate displacement over impulse window (displacementWindow bars ending at BOS)
// Not just single BOS candle - captures true momentum
calcDisplacement(int bosOffset) =>
    float displacement = 0.0

    if bar_index >= bosOffset + displacementWindow - 1
        // Measure max bar range over the displacement window (BOS bar and bars before it)
        float maxRange = 0.0
        for i = bosOffset to bosOffset + displacementWindow - 1
            float barRange = high[i] - low[i]
            if barRange > maxRange
                maxRange := barRange

        displacement := maxRange / atr

    displacement

// === FVG DETECTION (For OB Confluence) ===
bool hasBullishFVG = low > high[2]
bool hasBearishFVG = high < low[2]

// === ORDER BLOCK TYPE ===
type OrderBlock
    int creationBar       // Bar where OB was created
    float top             // OB zone top
    float bottom          // OB zone bottom
    string obType         // "BULL" or "BEAR"
    bool mitigated        // Has price penetrated the zone?
    float displacement    // Move strength after OB (ATR multiples)
    float score           // Quality score (0-100)
    bool hasFVG           // Overlaps with FVG?
    float relativeVolume  // Volume relative to SMA
    int bosBar            // Bar where BOS occurred
    box boxObj            // Visual box
    // Reaction Type Tracking
    string reactionState  // "Untouched", "Tap", "Deep", "Failure"
    float maxPenetration  // Max penetration % (0-100)
    int firstTouchBar     // Bar of first touch
    // Priority Score
    float priority        // Priority score (0-100)

// === OB STORAGE (Max 1 per direction) ===
var OrderBlock bullOB = na
var OrderBlock bearOB = na

// === OB REJECTION DEBUGGING ===
// Use simple integers that accumulate over time
int rejectDisplacement = 0
int rejectLocation = 0
int rejectVolume = 0
int rejectSize = 0
int rejectFVG = 0
int rejectSegmentNotFound = 0
int totalBOSAttempts = 0

// Initialize from previous bar
rejectDisplacement := nz(rejectDisplacement[1], 0)
rejectLocation := nz(rejectLocation[1], 0)
rejectVolume := nz(rejectVolume[1], 0)
rejectSize := nz(rejectSize[1], 0)
rejectFVG := nz(rejectFVG[1], 0)
rejectSegmentNotFound := nz(rejectSegmentNotFound[1], 0)
totalBOSAttempts := nz(totalBOSAttempts[1], 0)

// === OB SEGMENT EXTRACTION HELPER ===
// Extract OB from segment between swing and BOS
// Returns: [obTop, obBottom, obBarOffset, segmentFound]
extractOBFromSegment(bool isBull, int bosBarIndex, int swingBarIndex) =>
    int swingOffset = bar_index - swingBarIndex
    int bosOffset = bar_index - bosBarIndex
    int segmentLength = swingOffset - bosOffset

    int obBarOffset = na
    float obTop = na
    float obBottom = na
    bool segmentFound = false

    if segmentLength > 0
        // Find strongest impulse bar in segment
        int strongestBar = bosOffset
        float maxImpulse = 0.0

        if isBull
            for i = bosOffset to swingOffset - 1
                float impulse = close[i] - open[i]
                if impulse > maxImpulse
                    maxImpulse := impulse
                    strongestBar := i
        else
            for i = bosOffset to swingOffset - 1
                float impulse = open[i] - close[i]
                if impulse > maxImpulse
                    maxImpulse := impulse
                    strongestBar := i

        // Find last opposite candle before strongest impulse bar
        if isBull
            for i = strongestBar to swingOffset - 1
                if close[i] < open[i]
                    obTop := high[i]
                    obBottom := low[i]
                    obBarOffset := i
                    segmentFound := true
                    break
        else
            for i = strongestBar to swingOffset - 1
                if close[i] > open[i]
                    obTop := high[i]
                    obBottom := low[i]
                    obBarOffset := i
                    segmentFound := true
                    break

    [obTop, obBottom, obBarOffset, segmentFound]

// === OB CREATION CASCADE (Exactly one reject reason per BOS) ===
// FILTER 0: OB only created after BOS
if enableOrderBlocks
    // === BULLISH OB CASCADE ===
    bool tryCreateBullOB = bullishBOS

    if tryCreateBullOB
        totalBOSAttempts += 1

        // 1) RANGE MISMATCH (highest priority, already checked in BOS block)
        bool rangeOkBull = not na(lastSwingLowBar) and not na(bosTriggerHighBar) and (lastSwingLowBar > bosTriggerHighBar)

        if not rangeOkBull
            rejectRangeMismatch += 1
            if enableDebugLog and logRejectReasons
                log.info("OB REJECTED (BULL): Range mismatch")

        // 2) SEGMENT NOT FOUND
        else
            [obTop, obBottom, obBarOffset, segmentFoundBull] = extractOBFromSegment(true, bar_index, bosTriggerHighBar)

            if not segmentFoundBull
                rejectSegmentNotFound += 1
                if enableDebugLog and logRejectReasons
                    log.info("OB REJECTED (BULL): No opposite candle in BOS segment")

            // 3) DISPLACEMENT
            else
                float disp = calcDisplacement(0)

                if disp < minDisplacement
                    rejectDisplacement += 1

                    // Calculate full potential score
                    if enableDebugLog and logRejectReasons
                        float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                        float potentialScore = calculatePotentialScore(disp, relVol, hasBullishFVG, true, obBottom, obTop, bosRangeHigh, bosRangeLow)
                        float potentialPriority = calculateOBPriority(potentialScore)
                        string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                        log.info(str.format("OB REJECTED (BULL): Displacement {0} < {1}{2}",
                            str.tostring(disp, "#.##"), str.tostring(minDisplacement, "#.##"), lostTag))

                // 4) LOCATION (Premium/Discount)
                else
                    bool locationOkBull = true
                    if not na(bosRangeHigh) and not na(bosRangeLow)
                        float threshold = bosRangeLow + (bosRangeHigh - bosRangeLow) * obLocationThreshold
                        locationOkBull := obBottom < threshold

                    if not locationOkBull
                        rejectLocation += 1

                        // Calculate full potential priority
                        if enableDebugLog and logRejectReasons
                            float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                            float potentialScore = calculatePotentialScore(disp, relVol, hasBullishFVG, true, obBottom, obTop, bosRangeHigh, bosRangeLow)
                            float potentialPriority = calculateOBPriority(potentialScore)
                            string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                            log.info(str.format("OB REJECTED (BULL): Location not in discount{0}", lostTag))

                    // 5) OB SIZE
                    else
                        float obRange = obTop - obBottom
                        bool sizeOkBull = obRange <= maxOBRange * atr and obRange >= minOBRange * atr
                        bool continueToVolume = true

                        // Hard Limit mode: Reject if size invalid
                        if sizeFilterMode == "Hard Limit" and not sizeOkBull
                            rejectSize += 1
                            continueToVolume := false
                            if enableDebugLog and logRejectReasons
                                float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                                float potentialScore = calculatePotentialScore(disp, relVol, hasBullishFVG, true, obBottom, obTop, bosRangeHigh, bosRangeLow)
                                float potentialPriority = calculateOBPriority(potentialScore)
                                string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                                log.info(str.format("OB REJECTED (BULL): Size invalid ({0} ATR){1}", str.tostring(obRange / atr, "#.##"), lostTag))

                        // Score Penalty or Disabled mode: Continue (penalty applied in scoring later)

                        // 6) VOLUME (optional)
                        if continueToVolume
                            float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                            bool volumeOkBull = not useVolumFilter or not volumeIsReal or relVol > volumeFloor

                            if not volumeOkBull
                                rejectVolume += 1
                                if enableDebugLog and logRejectReasons
                                    float potentialScore = calculatePotentialScore(disp, relVol, hasBullishFVG, true, obBottom, obTop, bosRangeHigh, bosRangeLow)
                                    float potentialPriority = calculateOBPriority(potentialScore)
                                    string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                                    log.info(str.format("OB REJECTED (BULL): Volume filter failed ({0}x){1}", str.tostring(relVol, "#.##"), lostTag))

                            // 7) FVG (optional)
                            else
                                bool fvgOkBull = not requireFVG or hasBullishFVG

                                if not fvgOkBull
                                    rejectFVG += 1
                                    if enableDebugLog and logRejectReasons
                                        float potentialScore = calculatePotentialScore(disp, relVol, hasBullishFVG, true, obBottom, obTop, bosRangeHigh, bosRangeLow)
                                        float potentialPriority = calculateOBPriority(potentialScore)
                                        string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                                        log.info(str.format("OB REJECTED (BULL): No FVG present{0}", lostTag))

                                // ✅ SUCCESS - CREATE BULLISH OB
                                else
                                    // Calculate score
                                    float score = 15.0  // Base fresh score

                                    if disp >= 2.0
                                        score += 30.0
                                    else if disp >= 1.5
                                        score += 20.0
                                    else
                                        score += 10.0

                                    if hasBullishFVG
                                        score += 20.0

                                    if relVol > 2.0
                                        score += 15.0
                                    else if relVol > 1.5
                                        score += 10.0
                                    else if relVol > 1.0
                                        score += 5.0

                                    if not na(bosRangeHigh) and not na(bosRangeLow)
                                        float eq = (bosRangeHigh + bosRangeLow) / 2
                                        if obBottom < bosRangeLow + (bosRangeHigh - bosRangeLow) * 0.38
                                            score += 20.0
                                        else if obBottom < eq
                                            score += 10.0

                                    // Size Penalty (if Score Penalty mode and OB oversized)
                                    if sizeFilterMode == "Score Penalty" and not sizeOkBull
                                        float sizeRatio = obRange / (maxOBRange * atr)
                                        float penalty = (sizeRatio - 1.0) * 20.0  // Scale penalty with size excess
                                        score -= math.min(penalty, 30.0)  // Cap penalty at -30

                                    // Delete old OB if exists
                                    if not na(bullOB) and not na(bullOB.boxObj)
                                        box.delete(bullOB.boxObj)

                                    // Calculate priority score
                                    float obPriority = calculateOBPriority(score)

                                    // Create new OB
                                    bullOB := OrderBlock.new( creationBar = bar_index - obBarOffset, top = obTop, bottom = obBottom, obType = "BULL", mitigated = false, displacement = disp, score = score, hasFVG = hasBullishFVG, relativeVolume = relVol, bosBar = bar_index, boxObj = na, reactionState = "Untouched", maxPenetration = 0.0, firstTouchBar = na, priority = obPriority )

                                    if enableDebugLog and logOB and shouldLog(obPriority)
                                        string context = getContextSummary(obPriority, currentLegStrength, structureAligned, structureConflict)
                                        log.info(str.format("OB CREATED (BULL): {0} | Top: {1} | Bottom: {2} | Score: {3} | Disp: {4} ATR | Vol: {5}x",
                                            context,
                                            str.tostring(obTop, format.mintick), str.tostring(obBottom, format.mintick),
                                            str.tostring(math.round(score)),
                                            str.tostring(disp, "#.##"), str.tostring(relVol, "#.##")))

    // === BEARISH OB CASCADE ===
    bool tryCreateBearOB = bearishBOS

    if tryCreateBearOB
        totalBOSAttempts += 1

        // 1) RANGE MISMATCH
        bool rangeOkBear = not na(lastSwingHighBar) and not na(bosTriggerLowBar) and (lastSwingHighBar > bosTriggerLowBar)

        if not rangeOkBear
            rejectRangeMismatch += 1
            if enableDebugLog and logRejectReasons
                log.info("OB REJECTED (BEAR): Range mismatch")

        // 2) SEGMENT NOT FOUND
        else
            [obTop, obBottom, obBarOffset, segmentFoundBear] = extractOBFromSegment(false, bar_index, bosTriggerLowBar)

            if not segmentFoundBear
                rejectSegmentNotFound += 1
                if enableDebugLog and logRejectReasons
                    log.info("OB REJECTED (BEAR): No opposite candle in BOS segment")

            // 3) DISPLACEMENT
            else
                float disp = calcDisplacement(0)

                if disp < minDisplacement
                    rejectDisplacement += 1

                    // Calculate full potential score
                    if enableDebugLog and logRejectReasons
                        float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                        float potentialScore = calculatePotentialScore(disp, relVol, hasBearishFVG, false, obBottom, obTop, bosRangeHigh, bosRangeLow)
                        float potentialPriority = calculateOBPriority(potentialScore)
                        string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                        log.info(str.format("OB REJECTED (BEAR): Displacement {0} < {1}{2}",
                            str.tostring(disp, "#.##"), str.tostring(minDisplacement, "#.##"), lostTag))

                // 4) LOCATION (Premium/Discount)
                else
                    bool locationOkBear = true
                    if not na(bosRangeHigh) and not na(bosRangeLow)
                        float threshold = bosRangeLow + (bosRangeHigh - bosRangeLow) * obLocationThreshold
                        locationOkBear := obTop > threshold

                    if not locationOkBear
                        rejectLocation += 1

                        // Calculate full potential priority
                        if enableDebugLog and logRejectReasons
                            float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                            float potentialScore = calculatePotentialScore(disp, relVol, hasBearishFVG, false, obBottom, obTop, bosRangeHigh, bosRangeLow)
                            float potentialPriority = calculateOBPriority(potentialScore)
                            string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                            log.info(str.format("OB REJECTED (BEAR): Location not in premium{0}", lostTag))

                    // 5) OB SIZE
                    else
                        float obRange = obTop - obBottom
                        bool sizeOkBear = obRange <= maxOBRange * atr and obRange >= minOBRange * atr
                        bool continueToVolume = true

                        // Hard Limit mode: Reject if size invalid
                        if sizeFilterMode == "Hard Limit" and not sizeOkBear
                            rejectSize += 1
                            continueToVolume := false
                            if enableDebugLog and logRejectReasons
                                float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                                float potentialScore = calculatePotentialScore(disp, relVol, hasBearishFVG, false, obBottom, obTop, bosRangeHigh, bosRangeLow)
                                float potentialPriority = calculateOBPriority(potentialScore)
                                string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                                log.info(str.format("OB REJECTED (BEAR): Size invalid ({0} ATR){1}", str.tostring(obRange / atr, "#.##"), lostTag))

                        // Score Penalty or Disabled mode: Continue (penalty applied in scoring later)

                        // 6) VOLUME (optional)
                        if continueToVolume
                            float relVol = volumeIsReal ? volume[obBarOffset] / volumeSMA : na
                            bool volumeOkBear = not useVolumFilter or not volumeIsReal or relVol > volumeFloor

                            if not volumeOkBear
                                rejectVolume += 1
                                if enableDebugLog and logRejectReasons
                                    float potentialScore = calculatePotentialScore(disp, relVol, hasBearishFVG, false, obBottom, obTop, bosRangeHigh, bosRangeLow)
                                    float potentialPriority = calculateOBPriority(potentialScore)
                                    string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                                    log.info(str.format("OB REJECTED (BEAR): Volume filter failed ({0}x){1}", str.tostring(relVol, "#.##"), lostTag))

                            // 7) FVG (optional)
                            else
                                bool fvgOkBear = not requireFVG or hasBearishFVG

                                if not fvgOkBear
                                    rejectFVG += 1
                                    if enableDebugLog and logRejectReasons
                                        float potentialScore = calculatePotentialScore(disp, relVol, hasBearishFVG, false, obBottom, obTop, bosRangeHigh, bosRangeLow)
                                        float potentialPriority = calculateOBPriority(potentialScore)
                                        string lostTag = potentialPriority > 80 ? " POTENTIAL HIGH LOST" : potentialPriority > 50 ? " potential MED lost" : " (low priority)"
                                        log.info(str.format("OB REJECTED (BEAR): No FVG present{0}", lostTag))

                                // ✅ SUCCESS - CREATE BEARISH OB
                                else
                                    // Calculate score
                                    float score = 15.0  // Base fresh score

                                    if disp >= 2.0
                                        score += 30.0
                                    else if disp >= 1.5
                                        score += 20.0
                                    else
                                        score += 10.0

                                    if hasBearishFVG
                                        score += 20.0

                                    if relVol > 2.0
                                        score += 15.0
                                    else if relVol > 1.5
                                        score += 10.0
                                    else if relVol > 1.0
                                        score += 5.0

                                    if not na(bosRangeHigh) and not na(bosRangeLow)
                                        float eq = (bosRangeHigh + bosRangeLow) / 2
                                        if obTop > bosRangeLow + (bosRangeHigh - bosRangeLow) * 0.62
                                            score += 20.0
                                        else if obTop > eq
                                            score += 10.0

                                    // Size Penalty (if Score Penalty mode and OB oversized)
                                    if sizeFilterMode == "Score Penalty" and not sizeOkBear
                                        float sizeRatio = obRange / (maxOBRange * atr)
                                        float penalty = (sizeRatio - 1.0) * 20.0  // Scale penalty with size excess
                                        score -= math.min(penalty, 30.0)  // Cap penalty at -30

                                    // Delete old OB if exists
                                    if not na(bearOB) and not na(bearOB.boxObj)
                                        box.delete(bearOB.boxObj)

                                    // Calculate priority score
                                    float obPriority = calculateOBPriority(score)

                                    // Create new OB
                                    bearOB := OrderBlock.new( creationBar = bar_index - obBarOffset, top = obTop, bottom = obBottom, obType = "BEAR", mitigated = false, displacement = disp, score = score, hasFVG = hasBearishFVG, relativeVolume = relVol, bosBar = bar_index, boxObj = na, reactionState = "Untouched", maxPenetration = 0.0, firstTouchBar = na, priority = obPriority )

                                    if enableDebugLog and logOB and shouldLog(obPriority)
                                        string context = getContextSummary(obPriority, currentLegStrength, structureAligned, structureConflict)
                                        log.info(str.format("OB CREATED (BEAR): {0} | Top: {1} | Bottom: {2} | Score: {3} | Disp: {4} ATR | Vol: {5}x",
                                            context,
                                            str.tostring(obTop, format.mintick), str.tostring(obBottom, format.mintick),
                                            str.tostring(math.round(score)),
                                            str.tostring(disp, "#.##"), str.tostring(relVol, "#.##")))


// === OB REACTION TYPE TRACKING ===
// Track penetration depth and classify reaction type
trackReactionType(OrderBlock ob) =>
    if not na(ob) and ob.reactionState == "Untouched"
        float obRange = ob.top - ob.bottom
        float currentPenetration = 0.0
        bool touched = false

        if ob.obType == "BULL"
            // Check if price touched OB zone
            if low <= ob.top and low >= ob.bottom
                touched := true
                currentPenetration := (ob.top - low) / obRange * 100.0

                // Update first touch bar
                if na(ob.firstTouchBar)
                    ob.firstTouchBar := bar_index

                // Update max penetration
                if currentPenetration > ob.maxPenetration
                    ob.maxPenetration := currentPenetration
        else  // BEAR
            if high >= ob.bottom and high <= ob.top
                touched := true
                currentPenetration := (high - ob.bottom) / obRange * 100.0

                if na(ob.firstTouchBar)
                    ob.firstTouchBar := bar_index

                if currentPenetration > ob.maxPenetration
                    ob.maxPenetration := currentPenetration

        // Classify reaction type based on max penetration
        if touched and ob.maxPenetration > 0
            if ob.maxPenetration <= 30
                ob.reactionState := "Tap"
            else if ob.maxPenetration <= 70
                ob.reactionState := "Deep"
            else
                ob.reactionState := "Failure"

            // Log reaction type
            if enableDebugLog and logOB and ob.reactionState != "Untouched"
                int timeToReact = bar_index - ob.creationBar
                log.info(str.format("OB REACTION ({0}): {1} | Penetration: {2}% | Time: {3} bars",
                    ob.obType, ob.reactionState, str.tostring(ob.maxPenetration, "#.#"), str.tostring(timeToReact)))

// === OB MITIGATION CHECK ===
checkMitigation(OrderBlock ob) =>
    bool mitigated = false

    if not na(ob) and not ob.mitigated
        float penetrationSize = (ob.top - ob.bottom) * mitigationPenetration

        if ob.obType == "BULL"
            // FILTER 7: Mitigation by 30% penetration
            if low <= ob.top and low >= ob.bottom
                float penetration = ob.top - low
                if penetration >= penetrationSize
                    mitigated := true
        else
            if high >= ob.bottom and high <= ob.top
                float penetration = high - ob.bottom
                if penetration >= penetrationSize
                    mitigated := true

    mitigated

// === OB INVALIDATION CHECK ===
// FIX 4: Complete invalidation if price breaks through OB
checkInvalidation(OrderBlock ob) =>
    bool invalidated = false

    if not na(ob)
        if ob.obType == "BULL"
            // Bullish OB invalidated if low goes below OB bottom
            if low < ob.bottom
                invalidated := true
        else
            // Bearish OB invalidated if high goes above OB top
            if high > ob.top
                invalidated := true

    invalidated

// === OB TTL CHECK (FILTER 8) ===
checkTTL(OrderBlock ob) =>
    bool expired = false

    if not na(ob)
        int age = bar_index - ob.creationBar
        if age > obTTL
            expired := true

    expired

// === UPDATE OBs ===
// Track mitigation events for alerts
bool bullOBMitigatedNow = false
bool bearOBMitigatedNow = false

// Lifecycle checks start on the bar after the BOS that created the OB: the
// creation bar's own range is the impulse, not a return into the zone.
if enableOrderBlocks
    // Check bullish OB
    if not na(bullOB) and bar_index > bullOB.bosBar
        // Track reaction type BEFORE invalidation/mitigation checks
        trackReactionType(bullOB)

        // Check invalidation first (FIX 4)
        if checkInvalidation(bullOB)
            if not na(bullOB.boxObj)
                box.delete(bullOB.boxObj)
            bullOB := na
        else
            // Check mitigation (FILTER 7)
            if checkMitigation(bullOB) and not bullOB.mitigated
                bullOB.mitigated := true
                bullOB.score := bullOB.score - 15.0  // Remove fresh bonus
                bullOBMitigatedNow := true  // For alerts

            // Check TTL (FILTER 8)
            if checkTTL(bullOB)
                if not na(bullOB.boxObj)
                    box.delete(bullOB.boxObj)
                bullOB := na

    // Check bearish OB
    if not na(bearOB) and bar_index > bearOB.bosBar
        // Track reaction type BEFORE invalidation/mitigation checks
        trackReactionType(bearOB)

        // Check invalidation first (FIX 4)
        if checkInvalidation(bearOB)
            if not na(bearOB.boxObj)
                box.delete(bearOB.boxObj)
            bearOB := na
        else
            // Check mitigation (FILTER 7)
            if checkMitigation(bearOB) and not bearOB.mitigated
                bearOB.mitigated := true
                bearOB.score := bearOB.score - 15.0
                bearOBMitigatedNow := true  // For alerts

            // Check TTL (FILTER 8)
            if checkTTL(bearOB)
                if not na(bearOB.boxObj)
                    box.delete(bearOB.boxObj)
                bearOB := na

// === CONFIRMATION SIGNALS (For Layer 4 and Visualization) ===
bool confirmedBullish = bullishBOS and not na(bullOB) and not bullOB.mitigated
bool confirmedBearish = bearishBOS and not na(bearOB) and not bearOB.mitigated


// ============================================================================
// MODULE: layer4_warnings
// ============================================================================

// ==== LAYER 4: WARNINGS ====
// Optional early warnings that context might change
// Warnings do NOT change expectation
// Warnings are visually subtle, no alerts

// === WARNING STATE ===
var int lastSwingLevelBreakBar = na
var int lastEarlyTopBar = na
var int lastEarlyBottomBar = na

// === RSI FOR EARLY SIGNALS ===
float rsi = ta.rsi(close, rsiLength)

// === SWING LEVEL BREAK WARNING ===
bool swingLevelBreakBull = false
bool swingLevelBreakBear = false
var float swingLevelBreak = na

// Cross events computed unconditionally at global scope (consistent history)
bool warnCrossUnderLow  = ta.crossunder(close, lastConfirmedSwingLow * 0.995)
bool warnCrossOverHigh  = ta.crossover(close, lastConfirmedSwingHigh * 1.005)

if enableSwingLevelBreak and showLayer4
    // Warning level check: support/resistance level from last confirmed swing
    if not na(lastConfirmedSwingHigh) and not na(lastConfirmedSwingLow)
        // Bullish warning: cross below last swing low
        if not na(lastSwingLowBar) and bar_index > lastSwingLowBar + 2 and isBullishStructure
            // Only trigger on crossunder (real break event), not while below
            if warnCrossUnderLow
                if na(lastSwingLevelBreakBar) or bar_index - lastSwingLevelBreakBar > warningCooldown
                    swingLevelBreakBull := true
                    swingLevelBreak := lastConfirmedSwingLow
                    lastSwingLevelBreakBar := bar_index

        // Bearish warning: cross above last swing high
        if not na(lastSwingHighBar) and bar_index > lastSwingHighBar + 2 and isBearishStructure
            // Only trigger on crossover (real break event), not while above
            if warnCrossOverHigh
                if na(lastSwingLevelBreakBar) or bar_index - lastSwingLevelBreakBar > warningCooldown
                    swingLevelBreakBear := true
                    swingLevelBreak := lastConfirmedSwingHigh
                    lastSwingLevelBreakBar := bar_index

// === EARLY TOP WARNING ===
bool earlyTop = false

if enableEarlyTop and showLayer4
    // Conditions for early top:
    // 1. Large upper wick (relative to range)
    // 2. RSI overbought
    // 3. Close significantly below high
    // 4. In bullish structure or at resistance

    float wickRatio = (high - math.max(open, close)) / (high - low + 1e-10)
    bool largeUpperWick = wickRatio >= wickThreshold
    bool rsiOB = rsi > rsiOverbought
    bool closeBelowHigh = close < high * 0.98

    if largeUpperWick and rsiOB and closeBelowHigh
        if na(lastEarlyTopBar) or bar_index - lastEarlyTopBar > warningCooldown
            earlyTop := true
            lastEarlyTopBar := bar_index

// === EARLY BOTTOM WARNING ===
bool earlyBottom = false

if enableEarlyBottom and showLayer4
    // Conditions for early bottom:
    // 1. Large lower wick (relative to range)
    // 2. RSI oversold
    // 3. Close significantly above low
    // 4. In bearish structure or at support

    float wickRatio = (math.min(open, close) - low) / (high - low + 1e-10)
    bool largeLowerWick = wickRatio >= wickThreshold
    bool rsiOS = rsi < rsiOversold
    bool closeAboveLow = close > low * 1.02

    if largeLowerWick and rsiOS and closeAboveLow
        if na(lastEarlyBottomBar) or bar_index - lastEarlyBottomBar > warningCooldown
            earlyBottom := true
            lastEarlyBottomBar := bar_index

// === WARNINGS ARE PASSIVE ===
// These do not trigger entries or change expectations
// They are only shown visually in a subtle way
// No alertcondition() on warnings


// ============================================================================
// MODULE: visualization
// ============================================================================

// ==== VISUALIZATION MODULE ====
// Clean, context-focused visual display
// Layers are shown based on user toggles

// === PENDING-STATE VISUALIZATION ===
var line pendingBullLine = na
var line pendingBearLine = na
var label pendingBullLbl = na
var label pendingBearLbl = na

f_lineStyle(string _s) =>
    _s == "Solid" ? line.style_solid : _s == "Dashed" ? line.style_dashed : line.style_dotted

// Update pending visualizations
if showPendingViz and pendingBull
    // Update line
    if na(pendingBullLine)
        pendingBullLine := line.new(bar_index, pendingLevelBull, bar_index + 1, pendingLevelBull,
            extend = extend.right, style = f_lineStyle(pendingLineStyle), width = 1, color = color.new(color.green, 50))
    else
        line.set_x2(pendingBullLine, bar_index + 1)

    // Update countdown label
    int barsLeft = pendingUntilBull - bar_index
    if not na(pendingBullLbl)
        label.delete(pendingBullLbl)
    pendingBullLbl := label.new(bar_index, pendingLevelBull,
        str.format("Pending BOS ↑ ({0})", barsLeft),
        style = label.style_label_left, textcolor = color.white, color = color.new(color.green, 60), size = size.tiny)

if showPendingViz and pendingBear
    // Update line
    if na(pendingBearLine)
        pendingBearLine := line.new(bar_index, pendingLevelBear, bar_index + 1, pendingLevelBear,
            extend = extend.right, style = f_lineStyle(pendingLineStyle), width = 1, color = color.new(color.red, 50))
    else
        line.set_x2(pendingBearLine, bar_index + 1)

    // Update countdown label
    int barsLeft = pendingUntilBear - bar_index
    if not na(pendingBearLbl)
        label.delete(pendingBearLbl)
    pendingBearLbl := label.new(bar_index, pendingLevelBear,
        str.format("Pending BOS ↓ ({0})", barsLeft),
        style = label.style_label_left, textcolor = color.white, color = color.new(color.red, 60), size = size.tiny)

// Clear pending visualizations when not pending
if showPendingViz and not pendingBull
    if not na(pendingBullLine)
        line.delete(pendingBullLine)
        pendingBullLine := na
    if not na(pendingBullLbl)
        label.delete(pendingBullLbl)
        pendingBullLbl := na

if showPendingViz and not pendingBear
    if not na(pendingBearLine)
        line.delete(pendingBearLine)
        pendingBearLine := na
    if not na(pendingBearLbl)
        label.delete(pendingBearLbl)
        pendingBearLbl := na

// === PLOTS (MUST BE AT GLOBAL SCOPE) ===
// Swing High/Low lines (dashed to distinguish from JMA)
plot(showLayer1 and not minimalMode and showSwingLines ? lastConfirmedSwingHigh : na,
     "Last Swing High", color.new(structureColorBear, 50), 2, plot.style_steplinebr)
plot(showLayer1 and not minimalMode and showSwingLines ? lastConfirmedSwingLow : na,
     "Last Swing Low", color.new(structureColorBull, 50), 2, plot.style_steplinebr)

// Pending BOS markers (plotshape must be at global scope)
plotshape(showPendingViz and pendingBull, title = "Pending Bull BOS", style = shape.circle, location = location.belowbar, color = color.new(color.green, 30), size = size.tiny)
plotshape(showPendingViz and pendingBear, title = "Pending Bear BOS", style = shape.circle, location = location.abovebar, color = color.new(color.red, 30), size = size.tiny)

// === JMA OVERLAY ===
// JMA Approximation (aligned with CPM calculateJMA)
calculateJMA(float src, int length, float phase = 0.0, float power = 2.0) =>
    float phaseRatio = phase < -100 ? 0.5 : phase > 100 ? 2.5 : phase / 100 + 1.5
    float beta = 0.45 * (length - 1) / (0.45 * (length - 1) + 2)
    float alpha = math.pow(beta, power)

    var float e0 = na
    var float e1 = na
    var float e2 = na
    var float jma = na

    e0 := (1 - alpha) * src + alpha * nz(e0[1], src)
    e1 := (src - e0) * (1 - beta) + beta * nz(e1[1], 0)
    e2 := (e0 + phaseRatio * e1 - nz(jma[1], src)) * math.pow(1 - alpha, 2) + math.pow(alpha, 2) * nz(e2[1], 0)
    jma := e2 + nz(jma[1], src)
    jma

float jma1 = showJMA ? calculateJMA(close, jmaLen1, 0, 1.0) : na
float jma2 = showJMA ? calculateJMA(close, jmaLen2, 0, 1.0) : na
float jma3 = showJMA ? calculateJMA(close, jmaLen3, 0, 1.0) : na

plot(jma1, "JMA 20", color.new(jmaColor1, 70), 2)
plot(jma2, "JMA 75", color.new(jmaColor2, 70), 2)
plot(jma3, "JMA 200", color.new(jmaColor3, 70), 2)

// === LAYER 1 VISUALIZATION: STRUCTURE ===
// HH/HL/LH/LL labels
if showLayer1 and not minimalMode and showStructureLabels
    if not na(pivotHigh)
        string labelText = lastSwingType
        color labelColor = lastSwingType == "HH" ? structureColorBull : lastSwingType == "LH" ? structureColorBear : color.gray

        label.new( lastSwingHighBar, pivotHigh, labelText, style = label.style_label_down, color = labelColor, textcolor = color.white, size = size.tiny )

    if not na(pivotLow)
        string labelText = lastSwingType
        color labelColor = lastSwingType == "HL" ? structureColorBull : lastSwingType == "LL" ? structureColorBear : color.gray

        label.new( lastSwingLowBar, pivotLow, labelText, style = label.style_label_up, color = labelColor, textcolor = color.white, size = size.tiny )

// === LAYER 2 VISUALIZATION: EXPECTATION ===
var label expectationLbl = na
var box expectationZoneBox = na
if showLayer2 and not minimalMode
    // Expectation label (single, updated on last bar)
    if showExpectationLabel and expectationActive and barstate.islast
        string labelText = ""
        color labelColor = color.new(color.blue, 0)
        string labelStyle = label.style_label_up
        float labelY = low

        // Build label text without icons
        if currentExpectation == "Expect HL"
            labelText := "Expect HL"
            labelColor := color.new(expectationColorBull, 0)
            labelStyle := label.style_label_up
            labelY := low
        else if currentExpectation == "Expect LH"
            labelText := "Expect LH"
            labelColor := color.new(expectationColorBear, 0)
            labelStyle := label.style_label_down
            labelY := high
        else if currentExpectation == "Expect Continuation" and isBullishStructure
            labelText := "Expect Continuation"
            labelColor := color.new(expectationColorBull, 0)
            labelStyle := label.style_label_up
            labelY := low
        else if currentExpectation == "Expect Continuation" and isBearishStructure
            labelText := "Expect Continuation"
            labelColor := color.new(expectationColorBear, 0)
            labelStyle := label.style_label_down
            labelY := high
        else if currentExpectation == "Expect Failure"
            labelText := "Expect Failure"
            labelColor := color.new(color.gray, 0)
            labelStyle := label.style_label_center
            labelY := (high + low) / 2

        if labelText != ""
            if na(expectationLbl)
                expectationLbl := label.new( bar_index + 10, labelY, labelText, style = labelStyle, color = labelColor, textcolor = color.white, size = size.small )
            else
                label.set_xy(expectationLbl, bar_index + 10, labelY)
                label.set_text(expectationLbl, labelText)
                label.set_color(expectationLbl, labelColor)
                label.set_textcolor(expectationLbl, color.white)
                label.set_style(expectationLbl, labelStyle)
        else if not na(expectationLbl)
            label.delete(expectationLbl)
            expectationLbl := na
    else if not na(expectationLbl)
        label.delete(expectationLbl)
        expectationLbl := na

    // Expectation zone — single tracked box (recreating one per bar would stack
    // transparent boxes and churn through max_boxes_count)
    if not na(expectationZoneBox)
        box.delete(expectationZoneBox)
        expectationZoneBox := na

    if showExpectationZone and expectationActive and not na(expectationPrice)
        // Calculate end bar for expectation validity
        int endBar = math.min(bar_index, expectationBar + expectationExpiry)

        if currentExpectation == "Expect HL"
            // Draw box showing expected HL zone (above reference low)
            expectationZoneBox := box.new( expectationBar, expectationPrice * 1.02, endBar, expectationPrice, border_color = color.new(expectationColorBull, 70), bgcolor = color.new(expectationColorBull, 90) )

        if currentExpectation == "Expect LH"
            // Draw box showing expected LH zone (below reference high)
            expectationZoneBox := box.new( expectationBar, expectationPrice, endBar, expectationPrice * 0.98, border_color = color.new(expectationColorBear, 70), bgcolor = color.new(expectationColorBear, 90) )

// === LAYER 3 VISUALIZATION: CONFIRMATION ===
if showLayer3 and not minimalMode
    // BOS labels
    if showBOSLabels
        if bullishBOS
            string bosTooltip = str.format("Bullish Break of Structure\nMode: {0}\nLevel: {1}\nClose: {2}\nRange: {3} - {4}\nExpectation: {5}",
                bosMode,
                str.tostring(bosRangeHigh, format.mintick),
                str.tostring(close, format.mintick),
                str.tostring(bosRangeLow, format.mintick),
                str.tostring(bosRangeHigh, format.mintick),
                currentExpectation)

            label.new( bar_index, low, "BOS", style = label.style_label_up, color = color.new(color.lime, 0), textcolor = color.black, size = size.normal, tooltip = bosTooltip )

        if bearishBOS
            string bosTooltip = str.format("Bearish Break of Structure\nMode: {0}\nLevel: {1}\nClose: {2}\nRange: {3} - {4}\nExpectation: {5}",
                bosMode,
                str.tostring(bosRangeLow, format.mintick),
                str.tostring(close, format.mintick),
                str.tostring(bosRangeLow, format.mintick),
                str.tostring(bosRangeHigh, format.mintick),
                currentExpectation)

            label.new( bar_index, high, "BOS", style = label.style_label_down, color = color.new(color.red, 0), textcolor = color.white, size = size.normal, tooltip = bosTooltip )

    // Order Block visualization
    if enableOrderBlocks
        // Draw bullish OB
        if not na(bullOB)
            if na(bullOB.boxObj) or not na(bullOB.boxObj) and box.get_right(bullOB.boxObj) < bar_index
                // Delete old box
                if not na(bullOB.boxObj)
                    box.delete(bullOB.boxObj)

                // Create new box
                color boxColor = bullOB.mitigated ? color.new(obColorBull, 90) : obColorBull
                color borderColor = bullOB.mitigated ? color.new(color.gray, 50) : color.new(color.green, 40)
                int borderWidth = bullOB.score >= 75 ? 3 : bullOB.score >= 50 ? 2 : 1

                string labelText = "OB"

                // Calculate box end: min of (current bar + 50, creation + TTL)
                int boxEnd = math.min(bar_index + 50, bullOB.creationBar + obTTL)

                bullOB.boxObj := box.new( bullOB.creationBar, bullOB.top, boxEnd, bullOB.bottom, border_color = borderColor, bgcolor = boxColor, border_width = borderWidth, text = labelText, text_color = color.new(borderColor, 50), text_size = size.small, text_halign = text.align_right, text_valign = text.align_top )

        // Draw bearish OB
        if not na(bearOB)
            if na(bearOB.boxObj) or not na(bearOB.boxObj) and box.get_right(bearOB.boxObj) < bar_index
                // Delete old box
                if not na(bearOB.boxObj)
                    box.delete(bearOB.boxObj)

                // Create new box
                color boxColor = bearOB.mitigated ? color.new(obColorBear, 90) : obColorBear
                color borderColor = bearOB.mitigated ? color.new(color.gray, 50) : color.new(color.red, 40)
                int borderWidth = bearOB.score >= 75 ? 3 : bearOB.score >= 50 ? 2 : 1

                string labelText = "OB"

                // Calculate box end: min of (current bar + 50, creation + TTL)
                int boxEnd = math.min(bar_index + 50, bearOB.creationBar + obTTL)

                bearOB.boxObj := box.new( bearOB.creationBar, bearOB.top, boxEnd, bearOB.bottom, border_color = borderColor, bgcolor = boxColor, border_width = borderWidth, text = labelText, text_color = color.new(borderColor, 50), text_size = size.small, text_halign = text.align_right, text_valign = text.align_top )

// === LAYER 4 VISUALIZATION: WARNINGS ===
if showLayer4 and not minimalMode
    // Swing level break warnings (subtle side labels like Early Top)
    if swingLevelBreakBull and not na(swingLevelBreak)
        label.new( bar_index, swingLevelBreak, "Swing Break", style = label.style_label_left, color = color.new(color.orange, 50), textcolor = color.black, size = size.small, tooltip = "Swing Level Break: Price broke below the last swing low support level. This may indicate a potential reversal or weakening of the uptrend." )

    if swingLevelBreakBear and not na(swingLevelBreak)
        label.new( bar_index, swingLevelBreak, "Swing Break", style = label.style_label_left, color = color.new(color.orange, 50), textcolor = color.black, size = size.small, tooltip = "Swing Level Break: Price broke above the last swing high resistance level. This may indicate a potential reversal or weakening of the downtrend." )

    // Early top/bottom warnings (very subtle)
    if earlyTop
        label.new( bar_index, high, "Early Top", style = label.style_label_left, color = color.new(color.yellow, 50), textcolor = color.black, size = size.small )

    if earlyBottom
        label.new( bar_index, low, "Early Bottom", style = label.style_label_right, color = color.new(color.yellow, 50), textcolor = color.black, size = size.small )

// === REJECT HEATMAP TABLE ===
// Helper functions
f_tablePos(string _s) =>
    _s == "Top Left" ? position.top_left : _s == "Top Right" ? position.top_right : _s == "Bottom Left" ? position.bottom_left : position.bottom_right

f_pct(int _x, int _base) =>
    _base > 0 ? (100.0 * _x / _base) : na

f_heatAlpha(float _pct) =>
    na(_pct) ? 90 : _pct >= 60 ? 0 : _pct >= 40 ? 20 : _pct >= 25 ? 40 : _pct >= 15 ? 60 : _pct >= 8 ? 75 : 85

// Row renderer for heatmap (must be defined at module scope)
f_renderHeatmapRow(table tbl, int row, string name, int count, float pct) =>
    int a = f_heatAlpha(pct)
    string bar = na(pct) ? " " : pct >= 60 ? "█████" : pct >= 40 ? "████ " : pct >= 25 ? "███  " : pct >= 15 ? "██   " : pct >= 8 ? "█    " : "·    "
    // Dark text on light heat background, light text on saturated heat background
    color txtColor = a > 50 ? color.new(color.gray, 20) : color.white

    table.cell(tbl, 0, row, name, text_size = size.small, text_color = color.new(color.gray, 20))
    table.cell(tbl, 1, row, str.tostring(count), text_size = size.small, text_color = txtColor, bgcolor = color.new(color.red, a), text_halign = text.align_center)
    table.cell(tbl, 2, row, na(pct) ? "—" : str.format("{0,number,#.0}%", pct), text_size = size.small, text_color = txtColor, bgcolor = color.new(color.red, a), text_halign = text.align_center)
    table.cell(tbl, 3, row, bar, text_size = size.small, text_color = txtColor, bgcolor = color.new(color.red, a), text_halign = text.align_center)

// Initialize heatmap table (light theme)
var table rejTbl = na
if barstate.isfirst and showRejectHeatmap
    rejTbl := table.new(f_tablePos(heatmapCorner), 4, 10,
         bgcolor = color.new(color.white, 5),
         border_color = color.new(color.gray, 60),
         border_width = 1,
         frame_color = color.new(color.gray, 40),
         frame_width = 1)

// Render heatmap
if showRejectHeatmap and barstate.islast and not na(rejTbl)
    table.clear(rejTbl, 0, 0, 3, 9)

    color heatTc = color.new(color.gray, 20)
    color heatHd = color.new(color.gray, 90)

    // Header
    table.cell(rejTbl, 0, 0, "OB REJECTION HEATMAP", text_size = size.small, bgcolor = heatHd, text_color = heatTc, text_halign = text.align_center)
    table.merge_cells(rejTbl, 0, 0, 3, 0)

    // Column headers
    table.cell(rejTbl, 0, 1, "Reject Reason", text_size = size.small, bgcolor = heatHd, text_color = heatTc)
    table.cell(rejTbl, 1, 1, "Count", text_size = size.small, bgcolor = heatHd, text_color = heatTc, text_halign = text.align_center)
    table.cell(rejTbl, 2, 1, "%", text_size = size.small, bgcolor = heatHd, text_color = heatTc, text_halign = text.align_center)
    table.cell(rejTbl, 3, 1, "Heat", text_size = size.small, bgcolor = heatHd, text_color = heatTc, text_halign = text.align_center)

    int base = totalBOSAttempts

    // Data rows (prioritized by cascade order)
    f_renderHeatmapRow(rejTbl, 2, "Range", rejectRangeMismatch, f_pct(rejectRangeMismatch, base))
    f_renderHeatmapRow(rejTbl, 3, "Segment", rejectSegmentNotFound, f_pct(rejectSegmentNotFound, base))
    f_renderHeatmapRow(rejTbl, 4, "Displacement", rejectDisplacement, f_pct(rejectDisplacement, base))
    f_renderHeatmapRow(rejTbl, 5, "Location", rejectLocation, f_pct(rejectLocation, base))
    f_renderHeatmapRow(rejTbl, 6, "Size", rejectSize, f_pct(rejectSize, base))
    f_renderHeatmapRow(rejTbl, 7, "Volume", rejectVolume, f_pct(rejectVolume, base))
    f_renderHeatmapRow(rejTbl, 8, "FVG", rejectFVG, f_pct(rejectFVG, base))

    // Summary row
    int totalRejects = rejectRangeMismatch + rejectSegmentNotFound + rejectDisplacement + rejectLocation + rejectSize + rejectVolume + rejectFVG
    int passed = base - totalRejects
    float okPct = base > 0 ? 100.0 * passed / base : na

    table.cell(rejTbl, 0, 9, str.format("Total: {0}", str.tostring(base)), text_size = size.small, text_color = heatTc, bgcolor = heatHd)
    table.cell(rejTbl, 1, 9, str.tostring(passed), text_size = size.small, text_color = heatTc, bgcolor = color.new(#00c853, 70), text_halign = text.align_center)
    table.cell(rejTbl, 2, 9, na(okPct) ? "—" : str.format("{0,number,#.0}%", okPct), text_size = size.small, text_color = heatTc, bgcolor = color.new(#00c853, 70), text_halign = text.align_center)
    table.cell(rejTbl, 3, 9, "Pass", text_size = size.small, text_color = heatTc, bgcolor = color.new(#00c853, 70), text_halign = text.align_center)

// === DASHBOARD (light theme) ===
if showDashboard and not minimalMode
    var string dashPos = dashPosition == "Top Left" ? position.top_left : dashPosition == "Top Right" ? position.top_right : dashPosition == "Bottom Left" ? position.bottom_left : position.bottom_right

    color tc = color.new(color.gray, 20)   // standard text color
    color hd = color.new(color.gray, 90)   // header background

    var table dashboard = table.new(dashPos, 2, enableDebugLog ? 9 : 8,
        bgcolor = color.new(color.white, 5),
        border_color = color.new(color.gray, 60),
        border_width = 1,
        frame_color = color.new(color.gray, 40),
        frame_width = 1)

    if barstate.islast
        // Header
        table.cell(dashboard, 0, 0, "SMC STRUCTURE & EXPECTATION",
            text_color = tc,
            bgcolor = hd,
            text_size = size.small,
            text_halign = text.align_center)
        table.merge_cells(dashboard, 0, 0, 1, 0)

        // Layer 1: Structure
        table.cell(dashboard, 0, 1, "Structure", text_color = tc, text_size = size.small)

        string structureText = currentStructure
        color structureColor = isBullishStructure ? #00c853 : isBearishStructure ? #d50000 : tc

        table.cell(dashboard, 1, 1, structureText, text_color = structureColor, text_size = size.small)

        // Layer 2: Expectation
        table.cell(dashboard, 0, 2, "Expectation", text_color = tc, text_size = size.small)

        string expectText = currentExpectation
        color expectColor = expectHL or (expectContinuation and isBullishStructure) ? #00c853 : expectLH or (expectContinuation and isBearishStructure) ? #d50000 : expectFailure ? #ff6f00 : tc

        table.cell(dashboard, 1, 2, expectText, text_color = expectColor, text_size = size.small)

        // Layer 3: BOS Status
        table.cell(dashboard, 0, 3, "BOS", text_color = tc, text_size = size.small)

        string bosText = bullishBOS ? "Bullish" : bearishBOS ? "Bearish" : "None"
        color bosColor = bullishBOS ? #00c853 : bearishBOS ? #d50000 : tc

        table.cell(dashboard, 1, 3, bosText, text_color = bosColor, text_size = size.small)

        // Layer 3: OB Status
        table.cell(dashboard, 0, 4, "Order Blocks", text_color = tc, text_size = size.small)

        string obText = ""
        color obColor = tc
        if not na(bullOB) and not bullOB.mitigated
            string priorityLabel = bullOB.priority > 80 ? "HIGH" : bullOB.priority > 50 ? "MED" : "LOW"
            obText := str.format("Bull {0} P:{1}", priorityLabel, str.tostring(math.round(bullOB.priority)))
            obColor := bullOB.priority > 80 ? #00c853 : bullOB.priority > 50 ? color.new(#00c853, 40) : tc
        else if not na(bearOB) and not bearOB.mitigated
            string priorityLabel = bearOB.priority > 80 ? "HIGH" : bearOB.priority > 50 ? "MED" : "LOW"
            obText := str.format("Bear {0} P:{1}", priorityLabel, str.tostring(math.round(bearOB.priority)))
            obColor := bearOB.priority > 80 ? #d50000 : bearOB.priority > 50 ? color.new(#d50000, 40) : tc
        else
            obText := "None Active"

        table.cell(dashboard, 1, 4, obText, text_color = obColor, text_size = size.small)

        // Layer 4: Warnings
        table.cell(dashboard, 0, 5, "Warnings", text_color = tc, text_size = size.small)

        string warningText = swingLevelBreakBull or swingLevelBreakBear ? "Swing Level Break" : earlyTop ? "Early Top" : earlyBottom ? "Early Bottom" : "None"
        color warningColor = warningText == "None" ? tc : #ff6f00

        table.cell(dashboard, 1, 5, warningText, text_color = warningColor, text_size = size.small)

        // Last swing high
        table.cell(dashboard, 0, 6, "Last High", text_color = tc, text_size = size.small)
        table.cell(dashboard, 1, 6, str.tostring(lastConfirmedSwingHigh, format.mintick), text_color = #d50000, text_size = size.small)

        // Last swing low
        table.cell(dashboard, 0, 7, "Last Low", text_color = tc, text_size = size.small)
        table.cell(dashboard, 1, 7, str.tostring(lastConfirmedSwingLow, format.mintick), text_color = #00c853, text_size = size.small)

        // OB Rejection Summary (if Debug enabled)
        if enableDebugLog
            table.cell(dashboard, 0, 8, "OB Rejects", text_color = tc, text_size = size.small)

            string rejectSummary = str.format("Total BOS: {0}\nRng: {1} | Seg: {2} | Disp: {3} | Loc: {4}\nVol: {5} | Size: {6} | FVG: {7}",
                str.tostring(totalBOSAttempts),
                str.tostring(rejectRangeMismatch), str.tostring(rejectSegmentNotFound), str.tostring(rejectDisplacement), str.tostring(rejectLocation),
                str.tostring(rejectVolume), str.tostring(rejectSize), str.tostring(rejectFVG))

            table.cell(dashboard, 1, 8, rejectSummary, text_color = #ff6f00, text_size = size.small)

// === ALERTS (Only on Layer 3 Confirmation) ===
// No alerts on warnings (Layer 4)

// === ALERTS ===
bool alertOk = not alertsOnBarClose or barstate.isconfirmed

// Priority filter for alerts
bool bullPriorityOk = not na(bullOB) and (not onlyHighPriorityAlerts or bullOB.priority > 80)
bool bearPriorityOk = not na(bearOB) and (not onlyHighPriorityAlerts or bearOB.priority > 80)

// HIGH Priority Alerts (separate for clarity)
alertcondition(alertOk and bullishBOS and not na(bullOB) and bullOB.priority > 80,
    title = "HIGH Priority Bullish Setup",
    message = "SMCE · BULL BOS+OB HIGH LONG · {{ticker}} {{interval}} · Px={{close}}")

alertcondition(alertOk and bearishBOS and not na(bearOB) and bearOB.priority > 80,
    title = "HIGH Priority Bearish Setup",
    message = "SMCE · BEAR BOS+OB HIGH SHORT · {{ticker}} {{interval}} · Px={{close}}")

// Standard Alerts (only if HIGH priority filter is OFF)
alertcondition(alertOk and bullishBOS and bullPriorityOk and not (bullOB.priority > 80),
    title = "Bullish Confirmation",
    message = "SMCE · BULL BOS+OB LONG · {{ticker}} {{interval}} · Px={{close}}")

alertcondition(alertOk and bearishBOS and bearPriorityOk and not (bearOB.priority > 80),
    title = "Bearish Confirmation",
    message = "SMCE · BEAR BOS+OB SHORT · {{ticker}} {{interval}} · Px={{close}}")

alertcondition(alertOk and bullOBMitigatedNow,
    title = "Bullish OB Mitigated",
    message = "SMCE · BULL OB MITIGATED · {{ticker}} {{interval}} · Px={{close}}")

alertcondition(alertOk and bearOBMitigatedNow,
    title = "Bearish OB Mitigated",
    message = "SMCE · BEAR OB MITIGATED · {{ticker}} {{interval}} · Px={{close}}")

// === BAR COLORING ===
color barColor = na

if barColorMode != "Off"
    // Mode 1: Structure-Based
    if barColorMode == "Structure"
        if isBullishStructure
            barColor := color.new(color.green, 30)
        else if isBearishStructure
            barColor := color.new(color.red, 30)
        else
            barColor := color.new(color.gray, 60)

    // Mode 2: Expectation-Based
    else if barColorMode == "Expectation"
        if expectHL and not expectationExpired
            barColor := color.new(#00E676, 40)  // Light green (expect bullish continuation)
        else if expectLH and not expectationExpired
            barColor := color.new(#FF5252, 40)  // Light red (expect bearish continuation)
        else
            barColor := color.new(color.gray, 60)

    // Mode 3: BOS + OB Priority (DEFAULT)
    else if barColorMode == "BOS + OB Priority"
        // Check active OB priority
        float activePriority = na
        bool isBullOB = false

        if not na(bullOB) and not bullOB.mitigated
            activePriority := bullOB.priority
            isBullOB := true
        else if not na(bearOB) and not bearOB.mitigated
            activePriority := bearOB.priority
            isBullOB := false

        // Color by priority
        if not na(activePriority)
            if activePriority > 80  // HIGH
                barColor := isBullOB ? color.new(#00E676, 0) : color.new(#FF5252, 0)
            else if activePriority > 50  // MED
                barColor := isBullOB ? color.new(#4CAF50, 30) : color.new(#EF5350, 30)
            else  // LOW
                barColor := isBullOB ? color.new(#81C784, 60) : color.new(#E57373, 60)
        else
            // No OB: Fallback to structure with high transparency
            if isBullishStructure
                barColor := color.new(color.green, 80)
            else if isBearishStructure
                barColor := color.new(color.red, 80)
            else
                barColor := color.new(color.gray, 85)

        // BOS highlight (bright override)
        if bullishBOS
            barColor := color.new(#00FF00, 0)  // Bright green on BOS
        else if bearishBOS
            barColor := color.new(#FF0000, 0)  // Bright red on BOS

    // Mode 4: Multi-Layer Combined
    else if barColorMode == "Multi-Layer"
        // Base: Structure
        color baseColor = isBullishStructure ? color.green : isBearishStructure ? color.red : color.gray

        // Intensity: OB Priority
        int transparency = 70  // Default (no OB or LOW)

        if not na(bullOB) and not bullOB.mitigated
            if bullOB.priority > 80
                transparency := 0  // HIGH = full intensity
            else if bullOB.priority > 50
                transparency := 30  // MED
            else
                transparency := 60  // LOW
        else if not na(bearOB) and not bearOB.mitigated
            if bearOB.priority > 80
                transparency := 0
            else if bearOB.priority > 50
                transparency := 30
            else
                transparency := 60

        barColor := color.new(baseColor, transparency)

        // BOS override
        if bullishBOS
            barColor := color.new(#00FF00, 0)
        else if bearishBOS
            barColor := color.new(#FF0000, 0)

    // Mode 5: Alignment-Based
    else if barColorMode == "Alignment"
        if enableInternalStructure
            if structureAligned
                // Aligned = saturated colors
                barColor := isBullishStructure ? color.new(#00C853, 20) : color.new(#D50000, 20)
            else if structureConflict
                // Conflict = warning (orange/yellow)
                barColor := color.new(#FF6F00, 40)
            else
                // Neutral
                barColor := color.new(color.gray, 60)
        else
            // Fallback to structure if internal not enabled
            barColor := isBullishStructure ? color.new(color.green, 30) : isBearishStructure ? color.new(color.red, 30) : color.new(color.gray, 60)

barcolor(barColor)

// === VOLUME VALIDITY NOTICE ===
// One-time visibility hint (DATA_VALIDITY.md §7): the OB volume bonus/filter
// degrades to neutral on feeds without real trade volume (tick/n/a).
// Kurzer Hinweis unten links (unten rechts liegt per Default die Heatmap);
// was fehlt, steht im Data Contract oben.
var table volWarn = table.new(position.bottom_left, 1, 1)
if barstate.islast and not volumeIsReal
    table.cell(volWarn, 0, 0, "SMC-SE · volume not available",
         bgcolor = color.new(color.orange, 20), text_color = color.white, text_size = size.small)

Overview

SMC Structure & Expectation [WavesUnchained] maps external and internal market structure points (HH, HL, LH, LL), projects structural expectation zones for upcoming swing levels, detects Break of Structure (BOS) events, and plots filtered order blocks with displacement and mitigation tracking. User settings control visibility for each layer, pivot lookback periods, BOS confirmation requirements, and displacement thresholds. Pivot points and structure labels plot only after the configured lookback bars confirm.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

SMC / ICT / price action, Support & resistance, Trend

Markets

Any symbol

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.

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

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

SettingTypeDefaultVariable
Enable Debug LoggingboolfalseenableDebugLog
Log LevelstringSmartlogLevel
Log Structure EventsbooltruelogStructure
Log Expectation ChangesbooltruelogExpectation
Log BOS EventsbooltruelogBOS
Log OB LifecyclebooltruelogOB
Log OB Reject ReasonsboolfalselogRejectReasons
Bar Color ModestringBOS + OB PrioritybarColorMode
Show Layer 1 - Market StructurebooltrueshowLayer1
Show Layer 2 - ExpectationbooltrueshowLayer2
Show Layer 3 - Confirmation (BOS + OB)booltrueshowLayer3
Show Layer 4 - WarningsbooltrueshowLayer4
Show all 40 settings
SettingTypeDefaultVariable
External Pivot Lookback (Major Structure)int5structureLookback
Internal Pivot Lookback (Entry Timing)int3internalLookback
Enable Internal StructurebooltrueenableInternalStructure
Show HH/HL/LH/LL LabelsbooltrueshowStructureLabels
Show Swing High/Low LinesbooltrueshowSwingLines
Bullish Structure Colorcolorcolor.new(color.green, 0)structureColorBull
Bearish Structure Colorcolorcolor.new(color.red, 0)structureColorBear
Show Expectation LabelbooltrueshowExpectationLabel
Show Expected HL/LH ZonesbooltrueshowExpectationZone
Expectation Expiry (bars)int30expectationExpiry
Bullish Expectation Zonecolorcolor.new(color.green, 70)expectationColorBull
Bearish Expectation Zonecolorcolor.new(color.red, 70)expectationColorBear
Show BOS LabelsbooltrueshowBOSLabels
BOS ModestringClose-crossbosMode
Aggro requires close confirmationbooltruerequireConfirm
Aggro confirm barsint2confirmBars
Enable Order Block DetectionbooltrueenableOrderBlocks
Auto-Adjust Displacement SettingsbooltrueuseAutoDisplacement
Min Displacement (ATR multiples)float1.0Min Displacement (ATR multiples)
Displacement Lookback (bars)int3Displacement Lookback (bars)
Min OB Range (ATR)float0.3minOBRange
Size Filter ModestringScore PenaltysizeFilterMode
Auto-Adjust Max OB RangebooltrueuseAutoMaxRange
Manual Max OB Range (ATR)float1.0manualMaxOBRange
Require FVG OverlapboolfalserequireFVG
Premium/Discount Thresholdfloat0.58obLocationThreshold
OB Time-To-Live (bars)int100obTTL
Mitigation Penetration (%)float0.25mitigationPenetration
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

575 stars

This indicator detects equal highs and lows by comparing prices within user-defined left and right bars using a tolerance percentage. It plots horizontal lines on the price chart for equal highs in one color and lows in another. A screener allows input of mult

Indicator v5 Support & resistanceTrend
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows with Screener

This Pine Script indicator plots a ZigZag on the price chart. It identifies reversal points when price moves more than a set percentage from the last pivot. The script calculates forecasts using zigzag data and labels detected geometric patterns including harm

Indicator v4 TrendSupport & resistance
218 stars

This indicator plots market structure on the price chart by identifying swing highs and lows. It connects these points with lines to visualize trend direction and detect Break of Structure (BOS) when price breaks a prior swing in the current trend. Change of C

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Market Structure - BOS & CHoCH

Open Source Fractal

by cantolab
183 stars

This indicator implements the Fractal Model concept to plot market structure shifts, liquidity sweeps, and Change in State of Delivery (CISD) levels. It overlays custom higher-timeframe candle previews, open prices, and time markers directly on the price chart

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · MIT GitHub repository of Open Source Fractal
56 stars

BigBeluga Smart Money Concepts identifies and overlays market structure and volumetric order blocks directly on the price chart. It analyzes market swings, trend sweeps, and mapping structure using customizable pivot lengths and calculation modes. The indicato

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · None stated GitHub repository of BigBeluga - Smart Money Concepts
53 stars

Apex Trend & Liquidity Master calculates a trend cloud using customizable moving averages expanded by a volatility multiplier directly on the main price chart. It identifies dynamic support and resistance liquidity zones from swing high and low pivots, extendi

Indicator v6 TrendSupport & resistance
Updated 2 months ago · MPL-2.0 GitHub repository of Apex Trend & Liquidity Master

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