Long Term Bollinger Band Breakout Strategy for ThinkorSwim

Zachc

Zachc

Member
VIP
I am always looking for a nice long term system that is adept at entering into long term trends. I don't have time for day trading or even swing trading for that matter.

I was listening to the ChatwithTraders podcast which I highly recommend to anyone that has not listened to check out the huge back catalog Aaron has encompassing every market trading strategy and mindset you can think of. This month's episode is with Nick Radge which they took the time to outline Nick's process on building trading strategies which were very enlighting for a novice.

Nick outlined a simple trend-following the strategy he developed backtested and published years ago in his book Unholy grails he uses in his retirement portfolio. The strategy is long-only and is perfect for a long term set it and forget it type of strategy. Which would be perfect for my 2-year-old daughters UTMA account.

Anyways TLDR: I made a video for everyone explaining the strategy and showing some example trades.

Strategy Parameters
  • This is a long-only strategy
  • Buy and Sell the day after the signal at the open
  • Regime filter used to halt trading when the index is below the 200 SMA
  • Rate of change filters out any moves that are less than 10% looking back 20 periods
  • Added an ATR Trailing Stop to weed out the bigger losses due to the BBands expanding really wide during large trends
Bollinger Band Settings
  • Weekly Time Frame
  • 100 Period
  • Upper band 2 stdv
  • Lower band -1 stdv
  • SMA Average
BollBand_Long Breakout
BollBand_Short Breakout
Weekly Scan for new positions
Daily Scan for new positions

Typo on exactly in the image ugh.



Code:
##Nick Radge Bollinger Band Breakout Strat
##
##Basic Strategy used on a Daily, Weekly, Monthly timeframe.
# 1) Bollinger bands Period 100 days
# 2) Boll bands set to 2 standard deviation
# 3) Buy on the open day after the signal
# 4) Bottom Bollinger band set to -1 stdv
# 6) Sell when price closes below this level on the following day
# 7) Use a regime filter to gague the overall market sentimate
#    Gaged by the 200day MA on then index that you can select from the drop down.  SPX is default.

declare hide_on_intraday;
input roc = 20;
input bbPeriod = 100;
input bbUpper = 2;
input bbLower = -1;
input bbAvg = AverageType.SIMPLE;
input market = {default SPX, NDX, RUT, DJX};
Input Market_200Day_MA = 40;

## Indicator
def bbBreakUp = close > BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).UpperBand;
def bbBreakDown = close < BollingerBands(price = open, length = bbPeriod, "num dev dn" = bbLower, "num dev up" = bbUpper, "average type" = bbAvg).LowerBand;

## Regime Filter
#Is the market this stock is apart of above the 200 day MA
#8/13/19 Updated to 40 periods for the weekly time frame as the default credit mc01439

def length = Market_200Day_MA;
def RF = SimpleMovingAvg(close(symbol = market), length);

plot closeMA = close(symbol = market) < RF;

def marClose = close(market);
AddLabel(1, Concat("Index: ", Concat(market, Concat(" " + marClose, " 200SMA= " + RF))), if marClose > RF
  then Color.GREEN
  else Color.RED);

## Confirmation rate of change
def rate = RateOfChange(roc);

########################################################################
## Trailing Stop

input trailType = {default modified, unmodified};
input ATRPeriod = 5;
input ATRFactor = 3.5;
input firstTrade = {default long, short};

assert(ATRFactor > 0, "'atr factor' must be positive: " + ATRFactor);

def HiLo = Min(high - low, 1.5 * Average(high - low, ATRPeriod));
def HRef = if low <= high[1]
  then high - close[1]
  else (high - close[1]) - 0.5 * (low - high[1]);
def LRef = if high >= low[1]
  then close[1] - low
  else (close[1] - low) - 0.5 * (low[1] - high);
def ATRMod = ExpAverage(Max(HiLo, Max(HRef, LRef)), 2 * ATRPeriod - 1);
def loss = ATRFactor * ATRMod;
#  case unmodified:
#    loss = ATRFactor * ATR(high, close, low, ATRPeriod);


def state = {default init, long, short};
def trail;

switch (state[1]) {
  case init:
    if (!IsNaN(loss)) {
      switch (firstTrade) {
        case long:
          state = state.long;
          trail =  close - loss;
        case short:
          state = state.short;
          trail = close + loss;
      }
    } else {
      state = state.init;
      trail = Double.NaN;
    }

  case long:
    if (close > trail[1]) {
      state = state.long;
      trail = Max(trail[1], close - loss);
    } else {
      state = state.short;
      trail = close + loss;
    }
  case short:
    if (close < trail[1]) {
      state = state.short;
      trail = Min(trail[1], close + loss);
    } else {
      state = state.long;
      trail =  close - loss;
    }
}

plot TrailingStop = trail;
TrailingStop.SetPaintingStrategy(PaintingStrategy.POINTS);
TrailingStop.DefineColor("Buy", GetColor(0));
TrailingStop.DefineColor("Sell", GetColor(1));
TrailingStop.AssignValueColor(if state == state.long
  then TrailingStop.color("Sell")
  else TrailingStop.color("Buy"));

plot cross = close crosses below trailingstop;
cross.setPaintingStrategy(paintingStrategy.BOOLEAN_ARROW_DOWN);
#end


###Plots###
plot upper = BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).UpperBand;
plot lower = BollingerBands(close, length = bbPeriod, "num dev up" = bbLower, "average type" = bbAvg, "num dev dn" = bbLower).LowerBand;
plot buySignal = bbBreakUp and rate > 10 and marClose > RF;
    buySignal.SetpaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_UP);
    buySignal.SetLineWeight(2);
    buySignal.SetDefaultColor(Color.GREEN);
plot sellSignal = bbBreakDown;
    sellSignal.SetpaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_Down);
    sellSignal.SetLineWeight(2);
    sellSignal.SetDefaultColor(Color.Red);

### Order Entry ###
#8/13/19 changed the index to [-1] on the open variable to ensure the order is placed on the open of the following day credit: mc01439

AddOrder(condition = buySignal, type = OrderType.BUY_TO_OPEN, price = open[-1], name = "bbBreak_LE");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_CLOSE, price = open[-1], name = "bbBreak_LX");
AddOrder(condition = cross, type = OrderType.SELL_TO_CLOSE, price = open[-1], name = "TrailStop");
 
Last edited:
Zachc

Zachc

Member
VIP
If you are interested in and have an account that can go short equities, futures forex, etc. Here is the study reversed which will allow you to go short. Just add this study on top of the long version.

Also Added daily and weekly scans to the main post link section.

Code:
##Nick Radge Bollinger Band Breakout Strat short version
##
##Basic Strategy used on a Daily, Weekly timeframe.
# 1) Bollinger bands Period 100 days
# 2) Boll bands set to 1 standard deviation
# 3) Buy on the open day after the signal
# 4) Bottom Bollinger band set to -2 stdv
# 6) Sell when price closes above the top band on the following day
# 7) Use a regime filter to gague the overall market sentimate
# 8) Gaged by the 200day MA on then index that you can select from the drop down.  SPX is default.
# 9) 3.5 ATR trailing stop on by defult adjust according to your strategy.

input roc = 20;
input bbPeriod = 100;
input bbUpper = 1;
input bbLower = -2;
input bbAvg = AverageType.SIMPLE;
input market = {default SPX, NDX, RUT, DJX};
Input Market_200Day_MA = 40;

## Indicator
def bbBreakUp = close crosses above BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).UpperBand;
def bbBreakDown = close crosses below BollingerBands(price = open, length = bbPeriod, "num dev dn" = bbLower, "num dev up" = bbUpper, "average type" = bbAvg).LowerBand;
def bbShortUp = close crosses above BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).LowerBand;

## Regime Filter
#Is the market this stock is apart of above the 200 day MA
def length = Market_200Day_MA;
def RF = SimpleMovingAvg(close(symbol = market), length);

def marClose = close(market);
AddLabel(1, Concat("Index: ", Concat(market, Concat(" " + marClose, " 200SMA= " + RF))), if marClose > RF
  then Color.GREEN
  else Color.RED);

## Confirmation rate of change
def rate = RateOfChange(roc);

########################################################################
## Trailing Stop

input trailType = {default modified, unmodified};
input ATRPeriod = 5;
input ATRFactor = 3.5;
input firstTrade = {default short, long};

Assert(ATRFactor > 0, "'atr factor' must be positive: " + ATRFactor);

