Indicator · Pine Script v6 · On the chart

Exhaustion Scanner [WavesUnchained]

by casoon

Exhaustion Scanner is an overlay indicator that measures overextension and reversal risk by evaluating price stretch, momentum exhaustion, and candle triggers.

View on GitHub

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

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

exhaustion_scanner.pine
//@version=6
// ============================================================================
// Exhaustion Scanner [WavesUnchained]
// Version: 2.2.4
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Role-separated overextension / reversal-risk context. Three distinct role
// scores — Stretch (location), Exhaustion (momentum), Reaction (trigger) —
// combined into one score, scaled by a volatility multiplier and framed by a
// regime classifier. The regime does not block: it shifts the threshold and
// re-labels the signal as Continuation Risk / Trend Exhaustion / Range Fade.
//
// Features:
//   ✓ Regime classifier (EMA stack + ADX) frames every signal as a risk type
//     instead of a blind buy/sell — countertrend signals need more evidence
//   ✓ Three role scores, separately attributable:
//     Stretch (EMA/BB distance + structure), Exhaustion (RSI/StochRSI/WT +
//     divergence + money flow), Reaction (reclaim-fail / WT-cross-from-extreme)
//   ✓ Volatility acts as a multiplier, not a directionless score component
//   ✓ Divergence invalidates when price breaks beyond the divergence pivot
//   ✓ One score drives both the dashboard and the label (no Total/Trigger gap)
//   ✓ Exhaustion zones: a box over the overextended price area, persisting
//     until reclaimed; bold labels with the full role breakdown on hover
//   ✓ Market presets with role-weight profiles: Stocks / Commodities /
//     Forex-CFD / Crypto / Custom (Money Flow auto-off when no real volume)
//   ✓ Continuation Risk labels are colored orange, distinct from the red/green
//     used for Range Fade / Trend Exhaustion, so trend-fades don't read as flips
//   ✓ Dashboard with role breakdown, regime, signal reason, vol multiplier
//   ✓ Alerts: Top Risk, Bottom Risk, Any
//
// Data Contract:
//   Price:     REQUIRED   chart symbol
//   Volume:    OPTIONAL   real trade volume only — Money Flow role degrades to neutral, weights reassigned
//   OI:        NO
//   Reference: NO
//   Verdict:   CFD-degraded
//
// ============================================================================

indicator("Exhaustion Scanner [WavesUnchained]", shorttitle="Exhaustion Scanner", overlay=true, max_labels_count=100, max_boxes_count=100)

// ─────────────────────────────────────────────────────────────────────────────
// Inputs
// ─────────────────────────────────────────────────────────────────────────────

preset     = input.string("Commodities", "Market Preset", options=["Stocks", "Commodities", "Forex/CFD", "Crypto", "Custom"])
signalMode = input.string("Normal", "Signal Mode", options=["Early", "Normal", "Conservative"])
showLabels = input.bool(true, "Show Labels")
showZones  = input.bool(true, "Show Exhaustion Zones")
showBg     = input.bool(true, "Show Heatmap")
showTable  = input.bool(true, "Show Dashboard")
zoneMaxBars = input.int(40, "Zone Max Age (bars)", minval=5,
     tooltip="An exhaustion zone keeps extending right until price reclaims it or it reaches this age.")

grpSignals = "Signals"
confirmMode = input.string("Candle Reaction", "Confirmation", options=["Score Only", "Candle Reaction"], group=grpSignals,
     tooltip="Score Only marks the first bar reaching the threshold. Candle Reaction waits for a reclaim-fail / WT-cross-from-extreme before labeling.")
minSignalGap = input.int(8, "Min Bars Between Same-Side Signals", minval=0, group=grpSignals)
showCandidates = input.bool(true, "Show Setup Candidates", group=grpSignals,
     tooltip="Faint dots when the score first reaches setup level, before a reaction confirms.")
