Strategy · Pine Script v6 · Own pane

WaveTrend v3 [WavesUnchained] — Strategy

by casoon

Combines an upgraded WaveTrend oscillator engine with market structure swing pivots, volume profile point of control (POC), and multi-timeframe trend alignment scoring.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code2,238
Settings40inputs
LicenceMIT
Last update3 days ago27 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 strategy, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart. The trades and results appear in the Strategy Tester tab; adjust the settings with the gear icon.

Source Code

2,238 lines of Pine Script v6 from archive/strategies/wavetrend/wavetrend_v3_strategy.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

wavetrend_v3_strategy.pine
//@version=6
// ============================================================================
// WaveTrend v3 [WavesUnchained]
// Version: 3.2-experimental
// Author: WavesUnchained
// Build: 2026-05-04 (v3.10: BE merged into bracket exit; bar/local/score gates allow WT-extreme entries)
// ============================================================================
// Composite-Indikator. Nicht mehr "WaveTrend optimiert" sondern ein eigenes
// Konzept das mehrere Bausteine kombiniert:
//
//   1. WaveTrend Oscillator (mit v2-Formel-Verbesserungen)
//   2. Market Structure (HH/HL/LH/LL aus Price Pivots)
//   3. Volumen-Kontext (POC ± Standard-Abweichungen, Volume-Spike)
//   4. Multi-Timeframe Trend (Setup-HTF + Anchor-HTF)
//   5. Setup-Familien (Range-Reversal + Pullback-Continuation)
//
// Signal-Logik = Trigger + ALIGNMENT-SCORE: WT-Reversal als Timing,
// mehrstufige MTF-/Kontext-Bestätigung als Qualitätsfilter.
// ============================================================================

strategy("WaveTrend v3 [WavesUnchained] — Strategy", overlay=false,
     max_lines_count=500,
     max_labels_count=500,
     max_boxes_count=250,
     default_qty_type=strategy.percent_of_equity, default_qty_value=10,
     commission_type=strategy.commission.percent, commission_value=0.02,
     slippage=1, max_bars_back=1000)

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 1 — WaveTrend Oscillator (v2 formula)
// ═══════════════════════════════════════════════════════════════════════════

g_wt = '1. WaveTrend Engine'

chLength  = input.int(10, 'Channel Length',  minval=2, group=g_wt)
avgLength = input.int(21, 'Average Length',  minval=2, group=g_wt)
sigLength = input.int(4,  'Signal Length',   minval=1, group=g_wt)
ciConst   = input.float(0.015, 'CI Constant', minval=0.001, maxval=0.5, step=0.001, group=g_wt,
     tooltip='Channel Index normalization constant. Default 0.015 (Lambert/CCI). Higher for high-vol instruments.')
wtEnginePreset = input.string('Custom', 'WT Engine Preset', options=['Classic Stable', 'Custom'], group=g_wt,
     tooltip='Custom activates the smoothing type and norm mode below. Classic Stable forces EMA + Mean Abs Dev regardless of saved inputs.')
normMode  = input.string('Mean Abs Dev', 'Custom Volatility Norm', options=['Mean Abs Dev', 'ATR'], group=g_wt,
     tooltip='Mean Abs Dev is the classic WaveTrend normalization. ATR is gap-aware but can compress oscillator extremes after wide candles.')
smoothType= input.string('EMA', 'Custom Smoothing', options=['EMA', 'RMA', 'SMA', 'WMA', 'HMA', 'JMA'], group=g_wt,
     tooltip='EMA is the classic WaveTrend baseline. HMA halves lag at the same period but can overshoot on the oscillator.')
jmaPhase  = input.int(50, '  JMA Phase', minval=-100, maxval=100, group=g_wt)
jmaPower  = input.float(2.0, '  JMA Power', minval=1.0, maxval=4.0, step=0.25, group=g_wt)

upperBand = input.int(56, 'OB Threshold', group=g_wt)
lowerBand = input.int(-56, 'OS Threshold', group=g_wt)
showRawStrongWTCross = input.bool(true, 'Show Raw Strong WT Cross Diagnostics', group=g_wt,
     tooltip='Shows LazyBear-style raw strong WT crosses before strategy gates. These are diagnostics only: entries still require playbook/setup/score confirmation.')
zoneExitWindow = input.int(4, 'Zone Exit Trigger Window', minval=0, maxval=20, group=g_wt,
     tooltip='How many bars after an OB/OS visit a WT cross may still count as a valid reversal trigger. 0 = cross must happen inside the zone.')
useWVF     = input.bool(true, 'Williams Vix Fix Gate', group=g_wt,
     tooltip='Treat a WVF spike as equivalent to an OS/OB WT zone. Fires at price extremes before the WT oscillator catches up, effectively extending the reversal zone from the price side.')
wvfLen     = input.int(22, '  WVF Period', minval=5, maxval=100, group=g_wt)
wvfBBLen   = input.int(20, '  WVF BB Length', minval=5, maxval=100, group=g_wt)
wvfBBMult  = input.float(2.0, '  WVF BB Multiplier', minval=0.5, maxval=5.0, step=0.25, group=g_wt)
useAutoTF = input.bool(true, 'Auto-Calibrate Time Windows', group=g_wt,
     tooltip='Scales time-based bar windows from a 4H baseline. WT engine lengths are not scaled because they control oscillator shape.')

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 2 — Market Structure (HH / HL / LH / LL)
// ═══════════════════════════════════════════════════════════════════════════

g_struct = '2. Market Structure'

structPivLen = input.int(5, 'Swing Pivot Length', minval=2, maxval=20, group=g_struct,
     tooltip='Bars on each side required to confirm a price swing high/low.')
structLookback = input.int(50, 'Structure Lookback', minval=20, maxval=200, group=g_struct,
     tooltip='How far back to scan for the most recent confirmed swings to define current structure.')

showStructure = input.bool(true, 'Show Structure Labels', group=g_struct)

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 3 — Volume Context
// ═══════════════════════════════════════════════════════════════════════════

g_vol = '3. Volume Context'

volMALen     = input.int(50, 'Volume MA Length', minval=10, maxval=200, group=g_vol)
volSpikeMult = input.float(1.5, 'Volume Spike Multiplier', minval=1.0, maxval=5.0, step=0.1, group=g_vol,
     tooltip='Volume spike = current volume > MA × this multiplier. Used as conviction signal.')
useVolPOC    = input.bool(true, 'Use Volume Profile (POC)', group=g_vol,
     tooltip='Compute the Point of Control (POC) at two horizons. Local POC drives the entry-context score (reclaim/reject). Macro POC is shown as a longer-term magnet/bias reference.')
pocLookback  = input.int(100, '  Local POC Lookback', minval=30, maxval=500, group=g_vol,
     tooltip='Short-horizon POC used for the score component (pocReclaim / pocReject). Closer to current price action; reacts faster to regime change.')
pocMacroLookback = input.int(300, '  Macro POC Lookback', minval=50, maxval=1000, group=g_vol,
     tooltip='Long-horizon POC shown as bias/magnet. Not used for entry score by default; informational on chart and in dashboard.')
pocBins      = input.int(20, '  POC Bins', minval=10, maxval=50, group=g_vol,
     tooltip='Number of price bins for both POC computations. Higher = more precise but slower.')

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 4 — Multi-Timeframe Trend
// ═══════════════════════════════════════════════════════════════════════════

g_htf = '4. Multi-Timeframe Trend'

htfTimeframe = input.timeframe('', 'HTF (auto if empty)', group=g_htf,
     tooltip='Setup timeframe for trend context. Empty = auto (~4× chart TF; 1H→4H, 4H→D).')
anchorTimeframe = input.timeframe('', 'Anchor HTF (auto if empty)', group=g_htf,
     tooltip='Higher anchor timeframe for directional bias. Empty = auto (~24× chart TF; 1H→D, 4H→W).')
htfWeight    = input.float(1.0, 'HTF Score Weight', minval=0.0, maxval=2.0, step=0.5, group=g_htf,
     tooltip='Weight of setup HTF trend in the composite context score. If the setup HTF is used as a hard gate, it is not scored again.')
anchorWeight = input.float(1.0, 'Anchor HTF Score Weight', minval=0.0, maxval=2.0, step=0.5, group=g_htf,
     tooltip='Weight of anchor timeframe trend in the composite context score. 0 = ignore this timeframe.')
mtfGateMode = input.string('None', 'Hard MTF Gate',
     options=['None', 'Setup Only', 'Setup + Anchor'], group=g_htf,
     tooltip='Hard MTF entry gate. Default None means HTF is context/score only, not a hard blocker. Use hard gates only after logs show they help a specific timeframe.')
anchorScoreMode = input.string('Contrarian', 'Anchor Score Mode',
     options=['Ignore', 'Alignment', 'Contrarian'], group=g_htf,
     tooltip='How the anchor timeframe contributes to score. Contrarian rewards early reversals where setup HTF has turned but anchor HTF has not yet followed. Alignment rewards full MTF trend continuation.')
useAutoTFBias = input.bool(true, 'Auto Lower-TF Trend Bias', group=g_htf,
     tooltip='On chart timeframes up to 1H, use the automatically selected setup HTF as a hard directional bias only when its WT trend is decisive. Countertrend lower-TF entries are blocked, while neutral HTF keeps normal behavior.')
autoTFBiasMinWT = input.float(20.0, '  Bias Min HTF WT', minval=0.0, maxval=80.0, step=2.5, group=g_htf,
     tooltip='Minimum absolute setup-HTF WaveTrend value required before the lower-TF trend bias becomes active.')
autoTFBiasApplyTo = input.string('ER + PB', '  Bias Applies To',
     options=['Off', 'ER Only', 'PB Only', 'ER + PB', 'All'], group=g_htf,
     tooltip='Which entry families are blocked when they go against a decisive setup-HTF trend. RR can stay excluded to preserve true range reversals.')

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 5 — Setup Gate
// ═══════════════════════════════════════════════════════════════════════════

g_setup = '5. Setup Gate'

useSetupGate = input.bool(true, 'Require Setup Location + Room', group=g_setup,
     tooltip='Setup-first mode. Signals only pass when price is near a useful range edge and has enough room to a structure/range target.')
setupRangeLen = input.int(120, 'Setup Range Lookback', minval=30, maxval=500, group=g_setup,
     tooltip='Range used to define whether price is near the lower/upper part of the chart. 1H: about several trading days; 4H: several weeks.')
setupEdgePct = input.float(0.35, 'Range Edge Percent', minval=0.10, maxval=0.50, step=0.05, group=g_setup,
     tooltip='Longs require price in the lower part of the setup range; shorts require price in the upper part. 0.35 means bottom/top 35%.')
swingProximityATR = input.float(1.5, 'Swing Proximity (ATR×)', minval=0.25, maxval=10.0, step=0.25, group=g_setup,
     tooltip='Alternative setup location: allow entries near the latest confirmed swing support/resistance even if the larger range edge is not reached. This is important for lower-high shorts in downtrends.')
volumeGateMode = input.string('Lower TF Only', 'Volume Gate Mode',
     options=['Off', 'Lower TF Only', 'All TF', 'Reversal Only', 'Pullback Only'], group=g_setup,
     tooltip='When/where directional volume spike is required as part of setup gate.\n• Off: never required\n• Lower TF Only: required only on chart TFs ≤1H (legacy default)\n• All TF: required for every entry\n• Reversal Only / Pullback Only: required only for that family.\nProfile may override this — set Strategy Profile to Custom to use this raw value.')
usePullbackSetup = input.bool(true, 'Enable Pullback Continuation Setup', group=g_setup,
     tooltip='Adds a second setup family: trend impulse + controlled pullback + WT re-entry. This is separate from range-edge reversal and should increase useful 4H trade count.')
enableLongRangeReversal = input.bool(true, 'Enable LONG Range-Reversal Setup', group=g_setup,
     tooltip='Emergency direction switch for LONG range-reversal entries. The symmetric RR quality filter below should be the primary control.')
enableShortRangeReversal = input.bool(true, 'Enable SHORT Range-Reversal Setup', group=g_setup,
     tooltip='Emergency direction switch for SHORT range-reversal entries. The symmetric RR quality filter below should be the primary control.')
rrMinTFSec = input.int(3600, 'RR Minimum Timeframe (seconds)', minval=0, group=g_setup,
     tooltip='Range-Reversal is disabled on timeframes shorter than this. Default 3600 = 1H minimum. Test68-71 showed 15m RR consistently negative (WR 29-33%) while 15m ER is positive — the lag-entry problem is structural on fast TFs. Set to 0 to allow all TFs.')
rangeReversalQualityMode = input.string('Score 3.5 or POC+Volume', 'Range-Reversal Quality Filter',
     options=['Off', 'Score 3.5 or POC+Volume', 'POC+Volume'], group=g_setup,
     tooltip='Symmetric filter for LONG/SHORT range-reversal entries.\n• Off: no extra RR quality filter\n• Score 3.5 or POC+Volume: allow stronger context-score RR, or require local POC event plus directional volume\n• POC+Volume: strict mirrored confirmation only')
requirePullbackHTF = input.bool(false, '  Require HTF for Pullback', group=g_setup,
     tooltip='When off, pullback continuation uses chart structure as the hard requirement and leaves HTF as score/log context. This prevents MTF from silently starving pullback trades.')
pullbackEmaLen = input.int(34, '  Pullback EMA Length', minval=5, maxval=200, group=g_setup)
pullbackProximityATR = input.float(1.75, '  Pullback Proximity (ATR×)', minval=0.25, maxval=5.0, step=0.25, group=g_setup,
     tooltip='Pullback setup requires price near the EMA or a confirmed swing support/resistance by this ATR distance.')
minImpulseATR = input.float(2.0, '  Min Prior Impulse (ATR×)', minval=1.0, maxval=15.0, step=0.25, group=g_setup,
     tooltip='Minimum distance between the latest confirmed swing low/high. Avoids continuation entries when no meaningful prior move exists.')
allowConfirmedPBRoomOverride = input.bool(true, '  PB Room Override on Confirmation', group=g_setup,
     tooltip='Allow pullback-continuation even when structure target room is tight, but only with mirrored local confirmation: LONG needs POC reclaim or bullish volume; SHORT needs POC reject or bearish volume. Range-reversal still requires normal room.')
requireLowerTFPBMidline = input.bool(true, '  Lower-TF PB Requires WT Midline', group=g_setup,
     tooltip='On chart timeframes up to 1H, pullback-continuation must already be on the correct WT side: LONG above 0, SHORT below 0. Test40 showed lower-TF PB losers mostly fired too early before this confirmation.')
useEarlyRejectionSetup = input.bool(true, 'Enable Early Rejection Setup', group=g_setup,
     tooltip='Adds an early, unconfirmed rejection family: WT turns at a range/swing/EMA rejection, local POC/volume confirms, and MFI or Stoch RSI agrees. Designed mainly for 1H where HH/HL/LH/LL pivots confirm too late.')
earlyRejectTFMode = input.string('Lower TF Only', '  Early Reject TF Mode',
     options=['Off', 'Lower TF Only', 'All TF'], group=g_setup,
     tooltip='Controls where early rejection may trade. Lower TF Only means chart timeframes up to 1H. This protects 4H defaults from over-trading unconfirmed turns.')
earlyRejectMinScore = input.float(3.5, '  Early Reject Min Score', minval=1.0, maxval=6.0, step=0.5, group=g_setup)
useOneHourERQuality = input.bool(true, '  1H Early Reject Quality Filter', group=g_setup,
     tooltip='Extra 1H-only local quality. LONG early rejection needs bullish volume; SHORT early rejection needs POC reject. This removes the 1H false ER cluster seen in test40 while leaving 15m ER behavior intact.')
useOneHourERFollowThrough = input.bool(true, '  1H Early Reject Requires Follow-Through', group=g_setup,
     tooltip='Turns 1H early rejection into a pending setup. The strategy only enters if a later bar confirms through the rejection bar. This tests the test55 finding that 1H ER fires too early.')
oneHourERFollowThroughBars = input.int(2, '    1H ER Follow-Through Bars', minval=1, maxval=6, group=g_setup)
useOneHourERDmiConfirm = input.bool(true, '    1H ER Requires DMI/ADX Direction', group=g_setup,
     tooltip='Independent confirmation for 1H early rejection. LONG needs +DI above -DI; SHORT needs -DI above +DI. ADX must be above the threshold.')
oneHourERDmiLen = input.int(14, '    1H ER DMI Length', minval=3, maxval=50, group=g_setup)
oneHourERAdxSmoothing = input.int(14, '    1H ER ADX Smoothing', minval=3, maxval=50, group=g_setup)
oneHourERMinAdx = input.float(15.0, '    1H ER Min ADX', minval=0.0, maxval=50.0, step=1.0, group=g_setup)
erBlockCounterExpansion = input.bool(true, '  Early Reject: Block Counter-HTF HH+LL Expansion', group=g_setup,
     tooltip='Symmetric guard for broad expansion structure. In HH+LL without HL/LH, block LONG only when setup HTF is bearish and block SHORT only when setup HTF is bullish.')
earlyRejectProximityATR = input.float(1.25, '  Early Reject Proximity (ATR×)', minval=0.25, maxval=5.0, step=0.25, group=g_setup)
earlyRejectWickMin = input.float(0.45, '  Early Reject Wick/Close Threshold', minval=0.1, maxval=0.9, step=0.05, group=g_setup,
     tooltip='LONG needs close in the upper part of the bar; SHORT needs close in the lower part. 0.45 is permissive, 0.65 stricter.')
mfiLen = input.int(14, '  MFI Length', minval=2, maxval=100, group=g_setup)
stochRsiLen = input.int(14, '  Stoch RSI Length', minval=2, maxval=100, group=g_setup)
stochRsiSmoothK = input.int(3, '  Stoch RSI K Smoothing', minval=1, maxval=20, group=g_setup)
stochRsiSmoothD = input.int(3, '  Stoch RSI D Smoothing', minval=1, maxval=20, group=g_setup)
minTargetRoomATR = input.float(2.0, 'Min Room to Target (ATR×)', minval=0.5, maxval=10.0, step=0.25, group=g_setup,
     tooltip='Minimum distance from entry to the next structure/range target. Blocks trades with no realistic payoff room.')
minTargetRoomR = input.float(0.0, 'Min Room to Target (R)', minval=0.0, maxval=10.0, step=0.25, group=g_setup,
     tooltip='Optional reward/risk room gate. 0 disables it. Profile defaults keep this off except 1H Balanced/Aggressive, where test54 showed real winners were too small versus full -1R stops.')
lowerTfTacticalTargetR = input.float(0.0, 'Lower-TF RR/ER Tactical Target (R)', minval=0.0, maxval=5.0, step=0.25, group=g_setup,
     tooltip='Experimental execution target cap for 15m/1H range-reversal and early-rejection trades. Set 0 to disable. test51 showed a 1.25R cap cuts larger winners and worsens 15m/1H despite higher target-hit rate.')
oneHourPullbackTargetR = input.float(0.5, '1H Pullback Tactical Target (R)', minval=0.0, maxval=5.0, step=0.25, group=g_setup,
     tooltip='Separate target model for 1H pullback-continuation. test60 showed 1H PB often has enough MFE for a tactical continuation trade, while the structural target is too far and turns workable moves into full -1R losses. 0 disables this cap.')
stopBufferATR = input.float(0.5, 'Structure Stop Buffer (ATR×)', minval=0.0, maxval=5.0, step=0.1, group=g_setup,
     tooltip='Buffer beyond the latest swing low/high used for the initial stop.')
fallbackStopATR = input.float(2.5, 'Fallback Stop (ATR×)', minval=0.5, maxval=10.0, step=0.25, group=g_setup,
     tooltip='Fallback stop distance when no usable swing exists.')
shortRRRequireConfirm = input.bool(true, 'SHORT Range-Reversal: Require Pullback Confirm', group=g_setup,
     tooltip='Restricts SHORT range-reversal entries to those that also have pullback location (near EMA/swing resistance) or bear structure trend. Without it, a short reversal can fire anywhere in the range, with no resistance level or bear structure behind it.')
rrBlockCompression = input.bool(true, 'Range-Reversal: Block Unconfirmed HL+LH Compression', group=g_setup,
     tooltip='Symmetric guard for compression structure. In HL+LH compression, LONG range-reversal needs a confirmed HH and SHORT range-reversal needs a confirmed LL.')
structurePlaybookMode = input.string('1H Only', 'Structure Playbook Gate',
     options=['Off', '1H Only', 'All TF'], group=g_setup,
     tooltip='Entry-side concept gate based on the tested HH/HL/LL/LH matrix. This prevents setup families from trading structure phases where the logs showed stable damage, instead of trying to repair them with exits.')
longPBRequireConfirm = input.bool(true, 'LONG Pullback-Continuation: Require Range-Edge or Volume Confirm', group=g_setup,
     tooltip='Restricts LONG pullback-continuation entries to those that also have range-edge location OR a bullish volume spike with structure. Without it, any pullback qualifies as a continuation, including ones that are already a trend reversal.')
pullbackTriggerZone = input.float(35.0, 'Pullback WT Trigger Zone (±)', minval=10.0, maxval=50.0, step=2.5, group=g_setup,
     tooltip='Pullback continuation uses its own WT trigger: a cross while the oscillator is in the neutral band ±this value. Range-reversal still requires the OB/OS-based trigger.')
volatilityRegimeMode = input.string('Hard Gate', 'Volatility Regime Mode',
     options=['Off', 'Score Only', 'Hard Gate'], group=g_setup,
     tooltip='How the volatility regime check (atrNorm = ATR(14) / SMA(ATR(14), 100)) acts:\n• Off — ignored\n• Score Only — adds +0.5 context score when atrNorm is in band (advisory, never blocks)\n• Hard Gate — blocks all signals outside the band (default — preserves multi-instrument validation behavior)')
volatilityRegimeMin = input.float(0.7, 'Volatility Regime Min (atrNorm)', minval=0.0, maxval=2.0, step=0.05, group=g_setup,
     tooltip='Lower bound for the regime band. Below this, market is too quiet — no edge. Compression breakouts will be missed in Hard Gate mode.')
volatilityRegimeMax = input.float(1.5, 'Volatility Regime Max (atrNorm)', minval=0.5, maxval=5.0, step=0.1, group=g_setup,
     tooltip='Upper bound for the regime band. Above this, market is too chaotic — outside the strategy working range.')

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 6 — Composite Signal
// ═══════════════════════════════════════════════════════════════════════════

g_sig = '6. Composite Signal'

// ─── Trigger + Context model ─────────────────────────────────────────────────
// Trigger is a binary requirement (WT Cross after a fresh OB/OS visit). Context
// is a score across independent components. This separation prevents the
// double-counting bug where Cross-in-Zone added 2 score points for one event.

minContextScore = input.float(3.0, 'Min Context Score', minval=0, maxval=7, step=0.5, group=g_sig,
     tooltip='Trigger (WT Cross after a fresh OB/OS visit) is binary required. Then context score must reach this threshold:\n\n• +1 Structure (HL/no-LL for long, LH/no-HH for short — reversal-friendly)\n• +1 POC reclaim/reject (event-based, not just above/below)\n• +1 Directional volume spike (volume + bullish/bearish bar shape)\n• +htfWeight setup HTF WT direction agrees, only when not already gated\n• +anchorWeight according to Anchor Score Mode\n\nDefault needs setup HTF alignment, two local confirmations, and/or contrarian anchor context.')

pocRecentBars = input.int(5, '  POC Reclaim/Reject Window', minval=1, maxval=20, group=g_sig,
     tooltip='POC reclaim/reject is counted as context if it occurred within this many bars of the trigger.')

showSignals = input.bool(true, 'Show Composite Signals', group=g_sig)
debugMode   = input.bool(false, 'Debug Mode (verbose log)', group=g_sig)

// ATR length used by setup-gate room/proximity calcs and the volatility regime gate
atrLen  = input.int(14,  'ATR Length',     minval=1, group=g_sig, display=display.none)

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 7 — Profile & Per-Family Gating
// ═══════════════════════════════════════════════════════════════════════════
// Profile mode is a configuration layer over the raw inputs. When set to a
// named profile, it overrides minContextScore, volumeGateMode, mtfGateMode,
// mtfGateApplyTo, anchorScoreApplyTo, shortRRRequireConfirm,
// pullbackTriggerZone, usePullbackSetup. Set to "Custom" to use raw inputs.
// Per-family gates (mtfGateApplyTo, anchorScoreApplyTo) decide whether the
// MTF / anchor scoring/gating applies to range-reversal, pullback, both, none.

g_profile = '7. Profile & Per-Family Gates'

profileMode = input.string('Balanced', 'Strategy Profile',
     options=['Strict', 'Balanced', 'Aggressive', 'Reversal', 'Pullback', 'Diagnostic', 'Custom'], group=g_profile,
     tooltip='Preset configurations:\n• Strict — high score bar, all gates on\n• Balanced — current best defaults (PB-only MTF, SHORT-RR confirm)\n• Aggressive — minimal gating, more trades\n• Reversal — only range-reversal family, contrarian anchor\n• Pullback — only pullback-flavored gating, looser score\n• Diagnostic — accept everything, log everything\n• Custom — use the raw inputs as-is')

mtfGateApplyTo = input.string('Pullback Only', 'Apply MTF Gate To',
     options=['All', 'Reversal Only', 'Pullback Only', 'None'], group=g_profile,
     tooltip='Which setup family is hard-gated by MTF (when mtfGateMode != None). Range-reversal often happens before HTF turns, so Pullback Only is a sensible default.')

anchorScoreApplyTo = input.string('All', 'Apply Anchor Score To',
     options=['All', 'Reversal Only', 'Pullback Only', 'None'], group=g_profile,
     tooltip='Which setup family the anchor score component contributes to.')

// ═══════════════════════════════════════════════════════════════════════════
// SECTION 8 — Exit Logic
// ═══════════════════════════════════════════════════════════════════════════
// Active exit signals fired by the indicator (in addition to fixed SL/TP). Each
// can be toggled. They are visualized on chart so you can see WHY a trade exited.

g_exit = '8. Exit Logic'

useExitOpposite = input.bool(true, 'Exit on Opposite Signal', group=g_exit,
     tooltip='Close LONG when SHORT signal fires (and vice versa). Universal — fires regardless of family.')
oppositeExitMinScore = input.float(3.0, '  Opposite Exit Min Score (conviction gate)', minval=0.0, maxval=7.0, step=0.5, group=g_exit,
     tooltip='Require the opposite signal to have at least this context score before closing. Filters out weak counter-signals that would needlessly cut good trades. Default 3.0 = requires same conviction as Balanced minScore.')
useExitFamilyWT = input.bool(true, 'Exit on Family-WT Cross', group=g_exit,
     tooltip='Close when WT momentum turns against the open trade. This is trade-management logic, not a re-entry filter.')
useExitGiveback = input.bool(true, 'Exit on MFE Giveback', group=g_exit,
     tooltip='Protect trades that have already moved in favor. Once MFE reaches the trigger, close if too much of the open R profit is given back.')
mfeProtectR = input.float(1.5, '  MFE Protect Trigger (R)', minval=0.1, maxval=10.0, step=0.1, group=g_exit,
     tooltip='Minimum peak R before giveback protection starts.')
pbMfeProtectR = input.float(1.0, '  PB MFE Protect Trigger (R)', minval=0.1, maxval=10.0, step=0.1, group=g_exit,
     tooltip='Earlier giveback trigger for pullback-continuation trades. PB trades in NatGas often reached ~1R MFE and then slipped back to loss; RR keeps the main MFE trigger above.')
erMfeProtectR = input.float(1.5, '  ER MFE Protect Trigger (R)', minval=0.1, maxval=10.0, step=0.1, group=g_exit,
     tooltip='Giveback trigger for early-rejection trades. test61 showed 1.0R as a global default cuts too many 15m/1H ER target winners; use 1.0 only as an explicit capture experiment.')
maxGivebackPct = input.float(65.0, '  Max Giveback % of MFE', minval=10.0, maxval=90.0, step=5.0, group=g_exit,
     tooltip='How much of peak R may be given back before closing. Example: MFE 2R and 65% giveback exits near 0.7R.')
givebackFloorR = input.float(0.2, '  Giveback Floor (R)', minval=-1.0, maxval=3.0, step=0.1, group=g_exit,
     tooltip='Lowest acceptable R after protection has activated. Prevents a once-good trade from returning to a full loss.')
useStrategyGivebackStop = input.bool(false, '  Use Intrabar Giveback Stop', group=g_exit,
     tooltip='Optional. Uses the MFE giveback level as a live strategy.exit stop. test61 showed this removes full-stop-after-MFE losses, but cuts too many 15m/1H ER target winners when enabled globally.')
useConfirmedGivebackExit = input.bool(true, '  Exit Confirmed MFE Giveback', group=g_exit,
     tooltip='Close only after MFE protection is active, profit has given back below the allowed R level, and a confirmed adverse pivot or structure break appears. This tests the test61 conclusion without placing an intrabar stop.')
confirmedGivebackTFMode = input.string('4H Only', '    Confirmed Giveback TF Mode',
     options=['Off', '4H Only', '15m + 4H', 'All TF'], group=g_exit,
     tooltip='Where the confirmed MFE giveback exit may execute. test62 showed All TF hurts 15m, is neutral on 1H, and only helps 4H slightly.')
confirmedGivebackUseStructure = input.bool(true, '    Confirm With Structure Break', group=g_exit,
     tooltip='Allow confirmed giveback exit when opposite structure invalidates the trade after MFE protection is active.')
confirmedGivebackUsePivot = input.bool(true, '    Confirm With Adverse Pivot', group=g_exit,
     tooltip='Allow confirmed giveback exit when a confirmed pivot forms beyond the giveback R level after MFE protection is active.')
useExitTimeFail = input.bool(true, 'Exit on Time Failure (no progress)', group=g_exit,
     tooltip='Close trade when N bars passed AND running R-multiple is below threshold. Prevents stagnant trades from running to SL.')
maxBarsInTrade  = input.int(30,  '  Max Bars in Trade (4H baseline)', minval=5, maxval=200, group=g_exit,
     tooltip='Bar window after which Time-Failure exit applies. Auto-scaled to chart TF.')
minProfitR      = input.float(0.0, '  Min R-Multiple to Continue', minval=-2.0, maxval=3.0, step=0.1, group=g_exit,
     tooltip='If running profit (in R-multiples) is below this after maxBarsInTrade, the trade is closed. Default 0.0 = only close losing stagnant trades; breakeven stays open. Set to e.g. 0.5 if you want to also cut weak winners.')
useLowerTfEarlyFailure = input.bool(false, 'Exit Lower-TF RR/ER Non-Starters', group=g_exit,
     tooltip='Experimental 15m/1H-only exit for range-reversal and early-rejection non-starters. test53 showed the default worsened 15m/1H by creating churn and weakening indicator-exit flow, so it is off by default.')
lowerTfEarlyFailureBars = input.int(8, '  Lower-TF Early Failure Bars', minval=2, maxval=80, group=g_exit)
lowerTfEarlyFailureMfeR = input.float(0.50, '  Lower-TF Early Failure Min MFE (R)', minval=0.0, maxval=3.0, step=0.05, group=g_exit)
oneHourPullbackFailureMode = input.string('Off', 'Exit 1H Pullback Non-Starters',
     options=['Off', 'Aggressive Only', 'Balanced Only', 'All Profiles'], group=g_exit,
     tooltip='Experimental 1H pullback damage-control exit. test64 confirmed the non-starter class, but also showed that reactive exits are not the right default concept. Keep off unless explicitly isolating this mechanism.')
oneHourPullbackFailureBars = input.int(5, '  1H PB Failure Bars', minval=2, maxval=30, group=g_exit)
oneHourPullbackFailureMfeR = input.float(0.25, '  1H PB Failure Min MFE (R)', minval=0.0, maxval=2.0, step=0.05, group=g_exit)
useOneHourPullbackFailure = oneHourPullbackFailureMode == 'All Profiles' or (oneHourPullbackFailureMode == 'Aggressive Only' and profileMode == 'Aggressive') or (oneHourPullbackFailureMode == 'Balanced Only' and profileMode == 'Balanced')
useExitFollowThr = input.bool(false, 'Exit on Follow-Through Failure (opt-in)', group=g_exit,
     tooltip='After N bars from entry, require highest high (long) / lowest low (short) to be at least followThroughATR × ATR beyond entry. Aggressive — kills slow-but-eventual-winners.')
followThroughBars = input.int(3, '  Follow-Through Bars (4H baseline)', minval=2, maxval=20, group=g_exit)
followThroughATR  = input.float(0.3, '  Follow-Through ATR×', minval=0.0, maxval=3.0, step=0.05, group=g_exit)

showExitMarkers = input.bool(true, 'Show Exit Markers', group=g_exit,
     tooltip='Draw a label on chart at each indicator-driven exit, color-coded by reason.')

// ═══════════════════════════════════════════════════════════════════════════
// FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════

f_jma(src, length, phase, power) =>
    var float jma = na
    var float e0 = na
    var float e1 = na
    var float e2 = na
    _phaseRatio = phase < -100 ? 0.5 : phase > 100 ? 2.5 : phase / 100.0 + 1.5
    _beta  = 0.45 * (length - 1) / (0.45 * (length - 1) + 2)
    _alpha = math.pow(_beta, power)
    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 := nz(jma[1], src) + e2
    jma

f_smooth(src, length, mode) =>
    switch mode
        'SMA' => ta.sma(src, length)
        'RMA' => ta.rma(src, length)
        'HMA' => ta.hma(src, length)
        'WMA' => ta.wma(src, length)
        'JMA' => f_jma(src, length, jmaPhase, jmaPower)
        =>      ta.ema(src, length)

_chartSecRaw = timeframe.in_seconds()
_chartSec = na(_chartSecRaw) ? 14400 : _chartSecRaw
tfScale = useAutoTF ? 14400.0 / math.max(float(_chartSec), 60.0) : 1.0

f_scaleBars(base, floor, cap) =>
    int(math.min(math.max(math.round(base * tfScale), floor), cap))

f_getWT(src, n1, n2) =>
    _normMode = wtEnginePreset == 'Classic Stable' ? 'Mean Abs Dev' : normMode
    _smoothType = wtEnginePreset == 'Classic Stable' ? 'EMA' : smoothType
    _esa = ta.ema(src, n1)
    _d   = _normMode == 'ATR' ? ta.atr(n1) : ta.ema(math.abs(src - _esa), n1)
    _den = ciConst * _d
    _ci  = _den != 0 ? (src - _esa) / _den : 0
    f_smooth(_ci, n2, _smoothType)

f_autoTF(seconds) =>
    string _tf = "W"
    if seconds <= 3600
        _tf := "60"
    else if seconds <= 14400
        _tf := "240"
    else if seconds <= 86400
        _tf := "D"
    _tf

// HTF wtOsc — for use in the alignment score
_setupSec  = _chartSec * 4
_anchorSec = _chartSec * 24
zoneExitWindowA = f_scaleBars(zoneExitWindow, 1, 40)
structLookbackA = f_scaleBars(structLookback, 20, 500)
setupRangeLenA = f_scaleBars(setupRangeLen, 30, 500)
// pocLookbackA / pocMacroLookbackA are recomputed below after profile resolution
pocRecentBarsA = f_scaleBars(pocRecentBars, 1, 30)
volMALenA = f_scaleBars(volMALen, 10, 300)
pullbackEmaLenA = f_scaleBars(pullbackEmaLen, 5, 300)
_autoSetupHTF = f_autoTF(_setupSec)
_autoAnchorHTF = f_autoTF(_anchorSec)
_htfTF     = htfTimeframe == "" ? _autoSetupHTF : htfTimeframe
_anchorTF  = anchorTimeframe == "" ? _autoAnchorHTF : anchorTimeframe
_anchorActive = _anchorTF != _htfTF

htfWtOsc = request.security(syminfo.tickerid, _htfTF,
     f_getWT(hlc3, chLength, avgLength),
     lookahead = barmerge.lookahead_off)

anchorWtOsc = request.security(syminfo.tickerid, _anchorTF,
     f_getWT(hlc3, chLength, avgLength),
     lookahead = barmerge.lookahead_off)

// ═══════════════════════════════════════════════════════════════════════════
// PROFILE RESOLUTION — turns named profileMode into effective config
// ═══════════════════════════════════════════════════════════════════════════
// When profileMode != 'Custom' the profile overrides selected raw inputs.
// Inputs that the profile does NOT touch (HTF tfs, weights, edges, etc.)
// always come straight from the user.

_isCustomProfile = profileMode == 'Custom'
float _effMinScore = minContextScore
string _effVolMode = volumeGateMode
string _effMtfGateMode = mtfGateMode
string _effMtfApplyTo = mtfGateApplyTo
string _effAnchorApplyTo = anchorScoreApplyTo
bool _effShortRRConfirm = shortRRRequireConfirm
bool _effLongPBConfirm = longPBRequireConfirm
float _effPbZone = pullbackTriggerZone
bool _effUsePullback = usePullbackSetup
int _effUpperBand = upperBand
int _effLowerBand = lowerBand
int _effPocLocalLookback = pocLookback
int _effPocMacroLookback = pocMacroLookback
float _effSetupEdgePct = setupEdgePct
float _effSwingProximityATR = swingProximityATR
float _effVolRegimeMin = volatilityRegimeMin
float _effVolRegimeMax = volatilityRegimeMax
string _effRegimeMode = volatilityRegimeMode
float _effMinTargetRoomR = minTargetRoomR

if not _isCustomProfile
    if profileMode == 'Strict'
        _effMinScore := 4.0
        _effSetupEdgePct := 0.30
        _effSwingProximityATR := 1.25
        _effVolMode := 'All TF'
        _effMtfGateMode := 'Setup Only'
        _effMtfApplyTo := 'All'
        _effAnchorApplyTo := 'All'
        _effShortRRConfirm := true
        _effLongPBConfirm := true
        _effPbZone := 25.0
        _effUpperBand := 60
        _effLowerBand := -60
        _effPocLocalLookback := 120
        _effPocMacroLookback := 400
        _effVolRegimeMin := 0.8
        _effVolRegimeMax := 1.3
        _effRegimeMode := 'Hard Gate'
    else if profileMode == 'Balanced'
        // Aligned to Aggressive's loose setup gates so all edge candidates
        // are reachable, but keeps minScore=3.0 on the lower-TF baseline to
        // filter the noise tier (the Score 2.5 tier drops out — see test31 in
        // the assessment record).
        _effMinScore := 3.0
        _effSetupEdgePct := 0.45
        _effSwingProximityATR := 2.5
        _effVolMode := 'Lower TF Only'
        _effMtfGateMode := 'None'
        _effMtfApplyTo := 'None'
        _effAnchorApplyTo := 'All'
        _effShortRRConfirm := true
        _effLongPBConfirm := true
        _effPbZone := 50.0
        _effUpperBand := 45
        _effLowerBand := -45
        _effPocLocalLookback := 80
        _effPocMacroLookback := 300
        _effVolRegimeMin := 0.6
        _effVolRegimeMax := 1.6
        _effRegimeMode := 'Score Only'
        // test42 NatGas: 4H Score 3.5 and VOL=false were the main drag, while
        // 15m/Daily pullbacks underperformed their reversal families.
        if _chartSec == 14400
            _effMinScore := 4.0
            _effVolMode := 'All TF'
        if _chartSec <= 900 or _chartSec >= 86400
            _effUsePullback := false
        // test55 rejected an automatic 1H R-room gate as a profile default.
        // Keep `Min Room to Target (R)` manual/Custom unless explicitly set.
    else if profileMode == 'Aggressive'
        _effMinScore := 2.0
        _effSetupEdgePct := 0.45
        _effSwingProximityATR := 2.5
        _effVolMode := 'Off'
        _effMtfGateMode := 'None'
        _effMtfApplyTo := 'None'
        _effShortRRConfirm := true
        _effLongPBConfirm := true
        _effPbZone := 50.0
        _effUpperBand := 45
        _effLowerBand := -45
        _effPocLocalLookback := 80
        _effPocMacroLookback := 200
        _effVolRegimeMin := 0.5
        _effVolRegimeMax := 2.0
        _effRegimeMode := 'Score Only'
        // test44 NatGas: keep Aggressive as the higher-trade profile, but
        // remove the weak slices that showed up consistently by timeframe.
        if _chartSec == 900
            _effUsePullback := false
        if _chartSec == 3600
            _effMinScore := 3.0
        if _chartSec == 14400
            _effMinScore := 4.0
            _effVolMode := 'All TF'
    else if profileMode == 'Reversal'
        _effMinScore := 3.0
        _effSetupEdgePct := 0.40
        _effSwingProximityATR := 2.0
        _effVolMode := 'Reversal Only'
        _effMtfApplyTo := 'None'
        _effAnchorApplyTo := 'Reversal Only'
        _effShortRRConfirm := true
        _effUsePullback := false
        _effUpperBand := 45
        _effLowerBand := -45
        _effPocLocalLookback := 120
        _effPocMacroLookback := 400
        _effVolRegimeMin := 0.6
        _effVolRegimeMax := 1.4
        _effRegimeMode := 'Hard Gate'
    else if profileMode == 'Pullback'
        _effMinScore := 2.5
        _effSetupEdgePct := 0.35
        _effSwingProximityATR := 1.5
        _effVolMode := 'Pullback Only'
        _effMtfGateMode := 'Setup Only'
        _effMtfApplyTo := 'Pullback Only'
        _effAnchorApplyTo := 'Pullback Only'
        _effLongPBConfirm := true
        _effPbZone := 35.0
        _effUpperBand := 56
        _effLowerBand := -56
        _effPocLocalLookback := 100
        _effPocMacroLookback := 300
        _effVolRegimeMin := 0.7
        _effVolRegimeMax := 1.3
        _effRegimeMode := 'Hard Gate'
    else if profileMode == 'Diagnostic'
        _effMinScore := 0.0
        _effSetupEdgePct := 0.50
        _effSwingProximityATR := 5.0
        _effVolMode := 'Off'
        _effMtfApplyTo := 'None'
        _effAnchorApplyTo := 'None'
        _effShortRRConfirm := false
        _effLongPBConfirm := false
        _effPbZone := 50.0
        _effUpperBand := 20
        _effLowerBand := -20
        _effPocLocalLookback := 100
        _effPocMacroLookback := 300
        _effVolRegimeMin := 0.0
        _effVolRegimeMax := 99.0
        _effRegimeMode := 'Off'

// Per-family gate booleans (derived once, used downstream)
_mtfHardRR = _effMtfGateMode != 'None' and (_effMtfApplyTo == 'All' or _effMtfApplyTo == 'Reversal Only')
_mtfHardPB = _effMtfGateMode != 'None' and (_effMtfApplyTo == 'All' or _effMtfApplyTo == 'Pullback Only')
_anchorReqRR = _effAnchorApplyTo == 'All' or _effAnchorApplyTo == 'Reversal Only'
_anchorReqPB = _effAnchorApplyTo == 'All' or _effAnchorApplyTo == 'Pullback Only'
_volReqRR = _effVolMode == 'All TF' or _effVolMode == 'Reversal Only' or (_effVolMode == 'Lower TF Only' and _chartSec <= 3600)
_volReqPB = _effVolMode == 'Pullback Only'  // 'All TF' intentionally excluded: pullbacks have declining vol by design; vol is already score component

// POC lookbacks (dual horizon) use effective profile values, auto-scaled
pocLookbackA      = f_scaleBars(_effPocLocalLookback, 30, 500)
pocMacroLookbackA = f_scaleBars(_effPocMacroLookback, 50, 1000)

// ═══════════════════════════════════════════════════════════════════════════
// CALCULATIONS
// ═══════════════════════════════════════════════════════════════════════════

// SECTION 1 — WaveTrend
wtOsc = f_getWT(hlc3, chLength, avgLength)
wtSig = ta.sma(wtOsc, sigLength)
activeNormMode   = wtEnginePreset == 'Classic Stable' ? 'Mean Abs Dev' : normMode
activeSmoothType = wtEnginePreset == 'Classic Stable' ? 'EMA' : smoothType
hmaMode = activeSmoothType == 'HMA'

// Fast PB oscillator: HMA pass over wtOsc with a short window.
// RR/ER use wtOsc (base smoothing). PB uses wtOscFast (HMA re-smoothed) for
// earlier momentum turns in the neutral zone without affecting RR timing.
wtOscFast = f_smooth(wtOsc, math.max(2, sigLength), 'HMA')
wtSigFast = ta.sma(wtOscFast, sigLength)

// Williams Vix Fix — fires at price extremes, before WT oscillator catches up
wvf        = (ta.highest(close, wvfLen) - low)  / ta.highest(close, wvfLen) * 100
wvfInv     = (high - ta.lowest(close, wvfLen))  / ta.lowest(close, wvfLen)  * 100
wvfMean    = ta.sma(wvf,    wvfBBLen)
wvfInvMean = ta.sma(wvfInv, wvfBBLen)
wvfBBHigh    = wvfMean    + wvfBBMult * ta.stdev(wvf,    wvfBBLen)
wvfInvBBHigh = wvfInvMean + wvfBBMult * ta.stdev(wvfInv, wvfBBLen)
wvfSpike    = useWVF and wvf    >= wvfBBHigh
wvfInvSpike = useWVF and wvfInv >= wvfInvBBHigh
barsSinceWvfSpike    = nz(ta.barssince(wvfSpike),    999)
barsSinceWvfInvSpike = nz(ta.barssince(wvfInvSpike), 999)
wvfRecentBull = barsSinceWvfSpike    <= zoneExitWindowA
wvfRecentBear = barsSinceWvfInvSpike <= zoneExitWindowA

// Oscillator momentum — fires at the oscillator peak/trough, not the crossover.
// This is ~sigLen/2 bars earlier than ta.crossover(wtOsc,wtSig) and much closer
// to the actual price pivot.
wtMomUp   = ta.crossover(wtOsc - wtOsc[1], 0)
wtMomDown = ta.crossunder(wtOsc - wtOsc[1], 0)

// Reversal zone: WT oscillator recently in OS/OB, OR WVF spike recently fired.
// WVF extends the zone definition to the price side without adding a hard gate.
inOSZone = wtOsc < _effLowerBand
inOBZone = wtOsc > _effUpperBand
barsSinceOS = nz(ta.barssince(inOSZone), 999)
barsSinceOB = nz(ta.barssince(inOBZone), 999)
osRecent = barsSinceOS <= zoneExitWindowA or wvfRecentBull
obRecent = barsSinceOB <= zoneExitWindowA or wvfRecentBear

rawStrongBullCross = wtMomUp   and wtOsc < _effLowerBand
rawStrongBearCross = wtMomDown and wtOsc > _effUpperBand

// HMA mode fires earlier — add price-direction confirmation to avoid
// entries where the oscillator turns before price confirms.
crossUp   = wtMomUp   and osRecent and wtOsc < 0 and (not hmaMode or close > close[1])
crossDown = wtMomDown and obRecent and wtOsc > 0 and (not hmaMode or close < close[1])

