Market Regime Detection

Systematically identify whether the market is trending or ranging using ADX, Bollinger Band® width, and ATR percentile — and deploy only the appropriate strategy for each regime.

Markets spend time in two fundamentally different states: trending and ranging. In a trending state, price makes consistent directional progress — higher highs and higher lows in an uptrend, lower lows and lower highs in a downtrend. In a ranging state, price oscillates between support and resistance without sustained directional conviction.

The critical insight is that these two states require completely different strategy approaches. A breakout or momentum strategy that performs excellently during a trend will get chopped apart during a range — it buys breakouts that immediately reverse, and sells breakdowns that immediately recover. Conversely, a mean-reversion strategy that thrives in a range will produce a string of losses during a trend — it fades moves that keep going.

Market regime detection gives you a systematic, objective way to determine which state the market is in before applying any strategy. Rather than using discretion, the regime is scored from multiple independent measures — ADX, Bollinger Band® width percentile, and ATR percentile — and a combined score drives the assessment.

Why the regime matters before any other decision

Before asking "what is the signal?" the more important question is "what kind of market is this?" A strategy's statistical edge is conditional on market regime. The same EMA crossover system has a positive expectancy in trending conditions and a negative expectancy in ranging conditions. The setup doesn't change — the regime does.

This is why regime detection should be treated as a prerequisite filter, not an optional addition. Every entry signal should pass a regime check: is the current condition appropriate for the type of strategy generating this signal?

Method 1: ADX (Average Directional Index)

ADX directly measures trend strength on a 0–100 scale. It does not measure direction — only the intensity of directional movement, regardless of whether that movement is up or down.

  • ADX above 25: a trend is present and has reasonable strength
  • ADX below 20: no significant trend — the market is ranging or consolidating
  • ADX above 40: strong trend, often over-extended

The 25 threshold is the standard starting point, but it can be adjusted for the instrument and timeframe. Faster instruments on shorter timeframes may need a lower threshold; slower instruments on daily charts may warrant a higher one.

Method 2: Bollinger Band® Width Percentile

Bollinger Band® width — `(upper − lower) / basis` — measures how "wide" the bands are relative to the moving average. Wide bands = high volatility = price is moving. Narrow bands = compression = price is coiling.

The absolute width is not meaningful across different instruments and timeframes. The percentile rank of the current width relative to the last N bars is what matters: is the current width in the top half or bottom half of its recent historical distribution?

`ta.percentrank(bbWidth, 100)` returns 0–100: where the current band width sits within the last 100 values. A reading above 50 means bands are wider than their median over the last 100 bars — volatility is above average, which is consistent with trending conditions. Below 50 means compression.

Method 3: ATR Percentile

ATR measures the average bar range. Like band width, the absolute ATR value is instrument-specific. Comparing it to its own recent history via percentile rank gives a normalised reading.

`ta.percentrank(ta.atr(14), 100)` returns where the current ATR sits within the last 100 ATR values. High ATR percentile = bars are larger than usual = directional activity. Low ATR percentile = bars are small and compressed = ranging or consolidation.

The combined score

No single measure is reliable in isolation. ADX can lag at trend starts; band width can be high during volatile ranges; ATR percentile reflects all volatility, not just directional volatility.

Combining all three into a score (0–3) reduces false readings:

  • Score 2–3 (green): at least two measures agree the market is trending — use trend/momentum/breakout strategies
  • Score 0–1 (red): at least two measures agree the market is ranging — use mean reversion, support/resistance, or avoid trading entirely
  • Score = mixed: regime is ambiguous — reduce size or wait for clarity

Practical application

The regime score is most valuable as a strategy gate: before your strategy generates an entry, check the regime. If it doesn't match, skip the trade.

A simple implementation:

`if trendingRegime and bullishBreakoutSignal` → take the trade `if rangingRegime and meanReversionSignal` → take the trade `if regime doesn't match signal type` → pass

Over time, this filter should improve the strategy's profit factor even as it reduces total trade count — you are selecting only the trades where the strategy's edge is most likely to be present.

When the three measures disagree

It is common for the three indicators to agree cleanly in strong regimes (all three say "trending" on a breakaway move; all three say "ranging" in a clear consolidation). But they can also disagree — for example ADX > 25 (suggesting trend) but BB width in the bottom quartile (suggesting range) — which signals a genuinely ambiguous state: directional drift without the volatility expansion that usually accompanies a trend.

The practical rule in these mixed states is the cautious one: reduce size by half, or skip trading entirely, until at least two of the three measures agree. Entering a trade during regime disagreement means you are betting on whichever measure turns out to be right, and you have no edge in that guess. Waiting for alignment loses you nothing — strategies with genuine edge deliver more than enough signals per year, and the ones you pass on during ambiguous conditions are disproportionately the losers.

//@version=6
indicator("Market Regime Detection", overlay=false)

adxLen    = input.int(14,   "ADX Length")
adxThresh = input.float(25, "ADX Trend Threshold", minval=10, maxval=50)
lookback  = input.int(100,  "Percentile Lookback",  minval=20)

// ── Method 1: ADX ────────────────────────────────────────────────────
[diPlus, diMinus, adxVal] = ta.dmi(adxLen, adxLen)
adxTrending = adxVal > adxThresh

// ── Method 2: Bollinger Band® Width Percentile ─────────────────────────
[bbBasis, bbUpper, bbLower] = ta.bb(close, 20, 2.0)
bbWidth     = (bbUpper - bbLower) / bbBasis
bbwPct      = ta.percentrank(bbWidth, lookback)
bbwTrending = bbwPct > 50

// ── Method 3: ATR Percentile ──────────────────────────────────────────
atrPct      = ta.percentrank(ta.atr(14), lookback)
atrTrending = atrPct > 50

// ── Combined regime score (0 = ranging, 3 = strong trend) ─────────────
score    = (adxTrending ? 1 : 0) + (bbwTrending ? 1 : 0) + (atrTrending ? 1 : 0)
trending = score >= 2
ranging  = score <= 1

// ── Plots ─────────────────────────────────────────────────────────────
hline(adxThresh, "ADX Threshold",   color=color.new(color.blue, 40), linestyle=hline.style_dashed)
hline(50,        "50th Percentile", color=color.gray,               linestyle=hline.style_dotted)

plot(adxVal,       "ADX",           color=color.blue,   linewidth=2)
plot(bbwPct,       "BB Width %ile", color=color.purple, linewidth=1)
plot(atrPct,       "ATR %ile",      color=color.orange, linewidth=1)
plot(score * 20,   "Regime Score",  color=color.white,  linewidth=3, style=plot.style_histogram)

bgcolor(trending ? color.new(color.green, 90) : ranging ? color.new(color.red, 90) : na)