Quant500 Academy

5. A complete case: Bollinger Bands with RSI

Level 12: Backtesting and Real Risk Metrics

We are going to build a somewhat more complex strategy combining Bollinger Bands and the Relative Strength Index (RSI) with Backtrader. The logic is mean reversion with confirmation:

The idea of requiring two simultaneous conditions is to reduce false positives: the price can pierce the lower band in the middle of a downtrend with nothing to revert, and the RSI acts as a filter.

import backtrader as bt
import yfinance as yf
import datetime

class BollingerRSIStrategy(bt.Strategy):
    params = (
        ('bband_period', 20),
        ('rsi_period', 14),
        ('overbought', 70),
        ('oversold', 30),
        ('stddev', 2),
    )

    def __init__(self):
        self.bband = bt.indicators.BollingerBands(
            self.data.close,
            period=self.params.bband_period,
            devfactor=self.params.stddev
        )
        self.rsi = bt.indicators.RelativeStrengthIndex(
            period=self.params.rsi_period
        )

    def next(self):
        if self.data.close < self.bband.lines.bot and self.rsi < self.params.oversold:
            self.buy()
        elif self.data.close > self.bband.lines.top and self.rsi > self.params.overbought:
            self.sell()

data_df = yf.download('AAPL', start='2020-01-01', end='2022-01-01')
data = bt.feeds.PandasData(dataname=data_df)

cerebro = bt.Cerebro(stdstats=False)
cerebro.addstrategy(BollingerRSIStrategy)
cerebro.adddata(data)
cerebro.broker.set_cash(100000.0)
cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
cerebro.broker.setcommission(commission=0.001)

print('Starting portfolio value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final portfolio value: %.2f' % cerebro.broker.getvalue())

cerebro.plot(iplot=True)

What each piece does

BollingerBands builds three lines: mid (the simple moving average of the stated period), top and bot, placed devfactor standard deviations above and below the average. With devfactor=2 and roughly normal returns, around 5% of closes would fall outside the bands. Financial returns are not normal and have fat tails, so in practice the price leaves the bands considerably more often, especially during episodes of stress.

RelativeStrengthIndex with a period of 14 is Wilder's standard configuration. Watch out for a common trap: in an asset with a strong, sustained uptrend, the 14-period RSI can go months without dropping below 40. The strategy simply will not buy, and the backtest will return a flat result that can be mistaken for a prudent strategy when in reality it is an inactive one.

stdstats=False in Cerebro's constructor disables the standard observers (portfolio value, trades and buy/sell markers) on the chart. It is useful for keeping the plot clean when you already have several indicator panels, but it means you lose sight of the equity curve, which is precisely what you should be looking at most. Use it knowingly.

The author's warnings, which are worth taking seriously

About this particular piece of code there are two shortcomings you should already be able to spot on your own. The first: there is no stop-loss and no profit target, so a losing position can stay open indefinitely. The second: because self.position is never checked, several consecutive buy signals accumulate an uncontrolled position. Adding if not self.position before buying and using self.close() to exit is the bare minimum before taking any number that comes out of here seriously.

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.