operations_research/OR_HTML_04/Notebooks_04/cvar.ipynb

408 lines
18 KiB
Text
Raw Normal View History

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>
2026-09-08 01:20:09 +02:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten{idx:Transaktionskosten}\n",
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>
2026-09-08 01:20:09 +02:00
"\n",
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
Notebook-Tests: nbmake + Subprocess-Zellen + inkrementeller Test Alle 25 Notebooks sind nun fehlerfrei testbar (25 passed). Dafür drei Änderungen im Build und eine neue Test-Infrastruktur: 1. pyproject.toml: nbmake>=1.5 in der dev-Gruppe. 2. build_version_04.py: Notebooks bekommen zwei Setup-Zellen und Subprocess-Zellen für Programme, die nicht direkt im Kernel laufen: - NOTEBOOK_SETUP_2: __file__ definieren, sys.path für or_kern, multiprocessing auf fork (Spawn findet Kernel-Funktionen nicht). - _braucht_subprocess: erkennt highspy (Solver-Konflikt), pytest (SystemExit), get_context('spawn'), subprocess+__file__ (sucht .py-Dateien), sys.exit (SystemExit). Diese Programme erscheinen als Markdown-Codeblock (sichtbar, nicht ausführbar) plus einer Subprocess-Zelle mit NO_COLOR=1 (verhindert ANSI-Codes, die der Parser von Mutationstest.py nicht verarbeitet). - Solver-Konflikt-Erkennung: wenn ein Kapitel sowohl ortools als auch highspy importiert, laufen auch reine ortools-Programme als Subprocess (highspy schon im Kernel). 3. teste_code_04.py: inkrementeller Test — nur geänderte/neue Programme und Notebooks (via git status). Netzabhängige Dateien (yfinance) werden automatisch erkannt und nur getestet, wenn Internet verfügbar. 4. PLAN.md Abschnitt 10: neuer Verifikationsschritt 4 (pytest --nbmake). PROGRESS.md und CLAUDE.md aktualisiert. Gefundene und behobene Probleme: - or_kern-Import: sys.path im Notebook erweitert. - __file__ nicht definiert: zweite Setup-Zelle setzt es. - multiprocessing spawn: fork erzwungen (Ein_System_Vier_Ansaetze.py). - ortools/highspy-Konflikt: Subprocess für beide Solver. - pytest SystemExit: Subprocess für test_or_kern.py und Mutationstest.py. - sys.exit(0): Subprocess für Installationstest.py. - ANSI-Codes in pytest-Ausgabe: NO_COLOR=1 in Subprocess-Zelle.
2026-09-11 22:38:59 +02:00
"metadata": {
"tags": [
"nbmake-skip"
]
},
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>
2026-09-08 01:20:09 +02:00
"outputs": [],
"source": [
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
]
},
Notebook-Tests: nbmake + Subprocess-Zellen + inkrementeller Test Alle 25 Notebooks sind nun fehlerfrei testbar (25 passed). Dafür drei Änderungen im Build und eine neue Test-Infrastruktur: 1. pyproject.toml: nbmake>=1.5 in der dev-Gruppe. 2. build_version_04.py: Notebooks bekommen zwei Setup-Zellen und Subprocess-Zellen für Programme, die nicht direkt im Kernel laufen: - NOTEBOOK_SETUP_2: __file__ definieren, sys.path für or_kern, multiprocessing auf fork (Spawn findet Kernel-Funktionen nicht). - _braucht_subprocess: erkennt highspy (Solver-Konflikt), pytest (SystemExit), get_context('spawn'), subprocess+__file__ (sucht .py-Dateien), sys.exit (SystemExit). Diese Programme erscheinen als Markdown-Codeblock (sichtbar, nicht ausführbar) plus einer Subprocess-Zelle mit NO_COLOR=1 (verhindert ANSI-Codes, die der Parser von Mutationstest.py nicht verarbeitet). - Solver-Konflikt-Erkennung: wenn ein Kapitel sowohl ortools als auch highspy importiert, laufen auch reine ortools-Programme als Subprocess (highspy schon im Kernel). 3. teste_code_04.py: inkrementeller Test — nur geänderte/neue Programme und Notebooks (via git status). Netzabhängige Dateien (yfinance) werden automatisch erkannt und nur getestet, wenn Internet verfügbar. 4. PLAN.md Abschnitt 10: neuer Verifikationsschritt 4 (pytest --nbmake). PROGRESS.md und CLAUDE.md aktualisiert. Gefundene und behobene Probleme: - or_kern-Import: sys.path im Notebook erweitert. - __file__ nicht definiert: zweite Setup-Zelle setzt es. - multiprocessing spawn: fork erzwungen (Ein_System_Vier_Ansaetze.py). - ortools/highspy-Konflikt: Subprocess für beide Solver. - pytest SystemExit: Subprocess für test_or_kern.py und Mutationstest.py. - sys.exit(0): Subprocess für Installationstest.py. - ANSI-Codes in pytest-Ausgabe: NO_COLOR=1 in Subprocess-Zelle.
2026-09-11 22:38:59 +02:00
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Notebook-spezifisches Setup: __file__ definieren (Programme nutzen es\n",
"# fuer OUTPUT_DIR), Programmverzeichnis in den Suchpfad aufnehmen (or_kern)\n",
"# und multiprocessing auf 'fork' stellen (Spawn findet Kernel-Funktionen nicht).\n",
"import os, sys\n",
"__file__ = os.path.join(os.getcwd(), '_notebook.py')\n",
"for _p in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" if os.path.isfile(os.path.join(_p, 'or_kern.py')):\n",
" sys.path.insert(0, _p); break\n",
"import multiprocessing as _mp\n",
"try: _mp.set_start_method('fork', force=True)\n",
"except (RuntimeError, ValueError): pass"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Die zwei Schwächen des Markowitz-Modells\n",
"\n",
"`VaR_CVaR_Demo.py`\n"
]
},
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>
2026-09-08 01:20:09 +02:00
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Die zwei Schwächen des Markowitz-Modells\n",
"\n",
"`VaR_CVaR_Demo.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# VaR_CVaR_Demo.py\n",
"\"\"\"\n",
"Kapitel CVaR: Fat Tails, VaR und CVaR anschaulich.\n",
"\n",
"Teil 1: Wie oft treten \"unmoegliche\" Tage wirklich auf?\n",
"Teil 2: Warum ist der VaR nicht subadditiv - ein Gegenbeispiel zum Nachrechnen.\n",
"\"\"\"\n",
"\n",
"import numpy as np\n",
"from scipy import stats\n",
"\n",
"\n",
"def var_quantil(verluste: np.ndarray, alpha: float = 0.95) -> float:\n",
" \"\"\"VaR = Quantil der Verlustverteilung (Verluste positiv, Gewinne negativ).\"\"\"\n",
" return float(np.quantile(verluste, alpha))\n",
"\n",
"\n",
"def cvar_rockafellar(verluste: np.ndarray, alpha: float = 0.95) -> float:\n",
" \"\"\"\n",
" CVaR ueber die Rockafellar-Uryasev-Formel:\n",
" CVaR = min_gamma { gamma + 1/(1-alpha) * E[max(Verlust - gamma, 0)] }\n",
"\n",
" WICHTIG: Der naheliegende Weg \"Mittelwert aller Werte >= VaR\" ist FALSCH,\n",
" sobald die Verteilung Atome hat (z. B. genau zwei moegliche Verluste).\n",
" Dann liegt der VaR selbst auf einem Atom, und der Vergleich '>=' erfasst\n",
" zu viel Wahrscheinlichkeitsmasse. Die Formel unten behandelt das korrekt -\n",
" und ist zugleich genau der Ausdruck, den wir im Abschnitt 'Value at Risk\n",
" und Conditional Value at Risk' optimieren.\n",
" \"\"\"\n",
" kandidaten = np.unique(verluste) # Optimum liegt immer auf einem Datenpunkt\n",
" return float(min(g + np.mean(np.maximum(verluste - g, 0.0)) / (1.0 - alpha)\n",
" for g in kandidaten))\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" rng = np.random.default_rng(2026)\n",
"\n",
" # --- Teil 1: Fat Tails (analytisch, nicht simuliert) ------------------\n",
" print(\"=\" * 88)\n",
" print(\" TEIL 1: WIE OFT TRITT DAS 'UNMOEGLICHE' EIN?\")\n",
" print(\"=\" * 88)\n",
" print(\"Vergleich: Normalverteilung gegen t-Verteilung mit 3 Freiheitsgraden\")\n",
" print(\"(beide auf Standardabweichung 1 normiert).\\n\")\n",
"\n",
" t_verteilung = stats.t(df=3)\n",
" skalierung = t_verteilung.std() # auf Varianz 1 bringen\n",
"\n",
" print(f\"{'Ereignis':<22} {'Normal':>14} {'t (df=3)':>14} {'Faktor':>11} \"\n",
" f\"{'Normal: 1 Tag in':>18}\")\n",
" print(\"-\" * 88)\n",
" for k in [3, 4, 5, 6]:\n",
" p_normal = 2 * stats.norm.sf(k) # beidseitig\n",
" p_t = 2 * t_verteilung.sf(k * skalierung)\n",
" print(f\"Abweichung > {k} Sigma {p_normal*100:>13.6f} % {p_t*100:>13.6f} % \"\n",
" f\"{p_t/p_normal:>10.1f}x {1/p_normal/252:>15,.0f} Jahre\")\n",
"\n",
" print(\"\\nDeutung: Ein 5-Sigma-Tag ist unter Normalverteilung ein Ereignis von\")\n",
" print(\"etwa einmal in 6.900 Jahren. Reale Aktienmaerkte liefern solche Tage\")\n",
" print(\"mehrfach pro Jahrzehnt. Wer allein mit Varianz steuert, plant fuer\")\n",
" print(\"eine Welt, in der Crashs praktisch nicht vorkommen.\")\n",
"\n",
" # --- Teil 2: VaR ist nicht subadditiv ---------------------------------\n",
" print(\"\\n\" + \"=\" * 88)\n",
" print(\" TEIL 2: WARUM DER VaR KEIN KOHAERENTES RISIKOMASS IST\")\n",
" print(\"=\" * 88)\n",
" print(\"Zwei unabhaengige Anleihen, je 100 EUR Nominal.\")\n",
" print(\"Jede faellt mit 4 % Wahrscheinlichkeit aus (Verlust 100),\")\n",
" print(\"sonst zahlt sie 2 EUR Kupon (Verlust -2).\\n\")\n",
"\n",
" ziehungen = 2_000_000\n",
" verlust_a = np.where(rng.random(ziehungen) < 0.04, 100.0, -2.0)\n",
" verlust_b = np.where(rng.random(ziehungen) < 0.04, 100.0, -2.0)\n",
" verlust_ab = verlust_a + verlust_b\n",
"\n",
" print(f\"{'':<28} {'VaR 95%':>12} {'CVaR 95%':>12}\")\n",
" print(\"-\" * 88)\n",
" werte = {}\n",
" for name, v in [(\"Anleihe A allein\", verlust_a),\n",
" (\"Anleihe B allein\", verlust_b),\n",
" (\"Portfolio A+B\", verlust_ab)]:\n",
" werte[name] = (var_quantil(v), cvar_rockafellar(v))\n",
" print(f\"{name:<28} {werte[name][0]:>12.2f} {werte[name][1]:>12.2f}\")\n",
"\n",
" var_summe = werte[\"Anleihe A allein\"][0] + werte[\"Anleihe B allein\"][0]\n",
" cvar_summe = werte[\"Anleihe A allein\"][1] + werte[\"Anleihe B allein\"][1]\n",
" print(f\"{'Summe der Einzelwerte':<28} {var_summe:>12.2f} {cvar_summe:>12.2f}\")\n",
"\n",
" var_port, cvar_port = werte[\"Portfolio A+B\"]\n",
" print(\"-\" * 88)\n",
" print(f\"VaR: Portfolio {var_port:7.2f} vs. Summe {var_summe:7.2f} -> \"\n",
" f\"{'VERLETZT die Subadditivitaet!' if var_port > var_summe else 'subadditiv'}\")\n",
" print(f\"CVaR: Portfolio {cvar_port:7.2f} vs. Summe {cvar_summe:7.2f} -> \"\n",
" f\"{'subadditiv (kohaerent)' if cvar_port <= cvar_summe + 1e-6 else 'verletzt'}\")\n",
"\n",
" print(\"\\nDeutung: Einzeln betrachtet meldet der VaR fuer jede Anleihe einen\")\n",
" print(\"GEWINN von 2 EUR - denn mit 96 % Wahrscheinlichkeit passiert nichts,\")\n",
" print(\"und 4 % liegen unterhalb der 5-%-Schwelle. Im Portfolio steigt die\")\n",
" print(\"Wahrscheinlichkeit mindestens eines Ausfalls auf 7,8 % und damit UEBER\")\n",
" print(\"die Schwelle - der VaR springt auf 98. Er behauptet also, Streuung\")\n",
" print(\"habe das Risiko erhoeht. Das ist oekonomisch unsinnig und der Grund,\")\n",
" print(\"warum die Bankenaufsicht mit Basel III auf den Expected Shortfall\")\n",
" print(\"umgestellt hat.\")\n",
" print(\"=\" * 88)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Implementierung: CVaR-Portfolio mit Reibung\n",
"\n",
"`CVaR_Portfolio.py`\n"
]
},
Notebook-Tests: nbmake + Subprocess-Zellen + inkrementeller Test Alle 25 Notebooks sind nun fehlerfrei testbar (25 passed). Dafür drei Änderungen im Build und eine neue Test-Infrastruktur: 1. pyproject.toml: nbmake>=1.5 in der dev-Gruppe. 2. build_version_04.py: Notebooks bekommen zwei Setup-Zellen und Subprocess-Zellen für Programme, die nicht direkt im Kernel laufen: - NOTEBOOK_SETUP_2: __file__ definieren, sys.path für or_kern, multiprocessing auf fork (Spawn findet Kernel-Funktionen nicht). - _braucht_subprocess: erkennt highspy (Solver-Konflikt), pytest (SystemExit), get_context('spawn'), subprocess+__file__ (sucht .py-Dateien), sys.exit (SystemExit). Diese Programme erscheinen als Markdown-Codeblock (sichtbar, nicht ausführbar) plus einer Subprocess-Zelle mit NO_COLOR=1 (verhindert ANSI-Codes, die der Parser von Mutationstest.py nicht verarbeitet). - Solver-Konflikt-Erkennung: wenn ein Kapitel sowohl ortools als auch highspy importiert, laufen auch reine ortools-Programme als Subprocess (highspy schon im Kernel). 3. teste_code_04.py: inkrementeller Test — nur geänderte/neue Programme und Notebooks (via git status). Netzabhängige Dateien (yfinance) werden automatisch erkannt und nur getestet, wenn Internet verfügbar. 4. PLAN.md Abschnitt 10: neuer Verifikationsschritt 4 (pytest --nbmake). PROGRESS.md und CLAUDE.md aktualisiert. Gefundene und behobene Probleme: - or_kern-Import: sys.path im Notebook erweitert. - __file__ nicht definiert: zweite Setup-Zelle setzt es. - multiprocessing spawn: fork erzwungen (Ein_System_Vier_Ansaetze.py). - ortools/highspy-Konflikt: Subprocess für beide Solver. - pytest SystemExit: Subprocess für test_or_kern.py und Mutationstest.py. - sys.exit(0): Subprocess für Installationstest.py. - ANSI-Codes in pytest-Ausgabe: NO_COLOR=1 in Subprocess-Zelle.
2026-09-11 22:38:59 +02:00
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Implementierung: CVaR-Portfolio mit Reibung\n",
"\n",
"`CVaR_Portfolio.py`\n"
]
},
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>
2026-09-08 01:20:09 +02:00
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# CVaR_Portfolio.py\n",
"\"\"\"\n",
"Kapitel CVaR: CVaR-Optimierung mit L1-Transaktionskosten via CVXPY.\n",
"\n",
"Eigenschaften:\n",
" * Spaltenreihenfolge erzwungen\n",
" * Einheiten konsistent (alles taeglich, Annualisierung nur in der Ausgabe)\n",
" * Szenario-Nebenbedingungen VEKTORISIERT statt in einer Python-Schleife\n",
" (eine Matrixbedingung statt S einzelner Constraints - deutlich schneller)\n",
" * Vergleich CVaR- gegen Varianz-Optimierung\n",
" * Nachrechnung von VaR/CVaR aus den realisierten Szenarien\n",
"\"\"\"\n",
"\n",
"import time\n",
"\n",
"import cvxpy as cp\n",
"import numpy as np\n",
"import pandas as pd\n",
"import yfinance as yf\n",
"\n",
"TICKER = [\"AAPL\", \"MSFT\", \"NVDA\", \"AMZN\", \"JNJ\", \"PFE\", \"JPM\", \"GS\", \"XOM\", \"CVX\"]\n",
"ALPHA = 0.95 # Konfidenzniveau: schlechteste 5 % der Tage\n",
"MAX_GEWICHT = 0.25\n",
"GEBUEHRENSATZ = 0.002 # 0,2 % je Einheit Turnover (Spread + Brokerage)\n",
"RISIKOAVERSION = 1.5 # bezogen auf TAEGLICHE Groessen\n",
"HANDELSTAGE = 252\n",
"\n",
"\n",
"def lade_renditen():\n",
" ende = pd.Timestamp.today().normalize()\n",
" start = ende - pd.DateOffset(years=2)\n",
" roh = yf.download(TICKER, start=start, end=ende, auto_adjust=True, progress=False)\n",
" if roh.empty:\n",
" raise SystemExit(\"Download fehlgeschlagen (Netz, Ticker oder Rate-Limit pruefen).\")\n",
"\n",
" if isinstance(roh.columns, pd.MultiIndex):\n",
" kurse = roh[\"Close\"][TICKER].dropna() # erzwingt eigene Spaltenreihenfolge\n",
" else:\n",
" kurse = roh[[\"Close\"]].dropna()\n",
" kurse.columns = TICKER\n",
" assert list(kurse.columns) == TICKER, \"Spaltenreihenfolge weicht ab!\"\n",
" return kurse.pct_change().dropna()\n",
"\n",
"\n",
"def optimiere_cvar(R, w_alt, vektorisiert=True):\n",
" \"\"\"\n",
" Maximiere: taegliche Rendite - lambda * CVaR - Transaktionskosten\n",
" Alle Groessen TAEGLICH.\n",
" \"\"\"\n",
" S, N = R.shape\n",
" mu_taeglich = R.mean(axis=0)\n",
"\n",
" w = cp.Variable(N, nonneg=True)\n",
" gamma = cp.Variable() # wird im Optimum zum VaR\n",
" u = cp.Variable(S, nonneg=True) # Ueberschuss ueber die Schwelle\n",
"\n",
" cvar = gamma + (1.0 / (S * (1.0 - ALPHA))) * cp.sum(u)\n",
" turnover = cp.norm1(w - w_alt)\n",
" kosten = GEBUEHRENSATZ * turnover\n",
"\n",
" ziel = cp.Maximize(mu_taeglich @ w - RISIKOAVERSION * cvar - kosten)\n",
"\n",
" bedingungen = [cp.sum(w) == 1, w <= MAX_GEWICHT]\n",
" if vektorisiert:\n",
" # EINE Matrixbedingung statt S einzelner - deutlich schneller\n",
" bedingungen.append(u >= -(R @ w) - gamma)\n",
" else:\n",
" for s in range(S): # Alternative: S einzelne Constraints (langsamer)\n",
" bedingungen.append(u[s] >= -R[s] @ w - gamma)\n",
"\n",
" problem = cp.Problem(ziel, bedingungen)\n",
" problem.solve()\n",
" if problem.status not in (\"optimal\", \"optimal_inaccurate\"):\n",
" raise SystemExit(f\"CVaR-Problem nicht loesbar: {problem.status}\")\n",
" return w.value, float(gamma.value), float(cvar.value), float(turnover.value)\n",
"\n",
"\n",
"def optimiere_varianz(R, w_alt):\n",
" \"\"\"Klassisches Mean-Variance zum Vergleich - ebenfalls taeglich gerechnet.\n",
"\n",
" ACHTUNG, DCP-Falle: Die Standardabweichung ist hier NICHT als\n",
" cp.sqrt(cp.quad_form(w, sigma)) formulierbar. cp.sqrt ist konkav und\n",
" verlangt ein konkaves Argument; quad_form ist konvex - CVXPY lehnt den\n",
" Ausdruck mit einem DCPError ab, und zwar voellig zu Recht (Anhang\n",
" Fehlerdiagnose). cp.psd_wrap() hilft dagegen nicht: Es behebt eine\n",
" NUMERISCHE Beanstandung an sigma, keine Regelverletzung im Aufbau.\n",
"\n",
" Der Standardweg ist die Cholesky-Zerlegung sigma = L L^T. Damit gilt\n",
" w' sigma w = ||L^T w||^2, also ist die Standardabweichung die 2-Norm\n",
" eines AFFINEN Ausdrucks - konvex und damit regelkonform.\n",
" \"\"\"\n",
" N = R.shape[1]\n",
" mu_taeglich = R.mean(axis=0)\n",
" sigma = np.cov(R, rowvar=False, ddof=1)\n",
" # Der winzige Diagonalzuschlag faengt den Fall ab, dass sigma numerisch\n",
" # nur halbdefinit ist (mehr Titel als Handelstage, doppelte Spalten).\n",
" L = np.linalg.cholesky(sigma + 1e-12 * np.eye(N))\n",
"\n",
" w = cp.Variable(N, nonneg=True)\n",
" ziel = cp.Maximize(mu_taeglich @ w\n",
" - RISIKOAVERSION * cp.norm2(L.T @ w)\n",
" - GEBUEHRENSATZ * cp.norm1(w - w_alt))\n",
" problem = cp.Problem(ziel, [cp.sum(w) == 1, w <= MAX_GEWICHT])\n",
" problem.solve()\n",
" if problem.status not in (\"optimal\", \"optimal_inaccurate\"):\n",
" raise SystemExit(f\"Varianz-Problem nicht loesbar: {problem.status}\")\n",
" return w.value\n",
"\n",
"\n",
"def realisierte_kennzahlen(w, R):\n",
" \"\"\"VaR und CVaR direkt aus den Szenarien - unabhaengige Gegenprobe.\"\"\"\n",
" portfoliorenditen = R @ w\n",
" verluste = -portfoliorenditen\n",
" var = float(np.quantile(verluste, ALPHA))\n",
" cvar = float(verluste[verluste >= var].mean())\n",
" return var, cvar, float(portfoliorenditen.mean()), float(portfoliorenditen.std(ddof=1))\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" renditen = lade_renditen()\n",
" R = renditen.values\n",
" S, N = R.shape\n",
" w_alt = np.ones(N) / N # Ausgangslage: Gleichgewichtung\n",
"\n",
" t0 = time.perf_counter()\n",
" w_cvar, var_modell, cvar_modell, turnover = optimiere_cvar(R, w_alt, vektorisiert=True)\n",
" dauer_vektor = time.perf_counter() - t0\n",
"\n",
" w_var = optimiere_varianz(R, w_alt)\n",
"\n",
" print(\"=\" * 90)\n",
" print(\" CVaR-PORTFOLIO-OPTIMIERUNG MIT TRANSAKTIONSKOSTEN\")\n",
" print(\"=\" * 90)\n",
" print(f\"Datenbasis: {S} Handelstage, {N} Titel | Konfidenzniveau \"\n",
" f\"{ALPHA*100:.0f} % | Loesungszeit {dauer_vektor:.2f} s\\n\")\n",
"\n",
" # --- Gegenprobe: Modellwerte gegen realisierte Szenariowerte ---------\n",
" var_real, cvar_real, mu_real, sd_real = realisierte_kennzahlen(w_cvar, R)\n",
" print(\"--- Gegenprobe: stimmen Modell und Szenarien ueberein? ---\")\n",
" print(f\" VaR aus dem Modell (gamma): {var_modell*100:7.4f} % | \"\n",
" f\"aus den Szenarien: {var_real*100:7.4f} %\")\n",
" print(f\" CVaR aus dem Modell: {cvar_modell*100:7.4f} % | \"\n",
" f\"aus den Szenarien: {cvar_real*100:7.4f} %\")\n",
" assert abs(cvar_modell - cvar_real) < 1e-4, \"CVaR stimmt nicht mit den Szenarien!\"\n",
" print(\" -> Der Rockafellar-Uryasev-Trick liefert exakt den empirischen CVaR.\")\n",
"\n",
" # --- Kennzahlen beider Portfolios ------------------------------------\n",
" print(f\"\\n{'Portfolio':<24} {'Rendite p.a.':>13} {'Vola p.a.':>11} \"\n",
" f\"{'VaR 95% (Tag)':>15} {'CVaR 95% (Tag)':>16} {'Turnover':>10}\")\n",
" print(\"-\" * 90)\n",
" for name, w in [(\"CVaR-optimiert\", w_cvar), (\"Varianz-optimiert\", w_var),\n",
" (\"Gleichgewichtung\", w_alt)]:\n",
" v, c, m, s = realisierte_kennzahlen(w, R)\n",
" to = float(np.abs(w - w_alt).sum())\n",
" print(f\"{name:<24} {m*HANDELSTAGE*100:>12.2f} % \"\n",
" f\"{s*np.sqrt(HANDELSTAGE)*100:>10.2f} % {v*100:>14.3f} % \"\n",
" f\"{c*100:>15.3f} % {to*100:>9.1f} %\")\n",
"\n",
" print(\"\\nHinweis zur Annualisierung: Renditen werden mit 252 skaliert,\")\n",
" print(\"Volatilitaeten mit sqrt(252). Fuer VaR/CVaR ist eine solche Skalierung\")\n",
" print(\"nur unter starken Annahmen (Unabhaengigkeit, kein Drift) zulaessig -\")\n",
" print(\"sie werden hier deshalb bewusst als TAGESwerte ausgewiesen.\")\n",
"\n",
" # --- Allokationstabelle ------------------------------------------------\n",
" print(\"\\n--- Allokation ---\")\n",
" tabelle = pd.DataFrame({\n",
" \"Ticker\": TICKER,\n",
" \"vorher\": [f\"{v*100:5.1f} %\" for v in w_alt],\n",
" \"CVaR-opt.\": [f\"{v*100:5.1f} %\" for v in w_cvar],\n",
" \"Handel\": [f\"{(w_cvar[i]-w_alt[i])*100:+6.1f} %\" for i in range(N)],\n",
" \"Varianz-opt.\": [f\"{v*100:5.1f} %\" for v in w_var],\n",
" })\n",
" print(tabelle.to_string(index=False))\n",
" print(f\"\\nTurnover {turnover*100:.1f} % -> Transaktionskosten \"\n",
" f\"{GEBUEHRENSATZ*turnover*100:.3f} % des Portfoliowerts\")\n",
"\n",
" # --- Laufzeitvergleich vektorisiert vs. Schleife ----------------------\n",
" if S <= 600: # bei sehr vielen Szenarien zu langsam\n",
" t0 = time.perf_counter()\n",
" optimiere_cvar(R, w_alt, vektorisiert=False)\n",
" dauer_schleife = time.perf_counter() - t0\n",
" print(f\"\\n--- Laufzeit: {S} Nebenbedingungen aufbauen ---\")\n",
" print(f\" vektorisiert (u >= -(R @ w) - gamma): {dauer_vektor:6.2f} s\")\n",
" print(f\" Schleife ueber Szenarien (V01): {dauer_schleife:6.2f} s \"\n",
" f\"({dauer_schleife/dauer_vektor:.1f}x langsamer)\")\n",
" print(\"=\" * 90)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}