The Choppiness Index measures how much a market wanders compared with how far it actually travels. It adds up the true range of the last N bars and compares that path with the distance between the highest high and the lowest low of the same bars. When price covers a lot of ground bar after bar but ends up inside a narrow box, the reading is high; when most of that movement goes in one direction, the reading is low.
The weak point of the classic version is the fixed N. Fourteen bars can be a whole swing in one market and a fraction of one in another, and the same instrument changes rhythm from one month to the next.
The Adaptive Choppiness Index by blackcat1402 removes that fixed length. On every bar it measures the dominant cycle of the market with John Ehlers’ Homodyne Discriminator and uses a fraction of that cycle as the length of the Choppiness Index. The result is smoothed with an ALMA and drawn between the classic 38.2 and 61.8 levels. The line takes the colour of the zone it is in, and a “Trend” label marks the start of every trend zone.
Ehlers’ Homodyne Discriminator estimates the period of the dominant market cycle in a few steps:
The indicator multiplies this smoothed period by the Cycle Participation (0.25 by default), rounds it up and keeps it between 1 and 34 bars. With the default setting the length moves roughly between 2 and 13 bars: a quarter of the measured cycle.
With N the length from step 1:
CHOP = 100 * log10( sum of the true range of N bars / (highest high of N bars - lowest low of N bars) ) / log10(N)
The sum of the true ranges is the total path travelled; the high-low range is the net box. Their ratio goes from 1 (every bar moves in the same direction, path = box) to N (the market goes back and forth over the same box). Dividing the logarithm of the ratio by the logarithm of N maps it onto a 0 to 100 scale.
Because the length changes from bar to bar, the raw reading is jumpy. It is smoothed with an Arnaud Legoux Moving Average of 5 bars, offset 0.85 and sigma 6: Gaussian weights centred near the most recent bar, so the smoothing adds very little lag.
When the length is a single bar, or the range is completely flat, the index has no value, and the line stays empty until 5 consecutive bars have one again.
All parameters sit at the top of the code. Declare them as variables to change them from the indicator settings:
//--------------------------------------------
// PRC_Adaptive Choppiness
// version = 0
// 14.10.2026
// Ivan Gonzalez @ www.prorealcode.com
// Author: blackcat1402
// Sharing ProRealTime knowledge
//--------------------------------------------
// Choppiness Index whose length follows the dominant cycle
// measured with Ehlers' Homodyne Discriminator, smoothed with
// an ALMA(5, 0.85, 6). Above 61.8 and below 38.2 the bar is in
// a "Trend" zone; in between, "Sideways".
//--------------------------------------------
DEFPARAM DRAWONLASTBARONLY = true
//=== INPUTS (declare as Variables in the editor) ===
cycPart = 0.25 // Cycle Participation (fraction of the dominant cycle)
offsetValue = 0 // Shifts the CHOP line N bars to the right
labelMode = 1 // 0 = no labels, 1 = where a Trend zone starts, 2 = every bar
colorLine = 1 // 1 = line coloured by zone, 0 = single gold line
//=== DOMINANT CYCLE: EHLERS HOMODYNE DISCRIMINATOR ===
// Every state starts at zero and the previous bar of a missing
// history counts as zero, as in the original library.
IF barindex = 0 THEN
prevI2 = 0
prevQ2 = 0
prevRe = 0
prevIm = 0
prevPer = 0
prevSmPer = 0
ELSE
prevI2 = hdI2[1]
prevQ2 = hdQ2[1]
prevRe = hdRe[1]
prevIm = hdIm[1]
prevPer = hdPer[1]
prevSmPer = hdSmPer[1]
ENDIF
hdGain = 0.075 * prevPer + 0.54
hdSmooth = 0
hdDetr = 0
hdQ1 = 0
hdI1 = 0
hdJI = 0
hdJQ = 0
IF barindex > 5 THEN
// Hilbert transform of the 4-bar weighted price
hdSmooth = (4 * close + 3 * close[1] + 2 * close[2] + close[3]) / 10
hdDetr = (0.0962 * hdSmooth + 0.5769 * hdSmooth[2] - 0.5769 * hdSmooth[4] - 0.0962 * hdSmooth[6]) * hdGain
// In-phase and quadrature components
hdQ1 = (0.0962 * hdDetr + 0.5769 * hdDetr[2] - 0.5769 * hdDetr[4] - 0.0962 * hdDetr[6]) * hdGain
hdI1 = hdDetr[3]
// Advance the phase of I1 and Q1 by 90 degrees
hdJI = (0.0962 * hdI1 + 0.5769 * hdI1[2] - 0.5769 * hdI1[4] - 0.0962 * hdI1[6]) * hdGain
hdJQ = (0.0962 * hdQ1 + 0.5769 * hdQ1[2] - 0.5769 * hdQ1[4] - 0.0962 * hdQ1[6]) * hdGain
ENDIF
// Phasor addition for 3-bar averaging, smoothed before the discriminator
hdI2 = 0.2 * (hdI1 - hdJQ) + 0.8 * prevI2
hdQ2 = 0.2 * (hdQ1 + hdJI) + 0.8 * prevQ2
// Homodyne discriminator
hdRe = 0.2 * (hdI2 * prevI2 + hdQ2 * prevQ2) + 0.8 * prevRe
hdIm = 0.2 * (hdI2 * prevQ2 - hdQ2 * prevI2) + 0.8 * prevIm
hdRaw = 0
IF abs(hdIm) > 0 AND abs(hdRe) > 0 THEN
// ATAN returns degrees: 360 / atan in degrees = 2 * pi / atan in radians
hdRaw = 360 / ATAN(hdIm / hdRe)
ENDIF
IF hdRaw > 1.5 * prevPer THEN
hdRaw = 1.5 * prevPer
ENDIF
IF 0.67 * prevPer > hdRaw THEN
hdRaw = 0.67 * prevPer
ENDIF
// Keep the period between 6 and 50 bars
IF 6 > hdRaw THEN
hdRaw = 6
ENDIF
IF hdRaw > 50 THEN
hdRaw = 50
ENDIF
hdPer = 0.2 * hdRaw + 0.8 * prevPer
hdSmPer = 0.33 * hdPer + 0.67 * prevSmPer
// Length = cycle participation x smoothed period, rounded up, 1 to 34 bars
domCycle = ceil(cycPart * hdSmPer)
IF domCycle > 34 THEN
domCycle = 34
ENDIF
IF 1 > domCycle THEN
domCycle = 1
ENDIF
//=== ADAPTIVE CHOPPINESS INDEX ===
chopLen = domCycle
trSum = summation[chopLen](tr)
hiLo = highest[chopLen](high) - lowest[chopLen](low)
// A one-bar length or a flat range has no value (division by zero)
chopOk = 0
chopRaw = 0
IF chopLen > 1 AND hiLo > 0 AND trSum > 0 THEN
chopOk = 1
// log10(a) / log10(b) is the same ratio as log(a) / log(b)
chopRaw = 100 * LOG(trSum / hiLo) / LOG(chopLen)
ENDIF
//=== ALMA(5, 0.85, 6) SMOOTHING ===
// Gaussian weights centred at 0.85 x (5 - 1) = 3.4, sigma = 5 / 6.
// A bar without value inside the 5-bar window leaves the ALMA empty.
almaOk = 0
IF barindex >= 4 THEN
IF summation[5](chopOk) = 5 THEN
almaOk = 1
ENDIF
ENDIF
IF almaOk = 1 THEN
almaM = 0.85 * 4
almaS = 5 / 6
wSum = 0
wNorm = 0
FOR kW = 0 TO 4 DO
wK = EXP(-((kW - almaM) * (kW - almaM)) / (2 * almaS * almaS))
wSum = wSum + wK * chopRaw[4 - kW]
wNorm = wNorm + wK
NEXT
aChop = wSum / wNorm
ELSIF barindex = 0 THEN
aChop = 50
ELSE
// No value: keep the last one, hidden (alpha 0)
aChop = aChop[1]
ENDIF
//=== ZONE OF EACH BAR ===
// 1 = Trend above 61.8, -1 = Trend below 38.2, 0 = Sideways, 9 = no value
upperBand = 61.8
lowerBand = 38.2
chopSt = 9
IF almaOk = 1 THEN
IF aChop > upperBand THEN
chopSt = 1
ELSIF lowerBand > aChop THEN
chopSt = -1
ELSE
chopSt = 0
ENDIF
ENDIF
//=== PLOT ===
// Hidden while empty and on the first bar after a gap, so that no
// segment is drawn from the held value
offBars = max(0, round(offsetValue))
shown = 50
lineA = 0
stShown = 9
IF barindex >= offBars THEN
shown = aChop[offBars]
stShown = chopSt[offBars]
IF almaOk[offBars] = 1 AND almaOk[offBars + 1] = 1 THEN
lineA = 255
ENDIF
ENDIF
// Single gold line, or coloured by zone: green / red Trend, orange Sideways
lineR = 176
lineG = 110
lineB = 0
IF colorLine = 1 THEN
IF stShown = 1 THEN
lineR = 22
lineG = 163
lineB = 74
ELSIF stShown = -1 THEN
lineR = 242
lineG = 54
lineB = 69
ELSIF stShown = 0 THEN
lineR = 234
lineG = 88
lineB = 12
ENDIF
ENDIF
COLORBETWEEN(upperBand, lowerBand, 33, 150, 243, 25)
//=== LABELS (within the last 500 bars with a value) ===
IF labelMode >= 1 AND islastbarupdate THEN
// Pass 1: find the 500 most recent bars with a value and their range
vMax = upperBand
vMin = lowerBand
nLab = 0
jEnd = 0
jB = 0
WHILE 500 > nLab AND barindex >= jB DO
IF almaOk[jB] = 1 THEN
nLab = nLab + 1
vMax = max(vMax, aChop[jB])
vMin = min(vMin, aChop[jB])
jEnd = jB
ENDIF
jB = jB + 1
WEND
// Pass 2: "Trend" under the line above 61.8, above it below 38.2,
// "Sideways" on the line in between. Mode 1 labels only the first
// bar of each Trend zone; mode 2 labels every bar.
lblGap = (vMax - vMin) * 0.06
IF nLab > 0 THEN
FOR jB = 0 TO jEnd DO
sL = chopSt[jB]
drawIt = 0
IF labelMode >= 2 THEN
IF 9 > sL THEN
drawIt = 1
ENDIF
ELSIF abs(sL) = 1 THEN
IF abs(sL - chopSt[jB + 1]) > 0 THEN
drawIt = 1
ENDIF
ENDIF
IF drawIt = 1 THEN
vL = aChop[jB]
IF sL = 1 THEN
DRAWTEXT("Trend", barindex - jB, vL - lblGap) COLOURED(22, 163, 74)
ELSIF sL = -1 THEN
DRAWTEXT("Trend", barindex - jB, vL + lblGap) COLOURED(242, 54, 69)
ELSE
DRAWTEXT("Sideways", barindex - jB, vL) COLOURED(234, 88, 12)
ENDIF
ENDIF
NEXT
ENDIF
ENDIF
RETURN shown COLOURED(lineR, lineG, lineB, lineA) STYLE(line, 1) AS "CHOP", upperBand COLOURED(120, 123, 134) STYLE(dottedline2, 1) AS "Upper Band", 50 COLOURED(120, 123, 134, 127) STYLE(line, 1) AS "Middle Band", lowerBand COLOURED(120, 123, 134) STYLE(dottedline2, 1) AS "Lower Band"
The Adaptive Choppiness Index keeps the simple idea of the Choppiness Index, path against net distance, and lets the market choose the window through its dominant cycle. It works best as a regime filter next to a price-based signal, with the Cycle Participation as the one dial that sets how fast it reacts.