def HiLo = Min(high - low, 1.5 * Average(high - low, ATRPeriod));
def HRef = if low <= high[1]
  then high - close[1]
  else (high - close[1]) - 0.5 * (low - high[1]);
def LRef = if high >= low[1]
  then close[1] - low
  else (close[1] - low) - 0.5 * (low[1] - high);
def ATRMod = ExpAverage(Max(HiLo, Max(HRef, LRef)), 2 * ATRPeriod - 1);
def loss;
switch (trailType) {
case modified:
    loss = ATRFactor * ATRMod;
case unmodified:
    loss = ATRFactor * Average(TrueRange(high,  close,  low),  ATRPeriod);
}

def state = {default init, long, short};
def trail;

switch (state[1]) {
case init:
    if (!IsNaN(loss)) {
        switch (firstTrade) {
        case long:
            state = state.long;
            trail =  close - loss;
        case short:
            state = state.short;
            trail = close + loss;
    }
    } else {
        state = state.init;
        trail = Double.NaN;
    }
case long:
    if (close > trail[1]) {
        state = state.long;
        trail = Max(trail[1], close - loss);
    } else {
        state = state.short;
        trail = close + loss;
    }
case short:
    if (close < trail[1]) {
        state = state.short;
        trail = Min(trail[1], close + loss);
    } else {
        state = state.long;
        trail =  close - loss;
    }
}

plot TrailingStop = trail;
TrailingStop.SetPaintingStrategy(PaintingStrategy.POINTS);
TrailingStop.DefineColor("Buy", GetColor(0));
TrailingStop.DefineColor("Sell", GetColor(1));
TrailingStop.AssignValueColor(if state == state.long
  then TrailingStop.Color("Sell")
  else TrailingStop.Color("Buy"));

plot cross = close crosses TrailingStop;
cross.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_DOWN);
#end


###Plots###
plot upper = BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).UpperBand;
plot lower = BollingerBands(close, length = bbPeriod, "num dev up" = bbLower, "average type" = bbAvg, "num dev dn" = bbLower).LowerBand;
plot buySignal = bbBreakUp;
buySignal.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_UP);
buySignal.SetLineWeight(2);
buySignal.SetDefaultColor(Color.GREEN);
plot sellSignal = bbBreakDown and rate < 10 and marClose < RF;
sellSignal.SetPaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_DOWN);
sellSignal.SetLineWeight(2);
sellSignal.SetDefaultColor(Color.RED);

### Order Entry ###

AddOrder(condition = bbBreakUp, type = OrderType.BUY_TO_CLOSE, price = open[1], name = "bbBreak_SX");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_OPEN, price = open[1], name = "bbBreak_SE");
AddOrder(condition = cross, type = OrderType.BUY_TO_CLOSE, price = open[1], name = "TrailStop");
 
Last edited:
markos

markos

Well-known member
VIP
@Zachc Thanks for putting the work in on Nick Radge's study. This is a quality posting!
I follow him on Twitter and get his newsletter.
An excellent person for anyone to learn Trend Following from!
 
Zachc

Zachc

Member
VIP
@Zachc Thanks for putting the work in on Nick Radge's study. This is a quality posting!
I follow him on Twitter and get his newsletter.
An excellent person for anyone to learn Trend Following from!
I couldn't agree more about Nick he is a wealth of knowledge and provides very detailed instructions where a lot of the other guys only go into vague generalities about trading systems and how they develope them. I will be going through some of the other strategies over time. Just wanted to share this one since I currently trade with it.
 
Shinthus

Shinthus

Member
VIP
@Zachc Thanks for putting the work in on Nick Radge's study. This is a quality posting!
I follow him on Twitter and get his newsletter.
An excellent person for anyone to learn Trend Following from!
What’s his twitter handle?
 
mc01439

mc01439

Member
VIP
The way you wrote the order code has the trade open at the open the day the price broke the Upper band 2 stdv. This is hindsight and will cause issues with live order entries.

AddOrder(condition = buySignal, type = OrderType.BUY_TO_OPEN, price = open, name = "bbBreak_LE");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_CLOSE, price = open, name = "bbBreak_LX");
AddOrder(condition = cross, type = OrderType.SELL_TO_CLOSE, price = open, name = "TrailStop");

If you want to entry the day after the signal at the open may want to consider the following;

AddOrder(condition = buySignal, type = OrderType.BUY_TO_OPEN, price = open[-1], name = "bbBreak_LE");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_CLOSE, price = open[-1], name = "bbBreak_LX");
AddOrder(condition = cross, type = OrderType.SELL_TO_CLOSE, price = open[-1], name = "TrailStop");
 
Last edited:
mc01439

mc01439

Member
VIP
I am always looking for a nice long term system that is adept at entering into long term trends. I don't have time for day trading or even swing trading for that matter.

I was listening to the ChatwithTraders podcast which I highly recommend to anyone that has not listened to check out the huge back catalog Aaron has encompassing every market trading strategy and mindset you can think of. This month's episode is with Nick Radge which they took the time to outline Nick's process on building trading strategies which were very enlighting for a novice.

Nick outlined a simple trend-following the strategy he developed backtested and published years ago in his book Unholy grails he uses in his retirement portfolio. The strategy is long-only and is perfect for a long term set it and forget it type of strategy. Which would be perfect for my 2-year-old daughters UTMA account.

Anyways TLDR: I made a video for everyone explaining the strategy and showing some example trades.

Strategy Parameters
  • This is a long-only strategy
  • Buy and Sell the day after the signal at the open
  • Regime filter used to halt trading when the index is below the 200 SMA
  • Rate of change filters out any moves that are less than 10% looking back 20 periods
  • Added an ATR Trailing Stop to weed out the bigger losses due to the BBands expanding really wide during large trends
Bollinger Band Settings
  • Weekly Time Frame
  • 100 Period
  • Upper band 2 stdv
  • Lower band -1 stdv
  • SMA Average



Typo on exactly in the image ugh.



Code:
##Nick Radge Bollinger Band Breakout Strat
##
##Basic Strategy used on a Daily, Weekly, Monthly timeframe.
# 1) Bollinger bands Period 100 days
# 2) Boll bands set to 2 standard deviation
# 3) Buy on the open day after the signal
# 4) Bottom Bollinger band set to -1 stdv
# 6) Sell when price closes below this level on the following day
# 7) Use a regime filter to gague the overall market sentimate
#    Gaged by the 200day MA on then index that you can select from the drop down.  SPX is default.

declare hide_on_intraday;
input roc = 20;
input bbPeriod = 100;
input bbUpper = 2;
input bbLower = -1;
input bbAvg = AverageType.SIMPLE;
input market = {default SPX, NDX, RUT, DJX};


## Indicator
def bbBreakUp = close > BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).UpperBand;
def bbBreakDown = close < BollingerBands(price = open, length = bbPeriod, "num dev dn" = bbLower, "num dev up" = bbUpper, "average type" = bbAvg).LowerBand;

## Regime Filter
#Is the market this stock is apart of above the 200 day MA

def RF = SimpleMovingAvg(close(symbol = market), length = 200);

plot closeMA = close(symbol = market) < RF;

def marClose = close(market);
AddLabel(1, Concat("Index: ", Concat(market, Concat(" " + marClose, " 200SMA= " + RF))), if marClose > RF
  then Color.GREEN
  else Color.RED);

## Confirmation rate of change
def rate = RateOfChange(roc);

########################################################################
## Trailing Stop

input trailType = {default modified, unmodified};
input ATRPeriod = 5;
input ATRFactor = 3.5;
input firstTrade = {default long, short};

assert(ATRFactor > 0, "'atr factor' must be positive: " + ATRFactor);

def HiLo = Min(high - low, 1.5 * Average(high - low, ATRPeriod));
def HRef = if low <= high[1]
  then high - close[1]
  else (high - close[1]) - 0.5 * (low - high[1]);
def LRef = if high >= low[1]
  then close[1] - low
  else (close[1] - low) - 0.5 * (low[1] - high);
def ATRMod = ExpAverage(Max(HiLo, Max(HRef, LRef)), 2 * ATRPeriod - 1);
def loss = ATRFactor * ATRMod;
#  case unmodified:
#    loss = ATRFactor * ATR(high, close, low, ATRPeriod);


def state = {default init, long, short};
def trail;

switch (state[1]) {
  case init:
    if (!IsNaN(loss)) {
      switch (firstTrade) {
        case long:
          state = state.long;
          trail =  close - loss;
        case short:
          state = state.short;
          trail = close + loss;
      }
    } else {
      state = state.init;
      trail = Double.NaN;
    }

  case long:
    if (close > trail[1]) {
      state = state.long;
      trail = Max(trail[1], close - loss);
    } else {
      state = state.short;
      trail = close + loss;
    }
  case short:
    if (close < trail[1]) {
      state = state.short;
      trail = Min(trail[1], close + loss);
    } else {
      state = state.long;
      trail =  close - loss;
    }
}

