Bollinger Bands — first developed by John Bollinger in the early 1980’s — measure the volatility range of a security over time. They provide an envelop around the price and can be leveraged in a variety of trading strategies. Some use them on their own, but most frequently they’re combined with other indicators to confirm trends or signal reversals.

We’re going to walk through calculations with the math, pseudocode, and examples in Python. On top of that, we have a few different trading ideas and ways these can be incorporated into your system.

How to Calculate the Bollinger Bands

The Bands require two parameters, N and m. N gives the number of periods we are going to use to calculate the standard deviations (STD or 𝜎) and the simple moving average (SMA) used in to construct the Bands. m is a multiple that we apply to the standard deviations, so we’re going to set bands at 𝑚𝜎 above and below the SMA. Most people use N=20 and m=2 for these settings. With these, we can calculate the Bollinger Bands in 4 simple steps:

  1. Calculate the typical price (TP). Typical price is the average of the high, low, and close for the day.
  • TP[t] = (Close[t] + High[t] + Low[t]) / 3

2. Calculate the simple moving average of the typical price over the past N days (SMA(TP)).

  • SMA_TP[t] = sum(TP[-N:t]) / N

3. Calculate the sample standard deviation of the typical price for the past N days.

  • STD_TP[t] = sqrt(TP[t] - mean(TP))**2 / (N - 1)

4. Get the upper and lower Bands by adding and subtracting the standard deviation and the SMA(TP) values and multiplying by m.

  • UBB[t] = SMA_TP[t] + m * STD_TP[t]
  • LBB[t] = SMA_TP[t] - m * STD_TP[t]

Or, mathematically we can write:

$$TP_t = \frac{C_t + H_t + L_t}{3}$$ $$SMA_t^{TP} = \frac{1}{N} \sum_{t=1}^N TP_{t-N}$$ $$\sigma_t^{TP} = \sqrt{\frac{\sum (TP_t - \bar{TP})^2}{N -1}}$$ $$UBB_t = SMA_t^{TP} + m \sigma_t^{TP}$$ $$LBB_t = SMA_t^{TP} - m \sigma_t^{TP}$$

This should be pretty straightforward. Just average out the close, high, and low to get the typical price, calculate the moving average and standard deviation of the TP, then add and subtract that from the moving average to get the upper and lower bands.

Nothing fancy.

Let’s turn to providing the details in Python with an example.

Calculating Bollinger Bands in Python

First, we’ll start with data. In this case, let’s play with MCD.

ticker = "MCD"
start = '2015-01-01'
end = '2016-12-31'

# Get Data
yfObj = yf.Ticker(ticker)
data = yfObj.history(start=start, end=end)
data.drop(['Open', 'Volume', 'Dividends',
  'Stock Splits'], inplace=True, axis=1)

Now, we can implement our four steps in just a few lines of code and plot the results.

N = 20
m = 2
data['TP'] = data.apply(
  lambda x: np.mean(x[['High', 'Low', 'Close']]), axis=1)
data[f'SMA_{N}'] = data['TP'].rolling(N).mean()
data['STD'] = data['TP'].rolling(N).std(ddof=1)
data['UBB'] = data[f'SMA_{N}'] + m * data['STD']
data['LBB'] = data[f'SMA_{N}'] - m * data['STD']

# Plot the results
plt.figure(figsize=(15, 10))
plt.plot(data['Close'], label='Price')
plt.plot(data['UBB'], label='UBB')
plt.plot(data['LBB'], label='LBB')
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.title(f'Price and Bollinger Bands for {ticker}')
plt.legend()
plt.show()
bollinger-band-mcd-plot1.png

The Bollinger Bands create a smooth envelope around most of the price action. There are a few cases where the price breaks outside of the envelope, which may indicate trading signals. In fact, this is the most straightforward way to trade this signal — simply buy it when the price moves below the lower band or short it when it moves above. This provides a simple mean reversion strategy.

We do have the simple moving average of the TP (SMA(TP)) as well, which can be used like the centerline in an oscillator strategy like the RSI. We could close our position when the price reaches the SMA(TP), rather than wait for it to reach the other side of the Band.

Following the Trend with Bollinger Bands

Like many indicators, we can leverage the Bollinger Bands in a trend following strategy as well. Traders will often use two sets of Bands in conjunction with one another to identify trending price action. For example, we can add a 1𝜎 band and identify trends when the price is in between the 1𝜎 and 2𝜎 upper or lower bands. We’d do it like this:

m1 = 1
m2 = 2
data[f'SMA_{N}'] = data['TP'].rolling(N).mean()
data['STD'] = data['TP'].rolling(N).std(ddof=1)
data[f'UBB_{m1}'] = data[f'SMA_{N}'] + m1 * data['STD']
data[f'LBB_{m1}'] = data[f'SMA_{N}'] - m1 * data['STD']
data[f'UBB_{m2}'] = data[f'SMA_{N}'] + m2 * data['STD']
data[f'LBB_{m2}'] = data[f'SMA_{N}'] - m2 * data['STD']

And we can visualize our results with a nice plot:

colors = plt.rcParams['axes.prop_cycle'].by_key()['color']

plt.figure(figsize=(15, 10))
plt.plot(data['Close'], label='Price', zorder=100)
plt.plot(data[f'UBB_{m1}'], c=colors[2])
plt.plot(data[f'LBB_{m1}'], c=colors[2])
plt.fill_between(data.index, data[f'UBB_{m1}'], data[f'LBB_{m1}'],
  color=colors[2], label='Neutral Zone', alpha=0.3)
plt.plot(data[f'UBB_{m2}'], c=colors[1])
plt.plot(data[f'LBB_{m2}'], c=colors[4])
plt.fill_between(data.index, data[f'UBB_{m2}'], data[f'UBB_{m1}'],
  color=colors[1], label='Up-Trend', alpha=0.3)
plt.fill_between(data.index, data[f'LBB_{m2}'], data[f'LBB_{m1}'],
  color=colors[4], label='Down-Trend', alpha=0.3)
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.title(f'Price and Bollinger Bands for {ticker}')
plt.legend()
plt.show()
bollinger-band-mcd-plot2.png

You can see here that the price frequently stays within the neutral zone, but then breaks up or down and seems to keep a streak going. In the plot below, we zoom in on a quick price rise that exhibits this characteristic from mid-2015 to 2016.

plt.figure(figsize=(15, 10))
plt.plot(data['Close'], label='Price', marker='o', zorder=100)
plt.plot(data[f'UBB_{m1}'], c=colors[2])
plt.plot(data[f'LBB_{m1}'], c=colors[2])
plt.fill_between(data.index, data[f'UBB_{m1}'], data[f'LBB_{m1}'],
  color=colors[2], label='Neutral Zone', alpha=0.3)
plt.plot(data[f'UBB_{m2}'], c=colors[1])
plt.plot(data[f'LBB_{m2}'], c=colors[4])
plt.fill_between(data.index, data[f'UBB_{m2}'], data[f'UBB_{m1}'],
  color=colors[1], label='Up-Trend', alpha=0.3)
plt.fill_between(data.index, data[f'LBB_{m2}'], data[f'LBB_{m1}'],
  color=colors[4], label='Down-Trend', alpha=0.3)
plt.xlabel('Date')
plt.ylabel('Price ($)')
plt.title(f'Price and Bollinger Bands for {ticker}')
plt.xlim([pd.to_datetime('2015-08-01'), 
  pd.to_datetime('2016-05-01')])
plt.legend()
plt.show()
bollinger-band-mcd-plot3.png

In this plot, we added the individual data points to more clearly see the precise closing prices from day to day. Zooming in, you can see that breaks above the 1𝜎 upper band seem to be followed by a streak of days above, indicating times you’d be long, riding the trend as it increases. While less frequent and shorter, days below the neutral zone appear to persist, with the entry point often higher than the exit, indicating potentially profitable short opportunities.

Tightening our (Bollinger) Belt

You’ll notice that the width of the bands does not remain constant over time, they expand and contract with volatility. We can use this expansion and contraction to derive another, Bollinger Band-based indicator called Band Width.

This is calculated by subtracting the lower band from the upper and dividing by the SMA(TP).

$$BW_N = \frac{UBB_N - LBB_N}{SMA^{TP}_N}$$

We can implement that on our data with the following code:

data['BW'] = (data['UBB'] - data['LBB']) / data[f'SMA_{N}']

fig, ax = plt.subplots(2, figsize=(15, 10), sharex=True)

ax[0].plot(data['Close'], label='Price')
ax[0].plot(data['UBB'], c=colors[1])
ax[0].plot(data['LBB'], c=colors[1])
ax[0].fill_between(data.index, data['UBB'], data['LBB'],
  color=colors[1], alpha=0.3)
ax[0].set_title(f'Price and Bollinger Bands for {ticker}')
ax[0].set_ylabel('Price ($)')

ax[1].plot(data['BW'])
ax[1].set_ylabel('Band Width')
ax[1].set_xlabel('Date')
ax[1].set_title(f'Bollinger Band Width for {ticker}')

plt.tight_layout()
plt.show()
bollinger-band-width-mcd-plot1.png

Typically this value is going to stay fairly low, e.g. almost always less than 1. However, there are times that this can really blow up, such as in the case of GameStop during this year’s epic short-squeeze as shown below.

ticker = 'GME'
yfObj = yf.Ticker(ticker)
data = yfObj.history(start='2020-08-01', end='2021-07-01')

N = 20
m = 2
data['TP'] = data.apply(
  lambda x: np.mean(x[['High', 'Low', 'Close']]), axis=1)
data[f'SMA_{N}'] = data['TP'].rolling(N).mean()
data['STD'] = data['TP'].rolling(N).std(ddof=1)
data['UBB'] = data[f'SMA_{N}'] + m * data['STD']
data['LBB'] = data[f'SMA_{N}'] - m * data['STD']
data['BW'] = (data['UBB'] - data['LBB']) / data[f'SMA_{N}']

fig, ax = plt.subplots(2, figsize=(15, 10), sharex=True)

ax[0].plot(data['Close'], label='Price')
ax[0].plot(data['UBB'], c=colors[1])
ax[0].plot(data['LBB'], c=colors[1])
ax[0].fill_between(data.index, data['UBB'], data['LBB'],
  color=colors[1], alpha=0.3)
ax[0].annotate('GME Squeeze Begins',
  xy=(pd.to_datetime('2021-01-10'), 50),
  xytext=(pd.to_datetime('2020-12-01'), 100),
  arrowprops=dict(arrowstyle='->'))
ax[0].set_title(f'Price and Bollinger Bands for {ticker}')
ax[0].set_ylabel('Price ($)')

ax[1].plot(data['BW'])
ax[1].annotate('Band Width Blows Up',
  xy=(pd.to_datetime('2021-01-10'), 1),
  xytext=(pd.to_datetime('2020-12-01'), 2),
  arrowprops=dict(arrowstyle='->'))
ax[1].set_title('Bollinger Band Width for GME')
ax[1].set_xlabel('Date')
ax[1].set_ylabel('Band Width')

plt.tight_layout()
plt.show()
bollinger-band-width-gme-plot1.png

Bollinger himself states that lows in this Band Width are often followed by breakouts. To test this, we can combine this indicator with a directional indicator or some other confirmation signal such as an oscillator or EMA to see if we can hit profitable trades.

Test, and be Profitable!

Of course, we’re just giving a verbal description of how these strategies could work with some illustrations. You’d have to run a proper backtest in order to see if there’s a profitable signal to be traded or not.

We’re building complete backtest systems that will allow you to test your strategies, gauge your risk, and deploy in the markets — all with no code.

Try our free demo here and let us know what you think!