Indicator · Pine Script v6 · Own pane

MSO - Market Stress Oscillator [WavesUnchained]

by casoon

The Market Stress Oscillator plots a bi-directional Williams VIX Fix in a separate pane, with separate bull and bear stress lines.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code885
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

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

market_stress_oscillator.pine
//@version=6
// ============================================================================
// MSO - Market Stress Oscillator [WavesUnchained]
// Version: 1.1.1
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Bi-directional WVF stress oscillator with JMA trend context and ADX quality display.
//
// Features:
//   ✓ Bi-directional Williams VIX Fix — separate bull and bear stress lines
//   ✓ JMA smoothing on both series
//   ✓ ADX strength context for noise diagnosis
//   ✓ Peak-hold event markers for stress extremes
//
// ============================================================================
indicator("MSO - Market Stress Oscillator [WavesUnchained]", format=format.price, precision=2, overlay=false, max_labels_count=200, max_lines_count=500, max_boxes_count=500)

// NOTE: Modules are combined by the build system.

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

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

//──────────────────────────────────────────────────────────────────────────────
// Preset Selection (UI Table)
//──────────────────────────────────────────────────────────────────────────────

groupTop = "00. Stress"
groupPresetTF = "Presets: Timeframe"
groupPresetWvf = "Presets: WVF/Z-Score"
groupPresetZThr = "Presets: Z-Thresholds"
groupPresetTrend = "Presets: Trend Filters"
groupPresetQuality = "Presets: Quality"

groupCore = "Core"
groupFilters = "Filters"
groupQuality = "Quality"
groupDisplay = "Display"
groupPivot = "Pivot Validation"
groupLogging = "Logging"

stressColorMode = input.string("Intuitive", "Stress Color Mode", options=["Intuitive","Technical"], group=groupTop, tooltip="Intuitive: Downside=green, Upside=red. Technical: Downside=red, Upside=green.")

presetTF = input.string("Auto", "Preset Timeframe", options=["Auto","15m","1h","4h","D","W","M"], group=groupPresetTF)

f_tfKey(_tf) =>
    _tf == "15" ? "15m" : _tf == "60" ? "1h" : _tf == "240" ? "4h" : _tf == "D" ? "D" : _tf == "W" ? "W" : _tf == "M" ? "M" : "1h"

f_sel(tf, a15, a1h, a4h, aD, aW, aM) =>
    tf == "15m" ? a15 : tf == "1h" ? a1h : tf == "4h" ? a4h : tf == "D" ? aD : tf == "W" ? aW : tf == "M" ? aM : a1h

tfUsed = presetTF == "Auto" ? f_tfKey(timeframe.period) : presetTF

// WVF Lookback
wvf_15 = input.int(34, "WVF 15m", minval=5, group=groupPresetWvf)
wvf_1h = input.int(34, "WVF 1h", minval=5, group=groupPresetWvf)
wvf_4h = input.int(55, "WVF 4h", minval=5, group=groupPresetWvf)
wvf_D = input.int(89, "WVF D", minval=5, group=groupPresetWvf)
wvf_W = input.int(89, "WVF W", minval=5, group=groupPresetWvf)
wvf_M = input.int(144, "WVF M", minval=5, group=groupPresetWvf)

// Z-Window
z_15 = input.int(144, "ZWin 15m", minval=20, group=groupPresetWvf)
z_1h = input.int(144, "ZWin 1h", minval=20, group=groupPresetWvf)
z_4h = input.int(200, "ZWin 4h", minval=20, group=groupPresetWvf)
z_D = input.int(252, "ZWin D", minval=20, group=groupPresetWvf)
z_W = input.int(252, "ZWin W", minval=20, group=groupPresetWvf)
z_M = input.int(300, "ZWin M", minval=20, group=groupPresetWvf)

// Z-Thresholds
zThr_15 = input.float(2.0, "ZThr 15m", step=0.1, group=groupPresetZThr)
zThr_1h = input.float(2.0, "ZThr 1h", step=0.1, group=groupPresetZThr)
zThr_4h = input.float(2.1, "ZThr 4h", step=0.1, group=groupPresetZThr)
zThr_D = input.float(2.1, "ZThr D", step=0.1, group=groupPresetZThr)
zThr_W = input.float(2.2, "ZThr W", step=0.1, group=groupPresetZThr)
zThr_M = input.float(2.5, "ZThr M", step=0.1, group=groupPresetZThr)

// Trend Filters
jma_15 = input.int(55, "JMA 15m", minval=5, group=groupPresetTrend)
jma_1h = input.int(89, "JMA 1h", minval=5, group=groupPresetTrend)
jma_4h = input.int(144, "JMA 4h", minval=5, group=groupPresetTrend)
jma_D = input.int(200, "JMA D", minval=5, group=groupPresetTrend)
jma_W = input.int(233, "JMA W", minval=5, group=groupPresetTrend)
jma_M = input.int(377, "JMA M", minval=5, group=groupPresetTrend)

adxMin_15 = input.int(18, "ADX Min 15m", minval=5, group=groupPresetTrend)
adxMin_1h = input.int(18, "ADX Min 1h", minval=5, group=groupPresetTrend)
adxMin_4h = input.int(18, "ADX Min 4h", minval=5, group=groupPresetTrend)
adxMin_D = input.int(18, "ADX Min D", minval=5, group=groupPresetTrend)
adxMin_W = input.int(18, "ADX Min W", minval=5, group=groupPresetTrend)
adxMin_M = input.int(18, "ADX Min M", minval=5, group=groupPresetTrend)

// Quality Presets
minExt_15 = input.int(2, "MinExtreme 15m", minval=1, group=groupPresetQuality)
minExt_1h = input.int(2, "MinExtreme 1h", minval=1, group=groupPresetQuality)
minExt_4h = input.int(2, "MinExtreme 4h", minval=1, group=groupPresetQuality)
minExt_D = input.int(3, "MinExtreme D", minval=1, group=groupPresetQuality)
minExt_W = input.int(3, "MinExtreme W", minval=1, group=groupPresetQuality)
minExt_M = input.int(3, "MinExtreme M", minval=1, group=groupPresetQuality)

cool_15 = input.int(12, "Cooldown 15m", minval=0, group=groupPresetQuality)
cool_1h = input.int(8, "Cooldown 1h", minval=0, group=groupPresetQuality)
cool_4h = input.int(5, "Cooldown 4h", minval=0, group=groupPresetQuality)
cool_D = input.int(5, "Cooldown D", minval=0, group=groupPresetQuality)
cool_W = input.int(4, "Cooldown W", minval=0, group=groupPresetQuality)
cool_M = input.int(3, "Cooldown M", minval=0, group=groupPresetQuality)

follow_15 = input.float(0.7, "MinFollow ATR 15m", step=0.05, minval=0.0, group=groupPresetQuality)
follow_1h = input.float(0.6, "MinFollow ATR 1h", step=0.05, minval=0.0, group=groupPresetQuality)
follow_4h = input.float(0.5, "MinFollow ATR 4h", step=0.05, minval=0.0, group=groupPresetQuality)
follow_D = input.float(0.45, "MinFollow ATR D", step=0.05, minval=0.0, group=groupPresetQuality)
follow_W = input.float(0.45, "MinFollow ATR W", step=0.05, minval=0.0, group=groupPresetQuality)
follow_M = input.float(0.45, "MinFollow ATR M", step=0.05, minval=0.0, group=groupPresetQuality)

// Resolved preset values
wvfLen = f_sel(tfUsed, wvf_15, wvf_1h, wvf_4h, wvf_D, wvf_W, wvf_M)
zWin = f_sel(tfUsed, z_15, z_1h, z_4h, z_D, z_W, z_M)
zThr = f_sel(tfUsed, zThr_15, zThr_1h, zThr_4h, zThr_D, zThr_W, zThr_M)
jmaLen = f_sel(tfUsed, jma_15, jma_1h, jma_4h, jma_D, jma_W, jma_M)
adxMin = f_sel(tfUsed, adxMin_15, adxMin_1h, adxMin_4h, adxMin_D, adxMin_W, adxMin_M)

minExtremeBars = f_sel(tfUsed, minExt_15, minExt_1h, minExt_4h, minExt_D, minExt_W, minExt_M)
cooldownBars = f_sel(tfUsed, cool_15, cool_1h, cool_4h, cool_D, cool_W, cool_M)
minFollowATR = f_sel(tfUsed, follow_15, follow_1h, follow_4h, follow_D, follow_W, follow_M)

// Core Settings
adxLen = input.int(14, "ADX Length", minval=5, group=groupCore)
jmaPhase = input.float(0, "JMA Phase (-100..100)", minval=-100, maxval=100, group=groupCore)
jmaPower = input.float(2.0, "JMA Power (1..8)", minval=1, maxval=8, group=groupCore)

// Quality / Noise Control
reactBars = input.int(2, "Reaction bars (follow-through check)", minval=1, maxval=20, group=groupQuality)
usePeakHold = input.bool(true, "Peak-hold (only strongest per cluster)", group=groupQuality)

// Filters
useSlopeFilter = input.bool(true, "Use JMA slope filter", group=groupFilters)
requireTrendAlign = input.bool(false, "Require trend alignment for trigger", group=groupFilters, tooltip="Off (default): a stress extreme triggers on its own and trend alignment is a quality grade only — a trend filter must not block the reversal. On: the trigger also requires the JMA trend to point the trade's way (with-trend pullback mode).")
jmaSlopeLen = input.int(5, "JMA slope smoothing", minval=1, group=groupFilters)

// Display
showPresetSummary = input.bool(false, "Show preset summary (table)", group=groupDisplay, tooltip="Shows a screen-fixed summary table.")
zoneHalfHeight = input.float(0.15, "Zone half height", minval=0.01, step=0.01, group=groupDisplay, tooltip="Half height of the zone around the peak Z value.")
zoneAlpha = input.int(75, "Zone transparency", minval=0, maxval=95, group=groupDisplay, tooltip="Higher = more transparent zones.")
showPaneRegimeBg = input.bool(true, "Pane: Dominance background (weak)", group=groupDisplay, tooltip="Paint a very subtle background when bias dominates.")
paneBgAlpha = input.int(95, "Pane bg transparency", minval=80, maxval=100, group=groupDisplay, tooltip="Higher = more transparent background.")
biasBand = input.float(0.8, "Dominance band (b)", step=0.1, group=groupDisplay, tooltip="Bias threshold to consider a regime dominant.")
biasSmoothLen = input.int(5, "Bias smoothing length", minval=1, group=groupDisplay, tooltip="EMA smoothing for the bias line and dominance tests.")
showPaneCross = input.bool(true, "Pane: Bias zero-cross markers", group=groupDisplay, tooltip="Arrows show bias flips through zero (context only).")
showChartPeakLabels = input.bool(true, "Chart: Peak labels", group=groupDisplay, tooltip="Show DA/DR/UA/UR (or LA/LR/SA/SR) on the price chart.")
chartLabelOffsetATR = input.float(0.25, "Chart label offset (ATR)", step=0.05, group=groupDisplay, tooltip="Distance from the bar high/low for chart labels.")
labelMode = input.string("Directional", "Peak label text", options=["Directional","Classic"], group=groupDisplay, tooltip="Directional uses DA/DR/UA/UR. Classic uses LA/LR/SA/SR.")
showClarityLabel = input.bool(true, "Chart: Clarity label on regime entry", group=groupDisplay, tooltip="Labels when bias enters a regime and aligns with HTF, with absBias >= threshold.")
clarityThr = input.float(1.0, "Clarity threshold (absBias)", step=0.1, group=groupDisplay, tooltip="Minimum absolute bias for clarity labeling.")
clarityCooldown = input.int(50, "Clarity label cooldown (bars)", minval=0, group=groupDisplay, tooltip="Minimum bars between clarity labels.")
showWindLabel = input.bool(true, "Chart: HTF wind label on change", group=groupDisplay, tooltip="Labels when higher-timeframe bias flips.")
windTf = input.string("D", "HTF wind TF", options=["D","W"], group=groupDisplay, tooltip="Higher timeframe for wind labels.")
windBand = input.float(0.8, "HTF wind band", step=0.1, group=groupDisplay, tooltip="Bias band for HTF wind detection.")
windCooldown = input.int(100, "Wind label cooldown (bars)", minval=0, group=groupDisplay, tooltip="Minimum bars between wind labels.")
labelFifoMax = input.int(150, "Chart: max labels (FIFO)", minval=10, maxval=200, group=groupDisplay, tooltip="Keeps only the most recent labels to avoid hitting TradingView limits.")
lineFifoMax = input.int(200, "Chart: max lines (FIFO)", minval=20, maxval=500, group=groupDisplay, tooltip="Keeps only the most recent lines to avoid hitting TradingView limits.")
showReactionLine = input.bool(true, "Chart: reaction window line", group=groupDisplay, tooltip="Draw a line from peak to the reaction window end.")
showLevelLines = input.bool(true, "Chart: peak level lines", group=groupDisplay, tooltip="Draw horizontal lines at peak prices.")
levelLineBars = input.int(50, "Level line length (bars)", minval=1, group=groupDisplay, tooltip="How many bars the level line extends.")
levelLineAlpha = input.int(70, "Level line transparency", minval=0, maxval=95, group=groupDisplay, tooltip="Higher = more transparent level lines.")
zoneHistoryMax = input.int(100, "Pane: max zone history", minval=10, maxval=500, group=groupDisplay, tooltip="Keeps only the most recent zones in the pane.")
showAccScore = input.bool(true, "Show Acceptance Score (0..1) in pane", group=groupDisplay, tooltip="Plots acceptance score after reaction window.")
accScoreSmooth = input.int(3, "Score smoothing (EMA)", minval=1, group=groupDisplay, tooltip="EMA smoothing for acceptance score.")
accScoreStyle = input.string("Line", "Acceptance Score Style", options=["Line","Histogram"], group=groupDisplay, tooltip="Line is smoother, histogram is more punchy.")
zeroDashed = input.bool(true, "Zero line dashed", group=groupDisplay, tooltip="Draw zero line as dashed.")
dashPeriod = input.int(6, "Dash period", minval=2, group=groupDisplay, tooltip="Total bars in one dash cycle.")
dashOn = input.int(3, "Dash on bars", minval=1, group=groupDisplay, tooltip="Bars shown within one dash cycle.")
zeroContextOnly = input.bool(true, "Show zero line only when |z| > x", group=groupDisplay, tooltip="Hide zero line unless stress is meaningful.")
zeroContextX = input.float(0.8, "Zero line threshold", step=0.1, group=groupDisplay, tooltip="Minimum |z| to show zero line when context-only is enabled.")

// Pivot validation (ZigZag-style)
pivotL = input.int(5, "Pivot Left", minval=1, maxval=200, group=groupPivot, tooltip="Bars left of the pivot for confirmation.")
pivotR = input.int(5, "Pivot Right", minval=1, maxval=200, group=groupPivot, tooltip="Bars right of the pivot for confirmation.")
minSwingATR = input.float(1.5, "Min Pivot Size (ATR)", minval=0.5, maxval=20.0, step=0.5, group=groupPivot, tooltip="Minimum pivot size in ATR multiples.")
minSwingBars = input.int(3, "Min Bars Between Pivots", minval=1, maxval=200, group=groupPivot, tooltip="Minimum bars between consecutive pivots.")
pivotNearBars = input.int(12, "Pivot proximity (bars)", minval=1, maxval=200, group=groupPivot, tooltip="Distance in bars considered \"near\" a pivot.")

// Logging
enableLogging = input.bool(false, "Enable Pivot/Stress Logging", group=groupLogging, tooltip="Logs pivot and stress events for CSV export.")
matchWindow = input.int(20, "Stress→Pivot Window (bars)", minval=1, maxval=200, group=groupLogging, tooltip="Max bars to match the next pivot after a stress event.")
useRegimeMapping = input.bool(true, "Regime-aware STRESS_UP mapping", group=groupLogging, tooltip="Treat STRESS_UP as continuation in uptrend; otherwise reversal.")


// ============================================================================
// MODULE: core
// ============================================================================

//──────────────────────────────────────────────────────────────────────────────
// JMA Approximation (aligned with CPM calculateJMA)
//──────────────────────────────────────────────────────────────────────────────

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

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

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

jma = calculateJMA(close, jmaLen, jmaPhase, jmaPower)

// Slope filter (smoothed)
jmaSlope = jma - jma[1]
jmaSlopeSm = ta.ema(jmaSlope, jmaSlopeLen)

trendLongOk = useSlopeFilter ? (jmaSlopeSm > 0) : (close >= jma)
trendShortOk = useSlopeFilter ? (jmaSlopeSm < 0) : (close <= jma)

zThrLong = zThr
zThrShort = zThr

//──────────────────────────────────────────────────────────────────────────────
// WVF Long + Short, Z-Score
//──────────────────────────────────────────────────────────────────────────────

hhc = ta.highest(close, wvfLen)
llc = ta.lowest(close, wvfLen)

wvfLong = hhc == 0 ? na : (hhc - low) / hhc * 100.0
wvfShort = llc == 0 ? na : (high - llc) / llc * 100.0

f_z(x, win) =>
    mu = ta.sma(x, win)
    sd = ta.stdev(x, win)
    sd == 0 ? 0 : (x - mu) / sd

zL = f_z(wvfLong, zWin)
zS = f_z(wvfShort, zWin)

// Smoothed bias for regime-aware mapping (local name to avoid collisions)
biasSmCore = ta.ema(zL - zS, biasSmoothLen)

// Raw extremes
extLongRaw = zL >= zThrLong
extShortRaw = zS >= zThrShort

// Duration filter — extLongDur/extShortDur = bars the raw extreme has persisted
// including the current bar (1 on the first bar of a cluster)
extLongDur = ta.barssince(not extLongRaw)
extShortDur = ta.barssince(not extShortRaw)

extLong = extLongRaw and extLongDur >= minExtremeBars
extShort = extShortRaw and extShortDur >= minExtremeBars

//──────────────────────────────────────────────────────────────────────────────
// ADX filter (use DMI to avoid ta.adx availability issues)
//──────────────────────────────────────────────────────────────────────────────

[diPlus, diMinus, adx] = ta.dmi(adxLen, adxLen)
// ADX is a QUALITY/strength grade only — never a hard veto on stress events.
// trendStrengthOk classifies regime context (continuation vs reversal mapping) and
// flavors the displayed grade; it must NOT gate whether a stress trigger fires.
trendStrengthOk = adx >= adxMin
// Continuous strength grade (0..1) surfaced on labels; high ADX does not suppress.
adxGrade = math.min(1.0, adx / math.max(adxMin, 1))

//──────────────────────────────────────────────────────────────────────────────
// Acceptance / Rejection (ATR-normalized, forward follow-through)
//──────────────────────────────────────────────────────────────────────────────

atr = ta.atr(14)

//──────────────────────────────────────────────────────────────────────────────
// Pivot detection (confirmed left/right pivots)
//──────────────────────────────────────────────────────────────────────────────

float phVal = ta.pivothigh(high, pivotL, pivotR)
float plVal = ta.pivotlow(low, pivotL, pivotR)
int phBar = not na(phVal) ? (bar_index - pivotR) : na
int plBar = not na(plVal) ? (bar_index - pivotR) : na

var float lastPivotHigh = na
var int lastPivotHighBar = na
var float lastPivotLow = na
var int lastPivotLowBar = na

pivotAtr = nz(ta.atr(14)[pivotR], ta.atr(14))
barsOkHigh = na(lastPivotHighBar) ? true : (phBar - lastPivotHighBar >= minSwingBars)
barsOkLow = na(lastPivotLowBar) ? true : (plBar - lastPivotLowBar >= minSwingBars)

sizeOkHigh = na(lastPivotLow) ? true : (math.abs(phVal - lastPivotLow) >= minSwingATR * pivotAtr)
sizeOkLow = na(lastPivotHigh) ? true : (math.abs(plVal - lastPivotHigh) >= minSwingATR * pivotAtr)

newPivotHigh = not na(phVal) and barsOkHigh and sizeOkHigh
newPivotLow = not na(plVal) and barsOkLow and sizeOkLow

if newPivotHigh
    lastPivotHigh := phVal
    lastPivotHighBar := phBar
    if enableLogging and barstate.isconfirmed
        log.info("PIVOT_HIGH,price={0},bar={1}", str.tostring(phVal, format.mintick), str.tostring(phBar))

if newPivotLow
    lastPivotLow := plVal
    lastPivotLowBar := plBar
    if enableLogging and barstate.isconfirmed
        log.info("PIVOT_LOW,price={0},bar={1}", str.tostring(plVal, format.mintick), str.tostring(plBar))

// Stress -> pivot matching (reversal-aligned)
var int eventSeq = 0
var int lastLongEventLabelId = na
var int lastShortEventLabelId = na
var array<int> stressBars = array.new_int()
var array<int> stressTypes = array.new_int() // -1 = DOWN, +1 = UP
var array<int> stressUpExpect = array.new_int() // 1 = expect pivot high, 0 = expect pivot low
var array<int> stressAccept = array.new_int() // 1 = ACCEPT, 0 = REJECT
var array<int> stressLabelIds = array.new_int()

f_matchPivot(int _pivotType, int _pivotBar) =>
    if array.size(stressBars) > 0
        for i = 0 to array.size(stressBars) - 1
            sBar = array.get(stressBars, i)
            sType = array.get(stressTypes, i)
            sUpExpect = array.get(stressUpExpect, i)
            d = _pivotBar - sBar
            expType = sType == -1 ? 1 : (sUpExpect == 1 ? 1 : -1)
            if d > 0 and d <= matchWindow
                if enableLogging and barstate.isconfirmed
                    string accTxt = array.get(stressAccept, i) == 1 ? "ACCEPT" : "REJECT"
                    log.info("{0},result={1},accept={2},label=#{3},stressBar={4},pivotBar={5},dt={6}",
                        sType == -1 ? "STRESS_DOWN" : "STRESS_UP",
                        _pivotType == expType ? "ALIGNED_FIRST" : "OPPOSITE_FIRST",
                        accTxt,
                        str.tostring(array.get(stressLabelIds, i)),
                        str.tostring(sBar),
                        str.tostring(_pivotBar),
                        str.tostring(d))
                array.remove(stressBars, i)
                array.remove(stressTypes, i)
                array.remove(stressUpExpect, i)
                array.remove(stressAccept, i)
                array.remove(stressLabelIds, i)
                break

if enableLogging
    int expireBars = matchWindow + pivotR
    while array.size(stressBars) > 0 and (bar_index - array.get(stressBars, 0) > expireBars)
        oldBar = array.shift(stressBars)
        oldType = array.shift(stressTypes)
        array.shift(stressUpExpect)
        oldAccept = array.shift(stressAccept)
        oldLabelId = array.shift(stressLabelIds)
        if barstate.isconfirmed
            log.info("{0},result=NO_PIVOT,accept={1},label=#{2},stressBar={3}", oldType == -1 ? "STRESS_DOWN" : "STRESS_UP", oldAccept == 1 ? "ACCEPT" : "REJECT", str.tostring(oldLabelId), str.tostring(oldBar))

    if newPivotHigh
        f_matchPivot(1, phBar)
    if newPivotLow
        f_matchPivot(-1, plBar)

//──────────────────────────────────────────────────────────────────────────────
// Cooldown gating
//──────────────────────────────────────────────────────────────────────────────

var int lastLongEvent = na
var int lastShortEvent = na

allowLong = na(lastLongEvent) ? true : (bar_index - lastLongEvent >= cooldownBars)
allowShort = na(lastShortEvent) ? true : (bar_index - lastShortEvent >= cooldownBars)

// Base quality conditions — trend alignment is optional (default off): a trend
// filter must not block the reversal trigger; when off it grades quality only
longQual = extLong and (not requireTrendAlign or trendLongOk)
shortQual = extShort and (not requireTrendAlign or trendShortOk)

//──────────────────────────────────────────────────────────────────────────────
// Peak-hold (mark only strongest per cluster)
// Marker drawn on peak bar via offset when cluster ends
//──────────────────────────────────────────────────────────────────────────────

var float longPeakZ = na
var float longPeakPrice = na
var float longPeakClose = na
var float longPeakHigh = na
var int longPeakBarIndex = na
var float longPeakMaxHigh = na
var int longPeakBars = 0
var bool longInClus = false

var float shortPeakZ = na
var float shortPeakPrice = na
var float shortPeakClose = na
var float shortPeakLow = na
var int shortPeakBarIndex = na
var float shortPeakMinLow = na
var int shortPeakBars = 0
var bool shortInClus = false

if extLongRaw
    if not longInClus
        longInClus := true
        longPeakZ := zL
        longPeakPrice := low
        longPeakClose := close
        longPeakHigh := high
        longPeakBarIndex := bar_index
        longPeakMaxHigh := na
        longPeakBars := 0
    else
        if zL > nz(longPeakZ, zL)
            longPeakZ := zL
            longPeakPrice := low
            longPeakClose := close
            longPeakHigh := high
            longPeakBarIndex := bar_index
            longPeakMaxHigh := na
            longPeakBars := 0
        else
            // Follow-through high counts only bars after the peak bar
            longPeakMaxHigh := math.max(nz(longPeakMaxHigh, high), high)
            longPeakBars += 1
else
    if longInClus
        longInClus := false

if extShortRaw
    if not shortInClus
        shortInClus := true
        shortPeakZ := zS
        shortPeakPrice := high
        shortPeakClose := close
        shortPeakLow := low
        shortPeakBarIndex := bar_index
        shortPeakMinLow := na
        shortPeakBars := 0
    else
        if zS > nz(shortPeakZ, zS)
            shortPeakZ := zS
            shortPeakPrice := high
            shortPeakClose := close
            shortPeakLow := low
            shortPeakBarIndex := bar_index
            shortPeakMinLow := na
            shortPeakBars := 0
        else
            // Follow-through low counts only bars after the peak bar
            shortPeakMinLow := math.min(nz(shortPeakMinLow, low), low)
            shortPeakBars += 1
else
    if shortInClus
        shortInClus := false

endLongCluster = extLongRaw[1] and not extLongRaw
endShortCluster = extShortRaw[1] and not extShortRaw

// Evaluated on the cluster-end bar: the peak bar sits longPeakBars + 1 bars back,
// the finished cluster's length is extLongDur[1]
longQualAtPeak = usePeakHold ? (extLongDur[1] >= minExtremeBars and (not requireTrendAlign or trendLongOk[longPeakBars + 1])) : longQual
shortQualAtPeak = usePeakHold ? (extShortDur[1] >= minExtremeBars and (not requireTrendAlign or trendShortOk[shortPeakBars + 1])) : shortQual

// Follow-through state (delayed evaluation)
var bool pendingLong = false
var int pendingLongPeakBar = na
var float pendingLongPeakPrice = na
var float pendingLongPeakClose = na
var float pendingLongPeakHigh = na
var float pendingLongMaxHigh = na
var float pendingLongMinLow = na
var float pendingLongAtr = na
var float pendingLongPeakZ = na

var bool pendingShort = false
var int pendingShortPeakBar = na
var float pendingShortPeakPrice = na
var float pendingShortPeakClose = na
var float pendingShortPeakLow = na
var float pendingShortMinLow = na
var float pendingShortMaxHigh = na
var float pendingShortAtr = na
var float pendingShortPeakZ = na

var float longScore = na
var float shortScore = na

var int lastLongEventPeakBar = na
var float lastLongEventPeakPrice = na
var float lastLongEventPeakHigh = na
var float lastLongEventPeakAtr = na
var float lastLongEventPeakZ = na
var float lastLongEventEndClose = na
var int lastShortEventPeakBar = na
var float lastShortEventPeakPrice = na
var float lastShortEventPeakLow = na
var float lastShortEventPeakAtr = na
var float lastShortEventPeakZ = na
var float lastShortEventEndClose = na

longEventAccepted = false
longEventRejected = false
shortEventAccepted = false
shortEventRejected = false

