Drawdown (underwater) chart for SPY since 2000
Risk
Example from the compendium of canonical charts
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