What Are Bollinger Bands? Build Them from SMA and Standard Deviation with Python

AAPL candlestick chart with 20-period Bollinger Bands built from SMA and population standard deviation in Python

We already know both mathematical pieces needed for Bollinger Bands.

Simple Moving Average
→ center

Population Standard Deviation
→ dispersion

Bollinger Bands combine them.

Middle Band
=
SMA

Upper Band
=
SMA + k × standard deviation

Lower Band
=
SMA - k × standard deviation

This lesson is therefore about composition, not about memorizing another unrelated formula.

1. Start with the Middle Band

The traditional middle Bollinger Band is a simple moving average.

Middle Band
=
SMA(N)

With the common default:

N = 20

If you already understand SMA, the center line contains no new mathematics.

2. Add the Dispersion Block We Just Built

Our previous lesson built rolling population standard deviation.

Close prices
→ mean
→ deviations
→ squared deviations
→ variance
→ square root
→ standard deviation

Review: What Is Standard Deviation? Measure Price Dispersion with Python .

3. Build the Upper and Lower Bands

Upper Band
=
SMA + k × StdDev

Lower Band
=
SMA - k × StdDev

The traditional default is:

period = 20
k = 2

John Bollinger's official explanation emphasizes that these are defaults, not universal constants.

4. Work Through a Tiny Example

100, 102, 104, 106, 108

From the previous lesson:

Mean
=
104

Population StdDev
=
√8
≈
2.828427

With k = 2:

Upper Band
=
104 + 2 × 2.828427
≈
109.656854

Lower Band
=
104 - 2 × 2.828427
≈
98.343146

5. What Makes the Bands Expand?

larger price dispersion
→ larger StdDev
→ bands move farther from SMA

Band width adapts to recent price dispersion.

6. What Makes the Bands Contract?

prices cluster more tightly
→ smaller StdDev
→ bands move closer to SMA

7. The Middle Band and Band Width Answer Different Questions

Middle Band
→ Where is the smoothed price level?

Band distance
→ How dispersed are recent prices?

Bollinger Bands put those two properties on one chart.

8. A Touch of the Upper Band Is Not Automatically a Sell Signal

price touches Upper Band
does not automatically mean
sell

Bollinger's own rules explicitly warn against treating an upper-band tag as a sell signal by itself.

9. A Touch of the Lower Band Is Not Automatically a Buy Signal

price touches Lower Band
does not automatically mean
buy

A band touch is an observation. A trading rule requires additional definitions and testing.

10. Price Can Walk Along a Band

persistent upward movement
→ price may walk the Upper Band

persistent downward movement
→ price may walk the Lower Band

This is why the shortcut “upper = overbought, lower = oversold” can be misleading.

11. A Close Outside the Bands Is Not Automatically a Reversal

Bollinger's published rules also note that closes outside the bands are not automatically reversal signals.

measurement
≠
trading rule

12. Why Population Standard Deviation Matters

In Phase 3-22 we deliberately used:

variance
=
sum squared deviations
/
N

Bollinger's official explanation states that the traditional bands use the population calculation for standard deviation.

So the block we already built can be reused without changing its definition.

13. Build Bollinger Bands as One Higher-Level Function

window
   ↓
arithmetic_mean()
   ↓
Middle Band

window
   ↓
population_standard_deviation()
   ↓
StdDev

Middle ± k × StdDev
   ↓
Upper / Lower Bands

14. Why This Is a Good Example of Modular Indicator Design

known block
SMA

+

known block
Standard Deviation

=

new indicator
Bollinger Bands

More advanced does not always mean more mysterious.

15. What the AAPL Chart Shows

AAPL candlesticks

Middle Band
SMA 20

Upper Band
SMA + 2 StdDev

Lower Band
SMA - 2 StdDev

Rising candles use seagreen. Falling candles use firebrick.

16. Change the Multiplier Yourself

Start with:

standard_deviation_multiplier = 2.0

Then try:

1.0

and:

3.0

Ask:

How does band width change?

How often does price
move outside the bands?

17. Change the Period Yourself

Try:

band_period = 10

and:

band_period = 40
shorter period
→ faster center
→ faster-changing dispersion

longer period
→ slower center
→ slower-changing dispersion

18. Bollinger Bands Are Not a Complete Strategy

Bollinger Bands do not automatically define:

entry
exit
position size
stop
holding period

Those questions belong to Phase 4.

19. The Complete Python Program

This lesson introduces no new external package. It reuses FinanceDataReader, matplotlib, and Python's built-in math module.

phase3_bollinger_bands.py
from pathlib import Path
from datetime import date, timedelta
import math
import os

import FinanceDataReader as fdr
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

symbol = "AAPL"
recent_trading_days = 180
band_period = 20
standard_deviation_multiplier = 2.0

bullish_color = "seagreen"
bearish_color = "firebrick"
middle_band_color = "dimgray"
upper_band_color = "black"
lower_band_color = "black"

SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)

def arithmetic_mean(values):
    if len(values) == 0:
        raise ValueError("values must not be empty")
    return sum(float(v) for v in values) / len(values)

def population_standard_deviation(values):
    if len(values) == 0:
        raise ValueError("values must not be empty")
    mean_value = arithmetic_mean(values)
    variance = sum(
        (float(v) - mean_value) ** 2
        for v in values
    ) / len(values)
    return math.sqrt(variance)

def bollinger_bands(values, period, multiplier):
    middle = [None] * len(values)
    upper = [None] * len(values)
    lower = [None] * len(values)
    stds = [None] * len(values)

    if period <= 0:
        raise ValueError("period must be positive")
    if multiplier < 0:
        raise ValueError("multiplier must be non-negative")

    for i in range(period - 1, len(values)):
        window = values[i - period + 1 : i + 1]
        mean_value = arithmetic_mean(window)
        std_value = population_standard_deviation(window)

        middle[i] = mean_value
        upper[i] = mean_value + multiplier * std_value
        lower[i] = mean_value - multiplier * std_value
        stds[i] = std_value

    return middle, upper, lower, stds

def draw_candlesticks(ax, market_df, body_width=0.62):
    for x, (_, row) in enumerate(market_df.iterrows()):
        o = float(row["Open"])
        h = float(row["High"])
        l = float(row["Low"])
        c = float(row["Close"])

        color = bullish_color if c >= o else bearish_color

        ax.vlines(
            x, l, h,
            color=color,
            linewidth=1.0,
        )

        bottom = min(o, c)
        height = abs(c - o)

        if height == 0:
            height = max(h - l, 0.01) * 0.02

        ax.add_patch(
            Rectangle(
                (x - body_width / 2.0, bottom),
                body_width,
                height,
                facecolor=color,
                edgecolor=color,
                linewidth=0.8,
            )
        )

toy = [100.0, 102.0, 104.0, 106.0, 108.0]
m, u, l, s = bollinger_bands(
    toy,
    period=5,
    multiplier=2.0,
)

expected_mean = 104.0
expected_std = math.sqrt(8.0)
expected_upper = expected_mean + 2.0 * expected_std
expected_lower = expected_mean - 2.0 * expected_std

assert m[:4] == [None, None, None, None]
assert abs(m[4] - expected_mean) < 1e-12
assert abs(s[4] - expected_std) < 1e-12
assert abs(u[4] - expected_upper) < 1e-12
assert abs(l[4] - expected_lower) < 1e-12

print("Self-test")
print("=========")
print("Middle:", f"{m[4]:.6f}")
print("Population StdDev:", f"{s[4]:.6f}")
print("Upper:", f"{u[4]:.6f}")
print("Lower:", f"{l[4]:.6f}")
print("Self-test: PASS")
print()

today = date.today()
start_date = (today - timedelta(days=420)).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")

df = fdr.DataReader(symbol, start_date, end_date)
df = df.tail(recent_trading_days).copy()

close_values = [float(v) for v in df["Close"]]

middle, upper, lower, stds = bollinger_bands(
    close_values,
    period=band_period,
    multiplier=standard_deviation_multiplier,
)

df["Middle Band"] = middle
df["Upper Band"] = upper
df["Lower Band"] = lower
df["StdDev"] = stds

valid_df = df.dropna(
    subset=["Middle Band", "Upper Band", "Lower Band"]
)

latest = valid_df.iloc[-1]
latest_date = valid_df.index[-1]

print("Latest Bollinger Bands")
print("======================")
print("Symbol:", symbol)
print("Date:", latest_date.strftime("%Y-%m-%d"))
print("Close:", f'{latest["Close"]:.2f}')
print("Middle:", f'{latest["Middle Band"]:.2f}')
print("Upper:", f'{latest["Upper Band"]:.2f}')
print("Lower:", f'{latest["Lower Band"]:.2f}')
print("StdDev:", f'{latest["StdDev"]:.4f}')

plot_df = df.tail(100).copy()
x = list(range(len(plot_df)))

fig, ax = plt.subplots(figsize=(12, 8))
draw_candlesticks(ax, plot_df)

ax.plot(
    x,
    plot_df["Middle Band"],
    color=middle_band_color,
    linewidth=1.5,
    label=f"SMA {band_period}",
)

ax.plot(
    x,
    plot_df["Upper Band"],
    color=upper_band_color,
    linewidth=1.5,
    label="Upper Band",
)

ax.plot(
    x,
    plot_df["Lower Band"],
    color=lower_band_color,
    linewidth=1.5,
    label="Lower Band",
)

ax.fill_between(
    x,
    plot_df["Lower Band"].astype(float),
    plot_df["Upper Band"].astype(float),
    alpha=0.08,
)

ax.set_title(
    f"{symbol} — Bollinger Bands "
    f"({band_period}, {standard_deviation_multiplier:g})"
)
ax.set_ylabel("Price")
ax.grid(axis="y", alpha=0.20)
ax.legend()

step = max(1, len(plot_df) // 8)
positions = list(range(0, len(plot_df), step))
labels = [
    plot_df.index[i].strftime("%Y-%m-%d")
    for i in positions
]

ax.set_xticks(positions)
ax.set_xticklabels(labels, rotation=35, ha="right")

fig.subplots_adjust(
    left=0.09,
    right=0.98,
    top=0.93,
    bottom=0.15,
)

output_file = SCRIPT_DIR / "bollinger_bands_aapl.png"
fig.savefig(output_file, dpi=140)

print()
print("Chart saved:")
print(output_file)

plt.show()
plt.close(fig)

20. Run the Program

python phase3_bollinger_bands.py

Confirm:

Self-test: PASS

The chart is saved as:

bollinger_bands_aapl.png

Check Your Understanding

  • The Middle Band is a simple moving average in the traditional construction.
  • The Upper and Lower Bands are placed a multiple of population standard deviation above and below the middle band.
  • The common 20-period and ±2-standard-deviation settings are defaults, not universal laws.
  • Greater recent price dispersion produces wider bands.
  • A tag of the upper band is not automatically a sell signal.
  • A tag of the lower band is not automatically a buy signal.
  • Price can move along a band during a persistent trend.
  • Bollinger Bands reuse the SMA and population standard-deviation blocks already learned.

What You Just Learned

Close prices
     ↓
same rolling window
   ↙             ↘
SMA             StdDev
 ↓                 ↓
center          dispersion
   ↘             ↙
      combine
         ↓
Middle / Upper / Lower Bands
         ↓
dynamic price envelope

Bollinger Bands combine a moving center with a dispersion measure. The bands adapt because standard deviation changes through time.

Where Do We Go Next?

Where is today's Close
inside the recent
High-Low range?

That leads to the Stochastic Oscillator.

Sources