feat(chart): composite bubble indicators dashboard 2x2
This commit is contained in:
101
src/charts/bubble_dashboard.py
Normal file
101
src/charts/bubble_dashboard.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Composite Bubble Indicators Dashboard — 2x2 grid"""
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
|
||||
# Patch matplotlib Path.__deepcopy__ to break Python 3.14 recursion loop
|
||||
# This is a known bug: https://github.com/matplotlib/matplotlib/issues/29280
|
||||
try:
|
||||
from matplotlib.path import Path
|
||||
_original_path_deepcopy = Path.__deepcopy__
|
||||
def _safe_path_deepcopy(self, memo):
|
||||
if id(self) in memo:
|
||||
return memo[id(self)]
|
||||
memo[id(self)] = self
|
||||
return self
|
||||
Path.__deepcopy__ = _safe_path_deepcopy
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from src.data.market_bubbles import shiller_cape, buffett_indicator, sp500_pe
|
||||
from src.utils.styling import (
|
||||
get_theme, EXPORT_DPI, BUBBLE_ZONE, WARNING_ZONE, NORMAL_ZONE,
|
||||
GRAY_DARK, BLACK, WHITE, FIGURE_SIZE_WIDE
|
||||
)
|
||||
|
||||
|
||||
def plot_bubble_dashboard() -> str:
|
||||
"""Generate 2x2 composite dashboard: CAPE, Buffett, P/E, AI Multiples."""
|
||||
plt.rcParams.update(get_theme())
|
||||
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
|
||||
|
||||
# Panel 1: CAPE (top-left)
|
||||
ax1 = axes[0, 0]
|
||||
years = [d["year"] for d in shiller_cape]
|
||||
values = [d["value"] for d in shiller_cape]
|
||||
ax1.axhspan(0, 20, alpha=0.15, color=NORMAL_ZONE)
|
||||
ax1.axhspan(20, 30, alpha=0.15, color=WARNING_ZONE)
|
||||
ax1.axhspan(30, 50, alpha=0.15, color=BUBBLE_ZONE)
|
||||
ax1.plot(years, values, color=GRAY_DARK, linewidth=1)
|
||||
ax1.axhline(y=17.39, color="#333", linestyle="--", linewidth=0.8)
|
||||
ax1.set_title("Shiller CAPE (1880–2026)", fontsize=13, fontweight="bold")
|
||||
ax1.set_ylabel("CAPE")
|
||||
ax1.grid(True, alpha=0.2)
|
||||
ax1.set_ylim(0, 50)
|
||||
|
||||
# Panel 2: Buffett Indicator (top-right)
|
||||
ax2 = axes[0, 1]
|
||||
b_years = [d["year"] for d in buffett_indicator]
|
||||
b_values = [d["value"] for d in buffett_indicator]
|
||||
ax2.axhspan(0, 100, alpha=0.15, color=NORMAL_ZONE)
|
||||
ax2.axhspan(100, 200, alpha=0.15, color=WARNING_ZONE)
|
||||
ax2.axhspan(200, 300, alpha=0.15, color=BUBBLE_ZONE)
|
||||
ax2.plot(b_years, b_values, color=GRAY_DARK, linewidth=1)
|
||||
ax2.axhline(y=200, color=BUBBLE_ZONE, linestyle="--", linewidth=1)
|
||||
ax2.text(2010, 205, "Danger: 200%", fontsize=9, color=BUBBLE_ZONE)
|
||||
ax2.set_title("Buffett Indicator (1975–2026)", fontsize=13, fontweight="bold")
|
||||
ax2.set_ylabel("Mkt Cap / GDP (%)")
|
||||
ax2.grid(True, alpha=0.2)
|
||||
|
||||
# Panel 3: P/E (bottom-left)
|
||||
ax3 = axes[1, 0]
|
||||
pe_years = [d["year"] for d in sp500_pe]
|
||||
pe_values = [d["value"] for d in sp500_pe]
|
||||
ax3.plot(pe_years, pe_values, color=GRAY_DARK, linewidth=1)
|
||||
ax3.axhline(y=18.2, color="#333", linestyle="--", linewidth=0.8)
|
||||
ax3.set_title("S&P 500 P/E Ratio (1950–2026)", fontsize=13, fontweight="bold")
|
||||
ax3.set_ylabel("P/E Ratio")
|
||||
ax3.set_xlabel("Year")
|
||||
ax3.grid(True, alpha=0.2)
|
||||
ax3.set_ylim(0, 75)
|
||||
|
||||
# Panel 4: AI Startup Multiples (bottom-right)
|
||||
ax4 = axes[1, 1]
|
||||
companies = ["OpenAI", "Anthropic", "Perplexity", "Mistral AI", "S&P Avg (ref)"]
|
||||
multiples = [31, 40, 45, 120, 18]
|
||||
colors_bar = [WARNING_ZONE, WARNING_ZONE, BUBBLE_ZONE, BUBBLE_ZONE, NORMAL_ZONE]
|
||||
bars = ax4.barh(companies, multiples, color=colors_bar, edgecolor="white", height=0.6)
|
||||
ax4.set_xlabel("Revenue Multiple (x)", fontsize=11)
|
||||
ax4.set_title("AI Startup Valuation Multiples", fontsize=13, fontweight="bold")
|
||||
ax4.grid(True, alpha=0.2, axis="x")
|
||||
# Add value labels
|
||||
for bar, val in zip(bars, multiples):
|
||||
ax4.text(val + 2, bar.get_y() + bar.get_height() / 2, f"{val}x",
|
||||
va="center", fontsize=10, fontweight="bold")
|
||||
|
||||
fig.suptitle("Market Bubble Indicators — June 2026", fontsize=18, fontweight="bold", y=0.98)
|
||||
fig.subplots_adjust(hspace=0.35, wspace=0.25, top=0.93, bottom=0.05)
|
||||
|
||||
fig.savefig("output/charts/04_bubble_dashboard.png", dpi=EXPORT_DPI,
|
||||
facecolor=fig.get_facecolor(), edgecolor="none")
|
||||
plt.close(fig)
|
||||
return "output/charts/04_bubble_dashboard.png"
|
||||
|
||||
|
||||
def main():
|
||||
path = plot_bubble_dashboard()
|
||||
print(f"Dashboard saved: {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user