Bollinger Bands Indicator Chart — lets-plot

A Bollinger Bands chart displays price data with a volatility envelope consisting of three lines: a middle band (simple moving average), an upper band (SMA plus 2 standard deviations), and a lower band (SMA minus 2 standard deviations). This technical indicator helps traders identify overbought/oversold conditions, volatility patterns, and potential price breakouts by showing how prices relate to their recent statistical range.

Bollinger Bands Indicator Chart rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
indicator-bollinger: Bollinger Bands Indicator Chart
Library: letsplot 4.9.0 | Python 3.13.13
Quality: 92/100 | Updated: 2026-05-17
"""
# ruff: noqa: F403, F405
"""anyplot.ai
indicator-bollinger: Bollinger Bands Indicator Chart
Library: letsplot | Python 3.13
Quality: pending | Updated: 2026-05-17
"""

import os

import numpy as np
import pandas as pd
from lets_plot import *
from lets_plot.export import ggsave


LetsPlot.setup_html()

# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

BRAND = "#009E73"  # Okabe-Ito position 1
BAND_COLOR = "#4467A3"  # Okabe-Ito position 3

# Data - Generate synthetic stock price data with Bollinger Bands
np.random.seed(42)
n_periods = 120

# Generate price data with trend and volatility
dates = pd.date_range(start="2024-01-01", periods=n_periods, freq="B")
returns = np.random.normal(0.0005, 0.018, n_periods)
# Add some volatility clustering
volatility_multiplier = np.where((np.arange(n_periods) > 40) & (np.arange(n_periods) < 70), 1.8, 1.0)
returns = returns * volatility_multiplier
close = 100 * np.exp(np.cumsum(returns))

# Calculate Bollinger Bands (20-period SMA with 2 standard deviations)
window = 20
sma = pd.Series(close).rolling(window=window).mean().values
std = pd.Series(close).rolling(window=window).std().values
upper_band = sma + 2 * std
lower_band = sma - 2 * std

df = pd.DataFrame({"date": dates, "close": close, "sma": sma, "upper_band": upper_band, "lower_band": lower_band})

# Remove NaN values from rolling calculations
df = df.dropna().reset_index(drop=True)

# Create plot with legend using color aesthetic
plot = (
    ggplot(df)
    # Bollinger Bands fill area (between upper and lower bands)
    + geom_ribbon(
        aes(x="date", ymin="lower_band", ymax="upper_band", fill="Bollinger Bands"), fill=BAND_COLOR, alpha=0.2
    )
    # Lower band line
    + geom_line(aes(x="date", y="lower_band", color="Bollinger Bands"), color=BAND_COLOR, size=1.0, alpha=0.7)
    # Upper band line
    + geom_line(aes(x="date", y="upper_band", color="Bollinger Bands"), color=BAND_COLOR, size=1.0, alpha=0.7)
    # Middle band (SMA) - dashed line
    + geom_line(aes(x="date", y="sma", color="20-SMA"), color=BAND_COLOR, size=1.2, linetype="dashed", alpha=0.9)
    # Close price line - prominent
    + geom_line(aes(x="date", y="close", color="Close Price"), color=BRAND, size=1.8)
    # Labels
    + labs(title="indicator-bollinger · letsplot · anyplot.ai", x="Date", y="Price (USD)", color="", fill="")
    # Theme and styling
    + theme_minimal()
    + theme(
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        panel_grid_major=element_line(color=INK, size=0.15),
        panel_grid_minor=element_line(color=INK, size=0.1),
        axis_title=element_text(size=20, color=INK),
        axis_text=element_text(size=16, color=INK_SOFT),
        axis_line=element_line(color=INK_SOFT, size=0.5),
        plot_title=element_text(size=24, color=INK),
    )
    + ggsize(1600, 900)
)

# Save PNG (scale=3 gives 4800x2700)
ggsave(plot, f"plot-{THEME}.png", path=".", scale=3)

# Save HTML for interactivity
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Bollinger Bands Indicator Chart on anyplot.ai.

Other implementations