plot TrailingStop = trail;
TrailingStop.SetPaintingStrategy(PaintingStrategy.POINTS);
TrailingStop.DefineColor("Buy", GetColor(0));
TrailingStop.DefineColor("Sell", GetColor(1));
TrailingStop.AssignValueColor(if state == state.long
  then TrailingStop.color("Sell")
  else TrailingStop.color("Buy"));

plot cross = close crosses below trailingstop;
cross.setPaintingStrategy(paintingStrategy.BOOLEAN_ARROW_DOWN);
#end


###Plots###
plot upper = BollingerBands(close, length = bbPeriod, "num dev up" = bbUpper, "average type" = bbAvg, "num dev dn" = bbLower).UpperBand;
plot lower = BollingerBands(close, length = bbPeriod, "num dev up" = bbLower, "average type" = bbAvg, "num dev dn" = bbLower).LowerBand;
plot buySignal = bbBreakUp and rate > 10 and marClose > RF;
    buySignal.SetpaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_UP);
    buySignal.SetLineWeight(2);
    buySignal.SetDefaultColor(Color.GREEN);
plot sellSignal = bbBreakDown;
    sellSignal.SetpaintingStrategy(PaintingStrategy.BOOLEAN_ARROW_Down);
    sellSignal.SetLineWeight(2);
    sellSignal.SetDefaultColor(Color.Red);

AddOrder(condition = buySignal, type = OrderType.BUY_TO_OPEN, price = open, name = "bbBreak_LE");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_CLOSE, price = open, name = "bbBreak_LX");
AddOrder(condition = cross, type = OrderType.SELL_TO_CLOSE, price = open, name = "TrailStop");
One more thought - if you want the 200 day simple moving average for the index you should consider this change if using higher than a one day chart.

Line #25
## Regime Filter
#Is the market this stock is apart of above the 200 day MA = 40 Bars for the weekly chart
Input Market_200Day_MA = 40; #200/5
def length = Market_200Day_MA;

def RF = SimpleMovingAvg(close(symbol = market), length);
 
Zachc

Zachc

Member
VIP
@mc01439
The way you wrote the order code has the trade open at the open the day the price broke the Upper band 2 stdv. This is hindsight and will cause issues with live order entries.

AddOrder(condition = buySignal, type = OrderType.BUY_TO_OPEN, price = open, name = "bbBreak_LE");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_CLOSE, price = open, name = "bbBreak_LX");
AddOrder(condition = cross, type = OrderType.SELL_TO_CLOSE, price = open, name = "TrailStop");

If you want to entry the day after the signal at the open may want to consider the following;

AddOrder(condition = buySignal, type = OrderType.BUY_TO_OPEN, price = open[-1], name = "bbBreak_LE");
AddOrder(condition = bbBreakDown, type = OrderType.SELL_TO_CLOSE, price = open[-1], name = "bbBreak_LX");
AddOrder(condition = cross, type = OrderType.SELL_TO_CLOSE, price = open[-1], name = "TrailStop");
One more thought - if you want the 200 day simple moving average for the index you should consider this change if using higher than a one day chart.

Line #25
## Regime Filter
#Is the market this stock is apart of above the 200 day MA = 40 Bars for the weekly chart
Input Market_200Day_MA = 40; #200/5
def length = Market_200Day_MA;

def RF = SimpleMovingAvg(close(symbol = market), length);
Hey @mc01439 , thanks! I absolutely missed adding the [-1] index to the open on the order entry. Code has been updated across the board.

Also with the MA filter, I see what you did there that will make it much more accurate when moving between time frames which I frequently do. Something so simple can make such a huge difference. Thanks for all the input!
 
Zachc

Zachc

Member
VIP
@Shinthus @Zachc What are yours? Mine is Mark1Paul. I follow and unfollow often, please take no offense.
@markos Just followed I'm @MilcoZachC. I to purge my twitter follows frequently. I try to dial back the sheer amount of political nonsense so that I can focus on following fintwit. Its murky out there.
 

Top