longTrigger = not pendingLong and allowLong and (usePeakHold ? (endLongCluster and longQualAtPeak) : longQual)
shortTrigger = not pendingShort and allowShort and (usePeakHold ? (endShortCluster and shortQualAtPeak) : shortQual)

if longTrigger
    pendingLong := true
    pendingLongPeakBar := usePeakHold ? longPeakBarIndex : bar_index
    pendingLongPeakPrice := usePeakHold ? longPeakPrice : low
    pendingLongPeakClose := usePeakHold ? longPeakClose : close
    pendingLongPeakHigh := usePeakHold ? longPeakHigh : high
    pendingLongMaxHigh := usePeakHold ? longPeakMaxHigh : na
    pendingLongMinLow := usePeakHold ? longPeakPrice : low
    int longPeakOffset = usePeakHold and not na(longPeakBarIndex) ? (bar_index - longPeakBarIndex) : 0
    pendingLongAtr := usePeakHold ? atr[longPeakOffset] : atr
    pendingLongPeakZ := usePeakHold ? longPeakZ : zL

if shortTrigger
    pendingShort := true
    pendingShortPeakBar := usePeakHold ? shortPeakBarIndex : bar_index
    pendingShortPeakPrice := usePeakHold ? shortPeakPrice : high
    pendingShortPeakClose := usePeakHold ? shortPeakClose : close
    pendingShortPeakLow := usePeakHold ? shortPeakLow : low
    pendingShortMinLow := usePeakHold ? shortPeakMinLow : na
    pendingShortMaxHigh := usePeakHold ? shortPeakPrice : high
    int shortPeakOffset = usePeakHold and not na(shortPeakBarIndex) ? (bar_index - shortPeakBarIndex) : 0
    pendingShortAtr := usePeakHold ? atr[shortPeakOffset] : atr
    pendingShortPeakZ := usePeakHold ? shortPeakZ : zS

if pendingLong
    // Follow-through: highest high of the bars AFTER the peak bar, measured from the peak bar's close
    if bar_index > pendingLongPeakBar
        pendingLongMaxHigh := math.max(nz(pendingLongMaxHigh, high), high)
    pendingLongMinLow := math.min(nz(pendingLongMinLow, low), low)
    if bar_index - pendingLongPeakBar >= reactBars
        longDenom = math.max(nz(pendingLongAtr, atr), 0.0001)
        longFollow = (pendingLongMaxHigh - pendingLongPeakClose) / longDenom
        longScore := math.max(0.0, math.min(1.0, longFollow / minFollowATR))
        longEventAccepted := longFollow >= minFollowATR
        longEventRejected := longFollow < minFollowATR
        lastLongEventPeakBar := pendingLongPeakBar
        lastLongEventPeakPrice := pendingLongPeakPrice
        lastLongEventPeakHigh := pendingLongPeakHigh
        lastLongEventPeakAtr := pendingLongAtr
        lastLongEventPeakZ := pendingLongPeakZ
        lastLongEventEndClose := close
        eventSeq += 1
        lastLongEventLabelId := eventSeq
        if enableLogging
            array.push(stressBars, lastLongEventPeakBar)
            array.push(stressTypes, -1)
            array.push(stressUpExpect, 0)
            array.push(stressAccept, longEventAccepted ? 1 : 0)
            array.push(stressLabelIds, lastLongEventLabelId)
        if enableLogging and barstate.isconfirmed
            int barsToPivot = na(lastPivotLowBar) ? na : (pendingLongPeakBar - lastPivotLowBar)
            string nearPivot = not na(barsToPivot) and math.abs(barsToPivot) <= pivotNearBars ? "near" : "far"
            bool contBreak = not na(lastPivotLow) and pendingLongMinLow < lastPivotLow
            log.info("STRESS_DOWN,{0},label=#{1},peakZ={2},score={3},pivotBars={4},pivotNear={5},pivotLevel={6},contBreak={7},bar={8}",
                longEventAccepted ? "ACCEPT" : "REJECT",
                str.tostring(lastLongEventLabelId),
                str.tostring(pendingLongPeakZ, "#.00"),
                str.tostring(longScore, "#.00"),
                str.tostring(barsToPivot),
                nearPivot,
                str.tostring(lastPivotLow, format.mintick),
                str.tostring(contBreak),
                str.tostring(bar_index))
        pendingLong := false
        pendingLongAtr := na
        lastLongEvent := bar_index

if pendingShort
    // Follow-through: lowest low of the bars AFTER the peak bar, measured from the peak bar's close
    if bar_index > pendingShortPeakBar
        pendingShortMinLow := math.min(nz(pendingShortMinLow, low), low)
    pendingShortMaxHigh := math.max(nz(pendingShortMaxHigh, high), high)
    if bar_index - pendingShortPeakBar >= reactBars
        shortDenom = math.max(nz(pendingShortAtr, atr), 0.0001)
        shortFollow = (pendingShortPeakClose - pendingShortMinLow) / shortDenom
        shortScore := math.max(0.0, math.min(1.0, shortFollow / minFollowATR))
        shortEventAccepted := shortFollow >= minFollowATR
        shortEventRejected := shortFollow < minFollowATR
        lastShortEventPeakBar := pendingShortPeakBar
        lastShortEventPeakPrice := pendingShortPeakPrice
        lastShortEventPeakLow := pendingShortPeakLow
        lastShortEventPeakAtr := pendingShortAtr
        lastShortEventPeakZ := pendingShortPeakZ
        lastShortEventEndClose := close
        eventSeq += 1
        lastShortEventLabelId := eventSeq
        if enableLogging
            array.push(stressBars, lastShortEventPeakBar)
            array.push(stressTypes, 1)
            bool upRegime = (biasSmCore < -biasBand) and trendStrengthOk
            array.push(stressUpExpect, useRegimeMapping and upRegime ? 1 : 0)
            array.push(stressAccept, shortEventAccepted ? 1 : 0)
            array.push(stressLabelIds, lastShortEventLabelId)
        if enableLogging and barstate.isconfirmed
            int barsToPivot = na(lastPivotHighBar) ? na : (pendingShortPeakBar - lastPivotHighBar)
            string nearPivot = not na(barsToPivot) and math.abs(barsToPivot) <= pivotNearBars ? "near" : "far"
            bool contBreak = not na(lastPivotHigh) and pendingShortMaxHigh > lastPivotHigh
            log.info("STRESS_UP,{0},label=#{1},peakZ={2},score={3},pivotBars={4},pivotNear={5},pivotLevel={6},contBreak={7},bar={8}",
                shortEventAccepted ? "ACCEPT" : "REJECT",
                str.tostring(lastShortEventLabelId),
                str.tostring(pendingShortPeakZ, "#.00"),
                str.tostring(shortScore, "#.00"),
                str.tostring(barsToPivot),
                nearPivot,
                str.tostring(lastPivotHigh, format.mintick),
                str.tostring(contBreak),
                str.tostring(bar_index))
        pendingShort := false
        pendingShortAtr := na
        lastShortEvent := bar_index


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

//──────────────────────────────────────────────────────────────────────────────
// Panel plots (Z Long up, Z Short down)
//──────────────────────────────────────────────────────────────────────────────

useIntuitiveColors = stressColorMode == "Intuitive"
colDown = useIntuitiveColors ? color.lime : color.red
colUp = useIntuitiveColors ? color.red : color.lime

plot(zL, "Downside Stress (Dip)", color=color.new(colDown, 0), linewidth=2, display=display.pane)
plot(-zS, "Upside Stress (Blow-off)", color=color.new(colUp, 0), linewidth=2, display=display.pane)

zeroLine = zeroDashed ? ((bar_index % dashPeriod) < dashOn ? 0.0 : na) : 0.0
zeroCtx = math.max(math.abs(zL), math.abs(zS)) > zeroContextX
zeroLineCtx = (zeroContextOnly and not zeroCtx) ? na : zeroLine
plot(zeroLineCtx, "Zero", color=color.new(color.gray, 70), linewidth=1, display=display.pane)

plot(jma, "JMA", color=color.new(color.orange, 0), display=display.none)

showBiasLine = input.bool(true, "Bias: show line", group=groupDisplay)
showBiasBand = input.bool(true, "Bias: show band lines", group=groupDisplay)
showBiasFill = input.bool(true, "Bias: fill when usable", group=groupDisplay)

biasRaw = zL - zS
biasSm = ta.ema(biasRaw, biasSmoothLen)
b = biasBand

inDown = biasSm > b
inUp = biasSm < -b

biasCol = inDown ? color.new(colDown, 55) : inUp ? color.new(colUp, 55) : color.new(color.gray, 60)

pBias = plot(showBiasLine ? biasSm : na, "Macro Bias", color=biasCol, linewidth=1, style=plot.style_linebr, display=display.pane)
pUp = plot(showBiasBand ? b : na, "+BiasBand", color=color.new(color.gray, 88), linewidth=1, display=display.pane)
pDn = plot(showBiasBand ? -b : na, "-BiasBand", color=color.new(color.gray, 88), linewidth=1, display=display.pane)

fill(pBias, pUp, color=(showBiasFill and inDown) ? color.new(colDown, 98) : na)
fill(pBias, pDn, color=(showBiasFill and inUp) ? color.new(colUp, 98) : na)

bgcolor(showPaneRegimeBg and inDown ? color.new(colDown, paneBgAlpha) : showPaneRegimeBg and inUp ? color.new(colUp, paneBgAlpha) : na)

plotshape(showPaneCross and ta.crossover(biasSm, 0), style=shape.triangleup, location=location.bottom, size=size.tiny, color=color.new(colDown, 0), display=display.pane)
plotshape(showPaneCross and ta.crossunder(biasSm, 0), style=shape.triangledown, location=location.bottom, size=size.tiny, color=color.new(colUp, 0), display=display.pane)


longScoreSm = ta.ema(longScore, accScoreSmooth)
shortScoreSm = ta.ema(shortScore, accScoreSmooth)
accStyle = accScoreStyle == "Histogram" ? plot.style_histogram : plot.style_line
plot(showAccScore ? longScoreSm : na, "AccScore Downside", color=color.new(colDown, 0), linewidth=2, display=display.pane, style=accStyle)
plot(showAccScore ? -shortScoreSm : na, "AccScore Upside", color=color.new(colUp, 0), linewidth=2, display=display.pane, style=accStyle)

// (Candle coloring removed for pane-only mode)

//──────────────────────────────────────────────────────────────────────────────
// Markers (Peak-hold uses offset so marker lands on peak bar)
//──────────────────────────────────────────────────────────────────────────────

var array<label> labelQueue = array.new_label()
var array<line> lineQueue = array.new_line()
var int infoSeq = 0

f_labelPush(_lbl) =>
    array.push(labelQueue, _lbl)
    if array.size(labelQueue) > labelFifoMax
        old = array.shift(labelQueue)
        if not na(old)
            label.delete(old)

f_linePush(_ln) =>
    if not na(_ln)
        array.push(lineQueue, _ln)
        if array.size(lineQueue) > lineFifoMax
            old = array.shift(lineQueue)
            if not na(old)
                line.delete(old)

var box longZone = na
var box shortZone = na
var bool longZoneActive = false
var bool shortZoneActive = false
var array<box> zoneHistory = array.new_box()

f_pushZone(_bx) =>
    array.push(zoneHistory, _bx)
    if array.size(zoneHistory) > zoneHistoryMax
        old = array.shift(zoneHistory)
        if not na(old)
            box.delete(old)

f_finalizeZone(box _bx) =>
    if not na(_bx)
        box.set_right(_bx, bar_index)
        f_pushZone(_bx)

if longEventAccepted or longEventRejected
    f_finalizeZone(longZone)
    longZone := na
    zVal = nz(lastLongEventPeakZ, zL)
    zoneColor = longEventAccepted ? color.new(colDown, zoneAlpha) : color.new(colDown, math.min(95, zoneAlpha + 15))
    longZone := box.new(lastLongEventPeakBar, zVal + zoneHalfHeight, bar_index, zVal - zoneHalfHeight, bgcolor=zoneColor, border_color=color.new(colDown, 0))
    longZoneActive := true

if shortEventAccepted or shortEventRejected
    f_finalizeZone(shortZone)
    shortZone := na
    zVal = -nz(lastShortEventPeakZ, zS)
    zoneColor = shortEventAccepted ? color.new(colUp, zoneAlpha) : color.new(colUp, math.min(95, zoneAlpha + 15))
    shortZone := box.new(lastShortEventPeakBar, zVal + zoneHalfHeight, bar_index, zVal - zoneHalfHeight, bgcolor=zoneColor, border_color=color.new(colUp, 0))
    shortZoneActive := true

if longZoneActive and not na(longZone)
    box.set_right(longZone, bar_index)
    if zL < zThrLong
        longZoneActive := false
        f_finalizeZone(longZone)
        longZone := na

if shortZoneActive and not na(shortZone)
    box.set_right(shortZone, bar_index)
    if zS < zThrShort
        shortZoneActive := false
        f_finalizeZone(shortZone)
        shortZone := na

txtDownAcc = labelMode == "Directional" ? "DA" : "LA"
txtDownRej = labelMode == "Directional" ? "DR" : "LR"
txtUpAcc = labelMode == "Directional" ? "UA" : "SA"
txtUpRej = labelMode == "Directional" ? "UR" : "SR"

if showChartPeakLabels and (longEventAccepted or longEventRejected)
    y = lastLongEventPeakHigh + nz(lastLongEventPeakAtr, atr) * chartLabelOffsetATR
    txt = longEventAccepted ? txtDownAcc : txtDownRej
    upRegimeVis = (biasSmCore < -biasBand) and trendStrengthOk
    tip = str.format( "Downside stress {0}\nPeak Z: {1}\nFollow score: {2}\nMinFollowATR: {3}\nReactBars: {4}", longEventAccepted ? "accepted" : "rejected", str.tostring(lastLongEventPeakZ, "#.00"), str.tostring(longScore, "#.00"), str.tostring(minFollowATR, "#.00"), str.tostring(reactBars) )
    tip += "\nBiasCore: " + str.tostring(biasSmCore, "#.00") + " | upRegime: " + str.tostring(upRegimeVis)
    tip += "\nADX grade: " + str.tostring(adxGrade, "#.00") + " (quality only, non-blocking)"
    tip := "#" + str.tostring(lastLongEventLabelId) + " " + tip
    lbl = label.new(lastLongEventPeakBar, y, txt, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_down, textcolor=color.white, color=color.new(colDown, 0), size=size.small, force_overlay=true, tooltip=tip)
    f_labelPush(lbl)

