Combines an upgraded WaveTrend oscillator engine with market structure swing pivots, volume profile point of control (POC), and multi-timeframe trend alignment scoring.
//@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.
Multi-mode WaveTrend oscillator with custom smoothing
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Channel Length
int
10
chLength
Average Length
int
21
avgLength
Signal Length
int
4
sigLength
CI Constant
float
0.015
ciConst
WT Engine Preset
string
Custom
wtEnginePreset
Custom Volatility Norm
string
Mean Abs Dev
normMode
Custom Smoothing
string
EMA
smoothType
JMA Phase
int
50
jmaPhase
JMA Power
float
2.0
jmaPower
OB Threshold
int
56
upperBand
OS Threshold
int
-56
lowerBand
Show Raw Strong WT Cross Diagnostics
bool
true
showRawStrongWTCross
Show all 40 settings
Setting
Type
Default
Variable
Zone Exit Trigger Window
int
4
zoneExitWindow
Williams Vix Fix Gate
bool
true
useWVF
WVF Period
int
22
wvfLen
WVF BB Length
int
20
wvfBBLen
WVF BB Multiplier
float
2.0
wvfBBMult
Auto-Calibrate Time Windows
bool
true
useAutoTF
Swing Pivot Length
int
5
structPivLen
Structure Lookback
int
50
structLookback
Show Structure Labels
bool
true
showStructure
Volume MA Length
int
50
volMALen
Volume Spike Multiplier
float
1.5
volSpikeMult
Use Volume Profile (POC)
bool
true
useVolPOC
Local POC Lookback
int
100
pocLookback
Macro POC Lookback
int
300
pocMacroLookback
POC Bins
int
20
pocBins
HTF (auto if empty)
timeframe
htfTimeframe
Anchor HTF (auto if empty)
timeframe
anchorTimeframe
HTF Score Weight
float
1.0
htfWeight
Anchor HTF Score Weight
float
1.0
anchorWeight
Hard MTF Gate
string
None
mtfGateMode
Anchor Score Mode
string
Contrarian
anchorScoreMode
Auto Lower-TF Trend Bias
bool
true
useAutoTFBias
Bias Min HTF WT
float
20.0
autoTFBiasMinWT
Bias Applies To
string
ER + PB
autoTFBiasApplyTo
Require Setup Location + Room
bool
true
useSetupGate
Setup Range Lookback
int
120
setupRangeLen
Range Edge Percent
float
0.35
setupEdgePct
Swing Proximity (ATR×)
float
1.5
swingProximityATR
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This 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
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
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
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
Donald - AI Futures 5M 15M Timeframe is an overlay trading strategy designed for intraday futures trading. It utilizes rational quadratic kernel regression to construct dynamic momentum zones and smoothed price boundaries on the main price chart. The strategy
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