// SECTION 2 — Structure detection
_phFound = not na(ta.pivothigh(high, structPivLen, structPivLen))
_plFound = not na(ta.pivotlow(low, structPivLen, structPivLen))

var float lastSwingHigh1 = na
var float lastSwingHigh2 = na
var float lastSwingLow1  = na
var float lastSwingLow2  = na
var float lastSwingHighWt1 = na
var float lastSwingHighWt2 = na
var float lastSwingLowWt1  = na
var float lastSwingLowWt2  = na
var int lastSwingHighBar1 = na
var int lastSwingHighBar2 = na
var int lastSwingLowBar1  = na
var int lastSwingLowBar2  = na

if _phFound
    lastSwingHigh2 := lastSwingHigh1
    lastSwingHighWt2 := lastSwingHighWt1
    lastSwingHighBar2 := lastSwingHighBar1
    lastSwingHigh1 := high[structPivLen]
    lastSwingHighWt1 := wtOsc[structPivLen]
    lastSwingHighBar1 := bar_index - structPivLen

if _plFound
    lastSwingLow2 := lastSwingLow1
    lastSwingLowWt2 := lastSwingLowWt1
    lastSwingLowBar2 := lastSwingLowBar1
    lastSwingLow1 := low[structPivLen]
    lastSwingLowWt1 := wtOsc[structPivLen]
    lastSwingLowBar1 := bar_index - structPivLen

// Structure state — reversal-friendly (Fix #2):
// Long-OK requires either HL (constructive low) or absence of fresh LL.
// Short-OK requires either LH (constructive high) or absence of fresh HH.
// HH+HL is too late for reversal-trading (move already underway).
hhDetected = not na(lastSwingHigh1) and not na(lastSwingHigh2) and lastSwingHigh1 > lastSwingHigh2
hlDetected = not na(lastSwingLow1)  and not na(lastSwingLow2)  and lastSwingLow1  > lastSwingLow2
lhDetected = not na(lastSwingHigh1) and not na(lastSwingHigh2) and lastSwingHigh1 < lastSwingHigh2
llDetected = not na(lastSwingLow1)  and not na(lastSwingLow2)  and lastSwingLow1  < lastSwingLow2
wtBullDiv = not na(lastSwingLow1) and not na(lastSwingLow2) and not na(lastSwingLowWt1) and not na(lastSwingLowWt2) and lastSwingLow1 < lastSwingLow2 and lastSwingLowWt1 > lastSwingLowWt2
wtBearDiv = not na(lastSwingHigh1) and not na(lastSwingHigh2) and not na(lastSwingHighWt1) and not na(lastSwingHighWt2) and lastSwingHigh1 > lastSwingHigh2 and lastSwingHighWt1 < lastSwingHighWt2

// Structure freshness (Fix #6 — was structLookback unused)
var int lastStructBar = na
var int lastHighStructBar = na
var int lastLowStructBar = na
if _phFound or _plFound
    lastStructBar := bar_index
if _phFound
    lastHighStructBar := bar_index
if _plFound
    lastLowStructBar := bar_index
structureFresh = not na(lastStructBar) and (bar_index - lastStructBar) <= structLookbackA
highStructureFresh = not na(lastHighStructBar) and (bar_index - lastHighStructBar) <= structLookbackA
lowStructureFresh = not na(lastLowStructBar) and (bar_index - lastLowStructBar) <= structLookbackA
lastPivotWasLow = not na(lastLowStructBar) and (na(lastHighStructBar) or lastLowStructBar > lastHighStructBar)
barsSinceHighPivotConfirm = not na(lastHighStructBar) ? bar_index - lastHighStructBar : na
barsSinceLowPivotConfirm = not na(lastLowStructBar) ? bar_index - lastLowStructBar : na
barsSinceHighPivotActual = not na(lastSwingHighBar1) ? bar_index - lastSwingHighBar1 : na
barsSinceLowPivotActual = not na(lastSwingLowBar1) ? bar_index - lastSwingLowBar1 : na

// Require real swing history before granting structure points (no
// false-positives during warm-up where `not llDetected` is trivially true
// because no swings exist yet).
hasLowStructure  = not na(lastSwingLow1)  and not na(lastSwingLow2)
hasHighStructure = not na(lastSwingHigh1) and not na(lastSwingHigh2)

// Long conflict: HL with a still-active LH means compression under a lower high.
// Test11/Test12 showed these longs were consistently poor; wait for HH or a
// deeper reversal setup instead of treating HL alone as constructive.
structureLongConflict = hlDetected and lhDetected and not hhDetected
structureLongOk  = lowStructureFresh and hasLowStructure  and not structureLongConflict and (hlDetected or not llDetected)
// Short conflict: LL with a still-active LH often means the move is already
// mature. 4H NatGas tests showed these shorts were the main drag; prefer lower
// high rollover before a fresh lower low, or stronger non-structure context.
structureShortConflict = llDetected and lastPivotWasLow
structureShortOk = highStructureFresh and hasHighStructure and not structureShortConflict and (lhDetected or not hhDetected)

// Kept for dashboard / log display only (not used in score)
structureBull = hhDetected and hlDetected
structureBear = llDetected and lhDetected

// SECTION 3 — Volume context
volMA = ta.sma(volume, volMALenA)
volSpike = volume > volMA * volSpikeMult

// Directional volume spike (Fix #4) — Spike + bar-shape confirms direction.
// Avoids rewarding panic-selloff bars as bullish or vice versa.
_barRange = high - low
_closePos = _barRange > 0 ? (close - low) / _barRange : 0.5
bullVolSpike = volSpike and _closePos > 0.65
bearVolSpike = volSpike and _closePos < 0.35

// Momentum/flow confirmation for early rejection setups.
// MFI is useful on commodities because it combines price displacement with
// volume. Stoch RSI is faster and catches oscillator exhaustion; require only
// one of them so the setup remains usable on quiet bars.
mfiVal = ta.mfi(hlc3, mfiLen)
_rsiForStoch = ta.rsi(close, stochRsiLen)
_rsiRange = ta.highest(_rsiForStoch, stochRsiLen) - ta.lowest(_rsiForStoch, stochRsiLen)
_stochRsiRaw = _rsiRange > 0 ? 100.0 * (_rsiForStoch - ta.lowest(_rsiForStoch, stochRsiLen)) / _rsiRange : 50.0
stochRsiK = ta.sma(_stochRsiRaw, stochRsiSmoothK)
stochRsiD = ta.sma(stochRsiK, stochRsiSmoothD)
mfiBullConfirm = (mfiVal > mfiVal[1] and mfiVal < 65) or ta.crossover(mfiVal, 50)
mfiBearConfirm = (mfiVal < mfiVal[1] and mfiVal > 35) or ta.crossunder(mfiVal, 50)
stochRsiBullConfirm = ta.crossover(stochRsiK, stochRsiD) or (stochRsiK > stochRsiD and stochRsiK < 45)
stochRsiBearConfirm = ta.crossunder(stochRsiK, stochRsiD) or (stochRsiK < stochRsiD and stochRsiK > 55)
earlyMomentumBull = mfiBullConfirm or stochRsiBullConfirm
earlyMomentumBear = mfiBearConfirm or stochRsiBearConfirm
[_erPlusDI, _erMinusDI, _erAdx] = ta.dmi(oneHourERDmiLen, oneHourERAdxSmoothing)

// Simple POC computation: most-traded price level over pocLookback
f_computePOC(lookback, bins) =>
    _hi = ta.highest(high, lookback)
    _lo = ta.lowest(low, lookback)
    _binSize = (_hi - _lo) / bins
    var float[] _binVols = array.new<float>(bins, 0.0)
    if barstate.isconfirmed and _binSize > 0
        for i = 0 to bins - 1
            array.set(_binVols, i, 0.0)
        for j = 0 to lookback - 1
            _midPx = (high[j] + low[j]) / 2
            _binIdx = math.floor((_midPx - _lo) / _binSize)
            _binIdx := math.max(0, math.min(bins - 1, _binIdx))
            array.set(_binVols, _binIdx, array.get(_binVols, _binIdx) + nz(volume[j], 0))
    var float pocPx = na
    if barstate.isconfirmed and _binSize > 0
        _maxIdx = 0
        _maxVol = 0.0
        for i = 0 to bins - 1
            _v = array.get(_binVols, i)
            if _v > _maxVol
                _maxVol := _v
                _maxIdx := i
        pocPx := _lo + (_maxIdx + 0.5) * _binSize
    pocPx

// Local POC (short horizon) drives entry-context score (reclaim/reject).
// Macro POC (long horizon) is bias / informational only.
pocPrice      = useVolPOC ? f_computePOC(pocLookbackA, pocBins) : na
pocMacroPrice = useVolPOC ? f_computePOC(pocMacroLookbackA, pocBins) : na

// Above/Below for dashboard display (local POC, not used in score directly)
priceAbovePOC = useVolPOC and not na(pocPrice) and close > pocPrice
priceBelowPOC = useVolPOC and not na(pocPrice) and close < pocPrice
priceAboveMacroPOC = useVolPOC and not na(pocMacroPrice) and close > pocMacroPrice
priceBelowMacroPOC = useVolPOC and not na(pocMacroPrice) and close < pocMacroPrice

// POC reclaim/reject — event-based context (Fix #3).
// Long-meaningful: price was below POC and just closed back above.
// Short-meaningful: price was above POC and just closed back below.
// Explicit bar-over-bar comparison (both close[t] vs pocPrice[t] and
// close[t-1] vs pocPrice[t-1]) is robust against wandering POC.
pocReclaim = useVolPOC and not na(pocPrice) and not na(pocPrice[1]) and close > pocPrice and close[1] <= pocPrice[1]
pocReject  = useVolPOC and not na(pocPrice) and not na(pocPrice[1]) and close < pocPrice and close[1] >= pocPrice[1]
_barsSincePocReclaim = nz(ta.barssince(pocReclaim), 999)
_barsSincePocReject  = nz(ta.barssince(pocReject), 999)
pocReclaimRecent = _barsSincePocReclaim <= pocRecentBarsA
pocRejectRecent  = _barsSincePocReject  <= pocRecentBarsA

// SECTION 4 — Multi-Timeframe Trend (per-family gating)
// MTF / anchor are now applied per setup family. _mtfHardRR / _mtfHardPB
// (computed in profile resolution) decide whether the family is hard-gated.
htfBull = not na(htfWtOsc) and htfWtOsc > 0
htfBear = not na(htfWtOsc) and htfWtOsc < 0
anchorBull = not na(anchorWtOsc) and anchorWtOsc > 0
anchorBear = not na(anchorWtOsc) and anchorWtOsc < 0
autoTFBiasActive = useAutoTFBias and _chartSec <= 3600 and not na(htfWtOsc) and math.abs(htfWtOsc) >= autoTFBiasMinWT
autoTFBiasLongOk = not autoTFBiasActive or htfBull
autoTFBiasShortOk = not autoTFBiasActive or htfBear
autoTFBiasAppliesRR = autoTFBiasApplyTo == 'All'
autoTFBiasAppliesPB = autoTFBiasApplyTo == 'PB Only' or autoTFBiasApplyTo == 'ER + PB' or autoTFBiasApplyTo == 'All'
autoTFBiasAppliesER = autoTFBiasApplyTo == 'ER Only' or autoTFBiasApplyTo == 'ER + PB' or autoTFBiasApplyTo == 'All'
longRRBiasOk = not autoTFBiasAppliesRR or autoTFBiasLongOk
shortRRBiasOk = not autoTFBiasAppliesRR or autoTFBiasShortOk
longPBBiasOk = not autoTFBiasAppliesPB or autoTFBiasLongOk
shortPBBiasOk = not autoTFBiasAppliesPB or autoTFBiasShortOk
longERBiasOk  = not autoTFBiasAppliesER or autoTFBiasLongOk or (autoTFBiasActive and htfWtOsc <= _effLowerBand)
shortERBiasOk = not autoTFBiasAppliesER or autoTFBiasShortOk or (autoTFBiasActive and htfWtOsc >= _effUpperBand)

_setupGateActive = _effMtfGateMode == 'Setup Only' or _effMtfGateMode == 'Setup + Anchor'
_anchorGateActive = _effMtfGateMode == 'Setup + Anchor'

// Underlying alignment booleans, independent of whether the gate is hard for a family
_setupLongAlign = htfWeight == 0 or htfBull
_setupShortAlign = htfWeight == 0 or htfBear
_anchorLongAlign = not _anchorActive or anchorWeight == 0 or anchorBull
_anchorShortAlign = not _anchorActive or anchorWeight == 0 or anchorBear
_mtfLongAlign  = (not _setupGateActive or _setupLongAlign)  and (not _anchorGateActive or _anchorLongAlign)
_mtfShortAlign = (not _setupGateActive or _setupShortAlign) and (not _anchorGateActive or _anchorShortAlign)

// Per-family gate verdict (true = pass)
mtfLongOkRR  = not _mtfHardRR or _mtfLongAlign
mtfLongOkPB  = not _mtfHardPB or _mtfLongAlign
mtfShortOkRR = not _mtfHardRR or _mtfShortAlign
mtfShortOkPB = not _mtfHardPB or _mtfShortAlign

// HTF score component per family (0 when MTF is hard-gated for that family — no double-count)
longHtfScoreRR  = (not _mtfHardRR) and htfBull ? htfWeight : 0.0
longHtfScorePB  = (not _mtfHardPB) and htfBull ? htfWeight : 0.0
shortHtfScoreRR = (not _mtfHardRR) and htfBear ? htfWeight : 0.0
shortHtfScorePB = (not _mtfHardPB) and htfBear ? htfWeight : 0.0

// Anchor score raw (before per-family apply)
float _anchorLongRaw = 0.0
float _anchorShortRaw = 0.0
if _anchorActive and anchorScoreMode != 'Ignore'
    if anchorScoreMode == 'Alignment'
        _anchorLongRaw := anchorBull ? anchorWeight : 0
        _anchorShortRaw := anchorBear ? anchorWeight : 0
    else if anchorScoreMode == 'Contrarian'
        _anchorLongRaw := not na(anchorWtOsc) and not anchorBull ? anchorWeight : 0
        _anchorShortRaw := not na(anchorWtOsc) and not anchorBear ? anchorWeight : 0

longAnchorScoreRR  = _anchorReqRR ? _anchorLongRaw  : 0.0
longAnchorScorePB  = _anchorReqPB ? _anchorLongRaw  : 0.0
shortAnchorScoreRR = _anchorReqRR ? _anchorShortRaw : 0.0
shortAnchorScorePB = _anchorReqPB ? _anchorShortRaw : 0.0

// SECTION 5 — Setup-first location and payoff room
atrVal = ta.atr(atrLen)

// Volatility regime — three modes (per profile / Custom):
// • Off        — never blocks, no score bonus
// • Score Only — never blocks; +0.5 to context score when atrNorm is in band
// • Hard Gate  — blocks all signals when atrNorm out of band (default)
atrNorm = atrVal / ta.sma(atrVal, 100)
_inRegimeBand = not na(atrNorm) and atrNorm >= _effVolRegimeMin and atrNorm <= _effVolRegimeMax
volRegimeOk = _effRegimeMode != 'Hard Gate' or _inRegimeBand
volRegimeBonus = _effRegimeMode == 'Score Only' and _inRegimeBand ? 0.5 : 0.0
setupRangeHigh = ta.highest(high, setupRangeLenA)
setupRangeLow = ta.lowest(low, setupRangeLenA)
setupRange = setupRangeHigh - setupRangeLow
setupRangePos = setupRange > 0 ? (close - setupRangeLow) / setupRange : 0.5

nearSwingSupport = atrVal > 0 and not na(lastSwingLow1) and lastSwingLow1 < close and (close - lastSwingLow1) / atrVal <= _effSwingProximityATR
nearSwingResistance = atrVal > 0 and not na(lastSwingHigh1) and lastSwingHigh1 > close and (lastSwingHigh1 - close) / atrVal <= _effSwingProximityATR
longRangeLocationOk = setupRangePos <= _effSetupEdgePct
shortRangeLocationOk = setupRangePos >= 1.0 - _effSetupEdgePct
longSwingLocationOk = nearSwingSupport and structureLongOk
shortSwingLocationOk = nearSwingResistance and structureShortOk
longLocationOk = longRangeLocationOk or longSwingLocationOk
shortLocationOk = shortRangeLocationOk or shortSwingLocationOk

longStructureTarget = not na(lastSwingHigh1) and lastSwingHigh1 > close ? lastSwingHigh1 : setupRangeHigh
shortStructureTarget = not na(lastSwingLow1) and lastSwingLow1 < close ? lastSwingLow1 : setupRangeLow
longTargetRoomATR = atrVal > 0 ? (longStructureTarget - close) / atrVal : 0
shortTargetRoomATR = atrVal > 0 ? (close - shortStructureTarget) / atrVal : 0
longStopStruct = not na(lastSwingLow1) and lastSwingLow1 < close ? lastSwingLow1 - stopBufferATR * atrVal : close - fallbackStopATR * atrVal
shortStopStruct = not na(lastSwingHigh1) and lastSwingHigh1 > close ? lastSwingHigh1 + stopBufferATR * atrVal : close + fallbackStopATR * atrVal
longStop = math.min(longStopStruct, close - syminfo.mintick)
shortStop = math.max(shortStopStruct, close + syminfo.mintick)
longRisk = math.max(close - longStop, syminfo.mintick)
shortRisk = math.max(shortStop - close, syminfo.mintick)
longTargetRoomR = longRisk > 0 ? (longStructureTarget - close) / longRisk : 0
shortTargetRoomR = shortRisk > 0 ? (close - shortStructureTarget) / shortRisk : 0
longTargetRewardOk = _effMinTargetRoomR <= 0 or longTargetRoomR >= _effMinTargetRoomR
shortTargetRewardOk = _effMinTargetRoomR <= 0 or shortTargetRoomR >= _effMinTargetRoomR
longAtrRoomOk = longTargetRoomATR >= minTargetRoomATR
shortAtrRoomOk = shortTargetRoomATR >= minTargetRoomATR
longRoomOk = longAtrRoomOk and longTargetRewardOk
shortRoomOk = shortAtrRoomOk and shortTargetRewardOk

// Per-family volume gate (volume requirement may apply only to one family)
longSetupVolumeOkRR  = not _volReqRR or bullVolSpike
longSetupVolumeOkPB  = not _volReqPB or bullVolSpike
shortSetupVolumeOkRR = not _volReqRR or bearVolSpike
shortSetupVolumeOkPB = not _volReqPB or bearVolSpike

pullbackEma = ta.ema(close, pullbackEmaLenA)
nearPullbackEma = atrVal > 0 and math.abs(close - pullbackEma) / atrVal <= pullbackProximityATR
longImpulseATR = atrVal > 0 and hasLowStructure and hasHighStructure ? math.abs(lastSwingHigh1 - lastSwingLow1) / atrVal : 0
shortImpulseATR = atrVal > 0 and hasLowStructure and hasHighStructure ? math.abs(lastSwingHigh1 - lastSwingLow1) / atrVal : 0
// Pullback trend uses the permissive structureLongOk/structureShortOk (HL/no-LL,
// LH/no-HH) instead of strict HH+HL. Strict definition starved the pullback path
// to ~0.5% of accepted trades on NatGas; the permissive form is consistent with
// the range-reversal location check and unblocks meaningful continuation entries.
longPullbackTrendOk = structureLongOk and (not requirePullbackHTF or htfBull)
shortPullbackTrendOk = structureShortOk and (not requirePullbackHTF or htfBear)
longPullbackLocationOk = nearPullbackEma or nearSwingSupport
shortPullbackLocationOk = nearPullbackEma or nearSwingResistance
// LONG-PB confirm: pullback continuation must also have a range-edge location
// OR a strong bullish-context combo (volume spike + structure). Unconfirmed
// LONG-PB has neither a level nor a conviction signal behind the entry.
longPBConfirmOk = not _effLongPBConfirm or longRangeLocationOk or (bullVolSpike and structureLongOk)
longPBRoomOverride = allowConfirmedPBRoomOverride and longPullbackTrendOk and longPullbackLocationOk and longImpulseATR >= minImpulseATR and (pocReclaimRecent or bullVolSpike)
shortPBRoomOverride = allowConfirmedPBRoomOverride and shortPullbackTrendOk and shortPullbackLocationOk and shortImpulseATR >= minImpulseATR and (pocRejectRecent or bearVolSpike)
longPBRoomOk = (longAtrRoomOk or longPBRoomOverride) and longTargetRewardOk
shortPBRoomOk = (shortAtrRoomOk or shortPBRoomOverride) and shortTargetRewardOk
longPullbackSetupOk = _effUsePullback and longPullbackTrendOk and longPullbackLocationOk and longImpulseATR >= minImpulseATR and longPBRoomOk and longSetupVolumeOkPB and longPBConfirmOk
shortPullbackSetupOk = _effUsePullback and shortPullbackTrendOk and shortPullbackLocationOk and shortImpulseATR >= minImpulseATR and shortPBRoomOk and shortSetupVolumeOkPB

shortRRConfirmOk = not _effShortRRConfirm or shortPullbackLocationOk or shortPullbackTrendOk
rrCompression = hlDetected and lhDetected
longRRCompressionOk = not rrBlockCompression or not (rrCompression and not hhDetected)
shortRRCompressionOk = not rrBlockCompression or not (rrCompression and not llDetected)
longRangeReversalSetupOk = longLocationOk and longRoomOk and longSetupVolumeOkRR and longRRCompressionOk
shortRangeReversalSetupOk = shortLocationOk and shortRoomOk and shortSetupVolumeOkRR and shortRRConfirmOk and shortRRCompressionOk
// Family-typed setup label — used in tooltips/log "type=" field; useSetupGate
// is now applied in the per-family pass logic (longRRSetupPass etc.) below.
longSetupType = longPullbackSetupOk and not longRangeReversalSetupOk ? 'pullback_continuation' : longRangeReversalSetupOk ? 'range_reversal' : 'none'
shortSetupType = shortPullbackSetupOk and not shortRangeReversalSetupOk ? 'pullback_continuation' : shortRangeReversalSetupOk ? 'range_reversal' : 'none'

longTakeProfit = longStructureTarget > close ? longStructureTarget : close + longRisk * minTargetRoomATR
shortTakeProfit = shortStructureTarget < close ? shortStructureTarget : close - shortRisk * minTargetRoomATR

// Early rejection setup — separate from confirmed pivot structure.
// It looks for an actual rejection at a useful area and requires local flow
// confirmation. This is intended for 1H where confirmed pivots often arrive
// after the move has already started.
earlyLongRejectArea = atrVal > 0 and (longRangeLocationOk or nearPullbackEma or (not na(lastSwingLow1) and low >= lastSwingLow1 and (low - lastSwingLow1) / atrVal <= earlyRejectProximityATR))
earlyShortRejectArea = atrVal > 0 and (shortRangeLocationOk or nearPullbackEma or (not na(lastSwingHigh1) and high <= lastSwingHigh1 and (lastSwingHigh1 - high) / atrVal <= earlyRejectProximityATR))
earlyLongRejectBar  = (_closePos >= earlyRejectWickMin and close > open) or wtOsc <= _effLowerBand
earlyShortRejectBar = (_closePos <= 1.0 - earlyRejectWickMin and close < open) or wtOsc >= _effUpperBand
_isOneHourChart = _chartSec == 3600
earlyLocalLongConfirm = useOneHourERQuality and _isOneHourChart ? bullVolSpike : (pocReclaimRecent or bullVolSpike)
earlyLocalShortConfirm = useOneHourERQuality and _isOneHourChart ? pocRejectRecent : (pocRejectRecent or bearVolSpike)
earlyLongRoomOk = longRoomOk or (_chartSec <= 3600 and longPBRoomOverride and longTargetRewardOk)
earlyShortRoomOk = shortRoomOk or (_chartSec <= 3600 and shortPBRoomOverride and shortTargetRewardOk)
earlyLongTFOk = earlyRejectTFMode == 'All TF' or (earlyRejectTFMode == 'Lower TF Only' and _chartSec <= 3600)
earlyShortTFOk = earlyRejectTFMode == 'All TF' or (earlyRejectTFMode == 'Lower TF Only' and _chartSec <= 3600)
earlyLongContextScore = (earlyLocalLongConfirm ? 1 : 0) + (earlyMomentumBull ? 1 : 0) + (earlyLongRoomOk ? 1 : 0) + longHtfScorePB + longAnchorScorePB + volRegimeBonus
earlyShortContextScore = (earlyLocalShortConfirm ? 1 : 0) + (earlyMomentumBear ? 1 : 0) + (earlyShortRoomOk ? 1 : 0) + shortHtfScorePB + shortAnchorScorePB + volRegimeBonus