if showChartPeakLabels and (shortEventAccepted or shortEventRejected)
    y = lastShortEventPeakLow - nz(lastShortEventPeakAtr, atr) * chartLabelOffsetATR
    txt = shortEventAccepted ? txtUpAcc : txtUpRej
    upRegimeVis = (biasSmCore < -biasBand) and trendStrengthOk
    tip = str.format( "Upside stress {0}\nPeak Z: {1}\nFollow score: {2}\nMinFollowATR: {3}\nReactBars: {4}", shortEventAccepted ? "accepted" : "rejected", str.tostring(lastShortEventPeakZ, "#.00"), str.tostring(shortScore, "#.00"), str.tostring(minFollowATR, "#.00"), str.tostring(reactBars) )
    tip += "\nBiasCore: " + str.tostring(biasSmCore, "#.00") + " | upRegime: " + str.tostring(upRegimeVis)
    tip += "\nADX grade: " + str.tostring(adxGrade, "#.00") + " (quality only, non-blocking)"
    tip := "#" + str.tostring(lastShortEventLabelId) + " " + tip
    lbl = label.new(lastShortEventPeakBar, y, txt, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_up, textcolor=color.white, color=color.new(colUp, 0), size=size.small, force_overlay=true, tooltip=tip)
    f_labelPush(lbl)

var int lastClarityBar = na
allowClarity = na(lastClarityBar) ? true : (bar_index - lastClarityBar >= clarityCooldown)

windWvfLen = f_sel(windTf, wvf_15, wvf_1h, wvf_4h, wvf_D, wvf_W, wvf_M)
windZWin = f_sel(windTf, z_15, z_1h, z_4h, z_D, z_W, z_M)
windJmaLen = f_sel(windTf, jma_15, jma_1h, jma_4h, jma_D, jma_W, jma_M)

f_windZ(_len, _win, _high, _low, _close) =>
    _hhc = ta.highest(_close, _len)
    _llc = ta.lowest(_close, _len)
    _wvfLong = _hhc == 0 ? na : (_hhc - _low) / _hhc * 100.0
    _wvfShort = _llc == 0 ? na : (_high - _llc) / _llc * 100.0
    _zL = f_z(_wvfLong, _win)
    _zS = f_z(_wvfShort, _win)
    [_zL, _zS]

[windZL, windZS] = request.security(syminfo.tickerid, windTf, f_windZ(windWvfLen, windZWin, high, low, close), barmerge.gaps_off, barmerge.lookahead_off)
windBiasRaw = windZL - windZS
windBiasSm = calculateJMA(windBiasRaw, windJmaLen, jmaPhase, jmaPower)
windDown = windBiasSm > windBand
windUp = windBiasSm < -windBand
windFlip = (windDown and not windDown[1]) or (windUp and not windUp[1])
var int lastWindBar = na
allowWind = na(lastWindBar) ? true : (bar_index - lastWindBar >= windCooldown)

if showWindLabel and windFlip and allowWind
    txt = windDown ? ("HTF " + windTf + " DS") : ("HTF " + windTf + " US")
    windCol = windDown ? color.new(colDown, 35) : color.new(colUp, 35)
    tip = str.format( "HTF bias flip ({0})\nBias: {1}\nBand: {2}\nJMA len: {3}", windTf, str.tostring(windBiasSm, "#.00"), str.tostring(windBand, "#.00"), str.tostring(windJmaLen) )
    infoSeq += 1
    tip := "i#" + str.tostring(infoSeq) + " " + tip
    lbl = label.new(bar_index, high, txt, style=label.style_label_down, textcolor=color.white, color=windCol, size=size.small, force_overlay=true, tooltip=tip)
    f_labelPush(lbl)
    lastWindBar := bar_index

absBias = math.abs(biasSm)
regime = inDown ? 1 : inUp ? -1 : 0
regimeEnter = (regime != 0) and (regime != nz(regime[1], 0))
aligned = (regime == 1 and windDown) or (regime == -1 and windUp)
clarityEvent = regimeEnter and aligned and absBias >= clarityThr

if showClarityLabel and clarityEvent and allowClarity
    isDown = regime == 1
    dirTxt = isDown ? "CLR D" : "CLR U"
    clrCol = color.new(color.gray, 65)
    y = isDown ? (high + atr * chartLabelOffsetATR) : (low - atr * chartLabelOffsetATR)
    st = isDown ? label.style_label_down : label.style_label_up

    tip = str.format( "Bias regime entry (aligned)\nBias: {0}\nBand: {1}\nHTF: {2}\nabsBias: {3} (thr {4})", str.tostring(biasSm, "#.00"), str.tostring(b, "#.00"), isDown ? "Downside" : "Upside", str.tostring(absBias, "#.00"), str.tostring(clarityThr, "#.00") )
    infoSeq += 1
    tip := "i#" + str.tostring(infoSeq) + " " + tip
    if enableLogging and barstate.isconfirmed
        log.info("CLARITY,info=#{0},dir={1},absBias={2},thr={3},bias={4},bar={5}",
            str.tostring(infoSeq),
            isDown ? "DOWN" : "UP",
            str.tostring(absBias, "#.00"),
            str.tostring(clarityThr, "#.00"),
            str.tostring(biasSm, "#.00"),
            str.tostring(bar_index))
    lbl = label.new(bar_index, y, dirTxt, xloc=xloc.bar_index, yloc=yloc.price, style=st, textcolor=color.white, color=clrCol, size=size.small, force_overlay=true, tooltip=tip)
    f_labelPush(lbl)
    lastClarityBar := bar_index

// Alerts
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (longEventAccepted), "MSO Downside Accepted", "MSO · DOWNSIDE ACCEPTED · {{ticker}} {{interval}}")
alertcondition(alertOk and (longEventRejected), "MSO Downside Rejected", "MSO · DOWNSIDE REJECTED · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortEventAccepted), "MSO Upside Accepted", "MSO · UPSIDE ACCEPTED · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortEventRejected), "MSO Upside Rejected", "MSO · UPSIDE REJECTED · {{ticker}} {{interval}}")
alertcondition(alertOk and (clarityEvent), "MSO Clarity Regime Entry", "MSO · CLARITY REGIME ENTRY · {{ticker}} {{interval}}")
alertcondition(alertOk and (windFlip), "MSO HTF Wind Flip", "MSO · HTF WIND FLIP · {{ticker}} {{interval}}")

if showReactionLine and (longEventAccepted or longEventRejected)
    ln = line.new(lastLongEventPeakBar, lastLongEventPeakPrice, lastLongEventPeakBar + reactBars, lastLongEventEndClose, xloc=xloc.bar_index, color=color.new(color.gray, 60), width=1, extend=extend.none, force_overlay=true)
    f_linePush(ln)

