//@version=6
// ============================================================================
// Market Exhaustion [WavesUnchained]
// Version: 1.2.2
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// MFI + StochRSI fusion with optional JMA smoothing, HTF context, signals, and divergences
//
// Features:
// ✓ Adaptive MFI zones with optional background highlights
// ✓ Auto-HTF MFI context with bias label
// ✓ StochRSI timing overlay
// ✓ Context + timing signals
// ✓ Regular divergences on MFI
// ✓ Alert conditions for long/short signals
//
// Data Contract:
// Price: REQUIRED chart symbol
// Volume: OPTIONAL real trade volume only — MFI parts go na without it; CCI mode works
// OI: NO
// Reference: NO
// Verdict: CFD-degraded
//
// ============================================================================
indicator("Market Exhaustion [WavesUnchained]", format=format.price, shorttitle="Market Exhaustion", precision=0, max_lines_count=300, max_labels_count=500)
// MODULES:
// - inputs.pine
// - helpers.pine
// - core.pine
// - visuals.pine
// - signals.pine
// - divergences.pine
// ═══════════════════════════════════════════════════════════════════════════
// NOTE: Modules are combined by the build system.
// ============================================================================
// INLINE MODULES (Combined at build time)
// ============================================================================
// ============================================================================
// MODULE: inputs
// ============================================================================
//────────────────────────────────────────────────────────────────────
// Inputs
//────────────────────────────────────────────────────────────────────
groupCore = "Core"
src = input.source(hlc3, "Source", group=groupCore)
mfiLen = input.int(14, "MFI Length", minval=1, group=groupCore)
groupOsc = "Oscillator"
oscMode = input.string("MFI", "Oscillator mode", options=["MFI","CCI","MFI+CCI"], group=groupOsc)
oscEngine = input.string("MFI", "Engine source (for MFI+CCI)", options=["MFI","CCI"], group=groupOsc)
cciLen = input.int(34, "CCI length", minval=1, group=groupOsc)
cciSrc = input.source(hlc3, "CCI source", group=groupOsc)
cciMapScale = input.float(150.0, "CCI map scale (higher = flatter)", minval=50.0, maxval=500.0, step=10.0, group=groupOsc)
groupZones = "Zones"
useDynZones = input.bool(true, "Use dynamic zones (adaptive)", group=groupZones)
zoneLookback = input.int(200, "Dynamic zone lookback", minval=50, group=groupZones)
dynUpperFrac = input.float(0.85, "Upper fraction (0..1)", minval=0.50, maxval=1.00, step=0.01, group=groupZones)
dynLowerFrac = input.float(0.15, "Lower fraction (0..1)", minval=0.00, maxval=0.50, step=0.01, group=groupZones)
staticUpper = input.int(80, "Static Upper", minval=50, maxval=100, group=groupZones)
staticLower = input.int(20, "Static Lower", minval=0, maxval=50, group=groupZones)
showBgZones = input.bool(true, "Background highlight OB/OS", group=groupZones)
showBandFill = input.bool(true, "Fill between zones", group=groupZones)
showMidLine = input.bool(true, "Show midline (50)", group=groupZones)
groupViz = "Visuals"
mfiColorOB = input.color(color.new(color.red, 0), "MFI color OB", group=groupViz)
mfiColorOS = input.color(color.new(color.lime, 0), "MFI color OS", group=groupViz)
mfiColorNeutral = input.color(color.new(color.gray, 0), "MFI color neutral", group=groupViz)
stochColor = input.color(color.new(color.aqua, 55), "StochRSI K color", group=groupViz)
htfMfiColor = input.color(color.new(color.orange, 35), "HTF MFI color", group=groupViz)
cciColor = input.color(color.new(color.blue, 35), "CCI mapped color", group=groupViz)
zoneUpperColor = input.color(color.new(color.red, 75), "Zone upper line", group=groupViz)
zoneLowerColor = input.color(color.new(color.lime, 75), "Zone lower line", group=groupViz)
midlineColor = input.color(color.new(color.gray, 92), "Midline color", group=groupViz)
groupStoch = "StochRSI"
useStoch = input.bool(true, "Show StochRSI K", group=groupStoch)
rsiLen = input.int(14, "RSI Length", minval=1, group=groupStoch)
stochLen = input.int(14, "Stoch Length", minval=1, group=groupStoch)
kSmooth = input.int(3, "K smoothing", minval=1, group=groupStoch)
stochOS = input.int(20, "StochRSI OS level", minval=1, maxval=49, group=groupStoch)
stochOB = input.int(80, "StochRSI OB level", minval=51, maxval=99, group=groupStoch)
groupSignals = "Signals"
showSignals = input.bool(true, "Show context+timing signals", group=groupSignals)
needConfirm = input.int(1, "Zone confirm bars (0..5)", minval=0, maxval=5, group=groupSignals)
groupJMA = "JMA smoothing"
useJMA = input.bool(true, "Enable JMA smoothing", group=groupJMA)
jmaLen = input.int(20, "JMA Length", minval=1, group=groupJMA)
jmaPhase = input.int(0, "JMA Phase (-100..100)", minval=-100, maxval=100, group=groupJMA)
jmaPower = input.float(2.0, "JMA Power (1..8)", minval=1.0, maxval=8.0, step=0.1, group=groupJMA)
smoothMFI = input.bool(true, "Apply JMA to MFI", group=groupJMA)
smoothStoch = input.bool(true, "Apply JMA to StochRSI K", group=groupJMA)
groupMTF = "MTF context"
useMTF = input.bool(true, "Show HTF MFI (default on)", group=groupMTF)
useAutoTF = input.bool(true, "Auto-select HTF", group=groupMTF)
manualTF = input.timeframe("60", "Manual HTF (fallback)", group=groupMTF)
useHTFFilter = input.bool(false, "Use HTF as signal filter", group=groupMTF)
mtfSmooth = input.bool(true, "Smooth HTF MFI", group=groupMTF)
mtfEmaLen = input.int(10, "HTF EMA length (fallback)", minval=1, group=groupMTF)
biasLevel = input.int(50, "HTF bias level", minval=1, maxval=99, group=groupMTF)
showBiasLabel = input.bool(true, "Show HTF bias label (top)", group=groupMTF)
biasLabelOffset = input.int(50, "Bias label offset bars", minval=0, maxval=200, group=groupMTF)
groupDiv = "Divergences"
showDiv = input.bool(true, "Show divergences", group=groupDiv)
maxDivs = input.int(30, "Max divergence objects", minval=5, maxval=200, group=groupDiv)
requireOSOB = input.bool(true, "Require OS/OB area", group=groupDiv)
osobMode = input.string("StartOrEnd", "OS/OB check", options=["End","StartOrEnd","Both"], group=groupDiv)
osobBufferPct_in = input.float(0.10, "OS/OB buffer (% of band)", minval=0.0, maxval=0.50, step=0.01, group=groupDiv)
useAtrFilter = input.bool(true, "ATR swing filter", group=groupDiv)
divAtrLen = input.int(14, "ATR length", minval=1, group=groupDiv)
showDivScore = input.bool(true, "Show score instead of text", group=groupDiv)
showScoreLabel = input.bool(true, "Show mid score label", group=groupDiv)
scoreStrong = input.int(65, "Strong score threshold", minval=0, maxval=100, group=groupDiv)
divScoreScale = input.float(2.0, "Score scale", minval=0.5, maxval=10.0, step=0.1, group=groupDiv)
reactBars = input.int(10, "Reaction window (bars)", minval=2, maxval=100, group=groupDiv,
tooltip="Bars after the pivot over which the price reaction is measured (from the pivot-bar close). The divergence is drawn at pivot confirmation with a pending score; the score, tooltip and line width are finalized once this many bars after the pivot have passed.")
reactAtrFrac = input.float(0.35, "Reaction threshold (ATR frac)", minval=0.05, maxval=2.0, step=0.05, group=groupDiv)
useBiasBonus = input.bool(false, "Bias bonus (needs HTF bias)", group=groupDiv)
divProfile = input.string("More Signals", "Divergence profile", options=["More Signals","Conservative"], group=groupDiv)
usePreset = input.bool(true, "Use preset values", group=groupDiv)
pivLeft_in = input.int(7, "Pivot Left", minval=1, maxval=20, group=groupDiv)
pivRight_in = input.int(7, "Pivot Right", minval=1, maxval=20, group=groupDiv)
minMfiDelta_in = input.int(15, "Min MFI delta", minval=0, maxval=100, group=groupDiv)
minAtrMove_in = input.float(0.75, "Min swing (ATR)", minval=0.0, maxval=10.0, step=0.05, group=groupDiv)
dirPriceEps_in = input.float(0.0010, "Dir tolerance price (%)", minval=0.0, maxval=0.02, step=0.0001, group=groupDiv)
dirMfiEps_in = input.float(1.0, "Dir tolerance MFI (pts)", minval=0.0, maxval=10.0, step=0.1, group=groupDiv)
tfMinDiv = timeframe.in_seconds(timeframe.period) / 60.0
divPresetAuto = tfMinDiv <= 5 ? "5m" : tfMinDiv <= 15 ? "15m" : tfMinDiv <= 60 ? "1H" : tfMinDiv <= 240 ? "4H" : "1D"
pivL_p = divPresetAuto == "5m" ? 3 : divPresetAuto == "15m" ? 5 : divPresetAuto == "1H" ? 6 : divPresetAuto == "4H" ? 5 : 9
pivR_p = pivL_p
mfiD_base = divPresetAuto == "5m" ? 5 : divPresetAuto == "15m" ? 7 : divPresetAuto == "1H" ? 9 : divPresetAuto == "4H" ? 10 : 16
buf_base = divPresetAuto == "5m" ? 0.10 : divPresetAuto == "15m" ? 0.25 : divPresetAuto == "1H" ? 0.12 : divPresetAuto == "4H" ? 0.30 : 0.16
atr_base = divPresetAuto == "5m" ? 0.25 : divPresetAuto == "15m" ? 0.45 : divPresetAuto == "1H" ? 0.60 : divPresetAuto == "4H" ? 0.70 : 1.00
mfiEps_base = divPresetAuto == "5m" ? 0.8 : divPresetAuto == "15m" ? 1.0 : divPresetAuto == "1H" ? 1.2 : divPresetAuto == "4H" ? 1.5 : 2.0
priceEps_base = divPresetAuto == "5m" ? 0.0016 : divPresetAuto == "15m" ? 0.0015 : divPresetAuto == "1H" ? 0.0012 : divPresetAuto == "4H" ? 0.0010 : 0.0008
mfiD_p = divProfile == "Conservative" ? (mfiD_base + 4) : math.max(1, mfiD_base - 2)
buf_p = divProfile == "Conservative" ? math.max(0.05, buf_base - 0.07) : math.min(0.35, buf_base + 0.05)
atr_p = divProfile == "Conservative" ? (atr_base + 0.30) : math.max(0.15, atr_base - 0.15)
dirMfi_p = divProfile == "Conservative" ? math.max(0.2, mfiEps_base - 0.3) : (mfiEps_base + 0.5)
dirPrice_p = divProfile == "Conservative" ? math.max(0.0001, priceEps_base - 0.0004) : (priceEps_base + 0.0005)
pivLeft = usePreset ? pivL_p : pivLeft_in
pivRight = usePreset ? pivR_p : pivRight_in
minMfiDelta = usePreset ? mfiD_p : minMfiDelta_in
osobBufferPct = usePreset ? buf_p : osobBufferPct_in
minAtrMove = usePreset ? atr_p : minAtrMove_in
dirMfiEps = usePreset ? dirMfi_p : dirMfiEps_in
dirPriceEps = usePreset ? dirPrice_p : dirPriceEps_in
groupCandles = "Candle Colors"
colorCandles = input.bool(false, "Color candles", group=groupCandles)
candleMode = input.string("HTF Bias", "Mode", options=["HTF Bias","Signals"], group=groupCandles)
candleAlpha = input.int(70, "Opacity (0 solid .. 100 transparent)", minval=0, maxval=100, group=groupCandles)
groupDebug = "Debug"
enableDivLog = input.bool(false, "Log divergence diagnostics (>= 2025-02-01)", group=groupDebug)
// ============================================================================
// MODULE: helpers
// ============================================================================
//────────────────────────────────────────────────────────────────────
// Helpers
//────────────────────────────────────────────────────────────────────
clamp01(x) => math.max(0.0, math.min(1.0, x))
inZoneConfirmed(cond, bars) =>
if bars <= 0
cond
else
int okCount = 0
for i = 0 to bars
okCount += cond[i] ? 1 : 0
okCount == (bars + 1)
// JMA approximation (works well in practice)
jma(_src, _len, _phase, _power) =>
pr = _phase < -100 ? 0.5 : _phase > 100 ? 2.5 : _phase / 100.0 + 1.5
beta = 0.45 * (_len - 1) / (0.45 * (_len - 1) + 2)
alpha = math.pow(beta, _power)
e0 = 0.0
e1 = 0.0
e2 = 0.0
j = 0.0
e0 := (1 - alpha) * _src + alpha * nz(e0[1])
e1 := (_src - e0) * (1 - beta) + beta * nz(e1[1])
e2 := (e0 + pr * e1 - nz(j[1])) * math.pow(1 - alpha, 2) + math.pow(alpha, 2) * nz(e2[1])
j := e2 + nz(j[1])
j
getAutoHTF() =>
string res = "M"
if timeframe.isminutes
if timeframe.multiplier <= 5
res := "15"
else if timeframe.multiplier <= 15
res := "60"
else if timeframe.multiplier <= 60
res := "240"
else
res := "D"
else if timeframe.isdaily
res := "M"
res
// ============================================================================
// MODULE: core
// ============================================================================
//────────────────────────────────────────────────────────────────────
// Core series
//────────────────────────────────────────────────────────────────────
// MFI needs real trade volume; without it (CFD / tick / no volume) all MFI parts go na
bool volumeIsReal = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume)
mfiCalc = ta.mfi(src, mfiLen)
mfiRaw = volumeIsReal ? mfiCalc : na
mfi = (useJMA and smoothMFI) ? jma(mfiRaw, jmaLen, jmaPhase, jmaPower) : mfiRaw
// CCI mapped to 0..100
cciRaw = ta.cci(cciSrc, cciLen)
float cciNorm = cciRaw / cciMapScale
float cciExp = math.exp(2.0 * cciNorm)
float cciTanh = (cciExp - 1.0) / math.max(cciExp + 1.0, 1e-6)
cci01 = 0.5 + 0.5 * cciTanh
cci100 = 100.0 * cci01
// Active oscillator for divergence/score logic
osc = oscMode == "MFI" ? mfi : oscMode == "CCI" ? cci100 : (oscEngine == "CCI" ? cci100 : mfi)
// Zones
hi = ta.highest(mfi, zoneLookback)
lo = ta.lowest(mfi, zoneLookback)
upper = useDynZones ? (lo + (hi - lo) * clamp01(dynUpperFrac)) : float(staticUpper)
lower = useDynZones ? (lo + (hi - lo) * clamp01(dynLowerFrac)) : float(staticLower)
isOB = mfi > upper
isOS = mfi < lower
// StochRSI
rsi = ta.rsi(close, rsiLen)
rsiLo = ta.lowest(rsi, stochLen)
rsiHi = ta.highest(rsi, stochLen)
stochRsi = 100 * (rsi - rsiLo) / math.max(rsiHi - rsiLo, 1e-10)
kRaw = ta.sma(stochRsi, kSmooth)
k = (useJMA and smoothStoch) ? jma(kRaw, jmaLen, jmaPhase, jmaPower) : kRaw
// HTF MFI
string htf = useAutoTF ? getAutoHTF() : manualTF
mfiMtfReq = request.security(syminfo.tickerid, htf, ta.mfi(src, mfiLen), barmerge.gaps_off, barmerge.lookahead_off)
mfiMtfRaw = volumeIsReal ? mfiMtfReq : na
mfiMtf = mtfSmooth ? (useJMA ? jma(mfiMtfRaw, jmaLen, jmaPhase, jmaPower) : ta.ema(mfiMtfRaw, mtfEmaLen)) : mfiMtfRaw
htfBull = useMTF and not na(mfiMtf) and (mfiMtf >= biasLevel)
htfBear = useMTF and not na(mfiMtf) and (mfiMtf < biasLevel)
allowLong = not useHTFFilter or htfBull
allowShort = not useHTFFilter or htfBear
// ============================================================================
// MODULE: visuals
// ============================================================================
//────────────────────────────────────────────────────────────────────
// Visuals
//────────────────────────────────────────────────────────────────────
mfiColor = isOB ? mfiColorOB : isOS ? mfiColorOS : mfiColorNeutral
plot(mfi, "MFI", color=mfiColor, linewidth=2)
plot(useStoch ? k : na, "StochRSI K", color=stochColor, linewidth=1)
plot(useMTF ? mfiMtf : na, "MFI (HTF)", color=htfMfiColor, linewidth=2)
plot(oscMode == "CCI" or oscMode == "MFI+CCI" ? cci100 : na, "CCI (mapped 0-100)", color=cciColor, linewidth=1)
hline(80, "80", color=color.new(color.gray, 85))
hline(20, "20", color=color.new(color.gray, 85))
plot(showMidLine ? 50 : na, "50", color=midlineColor, linewidth=1)
hline(0, "", color=color.new(color.black, 100))
hline(100,"", color=color.new(color.black, 100))
pU = plot(useDynZones ? upper : na, "Upper (dyn)", color=zoneUpperColor, linewidth=1)
pL = plot(useDynZones ? lower : na, "Lower (dyn)", color=zoneLowerColor, linewidth=1)
fill(pU, pL, color=showBandFill ? color.new(color.gray, 92) : na)
bg = showBgZones ? (isOB ? color.new(color.red, 88) : isOS ? color.new(color.lime, 90) : na) : na
bgcolor(bg)
// Bias label (optional)
var label biasLbl = na
if barstate.islast and showBiasLabel and useMTF
label.delete(biasLbl)
biasText = "HTF " + htf + " MFI: " + str.tostring(mfiMtf, "#") + " | Bias: " + (htfBull ? "LONG" : htfBear ? "SHORT" : "n/a") + (useHTFFilter ? " (filter ON)" : " (filter OFF)")
biasLbl := label.new(bar_index + biasLabelOffset, 95, biasText, style=label.style_label_center, textcolor=color.white,
color=htfBull ? color.new(color.lime, 0) : color.new(color.red, 0))
// ============================================================================
// MODULE: signals
// ============================================================================
//────────────────────────────────────────────────────────────────────
// Signals: Context + Timing (+ optional HTF filter)
//────────────────────────────────────────────────────────────────────
mfiOS_confirm = inZoneConfirmed(mfi < lower, needConfirm)
mfiOB_confirm = inZoneConfirmed(mfi > upper, needConfirm)
longCond = showSignals and useStoch and allowLong and (mfiOS_confirm or ta.crossover(mfi, lower)) and ta.crossover(k, stochOS)
shortCond = showSignals and useStoch and allowShort and (mfiOB_confirm or ta.crossunder(mfi, upper)) and ta.crossunder(k, stochOB)
plotshape(longCond, title="Long", style=shape.triangleup, location=location.bottom, size=size.tiny, color=color.new(color.lime, 0), text="L")
plotshape(shortCond, title="Short", style=shape.triangledown, location=location.top, size=size.tiny, color=color.new(color.red, 0), text="S")
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (longCond), "Long Signal", "MEX · Long Signal · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortCond), "Short Signal", "MEX · Short Signal · {{ticker}} {{interval}}")
// ============================================================================
// MODULE: candle_colors
// ============================================================================
//────────────────────────────────────────────────────────────────────
// Candle coloring (optional)
//────────────────────────────────────────────────────────────────────
color cCol = na
if colorCandles
if candleMode == "HTF Bias"
cCol := htfBull ? color.new(color.lime, candleAlpha) : htfBear ? color.new(color.red, candleAlpha) : na
else if candleMode == "Signals"
cCol := longCond ? color.new(color.lime, candleAlpha) : shortCond ? color.new(color.red, candleAlpha) : na
barcolor(cCol)
// ============================================================================
// MODULE: divergences
// ============================================================================
//────────────────────────────────────────────────────────────────────
// DIVERGENCES (regular) anchored on PRICE pivots
// Bullish: price LL + MFI HL (MFI sampled at price pivot bar)
// Bearish: price HH + MFI LH (MFI sampled at price pivot bar)
// Labels: midpoint label + endpoint marker (points to divergence)
// Cleanup: keep only last maxDivs
//────────────────────────────────────────────────────────────────────
plPrice = ta.pivotlow(low, pivLeft, pivRight)
phPrice = ta.pivothigh(high, pivLeft, pivRight)
var float lastPL_price = na
var float lastPL_mfi = na
var int lastPL_bar = na
var float lastPH_price = na
var float lastPH_mfi = na
var int lastPH_bar = na
var int bullSeq = 0
var int bearSeq = 0
// Divergence drawn at pivot confirmation whose reaction component is still pending
type PendingDiv
int pivBar
bool isBull
float base01
int seq
float dMfi
int dtBars
line ln
label labMid
var PendingDiv[] pendingDivs = array.new<PendingDiv>()
var line[] divLines = array.new_line()
var label[] divLabels = array.new_label()
logStart = timestamp("GMT+0", 2025, 2, 1, 0, 0)
logActive = enableDivLog and time >= logStart
f_bool(bool v) => v ? "Y" : "N"
// OS/OB zones for the divergence area check, built from the ACTIVE oscillator
// (identical to the plotted MFI zones in MFI mode; CCI-based in CCI engine modes)
oscHi = ta.highest(osc, zoneLookback)
oscLo = ta.lowest(osc, zoneLookback)
divUpper = useDynZones ? (oscLo + (oscHi - oscLo) * clamp01(dynUpperFrac)) : float(staticUpper)
divLower = useDynZones ? (oscLo + (oscHi - oscLo) * clamp01(dynLowerFrac)) : float(staticLower)
// Fall back to fixed 20/80 zones until dynamic bands are ready (prevents NaNs in early bars).
bool zonesReady = not na(divUpper) and not na(divLower)
float band = zonesReady ? math.max(divUpper - divLower, 1.0) : na
float osLevel = 20.0
float obLevel = 80.0
float fallbackBuf = 5.0
inOS(_m) => zonesReady ? _m <= (divLower + band * osobBufferPct) : _m <= (osLevel + fallbackBuf)
inOB(_m) => zonesReady ? _m >= (divUpper - band * osobBufferPct) : _m >= (obLevel - fallbackBuf)
pushAndTrim(line ln, label labMid, label labEnd) =>
array.push(divLines, ln)
array.push(divLabels, labMid)
array.push(divLabels, labEnd)
while array.size(divLines) > maxDivs
line oldL = array.shift(divLines)
if not na(oldL)
line.delete(oldL)
if array.size(divLabels) > 0
label oldA = array.shift(divLabels)
if not na(oldA)
label.delete(oldA)
if array.size(divLabels) > 0
label oldB = array.shift(divLabels)
if not na(oldB)
label.delete(oldB)
atrDiv = ta.atr(divAtrLen)
// Score weights (former fatigue weight redistributed proportionally over the remaining components)
float W_MFI_DELTA = 0.18 / 0.82
float W_ATR = 0.16 / 0.82
float W_TIME = 0.18 / 0.82
float W_REACT = 0.18 / 0.82
float W_SEQ = 0.12 / 0.82
// Deferred reaction: measured over the reactBars bars after the pivot, from the pivot-bar close.
// A pending divergence is finalized pivBar + reactBars + reactLag bars after its pivot
// (reactLag > 0 only when the window already ended before the pivot was confirmed).
// ta.* run every bar; offsets are fixed per chart.
int reactLag = math.max(pivRight - reactBars, 0)
reactHighWin = ta.highest(high, reactBars)
reactLowWin = ta.lowest(low, reactBars)
float reactPivClose = close[reactBars + reactLag]
float reactUpMove = reactHighWin[reactLag] - reactPivClose
float reactDownMove = reactPivClose - reactLowWin[reactLag]
f_divTip(bool isBull, int seq, int score, float dMfi, int dtBars, string reactTxt) =>
str.format("{0}\nSeq: {1}\nScore: {2}\nΔMFI: {3}\nBars: {4}\nReact: {5}",
isBull ? "Bull Divergence" : "Bear Divergence",
str.tostring(seq),
str.tostring(score),
str.tostring(dMfi, "#.0"),
str.tostring(dtBars),
reactTxt)
if showDiv and not na(plPrice)
int x2 = bar_index - pivRight
float p2 = plPrice
float m2 = osc[pivRight]
bool isBullDiv = false
if not na(lastPL_price) and not na(lastPL_mfi)
int x1 = lastPL_bar
float p1 = lastPL_price
float m1 = lastPL_mfi
bool areaOk = not requireOSOB ? true : osobMode == "End" ? inOS(m2) : osobMode == "StartOrEnd" ? (inOS(m1) or inOS(m2)) : (inOS(m1) and inOS(m2))
float dMfi = m2 - m1
bool swingOk = not useAtrFilter or (math.abs(p1 - p2) >= atrDiv * minAtrMove)
bool priceOk = p2 <= (p1 * (1 + dirPriceEps))
bool mfiOk = m2 >= (m1 - dirMfiEps)
bool bullReg = priceOk and mfiOk and areaOk and (dMfi >= minMfiDelta) and swingOk
isBullDiv := bullReg
if logActive
log.info(str.format("DIV_BULL chk | bar={0} pivBar={1} p1={2} p2={3} m1={4} m2={5} dMfi={6} area={7} swing={8} minΔ={9} minATR={10} band={11} low={12} up={13} buf%={14} epsP={15} epsM={16}",
str.tostring(bar_index),
str.tostring(x2),
str.tostring(p1, format.mintick),
str.tostring(p2, format.mintick),
str.tostring(m1, "#.##"),
str.tostring(m2, "#.##"),
str.tostring(dMfi, "#.##"),
f_bool(areaOk),
f_bool(swingOk),
str.tostring(minMfiDelta),
str.tostring(minAtrMove, "#.##"),
str.tostring(band, "#.##"),
str.tostring(divLower, "#.##"),
str.tostring(divUpper, "#.##"),
str.tostring(osobBufferPct, "#.##"),
str.tostring(dirPriceEps, "#.####"),
str.tostring(dirMfiEps, "#.##")))
if bullReg
bullSeq += 1
bearSeq := 0
int dtBars = math.max(1, x2 - x1)
float time01 = clamp01(dtBars / 40.0)
float mfiDelta01 = minMfiDelta <= 0 ? 1.0 : clamp01(dMfi / (minMfiDelta * 2.0))
float atr01 = clamp01((math.abs(p1 - p2) / math.max(atrDiv * minAtrMove, 1e-10)) / 2.0)
float seq01 = clamp01(bullSeq / 3.0)
float biasBonus = useBiasBonus and htfBull ? 0.10 : 0.0
// Reaction component pending — finalized below once the reaction window has passed
float base01 = W_MFI_DELTA * mfiDelta01 + W_ATR * atr01 + W_TIME * time01 + W_SEQ * seq01 + biasBonus
int score = math.round(clamp01(base01) * 100)
int lnWidth = score >= scoreStrong ? 2 : 1
line ln = line.new(x1, m1, x2, m2, color=color.lime, width=lnWidth)
string txt = showDivScore ? str.tostring(score) : "Bull Div"
int midX = math.round((x1 + x2) / 2)
float midY = (m1 + m2) / 2
string tip = f_divTip(true, bullSeq, score, dMfi, dtBars, "pending")
label labMid = na
if showScoreLabel
labMid := label.new(midX, midY, txt,
style=label.style_label_center,
textcolor=color.white,
color=color.lime,
size=size.tiny,
tooltip=tip)
label labEnd = label.new(x2, m2, "▲",
style=label.style_label_up,
textcolor=color.white,
color=color.lime,
size=size.tiny,
tooltip="Divergence point (pivot)")
pushAndTrim(ln, labMid, labEnd)
array.push(pendingDivs, PendingDiv.new(x2, true, base01, bullSeq, dMfi, dtBars, ln, labMid))
// A confirmed pivot low that is not a bull divergence ends the bull sequence
if not isBullDiv
bullSeq := 0
lastPL_price := p2
lastPL_mfi := m2
lastPL_bar := x2
if showDiv and not na(phPrice)
int x2 = bar_index - pivRight
float p2 = phPrice
float m2 = osc[pivRight]
bool isBearDiv = false
if not na(lastPH_price) and not na(lastPH_mfi)
int x1 = lastPH_bar
float p1 = lastPH_price
float m1 = lastPH_mfi
bool areaOk = not requireOSOB ? true : osobMode == "End" ? inOB(m2) : osobMode == "StartOrEnd" ? (inOB(m1) or inOB(m2)) : (inOB(m1) and inOB(m2))
float dMfi = m1 - m2
bool swingOk = not useAtrFilter or (math.abs(p2 - p1) >= atrDiv * minAtrMove)
bool priceOk = p2 >= (p1 * (1 - dirPriceEps))
bool mfiOk = m2 <= (m1 + dirMfiEps)
bool bearReg = priceOk and mfiOk and areaOk and (dMfi >= minMfiDelta) and swingOk
isBearDiv := bearReg
if logActive
log.info(str.format("DIV_BEAR chk | bar={0} pivBar={1} p1={2} p2={3} m1={4} m2={5} dMfi={6} area={7} swing={8} minΔ={9} minATR={10} band={11} low={12} up={13} buf%={14} epsP={15} epsM={16}",
str.tostring(bar_index),
str.tostring(x2),
str.tostring(p1, format.mintick),
str.tostring(p2, format.mintick),
str.tostring(m1, "#.##"),
str.tostring(m2, "#.##"),
str.tostring(dMfi, "#.##"),
f_bool(areaOk),
f_bool(swingOk),
str.tostring(minMfiDelta),
str.tostring(minAtrMove, "#.##"),
str.tostring(band, "#.##"),
str.tostring(divLower, "#.##"),
str.tostring(divUpper, "#.##"),
str.tostring(osobBufferPct, "#.##"),
str.tostring(dirPriceEps, "#.####"),
str.tostring(dirMfiEps, "#.##")))
if bearReg
bearSeq += 1
bullSeq := 0
int dtBars = math.max(1, x2 - x1)
float time01 = clamp01(dtBars / 40.0)
float mfiDelta01 = minMfiDelta <= 0 ? 1.0 : clamp01(dMfi / (minMfiDelta * 2.0))
float atr01 = clamp01((math.abs(p2 - p1) / math.max(atrDiv * minAtrMove, 1e-10)) / 2.0)
float seq01 = clamp01(bearSeq / 3.0)
float biasBonus = useBiasBonus and htfBear ? 0.10 : 0.0
// Reaction component pending — finalized below once the reaction window has passed
float base01 = W_MFI_DELTA * mfiDelta01 + W_ATR * atr01 + W_TIME * time01 + W_SEQ * seq01 + biasBonus
int score = math.round(clamp01(base01) * 100)
int lnWidth = score >= scoreStrong ? 2 : 1
line ln = line.new(x1, m1, x2, m2, color=color.red, width=lnWidth)
string txt = showDivScore ? str.tostring(score) : "Bear Div"
int midX = math.round((x1 + x2) / 2)
float midY = (m1 + m2) / 2
string tip = f_divTip(false, bearSeq, score, dMfi, dtBars, "pending")
label labMid = na
if showScoreLabel
labMid := label.new(midX, midY, txt,
style=label.style_label_center,
textcolor=color.white,
color=color.red,
size=size.tiny,
tooltip=tip)
label labEnd = label.new(x2, m2, "▼",
style=label.style_label_down,
textcolor=color.white,
color=color.red,
size=size.tiny,
tooltip="Divergence point (pivot)")
pushAndTrim(ln, labMid, labEnd)
array.push(pendingDivs, PendingDiv.new(x2, false, base01, bearSeq, dMfi, dtBars, ln, labMid))
// A confirmed pivot high that is not a bear divergence ends the bear sequence
if not isBearDiv
bearSeq := 0
lastPH_price := p2
lastPH_mfi := m2
lastPH_bar := x2
// Finalize pending divergences once their reaction window has passed (FIFO: same delay for all).
// The divergence score is final only here — any alert that depends on it must fire at this point.
while array.size(pendingDivs) > 0
PendingDiv pd = array.get(pendingDivs, 0)
if bar_index - pd.pivBar < reactBars + reactLag
break
float reactMove = pd.isBull ? reactUpMove : reactDownMove
float react01 = clamp01(reactMove / math.max(atrDiv * reactAtrFrac, 1e-10))
int score = math.round(clamp01(pd.base01 + W_REACT * react01) * 100)
line.set_width(pd.ln, score >= scoreStrong ? 2 : 1)
if not na(pd.labMid)
if showDivScore
label.set_text(pd.labMid, str.tostring(score))
label.set_tooltip(pd.labMid, f_divTip(pd.isBull, pd.seq, score, pd.dMfi, pd.dtBars, str.tostring(react01 * 100, "#.0") + "%"))
array.shift(pendingDivs)
Overview
Market Exhaustion [WavesUnchained] offers customizable oscillator modes including MFI, Commodity Channel Index (CCI), or a composite blend. The indicator calculates standard static thresholds or dynamic adaptive zones based on historical lookback percentiles. It draws oscillator lines, band fills, and signals in a separate pane, with support for higher timeframe trend bias and regular divergence detection. MFI calculations rely on real trade volume and may degrade on volume-less symbols.
MFI and StochRSI fusion with optional CCI mode
Adaptive dynamic overbought and oversold zones
Higher timeframe MFI context and bias analysis
Regular divergence detection on MFI oscillator
Background highlights and customizable color zones
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Source
source
hlc3
src
MFI Length
int
14
mfiLen
Oscillator mode
string
MFI
oscMode
Engine source (for MFI+CCI)
string
MFI
oscEngine
CCI length
int
34
cciLen
CCI source
source
hlc3
cciSrc
CCI map scale (higher = flatter)
float
150.0
cciMapScale
Use dynamic zones (adaptive)
bool
true
useDynZones
Dynamic zone lookback
int
200
zoneLookback
Upper fraction (0..1)
float
0.85
dynUpperFrac
Lower fraction (0..1)
float
0.15
dynLowerFrac
Static Upper
int
80
staticUpper
Show all 40 settings
Setting
Type
Default
Variable
Static Lower
int
20
staticLower
Background highlight OB/OS
bool
true
showBgZones
Fill between zones
bool
true
showBandFill
Show midline (50)
bool
true
showMidLine
MFI color OB
color
color.new(color.red, 0)
mfiColorOB
MFI color OS
color
color.new(color.lime, 0)
mfiColorOS
MFI color neutral
color
color.new(color.gray, 0)
mfiColorNeutral
StochRSI K color
color
color.new(color.aqua, 55)
stochColor
HTF MFI color
color
color.new(color.orange, 35)
htfMfiColor
CCI mapped color
color
color.new(color.blue, 35)
cciColor
Zone upper line
color
color.new(color.red, 75)
zoneUpperColor
Zone lower line
color
color.new(color.lime, 75)
zoneLowerColor
Midline color
color
color.new(color.gray, 92)
midlineColor
Show StochRSI K
bool
true
useStoch
RSI Length
int
14
rsiLen
Stoch Length
int
14
stochLen
K smoothing
int
3
kSmooth
StochRSI OS level
int
20
stochOS
StochRSI OB level
int
80
stochOB
Show context+timing signals
bool
true
showSignals
Zone confirm bars (0..5)
int
1
needConfirm
Enable JMA smoothing
bool
true
useJMA
JMA Length
int
20
jmaLen
JMA Phase (-100..100)
int
0
jmaPhase
JMA Power (1..8)
float
2.0
jmaPower
Apply JMA to MFI
bool
true
smoothMFI
Apply JMA to StochRSI K
bool
true
smoothStoch
Show HTF MFI (default on)
bool
true
useMTF
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 calculates momentum setups based on the Relative Strength Index (RSI) and plots in a separate pane. It offers multiple scanning modes such as Continuous Break, Continuous Retracement, Flip Break, Flip Retracement, and both regular and hidden div
This indicator calculates the MACD oscillator from a configurable source with fast and slow lengths, then plots the histogram, MACD line, and signal line in a separate pane. It detects regular bullish divergences (price lower lows with oscillator higher lows),
This indicator calculates the Relative Strength Index (RSI) across four user-selected timeframes and plots the results in a separate pane. It identifies regular bullish and bearish divergences between price pivot points and the primary timeframe RSI value. Add
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
This indicator plots a standard Relative Strength Index (RSI) in a separate pane alongside customizable overbought and oversold threshold levels with shaded fills. It identifies pivot highs and lows on the oscillator to detect regular and hidden bullish and be
This indicator calculates a 14-period RSI from close and plots it in a separate pane with dotted overbought and dashed oversold lines at 70 and 30. It marks bullish divergence when a five-bar pivot low in price is lower than the price twenty bars earlier while