//@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.
Four-layer market structure analysis framework
External and internal swing pivot identification
Expectation bias zones for upcoming swing levels
Break of structure detection with close-cross confirmation
Filtered order blocks with displacement and mitigation tracking
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Enable Debug Logging
bool
false
enableDebugLog
Log Level
string
Smart
logLevel
Log Structure Events
bool
true
logStructure
Log Expectation Changes
bool
true
logExpectation
Log BOS Events
bool
true
logBOS
Log OB Lifecycle
bool
true
logOB
Log OB Reject Reasons
bool
false
logRejectReasons
Bar Color Mode
string
BOS + OB Priority
barColorMode
Show Layer 1 - Market Structure
bool
true
showLayer1
Show Layer 2 - Expectation
bool
true
showLayer2
Show Layer 3 - Confirmation (BOS + OB)
bool
true
showLayer3
Show Layer 4 - Warnings
bool
true
showLayer4
Show all 40 settings
Setting
Type
Default
Variable
External Pivot Lookback (Major Structure)
int
5
structureLookback
Internal Pivot Lookback (Entry Timing)
int
3
internalLookback
Enable Internal Structure
bool
true
enableInternalStructure
Show HH/HL/LH/LL Labels
bool
true
showStructureLabels
Show Swing High/Low Lines
bool
true
showSwingLines
Bullish Structure Color
color
color.new(color.green, 0)
structureColorBull
Bearish Structure Color
color
color.new(color.red, 0)
structureColorBear
Show Expectation Label
bool
true
showExpectationLabel
Show Expected HL/LH Zones
bool
true
showExpectationZone
Expectation Expiry (bars)
int
30
expectationExpiry
Bullish Expectation Zone
color
color.new(color.green, 70)
expectationColorBull
Bearish Expectation Zone
color
color.new(color.red, 70)
expectationColorBear
Show BOS Labels
bool
true
showBOSLabels
BOS Mode
string
Close-cross
bosMode
Aggro requires close confirmation
bool
true
requireConfirm
Aggro confirm bars
int
2
confirmBars
Enable Order Block Detection
bool
true
enableOrderBlocks
Auto-Adjust Displacement Settings
bool
true
useAutoDisplacement
Min Displacement (ATR multiples)
float
1.0
Min Displacement (ATR multiples)
Displacement Lookback (bars)
int
3
Displacement Lookback (bars)
Min OB Range (ATR)
float
0.3
minOBRange
Size Filter Mode
string
Score Penalty
sizeFilterMode
Auto-Adjust Max OB Range
bool
true
useAutoMaxRange
Manual Max OB Range (ATR)
float
1.0
manualMaxOBRange
Require FVG Overlap
bool
false
requireFVG
Premium/Discount Threshold
float
0.58
obLocationThreshold
OB Time-To-Live (bars)
int
100
obTTL
Mitigation Penetration (%)
float
0.25
mitigationPenetration
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.
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
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
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
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
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
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