123 lines
5.3 KiB
Python
123 lines
5.3 KiB
Python
|
|
#!/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)
|