Have you ever noticed that extra zing to the price action when trading with the trend? To help better identify the market state the script below modified the Dr Ehlers CorrelationCycleMarketState indicator to make it adaptive to the dominant market cycle using two different methods: the EhlersAutocorrelationPeriodogram (which is not very precise since TOS does not support matrices as needed to fully implement the complete spectral analysis), and the Band-Pass Filter method of measuring the market dominant cycle period which tests against a sinewave across a wide range of periods to provide precise, closely matching cycle lengths. As a result the indicator tells you if the market is trending up, down or is ranging. And it uses the dominant market cycle to do that to ensure there is no question as to whether its length is set right.
The market state is displayed in a simple label on the bottom right side of the price chart. Here is a screenshot of the label made using the periodogram method.
And here is a screenshot of the label made using the Band-Pass method. This screenshot also shows in the bottom-most study what the market state values were as a function of time as computed via the Band-Pass filter method. The study 2nd from the bottom illustrates the dominant cycle lengths produced by the band-Pass filter method.
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 Band-Pass Filter method provided in the for TOS code above when tested against a sinewave proved to be very precise. Here are a couple screenshots capturing the test results.
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:
The and-pass filter exactly matched the sinewave period for each sinewave tested hainvg even period lengths. I tested up to length 40:
But 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. This was the case for each odd period length value tested.
For sinewave period = 7, the band-pass filter gets DC = 6 or DC = 8, with the correct number being the average of the two:
Finally we now have a computationally inexpensive yet precise method for computing the dominant cycle of the market, the Band-Pass filter method. This method is described in more detail in chapter 5 of Dr Ehlers book, "Cycle analytics for traders". Here it has been used to make the Market State indicator adaptive. However, with this tool in your arsenal you can tune any of your indicators to the market dominant cycle. Enjoy!
The market state is displayed in a simple label on the bottom right side of the price chart. Here is a screenshot of the label made using the periodogram method.
And here is a screenshot of the label made using the Band-Pass method. This screenshot also shows in the bottom-most study what the market state values were as a function of time as computed via the Band-Pass filter method. The study 2nd from the bottom illustrates the dominant cycle lengths produced by the band-Pass filter method.
Here is the script:
CSS:
#Adaptive Market State Label for ThinkOrSwim by Sesqui
# [30AUG2025] - AutocorrelationPeriodogram method which is not precise since TOS does not support matrices needed to fully implemetn the method
# [12OCT2025] - Adds Band-Pass Method of measuring dominant cycle length, which tests against sinewaves to provide very precise cycle lengths
input UseBandPassFilter_DC = yes;
input UseVolatility_Adapter = 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{}
#-----------------------------------------------------------
script GetMarketState {
input limit = 20;
def sx = fold ii = 0 to limit with S do S + GetValue(close, ii);
def sxx = fold jj = 0 to limit with SS do SS + GetValue(Sqr(close), jj);
def sy1 = fold i1 = 0 to limit with s1 do (s1 + Cos(2 * Double.Pi * i1 / limit));
def sxy1 = fold i2 = 0 to limit with s2 do (s2 + GetValue(close, i2) * Cos(2 * Double.Pi * i2 / limit));
def syy1 = fold i3 = 0 to limit with s3 do (s3 + Sqr(Cos(2 * Double.Pi * i3 / limit)));
def sxx_sx = limit * sxx - Sqr(sx);
def corrCosine = if sxx_sx == 0 then 0 else (limit * sxy1 - sx * sy1) / Sqrt(sxx_sx * (limit * syy1 - Sqr(sy1)));
def sy2 = fold j1 = 0 to limit with t1 do (t1 - Sin(2 * Double.Pi * t1 / limit));
def sxy2 = fold j2 = 0 to limit with t2 do (t2 - GetValue(close, j2) * Sin(2 * Double.Pi * j2 / limit));
def syy2 = fold j3 = 0 to limit with t3 do (t3 + Sqr(Sin(2 * Double.Pi * j3 / limit)));
def corrNegSine = if sxx_sx == 0 then 0 else (limit * sxy2 - sx * sy2) / Sqrt(sxx_sx * (limit * syy2 - Sqr(sy2)));
def CorrelationWithCosine = corrCosine;
def CorrelationWithNegativeSine = corrNegSine;
# CorrelationCycleAngle Indicator made adaptive
def real = CorrelationWithCosine;
def imag = CorrelationWithNegativeSine;
def angleRad = if imag != 0 then Double.Pi / 2 + ATan(real / imag) - (if imag > 0 then Double.Pi else 0) else 0;
def angle = 180 / Double.Pi * angleRad;
def positiveAngle = if IsNaN(positiveAngle[1]) then 0 else if positiveAngle[1] - angle < 270 and angle < positiveAngle[1] then positiveAngle[1] else angle;
def CorrelationAngle = positiveAngle;
#----MarketState indicator modified to make it adpative----
#
# Charles Schwab & Co. (c) 2008-2025
#
input trendLength = 40;
def trendAngle = 360 / trendLength;
def MarketState;
if (AbsValue(angle - angle[1]) < trendAngle and angle < 0) {
MarketState = -1;
} else if (AbsValue(angle - angle[1]) < trendAngle and angle >= 0) {
MarketState = 1;
} else {
MarketState = 0;
}
plot State = MarketState;
State.HideBubble();
} # End Script GetMarketState{}
#================================================================================
# Makes the CorrelationCycle Indicator Adaptable
# Modified by implementing nested folds to enable adaptive indicator
input BandPassSource = ohlc4;
input BandPassPeriod = 20;
input BandPassBandwidth = 0.70;
#input UseBandPassFilter_DC = yes;
#input UseVolatility_Adapter = no;
def Length = If UseBandPassFilter_DC then GetCycleViaBandPassFilter(BandPassSource, BandPassPeriod, BandPassBandwidth).DC else AdaptLengthToVolatility().Length;
def limit = if IsNaN(Length) then limit[1] else Floor(Length);
def MarketState = GetMarketState(Limit).State;
#MarketState.SetLineWeight(3);
#MarketState.AssignValueColor(Color.WHITE);
# Label for Market State----------------------------
AddLabel(yes, if MarketState == 1 then "Adaptive: Trending UP" else if MarketState == -1 then "Adaptive: Trending DOWN" else "Adaptive: RANGING", if MarketState == 1 then Color.GREEN else if MarketState == -1 then Color.RED else CreateColor(255, 255, 204), Location.BOTTOM_RIGHT, FontSize.SMALL);
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 Band-Pass Filter method provided in the for TOS code above when tested against a sinewave proved to be very precise. Here are a couple screenshots capturing the test results.
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:
The and-pass filter exactly matched the sinewave period for each sinewave tested hainvg even period lengths. I tested up to length 40:
But 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. This was the case for each odd period length value tested.
For sinewave period = 7, the band-pass filter gets DC = 6 or DC = 8, with the correct number being the average of the two:
Finally we now have a computationally inexpensive yet precise method for computing the dominant cycle of the market, the Band-Pass filter method. This method is described in more detail in chapter 5 of Dr Ehlers book, "Cycle analytics for traders". Here it has been used to make the Market State indicator adaptive. However, with this tool in your arsenal you can tune any of your indicators to the market dominant cycle. Enjoy!
Last edited: