Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten
📌 Kapitel auf einen Blick
Worum geht es? Um zwei Schwächen des Markowitz-Modells: Es unterschätzt Extremverluste und ignoriert die Kosten des Umschichtens. Beide lassen sich mit konvexen Mitteln beheben.
Voraussetzungen: Kapitel 19, Kapitel 5 (LP-Formulierungen).
Danach können Sie: VaR und CVaR unterscheiden, den CVaR nach Rockafellar/Uryasev als lineares Programm formulieren, Turnover über die L_1-Norm bestrafen — und begründen, warum VaR-Kennzahlen nicht über Einheiten addiert werden dürfen.
Zeitbedarf: ca. 5 Stunden.
Programme:
VaR_CVaR_Demo.py
CVaR_Portfolio.pyNotebook: cvar.ipynb — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten
20.1 In 5 Minuten gelöst
🚀 In 5 Minuten gelöst: Zwei Anlagen, dasselbe Risiko?
Zwei Anlagen wurden über 100 Tage beobachtet. An 94 Tagen legten beide um 0,5 % zu. Sie unterscheiden sich nur in den sechs schlechten Tagen:
- Anlage A: sechsmal −3 %.
- Anlage B: fünfmal −3 % — und einmal −40 %.
import numpy as np gut = np.full(94, 0.5) A = np.concatenate([gut, np.full(6, -3.0)]) B = np.concatenate([gut, np.full(5, -3.0), [-40.0]]) for name, r in [("Anlage A", A), ("Anlage B", B)]: schwelle = np.percentile(r, 5) # 5-%-Quantil var = -schwelle # Value at Risk cvar = -r[r <= schwelle].mean() # Mittel der schlimmsten 5 % print(f"{name}: VaR(95) {var:5.2f} % CVaR(95) {cvar:5.2f} %")Ausgabe:
Anlage A: VaR(95) 3.00 % CVaR(95) 3.00 % Anlage B: VaR(95) 3.00 % CVaR(95) 9.17 %
Der Value at Risk ist für beide Anlagen exakt gleich: 3,00 %. Ein Risikobericht, der nur den VaR ausweist, würde die beiden als gleich riskant einstufen.
Dabei kann Anlage B an einem einzigen Tag 40 % verlieren. Das ist kein Randfall, den man übersehen darf — es ist der Fall, wegen dessen es Risikomanagement gibt.
Der Grund für diese Blindheit steckt in der Definition:
| Maß | Was es beantwortet | Was es dabei übersieht |
|---|---|---|
| VaR (95 %) | „Welchen Verlust überschreite ich an höchstens 5 % der Tage?“ | Alles, was jenseits dieser Schwelle passiert |
| CVaR (95 %) | „Wie hoch ist der Verlust im Mittel, wenn es schiefgeht?“ | nichts im Schwanz |
Der VaR ist ein Quantil — er markiert eine Grenze und schaut nicht dahinter. Ob hinter der Grenze −3 % oder −40 % liegen, ändert ihn nicht. Der CVaR mittelt genau über diesen Bereich und macht den Unterschied sichtbar: 3,00 % gegen 9,17 %.
🎯 Merksatz Der VaR sagt Ihnen, wie oft es schiefgeht. Der CVaR sagt Ihnen, wie schlimm es dann ist. Für die Frage, ob ein Unternehmen einen Verlust überlebt, zählt ausschließlich die zweite.
Und es kommt noch besser. Der CVaR ist nicht nur aussagekräftiger, er ist auch mathematisch handlicher: Er lässt sich als lineares Programm minimieren, während die Minimierung des VaR ein nicht-konvexes Problem mit vielen lokalen Optima ist (Kapitel 11). Das ist ein seltener Glücksfall — das bessere Maß ist hier zugleich das leichter optimierbare. Wie das geht, zeigt Abschnitt 20.4.
20.2 Lernziele
Nach diesem Kapitel können Sie …
- … erklären, warum Marktrenditen keine Normalverteilung haben und was daraus folgt.
- … VaR und CVaR definieren und begründen, warum nur der CVaR kohärent ist.
- … das Rockafellar-Uryasev-Theorem anwenden, um den CVaR linear zu formulieren.
- … Transaktionskosten über die L_1-Norm modellieren.
- … Einheiten konsistent halten und Nebenbedingungen vektorisieren.
- … erklären, warum zwei Anlagen mit identischem VaR völlig verschiedene Extremverluste haben können.
- … die Folgen fehlender Subadditivität für die Verteilung von Risikobudgets benennen.
20.3 Die zwei Schwächen des Markowitz-Modells
Erstens: die Normalverteilungs-Illusion. Die Varianz behandelt Aufwärts- und Abwärtsschwankungen gleich und unterstellt implizit symmetrische, dünn auslaufende Verteilungen. Reale Marktrenditen haben aber fette Ränder (fat tails) und negative Schiefe: Extreme Verluste treten deutlich häufiger auf, als eine Normalverteilung vorhersagt.
Zweitens: Reibungsblindheit. Ein ungedämpftes Mean-Variance-Modell schichtet bei minimalen Schätzänderungen das gesamte Portfolio um. Ohne Berücksichtigung von Gebühren, Spreads und Steuern frisst der Umschlag (turnover) den theoretischen Mehrertrag auf.
#!/usr/bin/env python3
# VaR_CVaR_Demo.py
"""
Kapitel CVaR: Fat Tails, VaR und CVaR anschaulich.
Teil 1: Wie oft treten "unmoegliche" Tage wirklich auf?
Teil 2: Warum ist der VaR nicht subadditiv - ein Gegenbeispiel zum Nachrechnen.
"""
import numpy as np
from scipy import stats
def var_quantil(verluste: np.ndarray, alpha: float = 0.95) -> float:
"""VaR = Quantil der Verlustverteilung (Verluste positiv, Gewinne negativ)."""
return float(np.quantile(verluste, alpha))
def cvar_rockafellar(verluste: np.ndarray, alpha: float = 0.95) -> float:
"""
CVaR ueber die Rockafellar-Uryasev-Formel:
CVaR = min_gamma { gamma + 1/(1-alpha) * E[max(Verlust - gamma, 0)] }
WICHTIG: Der naheliegende Weg "Mittelwert aller Werte >= VaR" ist FALSCH,
sobald die Verteilung Atome hat (z. B. genau zwei moegliche Verluste).
Dann liegt der VaR selbst auf einem Atom, und der Vergleich '>=' erfasst
zu viel Wahrscheinlichkeitsmasse. Die Formel unten behandelt das korrekt -
und ist zugleich genau der Ausdruck, den wir im Abschnitt 'Value at Risk
und Conditional Value at Risk' optimieren.
"""
kandidaten = np.unique(verluste) # Optimum liegt immer auf einem Datenpunkt
return float(min(g + np.mean(np.maximum(verluste - g, 0.0)) / (1.0 - alpha)
for g in kandidaten))
if __name__ == "__main__":
rng = np.random.default_rng(2026)
# --- Teil 1: Fat Tails (analytisch, nicht simuliert) ------------------
print("=" * 88)
print(" TEIL 1: WIE OFT TRITT DAS 'UNMOEGLICHE' EIN?")
print("=" * 88)
print("Vergleich: Normalverteilung gegen t-Verteilung mit 3 Freiheitsgraden")
print("(beide auf Standardabweichung 1 normiert).\n")
t_verteilung = stats.t(df=3)
skalierung = t_verteilung.std() # auf Varianz 1 bringen
print(f"{'Ereignis':<22} {'Normal':>14} {'t (df=3)':>14} {'Faktor':>11} "
f"{'Normal: 1 Tag in':>18}")
print("-" * 88)
for k in [3, 4, 5, 6]:
p_normal = 2 * stats.norm.sf(k) # beidseitig
p_t = 2 * t_verteilung.sf(k * skalierung)
print(f"Abweichung > {k} Sigma {p_normal*100:>13.6f} % {p_t*100:>13.6f} % "
f"{p_t/p_normal:>10.1f}x {1/p_normal/252:>15,.0f} Jahre")
print("\nDeutung: Ein 5-Sigma-Tag ist unter Normalverteilung ein Ereignis von")
print("etwa einmal in 6.900 Jahren. Reale Aktienmaerkte liefern solche Tage")
print("mehrfach pro Jahrzehnt. Wer allein mit Varianz steuert, plant fuer")
print("eine Welt, in der Crashs praktisch nicht vorkommen.")
# --- Teil 2: VaR ist nicht subadditiv ---------------------------------
print("\n" + "=" * 88)
print(" TEIL 2: WARUM DER VaR KEIN KOHAERENTES RISIKOMASS IST")
print("=" * 88)
print("Zwei unabhaengige Anleihen, je 100 EUR Nominal.")
print("Jede faellt mit 4 % Wahrscheinlichkeit aus (Verlust 100),")
print("sonst zahlt sie 2 EUR Kupon (Verlust -2).\n")
ziehungen = 2_000_000
verlust_a = np.where(rng.random(ziehungen) < 0.04, 100.0, -2.0)
verlust_b = np.where(rng.random(ziehungen) < 0.04, 100.0, -2.0)
verlust_ab = verlust_a + verlust_b
print(f"{'':<28} {'VaR 95%':>12} {'CVaR 95%':>12}")
print("-" * 88)
werte = {}
for name, v in [("Anleihe A allein", verlust_a),
("Anleihe B allein", verlust_b),
("Portfolio A+B", verlust_ab)]:
werte[name] = (var_quantil(v), cvar_rockafellar(v))
print(f"{name:<28} {werte[name][0]:>12.2f} {werte[name][1]:>12.2f}")
var_summe = werte["Anleihe A allein"][0] + werte["Anleihe B allein"][0]
cvar_summe = werte["Anleihe A allein"][1] + werte["Anleihe B allein"][1]
print(f"{'Summe der Einzelwerte':<28} {var_summe:>12.2f} {cvar_summe:>12.2f}")
var_port, cvar_port = werte["Portfolio A+B"]
print("-" * 88)
print(f"VaR: Portfolio {var_port:7.2f} vs. Summe {var_summe:7.2f} -> "
f"{'VERLETZT die Subadditivitaet!' if var_port > var_summe else 'subadditiv'}")
print(f"CVaR: Portfolio {cvar_port:7.2f} vs. Summe {cvar_summe:7.2f} -> "
f"{'subadditiv (kohaerent)' if cvar_port <= cvar_summe + 1e-6 else 'verletzt'}")
print("\nDeutung: Einzeln betrachtet meldet der VaR fuer jede Anleihe einen")
print("GEWINN von 2 EUR - denn mit 96 % Wahrscheinlichkeit passiert nichts,")
print("und 4 % liegen unterhalb der 5-%-Schwelle. Im Portfolio steigt die")
print("Wahrscheinlichkeit mindestens eines Ausfalls auf 7,8 % und damit UEBER")
print("die Schwelle - der VaR springt auf 98. Er behauptet also, Streuung")
print("habe das Risiko erhoeht. Das ist oekonomisch unsinnig und der Grund,")
print("warum die Bankenaufsicht mit Basel III auf den Expected Shortfall")
print("umgestellt hat.")
print("=" * 88)Erwartete Ausgabe (gekürzt):
========================================================================================
TEIL 1: WIE OFT TRITT DAS 'UNMOEGLICHE' EIN?
========================================================================================
Ereignis Normal t (df=3) Faktor Normal: 1 Tag in
----------------------------------------------------------------------------------------
Abweichung > 3 Sigma 0.269980 % 1.384683 % 5.1x 1 Jahre
Abweichung > 4 Sigma 0.006334 % 0.616537 % 97.3x 63 Jahre
Abweichung > 5 Sigma 0.000057 % 0.323904 % 5649.8x 6,922 Jahre
Abweichung > 6 Sigma 0.000000 % 0.190127 % 963561.0x 2,011,101 Jahre
========================================================================================
TEIL 2: WARUM DER VaR KEIN KOHAERENTES RISIKOMASS IST
========================================================================================
VaR 95% CVaR 95%
----------------------------------------------------------------------------------------
Anleihe A allein -2.00 79.59
Anleihe B allein -2.00 79.66
Portfolio A+B 98.00 101.33
Summe der Einzelwerte -4.00 159.24
----------------------------------------------------------------------------------------
VaR: Portfolio 98.00 vs. Summe -4.00 -> VERLETZT die Subadditivitaet!
CVaR: Portfolio 101.33 vs. Summe 159.24 -> subadditiv (kohaerent)
========================================================================================
Die 6-Sigma-Zeile setzt die Sache ins Verhältnis: Unter Normalverteilung wäre ein solcher Tag ein Ereignis von einmal in zwei Millionen Jahren. Unter der t-Verteilung mit drei Freiheitsgraden — die realen Aktienrenditen deutlich näher kommt — passiert er etwa alle zwei Jahre. Der Faktor beträgt fast eine Million.
Und Teil 2 zeigt das Grundproblem des VaR an einem Beispiel, das Sie von Hand nachrechnen können: Einzeln meldet er für jede Anleihe einen Gewinn von 2 €, im Portfolio einen Verlust von 98 €. Diversifikation hätte demnach das Risiko um 102 € erhöht. Der CVaR dagegen verhält sich korrekt: 101,33 € im Portfolio gegenüber 159,24 € bei getrennter Betrachtung — die Streuung senkt das Risiko, wie es sein muss.
⚠️ Eine Falle bei der CVaR-Berechnung Der naheliegende Weg — „Mittelwert aller Verluste \ge VaR“ — ist falsch, sobald die Verteilung Atome hat (also einzelne Werte mit positiver Wahrscheinlichkeit, wie hier die zwei möglichen Ausgänge). Der VaR liegt dann selbst auf einem Atom, und der Vergleich
>=erfasst zu viel Wahrscheinlichkeitsmasse. Im Beispiel oben liefert dieser naive Schätzer für Anleihe A den Wert 2,08 statt der korrekten 79,59 — ein Fehler um Faktor 38.Die Rockafellar-Uryasev-Formel behandelt Atome von sich aus korrekt. Verwenden Sie sie auch dann, wenn Sie „nur schnell“ einen CVaR ausrechnen wollen.
20.4 Value at Risk und Conditional Value at Risk
Value at Risk (\text{VaR}_\alpha): Der Verlust, der mit Wahrscheinlichkeit \alpha nicht überschritten wird.
Conditional Value at Risk (\text{CVaR}_\alpha, auch Expected Shortfall): Der durchschnittliche Verlust in den schlimmsten (1-\alpha) Prozent der Fälle.
📐 Der Unterschied in einem Satz Der VaR sagt: „In 95 % der Tage verlieren Sie höchstens 1,86 %.“ Der CVaR sagt: „Und wenn es doch schiefgeht, verlieren Sie im Mittel 2,99 %.“
Beide Zahlen stammen aus der Verteilung oben — nachzurechnen mit
bilder_04/erzeuge_var_cvar.py.Der VaR ist eine Schwelle, der CVaR ein Mittelwert dahinter. Der VaR sagt nichts darüber, wie schlimm es hinter der Schwelle wird — ob dort 2,4 % oder 40 % stehen, ist ihm gleich. In der Stichprobe oben liegt der schlechteste Tag bei −23,0 %; am VaR von 1,86 % ändert dieser eine Tag nichts, am CVaR sehr wohl.
| Eigenschaft | VaR | CVaR |
|---|---|---|
| Berücksichtigt Verlusthöhe im Rand | ✗ nein | ✓ ja |
| Subadditiv (\rho(A+B) \le \rho(A)+\rho(B)) | ✗ nein | ✓ ja |
| Kohärentes Risikomaß | ✗ nein | ✓ ja |
| Konvex und optimierbar | ✗ nein | ✓ ja |
| Regulatorischer Standard | bis Basel II | ab Basel III |
⚠️ Eine verbreitete Ungenauigkeit Der CVaR wird manchmal „streng konvex“ genannt. Das ist zu stark: Der CVaR ist konvex, in der Szenario-Darstellung sogar stückweise linear — und damit gerade nicht streng konvex. Genau das ist sein praktischer Vorteil: Stückweise linear heißt, er lässt sich als lineares Programm lösen.
Das Rockafellar-Uryasev-Theorem (2000)
Rockafellar und Uryasev zeigten, dass sich der CVaR über S Szenarien exakt als lineares Programm formulieren lässt:
\text{CVaR}_\alpha(\mathbf{w}) = \min_{\gamma,\ \mathbf{u}\ \ge 0}\ \ \gamma + \frac{1}{S(1-\alpha)}\sum_{s=1}^S u_s
\text{u. d. N.}\qquad u_s \ \ge\ -\mathbf{R}_s^\top\mathbf{w} - \gamma \quad\forall s, \qquad u_s \ge 0 \quad\forall s
📐 Formel-Lesehilfe — der Trick in drei Schritten * \gamma ist eine Hilfsvariable, die im Optimum automatisch den VaR annimmt. Man muss ihn also nicht vorher kennen — das ist der eigentliche Durchbruch. * -\mathbf{R}_s^\top\mathbf{w} ist der Verlust im Szenario s (Rendite mit negativem Vorzeichen). * u_s \ge \text{Verlust}_s - \gamma zusammen mit u_s \ge 0 bedeutet: u_s = \max(\text{Verlust}_s - \gamma,\ 0) — der Überschuss über die Schwelle. Liegt der Verlust unter \gamma, ist u_s = 0 und das Szenario zählt nicht.
Ohne Formel gesagt: „Wähle eine Schwelle \gamma. Zähle für jedes Szenario, wie weit der Verlust darüber hinausgeht. Der CVaR ist die Schwelle plus der gemittelte Überschuss — und zwar für diejenige Schwelle, bei der diese Summe minimal wird.“
Warum u_s automatisch das Maximum wird: Die Zielfunktion minimiert die Summe der u_s. Jedes u_s wird also so klein wie möglich gedrückt — bis an die Grenze, die die beiden Ungleichungen erlauben. Das ist genau das Maximum der beiden Untergrenzen.
🔤 Formel-Übersetzer: jedes Zeichen der Zielfunktion
Die Lesehilfe oben erklärt den Trick. Hier steht, was die einzelnen Zeichen bedeuten — vor allem der Bruch, an dem die meisten Leser hängen bleiben:
Mathematik Alltagssprache \alpha Das Konfidenzniveau, üblich 0{,}95 oder 0{,}99. Nicht der Randanteil. 1-\alpha Der Randanteil: die schlechtesten 5 % (bzw. 1 %) der Fälle. Das ist der Teil, um den es geht. S Die Anzahl der durchgerechneten Szenarien — Handelstage, Simulationsläufe, historische Perioden. S(1-\alpha) Wie viele Szenarien im Rand liegen. Bei S = 2\,000 und \alpha = 0{,}95: genau 100. \frac{1}{S(1-\alpha)}\sum_s u_s Kein Mittelwert über alle Szenarien, sondern über die Randszenarien allein. Genau deshalb steht dort nicht \frac1S. \gamma Die Schwelle, ab der ein Verlust zum Randfall wird — im Optimum der VaR. \gamma + \frac{1}{S(1-\alpha)}\sum_s u_s „Schwelle plus durchschnittlicher Überschuss darüber“ — und das ist der CVaR. In einem Satz: Der CVaR ist der Mittelwert der schlimmsten 1-\alpha Prozent — der Bruch vor der Summe sorgt allein dafür, dass durch die richtige Anzahl geteilt wird.
⚠️ Die teuerste Verwechslung des Kapitels > \alpha und 1-\alpha zu vertauschen führt zu keiner Fehlermeldung. Das Modell > rechnet weiter, nur eben über die falsche Menge. An 2 000 simulierten Tagesrenditen mit > Fat Tails gemessen: > > | Rechnung | gemittelt über | CVaR | > | — | — | — | > | richtig (\alpha = 0{,}95, Rand 5 %) | 100 von 2 000 Szenarien | 3,43 % | > | vertauscht (Rand 95 %) | 1 900 von 2 000 Szenarien | 0,18 % | > > Ein Faktor 19 — und die zweite Zahl ist kein schlecht geschätztes Randrisiko, sondern > überhaupt kein Randmaß mehr: Sie mittelt über fast alle Tage und schließt die schlimmen > gerade nicht ein. Der größte Einzelverlust der Stichprobe beträgt 21,4 %. > > Die Gegenprobe kostet eine Zeile: Ist der berechnete CVaR nicht deutlich größer als > der mittlere Verlust, stimmt \alpha nicht.
20.5 Transaktionskosten über die L_1-Norm
Sei \mathbf{w}_{\text{alt}} das bestehende Portfolio und \mathbf{w} das neue Ziel. Der Umschlag (turnover) ist:
\text{Turnover} = \sum_{i=1}^N \lvert w_i - w_{\text{alt},i}\rvert = \lVert \mathbf{w}-\mathbf{w}_{\text{alt}}\rVert_1
📐 Formel-Lesehilfe Die L_1-Norm ist die Summe der Beträge. Sie misst, wie viel Prozent des Portfolios insgesamt bewegt werden — Käufe und Verkäufe zusammen.
Ohne Formel gesagt: „Wenn du 5 % von A verkaufst und 5 % von B kaufst, hast du 10 % Umschlag und zahlst darauf Gebühren.“
Warum L_1 und nicht L_2? Die L_1-Norm ist konvex (also optimierbar) und erzeugt zusätzlich dünn besetzte Änderungen: Sie bevorzugt wenige große Umschichtungen gegenüber vielen kleinen. Das entspricht genau dem, was man in der Praxis will — nicht 50 Kleinstorders mit je 3 € Mindestgebühr.
In CVXPY schreibt man einfach cp.norm1(w - w_alt); intern wird das in lineare Hilfsvariablen zerlegt.
20.6 Implementierung: CVaR-Portfolio mit Reibung
⚠️ Einheiten konsistent halten
Ein häufiger Fehler verrechnet eine annualisierte Rendite gegen einen täglichen CVaR:
mu = returns_df.mean().values * 252 # ANNUALISIERT cvar = gamma + (1/(S*(1-alpha))) * cp.sum(u) # TAEGLICH objective = cp.Maximize(mu @ w - lambda_risk * cvar - trans_costs)Der Risikoterm ist dadurch faktisch um Faktor 252 zu schwach gewichtet:
lambda_risk = 1.5bedeutet dann effektiv eine Risikoaversion von 1{,}5/252 \approx 0{,}006. Das Modell ist nicht falsch im Sinne von unlösbar — aber der Parameter bedeutet nicht, was er zu bedeuten scheint, und lässt sich daher nicht sinnvoll einstellen.Dieses Programm rechnet deshalb durchgehend auf Tagesbasis und annualisiert erst in der Ausgabe.
#!/usr/bin/env python3
# CVaR_Portfolio.py
"""
Kapitel CVaR: CVaR-Optimierung mit L1-Transaktionskosten via CVXPY.
Eigenschaften:
* Spaltenreihenfolge erzwungen
* Einheiten konsistent (alles taeglich, Annualisierung nur in der Ausgabe)
* Szenario-Nebenbedingungen VEKTORISIERT statt in einer Python-Schleife
(eine Matrixbedingung statt S einzelner Constraints - deutlich schneller)
* Vergleich CVaR- gegen Varianz-Optimierung
* Nachrechnung von VaR/CVaR aus den realisierten Szenarien
"""
import time
import cvxpy as cp
import numpy as np
import pandas as pd
import yfinance as yf
TICKER = ["AAPL", "MSFT", "NVDA", "AMZN", "JNJ", "PFE", "JPM", "GS", "XOM", "CVX"]
ALPHA = 0.95 # Konfidenzniveau: schlechteste 5 % der Tage
MAX_GEWICHT = 0.25
GEBUEHRENSATZ = 0.002 # 0,2 % je Einheit Turnover (Spread + Brokerage)
RISIKOAVERSION = 1.5 # bezogen auf TAEGLICHE Groessen
HANDELSTAGE = 252
def lade_renditen():
ende = pd.Timestamp.today().normalize()
start = ende - pd.DateOffset(years=2)
roh = yf.download(TICKER, 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):
kurse = roh["Close"][TICKER].dropna() # erzwingt eigene Spaltenreihenfolge
else:
kurse = roh[["Close"]].dropna()
kurse.columns = TICKER
assert list(kurse.columns) == TICKER, "Spaltenreihenfolge weicht ab!"
return kurse.pct_change().dropna()
def optimiere_cvar(R, w_alt, vektorisiert=True):
"""
Maximiere: taegliche Rendite - lambda * CVaR - Transaktionskosten
Alle Groessen TAEGLICH.
"""
S, N = R.shape
mu_taeglich = R.mean(axis=0)
w = cp.Variable(N, nonneg=True)
gamma = cp.Variable() # wird im Optimum zum VaR
u = cp.Variable(S, nonneg=True) # Ueberschuss ueber die Schwelle
cvar = gamma + (1.0 / (S * (1.0 - ALPHA))) * cp.sum(u)
turnover = cp.norm1(w - w_alt)
kosten = GEBUEHRENSATZ * turnover
ziel = cp.Maximize(mu_taeglich @ w - RISIKOAVERSION * cvar - kosten)
bedingungen = [cp.sum(w) == 1, w <= MAX_GEWICHT]
if vektorisiert:
# EINE Matrixbedingung statt S einzelner - deutlich schneller
bedingungen.append(u >= -(R @ w) - gamma)
else:
for s in range(S): # Alternative: S einzelne Constraints (langsamer)
bedingungen.append(u[s] >= -R[s] @ w - gamma)
problem = cp.Problem(ziel, bedingungen)
problem.solve()
if problem.status not in ("optimal", "optimal_inaccurate"):
raise SystemExit(f"CVaR-Problem nicht loesbar: {problem.status}")
return w.value, float(gamma.value), float(cvar.value), float(turnover.value)
def optimiere_varianz(R, w_alt):
"""Klassisches Mean-Variance zum Vergleich - ebenfalls taeglich gerechnet.
ACHTUNG, DCP-Falle: Die Standardabweichung ist hier NICHT als
cp.sqrt(cp.quad_form(w, sigma)) formulierbar. cp.sqrt ist konkav und
verlangt ein konkaves Argument; quad_form ist konvex - CVXPY lehnt den
Ausdruck mit einem DCPError ab, und zwar voellig zu Recht (Anhang
Fehlerdiagnose). cp.psd_wrap() hilft dagegen nicht: Es behebt eine
NUMERISCHE Beanstandung an sigma, keine Regelverletzung im Aufbau.
Der Standardweg ist die Cholesky-Zerlegung sigma = L L^T. Damit gilt
w' sigma w = ||L^T w||^2, also ist die Standardabweichung die 2-Norm
eines AFFINEN Ausdrucks - konvex und damit regelkonform.
"""
N = R.shape[1]
mu_taeglich = R.mean(axis=0)
sigma = np.cov(R, rowvar=False, ddof=1)
# Der winzige Diagonalzuschlag faengt den Fall ab, dass sigma numerisch
# nur halbdefinit ist (mehr Titel als Handelstage, doppelte Spalten).
L = np.linalg.cholesky(sigma + 1e-12 * np.eye(N))
w = cp.Variable(N, nonneg=True)
ziel = cp.Maximize(mu_taeglich @ w
- RISIKOAVERSION * cp.norm2(L.T @ w)
- GEBUEHRENSATZ * cp.norm1(w - w_alt))
problem = cp.Problem(ziel, [cp.sum(w) == 1, w <= MAX_GEWICHT])
problem.solve()
if problem.status not in ("optimal", "optimal_inaccurate"):
raise SystemExit(f"Varianz-Problem nicht loesbar: {problem.status}")
return w.value
def realisierte_kennzahlen(w, R):
"""VaR und CVaR direkt aus den Szenarien - unabhaengige Gegenprobe."""
portfoliorenditen = R @ w
verluste = -portfoliorenditen
var = float(np.quantile(verluste, ALPHA))
cvar = float(verluste[verluste >= var].mean())
return var, cvar, float(portfoliorenditen.mean()), float(portfoliorenditen.std(ddof=1))
if __name__ == "__main__":
renditen = lade_renditen()
R = renditen.values
S, N = R.shape
w_alt = np.ones(N) / N # Ausgangslage: Gleichgewichtung
t0 = time.perf_counter()
w_cvar, var_modell, cvar_modell, turnover = optimiere_cvar(R, w_alt, vektorisiert=True)
dauer_vektor = time.perf_counter() - t0
w_var = optimiere_varianz(R, w_alt)
print("=" * 90)
print(" CVaR-PORTFOLIO-OPTIMIERUNG MIT TRANSAKTIONSKOSTEN")
print("=" * 90)
print(f"Datenbasis: {S} Handelstage, {N} Titel | Konfidenzniveau "
f"{ALPHA*100:.0f} % | Loesungszeit {dauer_vektor:.2f} s\n")
# --- Gegenprobe: Modellwerte gegen realisierte Szenariowerte ---------
var_real, cvar_real, mu_real, sd_real = realisierte_kennzahlen(w_cvar, R)
print("--- Gegenprobe: stimmen Modell und Szenarien ueberein? ---")
print(f" VaR aus dem Modell (gamma): {var_modell*100:7.4f} % | "
f"aus den Szenarien: {var_real*100:7.4f} %")
print(f" CVaR aus dem Modell: {cvar_modell*100:7.4f} % | "
f"aus den Szenarien: {cvar_real*100:7.4f} %")
assert abs(cvar_modell - cvar_real) < 1e-4, "CVaR stimmt nicht mit den Szenarien!"
print(" -> Der Rockafellar-Uryasev-Trick liefert exakt den empirischen CVaR.")
# --- Kennzahlen beider Portfolios ------------------------------------
print(f"\n{'Portfolio':<24} {'Rendite p.a.':>13} {'Vola p.a.':>11} "
f"{'VaR 95% (Tag)':>15} {'CVaR 95% (Tag)':>16} {'Turnover':>10}")
print("-" * 90)
for name, w in [("CVaR-optimiert", w_cvar), ("Varianz-optimiert", w_var),
("Gleichgewichtung", w_alt)]:
v, c, m, s = realisierte_kennzahlen(w, R)
to = float(np.abs(w - w_alt).sum())
print(f"{name:<24} {m*HANDELSTAGE*100:>12.2f} % "
f"{s*np.sqrt(HANDELSTAGE)*100:>10.2f} % {v*100:>14.3f} % "
f"{c*100:>15.3f} % {to*100:>9.1f} %")
print("\nHinweis zur Annualisierung: Renditen werden mit 252 skaliert,")
print("Volatilitaeten mit sqrt(252). Fuer VaR/CVaR ist eine solche Skalierung")
print("nur unter starken Annahmen (Unabhaengigkeit, kein Drift) zulaessig -")
print("sie werden hier deshalb bewusst als TAGESwerte ausgewiesen.")
# --- Allokationstabelle ------------------------------------------------
print("\n--- Allokation ---")
tabelle = pd.DataFrame({
"Ticker": TICKER,
"vorher": [f"{v*100:5.1f} %" for v in w_alt],
"CVaR-opt.": [f"{v*100:5.1f} %" for v in w_cvar],
"Handel": [f"{(w_cvar[i]-w_alt[i])*100:+6.1f} %" for i in range(N)],
"Varianz-opt.": [f"{v*100:5.1f} %" for v in w_var],
})
print(tabelle.to_string(index=False))
print(f"\nTurnover {turnover*100:.1f} % -> Transaktionskosten "
f"{GEBUEHRENSATZ*turnover*100:.3f} % des Portfoliowerts")
# --- Laufzeitvergleich vektorisiert vs. Schleife ----------------------
if S <= 600: # bei sehr vielen Szenarien zu langsam
t0 = time.perf_counter()
optimiere_cvar(R, w_alt, vektorisiert=False)
dauer_schleife = time.perf_counter() - t0
print(f"\n--- Laufzeit: {S} Nebenbedingungen aufbauen ---")
print(f" vektorisiert (u >= -(R @ w) - gamma): {dauer_vektor:6.2f} s")
print(f" Schleife ueber Szenarien (V01): {dauer_schleife:6.2f} s "
f"({dauer_schleife/dauer_vektor:.1f}x langsamer)")
print("=" * 90)💻 Code-Durchgang: Vektorisierung
Die entscheidende Zeile ist
bedingungen.append(u >= -(R @ w) - gamma) # EINE Matrixbedingungstatt
for s in range(S): bedingungen.append(u[s] >= -R[s] @ w - gamma) # S einzelne BedingungenBeide beschreiben dasselbe Modell. Der Unterschied liegt im Aufbau: CVXPY muss im zweiten Fall 500 einzelne Ausdrucksbäume erzeugen, prüfen und zusammensetzen. Bei einem Backtest mit 60 Rebalancings (Kapitel 21) summiert sich das erheblich.
Die allgemeine Regel: Wenn Sie in einer Modellierungssprache eine Python-Schleife über Datenzeilen schreiben, prüfen Sie, ob sich dasselbe als Matrixoperation ausdrücken lässt. Der Solver rechnet ohnehin mit Matrizen — der Umweg über einzelne Ausdrücke kostet nur Aufbauzeit.
⚠️ Die Standardabweichung ist in CVXPY keine Wurzel
Im Vergleichsmodell
optimiere_varianz()steht die Standardabweichung alsL = np.linalg.cholesky(sigma + 1e-12 * np.eye(N)) risiko = cp.norm2(L.T @ w) # = sqrt(w' sigma w)und nicht als das Naheliegende,
cp.sqrt(cp.quad_form(w, sigma)). Der naheliegende Ausdruck ist nicht DCP und wird von CVXPY mit einemDCPErrorabgelehnt:cp.sqrtist konkav und verlangt deshalb ein konkaves Argument,quad_formist aber konvex. Die Regel wird verletzt, obwohl die Funktion als ganze mathematisch völlig harmlos ist — \sqrt{w^\top \Sigma w} ist konvex, DCP kann es nur nicht sehen.Wichtig ist die Fehlerdiagnose dahinter, weil sie leicht in die Irre geht:
cp.psd_wrap(sigma)sieht nach der Lösung aus und ist keine. Es unterdrückt die numerische Beanstandung, dass \Sigma nicht als positiv semidefinit erkannt wird — gegen die strukturelle Regelverletzung im Aufbau hilft es nicht.Der Ausweg ist immer derselbe: Zerlegen Sie \Sigma = LL^\top (Cholesky). Dann ist w^\top \Sigma w = \lVert L^\top w\rVert_2^2, und die Standardabweichung wird zur 2-Norm eines affinen Ausdrucks — konvex, regelkonform und für den Solver sogar die bessere Formulierung, weil sie direkt ein Kegelproblem ist. Der winzige Diagonalzuschlag 10^{-12} fängt den Fall ab, dass \Sigma numerisch nur halbdefinit ist (mehr Titel als Handelstage, duplizierte Spalten). Die vollständige Fehlertabelle steht in Anhang C.
💡 Zur Annualisierung von VaR und CVaR Ein häufiger Fehler skaliert den täglichen CVaR mit \sqrt{252}. Diese Wurzel-Zeit-Regel gilt streng nur für Standardabweichungen unabhängig identisch verteilter Größen ohne Drift. Der CVaR ist ein Erwartungswert über einen Verteilungsrand — für ihn ist die Regel eine grobe Näherung, die bei fetten Rändern und Autokorrelation systematisch danebenliegt.
Dieses Buch weist VaR und CVaR deshalb als Tageswerte aus und benennt die Skalierungsproblematik ausdrücklich. Wer Mehrtageshorizonte braucht, simuliert sie (Monte-Carlo aus Kapitel 12) statt zu skalieren.
20.7 Übungsaufgaben
Lösungen: Abschnitt A.20.
Aufgabe 20.1 ⭐ — VaR und CVaR ablesen. Zehn Tagesverluste (in %): -1{,}2, 0{,}3, 2{,}1, -0{,}8, 5{,}4, 1{,}1, -2{,}0, 0{,}6, 8{,}9, -0{,}4 (positiv = Verlust). Bestimmen Sie \text{VaR}_{80\%} und \text{CVaR}_{80\%} von Hand.
Aufgabe 20.2 ⭐ — Warum kohärent? Erklären Sie in eigenen Worten, warum ein Risikomaß subadditiv sein sollte. Was bedeutet es wirtschaftlich, wenn diese Eigenschaft verletzt ist?
Aufgabe 20.3 ⭐⭐ — Rockafellar-Uryasev nachvollziehen. Zeigen Sie für die drei Szenarien mit Verlusten (1, 4, 9) und \alpha = 2/3: (a) Was ist der empirische CVaR? (b) Werten Sie \gamma + \frac{1}{S(1-\alpha)}\sum_s \max(\text{Verlust}_s-\gamma, 0) für \gamma \in \{0, 1, 4, 5, 9\} aus. (c) Bei welchem \gamma ist der Ausdruck minimal, und stimmt das Minimum mit (a) überein?
Aufgabe 20.4 ⭐⭐ — Einheiten prüfen. Angenommen, ein Modell verrechnet Jahresrendite gegen Tages-CVaR (siehe die Warnung in Abschnitt 20.6). Berechnen Sie: Welchem „effektiven“ täglichen \lambda entspräche lambda_risk = 1.5 dadurch? Welchen Wert müsste man stattdessen setzen, um dieselbe Wirkung wie RISIKOAVERSION = 1.5 im konsistenten Tagesmodell zu erzielen?
Aufgabe 20.5 ⭐⭐⭐ — Risikoaversion kalibrieren. Variieren Sie RISIKOAVERSION von 0 bis 20 und tragen Sie Rendite, Volatilität, CVaR und Turnover gegeneinander auf. (a) Wie sieht die „Effizienzgrenze“ im Rendite-CVaR-Raum aus? (b) Bei welchem Wert entspricht das CVaR-Portfolio ungefähr dem Varianz-Portfolio? (c) Welchen Wert würden Sie einem konservativen Stiftungsfonds empfehlen — und wie begründen Sie ihn ohne Fachjargon?
Aufgabe 20.6 ⭐⭐⭐ — Turnover-Grenze statt Strafe. Ersetzen Sie den Kostenterm durch eine harte Grenze \lVert\mathbf{w}-\mathbf{w}_{\text{alt}}\rVert_1 \le \tau und variieren Sie \tau \in \{0{,}05;\ 0{,}1;\ 0{,}25;\ 0{,}5;\ 1{,}0\}. (a) Wie verändert sich die erreichbare Rendite? (b) Was ist der Vorteil einer Grenze gegenüber einer Strafe — und was der Nachteil? (c) Wann würden Sie was einsetzen?
20.8 Finde den Denkfehler
🐛 Finde den Denkfehler: Das Risikobudget, das durch Diversifikation stieg
Eine Bank vergibt Risikobudgets je Abteilung. Zwei Kreditabteilungen halten je eine Unternehmensanleihe über 100 € Nominal mit 4 % Ausfallwahrscheinlichkeit (bei Ausfall Totalverlust, sonst 2 € Kupon). Der Risikocontroller misst für jede einzeln:
\text{VaR}_{95}(\text{A}) = -2{,}00\,€, \qquad \text{VaR}_{95}(\text{B}) = -2{,}00\,€
Ein negativer Verlust, also ein Gewinn. Auf dem 95-%-Niveau erscheint jede Anleihe risikolos, beide Abteilungen bekommen grünes Licht. Der Controller notiert als Gesamtrisiko die Summe: -4{,}00\,€.
Dann werden die Positionen in einem gemeinsamen Buch zusammengelegt. Der VaR des Gesamtbuchs beträgt +98,00 €.
Ihre Aufgabe:
Wie kann die Zusammenlegung zweier unabhängiger Positionen das gemessene Risiko von -4\,€ auf +98\,€ treiben? Rechnen Sie die entscheidende Wahrscheinlichkeit aus.
Warum meldet der VaR für eine einzelne Anleihe einen Gewinn, obwohl sie mit 4 % Wahrscheinlichkeit vollständig ausfällt?
Welche Eigenschaft eines Risikomaßes wird hier verletzt? Was bedeutet das konkret für ein Unternehmen, das Risikobudgets auf Abteilungen verteilt und wieder zusammenzählt?
Der CVaR liefert 79,59 € und 79,66 € einzeln, aber nur 101,33 € zusammen — statt der Summe von 159,24 €. Warum kann ihm das Verhalten des VaR nicht passieren?
Die Zahlen stammen aus Teil 2 von
VaR_CVaR_Demo.pyoben. Auflösung: Abschnitt A.20.
⚠️ Eine Falle beim Nachrechnen
Wenn Sie den CVaR selbst nachrechnen wollen: Der naheliegende Weg
verluste[verluste >= var].mean()ist falsch, sobald die Verteilung Atome hat — und hier hat sie genau zwei mögliche Werte. Der VaR liegt dann selbst auf einem Atom, und der Vergleich>=erfasst weit mehr Wahrscheinlichkeitsmasse als die vorgesehenen 5 %.Deshalb verwendet
cvar_rockafellar()oben die Optimierungsformel statt der naiven Mittelung. Wer stattdessen exakt abzählt, muss die schlechtesten \lceil (1-\alpha)\,N \rceil Szenarien nehmen — nicht alle, die eine Schwelle überschreiten.Diese Falle ist besonders unangenehm, weil das falsche Ergebnis plausibel aussieht. Sie fällt nur auf, wenn man wie hier eine unabhängige Gegenprobe hat: Ein CVaR, der die Subadditivität verletzt, kann nicht stimmen.
🎯 Merksatz Ein Risikomaß, dessen Werte man nicht addieren darf, ist als Steuerungsgröße unbrauchbar — denn genau das tut jede Organisation mit Kennzahlen: Sie verteilt sie auf Einheiten und zählt sie wieder zusammen. Der CVaR ist kohärent und erlaubt das; der VaR nicht. Aus diesem Grund hat die Bankenaufsicht mit Basel III vom VaR auf den Expected Shortfall umgestellt — dieselbe Größe, die hier CVaR heißt.
20.9 Micro-Quiz
❓ Micro-Quiz 20: Drei Fragen zum Selbstcheck
Genau eine Antwort ist jeweils richtig. Auflösung in Anhang A.
1. Zwei Anlagen haben denselben VaR(95) von 3 %. Was wissen Sie über ihre Extremverluste? (a) Sie sind gleich hoch — der VaR misst genau das. (b) Nichts. Der VaR ist ein Quantil: Er markiert die Schwelle und sagt nichts darüber, was dahinter liegt. Die eine kann dort −3 %, die andere −40 % haben. (c) Die Anlage mit der höheren Schwankung hat auch die höheren Extremverluste.
2. Warum lässt sich der CVaR als lineares Programm minimieren, der VaR aber nicht? (a) Der VaR ist nicht differenzierbar. (b) Der CVaR lässt sich nach Rockafellar/Uryasev über eine Hilfsvariable und Schlupfterme als konvexes — sogar lineares — Problem schreiben. Die VaR-Minimierung ist dagegen nicht konvex und hat viele lokale Optima. (c) Der CVaR ist immer größer als der VaR.
3. Sie sollen das Risiko von drei Handelsbüchern zu einer Gesamtkennzahl zusammenfassen. Welches Maß nehmen Sie? (a) VaR — er ist in der Praxis verbreiteter und leichter zu erklären. (b) CVaR, weil er subadditiv ist: Die Gesamtkennzahl kann nie größer werden als die Summe der Einzelkennzahlen, Diversifikation wird also korrekt belohnt. (c) Beide liefern dasselbe, solange die Bücher unabhängig sind.
20.10 Selbsttest
Antworten: Anhang A.
- Was misst der CVaR, was der VaR nicht misst?
- Warum ist der CVaR konvex, aber nicht streng konvex?
- Welche Rolle spielt die Hilfsvariable \gamma bei Rockafellar/Uryasev?
- Warum bevorzugt die L_1-Strafe wenige große statt vieler kleiner Umschichtungen?
- Warum sollte man tägliche und annualisierte Größen nicht in einer Zielfunktion mischen?
20.11 Zusammenfassung
- Reale Renditen haben fette Ränder. Ein 5-Sigma-Tag ist unter Normalverteilung ein Jahrtausendereignis — an echten Märkten passiert er alle paar Jahre.
- Der VaR ist ein Quantil und schaut nicht dahinter. Zwei Anlagen mit identischem VaR können Extremverluste von −3 % und −40 % haben. Der VaR sagt, wie oft es schiefgeht; der CVaR, wie schlimm es dann ist.
- Der VaR ist nicht kohärent: Er kann Diversifikation als Risikoerhöhung ausweisen. Deshalb die regulatorische Umstellung auf den Expected Shortfall. Praktische Folge: Man darf VaR-Kennzahlen nicht über Einheiten addieren — und genau das tut jede Organisation mit Kennzahlen.
- Das bessere Maß ist hier zugleich das leichter optimierbare — ein seltener Glücksfall. Der CVaR wird zum LP, die VaR-Minimierung bleibt nicht-konvex.
- Rockafellar/Uryasev machen den CVaR über eine Hilfsvariable \gamma linear optimierbar, ohne den VaR vorher zu kennen.
- Transaktionskosten gehören ins Modell, nicht in eine nachgelagerte Rechnung — sonst schichtet der Optimierer bei jedem Rauschen um.
- Einheiten konsistent halten. Tägliche Renditen zu täglichem Risiko; annualisiert wird erst in der Ausgabe.
- Vektorisieren Sie Szenario-Bedingungen — das spart bei wiederholten Läufen erheblich Zeit.
Ausblick. Kapitel 21 fügt alles zusammen: Datenpipeline, Signal, Optimierung, Rebalancing und ein Walk-Forward-Backtest ohne Lookahead-Bias.