Here is a script that provides three different tools you can use to make the Commodity Channel Index (CCI) adaptive (they are built into the script so you can select which one you want applied using the properties panel of the study), eliminating any doubt that its length is set right. Some traders recommend using 1/3*cycle length for the period, so an input has been included and is currently set to 1/3, however you can change it to any acceptable value.
The three methods are the EhlersAutoCorrelationPeriodogram, which requires matrices to fully implement and as a result is not precise, the Band-Pass Filter method of measuring dominant cycle, which when tested against a sinewave signal provides precise measurements of the dominant cycle, and finally a volatility adapter much like that used by the AMA in TOS.
The adaptive CCI is show in in the lower chart of this screenshot as produced using the EhlersAutoCorrelationPeriodogram.
Here is a screenshot showing how it looks when the Band-Pass Filter method is used. Also, the 2nd study from the bottom compares the cycle lengths produced by the Band-Pass Filter method and the Volatility adapter method. As can be seen the Band-Pass dominant cycles being longer than the volatility adapter method, will produce a smoother CCI.
Here is the script:
Note: Articles from Dr Ehlers use arrays to normalize the periodogram data and then a center of gravity method to pin point the dominant cycle. However, thinkscript does not support arrays as needed to do the drill down further from the periodogram to the precise dominant cycle, per se. However, the cycle value provided by the periodogram in TOS appears to be capable of finding more trading opportunities than found with static fixed length values. Be sure to test it out before using it.
Of course because the Periodogram method cannot be fully implemented without bogging down the computer, the cycle lengths will not be representative of the market cycles. But finally at long last we now have a precise method of measuring the market dominant cycles that produces precise cycle lengths when tested against a sinewave across a wide range of period lengths. I am sure you will enjoy this new tool in your trading arsenal. Enjoy!
Here are a few screenshots of test results showing that the Band-pass Filter method of measuring the DC length of a sinewave produces values that closely match the actual values.
In the following screenshot, the red line is the Dominant Cycle length measured by the zero crossings of the band-pass filter. Zero crossings of the yellow line are watched and the number of candles between crossings are counted and multiplied by two to get the measured value of the dominant cycle length. The yellow line was made by the band-pass filter. The Cyan line is the SineWave signal of given period length = 8. As shown in the screenshot, the band-pass filter measured the period length correctly getting DC = 8:
Above even with a period of 40 the Band-Pass filter method gets an exact match without changing any settings for the band-pass filter.
However, because the band-pass filter method is only counting one half of the cycle then multiplying by two, it does not get the correct DC length when the sinewave has a period length that is odd. However, the values that it gets are only off by +/- 1 as shown in the test cases below.
For sinewave period = 7, the band-pass filter gets DC = 6 or DC = 8, with the correct number being the average of the two:
And finally for sinewave DC = 39, the band-pass filter measures DC = 40 or DC = 38, with the correct value being the average of the two:
Enjoy!
The three methods are the EhlersAutoCorrelationPeriodogram, which requires matrices to fully implement and as a result is not precise, the Band-Pass Filter method of measuring dominant cycle, which when tested against a sinewave signal provides precise measurements of the dominant cycle, and finally a volatility adapter much like that used by the AMA in TOS.
The adaptive CCI is show in in the lower chart of this screenshot as produced using the EhlersAutoCorrelationPeriodogram.
Here is a screenshot showing how it looks when the Band-Pass Filter method is used. Also, the 2nd study from the bottom compares the cycle lengths produced by the Band-Pass Filter method and the Volatility adapter method. As can be seen the Band-Pass dominant cycles being longer than the volatility adapter method, will produce a smoother CCI.
Here is the script:
CSS:
# Adaptive CCI by Sesqui
# [31AUG2025] - using EhlersAutoCorrelationPeriodogram from TOS (not precise or representative of dominant cycles)
# [12OCT2025] - Adds Precise Dominant Cycle mesurements using Band-Pass Filter method of chapter 5 of "Cycle nalytics for Traders"
# [12OCT2025] - Adds AMA like Volatility adapter
declare lower;
input UseVolatility_Adapter = yes;
input UseBandPassFilter_Adpater = no;
input CycleFraction = 1.0;
input over_sold = -100;
input over_bought = 100;
input showBreakoutSignals = no;
#===========================================================================================================================
script GetCycle {
# Returns the dominant market cycle for use in adaptive indicators
#------------------------------------------
# Charles Schwab & Co. (c) 2016-2025
#
def lag = 48;
def x = EhlersRoofingFilter("cutoff length" = 8, "roof cutoff length" = 48);
def cosinePart = fold i = 3 to 48 with cosPart do cosPart + (3 * (x * GetValue(x, i) + GetValue(x, 1) * GetValue(x, i + 1) + GetValue(x, 2) * GetValue(x, i + 2)) - (x + GetValue(x, 1) + GetValue(x, 2)) * (GetValue(x, i) + GetValue(x, i + 1) + GetValue(x, i + 2))) / Sqrt((3 * (x * x + GetValue(x, 1) * GetValue(x, 1) + GetValue(x, 2) * GetValue(x, 2)) - Sqr(x + GetValue(x, 1) + GetValue(x, 2))) * (3 * (GetValue(x, i) * GetValue(x, i) + GetValue(x, i + 1) * GetValue(x, i + 1) + GetValue(x, i + 2) * GetValue(x, i + 2)) - Sqr(GetValue(x, i) + GetValue(x, i + 1) + GetValue(x, i + 2)))) * Cos(2 * Double.Pi * i / lag);
def sinePart = fold j = 3 to 48 with sinPart do sinPart + (3 * (x * GetValue(x, j) + GetValue(x, 1) * GetValue(x, j + 1) + GetValue(x, 2) * GetValue(x, j + 2)) - (x + GetValue(x, 1) + GetValue(x, 2)) * (GetValue(x, j) + GetValue(x, j + 1) + GetValue(x, j + 2))) / Sqrt((3 * (x * x + GetValue(x, 1) * GetValue(x, 1) + GetValue(x, 2) * GetValue(x, 2)) - Sqr(x + GetValue(x, 1) + GetValue(x, 2))) * (3 * (GetValue(x, j) * GetValue(x, j) + GetValue(x, j + 1) * GetValue(x, j + 1) + GetValue(x, j + 2) * GetValue(x, j + 2)) - Sqr(GetValue(x, j) + GetValue(x, j + 1) + GetValue(x, j + 2)))) * Sin(2 * Double.Pi * j / lag);
def sqSum = Sqr(cosinePart) + Sqr(sinePart);
plot Cycle = ExpAverage(sqSum, 9);
#-------------------------------------------
}# end Script GetCycle{}
script getStdDeviation {
input Cycle = 10;
input source = close;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
def sum = fold i = 0 to CycleLength with s = 0 do s + GetValue(source, i);
def x_bar = sum / CycleLength;
def SumDeviationSquared = fold j = 0 to CycleLength with sd = 0 do sd + Sqr(GetValue(source, j) - x_bar);
def avgDev = SumDeviationSquared / CycleLength;
def Sigma = Sqrt(avgDev);
plot StdDev = Sigma;
}#end Script getStdDeviation{}
script GetAvg {
input Cycle = 10;
input source = close;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
def sum = fold i = 0 to CycleLength with s = 0 do s + GetValue(source, i);
plot AvgDev = sum / CycleLength;
}# End Script AgetAvg{}
#======================================================================================================
## Script to determine the length needed to adapt to market volatility
Script AdaptLengthToVolatility{
# *** Computes the Efficiency Ratio ***
def effRatioLength = 10;
def fastLength = 2;
def slowLength = 30;
def ER = AbsValue((Close - Lowest(low, effRatioLength)) - (Highest(high, effRatioLength) - close)) / (Highest(high, effRatioLength) - Lowest(low, effRatioLength));
def FastSF = 2 / (fastLength + 1);
def SlowSF = 2 / (slowLength + 1);
def ScaledSF = ER * (FastSF - SlowSF) + SlowSF;
# *** Determines the Length ***
# for an EMA: alpha = 1/ (L+1); where L is the lag
# Using this expression and solving for Lag:
# L = 2/alpha -1; let ScaledSF = alpha, and solve for the
# value of lag, L, to get: L = 2/scaledSF - 1;
# This Length is returned by the script
def cutoffLength = 2 / ScaledSF - 1;
plot Length = cutoffLength;
Length.HideBubble();
}# End script AdaptLengthToVolatility{}
#===================================================================================
#*** Script to measure dominant cycle length using band-pass
#*** filter zero crossings method per Dr Ehlers
#*** Chapter 5 of "Cycle Analytics for Traders".
Script GetCycleViaBandPassFilter{
input source = ohlc4;
input Period = 20;
input Bandwidth = 0.70;
def cosPart = Cos(0.25 * Bandwidth * 2.0 * Double.Pi / Period);
def sinPart = Sin(0.25 * Bandwidth * 2.0 * Double.Pi / Period);
def alpha2 = (cosPart + sinPart - 1) / cosPart;
def HP = (1 + alpha2 / 2) * (source - source[1]) + (1 - alpha2) * HP[1];
def beta1 = Cos(2.0 * Double.Pi / Period);
def gamma1 = 1 / Cos(2.0 * Double.Pi * Bandwidth / Period);
def alpha1 = gamma1 - Sqrt(gamma1 * gamma1 - 1);
def BP = If BarNumber() == 1 or BarNumber() == 2 then 0 else 0.5 * (1 - alpha1) * (HP - HP[2]) + beta1 * (1 + alpha1) * BP[1] - alpha1 * BP[2];
def Peak = if absValue(BP) > 0.991*Peak[1] then AbsValue(BP) else 0.991*Peak[1];
def Real = if Peak <> 0 then BP/Peak else Double.NaN;
def crossAbove = Real crosses above 0;
def crossBelow = Real crosses below 0;
def zeroCrossing = crossAbove or crossBelow;
# Uses a recursive variable to count the bars between zero-crossings
# If a zero-crossing happens, resets the count to 0.
# Otherwise, increments the count by 1.
def barsSinceCross = if zeroCrossing then 0 else barsSinceCross[1] + 1;
def barsBetweenCrossings = barsSinceCross + 1;
def val = if barsBetweenCrossings < barsBetweenCrossings[1] then 2*barsBetweenCrossings[1] else val[1];
# The following line is a port of Dr Ehlers EasyLanguage program, kept here for reference
#def DC = If barsBetweenCrossings < 3 then 3 else If 2*(barsBetweenCrossings) > 1.25*DC[1] then 1.25*DC[1] else if 2*(barsBetweenCrossings) < 0.8*DC[1] then 0.8*DC[1] else DC[1];
plot DC = val;
DC.HideBubble();
} # End script GetCycleViaBandPassFilter{}
#=============================================================================
#------------------------------------------------
# Adaptive EMA used for ADX/DMI adaptive
script AdaptiveEMA {
input src = close;
input Cycle = 10;
def length = if IsNaN(Floor(Cycle)) then length[1] else Floor(Cycle);
def ExpMovAvg = if !IsNaN(ExpMovAvg[1]) then src*(2/(1+length))+ExpMovAvg[1]*(1-(2/(1+length))) else src*(2/(1+length));
plot EMA = ExpMovAvg;
EMA.HideBubble();
}# endScript AdaptiveEMA{}
#=============================================================================
# *** Computes the Universal Oscillator in this section ***
input BandPassSource = ohlc4;
input BandPassPeriod = 20;
input BandPassBandwidth = 0.70;
#input UseVolatility_Adapter = yes;
#input UseBandPassFilter_Adpater = no;
def CycleLength = if UseVolatility_Adapter then AdaptLengthToVolatility().Length else GetCycleViaBandPassFilter(BandPassSource, BandPassPeriod, BandPassBandwidth).DC;
def price = close + low + high;
def LinDev = GetStdDeviation(Floor(CycleLength), price);
plot CCI = if LinDev == 0 then 0 else (price - GetAvg(CycleLength * CycleFraction, price)) / (LinDev * 0.015);
CCI.AssignValueColor(Color.WHITE);
plot OverBought = over_bought;
plot ZeroLine = 0;
plot OverSold = over_sold;
plot UpSignal = if CCI crosses above ZeroLine then ZeroLine else Double.NaN;
plot DownSignal = if CCI crosses below ZeroLine then ZeroLine else Double.NaN;
UpSignal.SetHiding(!showBreakoutSignals);
DownSignal.SetHiding(!showBreakoutSignals);
OverBought.SetDefaultColor(GetColor(5));
ZeroLine.SetDefaultColor(GetColor(5));
OverSold.SetDefaultColor(GetColor(5));
UpSignal.SetDefaultColor(Color.UPTICK);
UpSignal.SetPaintingStrategy(PaintingStrategy.ARROW_UP);
DownSignal.SetDefaultColor(Color.DOWNTICK);
DownSignal.SetPaintingStrategy(PaintingStrategy.ARROW_DOWN);
Note: Articles from Dr Ehlers use arrays to normalize the periodogram data and then a center of gravity method to pin point the dominant cycle. However, thinkscript does not support arrays as needed to do the drill down further from the periodogram to the precise dominant cycle, per se. However, the cycle value provided by the periodogram in TOS appears to be capable of finding more trading opportunities than found with static fixed length values. Be sure to test it out before using it.
Of course because the Periodogram method cannot be fully implemented without bogging down the computer, the cycle lengths will not be representative of the market cycles. But finally at long last we now have a precise method of measuring the market dominant cycles that produces precise cycle lengths when tested against a sinewave across a wide range of period lengths. I am sure you will enjoy this new tool in your trading arsenal. Enjoy!
Here are a few screenshots of test results showing that the Band-pass Filter method of measuring the DC length of a sinewave produces values that closely match the actual values.
In the following screenshot, the red line is the Dominant Cycle length measured by the zero crossings of the band-pass filter. Zero crossings of the yellow line are watched and the number of candles between crossings are counted and multiplied by two to get the measured value of the dominant cycle length. The yellow line was made by the band-pass filter. The Cyan line is the SineWave signal of given period length = 8. As shown in the screenshot, the band-pass filter measured the period length correctly getting DC = 8:
Above even with a period of 40 the Band-Pass filter method gets an exact match without changing any settings for the band-pass filter.
However, because the band-pass filter method is only counting one half of the cycle then multiplying by two, it does not get the correct DC length when the sinewave has a period length that is odd. However, the values that it gets are only off by +/- 1 as shown in the test cases below.
For sinewave period = 7, the band-pass filter gets DC = 6 or DC = 8, with the correct number being the average of the two:
And finally for sinewave DC = 39, the band-pass filter measures DC = 40 or DC = 38, with the correct value being the average of the two:
Enjoy!
Last edited: