Version 04 als eigenes Repository

Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.

Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.

Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.

Zusaetzlich in diesem Commit:

* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
  figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
  unveraendert daneben bestehen. ortools steht in der Grundausstattung,
  highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
  schlanken Installation gar nicht erst auftreten.

* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
  importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
  steht in requirements.txt, wird aber von keinem Programm importiert,
  sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
  [empfehlungen]).

* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
  ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
  Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.

* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
  tatsaechlichen aus erzeuge_titelseite.py korrigiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 01:20:09 +02:00
commit b7af2f1d9a
468 changed files with 262141 additions and 0 deletions

View file

@ -0,0 +1,143 @@
#!/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)