Chris Parmer — home

Callan quilt / periodic table of returns

Asset Allocation

Example from the compendium of canonical charts

Asset Allocation — Callan quilt / periodic table of returns

Python Code

"""Asset Allocation — Callan quilt / periodic table of returns."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go
import yfinance as yf
import warnings

warnings.filterwarnings("ignore")

TICKERS = ["SPY", "AGG", "GLD", "VNQ", "EFA", "EEM", "IWM"]
LABELS = {"SPY": "US Large Cap", "AGG": "US Bonds", "GLD": "Gold",
          "VNQ": "REITs", "EFA": "Intl Dev", "EEM": "Emerging Mkts", "IWM": "US Small Cap"}


def generate():
    print("downloading 15-year annual closes from yfinance …")
    raw = yf.download(TICKERS, period="15y", auto_adjust=True, progress=False)["Close"]
    annual = raw.resample("A").last().pct_change().dropna()

    years = [str(y.year) for y in annual.index]
    tickers = annual.columns.tolist()

    # Build rank matrix: for each year, sort tickers by return (best=rank 0=top)
    n_years = len(years)
    n_assets = len(tickers)

    # Color each cell by asset class, not by return value
    ASSET_COLORS = {
        "SPY": "#845EEE",  # violet — US Large Cap
        "AGG": "#52B3D0",  # teal   — US Bonds
        "GLD": "#E9A23B",  # orange — Gold
        "VNQ": "#55B685",  # green  — REITs
        "EFA": "#DA5597",  # pink   — Intl Dev
        "EEM": "#4A90D9",  # blue   — Emerging Mkts
        "IWM": "#A06CCC",  # purple — US Small Cap
    }

    z_idx = np.full((n_assets, n_years), 0.0)  # ticker index for colorscale
    text_vals = [[""] * n_years for _ in range(n_assets)]
    hover_text = [[""] * n_years for _ in range(n_assets)]
    cell_colors = [[None] * n_years for _ in range(n_assets)]

    ticker_to_idx = {t: i for i, t in enumerate(tickers)}

    for j, yr in enumerate(annual.index):
        row = annual.loc[yr]
        ranked = row.sort_values(ascending=False)
        for rank, ticker in enumerate(ranked.index):
            ret = ranked[ticker]
            label = LABELS.get(ticker, ticker)
            text_vals[rank][j] = f"<b>{label}</b><br>{ret:+.1%}"
            hover_text[rank][j] = f"{label}: {ret:+.1%}"
            cell_colors[rank][j] = ASSET_COLORS.get(ticker, "#cccccc")

    # Build a figure using rectangles (one per cell) for full color control
    fig = go.Figure()

    cell_w = 0.9 / n_years if n_years > 0 else 0.1
    cell_h = 0.9 / n_assets if n_assets > 0 else 0.1

    for j, yr in enumerate(years):
        row = annual.loc[annual.index[j]]
        ranked = row.sort_values(ascending=False)
        for rank, ticker in enumerate(ranked.index):
            ret = ranked[ticker]
            label = LABELS.get(ticker, ticker)
            color = ASSET_COLORS.get(ticker, "#cccccc")
            fig.add_shape(
                type="rect",
                x0=j - 0.46, x1=j + 0.46,
                y0=rank - 0.46, y1=rank + 0.46,
                fillcolor=color,
                line_width=1,
                line_color="white",
            )
            fig.add_annotation(
                x=j, y=rank,
                text=f"<b>{ticker}</b><br>{ret:+.1%}",
                showarrow=False,
                font=dict(size=14, color="white"),
                xanchor="center", yanchor="middle",
            )

    # Add a legend via dummy invisible scatter traces
    for ticker, color in ASSET_COLORS.items():
        if ticker in tickers:
            fig.add_trace(go.Scatter(
                x=[None], y=[None], mode="markers",
                marker=dict(size=10, color=color, symbol="square"),
                name=LABELS.get(ticker, ticker),
                showlegend=True,
            ))

    fig.update_layout(
        xaxis=dict(
            title="Year", side="top",
            tickvals=list(range(n_years)),
            ticktext=years,
            showgrid=False,
        ),
        yaxis=dict(
            title="Rank (1 = Best)",
            tickvals=list(range(n_assets)),
            ticktext=[f"Rank {i+1}" for i in range(n_assets)],
            autorange="reversed",
            showgrid=False,
        ),
        legend=dict(orientation="h", y=-0.08, x=0),
        margin=dict(t=80, b=80, l=80, r=40),
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly