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>
96 lines
3.7 KiB
Python
Generated
96 lines
3.7 KiB
Python
Generated
#!/usr/bin/env python3
|
|
|
|
# Schaetzrauschen_Demo.py
|
|
"""
|
|
Kapitel Finanzdaten: Der Error-Maximizer-Effekt im Experiment.
|
|
|
|
Aufbau: Wir KENNEN die wahre Kovarianzmatrix, weil wir die Daten selbst
|
|
erzeugen. Dann schaetzen wir sie aus endlich vielen Beobachtungen und
|
|
vergleichen, wie gut das daraus optimierte Portfolio in der WAHREN Welt
|
|
abschneidet - mit und ohne Shrinkage.
|
|
|
|
Das ist der einzige saubere Weg, Schaetzfehler zu messen: Man braucht eine
|
|
Wahrheit zum Vergleich, und die gibt es nur in der Simulation.
|
|
"""
|
|
|
|
import numpy as np
|
|
from sklearn.covariance import LedoitWolf
|
|
|
|
|
|
def erzeuge_wahre_kovarianz(n, rng):
|
|
"""Ein-Faktor-Modell: gemeinsamer Marktfaktor plus titelspezifisches Rauschen."""
|
|
beta = rng.uniform(0.6, 1.4, size=n) # Marktsensitivitaeten
|
|
var_markt = 0.04 # Marktvarianz p.a.
|
|
var_spezifisch = rng.uniform(0.01, 0.09, size=n) # idiosynkratische Varianz
|
|
return np.outer(beta, beta) * var_markt + np.diag(var_spezifisch)
|
|
|
|
|
|
def minimum_varianz_gewichte(sigma):
|
|
"""Analytische GMV-Loesung: w = Sigma^-1 1 / (1' Sigma^-1 1). Ohne Restriktionen."""
|
|
eins = np.ones(len(sigma))
|
|
try:
|
|
loesung = np.linalg.solve(sigma, eins)
|
|
except np.linalg.LinAlgError:
|
|
loesung = np.linalg.pinv(sigma) @ eins # falls singulaer
|
|
return loesung / loesung.sum()
|
|
|
|
|
|
def portfoliovolatilitaet(w, sigma):
|
|
return float(np.sqrt(w @ sigma @ w))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
rng = np.random.default_rng(2026)
|
|
N = 40 # Anzahl Titel
|
|
WIEDERHOLUNGEN = 200
|
|
|
|
sigma_wahr = erzeuge_wahre_kovarianz(N, rng)
|
|
w_ideal = minimum_varianz_gewichte(sigma_wahr)
|
|
vola_ideal = portfoliovolatilitaet(w_ideal, sigma_wahr)
|
|
|
|
print("=" * 92)
|
|
print(" ERROR-MAXIMIZER: WIE TEUER IST SCHAETZRAUSCHEN?")
|
|
print("=" * 92)
|
|
print(f"Aufbau: {N} Titel, wahre Kovarianz bekannt (Ein-Faktor-Modell).")
|
|
print(f"Bestmoegliche Volatilitaet bei perfektem Wissen: "
|
|
f"{vola_ideal*100:.2f} % p.a.\n")
|
|
|
|
print(f"{'T (Tage)':>9} | {'T/N':>5} | {'Stichprobe':>22} | {'Ledoit-Wolf':>22} | "
|
|
f"{'Shrink':>7}")
|
|
print(f"{'':>9} | {'':>5} | {'Vola Aufschlag':>22} | "
|
|
f"{'Vola Aufschlag':>22} | {'delta':>7}")
|
|
print("-" * 92)
|
|
|
|
for T in [60, 120, 252, 504, 1260]:
|
|
vola_stichprobe, vola_lw, deltas = [], [], []
|
|
|
|
for _ in range(WIEDERHOLUNGEN):
|
|
# Daten aus der WAHREN Verteilung ziehen (taeglich)
|
|
daten = rng.multivariate_normal(np.zeros(N), sigma_wahr / 252, size=T)
|
|
|
|
# (a) Stichproben-Kovarianz
|
|
s_stich = np.cov(daten, rowvar=False, ddof=1) * 252
|
|
w_stich = minimum_varianz_gewichte(s_stich)
|
|
|
|
# (b) Ledoit-Wolf-Shrinkage
|
|
lw = LedoitWolf().fit(daten)
|
|
s_lw = lw.covariance_ * 252
|
|
w_lw = minimum_varianz_gewichte(s_lw)
|
|
deltas.append(lw.shrinkage_)
|
|
|
|
# Bewertung IMMER mit der wahren Kovarianz - das ist der Punkt
|
|
vola_stichprobe.append(portfoliovolatilitaet(w_stich, sigma_wahr))
|
|
vola_lw.append(portfoliovolatilitaet(w_lw, sigma_wahr))
|
|
|
|
m_stich, m_lw = np.mean(vola_stichprobe), np.mean(vola_lw)
|
|
print(f"{T:>9} | {T/N:>5.1f} | {m_stich*100:>8.2f} % "
|
|
f"{(m_stich/vola_ideal-1)*100:>+9.1f} % | "
|
|
f"{m_lw*100:>8.2f} % {(m_lw/vola_ideal-1)*100:>+9.1f} % | "
|
|
f"{np.mean(deltas):>7.3f}")
|
|
|
|
print("-" * 92)
|
|
print("'Aufschlag' = wie viel mehr Risiko das Portfolio in der WAHREN Welt traegt,")
|
|
print("verglichen mit dem Portfolio bei perfektem Wissen.")
|
|
print("Lesart: Je kleiner T/N, desto teurer das Schaetzrauschen - und desto mehr")
|
|
print("schrumpft Ledoit-Wolf (delta steigt).")
|
|
print("=" * 92)
|