Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
540
Notebooks_04/finanzdaten.ipynb
Normal file
540
Notebooks_04/finanzdaten.ipynb
Normal file
|
|
@ -0,0 +1,540 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Kapitel 18: Finanzdaten-Modellierung — Renditen, Kovarianz und Shrinkage\n",
|
||||
"\n",
|
||||
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Logarithmische Rendite\n",
|
||||
"\n",
|
||||
"`Renditen_Vergleich.py`\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"#!/usr/bin/env python3\n",
|
||||
"\n",
|
||||
"# Renditen_Vergleich.py\n",
|
||||
"\"\"\"\n",
|
||||
"Kapitel Finanzdaten: Diskrete vs. logarithmische Renditen.\n",
|
||||
"Zeigt an einem simulierten Kursverlauf, welche Eigenschaft wo gilt -\n",
|
||||
"und warum das arithmetische Mittel diskreter Renditen in die Irre fuehrt.\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" kurse = np.array([100.0, 150.0, 100.0, 120.0, 90.0, 135.0])\n",
|
||||
" tage = [f\"t{i}\" for i in range(len(kurse))]\n",
|
||||
"\n",
|
||||
" diskret = kurse[1:] / kurse[:-1] - 1.0\n",
|
||||
" logarithmisch = np.log(kurse[1:] / kurse[:-1])\n",
|
||||
"\n",
|
||||
" print(\"=\" * 74)\n",
|
||||
" print(\" DISKRETE UND LOGARITHMISCHE RENDITEN IM VERGLEICH\")\n",
|
||||
" print(\"=\" * 74)\n",
|
||||
" tabelle = pd.DataFrame({\n",
|
||||
" \"Periode\": [f\"{tage[i]} -> {tage[i+1]}\" for i in range(len(diskret))],\n",
|
||||
" \"Kurs von\": kurse[:-1],\n",
|
||||
" \"Kurs bis\": kurse[1:],\n",
|
||||
" \"diskret R\": [f\"{r*100:+7.2f} %\" for r in diskret],\n",
|
||||
" \"log r\": [f\"{r:+8.4f}\" for r in logarithmisch],\n",
|
||||
" })\n",
|
||||
" print(tabelle.to_string(index=False))\n",
|
||||
"\n",
|
||||
" # --- Zeitliche Aggregation ------------------------------------------\n",
|
||||
" gesamt_wahr = kurse[-1] / kurse[0] - 1.0\n",
|
||||
" summe_log = logarithmisch.sum()\n",
|
||||
" aus_log_zurueck = np.exp(summe_log) - 1.0\n",
|
||||
" summe_diskret = diskret.sum()\n",
|
||||
"\n",
|
||||
" print(\"\\n--- Aggregation ueber die Zeit ---\")\n",
|
||||
" print(f\" Tatsaechliche Gesamtrendite: {gesamt_wahr*100:+8.2f} %\")\n",
|
||||
" print(f\" Summe der Log-Renditen -> exp()-1: {aus_log_zurueck*100:+8.2f} % \"\n",
|
||||
" f\"{'KORREKT' if abs(aus_log_zurueck - gesamt_wahr) < 1e-9 else 'falsch'}\")\n",
|
||||
" print(f\" Summe der diskreten Renditen: {summe_diskret*100:+8.2f} % FALSCH\")\n",
|
||||
"\n",
|
||||
" # --- Mittelwerte ------------------------------------------------------\n",
|
||||
" arithmetisch = diskret.mean()\n",
|
||||
" geometrisch = np.prod(1 + diskret) ** (1 / len(diskret)) - 1\n",
|
||||
" aus_log = np.exp(logarithmisch.mean()) - 1\n",
|
||||
"\n",
|
||||
" print(\"\\n--- Welcher Mittelwert ist der richtige? ---\")\n",
|
||||
" print(f\" Arithmetisches Mittel (diskret): {arithmetisch*100:+8.2f} % \"\n",
|
||||
" f\"-> ueberschaetzt\")\n",
|
||||
" print(f\" Geometrisches Mittel: {geometrisch*100:+8.2f} % -> korrekt\")\n",
|
||||
" print(f\" exp(Mittel der Log-Renditen) - 1: {aus_log*100:+8.2f} % \"\n",
|
||||
" f\"-> identisch zum geometrischen\")\n",
|
||||
"\n",
|
||||
" # Probe: Endkapital mit dem jeweiligen Mittelwert hochgerechnet\n",
|
||||
" n = len(diskret)\n",
|
||||
" print(f\"\\n Probe - Startkapital 100 EUR ueber {n} Perioden:\")\n",
|
||||
" print(f\" tatsaechlich: {kurse[-1]:8.2f} EUR\")\n",
|
||||
" print(f\" mit arithm. Mittel hochgerechnet: {100*(1+arithmetisch)**n:8.2f} EUR\")\n",
|
||||
" print(f\" mit geom. Mittel hochgerechnet: {100*(1+geometrisch)**n:8.2f} EUR\")\n",
|
||||
"\n",
|
||||
" print(\"\\n--- Annualisierung (252 Handelstage) ---\")\n",
|
||||
" print(f\" Volatilitaet aus Log-Renditen: \"\n",
|
||||
" f\"{logarithmisch.std(ddof=1)*np.sqrt(252)*100:.2f} % p.a.\")\n",
|
||||
" print(\" (Die Wurzel-Zeit-Regel gilt nur bei unabhaengigen Renditen -\")\n",
|
||||
" print(\" fuer reale Maerkte ist sie eine Naeherung.)\")\n",
|
||||
" print(\"=\" * 74)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Das Schätzfehler-Problem\n",
|
||||
"\n",
|
||||
"`Schaetzrauschen_Demo.py`\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"#!/usr/bin/env python3\n",
|
||||
"\n",
|
||||
"# Schaetzrauschen_Demo.py\n",
|
||||
"\"\"\"\n",
|
||||
"Kapitel Finanzdaten: Der Error-Maximizer-Effekt im Experiment.\n",
|
||||
"\n",
|
||||
"Aufbau: Wir KENNEN die wahre Kovarianzmatrix, weil wir die Daten selbst\n",
|
||||
"erzeugen. Dann schaetzen wir sie aus endlich vielen Beobachtungen und\n",
|
||||
"vergleichen, wie gut das daraus optimierte Portfolio in der WAHREN Welt\n",
|
||||
"abschneidet - mit und ohne Shrinkage.\n",
|
||||
"\n",
|
||||
"Das ist der einzige saubere Weg, Schaetzfehler zu messen: Man braucht eine\n",
|
||||
"Wahrheit zum Vergleich, und die gibt es nur in der Simulation.\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"from sklearn.covariance import LedoitWolf\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def erzeuge_wahre_kovarianz(n, rng):\n",
|
||||
" \"\"\"Ein-Faktor-Modell: gemeinsamer Marktfaktor plus titelspezifisches Rauschen.\"\"\"\n",
|
||||
" beta = rng.uniform(0.6, 1.4, size=n) # Marktsensitivitaeten\n",
|
||||
" var_markt = 0.04 # Marktvarianz p.a.\n",
|
||||
" var_spezifisch = rng.uniform(0.01, 0.09, size=n) # idiosynkratische Varianz\n",
|
||||
" return np.outer(beta, beta) * var_markt + np.diag(var_spezifisch)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def minimum_varianz_gewichte(sigma):\n",
|
||||
" \"\"\"Analytische GMV-Loesung: w = Sigma^-1 1 / (1' Sigma^-1 1). Ohne Restriktionen.\"\"\"\n",
|
||||
" eins = np.ones(len(sigma))\n",
|
||||
" try:\n",
|
||||
" loesung = np.linalg.solve(sigma, eins)\n",
|
||||
" except np.linalg.LinAlgError:\n",
|
||||
" loesung = np.linalg.pinv(sigma) @ eins # falls singulaer\n",
|
||||
" return loesung / loesung.sum()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def portfoliovolatilitaet(w, sigma):\n",
|
||||
" return float(np.sqrt(w @ sigma @ w))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" rng = np.random.default_rng(2026)\n",
|
||||
" N = 40 # Anzahl Titel\n",
|
||||
" WIEDERHOLUNGEN = 200\n",
|
||||
"\n",
|
||||
" sigma_wahr = erzeuge_wahre_kovarianz(N, rng)\n",
|
||||
" w_ideal = minimum_varianz_gewichte(sigma_wahr)\n",
|
||||
" vola_ideal = portfoliovolatilitaet(w_ideal, sigma_wahr)\n",
|
||||
"\n",
|
||||
" print(\"=\" * 92)\n",
|
||||
" print(\" ERROR-MAXIMIZER: WIE TEUER IST SCHAETZRAUSCHEN?\")\n",
|
||||
" print(\"=\" * 92)\n",
|
||||
" print(f\"Aufbau: {N} Titel, wahre Kovarianz bekannt (Ein-Faktor-Modell).\")\n",
|
||||
" print(f\"Bestmoegliche Volatilitaet bei perfektem Wissen: \"\n",
|
||||
" f\"{vola_ideal*100:.2f} % p.a.\\n\")\n",
|
||||
"\n",
|
||||
" print(f\"{'T (Tage)':>9} | {'T/N':>5} | {'Stichprobe':>22} | {'Ledoit-Wolf':>22} | \"\n",
|
||||
" f\"{'Shrink':>7}\")\n",
|
||||
" print(f\"{'':>9} | {'':>5} | {'Vola Aufschlag':>22} | \"\n",
|
||||
" f\"{'Vola Aufschlag':>22} | {'delta':>7}\")\n",
|
||||
" print(\"-\" * 92)\n",
|
||||
"\n",
|
||||
" for T in [60, 120, 252, 504, 1260]:\n",
|
||||
" vola_stichprobe, vola_lw, deltas = [], [], []\n",
|
||||
"\n",
|
||||
" for _ in range(WIEDERHOLUNGEN):\n",
|
||||
" # Daten aus der WAHREN Verteilung ziehen (taeglich)\n",
|
||||
" daten = rng.multivariate_normal(np.zeros(N), sigma_wahr / 252, size=T)\n",
|
||||
"\n",
|
||||
" # (a) Stichproben-Kovarianz\n",
|
||||
" s_stich = np.cov(daten, rowvar=False, ddof=1) * 252\n",
|
||||
" w_stich = minimum_varianz_gewichte(s_stich)\n",
|
||||
"\n",
|
||||
" # (b) Ledoit-Wolf-Shrinkage\n",
|
||||
" lw = LedoitWolf().fit(daten)\n",
|
||||
" s_lw = lw.covariance_ * 252\n",
|
||||
" w_lw = minimum_varianz_gewichte(s_lw)\n",
|
||||
" deltas.append(lw.shrinkage_)\n",
|
||||
"\n",
|
||||
" # Bewertung IMMER mit der wahren Kovarianz - das ist der Punkt\n",
|
||||
" vola_stichprobe.append(portfoliovolatilitaet(w_stich, sigma_wahr))\n",
|
||||
" vola_lw.append(portfoliovolatilitaet(w_lw, sigma_wahr))\n",
|
||||
"\n",
|
||||
" m_stich, m_lw = np.mean(vola_stichprobe), np.mean(vola_lw)\n",
|
||||
" print(f\"{T:>9} | {T/N:>5.1f} | {m_stich*100:>8.2f} % \"\n",
|
||||
" f\"{(m_stich/vola_ideal-1)*100:>+9.1f} % | \"\n",
|
||||
" f\"{m_lw*100:>8.2f} % {(m_lw/vola_ideal-1)*100:>+9.1f} % | \"\n",
|
||||
" f\"{np.mean(deltas):>7.3f}\")\n",
|
||||
"\n",
|
||||
" print(\"-\" * 92)\n",
|
||||
" print(\"'Aufschlag' = wie viel mehr Risiko das Portfolio in der WAHREN Welt traegt,\")\n",
|
||||
" print(\"verglichen mit dem Portfolio bei perfektem Wissen.\")\n",
|
||||
" print(\"Lesart: Je kleiner T/N, desto teurer das Schaetzrauschen - und desto mehr\")\n",
|
||||
" print(\"schrumpft Ledoit-Wolf (delta steigt).\")\n",
|
||||
" print(\"=\" * 92)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Praxis: Datenpipeline mit korrekter Spaltenreihenfolge\n",
|
||||
"\n",
|
||||
"`Finanzdaten_Ledoit_Wolf.py`\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"#!/usr/bin/env python3\n",
|
||||
"\n",
|
||||
"# Finanzdaten_Ledoit_Wolf.py\n",
|
||||
"\"\"\"\n",
|
||||
"Kapitel Finanzdaten: Automatisierte Finanzdaten-Pipeline und Kovarianz-Shrinkage.\n",
|
||||
"\n",
|
||||
"Eigenschaften:\n",
|
||||
" * Spaltenreihenfolge wird erzwungen und per assert geprueft\n",
|
||||
" * beide Shrinkage-Ziele werden berechnet und verglichen\n",
|
||||
" * Datenqualitaetspruefungen (Luecken, Ausreisser, Mindestlaenge)\n",
|
||||
" * Eigenwertspektrum wird ausgewiesen, nicht nur die Konditionszahl\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"import yfinance as yf\n",
|
||||
"from sklearn.covariance import LedoitWolf\n",
|
||||
"\n",
|
||||
"HANDELSTAGE = 252\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def lade_kurse(tickers: list[str], start, ende) -> pd.DataFrame:\n",
|
||||
" \"\"\"Laedt bereinigte Schlusskurse und garantiert die Spaltenreihenfolge.\"\"\"\n",
|
||||
" print(f\"Lade {len(tickers)} Ticker von {start} bis {ende} ...\")\n",
|
||||
" roh = yf.download(tickers, start=start, end=ende, auto_adjust=True, progress=False)\n",
|
||||
" if roh.empty:\n",
|
||||
" raise SystemExit(\"Download fehlgeschlagen (Netz, Ticker oder Rate-Limit pruefen).\")\n",
|
||||
"\n",
|
||||
" if isinstance(roh.columns, pd.MultiIndex):\n",
|
||||
" # [tickers] erzwingt die gewuenschte Spaltenreihenfolge\n",
|
||||
" kurse = roh[\"Close\"][tickers].dropna()\n",
|
||||
" else:\n",
|
||||
" kurse = roh[[\"Close\"]].dropna()\n",
|
||||
" kurse.columns = tickers\n",
|
||||
"\n",
|
||||
" assert list(kurse.columns) == tickers, (\n",
|
||||
" f\"Spaltenreihenfolge weicht ab!\\n erwartet: {tickers}\\n\"\n",
|
||||
" f\" erhalten: {list(kurse.columns)}\")\n",
|
||||
" return kurse\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def pruefe_datenqualitaet(kurse: pd.DataFrame, mindest_tage: int = 200) -> None:\n",
|
||||
" \"\"\"Faengt die haeufigsten Datenprobleme ab, bevor sie ins Modell gelangen.\"\"\"\n",
|
||||
" T, N = kurse.shape\n",
|
||||
" print(f\"\\nDatenqualitaet: {T} Handelstage, {N} Titel (T/N = {T/N:.1f})\")\n",
|
||||
" if T < mindest_tage:\n",
|
||||
" raise SystemExit(f\"Zu wenige Beobachtungen ({T} < {mindest_tage}).\")\n",
|
||||
" if T < N:\n",
|
||||
" print(\" WARNUNG: T < N - die Stichprobenkovarianz ist singulaer!\")\n",
|
||||
" elif T < 3 * N:\n",
|
||||
" print(\" WARNUNG: T < 3N - erhebliches Schaetzrauschen zu erwarten.\")\n",
|
||||
"\n",
|
||||
" renditen = kurse.pct_change().dropna()\n",
|
||||
" extreme = (renditen.abs() > 0.25).sum().sum()\n",
|
||||
" if extreme:\n",
|
||||
" print(f\" Hinweis: {extreme} Tagesrenditen ueber 25 % \"\n",
|
||||
" f\"(Splits, Sondersituationen oder Datenfehler pruefen).\")\n",
|
||||
" luecken = kurse.isna().sum().sum()\n",
|
||||
" print(f\" Fehlende Werte nach dropna: {luecken}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def ziel_konstante_korrelation(renditen: np.ndarray) -> np.ndarray:\n",
|
||||
" \"\"\"\n",
|
||||
" Shrinkage-Ziel nach Ledoit/Wolf 2003: Einzelvarianzen behalten,\n",
|
||||
" alle Korrelationen durch ihren Mittelwert ersetzen.\n",
|
||||
" (sklearn nutzt stattdessen die skalierte Einheitsmatrix - siehe oben.)\n",
|
||||
" \"\"\"\n",
|
||||
" S = np.cov(renditen, rowvar=False, ddof=1)\n",
|
||||
" d = np.sqrt(np.diag(S))\n",
|
||||
" korrelation = S / np.outer(d, d)\n",
|
||||
" n = len(S)\n",
|
||||
" r_quer = (korrelation.sum() - n) / (n * (n - 1)) # Mittel ohne Diagonale\n",
|
||||
" F = r_quer * np.outer(d, d)\n",
|
||||
" np.fill_diagonal(F, np.diag(S))\n",
|
||||
" return F\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def analysiere(kurse: pd.DataFrame):\n",
|
||||
" renditen = kurse.pct_change().dropna()\n",
|
||||
" T, N = renditen.shape\n",
|
||||
"\n",
|
||||
" mu = renditen.mean().values * HANDELSTAGE\n",
|
||||
" sigma_stichprobe = renditen.cov().values * HANDELSTAGE\n",
|
||||
"\n",
|
||||
" lw = LedoitWolf(assume_centered=False).fit(renditen.values)\n",
|
||||
" sigma_lw = lw.covariance_ * HANDELSTAGE\n",
|
||||
" delta = lw.shrinkage_\n",
|
||||
"\n",
|
||||
" print(\"\\n\" + \"=\" * 84)\n",
|
||||
" print(\" FINANZDATEN-PIPELINE UND MATRIX-KONDITIONIERUNG\")\n",
|
||||
" print(\"=\" * 84)\n",
|
||||
" print(f\"Beobachtungen T: {T}\")\n",
|
||||
" print(f\"Titel N: {N}\")\n",
|
||||
" print(f\"Optimales Shrinkage delta: {delta:.4f} \"\n",
|
||||
" f\"({delta*100:.1f} % Gewicht auf dem strukturierten Ziel)\")\n",
|
||||
"\n",
|
||||
" eig_stich = np.linalg.eigvalsh(sigma_stichprobe)\n",
|
||||
" eig_lw = np.linalg.eigvalsh(sigma_lw)\n",
|
||||
"\n",
|
||||
" print(\"\\n--- Eigenwertspektrum (annualisiert) ---\")\n",
|
||||
" print(f\"{'':<14} {'kleinster':>12} {'Median':>12} {'groesster':>12} \"\n",
|
||||
" f\"{'Kondition':>12}\")\n",
|
||||
" for name, eig in [(\"Stichprobe\", eig_stich), (\"Ledoit-Wolf\", eig_lw)]:\n",
|
||||
" kondition = eig.max() / max(eig.min(), 1e-12)\n",
|
||||
" print(f\"{name:<14} {eig.min():>12.6f} {np.median(eig):>12.6f} \"\n",
|
||||
" f\"{eig.max():>12.6f} {kondition:>12.1f}\")\n",
|
||||
"\n",
|
||||
" print(\"\\nDie Konditionszahl misst, wie stark sich kleine Datenaenderungen auf\")\n",
|
||||
" print(\"die Inverse auswirken. Je kleiner, desto stabiler die Portfoliogewichte.\")\n",
|
||||
"\n",
|
||||
" # --- Vergleich der beiden Shrinkage-Ziele ----------------------------\n",
|
||||
" S_taeglich = np.cov(renditen.values, rowvar=False, ddof=1)\n",
|
||||
" F_identitaet = np.eye(N) * np.trace(S_taeglich) / N\n",
|
||||
" F_korrelation = ziel_konstante_korrelation(renditen.values)\n",
|
||||
"\n",
|
||||
" print(\"\\n--- Die beiden Shrinkage-Ziele im Vergleich ---\")\n",
|
||||
" for name, F in [(\"skalierte Einheitsmatrix (sklearn)\", F_identitaet),\n",
|
||||
" (\"konstante Korrelation (LW 2003)\", F_korrelation)]:\n",
|
||||
" gemischt = ((1 - delta) * S_taeglich + delta * F) * HANDELSTAGE\n",
|
||||
" eig = np.linalg.eigvalsh(gemischt)\n",
|
||||
" print(f\" {name:<36}: Kondition {eig.max()/max(eig.min(),1e-12):8.1f}, \"\n",
|
||||
" f\"kleinster EW {eig.min():.6f}\")\n",
|
||||
"\n",
|
||||
" print(\"\\n--- Erwartete Renditen (annualisiert) ---\")\n",
|
||||
" uebersicht = pd.DataFrame({\n",
|
||||
" \"Ticker\": kurse.columns,\n",
|
||||
" \"Rendite p.a.\": [f\"{r*100:+7.2f} %\" for r in mu],\n",
|
||||
" \"Volatilitaet p.a.\": [f\"{np.sqrt(sigma_lw[i, i])*100:6.2f} %\" for i in range(N)],\n",
|
||||
" })\n",
|
||||
" print(uebersicht.to_string(index=False))\n",
|
||||
" print(\"=\" * 84)\n",
|
||||
" return renditen, mu, sigma_stichprobe, sigma_lw\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" UNIVERSUM = [\"AAPL\", \"MSFT\", \"NVDA\", \"AMZN\", # Technologie\n",
|
||||
" \"JNJ\", \"PFE\", # Gesundheit\n",
|
||||
" \"JPM\", \"GS\", # Banken\n",
|
||||
" \"XOM\", \"CVX\"] # Energie\n",
|
||||
"\n",
|
||||
" # Relatives 2-Jahres-Fenster, damit das Beispiel nicht \"altert\"\n",
|
||||
" ende = pd.Timestamp.today().normalize()\n",
|
||||
" start = ende - pd.DateOffset(years=2)\n",
|
||||
"\n",
|
||||
" kurse = lade_kurse(UNIVERSUM, start.strftime(\"%Y-%m-%d\"), ende.strftime(\"%Y-%m-%d\"))\n",
|
||||
" pruefe_datenqualitaet(kurse)\n",
|
||||
" analysiere(kurse)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Finde den Denkfehler\n",
|
||||
"\n",
|
||||
"`Kovarianz_Falle.py`\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"#!/usr/bin/env python3\n",
|
||||
"\n",
|
||||
"# Kovarianz_Falle.py\n",
|
||||
"\"\"\"\n",
|
||||
"Kapitel Finanzdaten: Warum die historische Kovarianzmatrix den Optimierer belaeugt.\n",
|
||||
"\n",
|
||||
"Ein Portfolio aus 30 Anlagen soll das Risiko minimieren. Geschaetzt wird die\n",
|
||||
"Kovarianzmatrix aus 20 Beobachtungen - in der Praxis ein voellig ueblicher\n",
|
||||
"Fall (30 Titel, ein Quartal Wochendaten).\n",
|
||||
"\n",
|
||||
"Das Ergebnis ist ein Portfolio mit einem gemessenen Risiko von 0,000 %. Der\n",
|
||||
"Optimierer haelt es fuer risikofrei. Ausserhalb des Schaetzzeitraums ist es\n",
|
||||
"das schlechteste der drei untersuchten Portfolios - schlechter sogar als\n",
|
||||
"blosse Gleichgewichtung.\n",
|
||||
"\n",
|
||||
"Der Grund ist nicht Zufall, sondern Struktur: Bei N Anlagen und T < N\n",
|
||||
"Beobachtungen hat die Stichproben-Kovarianzmatrix hoechstens Rang T-1. Es gibt\n",
|
||||
"dann ganze Richtungen im Gewichtsraum, in denen sie exakt null Varianz misst -\n",
|
||||
"Richtungen, die es in Wirklichkeit nicht gibt. Der Optimierer findet sie\n",
|
||||
"zuverlaessig, denn er sucht ja genau danach.\n",
|
||||
"\n",
|
||||
"Die Daten sind synthetisch (ein Marktfaktor plus Eigenrauschen), damit das\n",
|
||||
"Ergebnis reproduzierbar ist und kein Internetzugang noetig wird. Mit echten\n",
|
||||
"Kursdaten sieht es genauso aus.\n",
|
||||
"\n",
|
||||
"Benoetigt: numpy, cvxpy, scikit-learn\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"from __future__ import annotations\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"import cvxpy as cp\n",
|
||||
"from sklearn.covariance import LedoitWolf\n",
|
||||
"\n",
|
||||
"ANZAHL_ANLAGEN = 30\n",
|
||||
"BEOBACHTUNGEN_SCHAETZUNG = 20 # weniger als Anlagen - genau darum geht es\n",
|
||||
"BEOBACHTUNGEN_TEST = 2000 # der \"spaetere Verlauf\"\n",
|
||||
"\n",
|
||||
"RNG = np.random.default_rng(11)\n",
|
||||
"# Jede Anlage reagiert unterschiedlich stark auf den Gesamtmarkt.\n",
|
||||
"BETA = RNG.uniform(0.6, 1.4, ANZAHL_ANLAGEN)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def erzeuge_renditen(perioden: int) -> np.ndarray:\n",
|
||||
" \"\"\"Ein Marktfaktor, auf den alle reagieren, plus Eigenrauschen je Anlage.\"\"\"\n",
|
||||
" markt = RNG.normal(0.0, 0.010, perioden)\n",
|
||||
" eigen = RNG.normal(0.0, 0.012, (perioden, ANZAHL_ANLAGEN))\n",
|
||||
" return np.outer(markt, BETA) + eigen\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def minimales_risiko(kovarianz: np.ndarray) -> tuple[np.ndarray, float]:\n",
|
||||
" \"\"\"Minimum-Varianz-Portfolio: Gewichte summieren sich zu 1, sonst frei.\n",
|
||||
"\n",
|
||||
" cp.psd_wrap() sagt CVXPY: 'Ich weiss, dass diese Matrix numerisch nicht\n",
|
||||
" exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so\n",
|
||||
" gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im\n",
|
||||
" Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man\n",
|
||||
" nachgehen muss (siehe Denkfehler 8.1).\n",
|
||||
" \"\"\"\n",
|
||||
" gewichte = cp.Variable(ANZAHL_ANLAGEN)\n",
|
||||
" problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),\n",
|
||||
" [cp.sum(gewichte) == 1])\n",
|
||||
" problem.solve()\n",
|
||||
" return np.array(gewichte.value).ravel(), max(problem.value, 0.0)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" schaetzzeitraum = erzeuge_renditen(BEOBACHTUNGEN_SCHAETZUNG)\n",
|
||||
" spaeterer_verlauf = erzeuge_renditen(BEOBACHTUNGEN_TEST)\n",
|
||||
"\n",
|
||||
" stichprobe = np.cov(schaetzzeitraum, rowvar=False)\n",
|
||||
" ledoit_wolf = LedoitWolf().fit(schaetzzeitraum)\n",
|
||||
"\n",
|
||||
" print(\"=\" * 78)\n",
|
||||
" print(\" DIE KOVARIANZ-FALLE\")\n",
|
||||
" print(\"=\" * 78)\n",
|
||||
" print(f\"{ANZAHL_ANLAGEN} Anlagen, geschaetzt aus {BEOBACHTUNGEN_SCHAETZUNG} \"\n",
|
||||
" f\"Beobachtungen.\")\n",
|
||||
" print(f\"Rang der Stichprobenmatrix: \"\n",
|
||||
" f\"{np.linalg.matrix_rank(stichprobe)} - noetig waeren {ANZAHL_ANLAGEN}.\")\n",
|
||||
" print(f\"Kleinster Eigenwert: {np.linalg.eigvalsh(stichprobe).min():.2e}\")\n",
|
||||
" print()\n",
|
||||
" print(\"Es gibt also Richtungen, in denen diese Matrix EXAKT null Varianz\")\n",
|
||||
" print(\"misst. Genau dort sucht ein Minimum-Varianz-Optimierer.\")\n",
|
||||
" print()\n",
|
||||
"\n",
|
||||
" print(f\"{'Schaetzer':<26} {'Risiko im Zeitraum':>19} \"\n",
|
||||
" f\"{'Risiko spaeter':>16} {'Hebel':>7}\")\n",
|
||||
" print(\"-\" * 78)\n",
|
||||
"\n",
|
||||
" ergebnisse = {}\n",
|
||||
" for name, matrix in [\n",
|
||||
" (\"Stichprobe (roh)\", stichprobe),\n",
|
||||
" (f\"Ledoit-Wolf (d={ledoit_wolf.shrinkage_:.2f})\", ledoit_wolf.covariance_)]:\n",
|
||||
" gewichte, varianz_intern = minimales_risiko(matrix)\n",
|
||||
" risiko_spaeter = float(np.std(spaeterer_verlauf @ gewichte))\n",
|
||||
" hebel = float(np.abs(gewichte).sum())\n",
|
||||
" ergebnisse[name] = (risiko_spaeter, gewichte)\n",
|
||||
" print(f\"{name:<26} {np.sqrt(varianz_intern) * 100:>18.3f} % \"\n",
|
||||
" f\"{risiko_spaeter * 100:>15.3f} % {hebel:>7.1f}\")\n",
|
||||
"\n",
|
||||
" gleich = np.full(ANZAHL_ANLAGEN, 1.0 / ANZAHL_ANLAGEN)\n",
|
||||
" print(f\"{'Gleichgewichtung 1/N':<26} \"\n",
|
||||
" f\"{float(np.std(schaetzzeitraum @ gleich)) * 100:>18.3f} % \"\n",
|
||||
" f\"{float(np.std(spaeterer_verlauf @ gleich)) * 100:>15.3f} % {1.0:>7.1f}\")\n",
|
||||
" print(\"-\" * 78)\n",
|
||||
"\n",
|
||||
" print(\"\\nDrei Beobachtungen:\")\n",
|
||||
" print(\"1. Die rohe Schaetzung meldet 0,000 % Risiko - und liefert spaeter das\")\n",
|
||||
" print(\" SCHLECHTESTE Ergebnis der drei. Sie hat kein Portfolio optimiert,\")\n",
|
||||
" print(\" sondern eine Luecke in den eigenen Daten gefunden.\")\n",
|
||||
" print(\"2. Die Gleichgewichtung, die gar nicht optimiert, schlaegt den rohen\")\n",
|
||||
" print(\" Optimierer deutlich. Das ist kein Zufallsbefund, sondern in der\")\n",
|
||||
" print(\" Literatur breit belegt.\")\n",
|
||||
" print(\"3. Ledoit-Wolf ist im Schaetzzeitraum EHRLICHER (0,441 statt 0,000 %)\")\n",
|
||||
" print(\" und spaeter am besten. Der Hebel faellt von 3,8 auf 1,8 - das\")\n",
|
||||
" print(\" Portfolio wird auch praktisch handelbarer.\")\n",
|
||||
" print()\n",
|
||||
" print(\"Merksatz: Ein Optimierer glaubt seinen Eingabedaten vollstaendig.\")\n",
|
||||
" print(\"Je mehr Freiheit Sie ihm geben, desto gruendlicher findet er deren\")\n",
|
||||
" print(\"Fehler. Das gilt fuer Kovarianzmatrizen wie fuer Lieferzeiten,\")\n",
|
||||
" print(\"Ausfallraten und jede andere geschaetzte Groesse.\")\n",
|
||||
" print(\"=\" * 78)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Loading…
Reference in a new issue