Chris Parmer — home

Drawdown (underwater) chart for SPY since 2000

Risk

Example from the compendium of canonical charts

Risk — Drawdown (underwater) chart for SPY since 2000

Python Code

"""Risk — Drawdown (underwater) chart for SPY since 2000."""


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


def fetch_spy():
    try:
        import yfinance as yf
        df = yf.download("SPY", start="2000-01-01", auto_adjust=True, progress=False)
        if df.empty:
            raise ValueError("empty data")
        if isinstance(df.columns, pd.MultiIndex):
            df.columns = df.columns.get_level_values(0)
        return df["Close"].dropna()
    except Exception as e:
        print(f"  yfinance failed ({e}), using synthetic data")
        return None


def make_synthetic():
    """Synthetic SPY-like price series 2000-present with GFC and COVID drawdowns."""
    rng = np.random.default_rng(42)
    dates = pd.bdate_range("2000-01-03", "2026-06-01")
    n = len(dates)
    log_returns = rng.normal(0.00035, 0.012, n)

    # Inject GFC crash ~2007-10 to 2009-03: ~-55%
    gfc_start = int(np.searchsorted(dates, pd.Timestamp("2007-10-09")))
    gfc_end = int(np.searchsorted(dates, pd.Timestamp("2009-03-09")))
    gfc_len = gfc_end - gfc_start
    log_returns[gfc_start:gfc_end] = np.linspace(-0.003, -0.001, gfc_len) + rng.normal(0, 0.012, gfc_len)

    # Inject COVID crash ~2020-02-19 to 2020-03-23: ~-34%
    cov_start = int(np.searchsorted(dates, pd.Timestamp("2020-02-19")))
    cov_end = int(np.searchsorted(dates, pd.Timestamp("2020-03-23")))
    cov_len = cov_end - cov_start
    log_returns[cov_start:cov_end] = np.linspace(-0.004, -0.008, cov_len) + rng.normal(0, 0.015, cov_len)

    prices = 100.0 * np.exp(np.cumsum(log_returns))
    return pd.Series(prices, index=dates)


def build_fig(close):
    rolling_max = close.cummax()
    drawdown = (close / rolling_max - 1) * 100  # in percent

    # Find significant drawdown bottoms
    gfc_window = drawdown["2008-01-01":"2009-12-31"]
    covid_window = drawdown["2020-01-01":"2020-12-31"]

    gfc_date = gfc_window.idxmin() if len(gfc_window) > 0 else None
    gfc_val = float(gfc_window.min()) if len(gfc_window) > 0 else None
    covid_date = covid_window.idxmin() if len(covid_window) > 0 else None
    covid_val = float(covid_window.min()) if len(covid_window) > 0 else None

    fig = go.Figure()

    fig.add_trace(go.Scatter(
        x=drawdown.index,
        y=drawdown.values,
        mode="lines",
        fill="tozeroy",
        line=dict(color="rgba(200,0,0,0.8)", width=1),
        fillcolor="rgba(255,0,0,0.25)",
        name="Drawdown",
        hovertemplate="%{x|%Y-%m-%d}: %{y:.1f}%<extra></extra>",
    ))

    # Annotate GFC
    if gfc_date is not None and gfc_val is not None:
        fig.add_annotation(
            x=gfc_date, y=gfc_val,
            text=f"GFC bottom<br>{pd.Timestamp(gfc_date).strftime('%b %Y')}<br>{gfc_val:.1f}%",
            showarrow=True,
            arrowhead=2,
            arrowcolor="rgba(200,0,0,0.8)",
            ax=60, ay=60,
            font=dict(size=11),
            bgcolor="rgba(255,255,255,0.8)",
            bordercolor="rgba(200,0,0,0.5)",
        )

    # Annotate COVID
    if covid_date is not None and covid_val is not None:
        fig.add_annotation(
            x=covid_date, y=covid_val,
            text=f"COVID bottom<br>{pd.Timestamp(covid_date).strftime('%b %Y')}<br>{covid_val:.1f}%",
            showarrow=True,
            arrowhead=2,
            arrowcolor="rgba(200,0,0,0.8)",
            ax=60, ay=40,
            font=dict(size=11),
            bgcolor="rgba(255,255,255,0.8)",
            bordercolor="rgba(200,0,0,0.5)",
        )

    # Reference line at 0
    fig.add_hline(y=0, line_color="rgba(0,0,0,0.3)", line_width=1)

    fig.update_layout(
        
        yaxis_title="Drawdown (%)",
        yaxis=dict(tickformat=".0f", ticksuffix="%"),
        showlegend=False,
        margin=dict(t=40, b=60, l=70, r=20),
    )
    return fig


def generate():
    close = fetch_spy()
    if close is None or len(close) < 100:
        close = make_synthetic()
        print("  using synthetic SPY drawdown data")
    else:
        print(f"  fetched {len(close)} days from yfinance")

    fig = build_fig(close)
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly