Adaptive CCI for ThinkOrSwim

Sesqui

Member
VIP
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.
1756698962171.png


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.
1760319428604.png



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:

1760319790465.png

1760319852040.png


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:
1760319922387.png


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:

1760319948752.png



Enjoy!
 
Last edited:

Join useThinkScript to post your question to a community of 21,000+ developers and traders.

Here is a script that uses the EhlersAutoCorrelationPeriodogram to make the Commodity Channel Index (CCI) adaptive so that it uses the dominant market cycle, 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 adaptive CCI is showin in the lower chart of this screenshot.
View attachment 25613

Here is the script:
CSS:
# Adaptive CCI using EhlersAutoCorrelationPeriodogram by Sesqui 31AUG2025

declare lower;

input CycleFraction = 0.333;
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{}

#==============================================================================================

def CycleLength = GetCycle().Cycle;

def price = close + low + high;
def LinDev = GetStdDeviation(CycleLength*CycleFraction, 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.
All I get is the Lower Indicator, Where is the rest? I am interested in the Volume Spike Label, does it have an Alert?
 
@Dman,

The volume spike is from the VIP indicator named, the "Anna Coulling's Volume Price Analysis -- VIP style". It also colors the price bars in a unique way that really helps reveal the trend aligned with the volume action. It can be accessed by VIP members....

PS - the post was intended to provide only an adaptive CCI indicator and as a result the rest of the features were just what one of my layouts had on it when I took a screenshot to show what the adaptive CCI looked like. But the Adaptive ADX label can be found here: Adaptive ADX Label

Here are the Buyer/Seller Volume Labels:
CSS:
# Buyer/Seller Volume Label

def O = open;
def H = high;
def C = close;
def L = low;

def Buying = Volume * (C - L) / (H - L);
def Selling = Volume * (H - C) / (H - L);


# Buyer and Seller Percentage Labels
def TotalVolume = Buying + Selling;
def BuyerPercentage = Buying / TotalVolume * 100;
def SellerPercentage = Selling / TotalVolume * 100;
AddLabel(1, "Buyer VOL: " + Round(BuyerPercentage, 2) + "%", Color.GREEN, Location.TOP_RIGHT, size = FontSize.SMALL);
AddLabel(1, "Seller VOL: " + Round(SellerPercentage, 2) + "%", Color.RED, Location.TOP_RIGHT, size = FontSize.SMALL);

Hope this helps....

Sesqui
 

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
479 Online
Create Post

Similar threads

Similar threads

The Market Trading Game Changer

Join 2,500+ subscribers inside the useThinkScript VIP Membership Club
  • Exclusive indicators
  • Proven strategies & setups
  • Private Discord community
  • ‘Buy The Dip’ signal alerts
  • Exclusive members-only content
  • Add-ons and resources
  • 1 full year of unlimited support

Frequently Asked Questions

What is useThinkScript?

useThinkScript is the #1 community of stock market investors using indicators and other tools to power their trading strategies. Traders of all skill levels use our forums to learn about scripting and indicators, help each other, and discover new ways to gain an edge in the markets.

How do I get started?

We get it. Our forum can be intimidating, if not overwhelming. With thousands of topics, tens of thousands of posts, our community has created an incredibly deep knowledge base for stock traders. No one can ever exhaust every resource provided on our site.

If you are new, or just looking for guidance, here are some helpful links to get you started.

What are the benefits of VIP Membership?
VIP members get exclusive access to these proven and tested premium indicators: Buy the Dip, Advanced Market Moves 2.0, Take Profit, and Volatility Trading Range. In addition, VIP members get access to over 50 VIP-only custom indicators, add-ons, and strategies, private VIP-only forums, private Discord channel to discuss trades and strategies in real-time, customer support, trade alerts, and much more. Learn all about VIP membership here.
How can I access the premium indicators?
To access the premium indicators, which are plug and play ready, sign up for VIP membership here.
Back
Top