// ═══════════════════════════════════════════════════════════════════════════
// SIGNAL — Per-family Trigger + Context model
// ═══════════════════════════════════════════════════════════════════════════
// Each setup family (range-reversal, pullback continuation) has its own WT
// trigger and its own context score. The combined long/short signal fires
// when either family fires.
_profileChartTfOk = (profileMode != 'Balanced' and profileMode != 'Aggressive') or (_chartSec >= 900 and _chartSec <= 14400)

// Triggers per family
_rrTfOk = _chartSec >= rrMinTFSec
rrTriggerLong  = crossUp   and _rrTfOk                                                      // OB/OS-based, momentum turn
rrTriggerShort = crossDown and _rrTfOk
pbTriggerLong  = ta.crossover(wtOscFast, wtSigFast)  and wtOscFast < _effPbZone and wtOscFast > -_effPbZone   // HMA fast cross, neutral zone
pbTriggerShort = ta.crossunder(wtOscFast, wtSigFast) and wtOscFast < _effPbZone and wtOscFast > -_effPbZone
erTriggerLong  = ta.crossover(wtOsc, wtSig)  and wtOsc <= 0
erTriggerShort = ta.crossunder(wtOsc, wtSig) and wtOsc >= 0

// Per-family context score (HTF / anchor components depend on family)
// volRegimeBonus is +0.5 when regime mode = "Score Only" AND atrNorm in band; else 0.
longContextScoreRR  = (structureLongOk  ? 1 : 0) + (pocReclaimRecent ? 1 : 0) + (bullVolSpike ? 1 : 0) + longHtfScoreRR  + longAnchorScoreRR  + volRegimeBonus
longContextScorePB  = (structureLongOk  ? 1 : 0) + (pocReclaimRecent ? 1 : 0) + (bullVolSpike ? 1 : 0) + longHtfScorePB  + longAnchorScorePB  + volRegimeBonus
shortContextScoreRR = (structureShortOk ? 1 : 0) + (pocRejectRecent  ? 1 : 0) + (bearVolSpike ? 1 : 0) + shortHtfScoreRR + shortAnchorScoreRR + volRegimeBonus
shortContextScorePB = (structureShortOk ? 1 : 0) + (pocRejectRecent  ? 1 : 0) + (bearVolSpike ? 1 : 0) + shortHtfScorePB + shortAnchorScorePB + volRegimeBonus

// Symmetric RR quality filter. This keeps parity: longs need bullish local
// confirmation, shorts need the mirrored bearish local confirmation. The score
// escape hatch preserves high-context reversal trades without hard-coding a side.
_rrLongLocalConfirm = pocReclaimRecent and bullVolSpike
_rrShortLocalConfirm = pocRejectRecent and bearVolSpike
rrLongQualityOk = rangeReversalQualityMode == 'Off' or (rangeReversalQualityMode == 'POC+Volume' ? _rrLongLocalConfirm : longContextScoreRR >= 3.5 or _rrLongLocalConfirm)
rrShortQualityOk = rangeReversalQualityMode == 'Off' or (rangeReversalQualityMode == 'POC+Volume' ? _rrShortLocalConfirm : shortContextScoreRR >= 3.5 or _rrShortLocalConfirm)

// Per-family setup pass — useSetupGate=false bypasses the family setup OK
longPBMidlineOk = not requireLowerTFPBMidline or _chartSec > 3600 or wtOsc > 0
shortPBMidlineOk = not requireLowerTFPBMidline or _chartSec > 3600 or wtOsc < 0
longRRSetupPass  = enableLongRangeReversal and longRRBiasOk and (not useSetupGate or longRangeReversalSetupOk)
longPBSetupPass  = _effUsePullback and (not useSetupGate or longPullbackSetupOk) and longPBMidlineOk and longPBBiasOk
shortRRSetupPass = enableShortRangeReversal and shortRRBiasOk and (not useSetupGate or shortRangeReversalSetupOk)
shortPBSetupPass = _effUsePullback and (not useSetupGate or shortPullbackSetupOk) and shortPBMidlineOk and shortPBBiasOk
// earlyLocalLongConfirm kept as hard gate on 1H (was protecting quality there).
// Removed from gate on ≤15m where it blocked 73% of signals with no benefit.
// Score component (+1 pt) still applies on all TFs.
_erLocalRequired = _isOneHourChart
longERSetupPass = useEarlyRejectionSetup and earlyRejectTFMode != 'Off' and earlyLongTFOk and longERBiasOk and earlyLongRejectArea and earlyLongRejectBar and (not _erLocalRequired or earlyLocalLongConfirm or wtOsc <= _effLowerBand) and earlyMomentumBull and earlyLongRoomOk
shortERSetupPass = useEarlyRejectionSetup and earlyRejectTFMode != 'Off' and earlyShortTFOk and shortERBiasOk and earlyShortRejectArea and earlyShortRejectBar and (not _erLocalRequired or earlyLocalShortConfirm or wtOsc >= _effUpperBand) and earlyMomentumBear and earlyShortRoomOk
erExpansion = hhDetected and llDetected and not hlDetected and not lhDetected
longERStructureOk  = not erBlockCounterExpansion or not (erExpansion and htfBear) or wtOsc <= _effLowerBand
shortERStructureOk = not erBlockCounterExpansion or not (erExpansion and htfBull) or wtOsc >= _effUpperBand
_oneHourERConfirmActive = useOneHourERFollowThrough and _isOneHourChart
_oneHourERDmiLongOk = not useOneHourERDmiConfirm or (_erPlusDI > _erMinusDI and _erAdx >= oneHourERMinAdx)
_oneHourERDmiShortOk = not useOneHourERDmiConfirm or (_erMinusDI > _erPlusDI and _erAdx >= oneHourERMinAdx)
_longERRawSignal = _profileChartTfOk and erTriggerLong and longERSetupPass and longERStructureOk and mtfLongOkPB and (earlyLongContextScore >= earlyRejectMinScore or wtOsc <= _effLowerBand) and volRegimeOk
_shortERRawSignal = _profileChartTfOk and erTriggerShort and shortERSetupPass and shortERStructureOk and mtfShortOkPB and (earlyShortContextScore >= earlyRejectMinScore or wtOsc >= _effUpperBand) and volRegimeOk

var int _pendingLongERBar = na
var int _pendingShortERBar = na
var float _pendingLongERHigh = na
var float _pendingShortERLow = na
if _oneHourERConfirmActive and _longERRawSignal
    _pendingLongERBar := bar_index
    _pendingLongERHigh := high
if _oneHourERConfirmActive and _shortERRawSignal
    _pendingShortERBar := bar_index
    _pendingShortERLow := low
_longERPendingFresh = _oneHourERConfirmActive and not na(_pendingLongERBar) and bar_index > _pendingLongERBar and bar_index - _pendingLongERBar <= oneHourERFollowThroughBars
_shortERPendingFresh = _oneHourERConfirmActive and not na(_pendingShortERBar) and bar_index > _pendingShortERBar and bar_index - _pendingShortERBar <= oneHourERFollowThroughBars
_longERFollowThroughOk = _longERPendingFresh and close > _pendingLongERHigh and _oneHourERDmiLongOk
_shortERFollowThroughOk = _shortERPendingFresh and close < _pendingShortERLow and _oneHourERDmiShortOk
if _oneHourERConfirmActive and not na(_pendingLongERBar) and bar_index - _pendingLongERBar > oneHourERFollowThroughBars
    _pendingLongERBar := na
    _pendingLongERHigh := na
if _oneHourERConfirmActive and not na(_pendingShortERBar) and bar_index - _pendingShortERBar > oneHourERFollowThroughBars
    _pendingShortERBar := na
    _pendingShortERLow := na

_structurePlaybookActive = structurePlaybookMode == 'All TF' or (structurePlaybookMode == '1H Only' and _chartSec == 3600)
// Pivot playbook from test59-test64:
// 1H did not fail because one universal filter was too loose. It failed when a
// setup family was allowed in a structure phase where that family had stable
// negative expectancy. Keep this as an entry gate, not as trade management.
_oneHourPlaybook = _structurePlaybookActive and _chartSec == 3600
_playbookStructure = hhDetected and hlDetected ? 'trend_up'
     : llDetected and lhDetected ? 'trend_down'
     : hhDetected and llDetected ? 'expansion'
     : hlDetected and lhDetected ? 'compression'
     : hhDetected ? 'high_break'
     : llDetected ? 'low_break'
     : hlDetected ? 'higher_low'
     : lhDetected ? 'lower_high'
     : 'unclassified'
_longPlaybook = not _oneHourPlaybook ? 'composite'
     : llDetected and lhDetected ? 'mature_reversal_long'
     : hhDetected and hlDetected ? 'trend_up_blocked_long'
     : 'selective_long'
_shortPlaybook = not _oneHourPlaybook ? 'composite'
     : hlDetected and lhDetected and not llDetected ? 'pullback_short'
     : hhDetected and llDetected ? 'expansion_rejection_short'
     : llDetected and lhDetected ? 'trend_down_reversal_short'
     : 'selective_short'
_longPBRouteOk = not _oneHourPlaybook or _longPlaybook == 'selective_long'
_shortPBRouteOk = not _oneHourPlaybook or _shortPlaybook == 'pullback_short'
_longRRRouteOk = not _oneHourPlaybook or _longPlaybook == 'mature_reversal_long' or _longPlaybook == 'selective_long'
_shortRRRouteOk = not _oneHourPlaybook or _shortPlaybook == 'trend_down_reversal_short'
_longERRouteOk = true
_shortERRouteOk = not _oneHourPlaybook or _shortPlaybook == 'expansion_rejection_short'
_playbookStr = ' | PLAYBOOK: mode=' + structurePlaybookMode + ' active=' + str.tostring(_structurePlaybookActive) + ' structure=' + _playbookStructure + ' long=' + _longPlaybook + ' short=' + _shortPlaybook
longPBPlaybookOk = _longPBRouteOk
shortPBPlaybookOk = _shortPBRouteOk
longRRPlaybookOk = _longRRRouteOk
shortRRPlaybookOk = _shortRRRouteOk
longERPlaybookOk = _longERRouteOk
shortERPlaybookOk = _shortERRouteOk

// Per-family signals (volRegimeOk applies universally — outside the regime, no entries)
longRRSignal  = _profileChartTfOk and rrTriggerLong  and longRRSetupPass  and longRRPlaybookOk  and rrLongQualityOk  and mtfLongOkRR  and longContextScoreRR  >= _effMinScore and volRegimeOk
longPBSignal  = _profileChartTfOk and pbTriggerLong  and longPBSetupPass  and longPBPlaybookOk  and mtfLongOkPB  and longContextScorePB  >= _effMinScore and volRegimeOk
shortRRSignal = _profileChartTfOk and rrTriggerShort and shortRRSetupPass and shortRRPlaybookOk and rrShortQualityOk and mtfShortOkRR and shortContextScoreRR >= _effMinScore and volRegimeOk
shortPBSignal = _profileChartTfOk and pbTriggerShort and shortPBSetupPass and shortPBPlaybookOk and mtfShortOkPB and shortContextScorePB >= _effMinScore and volRegimeOk
longERSignal = longERPlaybookOk and (_oneHourERConfirmActive ? _longERFollowThroughOk : _longERRawSignal)
shortERSignal = shortERPlaybookOk and (_oneHourERConfirmActive ? _shortERFollowThroughOk : _shortERRawSignal)
if longERSignal
    _pendingLongERBar := na
    _pendingLongERHigh := na
if shortERSignal
    _pendingShortERBar := na
    _pendingShortERLow := na

longSignal  = longRRSignal  or longPBSignal or longERSignal
shortSignal = shortRRSignal or shortPBSignal or shortERSignal

// Display aggregates (max of the two family scores per direction)
earlyLongDisplayScore = earlyLongTFOk ? earlyLongContextScore : 0.0
earlyShortDisplayScore = earlyShortTFOk ? earlyShortContextScore : 0.0
longContextScore  = math.max(math.max(longContextScoreRR,  longContextScorePB), earlyLongDisplayScore)
shortContextScore = math.max(math.max(shortContextScoreRR, shortContextScorePB), earlyShortDisplayScore)
longEntrySignal = longSignal and (not shortSignal or longContextScore > shortContextScore)
shortEntrySignal = shortSignal and (not longSignal or shortContextScore > longContextScore)
_useLowerTfTacticalTarget = _chartSec <= 3600 and lowerTfTacticalTargetR > 0
_useOneHourPullbackTarget = _chartSec == 3600 and oneHourPullbackTargetR > 0
longTacticalTakeProfit = close + longRisk * lowerTfTacticalTargetR
shortTacticalTakeProfit = close - shortRisk * lowerTfTacticalTargetR
longPullbackTakeProfit = close + longRisk * oneHourPullbackTargetR
shortPullbackTakeProfit = close - shortRisk * oneHourPullbackTargetR
longEntryTakeProfit = _useOneHourPullbackTarget and longPBSignal ? math.min(longTakeProfit, longPullbackTakeProfit) : _useLowerTfTacticalTarget and (longRRSignal or longERSignal) ? math.min(longTakeProfit, longTacticalTakeProfit) : longTakeProfit
shortEntryTakeProfit = _useOneHourPullbackTarget and shortPBSignal ? math.max(shortTakeProfit, shortPullbackTakeProfit) : _useLowerTfTacticalTarget and (shortRRSignal or shortERSignal) ? math.max(shortTakeProfit, shortTacticalTakeProfit) : shortTakeProfit
maxContextScoreRR = 3 + (_mtfHardRR ? 0 : htfWeight) + (_anchorActive and anchorScoreMode != 'Ignore' and _anchorReqRR ? anchorWeight : 0)
maxContextScorePB = 3 + (_mtfHardPB ? 0 : htfWeight) + (_anchorActive and anchorScoreMode != 'Ignore' and _anchorReqPB ? anchorWeight : 0)
maxContextScoreER = 3 + (_mtfHardPB ? 0 : htfWeight) + (_anchorActive and anchorScoreMode != 'Ignore' and _anchorReqPB ? anchorWeight : 0)
maxContextScore = math.max(math.max(maxContextScoreRR, maxContextScorePB), maxContextScoreER)

// Family of the active signal (for log/tooltip display)
longSignalFamily  = longRRSignal  ? 'range_reversal' : longPBSignal  ? 'pullback_continuation' : longERSignal ? 'early_rejection' : 'none'
shortSignalFamily = shortRRSignal ? 'range_reversal' : shortPBSignal ? 'pullback_continuation' : shortERSignal ? 'early_rejection' : 'none'

// ATR-based stops for strategy generation
// longStop/shortStop and entry take-profit variants are computed in the
// setup-first block above.

// ═══════════════════════════════════════════════════════════════════════════
// EXIT LOGIC — virtual signal diagnostics + 4 exit families
// ═══════════════════════════════════════════════════════════════════════════
// The indicator tracks an approximate position state for signal diagnostics.
// It can diverge from real strategy trades that close via SL/TP intrabar.
// Treat WT3 VIRTUAL EXIT logs and the Virtual Q table as diagnostics only.
// Real backtest trade results are logged separately from strategy.closedtrades.

// Auto-scale time-based exit windows from 4H baseline (consistent with other A-vars)
maxBarsInTradeA   = f_scaleBars(maxBarsInTrade, 5, 400)
followThroughBarsA = f_scaleBars(followThroughBars, 2, 40)

// --- Virtual position state ---
var int    _vPos        = 0     // 1 = long, -1 = short, 0 = flat
var int    _vEntryBar   = na
var float  _vEntryPx    = na
var float  _vRisk       = na    // entry-to-stop distance, used for R-multiple
var float  _vTargetPx   = na    // entry hypothesis target, used for pivot/target diagnostics
var string _vFamily     = na    // 'rr' or 'pb'
var float  _vHigh       = na    // running high since entry (for follow-through / MFE)
var float  _vLow        = na    // running low since entry / MFE
var float  _vEntryScore = na    // context-score at entry (quality reference)

// --- Virtual signal-quality journal (arrays grow with backtest) ---
var array<string> _trDir         = array.new<string>()
var array<string> _trFamily      = array.new<string>()
var array<string> _trReason      = array.new<string>()
var array<float>  _trEntryScore  = array.new<float>()
var array<float>  _trExitR       = array.new<float>()    // R-multiple at exit
var array<float>  _trMFE         = array.new<float>()    // peak R during trade
var array<int>    _trBarsHeld    = array.new<int>()

// --- Real strategy closed-trade journal (source of truth) ---
var int _loggedClosedTrades = 0
var array<string> _realDir = array.new<string>()
var array<string> _realFamily = array.new<string>()
var array<string> _realExit = array.new<string>()
var array<float> _realProfit = array.new<float>()
var array<float> _realExitR = array.new<float>()
var array<int> _realBars = array.new<int>()

// Update running high/low while in position
if _vPos == 1
    _vHigh := math.max(nz(_vHigh, high), high)
if _vPos == -1
    _vLow := math.min(nz(_vLow, low), low)

// --- Exit-condition components (only meaningful while in matching position) ---

// 1) Opposite signal — only with conviction (max active opposite-family score >= gate)
_oppShortScore = math.max(math.max(shortContextScoreRR, shortContextScorePB), earlyShortContextScore)
_oppLongScore  = math.max(math.max(longContextScoreRR,  longContextScorePB), earlyLongContextScore)
longExitOpposite  = _vPos == 1  and (shortRRSignal or shortPBSignal or shortERSignal) and _oppShortScore >= oppositeExitMinScore
shortExitOpposite = _vPos == -1 and (longRRSignal  or longPBSignal  or longERSignal)  and _oppLongScore  >= oppositeExitMinScore

// Track running R-multiple early — used by trade-management exits.
_barsInTrade = _vPos != 0 and not na(_vEntryBar) ? bar_index - _vEntryBar : 0
_currentRLong  = _vPos == 1  and not na(_vRisk) and _vRisk > 0 ? (close - _vEntryPx) / _vRisk : 0.0
_currentRShort = _vPos == -1 and not na(_vRisk) and _vRisk > 0 ? (_vEntryPx - close) / _vRisk : 0.0
_mfeRLong  = _vPos == 1  and not na(_vRisk) and _vRisk > 0 and not na(_vHigh) ? (_vHigh - _vEntryPx) / _vRisk : 0.0
_mfeRShort = _vPos == -1 and not na(_vRisk) and _vRisk > 0 and not na(_vLow)  ? (_vEntryPx - _vLow) / _vRisk : 0.0
_activeMfeProtectR = _vFamily == 'pb' ? pbMfeProtectR : _vFamily == 'er' ? erMfeProtectR : mfeProtectR
_givebackKeepPct = 1.0 - maxGivebackPct / 100.0
_givebackStopLongR  = math.max(givebackFloorR, _mfeRLong  * _givebackKeepPct)
_givebackStopShortR = math.max(givebackFloorR, _mfeRShort * _givebackKeepPct)
_confirmedGivebackTfOk = confirmedGivebackTFMode == 'All TF' or (confirmedGivebackTFMode == '4H Only' and _chartSec == 14400) or (confirmedGivebackTFMode == '15m + 4H' and (_chartSec == 900 or _chartSec == 14400))
longExitGiveback  = _vPos == 1  and _mfeRLong  >= _activeMfeProtectR and _currentRLong  <= _givebackStopLongR
shortExitGiveback = _vPos == -1 and _mfeRShort >= _activeMfeProtectR and _currentRShort <= _givebackStopShortR
_longVirtualTargetTouched = _vPos == 1 and not na(_vTargetPx) and high >= _vTargetPx
_shortVirtualTargetTouched = _vPos == -1 and not na(_vTargetPx) and low <= _vTargetPx
_longVirtualConfirmedPivot = confirmedGivebackUsePivot and _vPos == 1 and _plFound and (bar_index - structPivLen) >= nz(_vEntryBar, bar_index) and low[structPivLen] <= _vEntryPx + _givebackStopLongR * _vRisk
_shortVirtualConfirmedPivot = confirmedGivebackUsePivot and _vPos == -1 and _phFound and (bar_index - structPivLen) >= nz(_vEntryBar, bar_index) and high[structPivLen] >= _vEntryPx - _givebackStopShortR * _vRisk
_longVirtualStructureBreak = confirmedGivebackUseStructure and _vPos == 1 and (structureShortOk or (lhDetected and llDetected))
_shortVirtualStructureBreak = confirmedGivebackUseStructure and _vPos == -1 and (structureLongOk or (hhDetected and hlDetected))
longExitConfirmedGiveback = useConfirmedGivebackExit and _confirmedGivebackTfOk and _vPos == 1 and _mfeRLong >= _activeMfeProtectR and _currentRLong <= _givebackStopLongR and not _longVirtualTargetTouched and (_longVirtualConfirmedPivot or _longVirtualStructureBreak)
shortExitConfirmedGiveback = useConfirmedGivebackExit and _confirmedGivebackTfOk and _vPos == -1 and _mfeRShort >= _activeMfeProtectR and _currentRShort <= _givebackStopShortR and not _shortVirtualTargetTouched and (_shortVirtualConfirmedPivot or _shortVirtualStructureBreak)

// 2) Family-WT cross
//    WT is now momentum-management, not a profit gate. The old currentR>=1
//    guard failed after profit pullbacks: the trade had MFE, but no longer had
//    current profit when the exit finally appeared.
_wtCrossDown = ta.crossunder(wtOsc, wtSig)
_wtCrossUp   = ta.crossover(wtOsc, wtSig)
longExitFamilyWT  = _vPos == 1  and _wtCrossDown and ((_vFamily == 'rr' and (_mfeRLong >= mfeProtectR or wtOsc > 0)) or ((_vFamily == 'pb' or _vFamily == 'er') and (_mfeRLong >= _activeMfeProtectR or wtOsc < 0)))
shortExitFamilyWT = _vPos == -1 and _wtCrossUp   and ((_vFamily == 'rr' and (_mfeRShort >= mfeProtectR or wtOsc < 0)) or ((_vFamily == 'pb' or _vFamily == 'er') and (_mfeRShort >= _activeMfeProtectR or wtOsc > 0)))

// 3) Time-failure — after enough bars, require either current progress or
// already-protected MFE. This catches stagnant winners that gave back too much.
longExitTimeFail  = _vPos == 1  and _barsInTrade >= maxBarsInTradeA and _currentRLong  < minProfitR
shortExitTimeFail = _vPos == -1 and _barsInTrade >= maxBarsInTradeA and _currentRShort < minProfitR

// 4) Lower-TF early failure — cut RR/ER trades that never start moving, but
// leave pullbacks and any trade with useful MFE alone so runners are preserved.
_lowerTfEarlyFailureFamily = _vFamily == 'rr' or _vFamily == 'er'
longExitEarlyFail = useLowerTfEarlyFailure and _chartSec <= 3600 and _vPos == 1 and _lowerTfEarlyFailureFamily and _barsInTrade >= lowerTfEarlyFailureBars and _mfeRLong < lowerTfEarlyFailureMfeR and _currentRLong <= 0
shortExitEarlyFail = useLowerTfEarlyFailure and _chartSec <= 3600 and _vPos == -1 and _lowerTfEarlyFailureFamily and _barsInTrade >= lowerTfEarlyFailureBars and _mfeRShort < lowerTfEarlyFailureMfeR and _currentRShort <= 0
longExitOneHourPBFail = useOneHourPullbackFailure and _chartSec == 3600 and _vPos == 1 and _vFamily == 'pb' and _barsInTrade >= oneHourPullbackFailureBars and _mfeRLong < oneHourPullbackFailureMfeR and _currentRLong <= 0
shortExitOneHourPBFail = useOneHourPullbackFailure and _chartSec == 3600 and _vPos == -1 and _vFamily == 'pb' and _barsInTrade >= oneHourPullbackFailureBars and _mfeRShort < oneHourPullbackFailureMfeR and _currentRShort <= 0

// 5) Follow-through — at exactly N bars after entry, require directional progress
_atTriggerBar = _vPos != 0 and _barsInTrade == followThroughBarsA
longExitFollowThr  = _vPos == 1  and _atTriggerBar and not na(_vHigh) and _vHigh < _vEntryPx + followThroughATR * atrVal
shortExitFollowThr = _vPos == -1 and _atTriggerBar and not na(_vLow)  and _vLow  > _vEntryPx - followThroughATR * atrVal

// Aggregated exit signals (gated by toggles)
longExitSignal  = (useExitOpposite  and longExitOpposite)  or (useExitGiveback and longExitGiveback)  or longExitConfirmedGiveback or (useExitFamilyWT and longExitFamilyWT)  or longExitEarlyFail or longExitOneHourPBFail or (useExitTimeFail and longExitTimeFail)  or (useExitFollowThr and longExitFollowThr)
shortExitSignal = (useExitOpposite  and shortExitOpposite) or (useExitGiveback and shortExitGiveback) or shortExitConfirmedGiveback or (useExitFamilyWT and shortExitFamilyWT) or shortExitEarlyFail or shortExitOneHourPBFail or (useExitTimeFail and shortExitTimeFail) or (useExitFollowThr and shortExitFollowThr)

// Determine reason text (priority order: opposite > giveback > WT > non-starter > time > follow-through)
_longExitReason  = longExitOpposite  and useExitOpposite  ? 'Opp'
                 : longExitGiveback  and useExitGiveback  ? 'GB'
                 : longExitFamilyWT  and useExitFamilyWT  ? 'WT'
                 : longExitEarlyFail                    ? 'EF'
                 : longExitOneHourPBFail                ? 'PBF'
                 : longExitTimeFail  and useExitTimeFail  ? 'Time'
                 : longExitFollowThr and useExitFollowThr ? 'FTh' : ''
_shortExitReason = shortExitOpposite  and useExitOpposite  ? 'Opp'
                 : shortExitGiveback and useExitGiveback  ? 'GB'
                 : shortExitFamilyWT  and useExitFamilyWT  ? 'WT'
                 : shortExitEarlyFail                   ? 'EF'
                 : shortExitOneHourPBFail               ? 'PBF'
                 : shortExitTimeFail  and useExitTimeFail  ? 'Time'
                 : shortExitFollowThr and useExitFollowThr ? 'FTh' : ''
_pivotTimingStr = ' highConfirmBars=' + str.tostring(barsSinceHighPivotConfirm) + ' lowConfirmBars=' + str.tostring(barsSinceLowPivotConfirm) + ' highActualBars=' + str.tostring(barsSinceHighPivotActual) + ' lowActualBars=' + str.tostring(barsSinceLowPivotActual) + ' phNow=' + str.tostring(_phFound) + ' plNow=' + str.tostring(_plFound) + ' pivLen=' + str.tostring(structPivLen)
_structureSnapshot = ' | STRUCT: hh=' + str.tostring(hhDetected) + ' hl=' + str.tostring(hlDetected) + ' lh=' + str.tostring(lhDetected) + ' ll=' + str.tostring(llDetected) + ' longStruct=' + str.tostring(structureLongOk) + ' shortStruct=' + str.tostring(structureShortOk) + ' wtBullDiv=' + str.tostring(wtBullDiv) + ' wtBearDiv=' + str.tostring(wtBearDiv) + _pivotTimingStr

// --- State transitions ---
// Entry (only when flat or opposite is exiting). Opposite-signal exits handle the flip implicitly.
_enteringLong  = longEntrySignal  and (_vPos == 0 or (_vPos == -1 and shortExitSignal))
_enteringShort = shortEntrySignal and (_vPos == 0 or (_vPos == 1 and longExitSignal))

// Exit applies first (close current position + record trade), then entry can flip
if _vPos == 1 and longExitSignal
    _exitR  = (close - _vEntryPx) / _vRisk
    _mfeR   = not na(_vHigh) ? (_vHigh - _vEntryPx) / _vRisk : _exitR
    _captureL = _mfeR > 0 ? _exitR / _mfeR * 100 : 0.0
    _targetR = not na(_vTargetPx) and _vRisk > 0 ? (_vTargetPx - _vEntryPx) / _vRisk : na
    _targetHit = not na(_targetR) and _mfeR >= _targetR
    _targetCapture = not na(_targetR) and _targetR > 0 ? _exitR / _targetR * 100 : na
    array.push(_trDir,        'LONG')
    array.push(_trFamily,     _vFamily)
    array.push(_trReason,     _longExitReason)
    array.push(_trEntryScore, _vEntryScore)
    array.push(_trExitR,      _exitR)
    array.push(_trMFE,        _mfeR)
    array.push(_trBarsHeld,   _barsInTrade)
    if debugMode
        log.info(str.format('WT3 VIRTUAL LONG EXIT | family={0} | reason={1} | bar={2} | entryScore={3} | R={4} | MFE={5} | capturePct={6} | barsHeld={7} | win={8} | profile={9} | TARGET: px={10} targetR={11} hit={12} targetCapture={13}',
             str.tostring(_vFamily), _longExitReason, str.tostring(bar_index), str.tostring(_vEntryScore, '#.#'),
             str.tostring(_exitR, '#.##'), str.tostring(_mfeR, '#.##'), str.tostring(_captureL, '#.#'),
             str.tostring(_barsInTrade), str.tostring(_exitR > 0), profileMode,
             str.tostring(_vTargetPx, format.mintick), str.tostring(_targetR, '#.##'), str.tostring(_targetHit), str.tostring(_targetCapture, '#.#')) + _structureSnapshot)
    _vPos := 0
    _vEntryBar := na
    _vEntryPx := na
    _vRisk := na
    _vTargetPx := na
    _vFamily := na
    _vHigh := na
    _vEntryScore := na
if _vPos == -1 and shortExitSignal
    _exitR  = (_vEntryPx - close) / _vRisk
    _mfeR   = not na(_vLow) ? (_vEntryPx - _vLow) / _vRisk : _exitR
    _captureS = _mfeR > 0 ? _exitR / _mfeR * 100 : 0.0
    _targetR = not na(_vTargetPx) and _vRisk > 0 ? (_vEntryPx - _vTargetPx) / _vRisk : na
    _targetHit = not na(_targetR) and _mfeR >= _targetR
    _targetCapture = not na(_targetR) and _targetR > 0 ? _exitR / _targetR * 100 : na
    array.push(_trDir,        'SHORT')
    array.push(_trFamily,     _vFamily)
    array.push(_trReason,     _shortExitReason)
    array.push(_trEntryScore, _vEntryScore)
    array.push(_trExitR,      _exitR)
    array.push(_trMFE,        _mfeR)
    array.push(_trBarsHeld,   _barsInTrade)
    if debugMode
        log.info(str.format('WT3 VIRTUAL SHORT EXIT | family={0} | reason={1} | bar={2} | entryScore={3} | R={4} | MFE={5} | capturePct={6} | barsHeld={7} | win={8} | profile={9} | TARGET: px={10} targetR={11} hit={12} targetCapture={13}',
             str.tostring(_vFamily), _shortExitReason, str.tostring(bar_index), str.tostring(_vEntryScore, '#.#'),
             str.tostring(_exitR, '#.##'), str.tostring(_mfeR, '#.##'), str.tostring(_captureS, '#.#'),
             str.tostring(_barsInTrade), str.tostring(_exitR > 0), profileMode,
             str.tostring(_vTargetPx, format.mintick), str.tostring(_targetR, '#.##'), str.tostring(_targetHit), str.tostring(_targetCapture, '#.#')) + _structureSnapshot)
    _vPos := 0
    _vEntryBar := na
    _vEntryPx := na
    _vRisk := na
    _vTargetPx := na
    _vFamily := na
    _vLow := na
    _vEntryScore := na

if _enteringLong
    if _vPos == -1
        _vPos := 0
    _vPos := 1
    _vEntryBar := bar_index
    _vEntryPx := close
    _vRisk := math.max(close - longStop, syminfo.mintick)
    _vTargetPx := longEntryTakeProfit
    _vFamily := longRRSignal ? 'rr' : longPBSignal ? 'pb' : 'er'
    _vHigh := high
    _vLow := na
    _vEntryScore := longRRSignal ? longContextScoreRR : longPBSignal ? longContextScorePB : earlyLongContextScore
if _enteringShort
    if _vPos == 1
        _vPos := 0
    _vPos := -1
    _vEntryBar := bar_index
    _vEntryPx := close
    _vRisk := math.max(shortStop - close, syminfo.mintick)
    _vTargetPx := shortEntryTakeProfit
    _vFamily := shortRRSignal ? 'rr' : shortPBSignal ? 'pb' : 'er'
    _vLow := low
    _vHigh := na
    _vEntryScore := shortRRSignal ? shortContextScoreRR : shortPBSignal ? shortContextScorePB : earlyShortContextScore

// ═══════════════════════════════════════════════════════════════════════════
// PLOTTING (Pane)
// ═══════════════════════════════════════════════════════════════════════════

colorBull = #16a34a
colorBear = #dc2626
colorOsc  = #0ea5e9
colorSig  = #f97316

// Bands plotted via plot() (not hline) so they reflect the active profile's
// effective OB/OS levels rather than the raw inputs.
plot(_effUpperBand, 'OB', color.new(colorBear, 50), linewidth=1)
plot(0,             'Mid', color.new(color.gray, 40), linewidth=1)
plot(_effLowerBand, 'OS', color.new(colorBull, 50), linewidth=1)

p_osc = plot(wtOsc, 'WT', color.from_gradient(wtOsc, _effLowerBand, _effUpperBand, colorBull, colorBear), linewidth=2)
p_sig = plot(wtSig, 'Sig', color.new(colorSig, 20), linewidth=1)
p_mid = plot(0, display=display.none)
plot(htfWtOsc, 'HTF WT', color.new(color.purple, 40), linewidth=1)
plot(anchorWtOsc, 'Anchor HTF WT', color.new(color.blue, 55), linewidth=1)
fill(p_osc, p_mid, _effUpperBand, 0,             color.new(colorBear, 90), color.new(colorBear, 100))
fill(p_osc, p_mid, 0,             _effLowerBand, color.new(colorBull, 100), color.new(colorBull, 90))
plotshape(showRawStrongWTCross and rawStrongBullCross, title='Raw Strong WT Bull Cross', style=shape.triangleup,
     location=location.bottom, color=color.new(colorBull, 0), size=size.tiny, text='WT+')
plotshape(showRawStrongWTCross and rawStrongBearCross, title='Raw Strong WT Bear Cross', style=shape.triangledown,
     location=location.top, color=color.new(colorBear, 0), size=size.tiny, text='WT-')

// ═══════════════════════════════════════════════════════════════════════════
// PRICE-CHART OVERLAYS
// ═══════════════════════════════════════════════════════════════════════════

// POC line overlay
plot(useVolPOC ? pocPrice      : na, 'POC (local)', color.new(color.yellow, 30), linewidth=2, force_overlay=true)
plot(useVolPOC ? pocMacroPrice : na, 'POC (macro)', color.new(color.orange, 50), linewidth=1, style=plot.style_circles, force_overlay=true)

// Structure markers
if showStructure and _phFound and lhDetected
    label.new(bar_index - structPivLen, high[structPivLen], 'LH',
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_down, color=color.new(colorBear, 30), textcolor=color.white,
         size=size.tiny, force_overlay=true)
if showStructure and _phFound and hhDetected
    label.new(bar_index - structPivLen, high[structPivLen], 'HH',
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_down, color=color.new(colorBull, 30), textcolor=color.white,
         size=size.tiny, force_overlay=true)
if showStructure and _plFound and hlDetected
    label.new(bar_index - structPivLen, low[structPivLen], 'HL',
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_up, color=color.new(colorBull, 30), textcolor=color.white,
         size=size.tiny, force_overlay=true)
if showStructure and _plFound and llDetected
    label.new(bar_index - structPivLen, low[structPivLen], 'LL',
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_up, color=color.new(colorBear, 30), textcolor=color.white,
         size=size.tiny, force_overlay=true)

// Composite signal markers — show family, active context score, family-specific gates
_longActiveScore = longRRSignal ? longContextScoreRR : longPBSignal ? longContextScorePB : earlyLongContextScore
_longActiveMax   = longRRSignal ? maxContextScoreRR  : longPBSignal ? maxContextScorePB : maxContextScoreER
_longActiveVolOk = longRRSignal ? longSetupVolumeOkRR : longPBSignal ? longSetupVolumeOkPB : earlyLocalLongConfirm
_longActiveMtf   = longRRSignal ? mtfLongOkRR : mtfLongOkPB
_longActiveHtfSc = longRRSignal ? longHtfScoreRR : longHtfScorePB
_longActiveAncSc = longRRSignal ? longAnchorScoreRR : longAnchorScorePB
_longActiveTrig  = longRRSignal ? 'WT Cross Up after OS visit (≤' + str.tostring(zoneExitWindowA) + ' bars)' : longPBSignal ? 'WT Cross Up in neutral zone (±' + str.tostring(_effPbZone, '#.#') + ')' : 'Early rejection: WT Cross Up + MFI/Stoch RSI confirmation'
_shortActiveScore = shortRRSignal ? shortContextScoreRR : shortPBSignal ? shortContextScorePB : earlyShortContextScore
_shortActiveMax   = shortRRSignal ? maxContextScoreRR  : shortPBSignal ? maxContextScorePB : maxContextScoreER
_shortActiveVolOk = shortRRSignal ? shortSetupVolumeOkRR : shortPBSignal ? shortSetupVolumeOkPB : earlyLocalShortConfirm
_shortActiveMtf   = shortRRSignal ? mtfShortOkRR : mtfShortOkPB
_shortActiveHtfSc = shortRRSignal ? shortHtfScoreRR : shortHtfScorePB
_shortActiveAncSc = shortRRSignal ? shortAnchorScoreRR : shortAnchorScorePB
_shortActiveTrig  = shortRRSignal ? 'WT Cross Down after OB visit (≤' + str.tostring(zoneExitWindowA) + ' bars)' : shortPBSignal ? 'WT Cross Down in neutral zone (±' + str.tostring(_effPbZone, '#.#') + ')' : 'Early rejection: WT Cross Down + MFI/Stoch RSI confirmation'

if showSignals and longEntrySignal
    label.new(bar_index, low, '▲ ' + str.tostring(_longActiveScore, '#.#'),
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_up, color=colorBull, textcolor=color.white,
         size=size.normal, force_overlay=true,
         tooltip='WT3 Composite Long [' + longSignalFamily + ']  ·  ' + str.tostring(_longActiveScore, '#.#') + '/' + str.tostring(_longActiveMax, '#.#')
              + '  ·  profile=' + profileMode
              + '\n────────────'
              + '\nTRIGGER:  ' + _longActiveTrig
              + '\nSETUP:'
              + '\n  ' + (longLocationOk ? '✓' : '✗') + '  Location lower range/swing (pos=' + str.tostring(setupRangePos, '#.##') + ')'
              + '\n  ' + (longRoomOk ? '✓' : '✗') + '  Room ' + str.tostring(longTargetRoomATR, '#.#') + ' ATR / ' + str.tostring(longTargetRoomR, '#.##') + 'R'
              + '\n  ' + (_longActiveVolOk ? '✓' : '✗') + '  Volume Gate'
              + '\n  ' + (_longActiveMtf ? '✓' : '✗') + '  MTF Gate (apply=' + _effMtfApplyTo + ')'
              + '\n  Target: ' + str.tostring(longEntryTakeProfit, format.mintick) + '  Stop: ' + str.tostring(longStop, format.mintick)
              + '\nCONTEXT:'
              + '\n  ' + (structureLongOk  ? '✓' : '✗') + '  Structure'
              + '\n  ' + (pocReclaimRecent ? '✓' : '✗') + '  POC Reclaim'
              + '\n  ' + (bullVolSpike     ? '✓' : '✗') + '  Bull Volume Spike'
              + '\n  ' + (_longActiveHtfSc > 0 ? '✓' : '✗') + '  HTF score ×' + str.tostring(_longActiveHtfSc, '#.#')
              + '\n  ' + (_longActiveAncSc > 0 ? '✓' : '✗') + '  Anchor score ×' + str.tostring(_longActiveAncSc, '#.#')
              + '\n────────────'
              + '\nWT: ' + str.tostring(wtOsc, '#.##') + '  HTF: ' + str.tostring(htfWtOsc, '#.##') + '  Anchor: ' + str.tostring(anchorWtOsc, '#.##'))

if showSignals and shortEntrySignal
    label.new(bar_index, high, '▼ ' + str.tostring(_shortActiveScore, '#.#'),
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_down, color=colorBear, textcolor=color.white,
         size=size.normal, force_overlay=true,
         tooltip='WT3 Composite Short [' + shortSignalFamily + ']  ·  ' + str.tostring(_shortActiveScore, '#.#') + '/' + str.tostring(_shortActiveMax, '#.#')
              + '  ·  profile=' + profileMode
              + '\n────────────'
              + '\nTRIGGER:  ' + _shortActiveTrig
              + '\nSETUP:'
              + '\n  ' + (shortLocationOk ? '✓' : '✗') + '  Location upper range/swing (pos=' + str.tostring(setupRangePos, '#.##') + ')'
              + '\n  ' + (shortRoomOk ? '✓' : '✗') + '  Room ' + str.tostring(shortTargetRoomATR, '#.#') + ' ATR / ' + str.tostring(shortTargetRoomR, '#.##') + 'R'
              + '\n  ' + (_shortActiveVolOk ? '✓' : '✗') + '  Volume Gate'
              + '\n  ' + (_shortActiveMtf ? '✓' : '✗') + '  MTF Gate (apply=' + _effMtfApplyTo + ')'
              + '\n  Target: ' + str.tostring(shortEntryTakeProfit, format.mintick) + '  Stop: ' + str.tostring(shortStop, format.mintick)
              + '\nCONTEXT:'
              + '\n  ' + (structureShortOk ? '✓' : '✗') + '  Structure'
              + '\n  ' + (pocRejectRecent  ? '✓' : '✗') + '  POC Reject'
              + '\n  ' + (bearVolSpike     ? '✓' : '✗') + '  Bear Volume Spike'
              + '\n  ' + (_shortActiveHtfSc > 0 ? '✓' : '✗') + '  HTF score ×' + str.tostring(_shortActiveHtfSc, '#.#')
              + '\n  ' + (_shortActiveAncSc > 0 ? '✓' : '✗') + '  Anchor score ×' + str.tostring(_shortActiveAncSc, '#.#')
              + '\n────────────'
              + '\nWT: ' + str.tostring(wtOsc, '#.##') + '  HTF: ' + str.tostring(htfWtOsc, '#.##') + '  Anchor: ' + str.tostring(anchorWtOsc, '#.##'))

// ─── Exit markers ────────────────────────────────────────────────────────────
// Color-coded by reason. Long exits below high (close-long arrow), short exits
// above low (close-short arrow).
_exitColorOpp  = #6b7280   // gray
_exitColorGB   = #22c55e   // green
_exitColorWT   = #f59e0b   // amber
_exitColorTime = #3b82f6   // blue
_exitColorFTh  = #eab308   // yellow

f_exitColor(reason) =>
    switch reason
        'Opp'  => _exitColorOpp
        'GB'   => _exitColorGB
        'WT'   => _exitColorWT
        'Time' => _exitColorTime
        'FTh'  => _exitColorFTh
        =>        color.gray

// Helpers for exit-label tooltip — these read the LATEST recorded trade
_lastIdx       = array.size(_trExitR) - 1
_lastExitR     = _lastIdx >= 0 ? array.get(_trExitR, _lastIdx)      : 0.0
_lastMFE       = _lastIdx >= 0 ? array.get(_trMFE, _lastIdx)        : 0.0
_lastEntrySc   = _lastIdx >= 0 ? array.get(_trEntryScore, _lastIdx) : 0.0
_lastBarsHeld  = _lastIdx >= 0 ? array.get(_trBarsHeld, _lastIdx)   : 0
_lastCapturePc = _lastMFE > 0 ? _lastExitR / _lastMFE * 100 : 0.0
_lastExitWin   = _lastExitR > 0

if showExitMarkers and longExitSignal
    label.new(bar_index, high, '✕ ' + _longExitReason + ' · ' + str.tostring(_lastExitR, '#.##') + 'R',
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_down, color=f_exitColor(_longExitReason), textcolor=color.white,
         size=size.small, force_overlay=true,
         tooltip='LONG EXIT  ·  reason=' + _longExitReason + '  ·  ' + (_lastExitWin ? 'WIN' : 'LOSS')
              + '\n────────────'
              + '\nEntry score: ' + str.tostring(_lastEntrySc, '#.#')
              + '\nFamily: ' + str.tostring(array.get(_trFamily, _lastIdx))
              + '\nBars held: ' + str.tostring(_lastBarsHeld)
              + '\nR at exit: ' + str.tostring(_lastExitR, '#.##')
              + '\nMFE (peak R): ' + str.tostring(_lastMFE, '#.##')
              + '\nCapture: ' + str.tostring(_lastCapturePc, '#.#') + '%')

if showExitMarkers and shortExitSignal
    label.new(bar_index, low, '✕ ' + _shortExitReason + ' · ' + str.tostring(_lastExitR, '#.##') + 'R',
         xloc=xloc.bar_index, yloc=yloc.price,
         style=label.style_label_up, color=f_exitColor(_shortExitReason), textcolor=color.white,
         size=size.small, force_overlay=true,
         tooltip='SHORT EXIT  ·  reason=' + _shortExitReason + '  ·  ' + (_lastExitWin ? 'WIN' : 'LOSS')
              + '\n────────────'
              + '\nEntry score: ' + str.tostring(_lastEntrySc, '#.#')
              + '\nFamily: ' + str.tostring(array.get(_trFamily, _lastIdx))
              + '\nBars held: ' + str.tostring(_lastBarsHeld)
              + '\nR at exit: ' + str.tostring(_lastExitR, '#.##')
              + '\nMFE (peak R): ' + str.tostring(_lastMFE, '#.##')
              + '\nCapture: ' + str.tostring(_lastCapturePc, '#.#') + '%')

// ═══════════════════════════════════════════════════════════════════════════
// MINI DASHBOARD
// ═══════════════════════════════════════════════════════════════════════════

var table dash = table.new(position.top_right, 2, 10,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1)

// Separate virtual signal-quality table. It is not the real strategy trade
// journal; real closed-trade results are sourced from strategy.closedtrades.
var table qdash = table.new(position.bottom_right, 3, 6,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1)

var table rdash = table.new(position.bottom_left, 3, 5,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1)

color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)

if barstate.islast
    table.cell(dash, 0, 0, 'WT v3 Composite', text_color=tc, bgcolor=hd, text_size=size.small, text_halign=text.align_left)
    table.cell(dash, 1, 0, '', bgcolor=hd, text_size=size.small)

    table.cell(dash, 0, 1, '1 WT', text_color=tc, text_size=size.small, text_halign=text.align_left)
    _wtState = inOSZone ? 'OS' : inOBZone ? 'OB' : (wtOsc > 0 ? 'Bull' : 'Bear')
    table.cell(dash, 1, 1, _wtState + ' (' + str.tostring(wtOsc, '#.#') + ')', text_color=tc, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 2, '2 Structure', text_color=tc, text_size=size.small, text_halign=text.align_left)
    _structState = structureBull ? 'Bull HH/HL' : structureBear ? 'Bear LL/LH' : 'Mixed'
    _structColor = structureBull ? colorBull : structureBear ? colorBear : color.gray
    table.cell(dash, 1, 2, _structState, text_color=_structColor, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 3, '3 Volume', text_color=tc, text_size=size.small, text_halign=text.align_left)
    _volRatio = na(volMA) or volMA == 0 ? 1.0 : volume / volMA
    table.cell(dash, 1, 3, str.tostring(_volRatio, '#.#') + '× MA' + (volSpike ? ' ⚡' : ''),
         text_color=volSpike ? colorBull : tc, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 4, '   POC loc/macro', text_color=tc, text_size=size.small, text_halign=text.align_left)
    _pocLocSym = priceAbovePOC ? '↑' : priceBelowPOC ? '↓' : '·'
    _pocMacSym = priceAboveMacroPOC ? '↑' : priceBelowMacroPOC ? '↓' : '·'
    table.cell(dash, 1, 4, _pocLocSym + ' / ' + _pocMacSym, text_color=tc, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 5, '4 Setup HTF (' + _htfTF + ')', text_color=tc, text_size=size.small, text_halign=text.align_left)
    _htfState = htfBull ? 'Bull' : htfBear ? 'Bear' : 'Flat'
    _htfColor = htfBull ? colorBull : htfBear ? colorBear : color.gray
    table.cell(dash, 1, 5, _htfState + ' (' + str.tostring(htfWtOsc, '#.#') + ')', text_color=_htfColor, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 6, '   Anchor (' + _anchorTF + ')', text_color=tc, text_size=size.small, text_halign=text.align_left)
    _anchorState = not _anchorActive ? 'Same as setup' : anchorBull ? 'Bull' : anchorBear ? 'Bear' : 'Flat'
    _anchorColor = not _anchorActive ? color.gray : anchorBull ? colorBull : anchorBear ? colorBear : color.gray
    table.cell(dash, 1, 6, _anchorState + ' (' + str.tostring(anchorWtOsc, '#.#') + ')', text_color=_anchorColor, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 7, '', bgcolor=color.new(color.gray, 80), text_size=size.tiny)
    table.cell(dash, 1, 7, '', bgcolor=color.new(color.gray, 80), text_size=size.tiny)

    table.cell(dash, 0, 8, 'Long Ctx (' + profileMode + ')', text_color=tc, text_size=size.small, text_halign=text.align_left)
    table.cell(dash, 1, 8, str.tostring(longContextScore, '#.#') + ' / ' + str.tostring(maxContextScore, '#.#'),
         text_color=longContextScore >= _effMinScore ? colorBull : tc, text_size=size.small, text_halign=text.align_right)

    table.cell(dash, 0, 9, 'Short Ctx (' + profileMode + ')', text_color=tc, text_size=size.small, text_halign=text.align_left)
    table.cell(dash, 1, 9, str.tostring(shortContextScore, '#.#') + ' / ' + str.tostring(maxContextScore, '#.#'),
         text_color=shortContextScore >= _effMinScore ? colorBear : tc, text_size=size.small, text_halign=text.align_right)

    // ─── Virtual signal-quality table (separate, bottom_right) ───────────────
    int   _nL = 0
    int   _nS = 0
    int   _wL = 0
    int   _wS = 0
    float _sumRL = 0.0
    float _sumRS = 0.0
    float _sumMFEL = 0.0
    float _sumMFES = 0.0
    int _trN = array.size(_trExitR)
    if _trN > 0
        for i = 0 to _trN - 1
            _r = array.get(_trExitR, i)
            _m = array.get(_trMFE, i)
            if array.get(_trDir, i) == 'LONG'
                _nL := _nL + 1
                _sumRL := _sumRL + _r
                _sumMFEL := _sumMFEL + _m
                if _r > 0
                    _wL := _wL + 1
            else
                _nS := _nS + 1
                _sumRS := _sumRS + _r
                _sumMFES := _sumMFES + _m
                if _r > 0
                    _wS := _wS + 1
    _avgRL    = _nL > 0 ? _sumRL / _nL : 0.0
    _avgRS    = _nS > 0 ? _sumRS / _nS : 0.0
    _avgMFEL  = _nL > 0 ? _sumMFEL / _nL : 0.0
    _avgMFES  = _nS > 0 ? _sumMFES / _nS : 0.0
    _wrL      = _nL > 0 ? _wL * 100.0 / _nL : 0.0
    _wrS      = _nS > 0 ? _wS * 100.0 / _nS : 0.0
    _capL     = _avgMFEL > 0 ? _avgRL / _avgMFEL * 100 : 0.0
    _capS     = _avgMFES > 0 ? _avgRS / _avgMFES * 100 : 0.0

    table.cell(qdash, 0, 0, 'Virtual Q',  text_color=tc, bgcolor=hd, text_size=size.tiny, text_halign=text.align_left)
    table.cell(qdash, 1, 0, 'L',        text_color=tc, bgcolor=hd, text_size=size.tiny, text_halign=text.align_right)
    table.cell(qdash, 2, 0, 'S',        text_color=tc, bgcolor=hd, text_size=size.tiny, text_halign=text.align_right)

    table.cell(qdash, 0, 1, 'Signals',   text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(qdash, 1, 1, str.tostring(_nL), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(qdash, 2, 1, str.tostring(_nS), text_color=tc, text_size=size.tiny, text_halign=text.align_right)

    table.cell(qdash, 0, 2, 'WR %',     text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(qdash, 1, 2, str.tostring(_wrL, '#.#'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(qdash, 2, 2, str.tostring(_wrS, '#.#'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)

    table.cell(qdash, 0, 3, 'Avg R',    text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(qdash, 1, 3, str.tostring(_avgRL, '#.##'), text_color=_avgRL > 0 ? colorBull : colorBear, text_size=size.tiny, text_halign=text.align_right)
    table.cell(qdash, 2, 3, str.tostring(_avgRS, '#.##'), text_color=_avgRS > 0 ? colorBull : colorBear, text_size=size.tiny, text_halign=text.align_right)

    table.cell(qdash, 0, 4, 'Avg MFE',  text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(qdash, 1, 4, str.tostring(_avgMFEL, '#.##'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(qdash, 2, 4, str.tostring(_avgMFES, '#.##'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)

    table.cell(qdash, 0, 5, 'Capt %',   text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(qdash, 1, 5, str.tostring(_capL, '#.#'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(qdash, 2, 5, str.tostring(_capS, '#.#'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)

    int _rnL = 0
    int _rnS = 0
    int _rwL = 0
    int _rwS = 0
    float _rPnlL = 0.0
    float _rPnlS = 0.0
    float _rRL = 0.0
    float _rRS = 0.0
    _rtN = array.size(_realProfit)
    if _rtN > 0
        for i = 0 to _rtN - 1
            _p = array.get(_realProfit, i)
            _rrReal = array.get(_realExitR, i)
            if array.get(_realDir, i) == 'LONG'
                _rnL := _rnL + 1
                _rPnlL := _rPnlL + _p
                _rRL := _rRL + nz(_rrReal)
                if _p > 0
                    _rwL := _rwL + 1
            else
                _rnS := _rnS + 1
                _rPnlS := _rPnlS + _p
                _rRS := _rRS + nz(_rrReal)
                if _p > 0
                    _rwS := _rwS + 1
    _rWrL = _rnL > 0 ? _rwL * 100.0 / _rnL : 0.0
    _rWrS = _rnS > 0 ? _rwS * 100.0 / _rnS : 0.0
    _rAvgL = _rnL > 0 ? _rRL / _rnL : 0.0
    _rAvgS = _rnS > 0 ? _rRS / _rnS : 0.0

    table.cell(rdash, 0, 0, 'REAL Strategy', text_color=tc, bgcolor=hd, text_size=size.tiny, text_halign=text.align_left)
    table.cell(rdash, 1, 0, 'L', text_color=tc, bgcolor=hd, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 2, 0, 'S', text_color=tc, bgcolor=hd, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 0, 1, 'Trades', text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(rdash, 1, 1, str.tostring(_rnL), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 2, 1, str.tostring(_rnS), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 0, 2, 'Net', text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(rdash, 1, 2, str.tostring(_rPnlL, '#'), text_color=_rPnlL >= 0 ? colorBull : colorBear, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 2, 2, str.tostring(_rPnlS, '#'), text_color=_rPnlS >= 0 ? colorBull : colorBear, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 0, 3, 'WR %', text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(rdash, 1, 3, str.tostring(_rWrL, '#.#'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 2, 3, str.tostring(_rWrS, '#.#'), text_color=tc, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 0, 4, 'Avg R', text_color=tc, text_size=size.tiny, text_halign=text.align_left)
    table.cell(rdash, 1, 4, str.tostring(_rAvgL, '#.##'), text_color=_rAvgL >= 0 ? colorBull : colorBear, text_size=size.tiny, text_halign=text.align_right)
    table.cell(rdash, 2, 4, str.tostring(_rAvgS, '#.##'), text_color=_rAvgS >= 0 ? colorBull : colorBear, text_size=size.tiny, text_halign=text.align_right)

// ═══════════════════════════════════════════════════════════════════════════
// DEBUG LOG
// ═══════════════════════════════════════════════════════════════════════════

// Family-aware DECISION + BLOCKED logs.
// One DECISION per direction per bar (RR takes precedence over PB if both fire).
// One BLOCKED row per family-trigger that didn't pass.

_volRatio = na(volMA) or volMA == 0 ? 1.0 : volume / volMA
_rawWtCrossStr = ' | RAW_WT: strongBull=' + str.tostring(rawStrongBullCross) + ' strongBear=' + str.tostring(rawStrongBearCross) + ' osc=' + str.tostring(wtOsc, '#.##') + ' sig=' + str.tostring(wtSig, '#.##') + ' ob=' + str.tostring(_effUpperBand) + ' os=' + str.tostring(_effLowerBand) + _pivotTimingStr
_regimeStr = ' | REGIME: mode=' + _effRegimeMode + ' regimeOk=' + str.tostring(volRegimeOk) + ' inBand=' + str.tostring(_inRegimeBand) + ' atrNorm=' + str.tostring(atrNorm, '#.##') + ' min=' + str.tostring(_effVolRegimeMin, '#.#') + ' max=' + str.tostring(_effVolRegimeMax, '#.#') + ' bonus=' + str.tostring(volRegimeBonus, '#.#')
_tfBiasStr = ' | TFB: active=' + str.tostring(autoTFBiasActive) + ' htf=' + str.tostring(htfWtOsc, '#.#') + ' longOk=' + str.tostring(autoTFBiasLongOk) + ' shortOk=' + str.tostring(autoTFBiasShortOk)

// LONG DECISION (range-reversal preferred when both families fire same bar)
if debugMode and longEntrySignal and longRRSignal
    log.info(str.format('WT3 LONG DECISION | family=range_reversal | type=range_reversal | bar={0} | ctxScore={1}/{2} | profile={3}'
         + ' | WT: osc={4} sig={5} osRecent={6} barsSinceOS={7}'
         + ' | STRUCT: hl={8} ll={9} hh={10} lh={11} fresh={12} STRUCT={13}'
         + ' | VOLUME: ratio={14} bullSpike={15} VOL={16}'
         + ' | POC: px={17} reclaim={18} POC={19}'
         + ' | HTF: tf={20} bull={21} mtfOk={22} htf={23} anchor={24}'
         + ' | SETUP: ok={25} loc={26} rangeLoc={27} swingLoc={28} roomOk={29} volOk={30}'
         + ' | PB: pbOk={31} pbTrend={32} pbLoc={33} impulseATR={34}'
         + ' | TARGET: tp={35} sl={36}',
         str.tostring(bar_index), str.tostring(longContextScoreRR, '#.#'), str.tostring(maxContextScoreRR, '#.#'), profileMode,
         str.tostring(wtOsc, '#.##'), str.tostring(wtSig, '#.##'), str.tostring(osRecent), str.tostring(barsSinceOS),
         str.tostring(hlDetected), str.tostring(llDetected), str.tostring(hhDetected), str.tostring(lhDetected), str.tostring(structureFresh), str.tostring(structureLongOk),
         str.tostring(_volRatio, '#.##'), str.tostring(bullVolSpike), str.tostring(bullVolSpike),
         str.tostring(pocPrice, format.mintick), str.tostring(pocReclaimRecent), str.tostring(pocReclaimRecent),
         _htfTF, str.tostring(htfBull), str.tostring(mtfLongOkRR), str.tostring(longHtfScoreRR, '#.#'), str.tostring(longAnchorScoreRR, '#.#'),
         str.tostring(longRangeReversalSetupOk), str.tostring(longLocationOk), str.tostring(longRangeLocationOk), str.tostring(longSwingLocationOk), str.tostring(longRoomOk), str.tostring(longSetupVolumeOkRR),
         str.tostring(longPullbackSetupOk), str.tostring(longPullbackTrendOk), str.tostring(longPullbackLocationOk), str.tostring(longImpulseATR, '#.#'),
         str.tostring(longEntryTakeProfit, format.mintick), str.tostring(longStop, format.mintick)) + _structureSnapshot + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and longEntrySignal and longPBSignal and not longRRSignal
    log.info(str.format('WT3 LONG DECISION | family=pullback_continuation | type=pullback_continuation | bar={0} | ctxScore={1}/{2} | profile={3}'
         + ' | WT: osc={4} sig={5}'
         + ' | STRUCT: hl={6} ll={7} hh={8} lh={9} fresh={10} STRUCT={11}'
         + ' | VOLUME: ratio={12} bullSpike={13} VOL={14}'
         + ' | POC: px={15} reclaim={16} POC={17}'
         + ' | HTF: tf={18} bull={19} mtfOk={20} htf={21} anchor={22}'
         + ' | SETUP: ok={23} loc={24} rangeLoc={25} swingLoc={26} roomOk={27} pbRoomOk={28} midline={29} volOk={30}'
         + ' | PB: pbOk={31} pbTrend={32} pbLoc={33} impulseATR={34}'
         + ' | TARGET: tp={35} sl={36}',
         str.tostring(bar_index), str.tostring(longContextScorePB, '#.#'), str.tostring(maxContextScorePB, '#.#'), profileMode,
         str.tostring(wtOsc, '#.##'), str.tostring(wtSig, '#.##'),
         str.tostring(hlDetected), str.tostring(llDetected), str.tostring(hhDetected), str.tostring(lhDetected), str.tostring(structureFresh), str.tostring(structureLongOk),
         str.tostring(_volRatio, '#.##'), str.tostring(bullVolSpike), str.tostring(bullVolSpike),
         str.tostring(pocPrice, format.mintick), str.tostring(pocReclaimRecent), str.tostring(pocReclaimRecent),
         _htfTF, str.tostring(htfBull), str.tostring(mtfLongOkPB), str.tostring(longHtfScorePB, '#.#'), str.tostring(longAnchorScorePB, '#.#'),
         str.tostring(longPullbackSetupOk), str.tostring(longLocationOk), str.tostring(longRangeLocationOk), str.tostring(longSwingLocationOk), str.tostring(longRoomOk), str.tostring(longPBRoomOk), str.tostring(longPBMidlineOk), str.tostring(longSetupVolumeOkPB),
         str.tostring(longPullbackSetupOk), str.tostring(longPullbackTrendOk), str.tostring(longPullbackLocationOk), str.tostring(longImpulseATR, '#.#'),
         str.tostring(longEntryTakeProfit, format.mintick), str.tostring(longStop, format.mintick)) + _structureSnapshot + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and longEntrySignal and longERSignal and not longRRSignal and not longPBSignal
    log.info(str.format('WT3 LONG DECISION | family=early_rejection | type=early_rejection | bar={0} | ctxScore={1}/{2} | profile={3}'
         + ' | WT: osc={4} sig={5}'
         + ' | ER: setupOk={6} tfOk={7} area={8} barOk={9} local={10} momentum={11} mfi={12} stochK={13} stochD={14}'
         + ' | VOLUME: ratio={15} bullSpike={16}'
         + ' | POC: px={17} reclaim={18}'
         + ' | HTF: tf={19} bull={20} mtfOk={21} htf={22} anchor={23}'
         + ' | SETUP: roomOk={24} pbRoomOk={25} rangeLoc={26} nearEma={27}'
         + ' | TARGET: tp={28} sl={29}',
         str.tostring(bar_index), str.tostring(earlyLongContextScore, '#.#'), str.tostring(maxContextScoreER, '#.#'), profileMode,
         str.tostring(wtOsc, '#.##'), str.tostring(wtSig, '#.##'),
         str.tostring(longERSetupPass), str.tostring(earlyLongTFOk), str.tostring(earlyLongRejectArea), str.tostring(earlyLongRejectBar), str.tostring(earlyLocalLongConfirm), str.tostring(earlyMomentumBull), str.tostring(mfiVal, '#.#'), str.tostring(stochRsiK, '#.#'), str.tostring(stochRsiD, '#.#'),
         str.tostring(_volRatio, '#.##'), str.tostring(bullVolSpike),
         str.tostring(pocPrice, format.mintick), str.tostring(pocReclaimRecent),
         _htfTF, str.tostring(htfBull), str.tostring(mtfLongOkPB), str.tostring(longHtfScorePB, '#.#'), str.tostring(longAnchorScorePB, '#.#'),
         str.tostring(earlyLongRoomOk), str.tostring(longPBRoomOk), str.tostring(longRangeLocationOk), str.tostring(nearPullbackEma),
         str.tostring(longEntryTakeProfit, format.mintick), str.tostring(longStop, format.mintick)) + ' | ERSTRUCT: longOk=' + str.tostring(longERStructureOk) + _structureSnapshot + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

// LONG BLOCKED — one row per family trigger that didn't pass
if debugMode and rrTriggerLong and not longRRSignal
    log.info(str.format('WT3 LONG BLOCKED | family=range_reversal | type=range_reversal | bar={0} | ctxScore={1}/{2} min={3} | profile={4}'
         + ' | mtfOk={5} rrQuality={6} STRUCT={7} POC={8} VOL={9}'
         + ' | SETUP: ok={10} loc={11} rangeLoc={12} swingLoc={13} roomOk={14} volOk={15}'
         + ' | PB: pbOk={16} pbTrend={17} pbLoc={18}',
         str.tostring(bar_index), str.tostring(longContextScoreRR, '#.#'), str.tostring(maxContextScoreRR, '#.#'), str.tostring(_effMinScore, '#.#'), profileMode,
         str.tostring(mtfLongOkRR), str.tostring(rrLongQualityOk), str.tostring(structureLongOk), str.tostring(pocReclaimRecent), str.tostring(bullVolSpike),
         str.tostring(longRangeReversalSetupOk), str.tostring(longLocationOk), str.tostring(longRangeLocationOk), str.tostring(longSwingLocationOk), str.tostring(longRoomOk), str.tostring(longSetupVolumeOkRR),
         str.tostring(longPullbackSetupOk), str.tostring(longPullbackTrendOk), str.tostring(longPullbackLocationOk)) + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and pbTriggerLong and not longPBSignal
    log.info(str.format('WT3 LONG BLOCKED | family=pullback_continuation | type=pullback_continuation | bar={0} | ctxScore={1}/{2} min={3} | profile={4}'
         + ' | mtfOk={5} STRUCT={6} POC={7} VOL={8}'
         + ' | SETUP: ok={9} loc={10} rangeLoc={11} swingLoc={12} roomOk={13} midline={14} volOk={15}'
         + ' | PB: pbOk={16} pbTrend={17} pbLoc={18} impulseATR={19}',
         str.tostring(bar_index), str.tostring(longContextScorePB, '#.#'), str.tostring(maxContextScorePB, '#.#'), str.tostring(_effMinScore, '#.#'), profileMode,
         str.tostring(mtfLongOkPB), str.tostring(structureLongOk), str.tostring(pocReclaimRecent), str.tostring(bullVolSpike),
         str.tostring(longPullbackSetupOk), str.tostring(longLocationOk), str.tostring(longRangeLocationOk), str.tostring(longSwingLocationOk), str.tostring(longRoomOk), str.tostring(longPBMidlineOk), str.tostring(longSetupVolumeOkPB),
         str.tostring(longPullbackSetupOk), str.tostring(longPullbackTrendOk), str.tostring(longPullbackLocationOk), str.tostring(longImpulseATR, '#.#')) + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and erTriggerLong and not longERSignal
    _erLongReason = 'other'
    if not earlyLongTFOk
        _erLongReason := 'tf'
    else if not earlyLongRejectArea
        _erLongReason := 'area'
    else if not earlyLongRejectBar
        _erLongReason := 'bar'
    else if _erLocalRequired and not earlyLocalLongConfirm
        _erLongReason := 'local_gate'
    else if not earlyMomentumBull
        _erLongReason := 'momentum'
    else if not earlyLongRoomOk
        _erLongReason := 'room'
    else if not longERBiasOk
        _erLongReason := 'bias'
    else if not longERStructureOk
        _erLongReason := 'struct'
    else if not mtfLongOkPB
        _erLongReason := 'mtf'
    else if earlyLongContextScore < earlyRejectMinScore
        _erLongReason := 'score'
    log.info(str.format('WT3 LONG BLOCKED | family=early_rejection | type=early_rejection | bar={0} | ctxScore={1}/{2} min={3} | profile={4}'
         + ' | mtfOk={5} tfOk={6} erOk={7} area={8} barOk={9} local={10} momentum={11}'
         + ' | FLOW: mfi={12} mfiBull={13} stochK={14} stochD={15} stochBull={16}'
         + ' | SETUP: roomOk={17} pbRoomOk={18} rangeLoc={19} nearEma={20} volOk={21}',
         str.tostring(bar_index), str.tostring(earlyLongContextScore, '#.#'), str.tostring(maxContextScoreER, '#.#'), str.tostring(earlyRejectMinScore, '#.#'), profileMode,
         str.tostring(mtfLongOkPB), str.tostring(earlyLongTFOk), str.tostring(longERSetupPass), str.tostring(earlyLongRejectArea), str.tostring(earlyLongRejectBar), str.tostring(earlyLocalLongConfirm), str.tostring(earlyMomentumBull),
         str.tostring(mfiVal, '#.#'), str.tostring(mfiBullConfirm), str.tostring(stochRsiK, '#.#'), str.tostring(stochRsiD, '#.#'), str.tostring(stochRsiBullConfirm),
         str.tostring(earlyLongRoomOk), str.tostring(longPBRoomOk), str.tostring(longRangeLocationOk), str.tostring(nearPullbackEma), str.tostring(longSetupVolumeOkPB)) + ' | reason=' + _erLongReason + ' | ERSTRUCT: longOk=' + str.tostring(longERStructureOk) + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

// SHORT DECISION
if debugMode and shortEntrySignal and shortRRSignal
    log.info(str.format('WT3 SHORT DECISION | family=range_reversal | type=range_reversal | bar={0} | ctxScore={1}/{2} | profile={3}'
         + ' | WT: osc={4} sig={5} obRecent={6} barsSinceOB={7}'
         + ' | STRUCT: lh={8} hh={9} ll={10} hl={11} fresh={12} STRUCT={13}'
         + ' | VOLUME: ratio={14} bearSpike={15} VOL={16}'
         + ' | POC: px={17} reject={18} POC={19}'
         + ' | HTF: tf={20} bear={21} mtfOk={22} htf={23} anchor={24}'
         + ' | SETUP: ok={25} loc={26} rangeLoc={27} swingLoc={28} roomOk={29} volOk={30}'
         + ' | PB: pbOk={31} pbTrend={32} pbLoc={33} impulseATR={34}'
         + ' | TARGET: tp={35} sl={36}',
         str.tostring(bar_index), str.tostring(shortContextScoreRR, '#.#'), str.tostring(maxContextScoreRR, '#.#'), profileMode,
         str.tostring(wtOsc, '#.##'), str.tostring(wtSig, '#.##'), str.tostring(obRecent), str.tostring(barsSinceOB),
         str.tostring(lhDetected), str.tostring(hhDetected), str.tostring(llDetected), str.tostring(hlDetected), str.tostring(structureFresh), str.tostring(structureShortOk),
         str.tostring(_volRatio, '#.##'), str.tostring(bearVolSpike), str.tostring(bearVolSpike),
         str.tostring(pocPrice, format.mintick), str.tostring(pocRejectRecent), str.tostring(pocRejectRecent),
         _htfTF, str.tostring(htfBear), str.tostring(mtfShortOkRR), str.tostring(shortHtfScoreRR, '#.#'), str.tostring(shortAnchorScoreRR, '#.#'),
         str.tostring(shortRangeReversalSetupOk), str.tostring(shortLocationOk), str.tostring(shortRangeLocationOk), str.tostring(shortSwingLocationOk), str.tostring(shortRoomOk), str.tostring(shortSetupVolumeOkRR),
         str.tostring(shortPullbackSetupOk), str.tostring(shortPullbackTrendOk), str.tostring(shortPullbackLocationOk), str.tostring(shortImpulseATR, '#.#'),
         str.tostring(shortEntryTakeProfit, format.mintick), str.tostring(shortStop, format.mintick)) + _structureSnapshot + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and shortEntrySignal and shortPBSignal and not shortRRSignal
    log.info(str.format('WT3 SHORT DECISION | family=pullback_continuation | type=pullback_continuation | bar={0} | ctxScore={1}/{2} | profile={3}'
         + ' | WT: osc={4} sig={5}'
         + ' | STRUCT: lh={6} hh={7} ll={8} hl={9} fresh={10} STRUCT={11}'
         + ' | VOLUME: ratio={12} bearSpike={13} VOL={14}'
         + ' | POC: px={15} reject={16} POC={17}'
         + ' | HTF: tf={18} bear={19} mtfOk={20} htf={21} anchor={22}'
         + ' | SETUP: ok={23} loc={24} rangeLoc={25} swingLoc={26} roomOk={27} pbRoomOk={28} midline={29} volOk={30}'
         + ' | PB: pbOk={31} pbTrend={32} pbLoc={33} impulseATR={34}'
         + ' | TARGET: tp={35} sl={36}',
         str.tostring(bar_index), str.tostring(shortContextScorePB, '#.#'), str.tostring(maxContextScorePB, '#.#'), profileMode,
         str.tostring(wtOsc, '#.##'), str.tostring(wtSig, '#.##'),
         str.tostring(lhDetected), str.tostring(hhDetected), str.tostring(llDetected), str.tostring(hlDetected), str.tostring(structureFresh), str.tostring(structureShortOk),
         str.tostring(_volRatio, '#.##'), str.tostring(bearVolSpike), str.tostring(bearVolSpike),
         str.tostring(pocPrice, format.mintick), str.tostring(pocRejectRecent), str.tostring(pocRejectRecent),
         _htfTF, str.tostring(htfBear), str.tostring(mtfShortOkPB), str.tostring(shortHtfScorePB, '#.#'), str.tostring(shortAnchorScorePB, '#.#'),
         str.tostring(shortPullbackSetupOk), str.tostring(shortLocationOk), str.tostring(shortRangeLocationOk), str.tostring(shortSwingLocationOk), str.tostring(shortRoomOk), str.tostring(shortPBRoomOk), str.tostring(shortPBMidlineOk), str.tostring(shortSetupVolumeOkPB),
         str.tostring(shortPullbackSetupOk), str.tostring(shortPullbackTrendOk), str.tostring(shortPullbackLocationOk), str.tostring(shortImpulseATR, '#.#'),
         str.tostring(shortEntryTakeProfit, format.mintick), str.tostring(shortStop, format.mintick)) + _structureSnapshot + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and shortEntrySignal and shortERSignal and not shortRRSignal and not shortPBSignal
    log.info(str.format('WT3 SHORT DECISION | family=early_rejection | type=early_rejection | bar={0} | ctxScore={1}/{2} | profile={3}'
         + ' | WT: osc={4} sig={5}'
         + ' | ER: setupOk={6} tfOk={7} area={8} barOk={9} local={10} momentum={11} mfi={12} stochK={13} stochD={14}'
         + ' | VOLUME: ratio={15} bearSpike={16}'
         + ' | POC: px={17} reject={18}'
         + ' | HTF: tf={19} bear={20} mtfOk={21} htf={22} anchor={23}'
         + ' | SETUP: roomOk={24} pbRoomOk={25} rangeLoc={26} nearEma={27}'
         + ' | TARGET: tp={28} sl={29}',
         str.tostring(bar_index), str.tostring(earlyShortContextScore, '#.#'), str.tostring(maxContextScoreER, '#.#'), profileMode,
         str.tostring(wtOsc, '#.##'), str.tostring(wtSig, '#.##'),
         str.tostring(shortERSetupPass), str.tostring(earlyShortTFOk), str.tostring(earlyShortRejectArea), str.tostring(earlyShortRejectBar), str.tostring(earlyLocalShortConfirm), str.tostring(earlyMomentumBear), str.tostring(mfiVal, '#.#'), str.tostring(stochRsiK, '#.#'), str.tostring(stochRsiD, '#.#'),
         str.tostring(_volRatio, '#.##'), str.tostring(bearVolSpike),
         str.tostring(pocPrice, format.mintick), str.tostring(pocRejectRecent),
         _htfTF, str.tostring(htfBear), str.tostring(mtfShortOkPB), str.tostring(shortHtfScorePB, '#.#'), str.tostring(shortAnchorScorePB, '#.#'),
         str.tostring(earlyShortRoomOk), str.tostring(shortPBRoomOk), str.tostring(shortRangeLocationOk), str.tostring(nearPullbackEma),
         str.tostring(shortEntryTakeProfit, format.mintick), str.tostring(shortStop, format.mintick)) + _structureSnapshot + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

// SHORT BLOCKED
if debugMode and rrTriggerShort and not shortRRSignal
    log.info(str.format('WT3 SHORT BLOCKED | family=range_reversal | type=range_reversal | bar={0} | ctxScore={1}/{2} min={3} | profile={4}'
         + ' | mtfOk={5} rrQuality={6} STRUCT={7} POC={8} VOL={9}'
         + ' | SETUP: ok={10} loc={11} rangeLoc={12} swingLoc={13} roomOk={14} volOk={15}'
         + ' | PB: pbOk={16} pbTrend={17} pbLoc={18}',
         str.tostring(bar_index), str.tostring(shortContextScoreRR, '#.#'), str.tostring(maxContextScoreRR, '#.#'), str.tostring(_effMinScore, '#.#'), profileMode,
         str.tostring(mtfShortOkRR), str.tostring(rrShortQualityOk), str.tostring(structureShortOk), str.tostring(pocRejectRecent), str.tostring(bearVolSpike),
         str.tostring(shortRangeReversalSetupOk), str.tostring(shortLocationOk), str.tostring(shortRangeLocationOk), str.tostring(shortSwingLocationOk), str.tostring(shortRoomOk), str.tostring(shortSetupVolumeOkRR),
         str.tostring(shortPullbackSetupOk), str.tostring(shortPullbackTrendOk), str.tostring(shortPullbackLocationOk)) + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and pbTriggerShort and not shortPBSignal
    log.info(str.format('WT3 SHORT BLOCKED | family=pullback_continuation | type=pullback_continuation | bar={0} | ctxScore={1}/{2} min={3} | profile={4}'
         + ' | mtfOk={5} STRUCT={6} POC={7} VOL={8}'
         + ' | SETUP: ok={9} loc={10} rangeLoc={11} swingLoc={12} roomOk={13} midline={14} volOk={15}'
         + ' | PB: pbOk={16} pbTrend={17} pbLoc={18} impulseATR={19}',
         str.tostring(bar_index), str.tostring(shortContextScorePB, '#.#'), str.tostring(maxContextScorePB, '#.#'), str.tostring(_effMinScore, '#.#'), profileMode,
         str.tostring(mtfShortOkPB), str.tostring(structureShortOk), str.tostring(pocRejectRecent), str.tostring(bearVolSpike),
         str.tostring(shortPullbackSetupOk), str.tostring(shortLocationOk), str.tostring(shortRangeLocationOk), str.tostring(shortSwingLocationOk), str.tostring(shortRoomOk), str.tostring(shortPBMidlineOk), str.tostring(shortSetupVolumeOkPB),
         str.tostring(shortPullbackSetupOk), str.tostring(shortPullbackTrendOk), str.tostring(shortPullbackLocationOk), str.tostring(shortImpulseATR, '#.#')) + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)

if debugMode and erTriggerShort and not shortERSignal
    _erShortReason = 'other'
    if not earlyShortTFOk
        _erShortReason := 'tf'
    else if not earlyShortRejectArea
        _erShortReason := 'area'
    else if not earlyShortRejectBar
        _erShortReason := 'bar'
    else if _erLocalRequired and not earlyLocalShortConfirm
        _erShortReason := 'local_gate'
    else if not earlyMomentumBear
        _erShortReason := 'momentum'
    else if not earlyShortRoomOk
        _erShortReason := 'room'
    else if not shortERBiasOk
        _erShortReason := 'bias'
    else if not shortERStructureOk
        _erShortReason := 'struct'
    else if not mtfShortOkPB
        _erShortReason := 'mtf'
    else if earlyShortContextScore < earlyRejectMinScore
        _erShortReason := 'score'
    log.info(str.format('WT3 SHORT BLOCKED | family=early_rejection | type=early_rejection | bar={0} | ctxScore={1}/{2} min={3} | profile={4}'
         + ' | mtfOk={5} tfOk={6} erOk={7} area={8} barOk={9} local={10} momentum={11}'
         + ' | FLOW: mfi={12} mfiBear={13} stochK={14} stochD={15} stochBear={16}'
         + ' | SETUP: roomOk={17} pbRoomOk={18} rangeLoc={19} nearEma={20} volOk={21}',
         str.tostring(bar_index), str.tostring(earlyShortContextScore, '#.#'), str.tostring(maxContextScoreER, '#.#'), str.tostring(earlyRejectMinScore, '#.#'), profileMode,
         str.tostring(mtfShortOkPB), str.tostring(earlyShortTFOk), str.tostring(shortERSetupPass), str.tostring(earlyShortRejectArea), str.tostring(earlyShortRejectBar), str.tostring(earlyLocalShortConfirm), str.tostring(earlyMomentumBear),
         str.tostring(mfiVal, '#.#'), str.tostring(mfiBearConfirm), str.tostring(stochRsiK, '#.#'), str.tostring(stochRsiD, '#.#'), str.tostring(stochRsiBearConfirm),
         str.tostring(earlyShortRoomOk), str.tostring(shortPBRoomOk), str.tostring(shortRangeLocationOk), str.tostring(nearPullbackEma), str.tostring(shortSetupVolumeOkPB)) + ' | reason=' + _erShortReason + ' | ERSTRUCT: shortOk=' + str.tostring(shortERStructureOk) + _rawWtCrossStr + _playbookStr + _regimeStr + _tfBiasStr)
// ─── Strategy inputs (generated) ─────────────────────────────────────────────
g_strat       = "Strategy"
tradeDirInput = input.string("Both", "Trade Direction",
     options=["Both", "Long Only", "Short Only"], group=g_strat,
     tooltip="Filter entries by direction. Useful for asymmetric instruments.")
confirmClose  = input.bool(true,  "Entries on Confirmed Bar Only", group=g_strat,
     tooltip="Only trigger entries on closed bars. Prevents repainting in live trading.")
cooldownBars  = input.int(0,     "Cooldown Bars After Exit", minval=0, group=g_strat,
     tooltip="Block new entries for N bars after a position closes. Set 0 to disable.")
allowSignalReversal = input.bool(true, "Allow Same-Bar Signal Reversal", group=g_strat,
     tooltip="Allow a new opposite entry on the same bar when the current position also has an indicator exit. Same-direction conflicts are still resolved by context score.")
indicatorExitExecMode = input.string("1H Only", "Execute Indicator Exits",
     options=["All TF", "1H Only", "Off"], group=g_strat,
     tooltip="Controls whether indicator-managed exits are executed with strategy.close. test42 showed them useful on 1H but net-negative on 15m, 4H, and Daily.")
indicatorExitNeedsTargetOrStructure = input.bool(false, "Indicator Exits Need Target/Structure", group=g_strat,
     tooltip="Experimental. Only execute WT/GB/Opp/Time closes after the structural target was touched or opposite structure invalidates the trade. test49 showed this can worsen 1H Aggressive because indicator exits also act as damage control.")
oneHourExecutionMode = input.string("Bracket SL/TP", "1H Execution Mode",
     options=["Bracket SL/TP", "Stop + Signal Exit"], group=g_strat,
     tooltip="Concept test from test57/test58. Stop + Signal Exit keeps the protective stop but removes the fixed TP limit on 1H. test58 rejected it as a default because full-stop losses grew faster than signal-exit winners. 15m and 4H always keep bracket SL/TP.")
enableBE      = input.bool(true, "Break-Even Stop", group=g_strat,
     tooltip="Move SL to entry price once the trade is N×R in profit.")
beATRInput    = input.float(1.0,  "Break-Even Trigger (R×)", minval=0.1, step=0.1, group=g_strat,
     tooltip="R-multiple of the initial stop distance that triggers the break-even move. 1.0 = move SL to entry once 1R in profit.")

var int _lastExitBar = na
_justClosed = strategy.closedtrades > 0 and strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1) == bar_index
if _justClosed
    _lastExitBar := bar_index
_inCooldown = cooldownBars > 0 and not na(_lastExitBar) and (bar_index - _lastExitBar) < cooldownBars
_canEntry   = not _inCooldown and (not confirmClose or barstate.isconfirmed)

// ─── Strategy execution (generated, directional fixed SL/TP) ─────────────────
_canLong  = tradeDirInput != "Short Only"
_canShort = tradeDirInput != "Long Only"

var float _longSL = na
var float _longTP = na
var float _shortSL = na
var float _shortTP = na
var float _longHighExec = na
var float _longLowExec = na
var float _shortLowExec = na
var float _shortHighExec = na
var float _longRiskExec = na
var float _shortRiskExec = na
var float _longTargetExec = na
var float _shortTargetExec = na
var float _longEntryScoreExec = na
var float _shortEntryScoreExec = na
var float _longTargetRoomRExec = na
var float _shortTargetRoomRExec = na
var float _longTargetRoomATRExec = na
var float _shortTargetRoomATRExec = na
var float _longFavPivotExec = na
var float _shortFavPivotExec = na
var float _longAdvPivotExec = na
var float _shortAdvPivotExec = na
var int _longEntryBarExec = na
var int _shortEntryBarExec = na
var int _longTargetBarExec = na
var int _shortTargetBarExec = na
var int _longFavPivotBarExec = na
var int _shortFavPivotBarExec = na
var int _longAdvPivotBarExec = na
var int _shortAdvPivotBarExec = na
var bool _longTargetHitExec = false
var bool _shortTargetHitExec = false
var bool _longBETriggered = false
var bool _shortBETriggered = false
var string _longFamilyExec = ''
var string _shortFamilyExec = ''
var string _longEntryStructExec = ''
var string _shortEntryStructExec = ''
var string _pendingLongFamilyExec = ''
var string _pendingShortFamilyExec = ''
var string _pendingLongStructExec = ''
var string _pendingShortStructExec = ''
var float _pendingLongScoreExec = na
var float _pendingShortScoreExec = na
var float _pendingLongTargetRoomRExec = na
var float _pendingShortTargetRoomRExec = na
var float _pendingLongTargetRoomATRExec = na
var float _pendingShortTargetRoomATRExec = na

_newLongExec = strategy.position_size > 0 and strategy.position_size[1] <= 0
_newShortExec = strategy.position_size < 0 and strategy.position_size[1] >= 0
_flatExec = strategy.position_size == 0

if strategy.closedtrades > _loggedClosedTrades
    for _ct = _loggedClosedTrades to strategy.closedtrades - 1
        _entryId = strategy.closedtrades.entry_id(_ct)
        _exitId = strategy.closedtrades.exit_id(_ct)
        _entryPx = strategy.closedtrades.entry_price(_ct)
        _exitPx = strategy.closedtrades.exit_price(_ct)
        _profit = strategy.closedtrades.profit(_ct)
        _barsHeldReal = strategy.closedtrades.exit_bar_index(_ct) - strategy.closedtrades.entry_bar_index(_ct)
        _realLong = _entryId == 'Long'
        _realDirVal = _realLong ? 'LONG' : 'SHORT'
        _realFam = _realLong ? _longFamilyExec : _shortFamilyExec
        _realRisk = _realLong ? _longRiskExec : _shortRiskExec
        _realR = not na(_realRisk) and _realRisk > 0 ? (_realLong ? (_exitPx - _entryPx) / _realRisk : (_entryPx - _exitPx) / _realRisk) : na
        _realHigh = _realLong ? math.max(nz(_longHighExec, high), high) : math.max(nz(_shortHighExec, high), high)
        _realLow = _realLong ? math.min(nz(_longLowExec, low), low) : math.min(nz(_shortLowExec, low), low)
        _realMfeR = not na(_realRisk) and _realRisk > 0 ? (_realLong ? (_realHigh - _entryPx) / _realRisk : (_entryPx - _realLow) / _realRisk) : na
        _realMaeR = not na(_realRisk) and _realRisk > 0 ? (_realLong ? (_entryPx - _realLow) / _realRisk : (_realHigh - _entryPx) / _realRisk) : na
        _realCapturePct = not na(_realMfeR) and _realMfeR > 0 ? _realR / _realMfeR * 100 : na
        _realTarget = _realLong ? _longTargetExec : _shortTargetExec
        _realTargetRoomR = _realLong ? _longTargetRoomRExec : _shortTargetRoomRExec
        _realTargetRoomATR = _realLong ? _longTargetRoomATRExec : _shortTargetRoomATRExec
        _realTargetTouchedByRange = not na(_realTarget) and (_realLong ? _realHigh >= _realTarget : _realLow <= _realTarget)
        _realTargetHit = (_realLong ? _longTargetHitExec : _shortTargetHitExec) or _realTargetTouchedByRange
        _realTargetBar = _realLong ? _longTargetBarExec : _shortTargetBarExec
        _realBarsToTarget = _realTargetHit ? (not na(_realTargetBar) ? _realTargetBar - strategy.closedtrades.entry_bar_index(_ct) : strategy.closedtrades.exit_bar_index(_ct) - strategy.closedtrades.entry_bar_index(_ct)) : na
        _realEntryScore = _realLong ? _longEntryScoreExec : _shortEntryScoreExec
        _realEntryStruct = _realLong ? _longEntryStructExec : _shortEntryStructExec
        _realFavPivot = _realLong ? _longFavPivotExec : _shortFavPivotExec
        _realAdvPivot = _realLong ? _longAdvPivotExec : _shortAdvPivotExec
        _realFavPivotR = not na(_realRisk) and _realRisk > 0 and not na(_realFavPivot) ? (_realLong ? (_realFavPivot - _entryPx) / _realRisk : (_entryPx - _realFavPivot) / _realRisk) : na
        _realAdvPivotR = not na(_realRisk) and _realRisk > 0 and not na(_realAdvPivot) ? (_realLong ? (_entryPx - _realAdvPivot) / _realRisk : (_realAdvPivot - _entryPx) / _realRisk) : na
        _realFavPivotBar = _realLong ? _longFavPivotBarExec : _shortFavPivotBarExec
        _realAdvPivotBar = _realLong ? _longAdvPivotBarExec : _shortAdvPivotBarExec
        _realBarsToFavPivot = not na(_realFavPivotBar) ? _realFavPivotBar - strategy.closedtrades.entry_bar_index(_ct) : na
        _realBarsToAdvPivot = not na(_realAdvPivotBar) ? _realAdvPivotBar - strategy.closedtrades.entry_bar_index(_ct) : na
        array.push(_realDir, _realDirVal)
        array.push(_realFamily, _realFam)
        array.push(_realExit, _exitId)
        array.push(_realProfit, _profit)
        array.push(_realExitR, _realR)
        array.push(_realBars, _barsHeldReal)
        if debugMode
            log.info(str.format('WT3 REAL EXIT | dir={0} | family={1} | exit={2} | trade={3} | barsHeld={4} | pnl={5} | R={6} | entry={7} | exitPx={8} | profile={9} | tf={10} | chartSec={11} | executionMode={12} | MFE={13} | MAE={14} | capturePct={15} | targetPx={16} | targetRoomR={17} | targetRoomATR={18} | targetHit={19} | barsToTarget={20} | favPivotPx={21} | favPivotR={22} | barsToFavPivot={23} | advPivotPx={24} | advPivotR={25} | barsToAdvPivot={26} | entryScore={27} | entryStruct={28}',
                 _realDirVal, _realFam, _exitId, str.tostring(_ct), str.tostring(_barsHeldReal),
                 str.tostring(_profit, '#.##'), str.tostring(_realR, '#.##'),
                 str.tostring(_entryPx, format.mintick), str.tostring(_exitPx, format.mintick), profileMode,
                 timeframe.period, str.tostring(_chartSec), oneHourExecutionMode,
                 str.tostring(_realMfeR, '#.##'), str.tostring(_realMaeR, '#.##'), str.tostring(_realCapturePct, '#.#'),
                 str.tostring(_realTarget, format.mintick), str.tostring(_realTargetRoomR, '#.##'), str.tostring(_realTargetRoomATR, '#.##'),
                 str.tostring(_realTargetHit), str.tostring(_realBarsToTarget),
                 str.tostring(_realFavPivot, format.mintick), str.tostring(_realFavPivotR, '#.##'), str.tostring(_realBarsToFavPivot),
                 str.tostring(_realAdvPivot, format.mintick), str.tostring(_realAdvPivotR, '#.##'), str.tostring(_realBarsToAdvPivot),
                 str.tostring(_realEntryScore, '#.#'), _realEntryStruct))
    _loggedClosedTrades := strategy.closedtrades

if _newLongExec
    _longHighExec := high
    _longLowExec := low
    _longFamilyExec := _pendingLongFamilyExec
    _longEntryStructExec := _pendingLongStructExec
    _longEntryScoreExec := _pendingLongScoreExec
    _longTargetRoomRExec := _pendingLongTargetRoomRExec
    _longTargetRoomATRExec := _pendingLongTargetRoomATRExec
    _longFavPivotExec := na
    _longAdvPivotExec := na
    _longFavPivotBarExec := na
    _longAdvPivotBarExec := na
    _pendingLongFamilyExec := ''
    _pendingLongStructExec := ''
    _pendingLongScoreExec := na
    _pendingLongTargetRoomRExec := na
    _pendingLongTargetRoomATRExec := na
    _longRiskExec := math.max(strategy.position_avg_price - _longSL, syminfo.mintick)
    _longTargetExec := _longTP
    _longEntryBarExec := bar_index
    _longTargetHitExec := not na(_longTargetExec) and high >= _longTargetExec
    _longTargetBarExec := _longTargetHitExec ? bar_index : na
if _newShortExec
    _shortLowExec := low
    _shortHighExec := high
    _shortFamilyExec := _pendingShortFamilyExec
    _shortEntryStructExec := _pendingShortStructExec
    _shortEntryScoreExec := _pendingShortScoreExec
    _shortTargetRoomRExec := _pendingShortTargetRoomRExec
    _shortTargetRoomATRExec := _pendingShortTargetRoomATRExec
    _shortFavPivotExec := na
    _shortAdvPivotExec := na
    _shortFavPivotBarExec := na
    _shortAdvPivotBarExec := na
    _pendingShortFamilyExec := ''
    _pendingShortStructExec := ''
    _pendingShortScoreExec := na
    _pendingShortTargetRoomRExec := na
    _pendingShortTargetRoomATRExec := na
    _shortRiskExec := math.max(_shortSL - strategy.position_avg_price, syminfo.mintick)
    _shortTargetExec := _shortTP
    _shortEntryBarExec := bar_index
    _shortTargetHitExec := not na(_shortTargetExec) and low <= _shortTargetExec
    _shortTargetBarExec := _shortTargetHitExec ? bar_index : na
if _flatExec
    _longHighExec := na
    _longLowExec := na
    _shortLowExec := na
    _shortHighExec := na
    _longRiskExec := na
    _shortRiskExec := na
    _longTargetExec := na
    _shortTargetExec := na
    _longEntryScoreExec := na
    _shortEntryScoreExec := na
    _longTargetRoomRExec := na
    _shortTargetRoomRExec := na
    _longTargetRoomATRExec := na
    _shortTargetRoomATRExec := na
    _longFavPivotExec := na
    _shortFavPivotExec := na
    _longAdvPivotExec := na
    _shortAdvPivotExec := na
    _longEntryBarExec := na
    _shortEntryBarExec := na
    _longTargetBarExec := na
    _shortTargetBarExec := na
    _longFavPivotBarExec := na
    _shortFavPivotBarExec := na
    _longAdvPivotBarExec := na
    _shortAdvPivotBarExec := na
    _longTargetHitExec := false
    _shortTargetHitExec := false
    _longBETriggered := false
    _shortBETriggered := false
    _longFamilyExec := ''
    _shortFamilyExec := ''
    _longEntryStructExec := ''
    _shortEntryStructExec := ''

_longBarsExec = strategy.position_size > 0 and not na(_longEntryBarExec) ? bar_index - _longEntryBarExec : 0
_shortBarsExec = strategy.position_size < 0 and not na(_shortEntryBarExec) ? bar_index - _shortEntryBarExec : 0
_longHighNowExec = strategy.position_size > 0 ? math.max(nz(_longHighExec, high), high) : na
_shortLowNowExec = strategy.position_size < 0 ? math.min(nz(_shortLowExec, low), low) : na
_longLowNowExec = strategy.position_size > 0 ? math.min(nz(_longLowExec, low), low) : na
_shortHighNowExec = strategy.position_size < 0 ? math.max(nz(_shortHighExec, high), high) : na
_longMfeExecR = strategy.position_size > 0 and not na(_longRiskExec) and _longRiskExec > 0 and not na(_longHighNowExec) ? (_longHighNowExec - strategy.position_avg_price) / _longRiskExec : 0.0
_shortMfeExecR = strategy.position_size < 0 and not na(_shortRiskExec) and _shortRiskExec > 0 and not na(_shortLowNowExec) ? (strategy.position_avg_price - _shortLowNowExec) / _shortRiskExec : 0.0
_longCurrentExecR = strategy.position_size > 0 and not na(_longRiskExec) and _longRiskExec > 0 ? (close - strategy.position_avg_price) / _longRiskExec : 0.0
_shortCurrentExecR = strategy.position_size < 0 and not na(_shortRiskExec) and _shortRiskExec > 0 ? (strategy.position_avg_price - close) / _shortRiskExec : 0.0
_longExecEarlyFail = useLowerTfEarlyFailure and _chartSec <= 3600 and strategy.position_size > 0 and (_longFamilyExec == 'rr' or _longFamilyExec == 'er') and _longBarsExec >= lowerTfEarlyFailureBars and _longMfeExecR < lowerTfEarlyFailureMfeR and _longCurrentExecR <= 0
_shortExecEarlyFail = useLowerTfEarlyFailure and _chartSec <= 3600 and strategy.position_size < 0 and (_shortFamilyExec == 'rr' or _shortFamilyExec == 'er') and _shortBarsExec >= lowerTfEarlyFailureBars and _shortMfeExecR < lowerTfEarlyFailureMfeR and _shortCurrentExecR <= 0
_longExecOneHourPBFail = useOneHourPullbackFailure and _chartSec == 3600 and strategy.position_size > 0 and _longFamilyExec == 'pb' and _longBarsExec >= oneHourPullbackFailureBars and _longMfeExecR < oneHourPullbackFailureMfeR and _longCurrentExecR <= 0
_shortExecOneHourPBFail = useOneHourPullbackFailure and _chartSec == 3600 and strategy.position_size < 0 and _shortFamilyExec == 'pb' and _shortBarsExec >= oneHourPullbackFailureBars and _shortMfeExecR < oneHourPullbackFailureMfeR and _shortCurrentExecR <= 0
_longExecProtectR = _longFamilyExec == 'pb' ? pbMfeProtectR : _longFamilyExec == 'er' ? erMfeProtectR : mfeProtectR
_shortExecProtectR = _shortFamilyExec == 'pb' ? pbMfeProtectR : _shortFamilyExec == 'er' ? erMfeProtectR : mfeProtectR
_longExecGivebackR = math.max(givebackFloorR, _longMfeExecR * _givebackKeepPct)
_shortExecGivebackR = math.max(givebackFloorR, _shortMfeExecR * _givebackKeepPct)
_execConfirmedGivebackTfOk = confirmedGivebackTFMode == 'All TF' or (confirmedGivebackTFMode == '4H Only' and _chartSec == 14400) or (confirmedGivebackTFMode == '15m + 4H' and (_chartSec == 900 or _chartSec == 14400))
_longExecTargetTouched = strategy.position_size > 0 and not na(_longTP) and high >= _longTP
_shortExecTargetTouched = strategy.position_size < 0 and not na(_shortTP) and low <= _shortTP
_longExecStructureInvalid = strategy.position_size > 0 and (structureShortOk or (lhDetected and llDetected))
_shortExecStructureInvalid = strategy.position_size < 0 and (structureLongOk or (hhDetected and hlDetected))
_longExecConfirmedPivot = confirmedGivebackUsePivot and strategy.position_size > 0 and _plFound and (bar_index - structPivLen) >= nz(_longEntryBarExec, bar_index) and low[structPivLen] <= strategy.position_avg_price + _longExecGivebackR * _longRiskExec
_shortExecConfirmedPivot = confirmedGivebackUsePivot and strategy.position_size < 0 and _phFound and (bar_index - structPivLen) >= nz(_shortEntryBarExec, bar_index) and high[structPivLen] >= strategy.position_avg_price - _shortExecGivebackR * _shortRiskExec
_longExecConfirmedGiveback = useConfirmedGivebackExit and _execConfirmedGivebackTfOk and strategy.position_size > 0 and _longMfeExecR >= _longExecProtectR and _longCurrentExecR <= _longExecGivebackR and not _longExecTargetTouched and (_longExecConfirmedPivot or (confirmedGivebackUseStructure and _longExecStructureInvalid))
_shortExecConfirmedGiveback = useConfirmedGivebackExit and _execConfirmedGivebackTfOk and strategy.position_size < 0 and _shortMfeExecR >= _shortExecProtectR and _shortCurrentExecR <= _shortExecGivebackR and not _shortExecTargetTouched and (_shortExecConfirmedPivot or (confirmedGivebackUseStructure and _shortExecStructureInvalid))
_longExecOpposite = strategy.position_size > 0 and (shortRRSignal or shortPBSignal or shortERSignal) and _oppShortScore >= oppositeExitMinScore
_shortExecOpposite = strategy.position_size < 0 and (longRRSignal or longPBSignal or longERSignal) and _oppLongScore >= oppositeExitMinScore
_longExecGivebackSignal = strategy.position_size > 0 and _longMfeExecR >= _longExecProtectR and _longCurrentExecR <= _longExecGivebackR
_shortExecGivebackSignal = strategy.position_size < 0 and _shortMfeExecR >= _shortExecProtectR and _shortCurrentExecR <= _shortExecGivebackR
_longExecFamilyWT = strategy.position_size > 0 and _wtCrossDown and ((_longFamilyExec == 'rr' and (_longMfeExecR >= mfeProtectR or wtOsc > 0)) or ((_longFamilyExec == 'pb' or _longFamilyExec == 'er') and (_longMfeExecR >= _longExecProtectR or wtOsc < 0)))
_shortExecFamilyWT = strategy.position_size < 0 and _wtCrossUp and ((_shortFamilyExec == 'rr' and (_shortMfeExecR >= mfeProtectR or wtOsc < 0)) or ((_shortFamilyExec == 'pb' or _shortFamilyExec == 'er') and (_shortMfeExecR >= _shortExecProtectR or wtOsc > 0)))
_longExecTimeFail = strategy.position_size > 0 and _longBarsExec >= maxBarsInTradeA and _longCurrentExecR < minProfitR
_shortExecTimeFail = strategy.position_size < 0 and _shortBarsExec >= maxBarsInTradeA and _shortCurrentExecR < minProfitR
_longExecFollowThr = strategy.position_size > 0 and _longBarsExec == followThroughBarsA and not na(_longHighNowExec) and _longHighNowExec < strategy.position_avg_price + followThroughATR * atrVal
_shortExecFollowThr = strategy.position_size < 0 and _shortBarsExec == followThroughBarsA and not na(_shortLowNowExec) and _shortLowNowExec > strategy.position_avg_price - followThroughATR * atrVal
_longExecMgmtSignal = (useExitOpposite and _longExecOpposite) or (useExitGiveback and _longExecGivebackSignal) or _longExecConfirmedGiveback or (useExitFamilyWT and _longExecFamilyWT) or _longExecEarlyFail or _longExecOneHourPBFail or (useExitTimeFail and _longExecTimeFail) or (useExitFollowThr and _longExecFollowThr)
_shortExecMgmtSignal = (useExitOpposite and _shortExecOpposite) or (useExitGiveback and _shortExecGivebackSignal) or _shortExecConfirmedGiveback or (useExitFamilyWT and _shortExecFamilyWT) or _shortExecEarlyFail or _shortExecOneHourPBFail or (useExitTimeFail and _shortExecTimeFail) or (useExitFollowThr and _shortExecFollowThr)
_oneHourStopSignalMode = _chartSec == 3600 and oneHourExecutionMode == "Stop + Signal Exit"
_longExecLimit = _oneHourStopSignalMode ? na : _longTP
_shortExecLimit = _oneHourStopSignalMode ? na : _shortTP

if strategy.position_size > 0
    _longHighExec := _longHighNowExec
    _longLowExec := _longLowNowExec
    if na(_longFavPivotExec) and _phFound and (bar_index - structPivLen) >= nz(_longEntryBarExec, bar_index)
        _longFavPivotExec := high[structPivLen]
        _longFavPivotBarExec := bar_index - structPivLen
    if na(_longAdvPivotExec) and _plFound and (bar_index - structPivLen) >= nz(_longEntryBarExec, bar_index)
        _longAdvPivotExec := low[structPivLen]
        _longAdvPivotBarExec := bar_index - structPivLen
    if not _longTargetHitExec and not na(_longTargetExec) and high >= _longTargetExec
        _longTargetHitExec := true
        _longTargetBarExec := bar_index
    if enableBE and not na(_longRiskExec) and high >= strategy.position_avg_price + beATRInput * _longRiskExec
        _longBETriggered := true
    _longBEStop = _longBETriggered ? strategy.position_avg_price : _longSL
    _longGivebackStop = useExitGiveback and useStrategyGivebackStop and _longMfeExecR >= _longExecProtectR ? strategy.position_avg_price + _longExecGivebackR * _longRiskExec : _longSL
    strategy.exit("Long Exit", "Long", stop=math.max(_longBEStop, _longGivebackStop), limit=_longExecLimit)
if strategy.position_size < 0
    _shortLowExec := _shortLowNowExec
    _shortHighExec := _shortHighNowExec
    if na(_shortFavPivotExec) and _plFound and (bar_index - structPivLen) >= nz(_shortEntryBarExec, bar_index)
        _shortFavPivotExec := low[structPivLen]
        _shortFavPivotBarExec := bar_index - structPivLen
    if na(_shortAdvPivotExec) and _phFound and (bar_index - structPivLen) >= nz(_shortEntryBarExec, bar_index)
        _shortAdvPivotExec := high[structPivLen]
        _shortAdvPivotBarExec := bar_index - structPivLen
    if not _shortTargetHitExec and not na(_shortTargetExec) and low <= _shortTargetExec
        _shortTargetHitExec := true
        _shortTargetBarExec := bar_index
    if enableBE and not na(_shortRiskExec) and low <= strategy.position_avg_price - beATRInput * _shortRiskExec
        _shortBETriggered := true
    _shortBEStop = _shortBETriggered ? strategy.position_avg_price : _shortSL
    _shortGivebackStop = useExitGiveback and useStrategyGivebackStop and _shortMfeExecR >= _shortExecProtectR ? strategy.position_avg_price - _shortExecGivebackR * _shortRiskExec : _shortSL
    strategy.exit("Short Exit", "Short", stop=math.min(_shortBEStop, _shortGivebackStop), limit=_shortExecLimit)

// ─── Strategy-managed exits (real position state, same exit families) ─────────
_execIndicatorExit = indicatorExitExecMode == "All TF" or (indicatorExitExecMode == "1H Only" and _chartSec == 3600)
_longExecExitAllowed = not indicatorExitNeedsTargetOrStructure or _longExecTargetTouched or _longExecStructureInvalid
_shortExecExitAllowed = not indicatorExitNeedsTargetOrStructure or _shortExecTargetTouched or _shortExecStructureInvalid
_longExecExitSignal = (_execIndicatorExit and ((_longExecMgmtSignal and _longExecExitAllowed) or _longExecOneHourPBFail)) or _longExecConfirmedGiveback
_shortExecExitSignal = (_execIndicatorExit and ((_shortExecMgmtSignal and _shortExecExitAllowed) or _shortExecOneHourPBFail)) or _shortExecConfirmedGiveback

if strategy.position_size > 0 and _longExecExitSignal
    strategy.close("Long",  comment=_longExecConfirmedGiveback ? "Confirmed MFE Exit" : _longExecOneHourPBFail ? "1H PB Fail Exit" : "Indicator Exit")
if strategy.position_size < 0 and _shortExecExitSignal
    strategy.close("Short", comment=_shortExecConfirmedGiveback ? "Confirmed MFE Exit" : _shortExecOneHourPBFail ? "1H PB Fail Exit" : "Indicator Exit")

_canOpenLongNow = strategy.position_size == 0 or (allowSignalReversal and strategy.position_size < 0 and _shortExecExitSignal)
_canOpenShortNow = strategy.position_size == 0 or (allowSignalReversal and strategy.position_size > 0 and _longExecExitSignal)

if longEntrySignal and _canLong and _canEntry and _canOpenLongNow
    _longSL := longStop
    _longTP := longEntryTakeProfit
    _pendingLongFamilyExec := longRRSignal ? 'rr' : longPBSignal ? 'pb' : 'er'
    _pendingLongStructExec := 'hh=' + str.tostring(hhDetected) + ',hl=' + str.tostring(hlDetected) + ',lh=' + str.tostring(lhDetected) + ',ll=' + str.tostring(llDetected) + ',longStruct=' + str.tostring(structureLongOk) + ',shortStruct=' + str.tostring(structureShortOk) + ',structurePhase=' + _playbookStructure + ',playbook=' + _longPlaybook + ',wtBullDiv=' + str.tostring(wtBullDiv) + ',wtBearDiv=' + str.tostring(wtBearDiv) + ',highConfirmBars=' + str.tostring(barsSinceHighPivotConfirm) + ',lowConfirmBars=' + str.tostring(barsSinceLowPivotConfirm) + ',highActualBars=' + str.tostring(barsSinceHighPivotActual) + ',lowActualBars=' + str.tostring(barsSinceLowPivotActual) + ',rangePos=' + str.tostring(setupRangePos, '#.##') + ',target=' + str.tostring(longEntryTakeProfit, format.mintick) + ',stop=' + str.tostring(longStop, format.mintick)
    _pendingLongScoreExec := longRRSignal ? longContextScoreRR : longPBSignal ? longContextScorePB : earlyLongContextScore
    _pendingLongTargetRoomRExec := longTargetRoomR
    _pendingLongTargetRoomATRExec := longTargetRoomATR
    strategy.entry("Long", strategy.long)

if shortEntrySignal and _canShort and _canEntry and _canOpenShortNow
    _shortSL := shortStop
    _shortTP := shortEntryTakeProfit
    _pendingShortFamilyExec := shortRRSignal ? 'rr' : shortPBSignal ? 'pb' : 'er'
    _pendingShortStructExec := 'hh=' + str.tostring(hhDetected) + ',hl=' + str.tostring(hlDetected) + ',lh=' + str.tostring(lhDetected) + ',ll=' + str.tostring(llDetected) + ',longStruct=' + str.tostring(structureLongOk) + ',shortStruct=' + str.tostring(structureShortOk) + ',structurePhase=' + _playbookStructure + ',playbook=' + _shortPlaybook + ',wtBullDiv=' + str.tostring(wtBullDiv) + ',wtBearDiv=' + str.tostring(wtBearDiv) + ',highConfirmBars=' + str.tostring(barsSinceHighPivotConfirm) + ',lowConfirmBars=' + str.tostring(barsSinceLowPivotConfirm) + ',highActualBars=' + str.tostring(barsSinceHighPivotActual) + ',lowActualBars=' + str.tostring(barsSinceLowPivotActual) + ',rangePos=' + str.tostring(setupRangePos, '#.##') + ',target=' + str.tostring(shortEntryTakeProfit, format.mintick) + ',stop=' + str.tostring(shortStop, format.mintick)
    _pendingShortScoreExec := shortRRSignal ? shortContextScoreRR : shortPBSignal ? shortContextScorePB : earlyShortContextScore
    _pendingShortTargetRoomRExec := shortTargetRoomR
    _pendingShortTargetRoomATRExec := shortTargetRoomATR
    strategy.entry("Short", strategy.short)

Overview

Trades are triggered by WaveTrend reversal or pullback continuation signals that pass contextual alignment gates. Position entries use percent of equity sizing, while trade management is handled through bracket exits with break-even adjustments. Rendered in a separate pane below the price chart.

Details

Published by

casoon on GitHub

Draws

Separate pane below the chart

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
Channel Lengthint10chLength
Average Lengthint21avgLength
Signal Lengthint4sigLength
CI Constantfloat0.015ciConst
WT Engine PresetstringCustomwtEnginePreset
Custom Volatility NormstringMean Abs DevnormMode
Custom SmoothingstringEMAsmoothType
JMA Phaseint50jmaPhase
JMA Powerfloat2.0jmaPower
OB Thresholdint56upperBand
OS Thresholdint-56lowerBand
Show Raw Strong WT Cross DiagnosticsbooltrueshowRawStrongWTCross
Show all 40 settings
SettingTypeDefaultVariable
Zone Exit Trigger Windowint4zoneExitWindow
Williams Vix Fix GatebooltrueuseWVF
WVF Periodint22wvfLen
WVF BB Lengthint20wvfBBLen
WVF BB Multiplierfloat2.0wvfBBMult
Auto-Calibrate Time WindowsbooltrueuseAutoTF
Swing Pivot Lengthint5structPivLen
Structure Lookbackint50structLookback
Show Structure LabelsbooltrueshowStructure
Volume MA Lengthint50volMALen
Volume Spike Multiplierfloat1.5volSpikeMult
Use Volume Profile (POC)booltrueuseVolPOC
Local POC Lookbackint100pocLookback
Macro POC Lookbackint300pocMacroLookback
POC Binsint20pocBins
HTF (auto if empty)timeframehtfTimeframe
Anchor HTF (auto if empty)timeframeanchorTimeframe
HTF Score Weightfloat1.0htfWeight
Anchor HTF Score Weightfloat1.0anchorWeight
Hard MTF GatestringNonemtfGateMode
Anchor Score ModestringContrariananchorScoreMode
Auto Lower-TF Trend BiasbooltrueuseAutoTFBias
Bias Min HTF WTfloat20.0autoTFBiasMinWT
Bias Applies TostringER + PBautoTFBiasApplyTo
Require Setup Location + RoombooltrueuseSetupGate
Setup Range Lookbackint120setupRangeLen
Range Edge Percentfloat0.35setupEdgePct
Swing Proximity (ATR×)float1.5swingProximityATR
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

12 stars

This strategy plots on the price chart and looks for three-candle fair value gaps and inverse gaps on Bitcoin. It enters long when a wide enough bullish gap appears in an uptrend with RSI above 30 and volume above its average, and short on the opposite bearish

Strategy v6 SMC / ICT / price actionTrend Repaints
Updated 4 months ago · None stated GitHub repository of BTC 4h FVG/iFVG Strategy

Q-Trend + KVO Strategy

by TradersPost
228 stars

This strategy combines the ATR-banded Q-Trend indicator with the Klinger Volume Oscillator (KVO) to execute confluence trades. It enters long when a Q-Trend buy trigger aligns with a positive KVO value and a bullish candle confirmation, taking short trades und

Strategy v6 TrendVolume
Updated 3 months ago · None stated GitHub repository of Q-Trend + KVO Strategy

BTC MACD Basic Tests

by iamrichardD
57 stars

BTC MACD Basic Tests implements a Pine Script unit-testing framework designed to validate components of a long-only Bitcoin MACD trading system. It constructs structured test cases to evaluate mock functions for MACD crossover calculations, volume threshold ch

Strategy v6 Oscillators & momentumTrend
Updated 1 year ago · Apache-2.0 GitHub repository of BTC MACD Basic Tests
1 stars

This Pine Script strategy calculates the accumulation/distribution line with ta.accdist and its 21-period EMA. It plots these in a separate pane. The strategy issues long entries on ta.crossover of the accumulation/distribution line above its EMA and short ent

Strategy v6 VolumeMoving averages
Updated 7 days ago · Apache-2.0 GitHub repository of PF TA isolate - accdist EMA(21) cross

Altered OBV On MACD

by hasnocool
106 stars

This strategy calculates and plots 9, 21, 50, 90, and 200-period Simple Moving Averages on the main price chart. It constructs a modified On-Balance Volume (OBV) metric and calculates a MACD oscillator based on this custom volume series rather than price. Long

Strategy v5 Moving averagesVolume
Updated 2 years ago · GPL-3.0 GitHub repository of Altered OBV On MACD

More Oscillators & momentum scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

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

Compare brokers