feat(chart): S&P 500 P/E and dividend yield historical

This commit is contained in:
Orchestrator
2026-06-04 17:22:16 -05:00
parent b21f8c18f1
commit 0ba84225ca

84
src/charts/pe_dividend.py Normal file
View File

@@ -0,0 +1,84 @@
"""S&P 500 P/E and Dividend Yield Charts"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from src.data.market_bubbles import sp500_pe, sp500_dividend_yield
from src.utils.styling import get_theme, EXPORT_DPI, BUBBLE_ZONE, WARNING_ZONE, NORMAL_ZONE, GRAY_DARK
from src.utils.export import save_chart_tight
def plot_pe_dividend() -> str:
"""Generate 2-panel S&P 500 P/E Ratio and Dividend Yield chart."""
plt.rcParams.update(get_theme())
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# ── Panel 1: P/E ───────────────────────────────────────────────────────
years_pe = [d["year"] for d in sp500_pe]
values_pe = [d["value"] for d in sp500_pe]
ax1.plot(years_pe, values_pe, color=GRAY_DARK, linewidth=1.5)
ax1.axhspan(0, 15, alpha=0.15, color=NORMAL_ZONE)
ax1.axhspan(15, 25, alpha=0.15, color=WARNING_ZONE)
ax1.axhspan(25, 80, alpha=0.15, color=BUBBLE_ZONE)
ax1.axhline(y=18.2, color="#333", linestyle="--", linewidth=1, alpha=0.7)
ax1.text(1960, 18.7, "Mean: 18.2", fontsize=10)
ax1.set_ylabel("P/E Ratio", fontsize=12)
ax1.set_ylim(0, 80)
pe_events = [
(1999, 32.92, "1999 Peak"),
(2009, 70.91, "2009 Anomaly"),
(2021, 35.96, "2021"),
(2026, 29.60, "2026"),
]
for year, val, label in pe_events:
ax1.annotate(label, xy=(year, val), xytext=(year + 3, val + 3),
arrowprops=dict(arrowstyle="->", color="gray", lw=0.8),
fontsize=8)
# ── Panel 2: Dividend Yield ────────────────────────────────────────────
years_dy = [d["year"] for d in sp500_dividend_yield]
values_dy = [d["value"] for d in sp500_dividend_yield]
ax2.plot(years_dy, values_dy, color="#e74c3c", linewidth=1.5)
ax2.axhline(y=3.2, color="#333", linestyle="--", linewidth=1, alpha=0.7)
ax2.text(1960, 3.5, "Mean: 3.2%", fontsize=10)
ax2.set_ylabel("Dividend Yield (%)", fontsize=12)
ax2.set_xlabel("Year", fontsize=12)
ax2.set_ylim(0, 8)
dy_events = [
(1950, 7.44, "1950: 7.44%"),
(2000, 1.22, "2000: 1.22%"),
(2026, 1.04, "2026: 1.04%"),
]
for year, val, label in dy_events:
ax2.annotate(label, xy=(year, val), xytext=(year + 3, val + 0.5),
arrowprops=dict(arrowstyle="->", color="gray", lw=0.8),
fontsize=8)
fig.suptitle(
"S&P 500 Valuation Metrics — P/E Ratio and Dividend Yield",
fontsize=16, fontweight="bold",
)
for ax in (ax1, ax2):
ax.grid(True, alpha=0.3)
# Save — avoid tight_layout() and bbox_inches="tight" to bypass
# Python 3.14 + matplotlib deepcopy RecursionError
import os
output_dir = "output/charts"
os.makedirs(output_dir, exist_ok=True)
path = os.path.join(output_dir, "03_pe_dividend.png")
fig.subplots_adjust(top=0.90, bottom=0.06, left=0.08, right=0.95, hspace=0.28)
plt.savefig(path, dpi=EXPORT_DPI, facecolor=fig.get_facecolor(), edgecolor="none")
plt.close(fig)
return path
def main():
path = plot_pe_dividend()
print(f"Chart saved: {path}")
if __name__ == "__main__":
main()