//@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.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Stress Color Mode
string
Intuitive
stressColorMode
Preset Timeframe
string
Auto
presetTF
WVF 15m
int
34
wvf_15
WVF 1h
int
34
wvf_1h
WVF 4h
int
55
wvf_4h
WVF D
int
89
wvf_D
WVF W
int
89
wvf_W
WVF M
int
144
wvf_M
ZWin 15m
int
144
z_15
ZWin 1h
int
144
z_1h
ZWin 4h
int
200
z_4h
ZWin D
int
252
z_D
Show all 40 settings
Setting
Type
Default
Variable
ZWin W
int
252
z_W
ZWin M
int
300
z_M
ZThr 15m
float
2.0
zThr_15
ZThr 1h
float
2.0
zThr_1h
ZThr 4h
float
2.1
zThr_4h
ZThr D
float
2.1
zThr_D
ZThr W
float
2.2
zThr_W
ZThr M
float
2.5
zThr_M
JMA 15m
int
55
jma_15
JMA 1h
int
89
jma_1h
JMA 4h
int
144
jma_4h
JMA D
int
200
jma_D
JMA W
int
233
jma_W
JMA M
int
377
jma_M
ADX Min 15m
int
18
adxMin_15
ADX Min 1h
int
18
adxMin_1h
ADX Min 4h
int
18
adxMin_4h
ADX Min D
int
18
adxMin_D
ADX Min W
int
18
adxMin_W
ADX Min M
int
18
adxMin_M
MinExtreme 15m
int
2
minExt_15
MinExtreme 1h
int
2
minExt_1h
MinExtreme 4h
int
2
minExt_4h
MinExtreme D
int
3
minExt_D
MinExtreme W
int
3
minExt_W
MinExtreme M
int
3
minExt_M
Cooldown 15m
int
12
cool_15
Cooldown 1h
int
8
cool_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.
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
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
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
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
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
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