Chris Parmer — home

Technical Analysis

Price panel with Bollinger Bands, RSI, and MACD for AAPL

Example from the compendium of canonical charts

Technical Analysis — Price panel with Bollinger Bands, RSI, and MACD for AAPL

Python Code

"""Technical Analysis — Price panel with Bollinger Bands, RSI, and MACD for AAPL."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings("ignore")


def fetch_aapl():
    try:
        import yfinance as yf
        df = yf.download("AAPL", period="1y", auto_adjust=True, progress=False)
        if df.empty or len(df) < 30:
            raise ValueError("insufficient data")
        # Flatten MultiIndex columns if present
        if isinstance(df.columns, pd.MultiIndex):
            df.columns = df.columns.get_level_values(0)
        df.index = pd.to_datetime(df.index)
        return df
    except Exception as e:
        print(f"  yfinance failed ({e}), using synthetic data")
        return None


def make_synthetic():
    """Synthetic OHLCV data resembling AAPL 1-year history."""
    rng = np.random.default_rng(42)
    dates = pd.bdate_range("2025-06-01", periods=252)
    close = 180.0
    closes = [close]
    for _ in range(251):
        close = close * np.exp(rng.normal(0.0003, 0.015))
        closes.append(close)
    closes = np.array(closes)
    highs = closes * (1 + np.abs(rng.normal(0, 0.008, 252)))
    lows = closes * (1 - np.abs(rng.normal(0, 0.008, 252)))
    opens = closes * (1 + rng.normal(0, 0.005, 252))
    volume = rng.integers(50_000_000, 120_000_000, 252)
    df = pd.DataFrame({"Open": opens, "High": highs, "Low": lows,
                        "Close": closes, "Volume": volume}, index=dates)
    return df


def compute_indicators(df):
    close = df["Close"]
    # Bollinger Bands (20-day)
    sma20 = close.rolling(20).mean()
    std20 = close.rolling(20).std()
    bb_upper = sma20 + 2 * std20
    bb_lower = sma20 - 2 * std20

    # RSI(14)
    delta = close.diff()
    gain = delta.clip(lower=0).rolling(14).mean()
    loss = (-delta.clip(upper=0)).rolling(14).mean()
    rs = gain / loss.replace(0, np.nan)
    rsi = 100 - 100 / (1 + rs)

    # MACD(12,26,9)
    ema12 = close.ewm(span=12, adjust=False).mean()
    ema26 = close.ewm(span=26, adjust=False).mean()
    macd_line = ema12 - ema26
    signal_line = macd_line.ewm(span=9, adjust=False).mean()
    histogram = macd_line - signal_line

    return sma20, bb_upper, bb_lower, rsi, macd_line, signal_line, histogram


def build_fig(df):
    sma20, bb_upper, bb_lower, rsi, macd_line, signal_line, histogram = compute_indicators(df)
    dates = df.index

    fig = make_subplots(
        rows=3, cols=1,
        shared_xaxes=True,
        row_heights=[0.60, 0.20, 0.20],
        vertical_spacing=0.04,
        subplot_titles=("Price & Bollinger Bands", "RSI (14)", "MACD (12,26,9)"),
    )

    # Row 1: Candlestick + Bollinger
    fig.add_trace(go.Candlestick(
        x=dates, open=df["Open"], high=df["High"],
        low=df["Low"], close=df["Close"],
        name="AAPL",
        increasing_line_color=GREEN,
        decreasing_line_color=PINK,
        increasing_fillcolor=GREEN,
        decreasing_fillcolor=PINK,
        line_width=1,
    ), row=1, col=1)

    fig.add_trace(go.Scatter(
        x=dates, y=sma20, name="SMA 20",
        line=dict(color=TEAL, width=1.5, dash="dot"),
    ), row=1, col=1)
    fig.add_trace(go.Scatter(
        x=dates, y=bb_upper, name="BB Upper",
        line=dict(color=TEAL, width=1, dash="dash"),
    ), row=1, col=1)
    fig.add_trace(go.Scatter(
        x=dates, y=bb_lower, name="BB Lower",
        line=dict(color=TEAL, width=1, dash="dash"),
        fill="tonexty",
        fillcolor="rgba(82,179,208,0.08)",
    ), row=1, col=1)

    # Row 2: RSI
    fig.add_trace(go.Scatter(
        x=dates, y=rsi, name="RSI",
        line=dict(color=VIOLET, width=1.5),
        showlegend=True,
    ), row=2, col=1)
    fig.add_hline(y=70, row=2, col=1, line_color=PINK, line_dash="dot", line_width=1,
                  annotation_text="70", annotation_font_color=PINK)
    fig.add_hline(y=30, row=2, col=1, line_color=GREEN, line_dash="dot", line_width=1,
                  annotation_text="30", annotation_font_color=GREEN)

    # Row 3: MACD
    hist_colors = [GREEN if v >= 0 else PINK for v in histogram.fillna(0)]
    fig.add_trace(go.Bar(
        x=dates, y=histogram,
        name="Histogram",
        marker_color=hist_colors,
        opacity=0.6,
    ), row=3, col=1)
    fig.add_trace(go.Scatter(
        x=dates, y=macd_line, name="MACD",
        line=dict(color=VIOLET, width=1.5),
    ), row=3, col=1)
    fig.add_trace(go.Scatter(
        x=dates, y=signal_line, name="Signal",
        line=dict(color=TEAL, width=1.5, dash="dash"),
    ), row=3, col=1)

    fig.update_layout(
        xaxis_rangeslider_visible=False,
        
        yaxis_title="Price (USD)",
        yaxis2_title="RSI",
        yaxis3_title="MACD",
        legend=dict(orientation="h", y=1.02, x=0),
        margin=dict(t=60, b=40, l=60, r=20),
    )
    # Fix RSI axis range
    fig.update_yaxes(range=[0, 100], row=2, col=1)

    return fig


def generate():
    df = fetch_aapl()
    if df is None:
        df = make_synthetic()
        print("  using synthetic AAPL data")
    else:
        print(f"  fetched {len(df)} trading days from yfinance")

    fig = build_fig(df)
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly