feat(chart): S&P 500 P/E and dividend yield historical
This commit is contained in:
84
src/charts/pe_dividend.py
Normal file
84
src/charts/pe_dividend.py
Normal 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()
|
||||
Reference in New Issue
Block a user