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."""
from pathlib import Path
# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]
def apply_theme(fig):
"""Light gallery theme: white background, Inter font, soft gridlines."""
fig.update_layout(
paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
font=dict(family=FONT, color=TEXT, size=12),
legend=dict(font=dict(color=TEXT)),
hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
)
fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
def fetch_csv(url, **kwargs):
import io
import pandas as pd
import requests
r = requests.get(url, timeout=60)
r.raise_for_status()
return pd.read_csv(io.StringIO(r.text), **kwargs)
def fetch_json(url):
import requests
r = requests.get(url, timeout=60)
r.raise_for_status()
return r.json()
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)
apply_theme(fig)
return fig
fig = generate()
fig.show()
Made with Plotly