Kapitel 19: Die moderne Portfoliotheorie nach Markowitz

📌 Kapitel auf einen Blick

Worum geht es? Um das Modell, das 1952 die quantitative Finanzwirtschaft begründete — und um die Restriktionen, ohne die es in der Praxis unbrauchbar bleibt.

Voraussetzungen: Kapitel 11 (QP, KKT), Kapitel 18 (Kovarianz, Shrinkage).

Danach können Sie: Die Effizienzgrenze berechnen, das Maximum-Sharpe-Portfolio über die Korn-Transformation bestimmen, institutionelle Nebenbedingungen einbauen — und beurteilen, wann eine Renditeschätzung das Ergebnis wertlos macht.

Zeitbedarf: ca. 6 Stunden.

Programme:
Diversifikation_Demo.py
Markowitz_CVXPY.py
Renditeschaetzung_Falle.py

Notebook: markowitz.ipynb — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten


19.1 In 5 Minuten gelöst

🚀 In 5 Minuten gelöst: Die Effizienzlinie in acht Zeilen

Ein Anleger verteilt sein Vermögen auf drei Anlageklassen:

erwartete Rendite Schwankung
Anleihen 3,0 % 5 %
Aktien Welt 8,0 % 18 %
Immobilien 5,5 % 11 %

Statt einer Antwort rechnen wir gleich die ganze Auswahl durch — für jede Zielrendite das Portfolio mit dem geringsten Risiko:

import numpy as np
import cvxpy as cp

mu = np.array([0.03, 0.08, 0.055])                    # erwartete Rendite
sig = np.array([0.05, 0.18, 0.11])                    # Schwankung
R = np.array([[1.0, 0.1, 0.2], [0.1, 1.0, 0.5], [0.2, 0.5, 1.0]])
S = np.outer(sig, sig) * R                            # [Kovarianzmatrix](anhang-glossar.md#gloss:kovarianzmatrix){.glossar-link}

w, ziel = cp.Variable(3, nonneg=True), cp.[Parameter](anhang-glossar.md#gloss:parameter){.glossar-link}(nonneg=True)
problem = cp.Problem(cp.Minimize(cp.quad_form(w, S)),
                     [cp.sum(w) == 1, mu @ w >= ziel])

for z in [0.03, 0.04, 0.05, 0.06, 0.07, 0.08]:
    ziel.value = z
    problem.solve()
    risiko = np.sqrt(problem.value)
    print(f"{z*100:5.1f}% {risiko*100:7.2f}% {(mu @ w.value)/risiko:8.2f}   "
          f"{np.round(w.value, 3)}")

Ausgabe (Zielrendite, Risiko, Rendite-je-Risiko, Gewichte):

  3.0%    4.83%     0.69   [0.884 0.017 0.098]
  4.0%    5.35%     0.75   [0.711 0.111 0.178]
  5.0%    7.49%     0.67   [0.451 0.251 0.298]
  6.0%   10.35%     0.58   [0.191 0.391 0.418]
  7.0%   13.55%     0.52   [0.    0.6   0.4  ]
  8.0%   18.00%     0.44   [0.    1.    0.   ]

Diese sechs Zeilen sind die Effizienzlinie, und sie beantworten die Frage anders, als die meisten sie stellen. Es gibt kein „bestes“ Portfolio — es gibt eine Auswahl, und jede Zeile ist für ihre Zielrendite optimal.

Lesen Sie die dritte Spalte: Sie zeigt, wie viel Rendite je Einheit Risiko herausspringt (die Sharpe-Kennzahl). Sie ist nicht unten am höchsten:

  • Bei 3 % Zielrendite: 0,69 — das Portfolio ist sicher, aber lässt Ertrag liegen.
  • Bei 4 %: 0,75 — das beste Verhältnis.
  • Bei 8 %: 0,44 — das renditestärkste Portfolio ist das schlechteste je Risikoeinheit. Und es besteht zu 100 % aus Aktien: Wer die höchste Rendite fordert, bekommt zwangsläufig gar keine Diversifikation mehr.

Von 3 % auf 8 % Rendite ist ein Zuwachs um Faktor 2,7 — das Risiko steigt dabei um Faktor 3,7. Die letzten Prozentpunkte Rendite sind die teuersten.

🎯 Merksatz Ein Optimierer beantwortet nicht die Frage „was soll ich tun?“, sondern „was ist erreichbar?“. Die Effizienzlinie ist eine Speisekarte, keine Empfehlung. Welche Zeile die richtige ist, hängt von der Risikotragfähigkeit ab — und das ist keine mathematische, sondern eine unternehmerische Frage.

Warum funktioniert das? Weil cp.Parameter das Modell nur einmal aufbaut und dann sechsmal mit verschiedenen Zielwerten löst. Genau dafür gibt es Parameter in CVXPY: Bei 200 Punkten auf der Linie spart das den 200-fachen Kompilierungsaufwand (Abschnitt 3.8). Der Rest dieses Kapitels führt die Effizienzlinie sauber aus, zeigt, wie man das beste Verhältnis direkt berechnet statt es abzulesen — und warum das Ganze mit geschätzten Renditen gefährlicher ist, als es hier aussieht.


19.2 Lernziele

Nach diesem Kapitel können Sie …

  1. … erklären, warum Diversifikation Risiko senkt, ohne Rendite zu kosten.
  2. … das Mean-Variance-Modell aufstellen und den Risikoparameter \lambda deuten.
  3. … die Effizienzgrenze berechnen und korrekt begrenzen.
  4. … die Korn-Transformation anwenden, um die nicht-konvexe Sharpe-Ratio-Maximierung in ein QP zu überführen.
  5. … Positions-, Sektor- und Kardinalitätsgrenzen einbauen — mit Namen statt Indizes.
  6. … die Effizienzlinie als Auswahl lesen statt als Empfehlung.
  7. … begründen, warum das Minimum-Varianz-Portfolio in der Praxis oft robuster ist als das Maximum-Sharpe-Portfolio.

19.3 Warum Diversifikation funktioniert

Harry Markowitz’ zentrale Einsicht von 1952: Das Risiko eines Portfolios ist nicht die Summe der Einzelrisiken. Es hängt davon ab, wie die Titel zusammenspielen.

Für zwei Titel gilt:

\sigma_p^2 = w_1^2\sigma_1^2 + w_2^2\sigma_2^2 + 2w_1w_2\,\rho_{12}\,\sigma_1\sigma_2

📐 Formel-Lesehilfe * Die ersten beiden Terme sind die „eigenen“ Risikobeiträge. * Der dritte Term enthält die Korrelation \rho_{12} — und nur er kann negativ werden.

Ohne Formel gesagt: Wenn zwei Anlagen sich gegenläufig bewegen (\rho < 0), gleichen sich ihre Schwankungen teilweise aus. Das Portfolio schwankt dann weniger als jeder Einzeltitel — bei einer Rendite, die genau dem gewichteten Mittel entspricht. Diversifikation ist damit das einzige „Gratis-Mittagessen“ der Finanzwirtschaft.

✏️ Handrechnung 19.1: Das Gratis-Mittagessen in Zahlen

Zwei Titel, beide mit \mu = 8\,\% und \sigma = 20\,\%, Gewichtung 50/50. Die Portfoliorendite ist immer 8\,\% — unabhängig von der Korrelation. Das Risiko dagegen:

\sigma_p = \sqrt{0{,}25\cdot0{,}04 + 0{,}25\cdot0{,}04 + 2\cdot0{,}25\cdot\rho\cdot0{,}04} = 0{,}2\sqrt{0{,}5(1+\rho)}

\rho \sigma_p Risikoreduktion
+1{,}0 20{,}0\,\% 0 % (keine Diversifikation)
+0{,}5 17{,}3\,\% 13 %
0{,}0 14{,}1\,\% 29 %
-0{,}5 10{,}0\,\% 50 %
-1{,}0 0{,}0\,\% 100 % (perfekte Absicherung)

Bei gleicher Rendite von 8 %. Genau das meint Markowitz: Risiko lässt sich reduzieren, ohne dafür Ertrag aufzugeben — allein durch die richtige Kombination.

Die Kehrseite: In Krisen steigen Korrelationen. Ein Portfolio, dessen Diversifikation auf \rho = 0{,}2 beruht, kann bei \rho \to 0{,}9 plötzlich fast unkorrelierte Absicherung verlieren — genau dann, wenn man sie braucht. Kapitel 20 und Kapitel 22 kommen darauf zurück.

Die Tabelle aus der Handrechnung Das Gratis-Mittagessen in Zahlen lässt sich auf zwei unabhängigen Wegen prüfen: über die analytische Formel und über eine Simulation korrelierter Renditepaare, die von der Formel nichts weiß.

#!/usr/bin/env python3

# Diversifikation_Demo.py
"""
Kapitel Markowitz: Portfoliorisiko in Abhaengigkeit von der Korrelation - die
Handrechnung zum Gratis-Mittagessen, geprueft ueber die analytische Formel und
ueber simulierte, korrelierte Renditen.
"""

import numpy as np

MU = 0.08
SIGMA = 0.20


def sigma_portfolio(rho: float, w1: float = 0.5) -> float:
    """Analytische Formel: sigma_p^2 = w1^2 s1^2 + w2^2 s2^2 + 2 w1 w2 rho s1 s2."""
    w2 = 1 - w1
    varianz = w1**2 * SIGMA**2 + w2**2 * SIGMA**2 + 2 * w1 * w2 * rho * SIGMA**2
    return float(np.sqrt(varianz))


def simuliere(rho: float, n: int = 500_000, seed: int = 7) -> float:
    """Erzeugt n korrelierte Renditepaare und misst die Portfolio-Volatilitaet empirisch."""
    rng = np.random.default_rng(seed)
    kovarianz = np.array([[SIGMA**2, rho * SIGMA**2],
                           [rho * SIGMA**2, SIGMA**2]])
    renditen = rng.multivariate_normal([MU, MU], kovarianz, size=n)
    portfolio = 0.5 * renditen[:, 0] + 0.5 * renditen[:, 1]
    return float(portfolio.std(ddof=1))


if __name__ == "__main__":
    print("=" * 78)
    print("  PORTFOLIORISIKO IN ABHAENGIGKEIT VON DER KORRELATION")
    print("=" * 78)
    print(f"{'rho':>6} | {'sigma_p (Formel)':>18} | {'sigma_p (Simulation)':>20} | {'Risikoreduktion':>16}")
    print("-" * 78)
    for rho in [1.0, 0.5, 0.0, -0.5, -1.0]:
        formel = sigma_portfolio(rho)
        sim = simuliere(rho)
        reduktion = (1 - formel / SIGMA) * 100
        print(f"{rho:6.1f} | {formel*100:16.2f} % | {sim*100:18.2f} % | {reduktion:14.0f} %")

    print("\nDie simulierten Werte (500.000 gezogene Renditepaare je rho) bestaetigen")
    print("die Formel aus der Handrechnung bis auf statistisches Rauschen - und das,")
    print("obwohl Formel und Simulation nichts voneinander wissen.")

Erwartete Ausgabe:

==============================================================================
  PORTFOLIORISIKO IN ABHAENGIGKEIT VON DER KORRELATION
==============================================================================
   rho |   sigma_p (Formel) | sigma_p (Simulation) |  Risikoreduktion
------------------------------------------------------------------------------
   1.0 |            20.00 % |              20.01 % |              0 %
   0.5 |            17.32 % |              17.33 % |             13 %
   0.0 |            14.14 % |              14.13 % |             29 %
  -0.5 |            10.00 % |               9.99 % |             50 %
  -1.0 |             0.00 % |               0.00 % |            100 %

Die simulierten Werte (500.000 gezogene Renditepaare je rho) bestaetigen
die Formel aus der Handrechnung bis auf statistisches Rauschen - und das,
obwohl Formel und Simulation nichts voneinander wissen.

19.4 Das Mean-Variance-Modell

Sei \mathbf{w} = (w_1,\dots,w_n)^\top der Vektor der Portfoliogewichte:

\min_{\mathbf{w}} \quad \tfrac12\,\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w} \;-\; \lambda\,\boldsymbol{\mu}^\top\mathbf{w}

\text{u. d. N.}\qquad \sum_{i=1}^n w_i = 1,\qquad w_i \ge 0\ \ \forall i

📐 Formel-Lesehilfe * \tfrac12\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w} — die halbe Portfoliovarianz (das Risiko). * \lambda\,\boldsymbol{\mu}^\top\mathbf{w} — die mit \lambda gewichtete erwartete Rendite. Weil sie abgezogen wird, wird hohe Rendite belohnt. * \sum w_i = 1 — das Kapital wird vollständig investiert. * w_i \ge 0 — keine Leerverkäufe (long only).

Die Rolle von \lambda:

\lambda Verhalten Ergebnis
0 Risiko zählt allein GMV — Global Minimum Variance Portfolio
mittel Abwägung ein Punkt auf der Effizienzgrenze
\to\infty Rendite zählt allein alles in den Titel mit höchstem \mu

Ohne Formel gesagt: „Streue so, dass das Risiko klein bleibt — aber lass dir dafür nicht zu viel Rendite entgehen. Wie viel Rendite dir das Risiko wert ist, sagt \lambda.“

🔤 Formel-Übersetzer: die Matrixschreibweise auflösen

Der Sprung von der Zweititelformel weiter oben zu \mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w} sieht aus wie ein neues Konzept. Er ist keines — es ist dieselbe Formel, nur nicht mehr ausgeschrieben:

Mathematik Alltagssprache
\mathbf{w} „Wie viel Prozent liegt in welchem Titel?“ Ein Vektor mit einem Eintrag je Titel.
\boldsymbol{\Sigma} Die Tabelle „wie bewegen sich je zwei Titel zueinander?“. Auf der Diagonale steht das Eigenrisiko \sigma_i^2, daneben das Zusammenspiel \sigma_{ij}.
\boldsymbol{\mu} „Was erwarte ich je Titel an Rendite?“
\boldsymbol{\mu}^\top\mathbf{w} Das gewichtete Mittel der Renditen — die Portfoliorendite. Genau das, was jeder von Hand ausrechnen würde.
\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w} Die Doppelsumme über alle Paare: \sum_i\sum_j w_i w_j \sigma_{ij}. Bei n Titeln sind das n^2 Summanden — bei 5 Titeln 25, bei 500 Titeln 250 000. Deshalb schreibt man es als Matrixprodukt.

Die Probe. Setzen Sie n = 2 in die Doppelsumme ein: Sie erhalten w_1^2\sigma_1^2 + w_2^2\sigma_2^2 + 2w_1w_2\sigma_{12} — Buchstabe für Buchstabe die Formel aus Abschnitt 19.3. Die Matrixschreibweise führt nichts Neues ein; sie erspart nur das Ausschreiben.

In Zahlen. \sigma_1 = 20\,\%, \sigma_2 = 30\,\%, \rho = -0{,}4, Gewichte (0{,}6;\ 0{,}4):

Beitrag Wert
Eigenrisiken (die Diagonale) 0{,}0288
Zusammenspiel (alles daneben) -0{,}0115
Summe =\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w} \mathbf{0{,}0173}
Portfoliorisiko \sigma_p = \sqrt{\;\cdot\;} \mathbf{13{,}15\,\%}

In einem Satz: Die Diagonale von \boldsymbol{\Sigma} ist das Risiko, das jeder Titel mitbringt; alles daneben ist das, was Diversifikation daraus macht — und nur dieser zweite Teil kann negativ werden. 13,15 % liegen unter beiden Einzelrisiken, und das ist keine Näherung, sondern das exakte Ergebnis der Formel.

Die Effizienzgrenze und das Tangentialportfolio

Die Effizienzgrenze (efficient frontier) ist die Kurve aller Portfolios, die zu jedem Risikoniveau die höchstmögliche Rendite liefern.

Abb. 19.1: Efficient Frontier und Tangential-Portfolio

Das Tangentialportfolio maximiert die Sharpe Ratio:

\text{SR}(\mathbf{w}) = \frac{\mathbf{w}^\top\boldsymbol{\mu} - r_f}{\sqrt{\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w}}}

📐 Formel-Lesehilfe Zähler: Überrendite über den risikolosen Zins r_f. Nenner: die Volatilität.

Ohne Formel gesagt: „Wie viel Mehrertrag bekomme ich je Einheit Risiko, die ich eingehe?“ Eine Sharpe Ratio von 0,8 heißt: Für jeden Prozentpunkt Volatilität gibt es 0,8 Prozentpunkte Überrendite.

Die Korn-Transformation

Die Sharpe Ratio ist ein Bruch — und damit nicht konvex. CVXPY würde sie ablehnen. Der Ausweg ist ein eleganter Trick, der die Homogenität des Problems ausnutzt:

\min_{\mathbf{y},\kappa}\ \tfrac12\,\mathbf{y}^\top\boldsymbol{\Sigma}\mathbf{y} \qquad\text{u. d. N.}\qquad (\boldsymbol{\mu}-r_f\mathbf{1})^\top\mathbf{y} = 1,\quad \sum_i y_i = \kappa,\quad \mathbf{y}\ge\mathbf{0},\ \kappa>0

Die echten Gewichte ergeben sich am Ende durch Normierung:

\mathbf{w}^* = \frac{\mathbf{y}^*}{\kappa^*}

💡 Warum das funktioniert Die Sharpe Ratio ändert sich nicht, wenn man \mathbf{w} mit einer positiven Zahl skaliert — Zähler und Nenner wachsen gleichermaßen. Man darf deshalb die Skala frei festlegen. Die Bedingung (\boldsymbol{\mu}-r_f\mathbf{1})^\top\mathbf{y} = 1 normiert die Überrendite auf 1; dann ist das Maximieren der Sharpe Ratio gleichbedeutend mit dem Minimieren des Nenners — und das ist ein gewöhnliches QP.

Wichtig bei Restriktionen: Alle Nebenbedingungen müssen mitskaliert werden. Aus w_i \le 0{,}20 wird y_i \le 0{,}20\,\kappa. Wer das vergisst, erhält ein falsches Portfolio.


19.5 Institutionelle Nebenbedingungen

In der Praxis gelten regulatorische und interne Regeln:

Regel Formulierung Zweck
Positionsobergrenze w_i \le w_{\max} (z. B. 0,20) Klumpenrisiko begrenzen
Sektorlimit \sum_{i\in\text{Tech}} w_i \le 0{,}35 Branchenkonzentration begrenzen
Mindestposition w_i = 0 oder w_i \ge w_{\min} Streuverluste vermeiden
Kardinalität \sum_i z_i \le K, w_i \le M z_i Verwaltungsaufwand begrenzen
Turnover-Grenze \|\mathbf{w}-\mathbf{w}_{\text{alt}}\|_1 \le \tau Transaktionskosten dämpfen

Was diese Regeln kosten

Jede Regel schneidet etwas vom Erreichbaren ab. Wie viel, lässt sich ausrechnen — man legt die Effizienzlinie ohne und mit Beschränkungen übereinander:

Abb. 19.2: Was die Hausregeln kosten. Der senkrechte Abstand ist der Renditeverlust bei gleichem Risiko. Wichtiger ist aber das rechte Ende: Mit den Hausregeln bricht die Linie bei 9,06 % ab — die letzten 1,50 Punkte sind mit keinem Risiko der Welt mehr zu kaufen. Feste Instanz aus einem Faktormodell, erzeugt von bilder_04/erzeuge_effizienzlinie.py.

Zwei verschiedene Kosten, und die zweite ist die größere. Der senkrechte Abstand — hier höchstens 0,58 Prozentpunkte — ist das, was die meisten erwarten: bei gleichem Risiko etwas weniger Rendite. Der eigentliche Einschnitt steht am oberen Ende. Ohne Grenzen darf das Portfolio ganz in die renditestärksten Titel gehen und erreicht 10,55 %; mit einer 20-%-Grenze je Titel und den Sektorlimits ist bei 9,06 % Schluss. Diese 1,50 Punkte sind nicht teuer erkauft, sondern überhaupt nicht mehr erreichbar — auch nicht gegen mehr Risiko.

📎 Warum hier eine feste Instanz und nicht die Kursdaten Markowitz_CVXPY.py lädt Livedaten; ein Diagramm daraus sähe an jedem Tag anders aus. Die Figur rechnet deshalb ein festes Universum aus einem Faktormodell — dieselbe Struktur (zehn Titel, vier Sektoren) und dieselben Regeln wie im Kapitel, aber reproduzierbar. Die Zahlen Ihres Livelaufs weichen ab; die Form der Aussage nicht.

⚠️ Sektoren niemals über Positionsindizes definieren

Ein naiver, aber gefährlicher Ansatz definiert den Technologiesektor so:

tickers = ["AAPL", "MSFT", "NVDA", "AMZN", "JNJ", ...]
tech_indices = [0, 1, 2, 3]        # gemeint: AAPL, MSFT, NVDA, AMZN

Weil yfinance alphabetisch sortiert, trifft das tatsächlich auf AAPL, AMZN, CVX (Chevron) und GS (Goldman Sachs) zu — Microsoft und NVIDIA blieben unbeschränkt.

Robust ist die Definition über Namen:

SEKTOREN = {"Technologie": ["AAPL", "MSFT", "NVDA", "AMZN"], ...}
idx = [tickers.index(t) for t in SEKTOREN["Technologie"]]

Diese Schreibweise ist nicht nur sicher, sondern auch lesbar — und sie bricht laut ab, wenn ein Ticker fehlt, statt still das Falsche zu tun.


19.6 Vollimplementierung mit CVXPY

#!/usr/bin/env python3

# Markowitz_CVXPY.py
"""
Kapitel Markowitz: Markowitz-Mean-Variance-Optimierung mit CVXPY.
Enthaelt GMV, Maximum Sharpe (Korn-Transformation), Effizienzgrenze
und institutionelle Restriktionen.

Eigenschaften:
  * Spaltenreihenfolge erzwungen; Sektoren ueber NAMEN statt Indizes
  * Effizienzgrenze bis zur TATSAECHLICH erreichbaren Maximalrendite
    (nicht etwa max(mu)*0.95 - das waere unter Restriktionen oft
     unerreichbar und die Kurve wuerde stillschweigend vorzeitig abbrechen)
  * Pruefung aller Restriktionen nach dem Loesen
  * Vergleich mit Gleichgewichtung als Realitaetscheck
"""

import os

import cvxpy as cp
import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.covariance import LedoitWolf
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
os.makedirs(OUTPUT_DIR, exist_ok=True)

TICKER = ["AAPL", "MSFT", "NVDA", "AMZN", "JNJ", "PFE", "JPM", "GS", "XOM", "CVX"]

# Sektoren ueber Namen definiert, nicht ueber Positionen
SEKTOREN = {
    "Technologie": ["AAPL", "MSFT", "NVDA", "AMZN"],
    "Gesundheit":  ["JNJ", "PFE"],
    "Finanzen":    ["JPM", "GS"],
    "Energie":     ["XOM", "CVX"],
}
SEKTORGRENZEN = {"Technologie": 0.35, "Gesundheit": 0.40,
                 "Finanzen": 0.40, "Energie": 0.40}

MAX_GEWICHT = 0.20      # hoechstens 20 % je Einzeltitel
RISIKOFREI = 0.03       # 3 % p.a.
HANDELSTAGE = 252


def lade_daten():
    """Laedt Kurse und garantiert die Spaltenreihenfolge."""
    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()      # [TICKER] erzwingt die Reihenfolge
    else:
        kurse = roh[["Close"]].dropna()
        kurse.columns = TICKER

    assert list(kurse.columns) == TICKER, "Spaltenreihenfolge weicht ab!"
    renditen = kurse.pct_change().dropna()
    mu = renditen.mean().values * HANDELSTAGE
    sigma = LedoitWolf().fit(renditen.values).covariance_ * HANDELSTAGE
    return kurse, renditen, mu, sigma


def sektor_indizes():
    """Uebersetzt Sektornamen einmalig in Positionsindizes - mit Pruefung."""
    ergebnis = {}
    for sektor, titel in SEKTOREN.items():
        fehlend = [t for t in titel if t not in TICKER]
        if fehlend:
            raise ValueError(f"Sektor '{sektor}': Ticker {fehlend} nicht im Universum.")
        ergebnis[sektor] = [TICKER.index(t) for t in titel]
    return ergebnis


def basis_restriktionen(w, skala=None):
    """
    Standardrestriktionen. Bei der Korn-Transformation muessen ALLE Grenzen
    mit kappa mitskaliert werden - dafuer dient der Parameter 'skala'.
    """
    eins = 1.0 if skala is None else skala
    idx = sektor_indizes()
    bedingungen = [w >= 0, w <= MAX_GEWICHT * eins]
    for sektor, positionen in idx.items():
        bedingungen.append(cp.sum(w[positionen]) <= SEKTORGRENZEN[sektor] * eins)
    return bedingungen


def loese_gmv(sigma):
    """Global Minimum Variance: minimiere Risiko, ignoriere Rendite."""
    w = cp.Variable(len(sigma))
    problem = cp.Problem(cp.Minimize(0.5 * cp.quad_form(w, sigma)),
                         [cp.sum(w) == 1] + basis_restriktionen(w))
    problem.solve()
    if problem.status not in ("optimal", "optimal_inaccurate"):
        raise SystemExit(f"GMV nicht loesbar: {problem.status}")
    return w.value


def loese_max_sharpe(mu, sigma, r_f):
    """Maximum Sharpe Ratio ueber die Korn-Transformation."""
    n = len(mu)
    ueberrendite = mu - r_f
    if np.all(ueberrendite <= 0):
        raise SystemExit("Kein Titel schlaegt den risikofreien Zins - "
                         "Max-Sharpe-Portfolio existiert nicht.")

    y = cp.Variable(n)
    kappa = cp.Variable(nonneg=True)
    bedingungen = ([ueberrendite @ y == 1, cp.sum(y) == kappa]
                   + basis_restriktionen(y, skala=kappa))
    problem = cp.Problem(cp.Minimize(0.5 * cp.quad_form(y, sigma)), bedingungen)
    problem.solve()
    if problem.status not in ("optimal", "optimal_inaccurate"):
        raise SystemExit(f"Max-Sharpe nicht loesbar: {problem.status}")
    return y.value / kappa.value


def max_erreichbare_rendite(mu, sigma):
    """
    Achtung: max(mu)*0.95 als obere Grenze der Frontier anzunehmen, ist unter
    Positions- und Sektorgrenzen oft unerreichbar; die Kurve wuerde dann
    stillschweigend abbrechen. Hier wird die tatsaechliche Obergrenze berechnet.
    """
    w = cp.Variable(len(mu))
    problem = cp.Problem(cp.Maximize(mu @ w), [cp.sum(w) == 1] + basis_restriktionen(w))
    problem.solve()
    return float(problem.value)


def berechne_frontier(mu, sigma, ret_min, ret_max, punkte=40):
    """Effizienzgrenze durch Variation der Mindestrendite."""
    n = len(mu)
    w = cp.Variable(n)
    ziel_rendite = cp.Parameter()
    problem = cp.Problem(
        cp.Minimize(0.5 * cp.quad_form(w, sigma)),
        [cp.sum(w) == 1, mu @ w >= ziel_rendite] + basis_restriktionen(w))

    volas, renditen, uebersprungen = [], [], 0
    for ziel in np.linspace(ret_min, ret_max, punkte):
        ziel_rendite.value = ziel
        problem.solve()
        if problem.status in ("optimal", "optimal_inaccurate"):
            volas.append(float(np.sqrt(w.value @ sigma @ w.value)))
            renditen.append(float(mu @ w.value))
        else:
            uebersprungen += 1
    if uebersprungen:
        print(f"  Hinweis: {uebersprungen} Zielrenditen waren nicht erreichbar.")
    return np.array(volas), np.array(renditen)


def pruefe_restriktionen(w, bezeichnung):
    """Nach dem Loesen: haelt die Loesung wirklich alle Regeln ein?"""
    assert abs(w.sum() - 1) < 1e-6, f"{bezeichnung}: Summe != 1"
    assert w.min() > -1e-6, f"{bezeichnung}: negatives Gewicht"
    assert w.max() < MAX_GEWICHT + 1e-6, f"{bezeichnung}: Positionsgrenze verletzt"
    for sektor, positionen in sektor_indizes().items():
        anteil = w[positionen].sum()
        assert anteil < SEKTORGRENZEN[sektor] + 1e-6, \
            f"{bezeichnung}: Sektor {sektor} bei {anteil:.3f} ueber Grenze"


def kennzahlen(w, mu, sigma, r_f):
    rendite = float(mu @ w)
    vola = float(np.sqrt(w @ sigma @ w))
    return rendite, vola, (rendite - r_f) / vola


if __name__ == "__main__":
    kurse, renditen, mu, sigma = lade_daten()
    n = len(TICKER)

    w_gmv = loese_gmv(sigma)
    w_sharpe = loese_max_sharpe(mu, sigma, RISIKOFREI)
    w_gleich = np.ones(n) / n

    pruefe_restriktionen(w_gmv, "GMV")
    pruefe_restriktionen(w_sharpe, "Max Sharpe")

    print("=" * 88)
    print("        ERGEBNISSE DER MEAN-VARIANCE-OPTIMIERUNG")
    print("=" * 88)
    print(f"Datenbasis: {len(renditen)} Handelstage, {n} Titel, "
          f"risikofreier Zins {RISIKOFREI*100:.1f} %")
    print(f"Restriktionen: max. {MAX_GEWICHT*100:.0f} % je Titel, "
          f"Sektorgrenzen {SEKTORGRENZEN}\n")

    print(f"{'Portfolio':<26} {'Rendite':>10} {'Volatilitaet':>13} {'Sharpe':>9}")
    print("-" * 88)
    for name, w in [("Global Minimum Variance", w_gmv),
                    ("Maximum Sharpe Ratio", w_sharpe),
                    ("Gleichgewichtung (1/N)", w_gleich)]:
        r, v, sr = kennzahlen(w, mu, sigma, RISIKOFREI)
        print(f"{name:<26} {r*100:>9.2f} % {v*100:>12.2f} % {sr:>9.2f}")

    # --- Gewichte je Titel ------------------------------------------------
    print("\n--- Optimierte Portfoliogewichte ---")
    sektor_je_titel = {t: s for s, titel in SEKTOREN.items() for t in titel}
    tabelle = pd.DataFrame({
        "Ticker": TICKER,
        "Sektor": [sektor_je_titel[t] for t in TICKER],
        "Rendite p.a.": [f"{r*100:+6.1f} %" for r in mu],
        "Vola p.a.": [f"{np.sqrt(sigma[i, i])*100:5.1f} %" for i in range(n)],
        "GMV": [f"{w*100:5.1f} %" for w in w_gmv],
        "Max Sharpe": [f"{w*100:5.1f} %" for w in w_sharpe],
    })
    print(tabelle.to_string(index=False))

    print("\n--- Sektoraufteilung (Kontrolle) ---")
    print(f"{'Sektor':<14} {'Grenze':>8} {'GMV':>9} {'Max Sharpe':>12}")
    for sektor, positionen in sektor_indizes().items():
        print(f"{sektor:<14} {SEKTORGRENZEN[sektor]*100:>7.0f} % "
              f"{w_gmv[positionen].sum()*100:>8.1f} % "
              f"{w_sharpe[positionen].sum()*100:>11.1f} %")

    # --- Effizienzgrenze --------------------------------------------------
    ret_gmv = float(mu @ w_gmv)
    ret_max = max_erreichbare_rendite(mu, sigma)
    print(f"\nEffizienzgrenze von {ret_gmv*100:.2f} % bis {ret_max*100:.2f} % "
          f"(unter Restriktionen tatsaechlich erreichbar)")
    print(f"  Zum Vergleich: bester Einzeltitel {mu.max()*100:.2f} % - "
          f"durch die Grenzen nicht erreichbar.")
    volas, rets = berechne_frontier(mu, sigma, ret_gmv, ret_max)

    # --- Diagramm ---------------------------------------------------------
    plt.figure(figsize=(11, 6.5))
    plt.plot(volas * 100, rets * 100, "b-", lw=2.5, label="Effizienzgrenze (restringiert)")

    for w, farbe, marker, groesse, name in [
            (w_gmv, "green", "o", 150, "GMV"),
            (w_sharpe, "red", "*", 260, "Max Sharpe"),
            (w_gleich, "purple", "D", 110, "Gleichgewichtung")]:
        r, v, sr = kennzahlen(w, mu, sigma, RISIKOFREI)
        plt.scatter([v * 100], [r * 100], color=farbe, marker=marker, s=groesse,
                    zorder=5, label=f"{name} (SR={sr:.2f})")

    # Kapitalmarktlinie durch den risikofreien Zins und das Tangentialportfolio
    r_s, v_s, _ = kennzahlen(w_sharpe, mu, sigma, RISIKOFREI)
    x_linie = np.array([0, v_s * 1.25])
    plt.plot(x_linie * 100, (RISIKOFREI + (r_s - RISIKOFREI) / v_s * x_linie) * 100,
             color="orange", ls=":", lw=2, label="Kapitalmarktlinie")

    for i, t in enumerate(TICKER):
        plt.scatter(np.sqrt(sigma[i, i]) * 100, mu[i] * 100, color="gray", alpha=0.5, s=40)
        plt.annotate(t, (np.sqrt(sigma[i, i]) * 100 + 0.4, mu[i] * 100), fontsize=8)

    plt.title("Markowitz-Effizienzgrenze mit Sektor- und Positionsgrenzen", fontsize=12)
    plt.xlabel("Annualisierte Volatilitaet [%]")
    plt.ylabel("Annualisierte erwartete Rendite [%]")
    plt.grid(True, linestyle=":", alpha=0.6)
    plt.legend(loc="best")
    plt.tight_layout()
    ziel = os.path.join(OUTPUT_DIR, "markowitz_efficient_frontier.png")
    plt.savefig(ziel, dpi=150)
    print(f"\nDiagramm gespeichert unter '{ziel}'")
    print("=" * 88)

💻 Code-Durchgang: vier typische Fehlerquellen und ihre Absicherung

Stelle Naiver Ansatz Robuster Ansatz (hier verwendet)
roh["Close"][TICKER] roh["Close"] — alphabetisch Reihenfolge erzwungen + assert
SEKTOREN als Namensdict tech_indices = [0,1,2,3] Namen, mit Existenzprüfung
basis_restriktionen(y, skala=kappa) Grenzen von Hand mitskaliert zentral, dadurch nicht vergessbar
max_erreichbare_rendite() max(mu)*0.95 geraten tatsächliche Grenze berechnet

Zur letzten Zeile: Unter der 20-%-Positionsgrenze kann kein Portfolio die Rendite des besten Einzeltitels erreichen — man muss mindestens fünf Titel halten. Würde man die Obergrenze der Effizienzgrenze dennoch bei 0{,}95 \cdot \max(\mu) ansetzen, würden die unerreichbaren Zielrenditen vom if status == "optimal" stillschweigend übersprungen — die gezeichnete Kurve bräche dadurch früher ab, ohne dass es auffiele.

⚠️ Zur Reproduzierbarkeit Dieses Programm lädt Live-Daten. Ihre Zahlen werden abweichen. Prüfen Sie stattdessen die Struktur des Ergebnisses: Hält die Sektorgrenze? Summieren sich die Gewichte auf 100 %? Liegt die Sharpe Ratio des Max-Sharpe-Portfolios über der des GMV? Genau dafür sind die assert-Prüfungen da.

🎯 Der Realitätscheck: Gleichgewichtung Das Programm vergleicht immer mit dem 1/N-Portfolio. Das ist kein Scherz: DeMiguel, Garlappi und Uppal zeigten 2009 in einer viel beachteten Studie, dass die naive Gleichgewichtung viele optimierte Strategien out of sample schlägt — weil sie keinerlei Schätzung benötigt und damit auch keinen Schätzfehler enthält. Wenn Ihre Optimierung 1/N nicht schlägt, ist sie ihren Aufwand nicht wert. Diese Messlatte sollte in jedem Portfolioprojekt stehen.


19.7 Übungsaufgaben

Lösungen: Abschnitt A.19.

Aufgabe 19.1 ⭐ — Diversifikationseffekt rechnen. Zwei Titel: \mu_1 = 10\,\%, \sigma_1 = 25\,\%; \mu_2 = 6\,\%, \sigma_2 = 12\,\%; \rho = -0{,}2. Berechnen Sie Rendite und Volatilität für die Gewichtungen (1;0), (0{,}5;0{,}5), (0{,}3;0{,}7) und (0;1). Welche Kombination hat die beste Sharpe Ratio bei r_f = 2\,\%?

Aufgabe 19.2 ⭐ — Lambda deuten. Was passiert mit dem optimalen Portfolio, wenn \lambda von 0 auf \infty steigt? Skizzieren Sie den Weg auf der Effizienzgrenze.

Aufgabe 19.3 ⭐⭐ — Korn-Transformation nachvollziehen. Zeigen Sie rechnerisch, dass die Sharpe Ratio invariant gegenüber positiver Skalierung ist: \text{SR}(c\,\mathbf{w}) = \text{SR}(\mathbf{w}) für c > 0. Warum gilt das nicht, wenn die Nebenbedingung \sum w_i = 1 mitgeführt wird — und wie löst die Transformation dieses Problem?

Aufgabe 19.4 ⭐⭐ — Restriktionen kosten Rendite. Lassen Sie Markowitz_CVXPY.py mit MAX_GEWICHT von 1,0 (keine Grenze) bis 0,10 in Schritten von 0,05 laufen. Tabellieren Sie Sharpe Ratio und maximal erreichbare Rendite. (a) Was kostet die 20-%-Grenze an Sharpe Ratio? (b) Ab welcher Grenze wird das Problem unlösbar? Warum?

Aufgabe 19.5 ⭐⭐ — Den Sektorfehler nachstellen. Bauen Sie den Fehler bewusst nach: Verwenden Sie tech_indices = [0,1,2,3] auf den alphabetisch sortierten Spalten. Vergleichen Sie das Ergebnis mit der korrekten Version. (a) Welche Titel werden tatsächlich beschränkt? (b) Wie stark unterscheiden sich die Portfoliogewichte? (c) Fällt der Fehler in den ausgegebenen Kennzahlen auf?

Aufgabe 19.6 ⭐⭐⭐ — Kardinalität ergänzen. Erweitern Sie das Modell um „höchstens K = 5 Titel“ mit Binärvariablen. CVXPY braucht dafür cp.Variable(n, boolean=True) und einen MIQP-fähigen Solver (z. B. SCIP über cp.SCIP oder ECOS_BB). Vergleichen Sie Sharpe Ratio und Rechenzeit mit dem unbeschränkten Fall.

Aufgabe 19.7 ⭐⭐⭐ — Out-of-Sample-Test. Teilen Sie die Historie in zwei Hälften. Optimieren Sie auf der ersten, bewerten Sie auf der zweiten. Vergleichen Sie GMV, Max Sharpe und 1/N. (a) Welches Portfolio gewinnt out of sample? (b) Wie ändert sich das Bild mit Ledoit-Wolf gegenüber der Stichprobenkovarianz? (c) Welche Schlussfolgerung ziehen Sie für die Praxis?


19.8 Finde den Denkfehler

Kapitel 18 hat gezeigt, was eine schlecht geschätzte Kovarianzmatrix anrichtet. Bei den erwarteten Renditen ist es schlimmer — und weil sie in der Zielfunktion stehen statt in der Nebenbedingung, fällt es schwerer auf.

🐛 Finde den Denkfehler: Zwölf gleiche Anlagen, ein sehr ungleiches Portfolio

Ein Analyst optimiert ein Portfolio aus zwölf Anlagen. Erwartete Renditen und Kovarianzen schätzt er aus einem Jahr Tagesdaten — 250 Beobachtungen, in der Praxis durchaus üblich.

Zur Kontrolle bauen wir das Experiment so, dass die richtige Antwort feststeht: Alle zwölf Anlagen haben in Wahrheit exakt dieselbe Rendite (0,0400 % täglich) und dieselbe Schwankung, und sie sind unabhängig. Ein korrektes Verfahren müsste also gleichgewichten.

Der Optimierer legt stattdessen im Mittel 38 % in eine einzige Anlage — und meldet für das Portfolio eine Tagesrendite von 0,1261 %, mehr als das Dreifache dessen, was überhaupt erzielbar ist.

Ihre Aufgabe: (a) Woher kommt die Spanne von 0,24 Prozentpunkten zwischen der höchsten und der niedrigsten geschätzten Rendite, wenn doch alle Anlagen identisch sind? (b) Rechnen Sie nach, wie viele Beobachtungen nötig wären, um eine Rendite von 0,04 % bei 1,2 % Tagesschwankung überhaupt von null zu unterscheiden. (c) Warum wirkt sich derselbe Schätzfehler bei Renditen stärker aus als bei Kovarianzen? (d) Nennen Sie drei Gegenmittel — und begründen Sie, warum das erste davon das wirksamste ist.

Auflösung: Abschnitt A.19.

#!/usr/bin/env python3

# Renditeschaetzung_Falle.py
"""
Kapitel Markowitz: Warum geschaetzte Renditen noch gefaehrlicher sind als geschaetzte
Kovarianzen.

Das Kapitel Finanzdaten hat gezeigt, was eine schlecht geschaetzte Kovarianzmatrix
anrichtet. Bei den erwarteten RENDITEN ist es schlimmer - aus einem einfachen
statistischen Grund: Um eine Rendite von 0,04 % pro Tag von null zu
unterscheiden, braucht man bei 1,2 % Tagesschwankung ueber tausend
Beobachtungen. Kovarianzen konvergieren dagegen deutlich schneller.

Das Experiment ist bewusst so gebaut, dass die Antwort feststeht: ALLE zwoelf
Anlagen haben exakt dieselbe wahre Rendite und dieselbe Schwankung. Ein
korrektes Ergebnis waere also Gleichgewichtung. Was der Optimierer stattdessen
tut, zeigt der Lauf.

Wiederholt wird 200 mal, damit das Ergebnis nicht von einer Zufallsstichprobe
abhaengt.

Benoetigt: numpy, cvxpy
"""

from __future__ import annotations

import numpy as np
import cvxpy as cp

ANZAHL_ANLAGEN = 12
BEOBACHTUNGEN = 250              # ein Jahr Tagesdaten
BEOBACHTUNGEN_SPAETER = 2000     # der "spaetere Verlauf"
WIEDERHOLUNGEN = 200

# Die Wahrheit, die der Optimierer nicht kennt: alle Anlagen sind gleich.
WAHRE_RENDITE = 0.0004           # 0,04 % pro Tag
WAHRE_SCHWANKUNG = 0.012         # 1,2 % pro Tag
RISIKOAVERSION = 10.0

RNG = np.random.default_rng(5)


def baue_optimierer() -> tuple[cp.Problem, cp.Variable, cp.Parameter, cp.Parameter]:
    """Baut das Markowitz-Problem EINMAL mit Parametern.

    Bei 200 Wiederholungen ist das der Unterschied zwischen Sekunden und
    Minuten: cp.Parameter erlaubt es, nur die Daten zu tauschen, statt den
    Ausdrucksbaum jedes Mal neu zu kompilieren (siehe Kapitel Oekosystem).
    """
    gewichte = cp.Variable(ANZAHL_ANLAGEN, nonneg=True)
    renditen = cp.Parameter(ANZAHL_ANLAGEN)
    kovarianz = cp.Parameter((ANZAHL_ANLAGEN, ANZAHL_ANLAGEN), PSD=True)
    problem = cp.Problem(
        cp.Maximize(renditen @ gewichte
                    - RISIKOAVERSION * cp.quad_form(gewichte, kovarianz)),
        [cp.sum(gewichte) == 1])
    return problem, gewichte, renditen, kovarianz


def erzeuge(perioden: int) -> np.ndarray:
    """Renditen, bei denen alle Anlagen identisch verteilt und unabhaengig sind."""
    return RNG.normal(WAHRE_RENDITE, WAHRE_SCHWANKUNG,
                      (perioden, ANZAHL_ANLAGEN))


if __name__ == "__main__":
    problem, gewichte, renditen, kovarianz = baue_optimierer()

    im_zeitraum, spaeter, spaeter_gleich = [], [], []
    groesstes_gewicht, schaetzspanne = [], []

    for _ in range(WIEDERHOLUNGEN):
        schaetzdaten = erzeuge(BEOBACHTUNGEN)
        spaetere_daten = erzeuge(BEOBACHTUNGEN_SPAETER)

        geschaetzte_rendite = schaetzdaten.mean(axis=0)
        geschaetzte_kovarianz = np.cov(schaetzdaten, rowvar=False)

        renditen.value = geschaetzte_rendite
        # Symmetrisieren und minimal anheben: numerisches Rauschen kann die
        # Matrix sonst knapp unter die PSD-Grenze druecken (Kapitel Fundament).
        kovarianz.value = ((geschaetzte_kovarianz + geschaetzte_kovarianz.T) / 2
                           + 1e-10 * np.eye(ANZAHL_ANLAGEN))
        problem.solve()

        w = np.array(gewichte.value).ravel()
        im_zeitraum.append(float((schaetzdaten @ w).mean()))
        spaeter.append(float((spaetere_daten @ w).mean()))
        spaeter_gleich.append(float(spaetere_daten.mean()))
        groesstes_gewicht.append(float(w.max()))
        schaetzspanne.append(float(geschaetzte_rendite.max()
                                   - geschaetzte_rendite.min()))

    prozent = lambda werte: float(np.mean(werte)) * 100

    print("=" * 78)
    print("  WENN DER OPTIMIERER GESCHAETZTE RENDITEN GLAUBT")
    print("=" * 78)
    print(f"{ANZAHL_ANLAGEN} Anlagen, {WIEDERHOLUNGEN} Wiederholungen, "
          f"je {BEOBACHTUNGEN} Beobachtungen zur Schaetzung.")
    print(f"In Wahrheit haben ALLE dieselbe Tagesrendite von "
          f"{WAHRE_RENDITE * 100:.4f} % und dieselbe Schwankung.")
    print("Die richtige Antwort waere also: gleichgewichten.")
    print()
    print(f"Mittlere Spanne der geschaetzten Renditen: "
          f"{prozent(schaetzspanne):.4f} Prozentpunkte")
    print(f"  -> Das ist das {prozent(schaetzspanne) / (WAHRE_RENDITE * 100):.1f}-Fache "
          f"des wahren Werts. Aus reinem Rauschen.")
    print(f"Mittleres groesstes Einzelgewicht: "
          f"{prozent(groesstes_gewicht):.1f} % "
          f"(bei Gleichgewichtung waeren es {100 / ANZAHL_ANLAGEN:.1f} %)")
    print()
    print(f"{'':<38} {'Tagesrendite':>14}")
    print("-" * 78)
    print(f"{'Wahrheit (alle Anlagen)':<38} {WAHRE_RENDITE * 100:>13.4f} %")
    print(f"{'Optimierer IM Schaetzzeitraum':<38} {prozent(im_zeitraum):>13.4f} %"
          f"   <- Illusion")
    print(f"{'Optimierer AUSSERHALB':<38} {prozent(spaeter):>13.4f} %")
    print(f"{'Gleichgewichtung AUSSERHALB':<38} {prozent(spaeter_gleich):>13.4f} %")
    print("-" * 78)

    faktor = prozent(im_zeitraum) / (WAHRE_RENDITE * 100)
    vorsprung = prozent(spaeter) - prozent(spaeter_gleich)
    print(f"\nIm Schaetzzeitraum verspricht der Optimierer das "
          f"{faktor:.1f}-Fache der wahren Rendite.")
    print(f"Ausserhalb bleibt davon nichts: Er liegt {abs(vorsprung):.4f} "
          f"Prozentpunkte")
    print(f"{'schlechter' if vorsprung < 0 else 'besser'} als blosse Gleichgewichtung.")
    print()
    print("Der Grund ist statistisch, nicht finanzwirtschaftlich: Um eine")
    print(f"Rendite von {WAHRE_RENDITE*100:.2f} % taeglich von null zu unterscheiden,")
    print(f"braucht man bei {WAHRE_SCHWANKUNG*100:.1f} % Schwankung ueber tausend")
    print("Beobachtungen. Mit 250 misst man ueberwiegend Rauschen - und der")
    print("Optimierer nimmt jedes Rauschen fuer bare Muenze.")
    print()
    print("Drei praktische Konsequenzen:")
    print("  1. Verzichten Sie auf Renditeschaetzungen, wo es geht. Das")
    print("     Minimum-Varianz-Portfolio braucht gar keine.")
    print("  2. Wenn Sie welche brauchen: schrumpfen Sie sie stark zur Mitte")
    print("     (James-Stein, Black-Litterman) - viel staerker als Kovarianzen.")
    print("  3. Begrenzen Sie Einzelgewichte. Was der Optimierer nicht darf,")
    print("     kann er auch nicht auf Rauschen setzen.")
    print("=" * 78)

Erwartete Ausgabe:

==============================================================================
  WENN DER OPTIMIERER GESCHAETZTE RENDITEN GLAUBT
==============================================================================
12 Anlagen, 200 Wiederholungen, je 250 Beobachtungen zur Schaetzung.
In Wahrheit haben ALLE dieselbe Tagesrendite von 0.0400 % und dieselbe Schwankung.
Die richtige Antwort waere also: gleichgewichten.

Mittlere Spanne der geschaetzten Renditen: 0.2433 Prozentpunkte
  -> Das ist das 6.1-Fache des wahren Werts. Aus reinem Rauschen.
Mittleres groesstes Einzelgewicht: 38.0 % (bei Gleichgewichtung waeren es 8.3 %)

                                         Tagesrendite
------------------------------------------------------------------------------
Wahrheit (alle Anlagen)                       0.0400 %
Optimierer IM Schaetzzeitraum                 0.1261 %   <- Illusion
Optimierer AUSSERHALB                         0.0390 %
Gleichgewichtung AUSSERHALB                   0.0396 %
------------------------------------------------------------------------------

Im Schaetzzeitraum verspricht der Optimierer das 3.2-Fache der wahren Rendite.
Ausserhalb bleibt davon nichts: Er liegt 0.0006 Prozentpunkte
schlechter als blosse Gleichgewichtung.

Der Grund ist statistisch, nicht finanzwirtschaftlich: Um eine
Rendite von 0.04 % taeglich von null zu unterscheiden,
braucht man bei 1.2 % Schwankung ueber tausend
Beobachtungen. Mit 250 misst man ueberwiegend Rauschen - und der
Optimierer nimmt jedes Rauschen fuer bare Muenze.

Drei praktische Konsequenzen:
  1. Verzichten Sie auf Renditeschaetzungen, wo es geht. Das
     Minimum-Varianz-Portfolio braucht gar keine.
  2. Wenn Sie welche brauchen: schrumpfen Sie sie stark zur Mitte
     (James-Stein, Black-Litterman) - viel staerker als Kovarianzen.
  3. Begrenzen Sie Einzelgewichte. Was der Optimierer nicht darf,
     kann er auch nicht auf Rauschen setzen.
==============================================================================

🎯 Merksatz Die Effizienzlinie aus dem Kapitelanfang ist mathematisch tadellos — und praktisch nur so viel wert wie die Renditen, die man hineinsteckt. Das Minimum-Varianz-Portfolio ist deshalb in der Praxis oft die bessere Wahl als das Maximum-Sharpe-Portfolio: Es braucht überhaupt keine Renditeschätzung, sondern nur die Kovarianzmatrix — und die lässt sich ungleich zuverlässiger schätzen.


19.9 Micro-Quiz

❓ Micro-Quiz 19: Drei Fragen zum Selbstcheck

Genau eine Antwort ist jeweils richtig. Auflösung in Anhang A.

1. Auf der Effizienzlinie liegt das Portfolio mit der höchsten Rendite. Warum ist es trotzdem selten die richtige Wahl? (a) Weil es rechnerisch instabil ist. (b) Weil es das schlechteste Verhältnis von Rendite zu Risiko hat und zwangsläufig vollständig in die renditestärkste Einzelanlage geht — also gar nicht mehr diversifiziert ist. (c) Weil CVXPY dort keine Lösung mehr findet.

2. Warum ist das Minimum-Varianz-Portfolio in der Praxis oft robuster als das Maximum-Sharpe-Portfolio? (a) Weil es weniger Nebenbedingungen hat. (b) Weil es ohne Renditeschätzung auskommt. Renditen sind die mit Abstand am unzuverlässigsten geschätzte Eingangsgröße; wer sie nicht braucht, umgeht das Problem. (c) Weil es immer die höhere Rendite erzielt.

3. Ihr optimiertes Portfolio legt 44 % in einen einzigen von zwölf Titeln. Was ist Ihre erste Vermutung? (a) Dieser Titel ist tatsächlich deutlich besser als die anderen. (b) Der Optimierer folgt einem Schätzfehler in den erwarteten Renditen. Prüfen Sie, wie stark die Gewichte auf kleine Datenänderungen reagieren, und begrenzen Sie Einzelpositionen. (c) Die Kovarianzmatrix ist falsch skaliert.


19.10 Selbsttest

Antworten: Anhang A.

  1. Warum senkt Diversifikation das Risiko, ohne Rendite zu kosten?
  2. Welches Portfolio erhält man für \lambda = 0, und warum braucht es keine Renditeprognose?
  3. Warum ist die Sharpe Ratio nicht konvex, und wie behebt die Korn-Transformation das?
  4. Was muss man bei der Korn-Transformation mit den Nebenbedingungen tun?
  5. Warum ist die Gleichgewichtung ein ernstzunehmender Vergleichsmaßstab?

19.11 Zusammenfassung

  • Diversifikation ist das einzige Gratis-Mittagessen: Risiko sinkt, Rendite bleibt. Aber: In Krisen steigen Korrelationen, genau wenn man die Streuung braucht.
  • \lambda steuert zwischen GMV und Renditemaximierung. Das GMV benötigt keine Renditeprognose und ist deshalb robuster.
  • Die Korn-Transformation macht aus der nicht-konvexen Sharpe-Maximierung ein QP — alle Nebenbedingungen müssen dabei mit \kappa mitskaliert werden.
  • Sektoren immer über Namen definieren, nie über Positionsindizes.
  • Die Obergrenze der Effizienzgrenze muss berechnet werden, nicht geraten — sonst bricht die Kurve stillschweigend ab.
  • Vergleichen Sie immer mit 1/N. Wer die naive Gleichgewichtung nicht schlägt, hat Schätzrauschen optimiert.
  • Renditeschätzungen sind die schwächste Stelle des ganzen Modells. Der Standardfehler einer Renditeschätzung aus einem Jahr Tagesdaten ist rund doppelt so groß wie der geschätzte Wert selbst; für eine belastbare Schätzung bräuchte man über ein Jahrzehnt. Deshalb ist „keine Renditeschätzung brauchen“ (GMV) wirksamer als jede Verbesserung der Schätzung.
  • Die Effizienzlinie ist eine Speisekarte, keine Empfehlung. Welcher Punkt richtig ist, entscheidet die Risikotragfähigkeit — eine unternehmerische, keine mathematische Frage.

Ausblick. Kapitel 20 ersetzt die Varianz durch ein Risikomaß, das die tatsächlichen Verlustränder erfasst — und fügt Transaktionskosten hinzu.