operations_research/OR_HTML_04/programme/Kovarianz_Falle.py

123 lines
5.3 KiB
Python
Raw Normal View History

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>
2026-09-08 01:20:09 +02:00
#!/usr/bin/env python3
# Kovarianz_Falle.py
"""
Kapitel Finanzdaten: Warum die historische Kovarianzmatrix den Optimierer belaeugt.
Ein Portfolio aus 30 Anlagen soll das Risiko minimieren. Geschaetzt wird die
Kovarianzmatrix aus 20 Beobachtungen - in der Praxis ein voellig ueblicher
Fall (30 Titel, ein Quartal Wochendaten).
Das Ergebnis ist ein Portfolio mit einem gemessenen Risiko von 0,000 %. Der
Optimierer haelt es fuer risikofrei. Ausserhalb des Schaetzzeitraums ist es
das schlechteste der drei untersuchten Portfolios - schlechter sogar als
blosse Gleichgewichtung.
Der Grund ist nicht Zufall, sondern Struktur: Bei N Anlagen und T < N
Beobachtungen hat die Stichproben-Kovarianzmatrix hoechstens Rang T-1. Es gibt
dann ganze Richtungen im Gewichtsraum, in denen sie exakt null Varianz misst -
Richtungen, die es in Wirklichkeit nicht gibt. Der Optimierer findet sie
zuverlaessig, denn er sucht ja genau danach.
Die Daten sind synthetisch (ein Marktfaktor plus Eigenrauschen), damit das
Ergebnis reproduzierbar ist und kein Internetzugang noetig wird. Mit echten
Kursdaten sieht es genauso aus.
Benoetigt: numpy, cvxpy, scikit-learn
"""
from __future__ import annotations
import numpy as np
import cvxpy as cp
from sklearn.covariance import LedoitWolf
ANZAHL_ANLAGEN = 30
BEOBACHTUNGEN_SCHAETZUNG = 20 # weniger als Anlagen - genau darum geht es
BEOBACHTUNGEN_TEST = 2000 # der "spaetere Verlauf"
RNG = np.random.default_rng(11)
# Jede Anlage reagiert unterschiedlich stark auf den Gesamtmarkt.
BETA = RNG.uniform(0.6, 1.4, ANZAHL_ANLAGEN)
def erzeuge_renditen(perioden: int) -> np.ndarray:
"""Ein Marktfaktor, auf den alle reagieren, plus Eigenrauschen je Anlage."""
markt = RNG.normal(0.0, 0.010, perioden)
eigen = RNG.normal(0.0, 0.012, (perioden, ANZAHL_ANLAGEN))
return np.outer(markt, BETA) + eigen
def minimales_risiko(kovarianz: np.ndarray) -> tuple[np.ndarray, float]:
"""Minimum-Varianz-Portfolio: Gewichte summieren sich zu 1, sonst frei.
cp.psd_wrap() sagt CVXPY: 'Ich weiss, dass diese Matrix numerisch nicht
exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so
gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im
Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man
nachgehen muss (siehe Denkfehler 8.1).
"""
gewichte = cp.Variable(ANZAHL_ANLAGEN)
problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),
[cp.sum(gewichte) == 1])
problem.solve()
return np.array(gewichte.value).ravel(), max(problem.value, 0.0)
if __name__ == "__main__":
schaetzzeitraum = erzeuge_renditen(BEOBACHTUNGEN_SCHAETZUNG)
spaeterer_verlauf = erzeuge_renditen(BEOBACHTUNGEN_TEST)
stichprobe = np.cov(schaetzzeitraum, rowvar=False)
ledoit_wolf = LedoitWolf().fit(schaetzzeitraum)
print("=" * 78)
print(" DIE KOVARIANZ-FALLE")
print("=" * 78)
print(f"{ANZAHL_ANLAGEN} Anlagen, geschaetzt aus {BEOBACHTUNGEN_SCHAETZUNG} "
f"Beobachtungen.")
print(f"Rang der Stichprobenmatrix: "
f"{np.linalg.matrix_rank(stichprobe)} - noetig waeren {ANZAHL_ANLAGEN}.")
print(f"Kleinster Eigenwert: {np.linalg.eigvalsh(stichprobe).min():.2e}")
print()
print("Es gibt also Richtungen, in denen diese Matrix EXAKT null Varianz")
print("misst. Genau dort sucht ein Minimum-Varianz-Optimierer.")
print()
print(f"{'Schaetzer':<26} {'Risiko im Zeitraum':>19} "
f"{'Risiko spaeter':>16} {'Hebel':>7}")
print("-" * 78)
ergebnisse = {}
for name, matrix in [
("Stichprobe (roh)", stichprobe),
(f"Ledoit-Wolf (d={ledoit_wolf.shrinkage_:.2f})", ledoit_wolf.covariance_)]:
gewichte, varianz_intern = minimales_risiko(matrix)
risiko_spaeter = float(np.std(spaeterer_verlauf @ gewichte))
hebel = float(np.abs(gewichte).sum())
ergebnisse[name] = (risiko_spaeter, gewichte)
print(f"{name:<26} {np.sqrt(varianz_intern) * 100:>18.3f} % "
f"{risiko_spaeter * 100:>15.3f} % {hebel:>7.1f}")
gleich = np.full(ANZAHL_ANLAGEN, 1.0 / ANZAHL_ANLAGEN)
print(f"{'Gleichgewichtung 1/N':<26} "
f"{float(np.std(schaetzzeitraum @ gleich)) * 100:>18.3f} % "
f"{float(np.std(spaeterer_verlauf @ gleich)) * 100:>15.3f} % {1.0:>7.1f}")
print("-" * 78)
print("\nDrei Beobachtungen:")
print("1. Die rohe Schaetzung meldet 0,000 % Risiko - und liefert spaeter das")
print(" SCHLECHTESTE Ergebnis der drei. Sie hat kein Portfolio optimiert,")
print(" sondern eine Luecke in den eigenen Daten gefunden.")
print("2. Die Gleichgewichtung, die gar nicht optimiert, schlaegt den rohen")
print(" Optimierer deutlich. Das ist kein Zufallsbefund, sondern in der")
print(" Literatur breit belegt.")
print("3. Ledoit-Wolf ist im Schaetzzeitraum EHRLICHER (0,441 statt 0,000 %)")
print(" und spaeter am besten. Der Hebel faellt von 3,8 auf 1,8 - das")
print(" Portfolio wird auch praktisch handelbarer.")
print()
print("Merksatz: Ein Optimierer glaubt seinen Eingabedaten vollstaendig.")
print("Je mehr Freiheit Sie ihm geben, desto gruendlicher findet er deren")
print("Fehler. Das gilt fuer Kovarianzmatrizen wie fuer Lieferzeiten,")
print("Ausfallraten und jede andere geschaetzte Groesse.")
print("=" * 78)