Exhaustion Scanner is an overlay indicator that measures overextension and reversal risk by evaluating price stretch, momentum exhaustion, and candle triggers.
//@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.
Multi-factor scoring combining stretch, momentum, and trigger logic
EMA and ADX regime classifier for contextual signal categorization
On-chart exhaustion zone boxes and background volatility heatmaps
Market presets tailored for stocks, commodities, forex, and crypto
Comprehensive dashboard table showing role breakdown and regime status
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Market Preset
string
Commodities
preset
Signal Mode
string
Normal
signalMode
Show Labels
bool
true
showLabels
Show Exhaustion Zones
bool
true
showZones
Show Heatmap
bool
true
showBg
Show Dashboard
bool
true
showTable
Zone Max Age (bars)
int
40
zoneMaxBars
Confirmation
string
Candle Reaction
confirmMode
Min Bars Between Same-Side Signals
int
8
minSignalGap
Show Setup Candidates
bool
true
showCandidates
Divergence-Assisted Signals
bool
false
useDivergenceAssist
Regime Fast EMA
int
20
regimeFast
Show all 34 settings
Setting
Type
Default
Variable
Regime Slow EMA
int
100
regimeSlow
ADX Length
int
14
adxLen
ADX Trend Threshold
float
25
adxTrend
Countertrend Threshold Penalty
float
12
regimePenalty
Distance EMA Length
int
50
emaLen
ATR Length
int
14
atrLen
Bollinger Length
int
20
bbLen
Bollinger Multiplier
float
2.0
bbMult
RSI Length
int
14
rsiLen
StochRSI Length
int
14
stochLen
WaveTrend Channel Length
int
10
wtN1
WaveTrend Average Length
int
21
wtN2
Divergence Pivot Length
int
5
pivotLen
Divergence Validity (bars)
int
12
divMemory
Divergence Threshold Relief
float
8
divAssist
Structure Lookback
int
80
structureLen
High/Low Proximity (ATR ×)
float
0.75
nearAtrMult
Stretch Weight
float
40
customStretch
Exhaustion Weight
float
35
customExhaust
Reaction Weight
float
25
customReact
Include Money Flow
bool
true
customUseFlow
Alerts only on bar close (confirmed)
bool
true
alertsOnBarClose
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.
Advanced Inflection Point Pro is a reversal detection indicator that combines state analysis, pattern matching, and volume anomaly detection to identify turning points. It evaluates multi-factor probability scores to plot signal trails, market flow ribbons, re
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
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
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
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
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