This indicator classifies market conditions into Trend, Range, or Chaos regimes by combining the Kaufman Efficiency Ratio, Fractal Dimension Index, and Choppiness Index into a single trendiness metric.
//@version=6
// ============================================================================
// Regime Classifier [WavesUnchained]
// Version: 1.1.3
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Stage-1 regime filter for the reversal pipeline: fuses Fractal Dimension Index,
// Kaufman Efficiency Ratio and the Choppiness Index into one Trend / Range / Chaos
// classification. It is a Quality/context layer — it answers "is a reversal even
// sensible here?", it never fires a long/short trigger.
//
// Features:
// ✓ Three regime sensors (FDI, Efficiency Ratio, Choppiness) on one trendiness axis
// ✓ Trend / Range / Chaos classification with threshold hysteresis
// ✓ Symmetric reversal- and trend-permission outputs (0..1) for downstream gating
// ✓ Per-sensor visual breakdown + light-theme dashboard
// ✓ Regime-change watch markers and alerts (no trade triggers)
// ✓ Optional higher-timeframe regime line (visual context only, no veto)
// ✓ Debug log on every regime change
//
// ============================================================================
indicator("Regime Classifier [WavesUnchained]", "REGIME", format=format.price, precision=2, overlay=false, max_labels_count=500)
// ─────────────────────────────────────────────────────────────────────────────
// INPUTS
// ─────────────────────────────────────────────────────────────────────────────
g_sens = "1 · Sensors"
erLen = input.int(20, "Efficiency Ratio Length", minval=2, group=g_sens,
tooltip="Kaufman ER over this lookback. High ER = efficient/trending move, low ER = back-and-forth.")
fdiLen = input.int(30, "Fractal Dimension Length", minval=6, group=g_sens,
tooltip="Box-counting fractal dimension over this (even) lookback. D≈1 = smooth/trend, D≈1.5+ = rough/range.")
chopLen = input.int(14, "Choppiness Length", minval=2, group=g_sens,
tooltip="Choppiness Index lookback. High = choppy/ranging, low = directional.")
g_map = "2 · Sensor → Trendiness Mapping"
erTrendAt = input.float(0.55, "ER: full-trend at", minval=0.0, maxval=1.0, step=0.05, group=g_map)
erRangeAt = input.float(0.20, "ER: full-range at", minval=0.0, maxval=1.0, step=0.05, group=g_map)
fdiTrendAt = input.float(1.35, "FDI: full-trend at", minval=1.0, maxval=2.0, step=0.05, group=g_map)
fdiRangeAt = input.float(1.55, "FDI: full-range at", minval=1.0, maxval=2.0, step=0.05, group=g_map)
chopTrendAt= input.float(38.2, "Chop: full-trend at",minval=0.0, maxval=100.0, group=g_map)
chopRangeAt= input.float(61.8, "Chop: full-range at",minval=0.0, maxval=100.0, group=g_map)
g_w = "3 · Sensor Weights"
wEr = input.float(1.0, "Weight · Efficiency Ratio", minval=0.0, step=0.1, group=g_w)
wFdi = input.float(1.0, "Weight · Fractal Dimension",minval=0.0, step=0.1, group=g_w)
wChop = input.float(1.0, "Weight · Choppiness", minval=0.0, step=0.1, group=g_w)
g_reg = "4 · Regime Thresholds"
trendHi = input.float(0.62, "Trend enters above", minval=0.5, maxval=1.0, step=0.01, group=g_reg,
tooltip="Trendiness ≥ this → Trend regime. Between this and the Range level the previous regime is held (hysteresis).")
rangeLo = input.float(0.38, "Range enters below", minval=0.0, maxval=0.5, step=0.01, group=g_reg)
g_chaos = "5 · Chaos Override"
chopChaos = input.float(65.0, "Chaos: Chop above", minval=0.0, maxval=100.0, group=g_chaos,
tooltip="Chaos requires ALL three: Chop very high AND ER very low AND FDI very high — i.e. no exploitable structure either way.")
erChaos = input.float(0.18, "Chaos: ER below", minval=0.0, maxval=1.0, step=0.01, group=g_chaos)
fdiChaos = input.float(1.55, "Chaos: FDI above", minval=1.0, maxval=2.0, step=0.05, group=g_chaos)
chaosDamp = input.float(0.30, "Chaos permission multiplier", minval=0.0, maxval=1.0, step=0.05, group=g_chaos)
g_disp = "6 · Display"
showSensors = input.bool(true, "Show per-sensor lines", group=g_disp)
showWatch = input.bool(true, "Show regime-change markers", group=g_disp)
showTable = input.bool(true, "Show dashboard", group=g_disp)
tablePos = input.string("Top Right", "Dashboard position",
options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group=g_disp)
enableLog = input.bool(false, "Debug log on regime change", group=g_disp)
g_col = "7 · Colors"
colTrend = input.color(#00c853, "Trend", group=g_col)
colRange = input.color(#2962ff, "Range", group=g_col)
colChaos = input.color(#d50000, "Chaos", group=g_col)
g_htf = "8 · Higher-Timeframe Regime (visual)"
showHtf = input.bool(false, "Show HTF regime line", group=g_htf,
tooltip="Visual context only — the HTF regime is NOT used as a veto/gate (§3 over-filtering). Off by default.")
htfMode = input.string("Auto (4× chart)", "HTF source",
options=["Auto (4× chart)", "Manual"], group=g_htf)
htfManual= input.timeframe("240", "Manual HTF", group=g_htf,
tooltip="Used only when HTF source = Manual.")
// ─────────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────────
// Map a sensor reading onto a 0..1 trendiness axis given its full-trend / full-range
// anchor values. Direction-agnostic: works whether trend is the high or low anchor.
f_map(float v, float trendAt, float rangeAt) =>
float span = trendAt - rangeAt
float t = span == 0.0 ? 0.5 : (v - rangeAt) / span
math.max(0.0, math.min(1.0, t))
// ─────────────────────────────────────────────────────────────────────────────
// REGIME ENGINE (one source of truth — reused for the chart TF and the HTF line)
// ─────────────────────────────────────────────────────────────────────────────
// Returns [trendScore, er, fdi, chop, tEr, tFdi, tChop] on whatever timeframe it
// is evaluated on. Sensor convention: ER high = trend, FDI low = trend, Chop high
// = range; each is mapped to a 0..1 trendiness via its own anchors.
f_regime() =>
// Sensor 1 · Kaufman Efficiency Ratio (0..1, high = trend)
float erChange = math.abs(close - close[erLen])
float erVol = math.sum(math.abs(close - close[1]), erLen)
float erV = erVol > 0.0 ? erChange / erVol : 0.0
// Sensor 2 · Fractal Dimension Index (box-counting, 1..2, low = trend)
int fdiHalf = int(math.max(2, fdiLen / 2))
float r1 = (ta.highest(high, fdiHalf) - ta.lowest(low, fdiHalf)) / fdiHalf
float r2 = (ta.highest(high, fdiHalf)[fdiHalf] - ta.lowest(low, fdiHalf)[fdiHalf]) / fdiHalf
float r3 = (ta.highest(high, fdiLen) - ta.lowest(low, fdiLen)) / fdiLen
float fdiV = na
if r1 + r2 > 0.0 and r3 > 0.0
fdiV := (math.log(r1 + r2) - math.log(r3)) / math.log(2.0)
fdiV := math.max(1.0, math.min(2.0, nz(fdiV, 1.5)))
// Sensor 3 · Choppiness Index (0..100, high = chop)
float trV = ta.tr(true)
float sumTr = math.sum(trV, chopLen)
float chRange = ta.highest(high, chopLen) - ta.lowest(low, chopLen)
float chopV = chRange > 0.0 ? 100.0 * math.log10(sumTr / chRange) / math.log10(chopLen) : na
chopV := math.max(0.0, math.min(100.0, nz(chopV, 50.0)))
// Composite trendiness (0..1)
float tErV = f_map(erV, erTrendAt, erRangeAt)
float tFdiV = f_map(fdiV, fdiTrendAt, fdiRangeAt)
float tChopV = f_map(chopV, chopTrendAt, chopRangeAt)
float wSum = wEr + wFdi + wChop
float tsV = wSum > 0.0 ? (wEr * tErV + wFdi * tFdiV + wChop * tChopV) / wSum : 0.5
[tsV, erV, fdiV, chopV, tErV, tFdiV, tChopV]
[trendScore, er, fdi, chop, tEr, tFdi, tChop] = f_regime()
// ─────────────────────────────────────────────────────────────────────────────
// REGIME STATE (hysteresis: hold previous inside the dead zone)
// ─────────────────────────────────────────────────────────────────────────────
var int regimeState = 0 // 1 = Trend, -1 = Range, 0 = uninitialised
if trendScore >= trendHi
regimeState := 1
else if trendScore <= rangeLo
regimeState := -1
else if regimeState == 0
regimeState := trendScore >= 0.5 ? 1 : -1
bool chaos = chop >= chopChaos and er <= erChaos and fdi >= fdiChaos
string regimeLabel = chaos ? "Chaos" : (regimeState == 1 ? "Trend" : "Range")
color regimeCol = chaos ? colChaos : (regimeState == 1 ? colTrend : colRange)
// ─────────────────────────────────────────────────────────────────────────────
// PERMISSIONS (Quality outputs, symmetric — never a trade trigger)
// ─────────────────────────────────────────────────────────────────────────────
float chaosMult = chaos ? chaosDamp : 1.0
float reversalPermission = (1.0 - trendScore) * chaosMult
float trendPermission = trendScore * chaosMult
// ─────────────────────────────────────────────────────────────────────────────
// HIGHER-TIMEFRAME REGIME LINE (visual context only — never a veto/gate, §3)
// ─────────────────────────────────────────────────────────────────────────────
f_htfScore() =>
[tsV, erV, fdiV, chopV, tErV, tFdiV, tChopV] = f_regime()
tsV
int chartSec = timeframe.in_seconds(timeframe.period)
string htfTf = htfMode == "Manual" ? htfManual : timeframe.from_seconds(chartSec * 4)
float htfScore = request.security(syminfo.tickerid, htfTf, f_htfScore())
string htfReg = htfScore >= trendHi ? "Trend" : htfScore <= rangeLo ? "Range" : "Neutral"
color htfCol = htfScore >= trendHi ? colTrend : htfScore <= rangeLo ? colRange : color.new(color.gray, 40)
// ─────────────────────────────────────────────────────────────────────────────
// PLOTS
// ─────────────────────────────────────────────────────────────────────────────
color scoreCol = color.from_gradient(trendScore, rangeLo, trendHi, colRange, colTrend)
plot(trendScore * 100.0, "Trendiness", color=chaos ? colChaos : scoreCol, linewidth=2)
plot(showHtf ? htfScore * 100.0 : na, "HTF Trendiness", color=color.new(htfCol, 30),
linewidth=2, style=plot.style_stepline)
plot(showSensors ? tEr * 100.0 : na, "ER (mapped)", color=color.new(#ffb300, 35), linewidth=1)
plot(showSensors ? tFdi * 100.0 : na, "FDI (mapped)", color=color.new(#00bcd4, 35), linewidth=1)
plot(showSensors ? tChop * 100.0 : na, "Chop (mapped)", color=color.new(#ab47bc, 35), linewidth=1)
hline(trendHi * 100.0, "Trend Level", color=color.new(colTrend, 50), linestyle=hline.style_dashed)
hline(rangeLo * 100.0, "Range Level", color=color.new(colRange, 50), linestyle=hline.style_dashed)
hline(50.0, "Mid", color=color.new(color.gray, 70), linestyle=hline.style_dotted)
bgcolor(color.new(regimeCol, 90), title="Regime Tint")
// ─────────────────────────────────────────────────────────────────────────────
// REGIME-CHANGE WATCH MARKERS (state change, not a trigger — see §7)
// ─────────────────────────────────────────────────────────────────────────────
bool toRange = regimeLabel == "Range" and regimeLabel[1] != "Range"
bool toTrend = regimeLabel == "Trend" and regimeLabel[1] != "Trend"
bool toChaos = regimeLabel == "Chaos" and regimeLabel[1] != "Chaos"
plotshape(showWatch and toRange, "→ Range (reversal window)",
style=shape.circle, location=location.bottom, color=colRange, size=size.tiny, text="R")
plotshape(showWatch and toTrend, "→ Trend",
style=shape.circle, location=location.top, color=colTrend, size=size.tiny, text="T")
plotshape(showWatch and toChaos, "→ Chaos",
style=shape.xcross, location=location.top, color=colChaos, size=size.tiny, text="X")
// ─────────────────────────────────────────────────────────────────────────────
// DASHBOARD
// ─────────────────────────────────────────────────────────────────────────────
f_pos(string p) =>
switch p
"Top Left" => position.top_left
"Bottom Right" => position.bottom_right
"Bottom Left" => position.bottom_left
=> position.top_right
var table t = table.new(f_pos(tablePos), 2, 9,
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)
color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
if showTable and barstate.islast
table.cell(t, 0, 0, "REGIME", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 1, 0, regimeLabel, text_color=color.white, bgcolor=regimeCol, text_size=size.small)
table.cell(t, 0, 1, "Trendiness", text_color=tc, text_size=size.small)
table.cell(t, 1, 1, str.tostring(trendScore * 100.0, "#.0"), text_color=tc, text_size=size.small)
table.cell(t, 0, 2, "Efficiency Ratio", text_color=tc, text_size=size.small)
table.cell(t, 1, 2, str.tostring(er, "#.00"), text_color=tc, text_size=size.small)
table.cell(t, 0, 3, "Fractal Dimension", text_color=tc, text_size=size.small)
table.cell(t, 1, 3, str.tostring(fdi, "#.00"), text_color=tc, text_size=size.small)
table.cell(t, 0, 4, "Choppiness", text_color=tc, text_size=size.small)
table.cell(t, 1, 4, str.tostring(chop, "#.0"), text_color=tc, text_size=size.small)
table.cell(t, 0, 5, "", bgcolor=color.new(color.gray, 80), text_size=size.small)
table.cell(t, 1, 5, "", bgcolor=color.new(color.gray, 80), text_size=size.small)
table.cell(t, 0, 6, "Reversal OK", text_color=tc, text_size=size.small)
table.cell(t, 1, 6, str.tostring(reversalPermission * 100.0, "#") + "%",
text_color=reversalPermission >= 0.5 ? colRange : tc, text_size=size.small)
table.cell(t, 0, 7, "Trend-follow OK", text_color=tc, text_size=size.small)
table.cell(t, 1, 7, str.tostring(trendPermission * 100.0, "#") + "%",
text_color=trendPermission >= 0.5 ? colTrend : tc, text_size=size.small)
if showHtf
table.cell(t, 0, 8, "HTF " + htfTf + " (visual)", text_color=tc, text_size=size.small)
table.cell(t, 1, 8, htfReg + " " + str.tostring(htfScore * 100.0, "#"),
text_color=color.white, bgcolor=htfCol, text_size=size.small)
// ─────────────────────────────────────────────────────────────────────────────
// ALERTS (regime context, not entries)
// ─────────────────────────────────────────────────────────────────────────────
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (regimeLabel == "Range" and regimeLabel[1] != "Range"),
"Regime → Range", "RGC · REGIME RANGE · {{ticker}} {{interval}}")
alertcondition(alertOk and (regimeLabel == "Trend" and regimeLabel[1] != "Trend"),
"Regime → Trend", "RGC · REGIME TREND · {{ticker}} {{interval}}")
alertcondition(alertOk and (regimeLabel == "Chaos" and regimeLabel[1] != "Chaos"),
"Regime → Chaos", "RGC · REGIME CHAOS · {{ticker}} {{interval}}")
// ─────────────────────────────────────────────────────────────────────────────
// DEBUG LOG (§10 — answer "why this regime?")
// ─────────────────────────────────────────────────────────────────────────────
if enableLog and barstate.isconfirmed and regimeLabel != regimeLabel[1]
log.info("REGIME → {0} | trendScore={1} er={2} fdi={3} chop={4} | tEr={5} tFdi={6} tChop={7}",
regimeLabel,
str.tostring(trendScore, "#.000"),
str.tostring(er, "#.000"), str.tostring(fdi, "#.000"), str.tostring(chop, "#.0"),
str.tostring(tEr, "#.00"), str.tostring(tFdi, "#.00"), str.tostring(tChop, "#.00"))
Overview
Regime Classifier [WavesUnchained] normalizes and applies custom weightings to each sensor to evaluate overall market structure. The script plots the combined regime line, individual sensor values, and regime-change markers in a separate pane alongside an optional information dashboard. Users can adjust sensor lengths, weighting parameters, regime thresholds, and configure higher-timeframe overlays.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Efficiency Ratio Length
int
20
erLen
Fractal Dimension Length
int
30
fdiLen
Choppiness Length
int
14
chopLen
ER: full-trend at
float
0.55
erTrendAt
ER: full-range at
float
0.20
erRangeAt
FDI: full-trend at
float
1.35
fdiTrendAt
FDI: full-range at
float
1.55
fdiRangeAt
Chop: full-trend at
float
38.2
chopTrendAt
Chop: full-range at
float
61.8
chopRangeAt
Weight · Efficiency Ratio
float
1.0
wEr
Weight · Fractal Dimension
float
1.0
wFdi
Weight · Choppiness
float
1.0
wChop
Show all 30 settings
Setting
Type
Default
Variable
Trend enters above
float
0.62
trendHi
Range enters below
float
0.38
rangeLo
Chaos: Chop above
float
65.0
chopChaos
Chaos: ER below
float
0.18
erChaos
Chaos: FDI above
float
1.55
fdiChaos
Chaos permission multiplier
float
0.30
chaosDamp
Show per-sensor lines
bool
true
showSensors
Show regime-change markers
bool
true
showWatch
Show dashboard
bool
true
showTable
Dashboard position
string
Top Right
tablePos
Debug log on regime change
bool
false
enableLog
Trend
color
#00c853
colTrend
Range
color
#2962ff
colRange
Chaos
color
#d50000
colChaos
Show HTF regime line
bool
false
showHtf
HTF source
string
Auto (4× chart)
htfMode
Manual HTF
timeframe
240
htfManual
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.
This indicator scans a pasted list of symbols and shows matching long and short setups in a table in a separate pane. It uses a Supertrend crossover, then applies an ATR or fixed-point volatility range so quiet, sideways conditions can be skipped. Optional fil
Strategy-Picker evaluates market trend, momentum, and volatility across multiple timeframes (5m, 15m, and 60m) to recommend suitable options trading structures. It combines MACD, an 8-period EMA, and ADX readings with external VIX levels to suggest Put Credit
Manias, Panics and Crashes is a separate-pane market regime indicator that quantifies market sentiment and systemic risk. It processes technical metrics including RSI, moving averages, and volume alongside optional intermarket data such as VIX volatility and c
This indicator classifies the market into gamma regime types (negative gamma trend, positive gamma mean-reversion, or transition) by calculating a composite score based on daily ATR ratios, session VWAP crossover frequencies, and opening range displacement. It
This indicator uses a Hidden Markov Model (HMM) framework combined with an adaptive dual moving average engine and Directional Movement Index (DMI) features to identify market regimes (bullish, bearish, or ranging/chop). It calculates regime probabilities on e
This indicator calculates standardized trend strength and historical volatility to estimate position sizes. It computes the differences between three user-defined EMAs standardized by price using MACD logic, while historical volatility is derived from the stan