TTM Squeeze: Various
A very powerful/popular study within thinkorswim is called the TTM Squeeze. It is one of the few studies who's source code is hidden from view (shown here). This makes it impossible to use it for the backtesting strategies section, or to reference any part of it in a custom study. To do that we will have to write this study from the ground up, so that we can have something to reference in future studies.
First we extract and condense the necessary code from the BollingerBands and Keltner Channels studies:
#Both Bollinger and Keltner
input length = 20;
def average = MovingAverage(data = close, length = length, averageType = AverageType.EXPONENTIAL);
#Bollinger Bands
input Num_Dev = 2.0;
def sDev = StDev(data = close, length = length);
def bollinger_upper = average + Num_Dev * sDev;
def bollinger_lower = average - Num_Dev * sDev;
#Keltner Channels
input factor = 1.5;
def shift = factor * MovingAverage(data = TrueRange(high, close, low), length = length);
def Keltner_Upper = average + shift;
def Keltner_Lower = average - shift;
Next we make the red/green dots based on if the Bollinger is inside the Keltner or not
#Squeeze Dots
plot squeeze = if IsNaN(close) then Double.NaN else 0;
squeeze.SetPaintingStrategy(PaintingStrategy.POINTS);
squeeze.hideBubble();
squeeze.hidetitle();
squeeze.DefineColor("Squeeze", Color.RED);
squeeze.DefineColor("Out Of Squeeze", Color.GREEN);
def In_squeeze = bollinger_upper < Keltner_Upper and bollinger_lower > Keltner_Lower;
squeeze.AssignValueColor(if In_squeeze then squeeze.Color("Squeeze") else squeeze.Color("Out Of Squeeze"));
squeeze.setlineWeight(3);
Next we add the code for the histogram and its color changes:
#Histogram
def donchian_midline = (Highest(high, length) + Lowest(low, length)) / 2;
def delta = close - ((donchian_midline + average) / 2);
plot histogram = 100*(Inertia(delta, length));
histogram.SetPaintingStrategy(PaintingStrategy.HISTOGRAM);
histogram.DefineColor("Positive and Up", Color.cyan);
histogram.DefineColor("Positive and Down", Color.blue);
histogram.DefineColor("Negative and Down", Color.red);
histogram.DefineColor("Negative and Up", Color.yellow);
histogram.AssignValueColor(if histogram >= 0 then if histogram > histogram[1] then histogram.Color("Positive and Up") else histogram.Color("Positive and Down") else if histogram < histogram[1] then histogram.Color("Negative and Down") else histogram.Color("Negative and Up"));
Once we put it all together, this is the result. On the top is the original TTM squeeze, and on the bottom is the results of our script. As you can see, it matches 100% with the original, every green and red dot is the same, as well as the exact value of the histogram (on the right side of that chart).
Having all this script opens up a lot more uses for the TTM information. For example, the default TTM watchlist column only allows you to display the TTM histogram's value. Often times people want their column on the left to instead display whether the squeeze is active (if the green or red dot is showing).
If we modify the script a bit, we can use it in a custom column
#Both Bollinger and Keltner
def length = 20;
def average = MovingAverage(data = close, length = length, averageType = AverageType.EXPONENTIAL);
#Bollinger Bands
def Num_Dev = 2.0;
def sDev = StDev(data = close, length = length);
def bollinger_upper = average + Num_Dev * sDev;
def bollinger_lower = average - Num_Dev * sDev;
#Keltner Channels
def factor = 1.5;
def shift = factor * MovingAverage(data = TrueRange(high, close, low), length = length);
def Keltner_Upper = average + shift;
def Keltner_Lower = average - shift;
#Squeeze
plot In_squeeze = bollinger_upper < Keltner_Upper and bollinger_lower > Keltner_Lower;
assignbackgroundColor(if in_squeeze then color.red else color.green);
Now the column will show us which of these stocks are currently in a squeeze(in red). The timeframe the stock is in a squeeze on is selected by the user, in this case we are using a 1D timeframe.
A further use of this concept would be to make a column for multiple (in this case 3) different timeframes, to see if any symbols are in a squeeze on all three charts. That's fine for a small list (above), but when we are looking through a larger list, we encounter the custom expression subscription limit error, which means we have too many custom columns requesting data, and we no longer get anything useful on our list(below).
To solve this, we can combine all three timeframes into 1 script:
# All Timeframes
def length = 20;
def Num_Dev = 2.0;
def factor = 1.5;
# Bollinger and Keltner Daily
def average = MovingAverage(data = close(period = AggregationPeriod.DAY), length = length, averageType = AverageType.EXPONENTIAL);
# Bollinger Bands Daily
def sDev = StDev(data = close(period = AggregationPeriod.DAY), length = length);
def bollinger_upper = average + Num_Dev * sDev;
def bollinger_lower = average - Num_Dev * sDev;
# Keltner Channels Daily
def shift = factor * MovingAverage(data = TrueRange(high(period = AggregationPeriod.DAY), close(period = AggregationPeriod.DAY), low(period = AggregationPeriod.DAY)), length = length);
def Keltner_Upper = average + shift;
def Keltner_Lower = average - shift;
# Squeeze Daily
def In_squeeze = bollinger_upper < Keltner_Upper and bollinger_lower > Keltner_Lower;
# Bollinger and Keltner 4h
def average_4h = MovingAverage(data = close(period = AggregationPeriod.FOUR_HOURS), length = length, averageType = AverageType.EXPONENTIAL);
# Bollinger Bands 4h
def sDev_4h = StDev(data = close(period = AggregationPeriod.FOUR_HOURS), length = length);
def bollinger_upper_4h = average_4h + Num_Dev * sDev_4h;
def bollinger_lower_4h = average_4h - Num_Dev * sDev_4h;
# Keltner Channels 4h
def shift_4h = factor * MovingAverage(data = TrueRange(high(period = AggregationPeriod.FOUR_HOURS), close(period = AggregationPeriod.FOUR_HOURS), low(period = AggregationPeriod.FOUR_HOURS)), length = length);
def Keltner_Upper_4h = average_4h + shift_4h;
def Keltner_Lower_4h = average_4h - shift_4h;
# Squeeze 4h
def In_squeeze_4h = bollinger_upper_4h < Keltner_Upper_4h and bollinger_lower_4h > Keltner_Lower_4h;
# Bollinger and Keltner Weekly
def average_W = MovingAverage(data = close(period = AggregationPeriod.WEEK), length = length, averageType = AverageType.EXPONENTIAL);
# Bollinger Bands Weekly
def sDev_W = StDev(data = close(period = AggregationPeriod.WEEK), length = length);
def bollinger_upper_W = average_W + Num_Dev * sDev_W;
def bollinger_lower_W = average_W - Num_Dev * sDev_W;
# Keltner Channels Weekly
def shift_W = factor * MovingAverage(data = TrueRange(high(period = AggregationPeriod.WEEK), close(period = AggregationPeriod.WEEK), low(period = AggregationPeriod.WEEK)), length = length);
def Keltner_Upper_W = average_W + shift_W;
def Keltner_Lower_W = average_W - shift_W;
# Squeeze Weekly
def In_squeeze_W = bollinger_upper_W < Keltner_Upper_W and bollinger_lower_W > Keltner_Lower_W;
plot multi_squeeze = in_squeeze + in_squeeze_4h + in_squeeze_W;
assignbackgroundcolor(if multi_squeeze == 3 then color.dark_red else if multi_squeeze == 2 then color.red else if multi_squeeze == 1 then color.light_red else color.green);
Now if all 3 timeframes are in a squeeze, it will show a "3" plus the darkest shade of red, if just 2 of the 3 timeframes are showing it will be a "2" and so on. In addition to being able to far more quickly find which out of the potentially hundreds of stocks that match, we have reduced our total columns used by 2/3.
Another use of the TTM script is to lock it to longer timeframes while we are looking at a shorter timeframe:
declare lower;
# Bollinger and Keltner
input length = 20;
def average = MovingAverage(data = close(period = AggregationPeriod.DAY), length = length, averageType = AverageType.EXPONENTIAL);
# Bollinger Bands
input Num_Dev = 2.0;
def sDev = StDev(data = close(period = AggregationPeriod.DAY), length = length);
def bollinger_upper = average + Num_Dev * sDev;
def bollinger_lower = average - Num_Dev * sDev;
# Keltner Channels
input factor = 1.5;
def shift = factor * MovingAverage(data = TrueRange(high(period = AggregationPeriod.DAY), close(period = AggregationPeriod.DAY), low(period = AggregationPeriod.DAY)), length = length);
def Keltner_Upper = average + shift;
def Keltner_Lower = average - shift;
# Squeeze Dots
plot squeeze = if IsNaN(close(period = AggregationPeriod.DAY)) then Double.NaN else 0;
squeeze.SetPaintingStrategy(PaintingStrategy.POINTS);
squeeze.DefineColor("Squeeze", Color.RED);
squeeze.DefineColor("Out Of Squeeze", Color.GREEN);
def In_squeeze = bollinger_upper < Keltner_Upper and bollinger_lower > Keltner_Lower;
squeeze.AssignValueColor(if In_squeeze then squeeze.Color("Squeeze") else squeeze.Color("Out Of Squeeze"));
squeeze.setlineWeight(3);
# Histogram
def donchian_midline = (Highest(high(period = AggregationPeriod.DAY), length) + Lowest(low(period = AggregationPeriod.DAY), length)) / 2;
def delta = close(period = AggregationPeriod.DAY) - ((donchian_midline + average) / 2);
plot histogram = 100*(Inertia(delta, length));
def timeframe_lookback = if getaggregationPeriod() == aggregationPeriod.day then 1
else if getaggregationPeriod() == aggregationPeriod.FOUR_HOURS then 2
else if getaggregationPeriod() == aggregationPeriod.two_hours then 4
else if getAggregationPeriod() == aggregationPeriod.HOUR then 7
else if getaggregationPeriod() == aggregationPeriod.THIRTY_MIN then 13 else 26;
histogram.SetPaintingStrategy(PaintingStrategy.HISTOGRAM);
histogram.DefineColor("Positive and Up", Color.cyan);
histogram.DefineColor("Positive and Down", Color.blue);
histogram.DefineColor("Negative and Down", Color.red);
histogram.DefineColor("Negative and Up", Color.yellow);
histogram.AssignValueColor(if histogram >= 0 then if histogram >= histogram[timeframe_lookback] then histogram.Color("Positive and Up") else histogram.Color("Positive and Down") else if histogram <= histogram[timeframe_lookback] then histogram.Color("Negative and Down") else histogram.Color("Negative and Up"));
Now in this chart, we are viewing the price itself (on top) on the 4 hour timeframe, however the two TTM Squeeze's are showing what the TTM Squeeze would look like on the Daily and Weekly timeframes (bottom).
Finally, we can use this script to run a backtest on buying and selling off the TTM squeeze:
#Both Bollinger and Keltner
input length = 20;
def average = MovingAverage(data = close, length = length, averageType = AverageType.EXPONENTIAL);
#Bollinger Bands
input Num_Dev = 2.0;
def sDev = StDev(data = close, length = length);
def bollinger_upper = average + Num_Dev * sDev;
def bollinger_lower = average - Num_Dev * sDev;
#Keltner Channels
input factor = 1.5;
def shift = factor * MovingAverage(data = TrueRange(high, close, low), length = length);
def Keltner_Upper = average + shift;
def Keltner_Lower = average - shift;
#Squeeze Dots
def squeeze = if IsNaN(close) then Double.NaN else 0;
def In_squeeze = bollinger_upper < Keltner_Upper and bollinger_lower > Keltner_Lower;
#Histogram
def donchian_midline = (Highest(high, length) + Lowest(low, length)) / 2;
def delta = close - ((donchian_midline + average) / 2);
def histogram = 100 * (Inertia(delta, length));
def timeframe_lookback = if getaggregationPeriod() == aggregationPeriod.day then 50
else if getaggregationPeriod() == aggregationPeriod.FOUR_HOURS then 50
else if getaggregationPeriod() == aggregationPeriod.two_hours then 75
else if getAggregationPeriod() == aggregationPeriod.HOUR then 100
else if getaggregationPeriod() == aggregationPeriod.THIRTY_MIN then 125
else if getaggregationperiod() == aggregationperiod.fifteen_MIN then 150
else if getaggregationperiod() == aggregationPeriod.FIVE_MIN then 175 else 200;
#Histogram Trigger height:
def negative_histogram = if histogram < .3*lowest(histogram,timeframe_lookback) then 1 else double.nan;
def negative_reversal = if negative_histogram and histogram > histogram[1] then 1 else double.nan;
def positive_histogram = if histogram > .3*highest(histogram, timeframe_lookback) then 1 else double.nan;
def positive_reversal = if positive_histogram and histogram < histogram[1] then 1 else double.nan;
input offset = 0;
input price = ohlc4;
AddOrder(OrderType.BUY_to_open, negative_reversal, price[offset], tickColor = Color.cyan, arrowColor = Color.cyan);
addorder(ordertype.seLL_TO_CLOSE, histogram < histogram[1], price[offset]);
AddOrder(OrderType.sell_to_open, positive_reversal, price[offset]);
addorder(ordertype.buy_TO_CLOSE, histogram > histogram[1], price[offset], tickColor = Color.cyan);
The blue and purple marks on the chart show when the stock was bought and sold, and the chart at the very bottom shows the cumulative profit or loss based on the buys and sells of this backtest. In this instance we are profitable by $2,600 over 2 days from an initial investment of about $38,000 (100 shares x $380).