 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, pseudo-code, 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 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 \sigma) 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 m \sigma 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^{TP}_t = \frac{1}{N} \sum_{t=1}^N TP_{t-N} \sigma^{TP}_t = \sqrt{\frac{\sum (TP_t - \bar{TP})^2}{N-1}} UBB_t = SMA^{TP}_t + m \sigma^{TP}_t LBB_t = SMA^{TP}_t -m \sigma^{TP}_t

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.

table = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
df = table
syms = df['Symbol']
# Sample symbols
ticker = np.random.choice(syms.values)
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.

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']


Plotting 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()  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 \sigma band and identify trends when the price is in between the 1 \sigma and 2 \sigma 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'] 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) plt.plot(data[f'LBB_{m1}'], c=colors) plt.fill_between(data.index, data[f'UBB_{m1}'], data[f'LBB_{m1}'], color=colors, label='Neutral Zone', alpha=0.3) plt.plot(data[f'UBB_{m2}'], c=colors) plt.plot(data[f'LBB_{m2}'], c=colors) plt.fill_between(data.index, data[f'UBB_{m2}'], data[f'UBB_{m1}'], color=colors, label='Up-Trend', alpha=0.3) plt.fill_between(data.index, data[f'LBB_{m2}'], data[f'LBB_{m1}'], color=colors, 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()


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)
plt.plot(data[f'LBB_{m1}'], c=colors)
plt.fill_between(data.index, data[f'UBB_{m1}'], data[f'LBB_{m1}'],
color=colors, label='Neutral Zone', alpha=0.3)
plt.plot(data[f'UBB_{m2}'], c=colors)
plt.plot(data[f'LBB_{m2}'], c=colors)
plt.fill_between(data.index, data[f'UBB_{m2}'], data[f'UBB_{m1}'],
color=colors, label='Up-Trend', alpha=0.3)
plt.fill_between(data.index, data[f'LBB_{m2}'], data[f'LBB_{m1}'],
color=colors, 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()  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\sigma 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 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_N^{TP}} 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.plot(data['Close'], label='Price') ax.plot(data['UBB'], c=colors) ax.plot(data['LBB'], c=colors) ax.fill_between(data.index, data['UBB'], data['LBB'], color=colors, alpha=0.3) ax.set_title(f'Price and Bollinger Bands for {ticker}') ax.set_ylabel('Price ($)')

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

plt.tight_layout()
plt.show()


Typically this value is going to stay fairly low, e.g. less than 0.5 and almost always less than 1. 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.plot(data['Close'], label='Price')
ax.plot(data['UBB'], c=colors)
ax.plot(data['LBB'], c=colors)
ax.fill_between(data.index, data['UBB'], data['LBB'],
color=colors, alpha=0.3)
ax.annotate('GME Squeeze Begins',
xy=(pd.to_datetime('2021-01-10'), 50),
xytext=(pd.to_datetime('2020-12-01'), 100),
arrowprops=dict(arrowstyle='->'))
ax.set_title(f'Price and Bollinger Bands for {ticker}')
ax.set_ylabel('Price (\$)')

ax.plot(data['BW'])
ax.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.set_title('Bollinger Band Width for GME')
ax.set_xlabel('Date')
ax.set_ylabel('Band Width')
plt.tight_layout()
plt.show()


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 works 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. Sign up below to join the wait list and learn more!