if showReactionLine and (shortEventAccepted or shortEventRejected)
    ln = line.new(lastShortEventPeakBar, lastShortEventPeakPrice, lastShortEventPeakBar + reactBars, lastShortEventEndClose, xloc=xloc.bar_index, color=color.new(color.gray, 60), width=1, extend=extend.none, force_overlay=true)
    f_linePush(ln)

if showLevelLines and (longEventAccepted or longEventRejected)
    ln = line.new(lastLongEventPeakBar, lastLongEventPeakPrice, lastLongEventPeakBar + levelLineBars, lastLongEventPeakPrice, xloc=xloc.bar_index, color=color.new(colDown, levelLineAlpha), width=1, extend=extend.none, force_overlay=true)
    f_linePush(ln)

if showLevelLines and (shortEventAccepted or shortEventRejected)
    ln = line.new(lastShortEventPeakBar, lastShortEventPeakPrice, lastShortEventPeakBar + levelLineBars, lastShortEventPeakPrice, xloc=xloc.bar_index, color=color.new(colUp, levelLineAlpha), width=1, extend=extend.none, force_overlay=true)
    f_linePush(ln)


//──────────────────────────────────────────────────────────────────────────────
// Preset summary table (screen-fixed)
//──────────────────────────────────────────────────────────────────────────────

var table presetTbl = table.new(position.top_right, 1, 2,
     bgcolor=color.new(color.white, 5),
     border_color=color.new(color.gray, 60),
     border_width=1,
     frame_color=color.new(color.gray, 40),
     frame_width=1)

if showPresetSummary and barstate.islast
    txt = str.format( "TFUsed: {0}\nWVF: {1} | ZWin: {2} | ZThr: {3}\nJMA: {4} | ADXmin: {5}\nMinExt: {6} | Cooldown: {7} | Follow: {8}", tfUsed, str.tostring(wvfLen), str.tostring(zWin), str.tostring(zThr, "#.0"), str.tostring(jmaLen), str.tostring(adxMin), str.tostring(minExtremeBars), str.tostring(cooldownBars), str.tostring(minFollowATR, "#.00") )
    table.cell(presetTbl, 0, 0, "MSO Presets", text_color=color.new(color.gray, 20), bgcolor=color.new(color.gray, 90), text_size=size.small)
    table.cell(presetTbl, 0, 1, txt, text_color=color.new(color.gray, 20), text_size=size.small)
else if not showPresetSummary and barstate.islast
    table.clear(presetTbl, 0, 0, 0, 1)

Overview

MSO - Market Stress Oscillator [WavesUnchained] applies JMA smoothing to both series and shows ADX strength as context for judging noisy readings. Peak-hold markers mark stress extremes. A preset selects Auto or a fixed timeframe from 15 minutes through monthly, loading matching WVF lookbacks, z-score windows, z-score thresholds, JMA lengths, and ADX minimums. Stress color mode switches intuitive and technical palettes. It does not issue trade entries or exits.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

Oscillators & momentum, Volatility & bands

Markets

Any symbol

Timeframes

M15-H1, H4 and above

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
Stress Color ModestringIntuitivestressColorMode
Preset TimeframestringAutopresetTF
WVF 15mint34wvf_15
WVF 1hint34wvf_1h
WVF 4hint55wvf_4h
WVF Dint89wvf_D
WVF Wint89wvf_W
WVF Mint144wvf_M
ZWin 15mint144z_15
ZWin 1hint144z_1h
ZWin 4hint200z_4h
ZWin Dint252z_D
Show all 40 settings
SettingTypeDefaultVariable
ZWin Wint252z_W
ZWin Mint300z_M
ZThr 15mfloat2.0zThr_15
ZThr 1hfloat2.0zThr_1h
ZThr 4hfloat2.1zThr_4h
ZThr Dfloat2.1zThr_D
ZThr Wfloat2.2zThr_W
ZThr Mfloat2.5zThr_M
JMA 15mint55jma_15
JMA 1hint89jma_1h
JMA 4hint144jma_4h
JMA Dint200jma_D
JMA Wint233jma_W
JMA Mint377jma_M
ADX Min 15mint18adxMin_15
ADX Min 1hint18adxMin_1h
ADX Min 4hint18adxMin_4h
ADX Min Dint18adxMin_D
ADX Min Wint18adxMin_W
ADX Min Mint18adxMin_M
MinExtreme 15mint2minExt_15
MinExtreme 1hint2minExt_1h
MinExtreme 4hint2minExt_4h
MinExtreme Dint3minExt_D
MinExtreme Wint3minExt_W
MinExtreme Mint3minExt_M
Cooldown 15mint12cool_15
Cooldown 1hint8cool_1h
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

TTM Squeeze

by Alorse
330 stars

This indicator plots a TTM Squeeze momentum oscillator in a separate pane. It compares a Bollinger Band offset to a Keltner Channel offset and marks the zero line white when the difference is non-negative and black when the bands are inside the channel. A line

Indicator v5 Oscillators & momentumVolatility & bands
Updated 2 months ago · None stated GitHub repository of TTM Squeeze

Function - Logistic Equation

by ricardosantos79
261 stars

Calculates dynamic modeling values by applying a mathematical logistic differential equation to price change over a specified lookback length. It measures price momentum relative to the recent high-low price range. The script plots positive and negative logist

Indicator v5 Oscillators & momentumVolatility & bands
Updated 4 years ago · None stated GitHub repository of Function - Logistic Equation

MACD-V

by harryguiacorn
219 stars

MACD-V is a volatility-normalized momentum indicator based on research by Alex Spiroglou. It calculates the standard MACD histogram and normalizes it by dividing by the Average True Range (ATR). Plotted in a separate pane as a colored histogram, it includes us

Indicator v5 Oscillators & momentumVolatility & bands
Updated 1 year ago · None stated GitHub repository of MACD-V
218 stars

This indicator detects a Bollinger Band squeeze when the bands sit inside Keltner Channels, marking low volatility. It plots red dots during a squeeze and green dots when the bands are outside, in a separate pane. An optional histogram shows momentum as the cl

Indicator v6 Volatility & bandsOscillators & momentum
Updated 3 months ago · None stated GitHub repository of Bollinger Band Squeeze Detector
147 stars

This indicator generates trade entry signals and target levels optimized for 1-hour timeframes, designed for external signal extraction. It calculates 7-period and 16-period EMAs alongside 10-period RSI and ATR filters. Long signals occur when the fast EMA cro

Indicator v5 TrendOscillators & momentum
Updated 6 weeks ago · MIT GitHub repository of Signal Strategy 100 Candles of 1H Timeframe

Bubbles Indicator

by capissimo
114 stars

The Bubbles Indicator measures volatility squeezes and expansion bursts using Bollinger Bands combined with a sinusoid oscillator and RSI divergence. It processes price data through scaled Bollinger Bands to highlight volatility changes while confirming trend

Indicator v4 Volatility & bandsOscillators & momentum Repaints
Updated 1 year ago · None stated GitHub repository of Bubbles Indicator

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