useDivergenceAssist = input.bool(false, "Divergence-Assisted Signals", group=grpSignals,
     tooltip="An active WT divergence additionally lowers the effective threshold so stretched, diverging price can label earlier. Off by default: the divergence already counts in the Exhaustion score, so enabling this counts it a second time.")

grpRegime = "Regime"
regimeFast = input.int(20,  "Regime Fast EMA", minval=5,  group=grpRegime)
regimeSlow = input.int(100, "Regime Slow EMA", minval=20, group=grpRegime)
adxLen     = input.int(14,  "ADX Length",      minval=5,  group=grpRegime)
adxTrend   = input.float(25, "ADX Trend Threshold", step=1, group=grpRegime,
     tooltip="ADX above this with an aligned EMA stack = a real trend. Countertrend exhaustion then needs more evidence.")
regimePenalty = input.float(12, "Countertrend Threshold Penalty", step=1, group=grpRegime,
     tooltip="Extra score required for a signal that fights an established trend (Continuation Risk).")

grpBase = "Base"
emaLen = input.int(50, "Distance EMA Length", minval=10, group=grpBase)
atrLen = input.int(14, "ATR Length", minval=5, group=grpBase)
bbLen  = input.int(20, "Bollinger Length", minval=10, group=grpBase)
bbMult = input.float(2.0, "Bollinger Multiplier", step=0.1, group=grpBase)

grpMom = "Momentum"
rsiLen   = input.int(14, "RSI Length", minval=5, group=grpMom)
stochLen = input.int(14, "StochRSI Length", minval=5, group=grpMom)
wtN1     = input.int(10, "WaveTrend Channel Length", minval=3, group=grpMom)
wtN2     = input.int(21, "WaveTrend Average Length", minval=5, group=grpMom)

grpDiv = "Divergences"
pivotLen  = input.int(5, "Divergence Pivot Length", minval=2, group=grpDiv)
divMemory = input.int(12, "Divergence Validity (bars)", minval=1, group=grpDiv)
divAssist = input.float(8, "Divergence Threshold Relief", step=1, group=grpDiv,
     tooltip="How many points an active divergence lowers the effective threshold (when Divergence-Assisted is on).")

grpStruct = "Structure"
structureLen = input.int(80, "Structure Lookback", minval=20, group=grpStruct)
nearAtrMult  = input.float(0.75, "High/Low Proximity (ATR ×)", step=0.05, group=grpStruct)

grpCustom = "Custom Role Weights (preset = Custom)"
customStretch  = input.float(40, "Stretch Weight",    minval=0, group=grpCustom)
customExhaust  = input.float(35, "Exhaustion Weight", minval=0, group=grpCustom)
customReact    = input.float(25, "Reaction Weight",   minval=0, group=grpCustom)
customUseFlow  = input.bool(true, "Include Money Flow", group=grpCustom)

// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────

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

score01(x) =>
    clamp(x, 0.0, 1.0) * 100.0

safeDiv(a, b) =>
    b == 0.0 ? 0.0 : a / b

// ─────────────────────────────────────────────────────────────────────────────
// Preset Role Weights
// ─────────────────────────────────────────────────────────────────────────────

float wStretch = na
float wExhaust = na
float wReact = na
bool  useFlow = true

if preset == "Stocks"
    wStretch := 35
    wExhaust := 35
    wReact   := 30
    useFlow  := true
else if preset == "Commodities"
    wStretch := 40
    wExhaust := 35
    wReact   := 25
    useFlow  := true
else if preset == "Forex/CFD"
    wStretch := 45
    wExhaust := 30
    wReact   := 25
    useFlow  := false
else if preset == "Crypto"
    wStretch := 35
    wExhaust := 40
    wReact   := 25
    useFlow  := true
else
    wStretch := customStretch
    wExhaust := customExhaust
    wReact   := customReact
    useFlow  := customUseFlow

roleSum = math.max(wStretch + wExhaust + wReact, 1)

// ─────────────────────────────────────────────────────────────────────────────
// Core Data
// ─────────────────────────────────────────────────────────────────────────────

atr = ta.atr(atrLen)
ema = ta.ema(close, emaLen)

basis = ta.sma(close, bbLen)
dev   = ta.stdev(close, bbLen)
upper = basis + dev * bbMult
lower = basis - dev * bbMult

distAtr = safeDiv(close - ema, atr)
bbZ     = safeDiv(close - basis, dev)

rsi = ta.rsi(close, rsiLen)
stochRsiRaw = ta.stoch(rsi, rsi, rsi, stochLen)
stochRsi = ta.sma(stochRsiRaw, 3)

// WaveTrend
ap  = hlc3
esa = ta.ema(ap, wtN1)
de  = ta.ema(math.abs(ap - esa), wtN1)
ci  = safeDiv(ap - esa, 0.015 * de)
wt1 = ta.ema(ci, wtN2)
wt2 = ta.sma(wt1, 4)

// Money Flow — guard against na-volume feeds poisoning the rolling averages
vol = nz(volume)
mfiRaw = ta.mfi(hlc3, 14)
mfi = na(mfiRaw) ? 50.0 : mfiRaw
volMa = ta.sma(vol, 30)
volSpike = safeDiv(vol, volMa)

hasRealVolume = (ta.cum(nz(volume)) > 0 and syminfo.volumetype != "tick") and not na(volume) and volMa > 0
useFlowFinal = useFlow and hasRealVolume

// Volatility (multiplier source, not a directional score)
atrMa = ta.sma(atr, 50)
atrExpansion = safeDiv(atr, atrMa)
rangeAtr = safeDiv(high - low, atr)

// Structure
recentHigh = ta.highest(high, structureLen)
recentLow  = ta.lowest(low, structureLen)

nearHigh = recentHigh - close <= atr * nearAtrMult
nearLow  = close - recentLow <= atr * nearAtrMult

upperWick = safeDiv(high - math.max(open, close), high - low)
lowerWick = safeDiv(math.min(open, close) - low, high - low)

// ─────────────────────────────────────────────────────────────────────────────
// Stage 1 — Regime (frames the signal, never blocks it)
// ─────────────────────────────────────────────────────────────────────────────

emaFast = ta.ema(close, regimeFast)
emaSlow = ta.ema(close, regimeSlow)
[diPlus, diMinus, adx] = ta.dmi(adxLen, adxLen)

trendUp = emaFast > emaSlow and close > emaSlow
trendDn = emaFast < emaSlow and close < emaSlow
strongTrend = adx >= adxTrend

regimeUp = strongTrend and trendUp
regimeDn = strongTrend and trendDn
regimeLabel = regimeUp ? "Trend ↑" : regimeDn ? "Trend ↓" : "Range"

// ─────────────────────────────────────────────────────────────────────────────
// Divergences (WT vs. price pivots) — invalidate on a break beyond the pivot
// ─────────────────────────────────────────────────────────────────────────────

ph = ta.pivothigh(high, pivotLen, pivotLen)
pl = ta.pivotlow(low, pivotLen, pivotLen)

prevPh = ta.valuewhen(not na(ph), ph, 1)
prevPl = ta.valuewhen(not na(pl), pl, 1)

prevWtAtPh = ta.valuewhen(not na(ph), wt1[pivotLen], 1)
prevWtAtPl = ta.valuewhen(not na(pl), wt1[pivotLen], 1)

bearDiv = not na(ph) and not na(prevPh) and ph > prevPh and wt1[pivotLen] < prevWtAtPh
bullDiv = not na(pl) and not na(prevPl) and pl < prevPl and wt1[pivotLen] > prevWtAtPl

var float bearDivPivot = na
var float bullDivPivot = na
if bearDiv
    bearDivPivot := high[pivotLen]
if bullDiv
    bullDivPivot := low[pivotLen]

// active = within memory AND price has not broken beyond the divergence pivot
bearDivActive = ta.barssince(bearDiv) <= divMemory and not na(bearDivPivot) and high <= bearDivPivot
bullDivActive = ta.barssince(bullDiv) <= divMemory and not na(bullDivPivot) and low  >= bullDivPivot

// ─────────────────────────────────────────────────────────────────────────────
// Stage 4 — Stretch / Location (where are we?)
// ─────────────────────────────────────────────────────────────────────────────

stretchTop = score01(
     0.45 * math.max(safeDiv(distAtr, 3.0), safeDiv(bbZ, 2.5)) +
     0.35 * (nearHigh ? 1 : 0) +
     0.20 * (close > upper ? 1 : 0))

stretchBot = score01(
     0.45 * math.max(safeDiv(-distAtr, 3.0), safeDiv(-bbZ, 2.5)) +
     0.35 * (nearLow ? 1 : 0) +
     0.20 * (close < lower ? 1 : 0))

// ─────────────────────────────────────────────────────────────────────────────
// Stage 2 — Exhaustion / Momentum (is the move tiring?)
// ─────────────────────────────────────────────────────────────────────────────

momCoreTop = score01(
     0.40 * safeDiv(rsi - 60, 20) +
     0.30 * safeDiv(stochRsi - 75, 25) +
     0.30 * safeDiv(wt1 - 45, 25))
momCoreBot = score01(
     0.40 * safeDiv(40 - rsi, 20) +
     0.30 * safeDiv(25 - stochRsi, 25) +
     0.30 * safeDiv(-45 - wt1, 25))

flowTop = score01(
     0.55 * safeDiv(mfi - 75, 20) +
     0.45 * safeDiv(volSpike - 1.5, 1.5))
// volume spike counts toward both sides — capitulation volume marks bottoms
flowBot = score01(
     0.55 * safeDiv(25 - mfi, 20) +
     0.45 * safeDiv(volSpike - 1.5, 1.5))

divTop = bearDivActive ? 100.0 : 0.0
divBot = bullDivActive ? 100.0 : 0.0

float exhaustTop = na
float exhaustBot = na
if useFlowFinal
    exhaustTop := 0.55 * momCoreTop + 0.25 * flowTop + 0.20 * divTop
    exhaustBot := 0.55 * momCoreBot + 0.25 * flowBot + 0.20 * divBot
else
    exhaustTop := 0.70 * momCoreTop + 0.30 * divTop
    exhaustBot := 0.70 * momCoreBot + 0.30 * divBot

// ─────────────────────────────────────────────────────────────────────────────
// Stage 3 — Reaction / Trigger (did the market actually react?)
// ─────────────────────────────────────────────────────────────────────────────

// reclaim-fail: poked beyond the band / the PRIOR recent extreme (current bar
// excluded), then closed back inside
priorHigh = recentHigh[1]
priorLow  = recentLow[1]
topReclaimFail = (high > upper and close < upper) or (high > priorHigh and close < priorHigh)
botReclaimFail = (low < lower and close > lower)  or (low  < priorLow  and close > priorLow)

// WT cross is only a trigger if WT was actually in an extreme zone recently
wtCrossUp = ta.crossover(wt1, wt2)
wtCrossDn = ta.crossunder(wt1, wt2)
wtWasHigh = ta.highest(wt1, 3) >= 45
wtWasLow  = ta.lowest(wt1, 3)  <= -45
topWtTrigger = wtCrossDn and wtWasHigh
botWtTrigger = wtCrossUp and wtWasLow

// wick rejection only counts with close-position confirmation — a bare wick
// without the close pushing back through the midpoint is not a real rejection
topCloseWeak = close < open and close < hl2
botCloseWeak = close > open and close > hl2

topWickRej = topCloseWeak and upperWick >= 0.35
botWickRej = botCloseWeak and lowerWick >= 0.35

reactTop = score01(
     0.45 * (topReclaimFail ? 1 : 0) +
     0.30 * (topWtTrigger ? 1 : 0) +
     0.25 * (topCloseWeak ? safeDiv(upperWick - 0.35, 0.35) : 0.0))
reactBot = score01(
     0.45 * (botReclaimFail ? 1 : 0) +
     0.30 * (botWtTrigger ? 1 : 0) +
     0.25 * (botCloseWeak ? safeDiv(lowerWick - 0.35, 0.35) : 0.0))

// binary timing for Candle-Reaction confirmation
topReacted = topReclaimFail or topWtTrigger or topWickRej
botReacted = botReclaimFail or botWtTrigger or botWickRej

// ─────────────────────────────────────────────────────────────────────────────
// Composite Score — roles combined, volatility as multiplier
// ─────────────────────────────────────────────────────────────────────────────

volScore = score01(
     0.55 * safeDiv(atrExpansion - 1.1, 0.7) +
     0.45 * safeDiv(rangeAtr - 1.2, 1.2))
volFactor = 0.85 + safeDiv(volScore, 100) * 0.30

topScore = clamp((stretchTop * wStretch + exhaustTop * wExhaust + reactTop * wReact) / roleSum * volFactor, 0, 100)
botScore = clamp((stretchBot * wStretch + exhaustBot * wExhaust + reactBot * wReact) / roleSum * volFactor, 0, 100)

// ─────────────────────────────────────────────────────────────────────────────
// Thresholds (regime-shifted, divergence-relieved) and Signals
// ─────────────────────────────────────────────────────────────────────────────

int baseThresh = signalMode == "Early" ? 30 : signalMode == "Normal" ? 40 : 50

// countertrend signals need more evidence; an active divergence relieves it
topThresh = clamp(baseThresh + (regimeUp ? regimePenalty : 0.0) - (useDivergenceAssist and bearDivActive ? divAssist : 0.0), 20, 80)
botThresh = clamp(baseThresh + (regimeDn ? regimePenalty : 0.0) - (useDivergenceAssist and bullDivActive ? divAssist : 0.0), 20, 80)

topExtreme = topScore >= topThresh
botExtreme = botScore >= botThresh
topSetup = topScore >= topThresh - 10
botSetup = botScore >= botThresh - 10

topCandidate = topSetup and not topSetup[1]
botCandidate = botSetup and not botSetup[1]

topRawSignal = confirmMode == "Score Only" ? (topExtreme and not topExtreme[1]) : (topExtreme and topReacted)
botRawSignal = confirmMode == "Score Only" ? (botExtreme and not botExtreme[1]) : (botExtreme and botReacted)

var int lastTopSignalBar = na
var int lastBotSignalBar = na

topGapOk = minSignalGap == 0 or na(lastTopSignalBar) or bar_index - lastTopSignalBar > minSignalGap
botGapOk = minSignalGap == 0 or na(lastBotSignalBar) or bar_index - lastBotSignalBar > minSignalGap

topSignal = topRawSignal and topGapOk
botSignal = botRawSignal and botGapOk

if topSignal
    lastTopSignalBar := bar_index
if botSignal
    lastBotSignalBar := bar_index

// ─────────────────────────────────────────────────────────────────────────────
// Signal framing — risk type (from regime) + reason (from dominant role)
// ─────────────────────────────────────────────────────────────────────────────

// top = upside overextension. In an uptrend, fading it is Continuation Risk;
// in a downtrend it is the bounce exhausting (Trend Exhaustion); else Range Fade.
topKind = regimeUp ? "CONTINUATION RISK" : regimeDn ? "TREND EXHAUSTION" : "RANGE FADE"
botKind = regimeDn ? "CONTINUATION RISK" : regimeUp ? "TREND EXHAUSTION" : "RANGE FADE"

reasonOf(stretchScore, exhaustScore, reactScore, divOn, wStr, wEx, wRe) =>
    sC = stretchScore * wStr
    eC = exhaustScore * wEx
    rC = reactScore * wRe
    string r = na
    if divOn
        r := "Divergence"
    else if rC >= eC and rC >= sC
        r := "Rejection"
    else if sC >= eC
        r := "Stretch"
    else
        r := "Momentum"
    r

topReason = reasonOf(stretchTop, exhaustTop, reactTop, bearDivActive, wStretch, wExhaust, wReact)
botReason = reasonOf(stretchBot, exhaustBot, reactBot, bullDivActive, wStretch, wExhaust, wReact)

