Kapitel 18: Finanzdaten-Modellierung — Renditen, Kovarianz und Shrinkage
📌 Kapitel auf einen Blick
Worum geht es? Um die Eingangsdaten der Portfoliooptimierung — und darum, warum sie trügerisch sind. Wer historische Kovarianzen ungefiltert in einen Optimierer gibt, optimiert Rauschen.
Voraussetzungen: Kapitel 11 (Eigenwerte, positive Definitheit), Grundlagen der Statistik.
Danach können Sie: Kursdaten korrekt laden, diskrete und logarithmische Renditen unterscheiden, das Schätzfehlerproblem erklären, Ledoit-Wolf-Shrinkage anwenden — und erkennen, wann eine Kovarianzmatrix aus zu wenigen Beobachtungen stammt.
Zeitbedarf: ca. 5 Stunden.
Programme:
Renditen_Vergleich.py
Schaetzrauschen_Demo.py
Finanzdaten_Ledoit_Wolf.py
Kovarianz_Falle.py⚠️ Überspringen Sie dieses Kapitel nicht. Wer direkt bei Markowitz (Kapitel 19) einsteigt, erhält absurde Portfoliogewichte und weiß nicht, warum.
Notebook: finanzdaten.ipynb
In Google Colab öffnen
18.1 In 5 Minuten gelöst
🚀 In 5 Minuten gelöst: Die durchschnittliche Rendite, die es nicht gibt
Eine Anlage wurde über vier Perioden beobachtet. Der Kurs stand bei 100, dann bei 50, 75, 150 und schließlich bei 120.
import numpy as np kurse = np.array([100.0, 50.0, 75.0, 150.0, 120.0]) diskret = kurse[1:] / kurse[:-1] - 1 # (neu - alt) / alt log = np.log(kurse[1:] / kurse[:-1]) # ln(neu / alt) print("diskret:", np.round(diskret * 100, 2), "%") print("log :", np.round(log * 100, 2), "%") print(f"Mittelwert diskret {diskret.mean()*100:5.2f} % -> hochgerechnet " f"{100 * (1 + diskret.mean())**4:6.2f}") print(f"Mittelwert log {log.mean()*100:5.2f} % -> hochgerechnet " f"{100 * np.exp(log.mean() * 4):6.2f}")Ausgabe:
diskret: [-50. 50. 100. -20.] % log : [-69.31 40.55 69.31 -22.31] % Mittelwert diskret 20.00 % -> hochgerechnet 207.36 Mittelwert log 4.56 % -> hochgerechnet 120.00
Die durchschnittliche Rendite beträgt 20 % pro Periode — bei einer Anlage, die insgesamt 20 % gewonnen hat. Wer damit vier Perioden hochrechnet, landet bei 207,36 statt bei den tatsächlichen 120,00.
Der Grund steckt in der ersten Zeile: Ein Verlust von 50 % und ein Gewinn von 50 % heben sich nicht auf. Aus 100 werden 50, daraus 75 — nicht wieder 100. Diskrete Renditen darf man deshalb nicht mitteln und nicht addieren; sie multiplizieren sich.
Logarithmische Renditen dagegen addieren sich. Ihre Summe beträgt hier 18,23 %, und 100 \cdot e^{0{,}1823} = 120{,}00 — exakt der beobachtete Endkurs.
| diskrete Rendite | logarithmische Rendite | |
|---|---|---|
| Verknüpfung über die Zeit | multiplikativ: \prod(1+r_t) | additiv: \sum \ln(1+r_t) |
| Mitteln zulässig? | nein — führt systematisch zu hoch | ja |
| Verknüpfung über Anlagen | additiv: \sum w_i r_i | nein (Summe von Logs ≠ Log der Summe) |
| Wofür also | Portfoliogewichtung, Kennzahlen je Periode | Zeitreihenanalyse, Volatilität, Modelle |
🎯 Merksatz Beide Renditearten sind richtig — für verschiedene Richtungen. Diskrete Renditen addieren sich über die Anlagen eines Portfolios, logarithmische über die Zeit. Wer sie verwechselt, bekommt keine Fehlermeldung, sondern eine plausible falsche Zahl.
Warum funktioniert das? Weil der Logarithmus aus einem Produkt eine Summe macht: \ln(a \cdot b) = \ln a + \ln b. Genau diese Eigenschaft braucht man, wenn sich Renditen über die Zeit aufmultiplizieren. Der Preis dafür: Über mehrere Anlagen hinweg funktioniert es nicht mehr, denn der Logarithmus einer Summe ist nicht die Summe der Logarithmen. Der Rest dieses Kapitels behandelt die zweite große Fehlerquelle bei Finanzdaten — die Kovarianzmatrix, deren Schätzung noch weit trügerischer ist als die Rendite selbst.
18.2 Lernziele
Nach diesem Kapitel können Sie …
- … erklären, wann man diskrete und wann logarithmische Renditen verwendet.
- … beschreiben, warum die Stichproben-Kovarianzmatrix bei vielen Titeln unbrauchbar wird.
- … den Error-Maximizer-Effekt an einem Experiment nachweisen.
- … Ledoit-Wolf-Shrinkage anwenden und ihre Wirkung an Eigenwerten und Konditionszahl messen.
- … Kursdaten so laden, dass die Spaltenreihenfolge garantiert stimmt.
- … begründen, warum eine Schätzung aus T < N Beobachtungen dem Optimierer risikofreie Richtungen vorgaukelt — und welche drei Gegenmittel es gibt.
- … ein Portfolio außerhalb des Schätzzeitraums bewerten statt darin.
18.3 Diskrete und logarithmische Renditen
Sei P_{i,t} der bereinigte Schlusskurs (adjusted close) von Titel i zum Zeitpunkt t. „Bereinigt“ heißt: um Dividenden und Aktiensplits korrigiert — sonst erscheint jede Dividendenzahlung als Kurssturz.
Diskrete Rendite
R_{i,t} = \frac{P_{i,t} - P_{i,t-1}}{P_{i,t-1}} = \frac{P_{i,t}}{P_{i,t-1}} - 1
Eigenschaft — über Titel additiv:
R_{p,t} = \sum_{i=1}^n w_i R_{i,t} = \mathbf{w}^\top\mathbf{R}_t
Logarithmische Rendite
r_{i,t} = \ln\!\left(\frac{P_{i,t}}{P_{i,t-1}}\right) = \ln P_{i,t} - \ln P_{i,t-1}
Eigenschaft — über die Zeit additiv:
r_{i,0\to T} = \sum_{t=1}^T r_{i,t}
🎯 Die Merkregel Diskrete Renditen addieren sich über das Portfolio (mehrere Titel, ein Zeitpunkt) — deshalb braucht man sie für die Allokation. Logarithmische Renditen addieren sich über die Zeit (ein Titel, mehrere Perioden) — deshalb braucht man sie für Zeitreihenanalyse und Volatilitätsschätzung.
Man kann nicht beides gleichzeitig haben: Die gewichtete Summe von Log-Renditen ist nicht die Log-Rendite des Portfolios.
✏️ Handrechnung 18.1: Der Unterschied in Zahlen
Ein Kurs steigt von 100 auf 150, dann fällt er zurück auf 100.
Periode 1 Periode 2 Summe Wahrheit Diskret +50{,}0\,\% -33{,}3\,\% +16{,}7\,\% ✗ 0\,\% Logarithmisch +0{,}4055 -0{,}4055 0{,}0000 ✓ 0\,\% Die diskreten Renditen suggerieren einen Gewinn von 16,7 %, obwohl der Kurs exakt dort steht, wo er begann. Die Log-Renditen heben sich korrekt auf.
Deshalb ist die Aussage „im Mittel 5 % Rendite“ mehrdeutig. Das arithmetische Mittel diskreter Renditen überschätzt systematisch, was ein Anleger tatsächlich verdient hätte. Für Wachstumsaussagen nimmt man das geometrische Mittel — oder rechnet gleich in Log-Renditen.
#!/usr/bin/env python3
# Renditen_Vergleich.py
"""
Kapitel Finanzdaten: Diskrete vs. logarithmische Renditen.
Zeigt an einem simulierten Kursverlauf, welche Eigenschaft wo gilt -
und warum das arithmetische Mittel diskreter Renditen in die Irre fuehrt.
"""
import numpy as np
import pandas as pd
if __name__ == "__main__":
kurse = np.array([100.0, 150.0, 100.0, 120.0, 90.0, 135.0])
tage = [f"t{i}" for i in range(len(kurse))]
diskret = kurse[1:] / kurse[:-1] - 1.0
logarithmisch = np.log(kurse[1:] / kurse[:-1])
print("=" * 74)
print(" DISKRETE UND LOGARITHMISCHE RENDITEN IM VERGLEICH")
print("=" * 74)
tabelle = pd.DataFrame({
"Periode": [f"{tage[i]} -> {tage[i+1]}" for i in range(len(diskret))],
"Kurs von": kurse[:-1],
"Kurs bis": kurse[1:],
"diskret R": [f"{r*100:+7.2f} %" for r in diskret],
"log r": [f"{r:+8.4f}" for r in logarithmisch],
})
print(tabelle.to_string(index=False))
# --- Zeitliche Aggregation ------------------------------------------
gesamt_wahr = kurse[-1] / kurse[0] - 1.0
summe_log = logarithmisch.sum()
aus_log_zurueck = np.exp(summe_log) - 1.0
summe_diskret = diskret.sum()
print("\n--- Aggregation ueber die Zeit ---")
print(f" Tatsaechliche Gesamtrendite: {gesamt_wahr*100:+8.2f} %")
print(f" Summe der Log-Renditen -> exp()-1: {aus_log_zurueck*100:+8.2f} % "
f"{'KORREKT' if abs(aus_log_zurueck - gesamt_wahr) < 1e-9 else 'falsch'}")
print(f" Summe der diskreten Renditen: {summe_diskret*100:+8.2f} % FALSCH")
# --- Mittelwerte ------------------------------------------------------
arithmetisch = diskret.mean()
geometrisch = np.prod(1 + diskret) ** (1 / len(diskret)) - 1
aus_log = np.exp(logarithmisch.mean()) - 1
print("\n--- Welcher Mittelwert ist der richtige? ---")
print(f" Arithmetisches Mittel (diskret): {arithmetisch*100:+8.2f} % "
f"-> ueberschaetzt")
print(f" Geometrisches Mittel: {geometrisch*100:+8.2f} % -> korrekt")
print(f" exp(Mittel der Log-Renditen) - 1: {aus_log*100:+8.2f} % "
f"-> identisch zum geometrischen")
# Probe: Endkapital mit dem jeweiligen Mittelwert hochgerechnet
n = len(diskret)
print(f"\n Probe - Startkapital 100 EUR ueber {n} Perioden:")
print(f" tatsaechlich: {kurse[-1]:8.2f} EUR")
print(f" mit arithm. Mittel hochgerechnet: {100*(1+arithmetisch)**n:8.2f} EUR")
print(f" mit geom. Mittel hochgerechnet: {100*(1+geometrisch)**n:8.2f} EUR")
print("\n--- Annualisierung (252 Handelstage) ---")
print(f" Volatilitaet aus Log-Renditen: "
f"{logarithmisch.std(ddof=1)*np.sqrt(252)*100:.2f} % p.a.")
print(" (Die Wurzel-Zeit-Regel gilt nur bei unabhaengigen Renditen -")
print(" fuer reale Maerkte ist sie eine Naeherung.)")
print("=" * 74)18.4 Das Schätzfehler-Problem
Sei \mathbf{X}\in\mathbb{R}^{T\times N} die Matrix zentrierter Renditen von N Titeln über T Handelstage. Die Stichproben-Kovarianzmatrix lautet:
\mathbf{S} = \frac{1}{T-1}\mathbf{X}^\top\mathbf{X}
Das Problem: \mathbf{S} hat \frac{N(N+1)}{2} zu schätzende Parameter, aber nur T \cdot N Datenpunkte. Bei N = 50 Titeln sind das 1275 Parameter — geschätzt aus einem Jahr Daten (T = 252).
| Verhältnis | Folge |
|---|---|
| T \gg N | \mathbf{S} ist brauchbar |
| T \approx N | Extrem instabil; Eigenwerte streuen weit |
| T < N | \mathbf{S} ist singulär — nicht invertierbar, Rang < N |
Die Zufallsmatrizentheorie (Random Matrix Theory, Marchenko-Pastur-Gesetz) zeigt: Die kleinsten Eigenwerte von \mathbf{S} werden systematisch unterschätzt, die größten überschätzt. Und genau das ist fatal, denn:
🎯 Der Error-Maximizer-Effekt Ein Risikominimierer sucht die Richtungen mit der kleinsten Varianz — also genau jene Eigenrichtungen, deren Eigenwerte am stärksten nach unten verzerrt sind. Die Optimierung greift damit zielsicher in das Schätzrauschen hinein. Markowitz-Optimierung ist deshalb kein Fehlerdämpfer, sondern ein Fehlerverstärker.
#!/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)Erwartete Ausgabe:
============================================================================================
ERROR-MAXIMIZER: WIE TEUER IST SCHAETZRAUSCHEN?
============================================================================================
Aufbau: 40 Titel, wahre Kovarianz bekannt (Ein-Faktor-Modell).
Bestmoegliche Volatilitaet bei perfektem Wissen: 11.84 % p.a.
T (Tage) | T/N | Stichprobe | Ledoit-Wolf | Shrink
| | Vola Aufschlag | Vola Aufschlag | delta
--------------------------------------------------------------------------------------------
60 | 1.5 | 20.78 % +75.5 % | 15.44 % +30.4 % | 0.093
120 | 3.0 | 14.42 % +21.8 % | 13.79 % +16.5 % | 0.048
252 | 6.3 | 12.86 % +8.7 % | 12.76 % +7.8 % | 0.024
504 | 12.6 | 12.33 % +4.1 % | 12.30 % +3.9 % | 0.012
1260 | 31.5 | 12.03 % +1.6 % | 12.02 % +1.6 % | 0.005
--------------------------------------------------------------------------------------------
Diese Tabelle ist das Kernargument des ganzen Kapitels. Lesen Sie die erste Zeile: Bei nur 60 Beobachtungen für 40 Titel trägt das „risikominimale“ Portfolio in Wahrheit 75,5 % mehr Risiko als nötig. Der Optimierer hat nicht das Risiko minimiert, sondern das geschätzte Risiko — und die Differenz ist der Schätzfehler, in den er hineinoptimiert hat.
Drei Beobachtungen:
- Der Schaden wächst dramatisch, je knapper die Daten. Von T/N = 31{,}5 (+1,6 %) bis T/N = 1{,}5 (+75,5 %) ist es kein gleitender Übergang, sondern eine Explosion.
- Shrinkage halbiert den Schaden dort, wo er groß ist (75,5 % → 30,4 %) und schadet dort nichts, wo er klein ist (1,6 % → 1,6 %). Sie ist damit praktisch risikolos einzusetzen.
- \delta passt sich automatisch an: viel Schrumpfung bei wenig Daten (0,093), fast keine bei vielen (0,005). Man muss nichts von Hand einstellen.
🎯 Die Praxisregel Faustregel für Aktienportfolios: T \ge 5N sollte man mindestens haben, besser T \ge 10N. Bei 50 Titeln sind das 250 bis 500 Handelstage — ein bis zwei Jahre. Wer weniger hat, sollte entweder das Universum verkleinern oder mit Shrinkage arbeiten. Am besten beides.
18.5 Ledoit-Wolf-Shrinkage
Olivier Ledoit und Michael Wolf lösten das Dilemma durch lineare Schrumpfung:
\boldsymbol{\Sigma}_{\text{LW}} = (1-\delta)\,\mathbf{S} + \delta\,\mathbf{F}
📐 Formel-Lesehilfe * \mathbf{S} — die Stichprobenmatrix: unverzerrt, aber verrauscht. * \mathbf{F} — das Schrumpfungsziel: verzerrt, aber stabil und rauschfrei. * \delta \in [0,1] — wie weit man vom Rauschen zur Struktur zieht.
Ohne Formel gesagt: Man mischt eine zappelige, aber im Mittel richtige Schätzung mit einer ruhigen, aber etwas schiefen. Die Mischung ist besser als beide Bestandteile einzeln — ein klassischer Bias-Varianz-Kompromiss. Ledoit und Wolf berechnen dabei dasjenige \delta, das den erwarteten quadratischen Fehler zur unbekannten wahren Matrix minimiert — analytisch, ohne Kreuzvalidierung.
Welches Ziel \mathbf{F}?
Ein häufiges Missverständnis: Es gibt nämlich mehrere Ledoit-Wolf-Varianten, und Text und Code müssen sich konsistent auf dieselbe beziehen:
| Ziel \mathbf{F} | Formel | Quelle | Wer benutzt es |
|---|---|---|---|
| Skalierte Einheitsmatrix | \mathbf{F} = \frac{\operatorname{tr}(\mathbf{S})}{N}\,\mathbf{I} | Ledoit/Wolf 2004, „A well-conditioned estimator“ | sklearn.covariance.LedoitWolf |
| Konstante Korrelation | f_{ii} = s_{ii}, f_{ij} = \bar{r}\sqrt{s_{ii}s_{jj}} | Ledoit/Wolf 2003, „Honey, I shrunk the sample covariance matrix“ | oft in der Praxis, z. B. PyPortfolioOpt |
| Ein-Faktor-Modell | aus Marktbeta geschätzt | Ledoit/Wolf 2003 | Faktormodell-Ansätze |
Wichtig ist, Text und Code konsistent auf dieselbe Variante zu beziehen — zum Beispiel sklearn.covariance.LedoitWolf, das die skalierte Einheitsmatrix verwendet, versus eine manuelle Implementierung des Konstant-Korrelations-Ziels. Dieses Buch nennt beide und macht transparent, welches wo zum Einsatz kommt.
💡 Welches Ziel ist besser? Es kommt darauf an. Die Einheitsmatrix unterstellt, dass alle Titel gleich riskant und unkorreliert sind — eine starke, aber sehr stabile Annahme. Das Konstant-Korrelations-Ziel behält die geschätzten Einzelvarianzen und glättet nur die Korrelationen; das ist bei Aktien meist realistischer, weil Titel tatsächlich sehr unterschiedliche Volatilitäten haben. In der Aufgabe Beide Shrinkage-Ziele vergleichen (Abschnitt 18.7) implementieren Sie es selbst und vergleichen.
Das Ergebnis ist in jedem Fall garantiert positiv definit, wohlkonditioniert und stabil invertierbar.
18.6 Praxis: Datenpipeline mit korrekter Spaltenreihenfolge
⚠️ Die wichtigste Zeile dieses Kapitels
yfinanceliefert die Kursspalten alphabetisch sortiert, nicht in der Reihenfolge Ihrer Ticker-Liste:Ihre Liste: ['AAPL', 'MSFT', 'NVDA', 'AMZN', 'JNJ', 'PFE', 'JPM', 'GS', 'XOM', 'CVX'] prices.columns: ['AAPL', 'AMZN', 'CVX', 'GS', 'JNJ', 'JPM', 'MSFT', 'NVDA', 'PFE', 'XOM']Wer danach mit Positionsindizes arbeitet, beschriftet Ergebnisse falsch und beschränkt falsche Sektoren. Die Abhilfe ist eine Zeile:
prices = raw["Close"][tickers].dropna() # erzwingt die eigene Reihenfolge assert list(prices.columns) == tickers
#!/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)⚠️ Zur Reproduzierbarkeit Dieses Programm lädt Live-Daten. Ihre Zahlen werden von den hier abgedruckten abweichen — je nach Abrufdatum. Das ist beabsichtigt und selbst eine Lektion: Wenn sich das optimale Portfolio zwischen zwei Abrufen deutlich ändert, obwohl nur ein paar Tage vergangen sind, sehen Sie den Schätzfehler direkt. Prüfen Sie das ruhig einmal.
18.7 Übungsaufgaben
Lösungen: Abschnitt A.18.
Aufgabe 18.1 ⭐ — Renditeart wählen. Welche Renditeart nehmen Sie und warum? (a) Portfoliorendite aus Einzelrenditen berechnen. (b) Volatilität über 5 Jahre schätzen. (c) Kumulierte Wertentwicklung über 10 Jahre darstellen. (d) Korrelationen zwischen Titeln schätzen.
Aufgabe 18.2 ⭐ — Mittelwertfalle. Ein Fonds meldet Jahresrenditen von +60\,\%, -40\,\%, +60\,\%, -40\,\%. (a) Wie hoch ist das arithmetische Mittel? (b) Wie viel Kapital hat ein Anleger nach 4 Jahren aus 10 000 €? (c) Welche jährliche Rendite entspricht dem tatsächlich?
Aufgabe 18.3 ⭐⭐ — Konditionszahl verstehen. Erzeugen Sie eine Kovarianzmatrix für N = 30 Titel aus T = 40, T = 100 und T = 1000 simulierten Beobachtungen. Berechnen Sie jeweils kleinsten Eigenwert und Konditionszahl. Ab welchem T wird die Matrix brauchbar?
Aufgabe 18.4 ⭐⭐ — Spaltenreihenfolge prüfen. Laden Sie fünf Ticker in einer bewusst unsortierten Reihenfolge. Zeigen Sie, dass raw["Close"].columns abweicht, und dass raw["Close"][tickers] das behebt. Was passiert, wenn ein Ticker nicht existiert?
Aufgabe 18.5 ⭐⭐⭐ — Beide Shrinkage-Ziele vergleichen. Implementieren Sie das Konstant-Korrelations-Ziel vollständig (inklusive eigener Berechnung von \delta nach Ledoit/Wolf 2003 oder per Kreuzvalidierung). Vergleichen Sie mit sklearn anhand: (a) der Konditionszahl, (b) der Out-of-Sample-Volatilität eines GMV-Portfolios (nach dem Muster von Schaetzrauschen_Demo.py), (c) der Stabilität der Gewichte über rollierende Fenster.
Aufgabe 18.6 ⭐⭐⭐ — Error-Maximizer selbst messen. Erweitern Sie Schaetzrauschen_Demo.py um eine dritte Variante: die Gleichgewichtung (w_i = 1/N), die überhaupt keine Schätzung benötigt. (a) Ab welchem T/N schlägt die Stichproben-Optimierung die Gleichgewichtung? (b) Ab welchem T/N schlägt Ledoit-Wolf sie? (c) Was folgt daraus für die Praxis?
18.8 Finde den Denkfehler
🐛 Finde den Denkfehler: Das risikofreie Portfolio
Ein Analyst stellt ein Portfolio aus 30 Titeln zusammen. Für die Kovarianzmatrix nimmt er das letzte Quartal — bei Wochendaten sind das 20 Beobachtungen. Dann minimiert er die Varianz.
Das Ergebnis begeistert ihn: Das Portfolio hat ein Risiko von 0,000 %. Er hat ein risikofreies Aktienportfolio gefunden.
Ihre Aufgabe: (a) Wie hoch ist der Rang einer Kovarianzmatrix, die aus T Beobachtungen für N > T Anlagen geschätzt wurde — und was folgt daraus geometrisch? (b) Warum findet ausgerechnet ein Minimum-Varianz-Optimierer diese Stelle so zuverlässig? (c) Sehen Sie sich unten die Spalte „Hebel“ an. Was sagt ein Wert von 3,8 über das Portfolio aus? (d) Warum schlägt die simple Gleichgewichtung den Optimierer — und was folgt daraus für Ihre eigenen Modelle, auch außerhalb der Finanzwelt?
Auflösung: Abschnitt A.18.
Das folgende Programm führt den Fall vor. Es arbeitet mit synthetischen Daten, damit das Ergebnis reproduzierbar ist und kein Internetzugang nötig wird — mit echten Kursdaten sieht es genauso aus.
#!/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 den Denkfehler im Kapitel Quadratische und
nichtlineare Optimierung).
"""
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)Erwartete Ausgabe:
==============================================================================
DIE KOVARIANZ-FALLE
==============================================================================
30 Anlagen, geschaetzt aus 20 Beobachtungen.
Rang der Stichprobenmatrix: 19 - noetig waeren 30.
Kleinster Eigenwert: -6.47e-20
Es gibt also Richtungen, in denen diese Matrix EXAKT null Varianz
misst. Genau dort sucht ein Minimum-Varianz-Optimierer.
Schaetzer Risiko im Zeitraum Risiko spaeter Hebel
------------------------------------------------------------------------------
Stichprobe (roh) 0.000 % 1.287 % 3.8
Ledoit-Wolf (d=0.34) 0.441 % 0.974 % 1.8
Gleichgewichtung 1/N 0.808 % 1.026 % 1.0
------------------------------------------------------------------------------
Drei Beobachtungen:
1. Die rohe Schaetzung meldet 0,000 % Risiko - und liefert spaeter das
SCHLECHTESTE Ergebnis der drei. Sie hat kein Portfolio optimiert,
sondern eine Luecke in den eigenen Daten gefunden.
2. Die Gleichgewichtung, die gar nicht optimiert, schlaegt den rohen
Optimierer deutlich. Das ist kein Zufallsbefund, sondern in der
Literatur breit belegt.
3. Ledoit-Wolf ist im Schaetzzeitraum EHRLICHER (0,441 statt 0,000 %)
und spaeter am besten. Der Hebel faellt von 3,8 auf 1,8 - das
Portfolio wird auch praktisch handelbarer.
Merksatz: Ein Optimierer glaubt seinen Eingabedaten vollstaendig.
Je mehr Freiheit Sie ihm geben, desto gruendlicher findet er deren
Fehler. Das gilt fuer Kovarianzmatrizen wie fuer Lieferzeiten,
Ausfallraten und jede andere geschaetzte Groesse.
==============================================================================
🔤 Formel-Übersetzer: warum N > T alles kaputt macht
Mathematik Alltagssprache \mathbf{S} = \frac{1}{T-1}\sum_{t}(\mathbf{r}_t - \bar{\mathbf{r}})(\mathbf{r}_t - \bar{\mathbf{r}})^\top „Wie stark schwanken die Anlagen — und wie sehr gemeinsam?“ \operatorname{rang}(\mathbf{S}) \le T - 1 „Aus 20 Beobachtungen lassen sich höchstens 19 unabhängige Richtungen ablesen.“ N = 30 > 19 „Es bleiben 11 Richtungen übrig, über die die Daten nichts sagen.“ $,
e 0: ^ op = 0$ | „In genau diesen Richtungen misst die Matrix null Risiko — nicht weil keines da ist, sondern weil sie blind ist.“ | > | \min_{\mathbf{w}} \mathbf{w}^ op\mathbf{S}\mathbf{w} | „Und der Optimierer sucht zielsicher genau dort.“ | > > Die Faustregel: Für eine halbwegs verlässliche Kovarianzschätzung braucht man T \gtrsim 10 \cdot N Beobachtungen. Bei 30 Titeln also rund 300 Perioden — mehr als ein Jahr Tagesdaten. Wer weniger hat, muss schrumpfen.
18.9 Micro-Quiz
❓ Micro-Quiz 18: Drei Fragen zum Selbstcheck
Genau eine Antwort ist jeweils richtig. Auflösung in Anhang A.
1. Ein Kurs fällt um 50 % und steigt danach um 50 %. Wo steht er? (a) Wieder beim Ausgangswert — die Prozente heben sich auf. (b) Bei 75 % des Ausgangswerts. Diskrete Renditen verknüpfen sich multiplikativ, nicht additiv: 1{,}0 \cdot 0{,}5 \cdot 1{,}5 = 0{,}75. (c) Bei 25 % des Ausgangswerts.
2. Wofür nimmt man logarithmische Renditen, wofür diskrete? (a) Logarithmische sind grundsätzlich genauer und daher immer vorzuziehen. (b) Logarithmische addieren sich über die Zeit, diskrete über die Anlagen eines Portfolios. Man wählt nach der Richtung, in der summiert wird. (c) Diskrete für kurze, logarithmische für lange Zeiträume.
3. Sie schätzen eine Kovarianzmatrix für 50 Anlagen aus 40 Wochenrenditen. Was ist zu tun? (a) Nichts — 40 Beobachtungen sind eine ordentliche Stichprobe. (b) Die Matrix ist singulär und gaukelt dem Optimierer risikofreie Richtungen vor. Entweder mehr Daten beschaffen oder einen Shrinkage-Schätzer verwenden, der positive Definitheit garantiert. (c) Die Anzahl der Anlagen auf 40 reduzieren, dann passt es.
18.10 Selbsttest
Antworten: Anhang A.
- Warum addieren sich Log-Renditen über die Zeit, diskrete aber über das Portfolio?
- Was besagt der Error-Maximizer-Effekt?
- Warum ist \mathbf{S} bei T < N singulär, und was folgt daraus praktisch?
- Was mischt Ledoit-Wolf, und wie wird \delta bestimmt?
- Welche zwei Shrinkage-Ziele gibt es, und welches verwendet
scikit-learn?
18.11 Zusammenfassung
- Diskret über das Portfolio, logarithmisch über die Zeit — die Wahl der Renditeart ist keine Geschmacksfrage.
- Das arithmetische Mittel diskreter Renditen überschätzt die tatsächliche Wertentwicklung systematisch.
- Die Stichprobenkovarianz ist bei T \approx N unbrauchbar und bei T < N singulär.
- Markowitz verstärkt Schätzfehler, statt sie auszugleichen — der Error-Maximizer-Effekt.
- Shrinkage mischt Rauschen mit Struktur und liefert eine stabil invertierbare Matrix. Es gibt mehrere Ziele;
sklearnverwendet die skalierte Einheitsmatrix. - Erzwingen Sie die Spaltenreihenfolge nach jedem Datenimport und prüfen Sie sie per
assert. - Faustregel für die Datenmenge: T \gtrsim 10 \cdot N. Bei 30 Titeln also rund 300 Perioden. Wer weniger hat, muss schrumpfen — oder die Freiheit des Optimierers begrenzen.
- Bewerten Sie außerhalb des Schätzzeitraums. Ein Portfolio, das im eigenen Datenfenster 0,000 % Risiko meldet, ist kein gutes Portfolio, sondern ein Beleg dafür, dass die Daten nicht ausreichen.
- Das gilt weit über Finanzdaten hinaus. Bearbeitungszeiten, Ausfallraten, Nachfrageprognosen: Ein Optimierer sucht nicht die beste Lösung, sondern den größten Fehler in Ihren Schätzungen.
Ausblick. Mit stabilen Schätzungen können wir in Kapitel 19 endlich optimieren: die Markowitz-Effizienzgrenze mit realistischen institutionellen Restriktionen.