#!/usr/bin/env python3

# Finanzdaten_Ledoit_Wolf.py
"""
Kapitel Finanzdaten: Automatisierte Finanzdaten-Pipeline und Kovarianz-Shrinkage.

Eigenschaften:
  * Spaltenreihenfolge wird erzwungen und per assert geprueft
  * beide Shrinkage-Ziele werden berechnet und verglichen
  * Datenqualitaetspruefungen (Luecken, Ausreisser, Mindestlaenge)
  * Eigenwertspektrum wird ausgewiesen, nicht nur die Konditionszahl
"""

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.covariance import LedoitWolf

HANDELSTAGE = 252


def lade_kurse(tickers: list[str], start, ende) -> pd.DataFrame:
    """Laedt bereinigte Schlusskurse und garantiert die Spaltenreihenfolge."""
    print(f"Lade {len(tickers)} Ticker von {start} bis {ende} ...")
    roh = yf.download(tickers, start=start, end=ende, auto_adjust=True, progress=False)
    if roh.empty:
        raise SystemExit("Download fehlgeschlagen (Netz, Ticker oder Rate-Limit pruefen).")

    if isinstance(roh.columns, pd.MultiIndex):
        # [tickers] erzwingt die gewuenschte Spaltenreihenfolge
        kurse = roh["Close"][tickers].dropna()
    else:
        kurse = roh[["Close"]].dropna()
        kurse.columns = tickers

    assert list(kurse.columns) == tickers, (
        f"Spaltenreihenfolge weicht ab!\n  erwartet: {tickers}\n"
        f"  erhalten: {list(kurse.columns)}")
    return kurse


def pruefe_datenqualitaet(kurse: pd.DataFrame, mindest_tage: int = 200) -> None:
    """Faengt die haeufigsten Datenprobleme ab, bevor sie ins Modell gelangen."""
    T, N = kurse.shape
    print(f"\nDatenqualitaet: {T} Handelstage, {N} Titel (T/N = {T/N:.1f})")
    if T < mindest_tage:
        raise SystemExit(f"Zu wenige Beobachtungen ({T} < {mindest_tage}).")
    if T < N:
        print("  WARNUNG: T < N - die Stichprobenkovarianz ist singulaer!")
    elif T < 3 * N:
        print("  WARNUNG: T < 3N - erhebliches Schaetzrauschen zu erwarten.")

    renditen = kurse.pct_change().dropna()
    extreme = (renditen.abs() > 0.25).sum().sum()
    if extreme:
        print(f"  Hinweis: {extreme} Tagesrenditen ueber 25 % "
              f"(Splits, Sondersituationen oder Datenfehler pruefen).")
    luecken = kurse.isna().sum().sum()
    print(f"  Fehlende Werte nach dropna: {luecken}")


def ziel_konstante_korrelation(renditen: np.ndarray) -> np.ndarray:
    """
    Shrinkage-Ziel nach Ledoit/Wolf 2003: Einzelvarianzen behalten,
    alle Korrelationen durch ihren Mittelwert ersetzen.
    (sklearn nutzt stattdessen die skalierte Einheitsmatrix - siehe oben.)
    """
    S = np.cov(renditen, rowvar=False, ddof=1)
    d = np.sqrt(np.diag(S))
    korrelation = S / np.outer(d, d)
    n = len(S)
    r_quer = (korrelation.sum() - n) / (n * (n - 1))       # Mittel ohne Diagonale
    F = r_quer * np.outer(d, d)
    np.fill_diagonal(F, np.diag(S))
    return F


def analysiere(kurse: pd.DataFrame):
    renditen = kurse.pct_change().dropna()
    T, N = renditen.shape

    mu = renditen.mean().values * HANDELSTAGE
    sigma_stichprobe = renditen.cov().values * HANDELSTAGE

    lw = LedoitWolf(assume_centered=False).fit(renditen.values)
    sigma_lw = lw.covariance_ * HANDELSTAGE
    delta = lw.shrinkage_

    print("\n" + "=" * 84)
    print("         FINANZDATEN-PIPELINE UND MATRIX-KONDITIONIERUNG")
    print("=" * 84)
    print(f"Beobachtungen T:            {T}")
    print(f"Titel N:                    {N}")
    print(f"Optimales Shrinkage delta:  {delta:.4f} "
          f"({delta*100:.1f} % Gewicht auf dem strukturierten Ziel)")

    eig_stich = np.linalg.eigvalsh(sigma_stichprobe)
    eig_lw = np.linalg.eigvalsh(sigma_lw)

    print("\n--- Eigenwertspektrum (annualisiert) ---")
    print(f"{'':<14} {'kleinster':>12} {'Median':>12} {'groesster':>12} "
          f"{'Kondition':>12}")
    for name, eig in [("Stichprobe", eig_stich), ("Ledoit-Wolf", eig_lw)]:
        kondition = eig.max() / max(eig.min(), 1e-12)
        print(f"{name:<14} {eig.min():>12.6f} {np.median(eig):>12.6f} "
              f"{eig.max():>12.6f} {kondition:>12.1f}")

    print("\nDie Konditionszahl misst, wie stark sich kleine Datenaenderungen auf")
    print("die Inverse auswirken. Je kleiner, desto stabiler die Portfoliogewichte.")

    # --- Vergleich der beiden Shrinkage-Ziele ----------------------------
    S_taeglich = np.cov(renditen.values, rowvar=False, ddof=1)
    F_identitaet = np.eye(N) * np.trace(S_taeglich) / N
    F_korrelation = ziel_konstante_korrelation(renditen.values)

    print("\n--- Die beiden Shrinkage-Ziele im Vergleich ---")
    for name, F in [("skalierte Einheitsmatrix (sklearn)", F_identitaet),
                    ("konstante Korrelation (LW 2003)", F_korrelation)]:
        gemischt = ((1 - delta) * S_taeglich + delta * F) * HANDELSTAGE
        eig = np.linalg.eigvalsh(gemischt)
        print(f"  {name:<36}: Kondition {eig.max()/max(eig.min(),1e-12):8.1f}, "
              f"kleinster EW {eig.min():.6f}")

    print("\n--- Erwartete Renditen (annualisiert) ---")
    uebersicht = pd.DataFrame({
        "Ticker": kurse.columns,
        "Rendite p.a.": [f"{r*100:+7.2f} %" for r in mu],
        "Volatilitaet p.a.": [f"{np.sqrt(sigma_lw[i, i])*100:6.2f} %" for i in range(N)],
    })
    print(uebersicht.to_string(index=False))
    print("=" * 84)
    return renditen, mu, sigma_stichprobe, sigma_lw


if __name__ == "__main__":
    UNIVERSUM = ["AAPL", "MSFT", "NVDA", "AMZN",   # Technologie
                 "JNJ", "PFE",                      # Gesundheit
                 "JPM", "GS",                       # Banken
                 "XOM", "CVX"]                      # Energie

    # Relatives 2-Jahres-Fenster, damit das Beispiel nicht "altert"
    ende = pd.Timestamp.today().normalize()
    start = ende - pd.DateOffset(years=2)

    kurse = lade_kurse(UNIVERSUM, start.strftime("%Y-%m-%d"), ende.strftime("%Y-%m-%d"))
    pruefe_datenqualitaet(kurse)
    analysiere(kurse)
