Indicator · Pine Script v6 · Own pane

MTF WaveTrend Opportunity Hunter [WavesUnchained]

by casoon

MTF WaveTrend Opportunity Hunter calculates multi-timeframe momentum and confluence scores using WaveTrend across custom Regime, Setup, Trigger, and Context timeframes.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code1,480
Settings40inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

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

Source Code

1,480 lines of Pine Script v6 from indicators/momentum/mtf_wavetrend_opportunity_hunter/mtf_wavetrend_opportunity_hunter.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

mtf_wavetrend_opportunity_hunter.pine
//@version=6
// ============================================================================
// MTF WaveTrend Opportunity Hunter [WavesUnchained]
// Version: 2.7.0
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// MTF confluence pane around WaveTrend — shows WHY a signal fires (or
// doesn't) over time via a net confluence histogram plus per-layer heat
// ribbons, and fires on a slim three-condition gate instead of stacked
// score thresholds.
//
// Features:
//   ✓ Confluence pane: net score histogram (EMA/WMA/HMA) + raw column
//     layer + heat ribbons for Regime / Opportunity / Timing / Quality /
//     Noise / Context — the "why (no) signal" view over time
//   ✓ Context TF ribbon and dashboard row — weekly (or custom) directional
//     bias shown as sixth ribbon and dashboard row; purely informational,
//     never a gate condition
//   ✓ Confluence Rotation Map — RRG-style quadrant inset (Leading /
//     Improving / Weakening / Lagging) with polyline trail of the net score
//   ✓ Ultimate Smoother core (Ehlers, TASC 04/2024) — optional zero-lag
//     replacement for the EMA core of WaveTrend
//   ✓ Entropy noise floor — Shannon sign-entropy percentile rank as fifth
//     ribbon and Quality component (self-calibrating per instrument)
//   ✓ Entry/exit-decoupled stop-and-reverse: ENTRIES fire on a WT zone-cross
//     only (the entry trigger); an open position is closed EARLY to flat by
//     an opposite turn/exhaustion HOOK (profit-take at the extreme), then flat
//     until the next cross re-enters. A cross reverses outright. Hooks act as
//     exits only, crosses as entries
//   ✓ Non-blocking tradeable flag: opportunity ≥ threshold marks the
//     high-opportunity subset (★, full colour) vs watch (·, dimmed)
//     — the gate no longer hides signals, it only grades them
//   ✓ Live layer panel measures whether the tradeable subset differs from
//     watch signals (Trade✓ vs Watch avg R) on numbers, not by eye
//   ✓ Situation panel: avg R per situational bucket (HTF slope, drift,
//     stretch from the 4H mean, volatility) split Long / Short
//   ✓ Compact on-chart signal labels (dir · ★/· · opportunity, full breakdown
//     in the tooltip) — no fixed TP/SL boxes; the exit is dynamic (the
//     opposite signal), so static targets would misrepresent it
//   ✓ Three-tier MTF framework: Regime (D) → Setup (4H) → Trigger (1H)
//   ✓ Signals fire on any chart TF; sampled-trigger warning when chart ≠
//     Trigger TF
//   ✓ Exit warnings gated to active trades — no noise outside of signals
//   ✓ Per-side dashboard with ARMED / SETUP / NEAR / BIAS / REGIME states
//   ✓ Pivot reference control overlay (ta.pivothigh/low) — markers, dotted
//     pivot→signal connector and bar/ATR distance/proximity/knife flag in
//     the label to review signal accuracy; never gates or triggers a signal
//   ✓ Rich WT oscillator visualization: gradient line color (OS → OB),
//     shadow + momentum fills, OB/OS zone fills, and cross dots for cadence
//
// ============================================================================

indicator("MTF WaveTrend Opportunity Hunter [WavesUnchained]", format=format.price, precision=2, overlay=false, max_labels_count=500, max_lines_count=100, max_boxes_count=100, max_polylines_count=10)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Inputs
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

g_tf = "Timeframes"
tfRegime  = input.timeframe("D",   "Regime TF", group=g_tf)
tfSetup   = input.timeframe("240", "Setup TF", group=g_tf)
tfTrigger = input.timeframe("60",  "Trigger TF", group=g_tf)

g_ctx = "Context TF (Directional Bias — Info Only)"
tfContext       = input.timeframe("W",    "Context TF", group=g_ctx)
showContextLayer = input.bool(true, "Show Context Ribbon + Dashboard Row", group=g_ctx,
     tooltip="Weekly (or custom) directional bias. Never a gate — signals fire regardless of context. Use it to stay aware of the higher-order trend.")

g_wt = "WaveTrend"
wtChLen  = input.int(10, "WT Channel Length", minval=1, group=g_wt)
wtAvgLen = input.int(21, "WT Average Length", minval=1, group=g_wt)
wtSigLen = input.int(4,  "WT Signal Length", minval=1, group=g_wt)
wtExtreme = input.float(55.0, "WT Extreme Level", minval=10, group=g_wt)
useJmaCore = input.bool(true, "EMA + JMA Hybrid Core", group=g_wt,
     tooltip="Keeps EMA as the WaveTrend base and blends in JMA smoothing. More stable than replacing the core with a zero-lag filter.")
jmaCoreBlend = input.float(0.35, "JMA Blend", minval=0.0, maxval=1.0, step=0.05, group=g_wt,
     tooltip="0 = pure EMA, 1 = JMA-smoothed EMA. 0.25-0.45 is usually the useful range for volatile markets.")
useUltimate = input.bool(false, "Ultimate Smoother Core (Ehlers 2024)", group=g_wt,
     tooltip="Replaces the EMA core of WaveTrend with John Ehlers' Ultimate Smoother (TASC 04/2024) — zero lag in the passband, earlier crosses at the same length.")

g_trend = "Trend / Regime"
emaFastLen = input.int(50,  "EMA Fast", minval=1, group=g_trend)
emaSlowLen = input.int(200, "EMA Slow", minval=1, group=g_trend)
adxLen     = input.int(14,  "ADX Length", minval=1, group=g_trend)
adxMin     = input.float(18.0, "Min ADX", minval=1, group=g_trend)

g_jma = "JMA Pullback Smoothing"
jmaLen   = input.int(34,  "JMA Length", minval=1, group=g_jma)
jmaPhase = input.int(50,  "JMA Phase",  minval=-100, maxval=100, group=g_jma)
jmaPower = input.float(2.0, "JMA Power", minval=1.0, maxval=4.0, step=0.25, group=g_jma)

g_quality = "Quality"
erLen      = input.int(20, "Efficiency Ratio Length", minval=2, group=g_quality)
atrLen     = input.int(14, "ATR Length", minval=1, group=g_quality)
entropyLen = input.int(40, "Entropy Window (Trigger TF)", minval=10, group=g_quality,
     tooltip="Shannon sign-entropy over this many Trigger-TF bars. Low entropy = directional tape, high entropy = noise.")
entropyRankLen = input.int(200, "Entropy Rank Lookback", minval=50, group=g_quality,
     tooltip="Percentile-rank window — makes the noise floor self-calibrating per instrument.")
useRS      = input.bool(false, "Use Relative Strength Filter", group=g_quality)
benchmark  = input.symbol("SPY", "Benchmark Symbol", group=g_quality)

g_signal = "Signal Engine"
minRegime      = input.float(65, "Min Regime Score", minval=0, maxval=100, group=g_signal)
minOpportunity = input.float(35, "Min Opportunity Score", minval=0, maxval=100, group=g_signal)
minOppSignal   = input.float(40, "Tradeable Opportunity ≥", minval=0, maxval=100, group=g_signal,
     tooltip="Marks a signal 'tradeable' (★) when its 4H opportunity is at least this. It does NOT block: every WT reversal is drawn so nothing is hidden — this only flags the high-opportunity subset. The Trade✓ / Watch rows of the layer panel measure whether that subset actually differs.")
gateBuffer     = input.float(10, "Gate Buffer", minval=0, maxval=30, group=g_signal,
     tooltip="Softens Regime/Opportunity gates. A value of 10 means a 65 threshold arms from 55 upward; the dashboard still shows the raw score.")
nearSetupBuffer = input.float(10, "Near Setup Buffer", minval=0, maxval=30, group=g_signal,
     tooltip="Marks a near setup when Regime passes and Opportunity is within this many points of the effective Opportunity gate.")
cooldownBars = input.int(8, "Minimum Hold (bars)", minval=1, maxval=30, group=g_signal,
     tooltip="Minimum bars a position is held before the opposite WT trigger may reverse it. Prevents whipsaw flip-flopping when the WT oscillates fast. Higher = holds through minor counter-crosses; lower = reverses sooner.")
wtCrossZone = input.float(0.0, "WT Cross Zone Limit", minval=-40, maxval=40, step=5, group=g_signal,
     tooltip="ENTRY trigger. Long entry only fires when the WT signal line was BELOW this value before the cross (0 = must come from negative territory, 20 = also accept mid-range continuation crosses up to +20). Short mirrors at -value. Higher = more entries incl. momentum continuation in the range; lower = only deep zero-line reversals. Above +~30 starts buying into overbought. Entries are always crosses — the validated entry trigger.")
useHookExit = input.bool(false, "Early Exit on Turn/Exhaustion Hook", group=g_signal,
     tooltip="Decouples exit from entry. Entries fire on a WT CROSS; an open position is then closed EARLY to flat by an opposite TURN or EXHAUSTION hook out of the extreme (a profit-take at the top/bottom), and stays flat until the next cross re-enters. The data showed cross entries carry the edge while turn/exhaustion are good EXITS but bad entries — this pairs the two. Off = pure stop-and-reverse (a position is held until the opposite cross reverses it).")
triggerTurnLevel = input.float(35.0, "WT Turn Level (hook exit)", minval=5, maxval=100, group=g_signal,
     tooltip="Hook-exit sensitivity. A bear hook (closes a long) needs WT above +level and hooking down; a bull hook (closes a short) needs WT below -level and hooking up. Lower = exits earlier on shallower extremes (more, earlier exits).")
turnFallBars = input.int(2, "WT Turn Min Falling Bars", minval=1, maxval=10, group=g_signal,
     tooltip="A hook only counts if WT had been rising (bear hook) / falling (bull hook) for at least this many bars into the extreme before it hooks back. Higher = stricter, fewer false hooks; lower = earlier but noisier. The hook fires on the turn itself, decoupled from the lagging histogram.")
useExhaustion = input.bool(true, "Exhaustion Hook (deep-extreme exit)", group=g_signal,
     tooltip="Adds a second hook-exit path: closes an open position the moment WT1 hooks back from a deep extreme (|WT1| ≥ Exhaustion Level) — the sharp top/bottom. Narrow by design (only at the extremes). Combined with the Turn hook above as the early-exit trigger.")
exhaustLevel = input.float(50.0, "Exhaustion WT Level", minval=20, maxval=80, step=5, group=g_signal,
     tooltip="WT1 must have reached at least this far into overbought / oversold on the prior bar before the hook counts as an exhaustion exit. Higher = only the most extreme tops/bottoms; lower = also milder extremes (more, earlier exits).")

g_mode = "Mode"
allowLong  = input.bool(true, "Allow Long Signals", group=g_mode)
allowShort = input.bool(true, "Allow Short Signals", group=g_mode)

g_rot = "Confluence Rotation Map"
showRotation = input.bool(false, "Show Rotation Map", group=g_rot,
     tooltip="RRG-style quadrant inset right of the last bar: x = net confluence, y = confluence momentum. Leading / Improving / Weakening / Lagging.")
rotMomLen = input.int(8, "Momentum Lookback", minval=1, maxval=30, group=g_rot)
rotSmooth = input.int(5, "Rotation Smoothing", minval=1, maxval=20, group=g_rot,
     tooltip="EMA smoothing of both rotation axes — like real RRGs, the path is meaningless without it.")
rotStep   = input.int(2, "Trail Sampling (bars)", minval=1, maxval=10, group=g_rot)
rotTrail  = input.int(24, "Trail Points", minval=5, maxval=100, group=g_rot)

g_vis = "Visuals"
histSmooth    = input.int(3, "Histogram Smoothing", minval=1, maxval=20, group=g_vis,
     tooltip="Smoothing length applied to the net confluence line. 1 = raw. The smoothed line overlays raw columns.")
histSmoothType = input.string("EMA", "Histogram Smooth Type", options=["EMA", "WMA", "HMA"], group=g_vis,
     tooltip="EMA = classic exponential. WMA = linearly-weighted (faster response). HMA (Hull) = low-lag, nearly no overshoot.")
showRibbons   = input.bool(true, "Show Layer Heat Ribbons", group=g_vis)
showBg        = input.bool(true, "Show Regime Background", group=g_vis)
showDashboard = input.bool(true, "Show Dashboard", group=g_vis)
showStats     = input.bool(true, "Show Signal Stats", group=g_vis,
     tooltip="Live performance panel over the visible chart history — signal count, win rate, average R per closed position, and failed %. Lets you tune the signal knobs against numbers right here on the 1H chart. R is measured vs the fresh entry (re-entries averaged in are ignored).")
showTfWarning = input.bool(false, "Show Lower-TF Sampling Warning", group=g_vis,
     tooltip="Warns when Setup or Trigger TF is below the chart TF. request.security then samples lower-TF values only once per chart bar, so intrabar crosses can be missed.")
labelMode     = input.string("Compact", "Signal Label Detail", options=["Compact", "Full", "None"], group=g_vis,
     tooltip="Compact = a small tag (dir · ★/· tradeable · opportunity); Full = all scores + quality breakdown on the chart; None = no on-chart label (pane triangle only). The full breakdown is always in the label's tooltip on hover.")
showWtShadow  = input.bool(true,  "Show WT Shadow Fill",     group=g_vis,
     tooltip="Gradient fill between WT1 and the zero line — bull tint below, bear tint above. Very transparent so it does not obscure the confluence histogram.")
showWtFill    = input.bool(true,  "Show WT / Signal Fill",   group=g_vis,
     tooltip="Tint between WT1 and WT2 — bull when WT1 > WT2, bear when below. Shows momentum split at a glance.")
showWtZones   = input.bool(true,  "Show WT OB/OS Zone Fill", group=g_vis,
     tooltip="Light zone fills between WT Extreme Level (±55 default) and the hard clamp (±80). Marks the overbought / oversold territory on the oscillator.")
showWtDots    = input.bool(true,  "Show WT Cross Dots",      group=g_vis,
     tooltip="Small dots on the WT1 line at every WT cross. Entry signals show as the larger triangle; all other crosses show as dots. Lets you see oscillator cadence and which crosses did not produce a signal.")

g_fail = "Failed Signals (study)"
markFailed    = input.bool(true, "Mark Failed Signals", group=g_fail,
     tooltip="Recolours a signal's tag once price trades the 'adverse ATR' distance against its entry (where the old stop sat) — so you can see at a glance which entries went wrong.")
failAtrMult   = input.float(1.2, "Failed = adverse ATR×", minval=0.3, maxval=5.0, step=0.1, group=g_fail,
     tooltip="A signal counts as failed once price moves this many ATR against its entry. 1.2 matches the previous fixed stop distance.")
failColor     = input.color(#29b6f6, "Failed Marker Color", group=g_fail,
     tooltip="Colour for failed signal tags and debug labels (light blue by default; orange #ff6f00 is the obvious alternative).")
showFailDebug = input.bool(true, "Failed Debug Label", group=g_fail,
     tooltip="Prints a larger label at each failed signal with debug values — most importantly the count of WT crosses in the window before entry. A high count means the entry sat in a choppy multi-cross zone (the 'too early' pattern), which helps decide how to push the entry later.")
failCrossLookback = input.int(10, "Failed Debug: Cross Lookback (bars)", minval=3, maxval=40, group=g_fail,
     tooltip="Window before the entry over which the debug label counts WT crosses. High count = choppy zone = likely too-early entry.")

g_piv = "Pivot Reference (Control Overlay — Never a Gate)"
showPivots   = input.bool(true, "Show Pivot Markers", group=g_piv,
     tooltip="Confirmed swing highs/lows on the price chart (ta.pivothigh/low). Pure control overlay — never influences which signals fire. Use it to judge how close a signal landed to the actual turn.")
pivLeftBars  = input.int(5, "Pivot Left Bars", minval=1, group=g_piv)
pivRightBars = input.int(5, "Pivot Right Bars", minval=1, group=g_piv,
     tooltip="Bars of confirmation. The marker prints this many bars after the pivot — the same lag the eye needs to call a swing.")
showPivDist  = input.bool(true, "Show Pivot → Signal Distance", group=g_piv,
     tooltip="Dotted connector from the current swing extreme to the entry. Long is measured against the swing low, short against the swing high. Bar/ATR distance and proximity also printed in the signal label — diagnostic only, small distance = signal fired close to the swing.")

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Helpers
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

clamp(x, lo, hi) =>
    math.max(lo, math.min(hi, x))

scoreBool(cond, pts) =>
    cond ? pts : 0.0

f_jma(src, length, phase, power) =>
    var float jma = na
    var float e0  = na
    var float e1  = na
    var float e2  = na
    _pr    = 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 + _pr * 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

// Ehlers Ultimate Smoother (TASC 04/2024) — zero lag in the passband
f_ultimateSmoother(src, period) =>
    f  = 1.414 * math.pi / period
    a1 = math.exp(-f)
    c2 = 2 * a1 * math.cos(f)
    c3 = -a1 * a1
    c1 = (1 - c2 - c3) / 4
    var float us = na
    us := (1 - c1) * src + (2 * c1 - c2) * nz(src[1], src) - (c1 + c3) * nz(src[2], src) + c2 * nz(us[1], src) + c3 * nz(us[2], src)
    us

// EMA remains the stable base. JMA is blended on top; Ultimate can still
// replace the core when explicitly enabled.
f_smoothCore(src, len, useUS, useJma, blend, phase, power) =>
    e = ta.ema(src, len)
    j = f_jma(e, len, phase, power)
    u = f_ultimateSmoother(src, len)
    h = e * (1.0 - blend) + j * blend
    useUS ? u : useJma ? h : e

waveTrend(src, chLen, avgLen, sigLen, useUS, useJma, blend, phase, power) =>
    esa = f_smoothCore(src, chLen, useUS, useJma, blend, phase, power)
    deRaw = f_smoothCore(math.abs(src - esa), chLen, useUS, useJma, blend, phase, power)
    de = math.max(deRaw, 0.0)
    ci = de > 0 ? (src - esa) / (0.015 * de) : 0.0
    wt1 = f_smoothCore(ci, avgLen, useUS, useJma, blend, phase, power)
    wt2 = ta.sma(wt1, sigLen)
    [wt1, wt2, wt1 - wt2]

efficiencyRatio(src, len) =>
    change = math.abs(src - src[len])
    volatility = ta.sma(math.abs(src - src[1]), len) * len
    volatility != 0 ? change / volatility : 0.0

adxValue(len) =>
    [_, _, adx] = ta.dmi(len, len)
    adx

// Shannon sign-entropy: 1.0 = coin flip (noise), → 0 = one-sided tape
f_signEntropy(len) =>
    int ups = 0
    for i = 0 to len - 1
        if close[i] > close[i + 1]
            ups += 1
    p = ups / float(len)
    float h = 1.0
    if p <= 0.0 or p >= 1.0
        h := 0.0
    else
        h := -(p * math.log(p) + (1 - p) * math.log(1 - p)) / math.log(2)
    h

// Hull MA (low-lag, minimal overshoot)
f_hma(src, len) =>
    half   = math.max(len / 2, 1)
    sqLen  = math.max(int(math.sqrt(float(len))), 1)
    ta.wma(2.0 * ta.wma(src, half) - ta.wma(src, len), sqLen)

// Selectable histogram smoother
f_smoothHist(src, len, typ) =>
    if typ == "HMA"
        f_hma(src, len)
    else if typ == "WMA"
        ta.wma(src, len)
    else
        ta.ema(src, len)

// Heat color for a net layer score (-100..+100): green bull, red bear, gray neutral
f_heatColor(net) =>
    color c = color.new(color.gray, 72)
    if not na(net)
        if net >= 15
            c := color.from_gradient(net, 15, 70, color.new(#00c853, 65), color.new(#00c853, 0))
        else if net <= -15
            c := color.from_gradient(net, -70, -15, color.new(#d50000, 0), color.new(#d50000, 65))
    c

// Noise color: green = unusually clean tape, orange = unusually noisy
f_noiseColor(rank) =>
    color c = color.new(color.gray, 72)
    if not na(rank)
        if rank <= 30
            c := color.from_gradient(rank, 0, 30, color.new(#00c853, 0), color.new(#00c853, 65))
        else if rank >= 70
            c := color.from_gradient(rank, 70, 100, color.new(#ff6f00, 65), color.new(#ff6f00, 0))
    c

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// MTF Data
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[dWT1, dWT2, dHist] = request.security(syminfo.tickerid, tfRegime, waveTrend(hlc3, wtChLen, wtAvgLen, wtSigLen, useUltimate, useJmaCore, jmaCoreBlend, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)
dClose   = request.security(syminfo.tickerid, tfRegime, close, barmerge.gaps_off, barmerge.lookahead_off)
dEMA50   = request.security(syminfo.tickerid, tfRegime, ta.ema(close, emaFastLen), barmerge.gaps_off, barmerge.lookahead_off)
dEMA200  = request.security(syminfo.tickerid, tfRegime, ta.ema(close, emaSlowLen), barmerge.gaps_off, barmerge.lookahead_off)
dADX     = request.security(syminfo.tickerid, tfRegime, adxValue(adxLen), barmerge.gaps_off, barmerge.lookahead_off)

[h4WT1, h4WT2, h4Hist] = request.security(syminfo.tickerid, tfSetup, waveTrend(hlc3, wtChLen, wtAvgLen, wtSigLen, useUltimate, useJmaCore, jmaCoreBlend, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)
h4Close = request.security(syminfo.tickerid, tfSetup, close, barmerge.gaps_off, barmerge.lookahead_off)
h4ATR   = request.security(syminfo.tickerid, tfSetup, ta.atr(atrLen), barmerge.gaps_off, barmerge.lookahead_off)
h4JMA   = request.security(syminfo.tickerid, tfSetup, f_jma(close, jmaLen, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)

[h1WT1, h1WT2, h1Hist] = request.security(syminfo.tickerid, tfTrigger, waveTrend(hlc3, wtChLen, wtAvgLen, wtSigLen, useUltimate, useJmaCore, jmaCoreBlend, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)
h1Close = request.security(syminfo.tickerid, tfTrigger, close, barmerge.gaps_off, barmerge.lookahead_off)
h1JMA   = request.security(syminfo.tickerid, tfTrigger, f_jma(close, jmaLen, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)
h1ER    = request.security(syminfo.tickerid, tfTrigger, efficiencyRatio(close, erLen), barmerge.gaps_off, barmerge.lookahead_off)
h1ATR   = request.security(syminfo.tickerid, tfTrigger, ta.atr(atrLen), barmerge.gaps_off, barmerge.lookahead_off)
h1Entropy = request.security(syminfo.tickerid, tfTrigger, f_signEntropy(entropyLen), barmerge.gaps_off, barmerge.lookahead_off)

// Context TF — directional bias only, never a gate
[ctxWT1, ctxWT2, _ctxHist] = request.security(syminfo.tickerid, tfContext, waveTrend(hlc3, wtChLen, wtAvgLen, wtSigLen, useUltimate, useJmaCore, jmaCoreBlend, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)
ctxClose  = request.security(syminfo.tickerid, tfContext, close, barmerge.gaps_off, barmerge.lookahead_off)
ctxEMA50  = request.security(syminfo.tickerid, tfContext, ta.ema(close, emaFastLen), barmerge.gaps_off, barmerge.lookahead_off)
ctxEMA200 = request.security(syminfo.tickerid, tfContext, ta.ema(close, emaSlowLen), barmerge.gaps_off, barmerge.lookahead_off)

benchClose = request.security(benchmark, tfRegime, close, barmerge.gaps_off, barmerge.lookahead_off)
rsLine = useRS and benchClose != 0 ? dClose / benchClose : na
rsSlope = useRS ? ta.linreg(rsLine, 10, 0) - ta.linreg(rsLine, 10, 1) : 0.0

chartTfSec = timeframe.in_seconds(timeframe.period)
setupTfSec = timeframe.in_seconds(tfSetup)
triggerTfSec = timeframe.in_seconds(tfTrigger)
setupTfBelowChart = not na(chartTfSec) and not na(setupTfSec) and setupTfSec < chartTfSec
triggerTfBelowChart = not na(chartTfSec) and not na(triggerTfSec) and triggerTfSec < chartTfSec
tfSamplingWarning = setupTfBelowChart or triggerTfBelowChart
tfSamplingScope = setupTfBelowChart and triggerTfBelowChart ? "Setup + Trigger TF" : setupTfBelowChart ? "Setup TF" : "Trigger TF"
tfSamplingText = "Chart " + timeframe.period + " > " + tfSamplingScope + ": lower-TF data is sampled once per chart bar; intrabar WT crosses/signals can be missed."

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Regime Score
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

trendBull = dClose > dEMA50 and dEMA50 > dEMA200
trendBear = dClose < dEMA50 and dEMA50 < dEMA200

adxOk = dADX >= adxMin

bullRegime =
     scoreBool(dWT1 > dWT2, 25) +
     scoreBool(dWT1 > dWT1[1], 20) +
     scoreBool(trendBull, 30) +
     scoreBool(adxOk, 15) +
     scoreBool(dHist > dHist[1], 10)

bearRegime =
     scoreBool(dWT1 < dWT2, 25) +
     scoreBool(dWT1 < dWT1[1], 20) +
     scoreBool(trendBear, 30) +
     scoreBool(adxOk, 15) +
     scoreBool(dHist < dHist[1], 10)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Opportunity Score
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

h4DistAtr = h4ATR != 0 ? (h4Close - h4JMA) / h4ATR : 0.0

// ── Situation context for the measurement panel (diagnostics only, never gates a signal) ──
// All four are recorded at entry and read RELATIVE to the trade direction, so the buckets stay
// symmetric: a long and a short in the mirrored situation land in the same bucket.
f_jmaSlope(src, length, phase, power) =>
    float j = f_jma(src, length, phase, power)
    j - nz(j[1], j)
h4JmaSlope = request.security(syminfo.tickerid, tfSetup, f_jmaSlope(close, jmaLen, jmaPhase, jmaPower), barmerge.gaps_off, barmerge.lookahead_off)
sitDriftLen   = input.int(100, "Situation: Drift Lookback (bars)", minval=20, maxval=500, group=g_vis,
     tooltip="Measurement only. Net price change over this many chart bars at entry. The Situation panel splits trades into 'with drift' and 'against drift' to show whether the long/short asymmetry just mirrors the market's net direction.")
sitStretchAtr = input.float(1.0, "Situation: Stretch Threshold (4H ATR)", minval=0.2, maxval=5.0, step=0.1, group=g_vis,
     tooltip="Measurement only. Distance of the 4H close from the 4H JMA, in 4H ATR, read relative to the trade: 'Room' = entering from the far side of the mean (long below / short above by at least this), 'Extended' = already this far on the trade's side, 'Near' = in between.")
sitDrift = close - nz(close[sitDriftLen], close)

// 25 at JMA, 100 at 1.5-ATR pullback, 0 from +0.5 ATR adverse extension onward
longPullbackScore  = clamp((-h4DistAtr + 0.5) / 2.0 * 100.0, 0, 100)
shortPullbackScore = clamp((h4DistAtr + 0.5) / 2.0 * 100.0, 0, 100)

longWtExtremeScore =
     clamp((-h4WT1 / wtExtreme) * 100.0, 0, 100)

shortWtExtremeScore =
     clamp((h4WT1 / wtExtreme) * 100.0, 0, 100)

longOpportunity =
     longPullbackScore * 0.45 +
     longWtExtremeScore * 0.35 +
     scoreBool(h4WT1 > h4WT1[1], 20)

shortOpportunity =
     shortPullbackScore * 0.45 +
     shortWtExtremeScore * 0.35 +
     scoreBool(h4WT1 < h4WT1[1], 20)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Timing Score (display context — the gate itself is the WT trigger)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Clamp before cross detection so that detected crosses match the plotted lines.
// Without this, crosses in the clamped "flat zone" (|WT| > 80) fire signals
// with no visible cross in the pane.
plotWT1 = clamp(h1WT1, -80, 80)
plotWT2 = clamp(h1WT2, -80, 80)

h1BullCross = ta.crossover(plotWT1, plotWT2)
h1BearCross = ta.crossunder(plotWT1, plotWT2)
// WT turn = WT1 hooks out of the extreme on the bar it ticks back, after a confirmed
// falling/rising run into the zone. Decoupled from the histogram (hist lags the WT1 low
// by 1-2 bars), so the turn fires at the hook — closer to the actual swing.
h1BullTurnRaw = h1WT1 < -triggerTurnLevel and h1WT1 > h1WT1[1] and h1WT1[1] < h1WT1[1 + turnFallBars]
h1BearTurnRaw = h1WT1 >  triggerTurnLevel and h1WT1 < h1WT1[1] and h1WT1[1] > h1WT1[1 + turnFallBars]
h1BullTurn = h1BullTurnRaw and not h1BullTurnRaw[1]
h1BearTurn = h1BearTurnRaw and not h1BearTurnRaw[1]

// Exhaustion reversal: WT1 was at a deep extreme on the prior bar and hooks back now — the
// sharp top/bottom turn. Narrow (only at |WT1| ≥ exhaustLevel) so it catches the big
// exhaustion reversals a lagging core detects too late, without flooding the mid-range.
h1BearExhaustRaw = useExhaustion and h1WT1[1] >=  exhaustLevel and h1WT1 < h1WT1[1]
h1BullExhaustRaw = useExhaustion and h1WT1[1] <= -exhaustLevel and h1WT1 > h1WT1[1]
h1BearExhaust = h1BearExhaustRaw and not h1BearExhaustRaw[1]
h1BullExhaust = h1BullExhaustRaw and not h1BullExhaustRaw[1]

// Zone filter: cross must emerge from the correct side (WT signal line was below zone for long, above for short)
rawBullCross = h1BullCross and h1WT2[1] < wtCrossZone
rawBearCross = h1BearCross and h1WT2[1] > -wtCrossZone

// ── Entry vs exit triggers (decoupled) ──────────────────────────────────────────────────
// ENTRY = the zone-filtered WT cross only — the entry trigger (cross entries carry the
// edge; turn/exhaustion entries were knife-catches). HOOK = a turn or exhaustion hook out of
// the extreme — repurposed as an early profit-take EXIT (good at the top/bottom, bad as entry).
bullEntryTrig = rawBullCross
bearEntryTrig = rawBearCross
bullHook = useHookExit and (h1BullTurn or h1BullExhaust)   // hook up from a low  → closes a short
bearHook = useHookExit and (h1BearTurn or h1BearExhaust)   // hook down from a high → closes a long
// Any bull/bear WT event — used only for the Timing score + dashboard "WT Trigger" display
h1BullTrigger = rawBullCross or h1BullTurn or h1BullExhaust
h1BearTrigger = rawBearCross or h1BearTurn or h1BearExhaust

// Directional WT extreme over the last few bars — how stretched the zone the cross emerged
// from was. A long entry from a deep oversold (wtLow5 ≪ 0) is a real reversal cross; one from
// the mid-range (wtLow5 near 0) is a shallow continuation cross — the prime fail candidate.
// This is the entry-quality criterion captured at the cross and shown on every signal label.
wtLow5  = ta.lowest(h1WT1, 5)   // most oversold recently  (long entries read this)
wtHigh5 = ta.highest(h1WT1, 5)  // most overbought recently (short entries read this)

h1BullMomentum = h1WT1 > h1WT1[1] and h1Hist > h1Hist[1]
h1BearMomentum = h1WT1 < h1WT1[1] and h1Hist < h1Hist[1]

h1BullStructure = h1Close > h1JMA and h1JMA > h1JMA[1]
h1BearStructure = h1Close < h1JMA and h1JMA < h1JMA[1]

// Setup-TF WT direction (h4WT1 rising/falling) is scored in Opportunity only — not
// repeated here. The three Trigger-TF components are rescaled 35/30/25 → 39/33/28 so
// Timing still tops out at 100.
longTiming =
     scoreBool(h1BullTrigger, 39) +
     scoreBool(h1BullMomentum, 33) +
     scoreBool(h1BullStructure, 28)

shortTiming =
     scoreBool(h1BearTrigger, 39) +
     scoreBool(h1BearMomentum, 33) +
     scoreBool(h1BearStructure, 28)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Quality Score (label diagnostic only — does not block or grade signals)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// JMA slope computed first — used for acceleration and direction-sensitive ER below
jmaSlope5  = ta.linreg(h1JMA, 5,  0) - ta.linreg(h1JMA, 5,  1)
jmaSlope10 = ta.linreg(h1JMA, 10, 0) - ta.linreg(h1JMA, 10, 1)
// Continuous acceleration: how strong is slope5 pulling away from slope10, as a self-
// calibrating z-score (÷ its own 50-bar stdev). Replaces the old binary accel > / < step,
// so a strong momentum thrust at the reversal scores higher than a marginal one.
accelMag    = jmaSlope5 - jmaSlope10
accelStdev  = ta.stdev(accelMag, 50)
accelNorm   = accelStdev > 0 ? accelMag / accelStdev : 0.0
longAccelScore  = clamp(accelNorm,  0, 2) / 2.0 * 15.0
shortAccelScore = clamp(-accelNorm, 0, 2) / 2.0 * 15.0

// Direction-sensitive ER: high efficiency ratio during a counter-trend move is NOT
// evidence of quality for the signal direction. Penalize ER when the 1H JMA trend
// opposes the signal (e.g. strong downtrend → ER penalized for longs, not for shorts).
erScoreRaw   = clamp(h1ER * 100.0, 0, 100)
longERScore  = jmaSlope5 > 0 ? erScoreRaw : erScoreRaw * 0.35
shortERScore = jmaSlope5 < 0 ? erScoreRaw : erScoreRaw * 0.35

// Continuous vol: a gradient that peaks in the healthy ATR band and falls off toward dead
// (too quiet) and wild (too volatile) tape — replaces the old binary 75/35 step.
volSma   = ta.sma(h1ATR, 20)
volRatio = volSma > 0 ? h1ATR / volSma : 1.0
float volScore = 50.0
if volRatio < 0.6
    volScore := 25.0
else if volRatio < 1.0
    volScore := 25.0 + (volRatio - 0.6) / 0.4 * 55.0
else if volRatio <= 1.4
    volScore := 80.0
else if volRatio <= 2.2
    volScore := 80.0 - (volRatio - 1.4) / 0.8 * 45.0
else
    volScore := 30.0

// RS only contributes when enabled. When off (default), its 15% is NOT spent on a flat
// neutral 50 (dead weight that compressed every score toward the middle) — it is handed to
// entropy, which is direction-NEUTRAL. (Not to ER: ER is direction-penalised ×0.35 against
// the trend, so weighting it more would amplify the counter-trend penalty and starve longs
// in a downtrend / shorts in an uptrend. Entropy keeps the reclaim symmetric.)
rsBullScore = scoreBool(rsSlope > 0, 100)
rsBearScore = scoreBool(rsSlope < 0, 100)
erW  = 0.30
entW = useRS ? 0.20 : 0.35
rsW  = useRS ? 0.15 : 0.0

// Entropy noise floor — percentile rank makes it self-calibrating
entropyRank  = ta.percentrank(h1Entropy, entropyRankLen)
entropyScore = 100.0 - entropyRank

longQuality =
     longERScore  * erW +
     volScore * 0.20 +
     rsBullScore * rsW +
     longAccelScore +
     entropyScore * entW

shortQuality =
     shortERScore * erW +
     volScore * 0.20 +
     rsBearScore * rsW +
     shortAccelScore +
     entropyScore * entW

// Weighted quality contributions (sum to Quality) — exposed in the entry label so the
// reworked score is legible on a screenshot: which component carries it, ER vs Vol vs
// Accel vs Entropy (vs RS when enabled).
longQualBreak  = "ER" + str.tostring(longERScore  * erW, "#") + " V" + str.tostring(volScore * 0.20, "#") + " A" + str.tostring(longAccelScore, "#") + " E" + str.tostring(entropyScore * entW, "#") + (useRS ? " R" + str.tostring(rsBullScore * rsW, "#") : "")
shortQualBreak = "ER" + str.tostring(shortERScore * erW, "#") + " V" + str.tostring(volScore * 0.20, "#") + " A" + str.tostring(shortAccelScore, "#") + " E" + str.tostring(entropyScore * entW, "#") + (useRS ? " R" + str.tostring(rsBearScore * rsW, "#") : "")

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Context TF Bias (info only — not a gate)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ctxTrendBull = ctxClose > ctxEMA50 and ctxEMA50 > ctxEMA200
ctxTrendBear = ctxClose < ctxEMA50 and ctxEMA50 < ctxEMA200

ctxBullNet =
     scoreBool(ctxWT1 > ctxWT2, 40) +
     scoreBool(ctxWT1 > 0, 20) +
     scoreBool(ctxTrendBull, 40)

ctxBearNet =
     scoreBool(ctxWT1 < ctxWT2, 40) +
     scoreBool(ctxWT1 < 0, 20) +
     scoreBool(ctxTrendBear, 40)

netContext = float(ctxBullNet) - float(ctxBearNet)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Final / Net Scores
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

longFinal =
     bullRegime * 0.35 +
     longOpportunity * 0.30 +
     longTiming * 0.25 +
     longQuality * 0.10

shortFinal =
     bearRegime * 0.35 +
     shortOpportunity * 0.30 +
     shortTiming * 0.25 +
     shortQuality * 0.10

netRegime  = bullRegime - bearRegime
netOpp     = longOpportunity - shortOpportunity
netTiming  = longTiming - shortTiming
netQuality = longQuality - shortQuality
netFinal   = longFinal - shortFinal

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Pivot Reference — swing extremes, proximity, knife-catch (control overlay only — never a gate)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

riskAtr = ta.atr(atrLen)
sitVolRank = ta.percentrank(riskAtr, 100)  // situation panel: volatility regime at entry

pivHi = ta.pivothigh(high, pivLeftBars, pivRightBars)
pivLo = ta.pivotlow(low,  pivLeftBars, pivRightBars)

// Last *confirmed* pivot — used for the knife-catch test against established structure
var float lastPivLoPrice = na
var float lastPivHiPrice = na
if not na(pivLo)
    lastPivLoPrice := pivLo
if not na(pivHi)
    lastPivHiPrice := pivHi

// Current-leg extremes — the relevant reference for an entry, tracked live so a
// fresh (still-unconfirmed) low/high is used instead of a stale confirmed pivot.
// Down-leg trough resets at a confirmed pivot high; up-leg peak at a confirmed pivot low.
var float legLow     = na
var int   legLowBar  = na
var float legHigh    = na
var int   legHighBar = na
if na(legLow) or not na(pivHi)
    legLow    := low
    legLowBar := bar_index
else if low <= legLow
    legLow    := low
    legLowBar := bar_index
if na(legHigh) or not na(pivLo)
    legHigh    := high
    legHighBar := bar_index
else if high >= legHigh
    legHigh    := high
    legHighBar := bar_index

// Markers at the actual pivot bar (offset back by the right-bar confirmation lag)
plotshape(showPivots ? pivLo : na, title="Pivot Low",  style=shape.triangleup,   location=location.belowbar, offset=-pivRightBars, color=color.new(#26a69a, 0), size=size.tiny, force_overlay=true)
plotshape(showPivots ? pivHi : na, title="Pivot High", style=shape.triangledown, location=location.abovebar, offset=-pivRightBars, color=color.new(#ef5350, 0), size=size.tiny, force_overlay=true)

// Distances: long vs swing low, short vs swing high (R is always ≥ 0 by construction)
longPivBars  = bar_index - legLowBar
shortPivBars = bar_index - legHighBar
longPivR     = riskAtr > 0 ? (close - legLow)  / riskAtr : 0.0
shortPivR    = riskAtr > 0 ? (legHigh - close) / riskAtr : 0.0

// Knife-catch: entering below the last confirmed swing low (long) / above the high (short)
longKnife  = not na(lastPivLoPrice) and close < lastPivLoPrice
shortKnife = not na(lastPivHiPrice) and close > lastPivHiPrice

// Proximity 0..100 (display only): 100 = right at the swing extreme, decays with bars and ATR distance
longProx  = clamp(100.0 - longPivBars  * 4.0 - longPivR  * 30.0, 0, 100)
shortProx = clamp(100.0 - shortPivBars * 4.0 - shortPivR * 30.0, 0, 100)

f_pivDistStr(barsAway, rAway) =>
    str.tostring(barsAway) + "b / " + str.tostring(rAway, "#.0") + "R"

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Signals — stop-and-reverse on every WT trigger; opportunity flags 'tradeable' (no gate)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

minRegimeEff = clamp(minRegime - gateBuffer, 0, 100)
minOpportunityEff = clamp(minOpportunity - gateBuffer, 0, 100)
minOpportunityNear = clamp(minOpportunityEff - nearSetupBuffer, 0, 100)

// Regime dominance — grading only, not a gate
regimeLongDominant  = bullRegime >= minRegimeEff and bullRegime > bearRegime
regimeShortDominant = bearRegime >= minRegimeEff and bearRegime > bullRegime

// Bias: regime-aligned direction (informational — dashboard only)
longBias  = allowLong  and regimeLongDominant
shortBias = allowShort and regimeShortDominant

// Setup: opportunity score alone — no binary structural gate
// The score already captures WT extreme, pullback depth, and 4H momentum direction.
// A higher minOpportunity threshold (default 50) requires a meaningful 4H setup
// without hard-blocking valid signals at turning points.
longSetup  = allowLong  and longOpportunity >= minOpportunityEff
shortSetup = allowShort and shortOpportunity >= minOpportunityEff

longNearSetup  = allowLong  and not longSetup  and longOpportunity >= minOpportunityNear
shortNearSetup = allowShort and not shortSetup and shortOpportunity >= minOpportunityNear

chartIsTriggerTf = chartTfSec == triggerTfSec

// Setup/Near start transitions — must be defined before catchup uses them
longSetupStart      = longSetup      and not longSetup[1]
shortSetupStart     = shortSetup     and not shortSetup[1]
longNearSetupStart  = longNearSetup  and not longNearSetup[1]
shortNearSetupStart = shortNearSetup and not shortNearSetup[1]

// ── Stop-and-reverse alternation ────────────────────────────────────────────
// The exit of a position IS the opposite signal: a long closes any open short and a short
// closes any open long. Signals therefore alternate — every short is eventually faced by a
// long — and the exit is indicator-native (the opposite WT trigger, not an external stop).
// Direction is enforced by the open position, not by any gate: the counter-trigger always
// fires, so the alternation can never get stuck spamming one side. Opportunity (tradeable)
// stays on the labels as conviction info; it no longer blocks a signal.
var bool  inLong  = false
var bool  inShort = false
var int   posBar  = -999
var float longEntryPx  = na
var float shortEntryPx = na
// Per-position state for failed-signal marking + stats (captured at the fresh entry)
var float longOpenPx   = na
var float shortOpenPx  = na
var float longOpenAtr  = na
var float shortOpenAtr = na
var float longSLlvl   = na
var float shortSLlvl  = na
var int   longOpenBar  = na
var int   shortOpenBar = na
var float longCrossCtx  = na
var float shortCrossCtx = na
var bool  longWasWith   = false
var bool  shortWasWith  = false
var bool  longOppStrong  = false
var bool  shortOppStrong = false
var bool  longWtDeep   = false  // entry came off a deep WT extreme (oversold ≤ −60 / overbought ≥ 60)
var bool  shortWtDeep  = false
var float longEntryDepth  = na  // the directional WT extreme the entry cross emerged from
var float shortEntryDepth = na
var bool  longExitHookWas  = false  // last long closed via a hook-exit (true) vs a cross-reverse (false)
var bool  longSitHtfWith   = false  // 4H JMA slope pointed in the trade direction at entry
var bool  shortSitHtfWith  = false
var bool  longSitDriftWith  = false // net drift over sitDriftLen pointed in the trade direction
var bool  shortSitDriftWith = false
var int   longSitStretch   = 0      // +1 room (from the far side of the 4H mean), 0 near, -1 extended
var int   shortSitStretch  = 0
var bool  longSitVolHigh   = false  // ATR percentile ≥ 50 at entry
var bool  shortSitVolHigh  = false
var bool  shortExitHookWas = false
var bool  longFailed  = false
var bool  shortFailed = false
var label longPosLbl  = na
var label shortPosLbl = na
barsHeld = bar_index - posBar
held = barsHeld >= cooldownBars   // minimum bars held before a reverse

// Non-blocking tradeable flag: opportunity ≥ minOppSignal marks the high-opportunity subset.
// It does NOT block — every WT reversal still fires. Whether the subset separates is measured
// in the layer panel (calibration record: it did not on the full NatGas sample).
longTradeable  = longOpportunity  >= minOppSignal
shortTradeable = shortOpportunity >= minOppSignal

// ── Entry/exit decoupled stop-and-reverse ────────────────────────────────────────────────
// ENTRIES fire on a WT CROSS only (the entry trigger). An open position is closed
// EARLY to flat by an opposite TURN/EXHAUSTION HOOK (profit-take at the extreme), then stays
// flat until the next cross re-enters. A cross in the opposite direction reverses outright.
// The minimum hold gates exits/reverses only — a flat entry fires on any cross immediately,
// so an exit-at-the-top can re-enter short on the very next confirmed cross.
flat = not inLong and not inShort
// A WT cross ARMS a pending entry (raw cross, gated by direction + minimum hold)
longEntrySig  = allowLong  and bullEntryTrig and not inLong  and (flat or held)
shortEntrySig = allowShort and bearEntryTrig and not inShort and (flat or held)
// Ambiguous double-cross bar → take neither
if longEntrySig and shortEntrySig
    longEntrySig  := false
    shortEntrySig := false

// Hook exit to flat — only when not simultaneously reversed by an opposite confirmed entry
longHookExit  = inLong  and held and bearHook and not shortEntrySig
shortHookExit = inShort and held and bullHook and not longEntrySig

// Every entry opens a fresh position (no same-direction re-entry)
longFresh  = longEntrySig
shortFresh = shortEntrySig
// WT cross density in the window before the bar — high = choppy multi-cross zone (too-early risk)
crossCtxNow = math.sum((h1BullCross or h1BearCross) ? 1.0 : 0.0, failCrossLookback)

if longEntrySig
    longOpenPx    := close
    longOpenAtr   := riskAtr
    longSLlvl     := close - failAtrMult * riskAtr
    longOpenBar   := bar_index
    longCrossCtx  := crossCtxNow
    longWasWith   := regimeLongDominant
    longOppStrong := longTradeable
    longEntryDepth := wtLow5
    longWtDeep    := wtLow5 <= -60
    longSitHtfWith   := h4JmaSlope > 0
    longSitDriftWith := sitDrift > 0
    longSitStretch   := h4DistAtr <= -sitStretchAtr ? 1 : h4DistAtr >= sitStretchAtr ? -1 : 0
    longSitVolHigh   := sitVolRank >= 50
    longFailed    := false
    inLong  := true
    inShort := false
    posBar  := bar_index
    longEntryPx := close
if shortEntrySig
    shortOpenPx    := close
    shortOpenAtr   := riskAtr
    shortSLlvl     := close + failAtrMult * riskAtr
    shortOpenBar   := bar_index
    shortCrossCtx  := crossCtxNow
    shortWasWith   := regimeShortDominant
    shortOppStrong := shortTradeable
    shortEntryDepth := wtHigh5
    shortWtDeep    := wtHigh5 >= 60
    shortSitHtfWith   := h4JmaSlope < 0
    shortSitDriftWith := sitDrift < 0
    shortSitStretch   := h4DistAtr >= sitStretchAtr ? 1 : h4DistAtr <= -sitStretchAtr ? -1 : 0
    shortSitVolHigh   := sitVolRank >= 50
    shortFailed    := false
    inShort := true
    inLong  := false
    posBar  := bar_index
    shortEntryPx := close
// Hook exits flip the position to flat (after the entry/reverse logic above has run)
if longHookExit
    inLong := false
if shortHookExit
    inShort := false

// Entry signals fire on any chart TF; sampled flag is informational only
longSignal  = longEntrySig
shortSignal = shortEntrySig
sampledLongSignal  = longEntrySig  and not chartIsTriggerTf
sampledShortSignal = shortEntrySig and not chartIsTriggerTf

longNearTip  ="Long Near Setup\nOpportunity: " + str.tostring(longOpportunity, "#.0") + "  (threshold: " + str.tostring(minOpportunityNear, "#.0") + ")\nRegime: " + str.tostring(bullRegime, "#.0") + " vs " + str.tostring(bearRegime, "#.0")
shortNearTip = "Short Near Setup\nOpportunity: " + str.tostring(shortOpportunity, "#.0") + "  (threshold: " + str.tostring(minOpportunityNear, "#.0") + ")\nRegime: " + str.tostring(bearRegime, "#.0") + " vs " + str.tostring(bullRegime, "#.0")
longSetupTip  = "Long Setup — waiting for WT bull cross\nOpportunity: " + str.tostring(longOpportunity, "#.0") + "  Regime: " + str.tostring(bullRegime, "#.0") + " vs " + str.tostring(bearRegime, "#.0")
shortSetupTip = "Short Setup — waiting for WT bear cross\nOpportunity: " + str.tostring(shortOpportunity, "#.0") + "  Regime: " + str.tostring(bearRegime, "#.0") + " vs " + str.tostring(bullRegime, "#.0")

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Confluence Rotation (RRG-inspired): x = net confluence, y = its momentum
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Both axes EMA-smoothed — like real RRGs, the raw path is unreadable noise
rotXSm = ta.ema(netFinal, rotSmooth)
rotMomRaw = rotXSm - nz(rotXSm[rotMomLen], rotXSm)
rotYSm = ta.ema(rotMomRaw, rotSmooth)
rotX = clamp(rotXSm, -100, 100)
rotY = clamp(rotYSm * 2.5, -100, 100)

string rotQuad = "LAGGING"
color rotQuadColor = color.new(#d50000, 0)
if rotX >= 0 and rotY >= 0
    rotQuad := "LEADING"
    rotQuadColor := color.new(#00c853, 0)
else if rotX >= 0
    rotQuad := "WEAKENING"
    rotQuadColor := color.new(#ff6f00, 0)
else if rotY >= 0
    rotQuad := "IMPROVING"
    rotQuadColor := color.new(#ffd600, 0)

var array<float> rotXs = array.new_float()
var array<float> rotYs = array.new_float()
if barstate.isconfirmed and bar_index % rotStep == 0
    array.push(rotXs, rotX)
    array.push(rotYs, rotY)
    if array.size(rotXs) > rotTrail
        array.shift(rotXs)
        array.shift(rotYs)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Position exits — the opposite signal closes a trade (stop-and-reverse)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// A position closes when it was open at bar start and is no longer open — covers BOTH a
// cross-reverse and an early hook-exit to flat. The flip is resolved in the signal section
// above; here we derive the close events for stats, markers and alerts.
longExit  = inLong[1]  and not inLong
shortExit = inShort[1] and not inShort
// Record HOW it closed so the layer test can compare hook-exit R vs cross-reverse R
if longExit
    longExitHookWas := longHookExit
if shortExit
    shortExitHookWas := shortHookExit

// Live stats over the loaded history, split Long / Short — measure the config (win rate /
// avg R / failed %) on the chart, so the knobs can be tuned against numbers and the long vs
// short asymmetry is visible. R = (exit−entry)/ATR-at-entry vs the fresh open (re-entry
// averaging ignored — a slight approximation).
var int   statNL = 0
var int   statWinsL = 0
var float statSumRL = 0.0
var int   statFailsL = 0
var int   statNS = 0
var int   statWinsS = 0
var float statSumRS = 0.0
var int   statFailsS = 0
// Layer-test buckets (both directions): does Regime alignment (with-trend vs counter-trend)
// or Setup strength (opportunity ≥ 40 vs below) discriminate avg R? If a bucket separates,
// that layer is a real filter; if not, it is dead weight.
var int   statN_w = 0   // with-trend
var float statR_w = 0.0
var int   statN_c = 0   // counter-trend
var float statR_c = 0.0
var int   statN_s = 0   // strong setup (tradeable)
var float statR_s = 0.0
var int   statN_k = 0   // weak setup (watch)
var float statR_k = 0.0
// Exit-type buckets — validates the decoupling: does an early HOOK exit (profit-take at the
// extreme) actually beat a CROSS reverse exit? And does the depth of the extreme the ENTRY
// came off (≥60 vs shallow) matter?
var int   statN_hk = 0  // closed via hook exit (to flat)
var float statR_hk = 0.0
var int   statN_rv = 0  // closed via opposite-cross reverse
var float statR_rv = 0.0
var int   statN_dp = 0  // deep WT extreme (≥60)
var float statR_dp = 0.0
var int   statN_sh = 0  // shallow extreme
var float statR_sh = 0.0
// Situation buckets, kept PER DIRECTION (index = bucket * 2 + dir, dir 0 = long, 1 = short).
// Pooling long and short would hide exactly the asymmetry these buckets are meant to explain.
// Buckets: 0 HTF with, 1 HTF against, 2 drift with, 3 drift against, 4 room, 5 near,
// 6 extended, 7 vola high, 8 vola low
var array<int>   sitN = array.new_int(18, 0)
var array<float> sitR = array.new_float(18, 0.0)
f_sitAdd(int bucket, int dir, float rr) =>
    int i = bucket * 2 + dir
    sitN.set(i, sitN.get(i) + 1)
    sitR.set(i, sitR.get(i) + rr)
f_sitRecord(int dir, bool htfWith, bool driftWith, int stretch, bool volHigh, float rr) =>
    f_sitAdd(htfWith ? 0 : 1, dir, rr)
    f_sitAdd(driftWith ? 2 : 3, dir, rr)
    f_sitAdd(stretch == 1 ? 4 : stretch == 0 ? 5 : 6, dir, rr)
    f_sitAdd(volHigh ? 7 : 8, dir, rr)
if longExit and not na(longOpenPx) and longOpenAtr > 0
    float rr = (close - longOpenPx) / longOpenAtr
    statNL    += 1
    statSumRL += rr
    statWinsL += rr > 0 ? 1 : 0
    statFailsL += longFailed ? 1 : 0
    statN_w := statN_w + (longWasWith ? 1 : 0)
    statR_w := statR_w + (longWasWith ? rr : 0.0)
    statN_c := statN_c + (longWasWith ? 0 : 1)
    statR_c := statR_c + (longWasWith ? 0.0 : rr)
    statN_s := statN_s + (longOppStrong ? 1 : 0)
    statR_s := statR_s + (longOppStrong ? rr : 0.0)
    statN_k := statN_k + (longOppStrong ? 0 : 1)
    statR_k := statR_k + (longOppStrong ? 0.0 : rr)
    statN_hk := statN_hk + (longExitHookWas ? 1 : 0)
    statR_hk := statR_hk + (longExitHookWas ? rr : 0.0)
    statN_rv := statN_rv + (longExitHookWas ? 0 : 1)
    statR_rv := statR_rv + (longExitHookWas ? 0.0 : rr)
    statN_dp := statN_dp + (longWtDeep ? 1 : 0)
    statR_dp := statR_dp + (longWtDeep ? rr : 0.0)
    statN_sh := statN_sh + (longWtDeep ? 0 : 1)
    statR_sh := statR_sh + (longWtDeep ? 0.0 : rr)
    f_sitRecord(0, longSitHtfWith, longSitDriftWith, longSitStretch, longSitVolHigh, rr)
if shortExit and not na(shortOpenPx) and shortOpenAtr > 0
    float rr = (shortOpenPx - close) / shortOpenAtr
    statNS    += 1
    statSumRS += rr
    statWinsS += rr > 0 ? 1 : 0
    statFailsS += shortFailed ? 1 : 0
    statN_w := statN_w + (shortWasWith ? 1 : 0)
    statR_w := statR_w + (shortWasWith ? rr : 0.0)
    statN_c := statN_c + (shortWasWith ? 0 : 1)
    statR_c := statR_c + (shortWasWith ? 0.0 : rr)
    statN_s := statN_s + (shortOppStrong ? 1 : 0)
    statR_s := statR_s + (shortOppStrong ? rr : 0.0)
    statN_k := statN_k + (shortOppStrong ? 0 : 1)
    statR_k := statR_k + (shortOppStrong ? 0.0 : rr)
    statN_hk := statN_hk + (shortExitHookWas ? 1 : 0)
    statR_hk := statR_hk + (shortExitHookWas ? rr : 0.0)
    statN_rv := statN_rv + (shortExitHookWas ? 0 : 1)
    statR_rv := statR_rv + (shortExitHookWas ? 0.0 : rr)
    statN_dp := statN_dp + (shortWtDeep ? 1 : 0)
    statR_dp := statR_dp + (shortWtDeep ? rr : 0.0)
    statN_sh := statN_sh + (shortWtDeep ? 0 : 1)
    statR_sh := statR_sh + (shortWtDeep ? 0.0 : rr)
    f_sitRecord(1, shortSitHtfWith, shortSitDriftWith, shortSitStretch, shortSitVolHigh, rr)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Signal labels on the price chart (compact / full / none; full detail in the tooltip)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

if longSignal and labelMode != "None"
    string longTag  = longTradeable ? "★" : "·"   // ★ = tradeable (opp ≥ threshold), · = watch
    string longFull = "LONG " + longTag + (longKnife ? " ⚠K" : "") + "\ncross from WT " + str.tostring(longEntryDepth, "#") + (longWtDeep ? " (deep)" : " (shallow)") + "\nΔLow " + f_pivDistStr(longPivBars, longPivR) + " · P" + str.tostring(longProx, "#") + "\nO" + str.tostring(longOpportunity, "#") + " Rg" + str.tostring(bullRegime, "#") + " T" + str.tostring(longTiming, "#") + " Q" + str.tostring(longQuality, "#") + "\n" + longQualBreak
    string longTxt  = labelMode == "Full" ? longFull : "L" + longTag + (longKnife ? "⚠" : "") + " O" + str.tostring(longOpportunity, "#") + " z" + str.tostring(longEntryDepth, "#")
    label ll = label.new(bar_index, low, longTxt, style=label.style_label_up, color=color.new(#00c853, longTradeable ? 0 : 45), textcolor=color.white, size=labelMode == "Full" ? size.small : size.tiny, yloc=yloc.belowbar, force_overlay=true, tooltip=longFull)
    if longFresh
        longPosLbl := ll
if shortSignal and labelMode != "None"
    string shortTag  = shortTradeable ? "★" : "·"
    string shortFull = "SHORT " + shortTag + (shortKnife ? " ⚠K" : "") + "\ncross from WT +" + str.tostring(shortEntryDepth, "#") + (shortWtDeep ? " (deep)" : " (shallow)") + "\nΔHigh " + f_pivDistStr(shortPivBars, shortPivR) + " · P" + str.tostring(shortProx, "#") + "\nO" + str.tostring(shortOpportunity, "#") + " Rg" + str.tostring(bearRegime, "#") + " T" + str.tostring(shortTiming, "#") + " Q" + str.tostring(shortQuality, "#") + "\n" + shortQualBreak
    string shortTxt  = labelMode == "Full" ? shortFull : "S" + shortTag + (shortKnife ? "⚠" : "") + " O" + str.tostring(shortOpportunity, "#") + " z" + str.tostring(shortEntryDepth, "#")
    label sl = label.new(bar_index, high, shortTxt, style=label.style_label_down, color=color.new(#d50000, shortTradeable ? 0 : 45), textcolor=color.white, size=labelMode == "Full" ? size.small : size.tiny, yloc=yloc.abovebar, force_overlay=true, tooltip=shortFull)
    if shortFresh
        shortPosLbl := sl

// Hook-exit-to-flat markers — a small neutral tag where a position was closed early on a hook
// (profit-take at the extreme) rather than reversed by a cross. Shows the flat periods.
if longHookExit and labelMode != "None"
    label.new(bar_index, high, "✕", style=label.style_label_down, color=color.new(color.gray, 25), textcolor=color.white, size=size.tiny, yloc=yloc.abovebar, force_overlay=true, tooltip="Long closed early on a bear hook (profit-take at the top) → flat until next cross")
if shortHookExit and labelMode != "None"
    label.new(bar_index, low, "✕", style=label.style_label_up, color=color.new(color.gray, 25), textcolor=color.white, size=size.tiny, yloc=yloc.belowbar, force_overlay=true, tooltip="Short closed early on a bull hook (profit-take at the bottom) → flat until next cross")

// ── Failed-signal marking: recolour the entry tag + debug label once price trades the
//    adverse-ATR distance against the open (where the old stop sat). Studies early entries.
//    The entry bar is skipped: its low/high formed before the close entry. ──
if markFailed and inLong and not longFailed and not na(longSLlvl) and bar_index > longOpenBar and low <= longSLlvl
    longFailed := true
    if not na(longPosLbl)
        label.set_color(longPosLbl, failColor)
    if showFailDebug
        int heldBars = bar_index - longOpenBar
        string dbg = "FAILED LONG " + (longOppStrong ? "★" : "·") + "\ncross from WT " + str.tostring(longEntryDepth, "#") + (longWtDeep ? " (deep)" : " (SHALLOW)") + "\nx" + str.tostring(longCrossCtx, "#") + " crosses in " + str.tostring(failCrossLookback) + "b before entry\nheld " + str.tostring(heldBars) + "b → -" + str.tostring(failAtrMult, "#.0") + "R\nentry " + str.tostring(longEntryPx, format.mintick)
        label.new(bar_index, low, dbg, style=label.style_label_up, color=failColor, textcolor=color.white, size=size.small, yloc=yloc.belowbar, force_overlay=true)
if markFailed and inShort and not shortFailed and not na(shortSLlvl) and bar_index > shortOpenBar and high >= shortSLlvl
    shortFailed := true
    if not na(shortPosLbl)
        label.set_color(shortPosLbl, failColor)
    if showFailDebug
        int heldBars = bar_index - shortOpenBar
        string dbg = "FAILED SHORT " + (shortOppStrong ? "★" : "·") + "\ncross from WT +" + str.tostring(shortEntryDepth, "#") + (shortWtDeep ? " (deep)" : " (SHALLOW)") + "\nx" + str.tostring(shortCrossCtx, "#") + " crosses in " + str.tostring(failCrossLookback) + "b before entry\nheld " + str.tostring(heldBars) + "b → -" + str.tostring(failAtrMult, "#.0") + "R\nentry " + str.tostring(shortEntryPx, format.mintick)
        label.new(bar_index, high, dbg, style=label.style_label_down, color=failColor, textcolor=color.white, size=size.small, yloc=yloc.abovebar, force_overlay=true)

// Pivot → signal connectors: visualise how far the entry sits from the swing extreme
if showPivDist and longSignal and not na(legLowBar)
    line.new(legLowBar, legLow, bar_index, close, color=color.new(#26a69a, 30), style=line.style_dotted, force_overlay=true)
if showPivDist and shortSignal and not na(legHighBar)
    line.new(legHighBar, legHigh, bar_index, close, color=color.new(#ef5350, 30), style=line.style_dotted, force_overlay=true)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Pane Plots — net confluence + layer heat ribbons
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// Require 3 consecutive bars — a regime hovering around the threshold
// otherwise paints flickering single-bar stripes
bullBgOk = bullRegime >= minRegimeEff and bullRegime >= bearRegime
bearBgOk = bearRegime >= minRegimeEff and bearRegime > bullRegime
color bgColor = na
if showBg and bullBgOk and bullBgOk[1] and bullBgOk[2]
    bgColor := color.new(color.green, 92)
else if showBg and bearBgOk and bearBgOk[1] and bearBgOk[2]
    bgColor := color.new(color.red, 92)
bgcolor(bgColor)


// Raw columns as the base layer — shows unsmoothed bar-by-bar signal
plot(netFinal, "Net Confluence Raw", color=netFinal >= 0 ? color.new(#00c853, 78) : color.new(#d50000, 78), style=plot.style_columns)

// Smoothed area + line on top — trend direction and momentum at a glance
netFinalSm = f_smoothHist(netFinal, histSmooth, histSmoothType)
plot(netFinalSm, "Net Confluence", color=netFinalSm >= 0 ? color.new(#00c853, 60) : color.new(#d50000, 60), style=plot.style_area)
plot(netFinalSm, "Net Confluence Line", color=netFinalSm >= 0 ? color.new(#00c853, 0) : color.new(#d50000, 0), linewidth=2)

hline(0, "Zero", color=color.new(color.gray, 50))
hline(60, "Strong Long Confluence", color=color.new(#00c853, 70), linestyle=hline.style_dotted)
hline(-60, "Strong Short Confluence", color=color.new(#d50000, 70), linestyle=hline.style_dotted)

// WT OB/OS zone hlines + fills — at ±wtExtreme (scoring threshold) and ±80 (hard clamp)
hOB2 = hline( 80,          "WT OB Hard", color=color.new(#ef5350, 45), linestyle=hline.style_solid,  linewidth=1)
hOB1 = hline(wtExtreme,    "WT OB",      color=color.new(#ef5350, 65), linestyle=hline.style_dashed, linewidth=1)
hOS1 = hline(-wtExtreme,   "WT OS",      color=color.new(#26a69a, 65), linestyle=hline.style_dashed, linewidth=1)
hOS2 = hline(-80,          "WT OS Hard", color=color.new(#26a69a, 45), linestyle=hline.style_solid,  linewidth=1)
fill(hOB1, hOB2, color=showWtZones ? color.new(#ef5350, 92) : na, title="WT OB Zone")
fill(hOS2, hOS1, color=showWtZones ? color.new(#26a69a, 92) : na, title="WT OS Zone")

// Gradient WT lines: teal at OS → red at OB
color wtCol1 = color.from_gradient(plotWT1, -wtExtreme, wtExtreme, #26a69a, #ef5350)
color wtCol2 = color.from_gradient(plotWT2, -wtExtreme, wtExtreme, color.new(#26a69a, 40), color.new(#ef5350, 40))
pZeroWt = plot(0.0, display=display.none)
pWT2    = plot(plotWT2, "Trigger WT2",  color=wtCol2, linewidth=1)
pWT1    = plot(plotWT1, "Trigger WT1",  color=wtCol1, linewidth=2)

// Shadow fill: fades from near-opaque at the extreme level toward transparent at zero
fill(pWT1, pZeroWt, wtExtreme, 0.0,
     color.new(#ef5350, 78), color.new(#ef5350, 100),
     title="WT Bear Shadow", display=showWtShadow ? display.all : display.none)
fill(pWT1, pZeroWt, 0.0, -wtExtreme,
     color.new(#26a69a, 100), color.new(#26a69a, 78),
     title="WT Bull Shadow", display=showWtShadow ? display.all : display.none)

// Momentum fill between WT1 and WT2
fill(pWT1, pWT2,
     color=showWtFill ? (plotWT1 >= plotWT2 ? color.new(#26a69a, 88) : color.new(#ef5350, 88)) : na,
     title="WT Momentum Fill")

plot(triggerTurnLevel, "WT Turn Level +", color=color.new(#ff6f00, 75), linewidth=1, style=plot.style_linebr)
plot(-triggerTurnLevel, "WT Turn Level -", color=color.new(#00c853, 75), linewidth=1, style=plot.style_linebr)

plot(showRibbons ? 92.0 : na,  "Regime Ribbon",      color=f_heatColor(netRegime),    linewidth=6, style=plot.style_line)
plot(showRibbons ? 102.0 : na, "Opportunity Ribbon", color=f_heatColor(netOpp),        linewidth=6, style=plot.style_line)
plot(showRibbons ? 112.0 : na, "Timing Ribbon",      color=f_heatColor(netTiming),     linewidth=6, style=plot.style_line)
plot(showRibbons ? 122.0 : na, "Quality Ribbon",     color=f_heatColor(netQuality),    linewidth=6, style=plot.style_line)
plot(showRibbons ? 132.0 : na, "Noise Ribbon",       color=f_noiseColor(entropyRank),  linewidth=6, style=plot.style_line)
plot(showRibbons and showContextLayer ? 142.0 : na, "Context Ribbon", color=f_heatColor(netContext), linewidth=6, style=plot.style_line)

var label ribR = na
var label ribO = na
var label ribT = na
var label ribQ = na
var label ribN = na
var label ribC = na
if showRibbons and barstate.islast
    if na(ribR)
        ribR := label.new(bar_index, 92,  "Regime",      style=label.style_label_left, color=color.new(color.white, 100), textcolor=color.new(color.gray, 25), size=size.small)
        ribO := label.new(bar_index, 102, "Opportunity", style=label.style_label_left, color=color.new(color.white, 100), textcolor=color.new(color.gray, 25), size=size.small)
        ribT := label.new(bar_index, 112, "Timing",      style=label.style_label_left, color=color.new(color.white, 100), textcolor=color.new(color.gray, 25), size=size.small)
        ribQ := label.new(bar_index, 122, "Quality",     style=label.style_label_left, color=color.new(color.white, 100), textcolor=color.new(color.gray, 25), size=size.small)
        ribN := label.new(bar_index, 132, "Noise",       style=label.style_label_left, color=color.new(color.white, 100), textcolor=color.new(color.gray, 25), size=size.small)
        ribC := label.new(bar_index, 142, "Context",     style=label.style_label_left, color=color.new(color.white, 100), textcolor=color.new(color.gray, 25), size=size.small)
    label.set_x(ribR, bar_index + 2)
    label.set_x(ribO, bar_index + 2)
    label.set_x(ribT, bar_index + 2)
    label.set_x(ribQ, bar_index + 2)
    label.set_x(ribN, bar_index + 2)
    label.set_x(ribC, bar_index + 2)

// Setup markers — only shown when trigger-TF WT is already in the correct zone.
// This ensures "LS" only appears when WT is oversold and "SS" when overbought,
// matching what the user sees in the pane rather than firing on 4H-driven state changes.
longSetupWtOk  = plotWT1 < wtCrossZone
shortSetupWtOk = plotWT1 > -wtCrossZone
if longNearSetupStart and longSetupWtOk
    label.new(bar_index, low, "LN", style=label.style_label_up, color=color.new(#00c853, 35), textcolor=color.white, size=size.tiny, yloc=yloc.belowbar, force_overlay=true, tooltip=longNearTip)
if shortNearSetupStart and shortSetupWtOk
    label.new(bar_index, high, "SN", style=label.style_label_down, color=color.new(#d50000, 35), textcolor=color.white, size=size.tiny, yloc=yloc.abovebar, force_overlay=true, tooltip=shortNearTip)
if longSetupStart and longSetupWtOk
    label.new(bar_index, low, "LS", style=label.style_label_up, color=color.new(#00c853, 0), textcolor=color.white, size=size.tiny, yloc=yloc.belowbar, force_overlay=true, tooltip=longSetupTip)
if shortSetupStart and shortSetupWtOk
    label.new(bar_index, high, "SS", style=label.style_label_down, color=color.new(#d50000, 0), textcolor=color.white, size=size.tiny, yloc=yloc.abovebar, force_overlay=true, tooltip=shortSetupTip)

// Entry marker placed AT the WT cross (on the WT1 line) — so you can see at a glance whether
// the entry actually sits on a real WT1/WT2 cross and where on the oscillator it fired.
// Full colour = tradeable (opp ≥ threshold), dimmed = watch; orange overrides when sampled.
plotshape(longSignal  ? plotWT1 : na, title="Long Signal (Pane)",  style=shape.triangleup,   location=location.absolute, color=sampledLongSignal  ? color.new(#ff6f00, 0) : color.new(#00c853, longTradeable  ? 0 : 60), size=size.small)
plotshape(shortSignal ? plotWT1 : na, title="Short Signal (Pane)", style=shape.triangledown, location=location.absolute, color=sampledShortSignal ? color.new(#ff6f00, 0) : color.new(#d50000, shortTradeable ? 0 : 60), size=size.small)

// Cross dots — all WT crosses; entry crosses are already covered by the larger triangles above
plotshape(showWtDots and h1BullCross ? plotWT1 : na, "WT Bull Cross Dot",
     style=shape.circle, location=location.absolute, color=color.new(#26a69a, 30), size=size.tiny)
plotshape(showWtDots and h1BearCross ? plotWT1 : na, "WT Bear Cross Dot",
     style=shape.circle, location=location.absolute, color=color.new(#ef5350, 30), size=size.tiny)


//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Rotation Map inset (right of the last bar, lower pane area)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

var polyline rotPl = na
var box rotFrame = na
var box rotQLead = na
var box rotQImp  = na
var box rotQWeak = na
var box rotQLag  = na
var line rotVLn = na
var line rotHLn = na
var label rotDot = na
var label rotTitle = na
var label rotLbLead = na
var label rotLbImp = na
var label rotLbWeak = na
var label rotLbLag = na

if showRotation and barstate.islast and array.size(rotXs) >= 2
    polyline.delete(rotPl)
    box.delete(rotFrame)
    box.delete(rotQLead)
    box.delete(rotQImp)
    box.delete(rotQWeak)
    box.delete(rotQLag)
    line.delete(rotVLn)
    line.delete(rotHLn)
    label.delete(rotDot)
    label.delete(rotTitle)
    label.delete(rotLbLead)
    label.delete(rotLbImp)
    label.delete(rotLbWeak)
    label.delete(rotLbLag)

    int xL = bar_index + 6
    int rotW = 34
    float yB = -95.0
    float rotHt = 78.0
    int cx = xL + rotW / 2
    float cy = yB + rotHt / 2
    float yT = yB + rotHt

    // Quadrant tints make the map self-explanatory
    rotQImp  := box.new(xL, yT, cx, cy, border_color=color.new(color.white, 100), bgcolor=color.new(#ffd600, 94))
    rotQLead := box.new(cx, yT, xL + rotW, cy, border_color=color.new(color.white, 100), bgcolor=color.new(#00c853, 94))
    rotQLag  := box.new(xL, cy, cx, yB, border_color=color.new(color.white, 100), bgcolor=color.new(#d50000, 95))
    rotQWeak := box.new(cx, cy, xL + rotW, yB, border_color=color.new(color.white, 100), bgcolor=color.new(#ff6f00, 95))

    rotFrame := box.new(xL, yT, xL + rotW, yB, border_color=color.new(color.gray, 50), bgcolor=color.new(color.white, 100))
    rotVLn := line.new(cx, yB, cx, yT, color=color.new(color.gray, 60), style=line.style_dotted)
    rotHLn := line.new(xL, cy, xL + rotW, cy, color=color.new(color.gray, 60), style=line.style_dotted)

    rotTitle := label.new(cx, yT, "Confluence Rotation", style=label.style_label_down, color=color.new(color.white, 100), textcolor=color.new(color.gray, 35), size=size.tiny)

    rotLbLead := label.new(xL + rotW, yT, "LEAD", style=label.style_label_lower_left,  color=color.new(color.white, 100), textcolor=color.new(#00c853, 30), size=size.tiny)
    rotLbImp  := label.new(xL,        yT, "IMPR", style=label.style_label_lower_right, color=color.new(color.white, 100), textcolor=color.new(#c8a800, 20), size=size.tiny)
    rotLbWeak := label.new(xL + rotW, yB, "WEAK", style=label.style_label_upper_left,  color=color.new(color.white, 100), textcolor=color.new(#ff6f00, 30), size=size.tiny)
    rotLbLag  := label.new(xL,        yB, "LAG",  style=label.style_label_upper_right, color=color.new(color.white, 100), textcolor=color.new(#d50000, 30), size=size.tiny)

    pts = array.new<chart.point>()
    int n = array.size(rotXs)
    for i = 0 to n - 1
        float vx = array.get(rotXs, i)
        float vy = array.get(rotYs, i)
        int px = xL + int(math.round((vx + 100.0) / 200.0 * rotW))
        float py = yB + (vy + 100.0) / 200.0 * rotHt
        array.push(pts, chart.point.from_index(px, py))
    rotPl := polyline.new(pts, curved=true, closed=false, line_color=color.new(color.gray, 45), line_width=1)

    int pxN = xL + int(math.round((rotX + 100.0) / 200.0 * rotW))
    float pyN = yB + (rotY + 100.0) / 200.0 * rotHt
    rotDot := label.new(pxN, pyN, "", style=label.style_circle, color=rotQuadColor, size=size.small)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Dashboard
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

var table dash = table.new(position.top_right, 3, 13,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1,
     frame_color=color.new(color.gray, 40),
     frame_width=1,
     force_overlay=true)

color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
color cGood = #00c853
color cBad  = #d50000

var table tfWarn = table.new(position.top_center, 1, 2,
     bgcolor=color.new(#fff3e0, 0),
     border_color=color.new(#ff6f00, 15),
     border_width=1,
     frame_color=color.new(#ff6f00, 0),
     frame_width=1,
     force_overlay=true)

if barstate.islast
    table.clear(tfWarn, 0, 0, 0, 1)
    if showTfWarning and tfSamplingWarning
        table.cell(tfWarn, 0, 0, "⚠ Lower-TF sampling", text_color=color.new(#e65100, 0), bgcolor=color.new(#ffe0b2, 0), text_size=size.small,
             tooltip=tfSamplingText)
        table.cell(tfWarn, 0, 1, tfSamplingText, text_color=color.new(#5d4037, 0), bgcolor=color.new(#fff8e1, 0), text_size=size.small,
             tooltip="Use the Trigger TF chart for exact trigger crosses, or set Trigger/Setup TF at or above the chart TF.")

if showDashboard and barstate.islast
    table.cell(dash, 0, 0, tfSamplingWarning ? "MTF WT Hunter ⚠" : "MTF WT Hunter", text_color=tfSamplingWarning ? color.new(#ff6f00, 0) : tc, bgcolor=hd, text_size=size.small,
         tooltip=tfSamplingWarning ? tfSamplingText : "")
    table.cell(dash, 1, 0, "Long",  text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(dash, 2, 0, "Short", text_color=tc, bgcolor=hd, text_size=size.small)

    table.cell(dash, 0, 1, "Regime", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 1, str.tostring(bullRegime, "#.0"), text_color=bullRegime >= minRegimeEff ? cGood : tc, text_size=size.small)
    table.cell(dash, 2, 1, str.tostring(bearRegime, "#.0"), text_color=bearRegime >= minRegimeEff ? cBad : tc, text_size=size.small)

    table.cell(dash, 0, 2, "Opportunity", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 2, str.tostring(longOpportunity, "#.0"), text_color=longOpportunity >= minOpportunityEff ? cGood : tc, text_size=size.small)
    table.cell(dash, 2, 2, str.tostring(shortOpportunity, "#.0"), text_color=shortOpportunity >= minOpportunityEff ? cBad : tc, text_size=size.small)

    table.cell(dash, 0, 3, "Timing", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 3, str.tostring(longTiming, "#.0"), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 3, str.tostring(shortTiming, "#.0"), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 4, "Quality", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 4, str.tostring(longQuality, "#.0") + (longTradeable ? " ★" : ""),  text_color=longTradeable ? cGood : tc, text_size=size.small)
    table.cell(dash, 2, 4, str.tostring(shortQuality, "#.0") + (shortTradeable ? " ★" : ""), text_color=shortTradeable ? cBad : tc, text_size=size.small)

    table.cell(dash, 0, 5, "Final", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 5, str.tostring(longFinal, "#.0"), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 5, str.tostring(shortFinal, "#.0"), text_color=tc, text_size=size.small)

    color noiseCol = tc
    if entropyRank >= 70
        noiseCol := #ff6f00
    else if entropyRank <= 30
        noiseCol := cGood
    table.cell(dash, 0, 6, "Noise", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 6, str.tostring(entropyRank, "#") + "%", text_color=noiseCol, text_size=size.small)
    table.cell(dash, 2, 6, "", text_size=size.small)

    // Context TF row — informational, green/red if strongly aligned, gray if mixed
    color ctxLCol = netContext >= 40 ? cGood : netContext <= -40 ? color.new(cGood, 60) : tc
    color ctxSCol = netContext <= -40 ? cBad  : netContext >= 40  ? color.new(cBad, 60)  : tc
    table.cell(dash, 0, 7, "Ctx " + tfContext, text_color=tc, text_size=size.small,
         tooltip="Context TF directional bias — informational only, not a gate. Green/red = strong alignment.")
    table.cell(dash, 1, 7, str.tostring(ctxBullNet, "#"), text_color=ctxLCol, text_size=size.small)
    table.cell(dash, 2, 7, str.tostring(ctxBearNet, "#"), text_color=ctxSCol, text_size=size.small)

    table.cell(dash, 0, 8, "Rotation", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 8, rotQuad, text_color=rotQuadColor, text_size=size.small)
    table.cell(dash, 2, 8, "", text_size=size.small)

    string setupL = longSetup ? "SETUP" : longNearSetup ? "NEAR" : longBias ? "BIAS" : "–"
    string setupS = shortSetup ? "SETUP" : shortNearSetup ? "NEAR" : shortBias ? "BIAS" : "–"
    table.cell(dash, 0, 9, "Setup", text_color=tc, bgcolor=color.new(color.gray, 80), text_size=size.small)
    table.cell(dash, 1, 9, setupL, text_color=longSetup ? cGood : longNearSetup ? color.new(#ff6f00, 0) : longBias ? color.new(#00c853, 40) : tc, bgcolor=color.new(color.gray, 80), text_size=size.small)
    table.cell(dash, 2, 9, setupS, text_color=shortSetup ? cBad  : shortNearSetup ? color.new(#ff6f00, 0) : shortBias ? color.new(#d50000, 40) : tc, bgcolor=color.new(color.gray, 80), text_size=size.small)

    table.cell(dash, 0, 10, "WT Trigger", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 10, h1BullTrigger ? triggerTfBelowChart ? "⚠" : "✓" : "–", text_color=h1BullTrigger ? triggerTfBelowChart ? color.new(#ff6f00, 0) : cGood : tc, text_size=size.small,
         tooltip=h1BullTrigger and triggerTfBelowChart ? "Sampled lower-TF WT trigger. Switch chart to Trigger TF for actionable entry." : h1BullCross ? "WT bull cross" : h1BullTurn ? "WT bull turn from extended zone" : h1BullExhaust ? "WT bull exhaustion reversal (hook from deep oversold)" : "")
    table.cell(dash, 2, 10, h1BearTrigger ? triggerTfBelowChart ? "⚠" : "✓" : "–", text_color=h1BearTrigger ? triggerTfBelowChart ? color.new(#ff6f00, 0) : cBad : tc, text_size=size.small,
         tooltip=h1BearTrigger and triggerTfBelowChart ? "Sampled lower-TF WT trigger. Switch chart to Trigger TF for actionable entry." : h1BearCross ? "WT bear cross" : h1BearTurn ? "WT bear turn from extended zone" : h1BearExhaust ? "WT bear exhaustion reversal (hook from deep overbought)" : "")

    string stL = longBias ? "BIAS" : "–"
    color stLBg = color.new(color.gray, 80)
    color stLTx = longBias ? color.new(#00c853, 40) : tc
    if not allowLong
        stL := "OFF"
        stLBg := color.new(color.gray, 80)
        stLTx := tc
    else if longSignal
        stL := "SIGNAL"
        stLBg := color.new(#00c853, 0)
        stLTx := color.white
    else if sampledLongSignal
        stL := "TRIG TF"
        stLBg := color.new(#ff6f00, 0)
        stLTx := color.white
    else if longSetup
        stL := "SETUP"
        stLBg := color.new(#00c853, 65)
        stLTx := color.white
    else if longNearSetup
        stL := "NEAR"
        stLBg := color.new(#ff6f00, 35)
        stLTx := color.white
    else if longBias
        stL := "BIAS"
        stLBg := color.new(color.gray, 80)
        stLTx := tc

    string stS = shortBias ? "BIAS" : "–"
    color stSBg = color.new(color.gray, 80)
    color stSTx = shortBias ? color.new(#d50000, 40) : tc
    if not allowShort
        stS := "OFF"
        stSBg := color.new(color.gray, 80)
        stSTx := tc
    else if shortSignal
        stS := "SIGNAL"
        stSBg := color.new(#d50000, 0)
        stSTx := color.white
    else if sampledShortSignal
        stS := "TRIG TF"
        stSBg := color.new(#ff6f00, 0)
        stSTx := color.white
    else if shortSetup
        stS := "SETUP"
        stSBg := color.new(#d50000, 65)
        stSTx := color.white
    else if shortNearSetup
        stS := "NEAR"
        stSBg := color.new(#ff6f00, 35)
        stSTx := color.white
    else if shortBias
        stS := "BIAS"
        stSBg := color.new(color.gray, 80)
        stSTx := tc

    table.cell(dash, 0, 11, "Status", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 11, stL, text_color=stLTx, bgcolor=stLBg, text_size=size.small)
    table.cell(dash, 2, 11, stS, text_color=stSTx, bgcolor=stSBg, text_size=size.small)

    // Separator row
    table.cell(dash, 0, 12, "", bgcolor=color.new(color.gray, 80), text_size=size.small)
    table.cell(dash, 1, 12, "", bgcolor=color.new(color.gray, 80), text_size=size.small)
    table.cell(dash, 2, 12, "", bgcolor=color.new(color.gray, 80), text_size=size.small)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Signal Stats panel — live performance over the visible history (tune knobs by numbers)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

var table statsTbl = table.new(position.bottom_right, 3, 6,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1,
     frame_color=color.new(color.gray, 40),
     frame_width=1,
     force_overlay=true)

f_avgRcol(v) => v > 0 ? cGood : v < 0 ? cBad : tc

if showStats and barstate.islast
    float winL  = statNL > 0 ? statWinsL / float(statNL) * 100.0 : 0.0
    float winS  = statNS > 0 ? statWinsS / float(statNS) * 100.0 : 0.0
    float avgRL = statNL > 0 ? statSumRL / float(statNL) : 0.0
    float avgRS = statNS > 0 ? statSumRS / float(statNS) : 0.0
    float failL = statNL > 0 ? statFailsL / float(statNL) * 100.0 : 0.0
    float failS = statNS > 0 ? statFailsS / float(statNS) * 100.0 : 0.0
    table.cell(statsTbl, 0, 0, "Stats", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(statsTbl, 1, 0, "Long",  text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(statsTbl, 2, 0, "Short", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(statsTbl, 0, 1, "N", text_color=tc, text_size=size.small)
    table.cell(statsTbl, 1, 1, str.tostring(statNL), text_color=tc, text_size=size.small)
    table.cell(statsTbl, 2, 1, str.tostring(statNS), text_color=tc, text_size=size.small)
    table.cell(statsTbl, 0, 2, "Win %", text_color=tc, text_size=size.small)
    table.cell(statsTbl, 1, 2, str.tostring(winL, "#") + "%", text_color=winL >= 50 ? cGood : tc, text_size=size.small)
    table.cell(statsTbl, 2, 2, str.tostring(winS, "#") + "%", text_color=winS >= 50 ? cGood : tc, text_size=size.small)
    table.cell(statsTbl, 0, 3, "Avg R", text_color=tc, text_size=size.small)
    table.cell(statsTbl, 1, 3, str.tostring(avgRL, "+0.00;-0.00"), text_color=f_avgRcol(avgRL), text_size=size.small)
    table.cell(statsTbl, 2, 3, str.tostring(avgRS, "+0.00;-0.00"), text_color=f_avgRcol(avgRS), text_size=size.small)
    table.cell(statsTbl, 0, 4, "Net R", text_color=tc, text_size=size.small)
    table.cell(statsTbl, 1, 4, str.tostring(statSumRL, "+0.0;-0.0"), text_color=f_avgRcol(statSumRL), text_size=size.small)
    table.cell(statsTbl, 2, 4, str.tostring(statSumRS, "+0.0;-0.0"), text_color=f_avgRcol(statSumRS), text_size=size.small)
    table.cell(statsTbl, 0, 5, "Fail %", text_color=tc, text_size=size.small)
    table.cell(statsTbl, 1, 5, str.tostring(failL, "#") + "%", text_color=failL >= 50 ? cBad : tc, text_size=size.small)
    table.cell(statsTbl, 2, 5, str.tostring(failS, "#") + "%", text_color=failS >= 50 ? cBad : tc, text_size=size.small)

// Layer test: avg R per situational bucket. If a pair/group separates clearly, that layer is a
// real, situation-driven filter; if not, it is dead weight. Regime (with/counter) and Setup
// (tradeable/watch) proved flat on the full sample; trigger-type and WT-extreme-depth are the
// new hypotheses for the long/short asymmetry.
var table layerTbl = table.new(position.middle_right, 3, 9,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1,
     frame_color=color.new(color.gray, 40),
     frame_width=1,
     force_overlay=true)
if showStats and barstate.islast
    float arW = statN_w > 0 ? statR_w / float(statN_w) : 0.0
    float arC = statN_c > 0 ? statR_c / float(statN_c) : 0.0
    float arS = statN_s > 0 ? statR_s / float(statN_s) : 0.0
    float arK = statN_k > 0 ? statR_k / float(statN_k) : 0.0
    float arHk = statN_hk > 0 ? statR_hk / float(statN_hk) : 0.0
    float arRv = statN_rv > 0 ? statR_rv / float(statN_rv) : 0.0
    float arDp = statN_dp > 0 ? statR_dp / float(statN_dp) : 0.0
    float arSh = statN_sh > 0 ? statR_sh / float(statN_sh) : 0.0
    table.cell(layerTbl, 0, 0, "Layer", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(layerTbl, 1, 0, "AvgR", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(layerTbl, 2, 0, "N", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(layerTbl, 0, 1, "Regime+", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 1, str.tostring(arW, "+0.00;-0.00"), text_color=f_avgRcol(arW), text_size=size.small)
    table.cell(layerTbl, 2, 1, str.tostring(statN_w), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 2, "Regime−", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 2, str.tostring(arC, "+0.00;-0.00"), text_color=f_avgRcol(arC), text_size=size.small)
    table.cell(layerTbl, 2, 2, str.tostring(statN_c), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 3, "Trade✓", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 3, str.tostring(arS, "+0.00;-0.00"), text_color=f_avgRcol(arS), text_size=size.small)
    table.cell(layerTbl, 2, 3, str.tostring(statN_s), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 4, "Watch", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 4, str.tostring(arK, "+0.00;-0.00"), text_color=f_avgRcol(arK), text_size=size.small)
    table.cell(layerTbl, 2, 4, str.tostring(statN_k), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 5, "HookExit", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 5, str.tostring(arHk, "+0.00;-0.00"), text_color=f_avgRcol(arHk), text_size=size.small)
    table.cell(layerTbl, 2, 5, str.tostring(statN_hk), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 6, "CrossRev", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 6, str.tostring(arRv, "+0.00;-0.00"), text_color=f_avgRcol(arRv), text_size=size.small)
    table.cell(layerTbl, 2, 6, str.tostring(statN_rv), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 7, "Deep≥60", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 7, str.tostring(arDp, "+0.00;-0.00"), text_color=f_avgRcol(arDp), text_size=size.small)
    table.cell(layerTbl, 2, 7, str.tostring(statN_dp), text_color=tc, text_size=size.small)
    table.cell(layerTbl, 0, 8, "Shallow", text_color=tc, text_size=size.small)
    table.cell(layerTbl, 1, 8, str.tostring(arSh, "+0.00;-0.00"), text_color=f_avgRcol(arSh), text_size=size.small)
    table.cell(layerTbl, 2, 8, str.tostring(statN_sh), text_color=tc, text_size=size.small)


// Situation panel: avg R and N per situational bucket, split Long / Short. A bucket pair is a
// candidate driver of the long/short asymmetry only if it separates on BOTH sides in the same
// direction (e.g. "with HTF" better than "against HTF" for longs AND shorts) — a split that only
// shows on one side is the asymmetry itself, not its cause.
var table sitTbl = table.new(position.bottom_left, 3, 10,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1,
     frame_color=color.new(color.gray, 40),
     frame_width=1,
     force_overlay=true)
f_sitCell(int bucket, int dir) =>
    int i = bucket * 2 + dir
    int n = sitN.get(i)
    float ar = n > 0 ? sitR.get(i) / float(n) : 0.0
    [n > 0 ? str.tostring(ar, "+0.00;-0.00") + " ·" + str.tostring(n) : "—", n > 0 ? f_avgRcol(ar) : tc]
if showStats and barstate.islast
    table.cell(sitTbl, 0, 0, "Situation", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(sitTbl, 1, 0, "Long R ·N", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(sitTbl, 2, 0, "Short R ·N", text_color=tc, bgcolor=hd, text_size=size.small)
    array<string> sitNames = array.from("HTF with", "HTF against", "Drift with", "Drift against", "Room", "Near", "Extended", "Vola high", "Vola low")
    for b = 0 to 8
        [txtL, colL] = f_sitCell(b, 0)
        [txtS, colS] = f_sitCell(b, 1)
        table.cell(sitTbl, 0, b + 1, sitNames.get(b), text_color=tc, text_size=size.small)
        table.cell(sitTbl, 1, b + 1, txtL, text_color=colL, text_size=size.small)
        table.cell(sitTbl, 2, b + 1, txtS, text_color=colS, text_size=size.small)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Alerts
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (longSignal),                title="MTF WT Hunter Long",              message="WTOH · LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortSignal),               title="MTF WT Hunter Short",             message="WTOH · SHORT · {{ticker}} {{interval}}")
alertcondition(alertOk and (longSignal and longTradeable), title="MTF WT Hunter Long (Tradeable)",   message="WTOH · LONG TRADEABLE · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortSignal and shortTradeable), title="MTF WT Hunter Short (Tradeable)", message="WTOH · SHORT TRADEABLE · {{ticker}} {{interval}}")
alertcondition(alertOk and (sampledLongSignal),         title="MTF WT Hunter Long (Sampled)",    message="WTOH · LONG SAMPLED · {{ticker}} {{interval}}")
alertcondition(alertOk and (sampledShortSignal),        title="MTF WT Hunter Short (Sampled)",   message="WTOH · SHORT SAMPLED · {{ticker}} {{interval}}")
alertcondition(alertOk and (longExit),                  title="MTF WT Hunter Long Closed",       message="WTOH · EXIT LONG · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortExit),                 title="MTF WT Hunter Short Closed",      message="WTOH · EXIT SHORT · {{ticker}} {{interval}}")

Overview

MTF WaveTrend Opportunity Hunter [WavesUnchained] plots a net confluence score histogram, multi-layer heat ribbons, an RRG-style Rotation Map inset, and WaveTrend momentum lines in a separate pane. On-chart labels and dashboard tables highlight tradeable setup conditions alongside entry and exit signals. Core settings configure timeframe levels, WaveTrend lengths, JMA or Ehlers Ultimate Smoother filters, ADX parameters, and score thresholds. Live higher-timeframe values dynamically update until higher-timeframe bars close.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

Trend, Oscillators & momentum, Screeners & dashboards, Moving averages

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

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

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

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

SettingTypeDefaultVariable
Regime TFtimeframeDtfRegime
Setup TFtimeframe240tfSetup
Trigger TFtimeframe60tfTrigger
Context TFtimeframeWtfContext
Show Context Ribbon + Dashboard RowbooltrueshowContextLayer
WT Channel Lengthint10wtChLen
WT Average Lengthint21wtAvgLen
WT Signal Lengthint4wtSigLen
WT Extreme Levelfloat55.0wtExtreme
EMA + JMA Hybrid CorebooltrueuseJmaCore
JMA Blendfloat0.35jmaCoreBlend
Ultimate Smoother Core (Ehlers 2024)boolfalseuseUltimate
Show all 40 settings
SettingTypeDefaultVariable
EMA Fastint50emaFastLen
EMA Slowint200emaSlowLen
ADX Lengthint14adxLen
Min ADXfloat18.0adxMin
JMA Lengthint34jmaLen
JMA Phaseint50jmaPhase
JMA Powerfloat2.0jmaPower
Efficiency Ratio Lengthint20erLen
ATR Lengthint14atrLen
Entropy Window (Trigger TF)int40entropyLen
Entropy Rank Lookbackint200entropyRankLen
Use Relative Strength FilterboolfalseuseRS
Benchmark SymbolsymbolSPYbenchmark
Min Regime Scorefloat65minRegime
Min Opportunity Scorefloat35minOpportunity
Tradeable Opportunity ≥float40minOppSignal
Gate Bufferfloat10gateBuffer
Near Setup Bufferfloat10nearSetupBuffer
Minimum Hold (bars)int8cooldownBars
WT Cross Zone Limitfloat0.0wtCrossZone
Early Exit on Turn/Exhaustion HookboolfalseuseHookExit
WT Turn Level (hook exit)float35.0triggerTurnLevel
WT Turn Min Falling Barsint2turnFallBars
Exhaustion Hook (deep-extreme exit)booltrueuseExhaustion
Exhaustion WT Levelfloat50.0exhaustLevel
Allow Long SignalsbooltrueallowLong
Allow Short SignalsbooltrueallowShort
Show Rotation MapboolfalseshowRotation
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

218 stars

Multi-Timeframe Analysis Suite calculates trend direction and momentum conditions across five user-selected timeframes using moving averages, RSI, and Stochastic oscillators. It evaluates moving average slope and oscillator levels to compute trend alignment, m

Indicator v6 Screeners & dashboardsTrend
Updated 3 months ago · None stated GitHub repository of Multi-Timeframe Analysis Suite

KamaFama

by Mastaaa1987
141 stars

KamaFama is an indicator that translates a Freqtrade trading strategy into Pine Script for visualization. It combines Kaufman Adaptive Moving Average (KAMA), MAMA/FAMA calculations derived via Hilbert Transform, Williams %R, and correlation trend filters to ev

Indicator v6 Moving averagesOscillators & momentum
Updated 6 weeks ago · MIT GitHub repository of KamaFama

Ultimate Trading System

by EngineeredSuccess
3 stars

The Ultimate Trading System is an integrated indicator that combines multiple analysis methodologies—including Pi Cycle Top, SPX/Gold ratio, Elliott Wave, and multi-source market signals—into a composite oscillator scored from 0 to 100. Plotted in a separate p

Indicator v6 Screeners & dashboardsOscillators & momentum
Updated 1 year ago · Custom licence GitHub repository of Ultimate Trading System
53 stars

This indicator analyzes historical candle direction and price range over a configurable lookback window to gauge market trend. It categorizes trend direction using candle count, price range sum, or both. Signals are filtered using a Simple Moving Average to en

Indicator v6 TrendOscillators & momentum
Updated 2 months ago · MPL-2.0 GitHub repository of Candle Counter + MA + RSI + Mode

Pocket Scout - SPY/QQQ

by chris-c-thomas
14 stars

Pocket Scout - SPY/QQQ is a mobile-optimized intraday bias indicator designed for major US equity ETFs and indices. It overlays a 9/21/50 EMA ribbon, session VWAP, prior day high/low/close levels, and intraday high/low lines directly on the main price chart. T

Indicator v6 TrendOscillators & momentum Repaints
Updated 6 months ago · MIT GitHub repository of Pocket Scout - SPY/QQQ

Portfolio Backtester

by f-cksociety
11 stars

The script is a Pine Script v5 indicator that emulates strategy backtesting directly on the price chart. It evaluates modular entry strategies, trade directions, entry stop-loss types such as ATR multiples or fixed percentages, and a wide variety of trade filt

Indicator v5 TrendOscillators & momentum
Updated 4 years ago · MPL-2.0 GitHub repository of Portfolio Backtester

More indicators

Trade Your Charts With a Regulated Broker

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

Compare brokers