// ─────────────────────────────────────────────────────────────────────────────
// Visuals
// ─────────────────────────────────────────────────────────────────────────────

plot(ema, "EMA", color=color.new(color.gray, 35))
plot(upper, "BB Upper", color=color.new(color.gray, 80))
plot(lower, "BB Lower", color=color.new(color.gray, 80))

color bgColor = na
if topExtreme
    bgColor := color.new(color.red, 84)
else if botExtreme
    bgColor := color.new(color.green, 84)
else if topSetup
    bgColor := color.new(color.red, 92)
else if botSetup
    bgColor := color.new(color.green, 92)

bgcolor(showBg ? bgColor : na)

plotshape(showCandidates and topCandidate and not topSignal ? high : na, "Top Setup Candidate", shape.circle, location.absolute, color.new(#d50000, 65), size=size.tiny)
plotshape(showCandidates and botCandidate and not botSignal ? low : na, "Bottom Setup Candidate", shape.circle, location.absolute, color.new(#00c853, 65), size=size.tiny)

// Full breakdown lives on hover, so the on-chart label stays compact
tipOf(side, kind, sc, th, reason, stretchSc, exhaustSc, reactSc) =>
    side + " EXHAUSTION  —  " + kind + "\n" +
     "Score " + str.tostring(math.round(sc)) + "/100   (threshold " + str.tostring(math.round(th)) + ")\n" +
     "Reason: " + reason + "\n" +
     "Stretch " + str.tostring(math.round(stretchSc)) +
     "  ·  Exhaustion " + str.tostring(math.round(exhaustSc)) +
     "  ·  Reaction " + str.tostring(math.round(reactSc)) + "\n" +
     "Regime: " + regimeLabel + "   ADX " + str.tostring(math.round(adx)) +
     "   Vol ×" + str.tostring(volFactor, "#.00")

// Continuation Risk fades against an established trend — flag it as caution
// (orange) rather than the same red/green used for Range Fade / Trend Exhaustion
color topLabelColor = topKind == "CONTINUATION RISK" ? #ff6f00 : #d50000
color botLabelColor = botKind == "CONTINUATION RISK" ? #ff6f00 : #00c853

if showLabels and topSignal
    label.new(bar_index, high, "▼ " + topKind + "\n" + str.tostring(math.round(topScore)) + " · " + topReason,
         style=label.style_label_down, textcolor=color.white, color=color.new(topLabelColor, 0), size=size.normal,
         tooltip=tipOf("TOP", topKind, topScore, topThresh, topReason, stretchTop, exhaustTop, reactTop))

if showLabels and botSignal
    label.new(bar_index, low, "▲ " + botKind + "\n" + str.tostring(math.round(botScore)) + " · " + botReason,
         style=label.style_label_up, textcolor=color.white, color=color.new(botLabelColor, 0), size=size.normal,
         tooltip=tipOf("BOTTOM", botKind, botScore, botThresh, botReason, stretchBot, exhaustBot, reactBot))

// ── Exhaustion zones — the overextended price area, persisting until reclaimed
var box  topZone = na
var int  topZoneStart = na
var box  botZone = na
var int  botZoneStart = na

if showZones and topSignal
    topZone := box.new(bar_index, math.max(high, upper), bar_index, math.min(upper, close),
         border_color=color.new(#d50000, 30), border_width=1, bgcolor=color.new(#d50000, 88))
    topZoneStart := bar_index
else if not na(topZone)
    if close < upper or bar_index - topZoneStart > zoneMaxBars
        box.delete(topZone)
        topZone := na
    else
        box.set_right(topZone, bar_index)

if showZones and botSignal
    botZone := box.new(bar_index, math.max(lower, close), bar_index, math.min(low, lower),
         border_color=color.new(#00c853, 30), border_width=1, bgcolor=color.new(#00c853, 88))
    botZoneStart := bar_index
else if not na(botZone)
    if close > lower or bar_index - botZoneStart > zoneMaxBars
        box.delete(botZone)
        botZone := na
    else
        box.set_right(botZone, bar_index)

plotshape(bearDiv, "Bearish WT Divergence", shape.triangledown, location.abovebar, color=color.new(#d50000, 0), size=size.tiny, text="Div")
plotshape(bullDiv, "Bullish WT Divergence", shape.triangleup, location.belowbar, color=color.new(#00c853, 0), size=size.tiny, text="Div")

// ─────────────────────────────────────────────────────────────────────────────
// Dashboard (light theme)
// ─────────────────────────────────────────────────────────────────────────────

color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
regimeColor = regimeUp ? #00c853 : regimeDn ? #d50000 : tc

var table dash = table.new(position.top_right, 3, 11,
     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 barstate.islast and showTable
    table.cell(dash, 0, 0, "Exhaustion", text_color=tc, bgcolor=hd, text_size=size.small)
    table.cell(dash, 1, 0, "Top",    text_color=color.white, bgcolor=#d50000, text_size=size.small)
    table.cell(dash, 2, 0, "Bottom", text_color=color.white, bgcolor=#00c853, text_size=size.small)

    color topC = topSignal ? #d50000 : tc
    color botC = botSignal ? #00c853 : tc
    table.cell(dash, 0, 1, "Score", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 1, str.tostring(math.round(topScore)), text_color=topC, text_size=size.small)
    table.cell(dash, 2, 1, str.tostring(math.round(botScore)), text_color=botC, text_size=size.small)

    table.cell(dash, 0, 2, "Threshold", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 2, str.tostring(math.round(topThresh)), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 2, str.tostring(math.round(botThresh)), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 3, "Stretch", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 3, str.tostring(math.round(stretchTop)), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 3, str.tostring(math.round(stretchBot)), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 4, "Exhaustion", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 4, str.tostring(math.round(exhaustTop)), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 4, str.tostring(math.round(exhaustBot)), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 5, "Reaction", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 5, str.tostring(math.round(reactTop)), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 5, str.tostring(math.round(reactBot)), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 6, "Vol ×", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 6, str.tostring(volFactor, "#.00"), text_color=tc, text_size=size.small)
    table.cell(dash, 2, 6, str.tostring(volFactor, "#.00"), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 7, "Regime", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 7, regimeLabel, text_color=regimeColor, text_size=size.small)
    table.cell(dash, 2, 7, "ADX " + str.tostring(math.round(adx)), text_color=tc, text_size=size.small)

    table.cell(dash, 0, 8, "Reason", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 8, topReason, text_color=tc, text_size=size.small)
    table.cell(dash, 2, 8, botReason, text_color=tc, text_size=size.small)

    table.cell(dash, 0, 9, "Preset", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 9, preset, text_color=tc, text_size=size.small)
    table.cell(dash, 2, 9, signalMode, text_color=tc, text_size=size.small)

    table.cell(dash, 0, 10, "Volume", text_color=tc, text_size=size.small)
    table.cell(dash, 1, 10, hasRealVolume ? "Real" : syminfo.volumetype, text_color=tc, text_size=size.small)
    table.cell(dash, 2, 10, useFlowFinal ? "Flow: On" : "Flow: Off", text_color=tc, text_size=size.small)

// ─────────────────────────────────────────────────────────────────────────────
// Alerts
// ─────────────────────────────────────────────────────────────────────────────

bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (topSignal), "Top Risk", "EXS · TOP RISK · {{ticker}} {{interval}}")
alertcondition(alertOk and (botSignal), "Bottom Risk", "EXS · BOTTOM RISK · {{ticker}} {{interval}}")
alertcondition(alertOk and (topSignal or botSignal), "Any Risk", "EXS · REVERSAL RISK · {{ticker}} {{interval}}")

Overview

Exhaustion Scanner [WavesUnchained] combines distance from moving averages and Bollinger Bands with momentum oscillators, divergence logic, and an EMA/ADX regime classifier to label risk conditions like Range Fade, Trend Exhaustion, or Continuation Risk. The indicator displays exhaustion zone boxes, background heatmaps, signal labels, and an on-chart dashboard. Input settings adjust market presets, signal modes, and indicator parameters.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Oscillators & momentum, Volatility & bands, Support & resistance, Screeners & dashboards

Markets

Forex, Crypto, Stocks, Oil & commodities

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
Market PresetstringCommoditiespreset
Signal ModestringNormalsignalMode
Show LabelsbooltrueshowLabels
Show Exhaustion ZonesbooltrueshowZones
Show HeatmapbooltrueshowBg
Show DashboardbooltrueshowTable
Zone Max Age (bars)int40zoneMaxBars
ConfirmationstringCandle ReactionconfirmMode
Min Bars Between Same-Side Signalsint8minSignalGap
Show Setup CandidatesbooltrueshowCandidates
Divergence-Assisted SignalsboolfalseuseDivergenceAssist
Regime Fast EMAint20regimeFast
Show all 34 settings
SettingTypeDefaultVariable
Regime Slow EMAint100regimeSlow
ADX Lengthint14adxLen
ADX Trend Thresholdfloat25adxTrend
Countertrend Threshold Penaltyfloat12regimePenalty
Distance EMA Lengthint50emaLen
ATR Lengthint14atrLen
Bollinger Lengthint20bbLen
Bollinger Multiplierfloat2.0bbMult
RSI Lengthint14rsiLen
StochRSI Lengthint14stochLen
WaveTrend Channel Lengthint10wtN1
WaveTrend Average Lengthint21wtN2
Divergence Pivot Lengthint5pivotLen
Divergence Validity (bars)int12divMemory
Divergence Threshold Relieffloat8divAssist
Structure Lookbackint80structureLen
High/Low Proximity (ATR ×)float0.75nearAtrMult
Stretch Weightfloat40customStretch
Exhaustion Weightfloat35customExhaust
Reaction Weightfloat25customReact
Include Money FlowbooltruecustomUseFlow
Alerts only on bar close (confirmed)booltruealertsOnBarClose
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 overlay indicator plots a Supertrend from an ATR length and factor, four exponential moving averages, and a selectable VWAP or TWAP taken from a chosen timeframe on the price chart. It also shows MACD and stochastic with adjustable lengths, smoothing, ver

Indicator v5 Oscillators & momentumMoving averages
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup: Vijay Thakare Option Buying Scalping Setup
330 stars

This multi-symbol indicator evaluates a combined trading condition across up to 40 user-defined symbols. It processes MACD bullish crossovers, RSI thresholds, Bollinger Band boundaries, and DMI/ADX trend filters using security calls. When buy or sell criteria

Indicator v5 Oscillators & momentumVolatility & bands
Updated 2 months ago · None stated GitHub repository of Multi MACD + BB + RSI [Alorse]
218 stars

This indicator maps market structure and supply and demand zones on the price chart. It marks validated and optional unvalidated swing highs and lows using a pivot length and extra confirmation bars. Consolidation and impulse moves are measured against ATR to

Indicator v6 SMC / ICT / price actionSupport & resistance Repaints
Updated 3 months ago · None stated GitHub repository of Market Structure + Supply/Demand

8zz反指標v2

by hansai-art
121 stars

The 8zz Contrarian Indicator plots reverse sentiment trading signals directly on the price chart based on tracked retail post activity. It generates bullish signals during retail panic and bearish signals during market euphoria. The indicator features adjustab

Indicator v6 Moving averagesOscillators & momentum
Updated 3 months ago · None stated GitHub repository of 8zz反指標v2

Moon Dev Lunar Sniper

by moondevonyt
92 stars

Moon Dev Lunar Sniper is a multi-indicator overlay that scores confluence across fast and slow EMAs, RSI, MACD, DMI, and BB width to fire signals only when score meets the minimum threshold. It plots buy and sell signals on the price chart, highlights cosmic b

Indicator v6 TrendOscillators & momentum Repaints
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev Lunar Sniper

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