In a previous post I provided a script for the Adaptive Schaff Trend Cycle of Price (close). Here are the scripts for applying the Adaptive Schaff Trend Cycle to the On Balance Volume (OBV) and then to the Relative Volatility Index (RVI). The STC is made adaptive using EhlersAutocorrelationPeriodogram.
With all three you have adaptive price, volume and volatility indicators if needed.
Script for Adaptive STC of OBV:
Script for adaptive STC of Relative Volatility Index (RVI):
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 nail down the precise dominant cycle values, per se. However, the Band-Pass Filter method provided in the scripts above when tested against sine waves having a wide range of period values identified precise values of dominant cycle periods. Consider test driving the indicators made adaptive via the Band-Pass filter method. Enjoy!
With all three you have adaptive price, volume and volatility indicators if needed.
Script for Adaptive STC of OBV:
CSS:
#Adaptive Schaff Trend Cycle (STC) of On Balance Volume (OBV) by Sesqui
#
# [15AUG2025]: Uses the TOS periodgram method to make it adaptive. However, TOS does not support matrices
# as needed to precisely obtain the dominant cycle preiods. Furthermore the periodogram is computationally
# demanding and may bog down some computers. As a result it is not recomended.
#
# [18OCT2025]: Sesqui added the more precise method of obtaining dominant cycle filters via the Band-Pass
# filter method of CH 5 of Dr Ehlers' book, "Cycle analytics for Traders". When tested against sinewaves
# having a wide range of periods, this method obtained precise values of the dominant cycle period and
# performed well on the TOS platform.
declare lower;
#===========================================================================================================================
script GetCycle {
# Returns the dominant market cycle for use in adaptive indicators
input source = close;
#------------------------------------------
# Charles Schwab & Co. (c) 2016-2025
#
def lag = 48;
def x = EhlersRoofingFilter("price" = source, "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{}
#------------------------------------------------
# Adaptive EMA used by Adaptive MACD (AMACD)
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{}
#------------------------------------------------
script AdaptiveMACD {
# Computes the Adaptive MACD
input source = close;
input fastLength = 3;
input Cycle = 10; # pass Cycle into this parameter for adaptive MACD
input SmoothLength = 16;
input averageType = AverageType.EXPONENTIAL;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
plot Value = MovingAverage(averageType, source, fastLength) - AdaptiveEMA(source, CycleLength).EMA;
plot Avg = MovingAverage(averageType, Value, SmoothLength);
}# end AdaptiveMACD{}
#---------------------------------------------------
#===================================================================================
#*** 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 get_Highest {
input source = high;
input CycleLength = 10;
def length = if IsNaN(Floor(CycleLength)) then length[1] else Floor(CycleLength);
def cur = source[0];
def val = fold i = 0 to length with max = cur do if max >= GetValue(source, i + 1) then max else GetValue(source, i + 1);
plot Highest = val;
Highest.HideBubble();
}# end Script getHighest{}
#--------------
script get_Lowest {
input source = low;
input CycleLength = 10;
def length = if IsNaN(Floor(CycleLength)) then length[1] else Floor(CycleLength);
def cur = source[0];
def val = fold i = 0 to length with min = cur do if min <= GetValue(source, i + 1) then min else GetValue(source, i + 1);
plot Lowest = val;
Lowest.HideBubble();
}# end Script getLowest{}
#------------------------------------------------------------------------------
script AdaptSMA {
input source = close;
input CycleLength = 10;
def length = if IsNaN(Floor(CycleLength)) then length[1] else Floor(CycleLength);
def sum = fold index = 0 to length with s = 0 do s + GetValue(source, index);
plot SMA = sum / CycleLength;
SMA.HideBubble();
}# End Script AdaptSMA{}
#======================================================================================
# *** Computes the Adaptive SchaffTrendCycle Trend Cycle (ASTC) in this section ***
def STC_SOURCE = OnBalanceVolume();
input BandPassPeriod = 20;
input BandPassBandwidth = 0.70;
def Cycle = GetCycleViaBandPassFilter(STC_SOURCE, BandPassPeriod, BandPassBandwidth).DC;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
input KPeriod = 5;
input DPeriod = 3;
input over_bought = 80;
input over_sold = 20;
input averageType = AverageType.EXPONENTIAL;
def macd = AdaptiveMACD(STC_SOURCE, 3, CycleLength).Value;
def fastK1 = FastKCustom(macd, KPeriod);
def fastD1 = MovingAverage(averageType, fastK1, DPeriod);
def fastK2 = FastKCustom(fastD1, KPeriod);
plot STC = MovingAverage(averageType, fastK2, DPeriod);
STC.HideBubble();
plot CenterLine = 50;
CenterLine.HideBubble();
CenterLine.AssignValueColor(Color.GRAY);
CenterLine.SetPaintingStrategy(PaintingStrategy.DASHES);
plot OverBought = over_bought;
OverBought.HideBubble();
plot OverSold = over_sold;
OverSold.HideBubble();
STC.SetDefaultColor(GetColor(8));
OverBought.SetDefaultColor(GetColor(7));
OverSold.SetDefaultColor(GetColor(7));
def Diff = STC - STC[1];
STC.SetLineWeight(3);
STC.DefineColor("Positive and Up", Color.GREEN);
STC.DefineColor("Positive and Down", Color.DARK_GREEN);
STC.DefineColor("Negative and Down", Color.RED);
STC.DefineColor("Negative and Up", Color.DARK_RED);
STC.AssignValueColor(if Diff >= 0 then if Diff > Diff[1] then STC.color("Positive and Up") else STC.color("Positive and Down") else if Diff < Diff[1] then STC.color("Negative and Down") else STC.color("Negative and Up"));
AddCloud(OverSold,0,Color.LIGHT_GREEN);
AddCloud(100,OverBought,Color.LIGHT_RED);
Script for adaptive STC of Relative Volatility Index (RVI):
CSS:
# Script for Adaptive Schaff Trend Cycle of Relative Volatility Index (RVI)
#
# [15AUG2025]: Uses the TOS periodgram method to make it adaptive. However, TOS does not support matrices
# as needed to precisely obtain the dominant cycle preiods. Furthermore the periodogram is computationally
# demanding and may bog down some computers. As a result it is not recomended.
#
# [18OCT2025]: Sesqui added the more precise method of obtaining dominant cycle filters via the Band-Pass
# filter method of CH 5 of Dr Ehlers' book, "Cycle analytics for Traders". When tested against sinewaves
# having a wide range of periods, this method obtained precise values of the dominant cycle period and
# performed well on the TOS platform.
declare lower;
#===========================================================================================================================
script GetCycle {
# Returns the dominant market cycle for use in adaptive indicators
input source = close;
#------------------------------------------
# Charles Schwab & Co. (c) 2016-2025
#
def lag = 48;
def x = EhlersRoofingFilter("price" = source, "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{}
#------------------------------------------------
# Adaptive EMA used by Adaptive MACD (AMACD)
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{}
#------------------------------------------------
script AdaptiveMACD {
# Computes the Adaptive MACD
input source = close;
input fastLength = 3;
input Cycle = 10; # pass Cycle into this parameter for adaptive MACD
input SmoothLength = 16;
input averageType = AverageType.EXPONENTIAL;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
plot Value = MovingAverage(averageType, source, fastLength) - AdaptiveEMA(source, CycleLength).EMA;
plot Avg = MovingAverage(averageType, Value, SmoothLength);
}# end AdaptiveMACD{}
#---------------------------------------------------
#===================================================================================
#*** 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 AdaptiveStdDev {
input source = close;
input Cycle = 10;
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 AdaptiveStdDev{}
Script adaptiveRVI{
input Cycle = 10;
input averageLength = 5;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
def stdDevHi = AdaptiveStdDev(high, CycleLength).StdDev;
def stdDevLo = AdaptiveStdDev(low, CycleLength).StdDev;
def avgStDevHiUp = AdaptiveEMA(if high > high[1] then stdDevHi else 0, Cycle);
def avgStDevHiDown = AdaptiveEMA(if high < high[1] then stdDevHi else 0, Cycle);
def avgStDevLoUp = AdaptiveEMA(if low > low[1] then stdDevLo else 0, Cycle);
def avgStDevLoDown = AdaptiveEMA(if low < low[1] then stdDevLo else 0, Cycle);
def rviHi = if avgStDevHiUp + avgStDevHiDown == 0 then 50 else 100 * avgStDevHiUp / (avgStDevHiUp + avgStDevHiDown);
def rviLo = if avgStDevLoUp + avgStDevLoDown == 0 then 50 else 100 * avgStDevLoUp / (avgStDevLoUp + avgStDevLoDown);
plot ARVI = (rviHi + rviLo) / 2;
ARVI.HideBubble();
}#end script AdaptiveRVI{}
#==============================================================================================
input BandPassPeriod = 20;
input BandPassBandwidth = 0.70;
input KPeriod = 5;
input DPeriod = 3;
input over_bought = 80;
input over_sold = 20;
def Cycle = GetCycleViaBandPassFilter(ohlc4, BandPassPeriod, BandPassBandwidth).DC;
def CycleLength = if IsNaN(Floor(Cycle)) then CycleLength[1] else Floor(Cycle);
def STC_SOURCE = AdaptiveRVI(CycleLength, CycleLength);
def macd = AdaptiveMACD(STC_SOURCE, 3, CycleLength).Value;
def fastK1 = FastKCustom(macd, KPeriod);
def fastD1 = MovingAverage(averageType.Exponential,fastK1, DPeriod);
def fastK2 = FastKCustom(fastD1, KPeriod);
plot STC = MovingAverage(averageType.Exponential, fastK2, DPeriod);
STC.HideBubble();
plot CenterLine = 50;
CenterLine.HideBubble();
CenterLine.AssignValueColor(Color.GRAY);
CenterLine.SetPaintingStrategy(PaintingStrategy.DASHES);
plot OverBought = over_bought;
OverBought.HideBubble();
plot OverSold = over_sold;
OverSold.HideBubble();
STC.SetDefaultColor(GetColor(8));
OverBought.SetDefaultColor(GetColor(7));
OverSold.SetDefaultColor(GetColor(7));
def Diff = STC - STC[1];
STC.SetLineWeight(3);
STC.DefineColor("Positive and Up", Color.GREEN);
STC.DefineColor("Positive and Down", Color.DARK_GREEN);
STC.DefineColor("Negative and Down", Color.RED);
STC.DefineColor("Negative and Up", Color.DARK_RED);
STC.AssignValueColor(if Diff >= 0 then if Diff > Diff[1] then STC.Color("Positive and Up") else STC.Color("Positive and Down") else if Diff < Diff[1] then STC.Color("Negative and Down") else STC.Color("Negative and Up"));
AddCloud(OverSold, 0, Color.LIGHT_GREEN);
AddCloud(100, OverBought, Color.LIGHT_RED);
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 nail down the precise dominant cycle values, per se. However, the Band-Pass Filter method provided in the scripts above when tested against sine waves having a wide range of period values identified precise values of dominant cycle periods. Consider test driving the indicators made adaptive via the Band-Pass filter method. Enjoy!
Last edited: