Draw a moving average with a band two standard deviations above and below it, and measure where the close sits inside that band.
Bollinger bands are three lines. The middle one is a moving average of the close. The other two sit a number of standard deviations above and below it, with the standard deviation measured over the same window as the average. Two standard deviations and a 20 day window are the usual settings.
In this lesson I build the three lines on eight prices I can check by hand, then on AAPL with a 20 day window, and I measure where each close sits inside its band.
Step 1. Three lines on eight prices
rolling(5).mean() gives the middle line and rolling(5).std(ddof=0) gives the standard deviation over the same five closes. Both are NaN until five closes exist.
import pandas as pdimport numpy as npclose = pd.Series([100.0, 102.0, 101.0, 104.0, 103.0, 107.0, 106.0, 110.0])mid = close.rolling(5).mean()sd = close.rolling(5).std(ddof=0)print(pd.DataFrame({"close": close, "mid": mid, "sd": sd}))# -> close mid sd# -> 0 100.0 NaN NaN# -> 1 102.0 NaN NaN# -> 2 101.0 NaN NaN# -> 3 104.0 NaN NaN# -> 4 103.0 102.0 1.414214# -> 5 107.0 103.4 2.059126# -> 6 106.0 104.2 2.135416# -> 7 110.0 106.0 2.449490
close mid sd
0 100.0 NaN NaN
1 102.0 NaN NaN
2 101.0 NaN NaN
3 104.0 NaN NaN
4 103.0 102.0 1.414214
5 107.0 103.4 2.059126
6 106.0 104.2 2.135416
7 110.0 106.0 2.449490
The bands are the middle line plus and minus two of those standard deviations.
upper = mid +2* sdlower = mid -2* sdbands = pd.DataFrame({"lower": lower, "mid": mid, "upper": upper})print(bands.round(4))# -> lower mid upper# -> 0 NaN NaN NaN# -> 1 NaN NaN NaN# -> 2 NaN NaN NaN# -> 3 NaN NaN NaN# -> 4 99.1716 102.0 104.8284# -> 5 99.2817 103.4 107.5183# -> 6 99.9292 104.2 108.4708# -> 7 101.1010 106.0 110.8990
lower mid upper
0 NaN NaN NaN
1 NaN NaN NaN
2 NaN NaN NaN
3 NaN NaN NaN
4 99.1716 102.0 104.8284
5 99.2817 103.4 107.5183
6 99.9292 104.2 108.4708
7 101.1010 106.0 110.8990
Row 4 is the first complete row, so it uses closes 0 to 4. Here is that row worked out from those five numbers.
ddof sets what the squared deviations are divided by: ddof=0 divides by the 5 closes in the window, ddof=1 divides by 4 and is what pandas uses if you say nothing. Bollinger bands are normally drawn with ddof=0, so I pass it every time.
print(round(close.iloc[0:5].std(), 4)) # -> 1.5811 <- pandas default, ddof=1print(round(close.iloc[0:5].std(ddof=0), 4)) # -> 1.4142 <- what I use
1.5811
1.4142
On a 20 day window the ddof=1 figure is 2.6% larger, which moves the bands out slightly but does not change their shape.
Step 3. Bands on AAPL
prices.csv sits next to this lesson and holds simulated data: six years of daily closes for four tickers. I take AAPL, put the dates on the index, and use the standard 20 day window.
%B rescales the close so that the lower band is 0 and the upper band is 1: (close - lower) / (upper - lower). It is 0.5 when the close is on the moving average, above 1 when the close is above the upper band, and below 0 when it is under the lower band.
AAPL closed outside its bands on 193 of 1547 days, 134 of them above and 59 below. The average %B is 0.55, and the extremes over the whole sample are -0.23 and 1.28.
That is the indicator. Turning it into a position is Lesson 29.
Your turn
Build 20 day Bollinger bands on MSFT from prices.csv and count the days its close finished outside them. Does MSFT sit outside more or less often than AAPL?