Indicator · Pine Script v6 · Own pane

Market Exhaustion [WavesUnchained]

by casoon

Market Exhaustion combines the Money Flow Index (MFI) and Stochastic RSI to pinpoint overbought and oversold conditions.

View on GitHub

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

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

market_exhaustion.pine
//@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.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

Oscillators & momentum, Divergence

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

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

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

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

SettingTypeDefaultVariable
Sourcesourcehlc3src
MFI Lengthint14mfiLen
Oscillator modestringMFIoscMode
Engine source (for MFI+CCI)stringMFIoscEngine
CCI lengthint34cciLen
CCI sourcesourcehlc3cciSrc
CCI map scale (higher = flatter)float150.0cciMapScale
Use dynamic zones (adaptive)booltrueuseDynZones
Dynamic zone lookbackint200zoneLookback
Upper fraction (0..1)float0.85dynUpperFrac
Lower fraction (0..1)float0.15dynLowerFrac
Static Upperint80staticUpper
Show all 40 settings
SettingTypeDefaultVariable
Static Lowerint20staticLower
Background highlight OB/OSbooltrueshowBgZones
Fill between zonesbooltrueshowBandFill
Show midline (50)booltrueshowMidLine
MFI color OBcolorcolor.new(color.red, 0)mfiColorOB
MFI color OScolorcolor.new(color.lime, 0)mfiColorOS
MFI color neutralcolorcolor.new(color.gray, 0)mfiColorNeutral
StochRSI K colorcolorcolor.new(color.aqua, 55)stochColor
HTF MFI colorcolorcolor.new(color.orange, 35)htfMfiColor
CCI mapped colorcolorcolor.new(color.blue, 35)cciColor
Zone upper linecolorcolor.new(color.red, 75)zoneUpperColor
Zone lower linecolorcolor.new(color.lime, 75)zoneLowerColor
Midline colorcolorcolor.new(color.gray, 92)midlineColor
Show StochRSI KbooltrueuseStoch
RSI Lengthint14rsiLen
Stoch Lengthint14stochLen
K smoothingint3kSmooth
StochRSI OS levelint20stochOS
StochRSI OB levelint80stochOB
Show context+timing signalsbooltrueshowSignals
Zone confirm bars (0..5)int1needConfirm
Enable JMA smoothingbooltrueuseJMA
JMA Lengthint20jmaLen
JMA Phase (-100..100)int0jmaPhase
JMA Power (1..8)float2.0jmaPower
Apply JMA to MFIbooltruesmoothMFI
Apply JMA to StochRSI KbooltruesmoothStoch
Show HTF MFI (default on)booltrueuseMTF
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

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

Indicator v6 Oscillators & momentumDivergence Repaints
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup - RSI Directional Momentum Indicator
330 stars

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),

Indicator v5 Oscillators & momentumDivergence
Updated 2 months ago · None stated GitHub repository of MACD + Divergences [Alorse]
218 stars

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

Indicator v6 Oscillators & momentumDivergence
Updated 3 months ago · None stated GitHub repository of Multi-Timeframe RSI with Divergence

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

RSI [SystemAlpha]

by iamc1oud
56 stars

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

Indicator v4 Oscillators & momentumDivergence Repaints
Updated 2 months ago · None stated GitHub repository of RSI [SystemAlpha]
55 stars

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

Indicator v6 Oscillators & momentumDivergence
Updated 6 months ago · Apache-2.0 GitHub repository of RSI Divergence Detector

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