operations_research/OR_HTML_04/Notebooks_04/unsicherheit.ipynb

469 lines
20 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 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit\n",
"\n",
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"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"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Der Fluch des Durchschnitts\n",
"\n",
"`Fluch_des_Durchschnitts.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Fluch_des_Durchschnitts.py\n",
"\"\"\"\n",
"Kapitel Unsicherheit: Der Fluch des Durchschnitts (die Handrechnung dazu) als Scan ueber alle\n",
"moeglichen Kapazitaeten - zeigt, dass das Optimum nicht beim Mittelwert liegt.\n",
"\"\"\"\n",
"\n",
"import numpy as np\n",
"\n",
"SZENARIEN = np.array([100, 250, 500])\n",
"WAHRSCHEINLICHKEITEN = np.array([0.5, 0.3, 0.2])\n",
"PREIS_VORAB = 40.0\n",
"PREIS_ZUKAUF = 120.0\n",
"PREIS_VERWALTUNG = 5.0\n",
"\n",
"ERWARTUNGSWERT = float(SZENARIEN @ WAHRSCHEINLICHKEITEN)\n",
"\n",
"\n",
"def erwartete_kosten(x):\n",
" kosten = np.where(\n",
" SZENARIEN >= x,\n",
" PREIS_VORAB * x + PREIS_ZUKAUF * (SZENARIEN - x),\n",
" PREIS_VORAB * x + PREIS_VERWALTUNG * (x - SZENARIEN),\n",
" )\n",
" return float(kosten @ WAHRSCHEINLICHKEITEN)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 70)\n",
" print(\" DER FLUCH DES DURCHSCHNITTS\")\n",
" print(\"=\" * 70)\n",
" print(f\"Erwarteter Bedarf (Mittelwert): {ERWARTUNGSWERT:.1f}\")\n",
" print(f\"Kosten bei naiver Planung x={ERWARTUNGSWERT:.0f}: \"\n",
" f\"{erwartete_kosten(ERWARTUNGSWERT):,.2f} EUR\\n\")\n",
"\n",
" x_werte = np.arange(0, 501, 1)\n",
" kosten_werte = np.array([erwartete_kosten(x) for x in x_werte])\n",
" x_optimal = x_werte[np.argmin(kosten_werte)]\n",
" kosten_optimal = kosten_werte.min()\n",
"\n",
" print(f\"Optimales x (durch Scan gefunden): {x_optimal}\")\n",
" print(f\"Kosten beim Optimum: {kosten_optimal:,.2f} EUR\")\n",
" print(f\"Ersparnis gegenueber naiver Planung: \"\n",
" f\"{erwartete_kosten(ERWARTUNGSWERT) - kosten_optimal:,.2f} EUR\\n\")\n",
"\n",
" print(\"Kosten fuer ausgewaehlte x zum Vergleich:\")\n",
" for x in [100, 225, 250, 300, 500]:\n",
" markierung = \" <- Mittelwert\" if x == 225 else (\" <- Optimum\" if x == 250 else \"\")\n",
" print(f\" x={x:4d}: {erwartete_kosten(x):>12,.2f} EUR{markierung}\")\n",
"\n",
" print(\"\\nDas Optimum liegt exakt auf einem Szenariowert (250 = 'Volatil'),\")\n",
" print(\"nicht beim Mittelwert 225 - typisch fuer asymmetrische Kostenfunktionen.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Monte-Carlo-Simulation\n",
"\n",
"`Monte_Carlo.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Monte_Carlo.py\n",
"\"\"\"\n",
"Kapitel Unsicherheit: Monte-Carlo-Bewertung von Kapazitaetsplaenen.\n",
"\n",
"Monte Carlo OPTIMIERT nicht - es BEWERTET. Der Nutzen liegt darin, dass man\n",
"beliebige Kennzahlen ablesen kann: Erwartungswert, Quantile, Ausfallwahr-\n",
"scheinlichkeit, Worst Case. Genau diese Groessen braucht man, um zwischen\n",
"Plaenen zu entscheiden.\n",
"\"\"\"\n",
"\n",
"import numpy as np\n",
"\n",
"KOSTEN_VORAB = 40.0 # EUR je Einheit, im Voraus gekauft\n",
"KOSTEN_SPOT = 120.0 # EUR je Einheit, kurzfristig zugekauft\n",
"KOSTEN_LEERLAUF = 5.0 # EUR je ungenutzter Einheit\n",
"\n",
"ANZAHL_ZIEHUNGEN = 100_000\n",
"\n",
"\n",
"def ziehe_bedarf(rng, ziehungen):\n",
" \"\"\"\n",
" Bedarfsmodell: Mischverteilung aus Normalbetrieb und seltenen Lastspitzen.\n",
" Realistischer als eine reine Normalverteilung - Krisen sind selten,\n",
" aber extrem (fat tail).\n",
" \"\"\"\n",
" normal = rng.normal(loc=150, scale=40, size=ziehungen)\n",
" spitze = rng.normal(loc=450, scale=80, size=ziehungen)\n",
" ist_spitze = rng.random(ziehungen) < 0.15 # 15 % Lastspitzen\n",
" return np.maximum(np.where(ist_spitze, spitze, normal), 0.0)\n",
"\n",
"\n",
"def kosten_fuer(kapazitaet, bedarf):\n",
" \"\"\"Gesamtkosten je Szenario fuer eine gegebene Vorabkapazitaet.\"\"\"\n",
" unterdeckung = np.maximum(bedarf - kapazitaet, 0.0)\n",
" ueberdeckung = np.maximum(kapazitaet - bedarf, 0.0)\n",
" return (KOSTEN_VORAB * kapazitaet\n",
" + KOSTEN_SPOT * unterdeckung\n",
" + KOSTEN_LEERLAUF * ueberdeckung)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" rng = np.random.default_rng(2026)\n",
" bedarf = ziehe_bedarf(rng, ANZAHL_ZIEHUNGEN)\n",
"\n",
" print(\"=\" * 88)\n",
" print(f\" MONTE-CARLO-BEWERTUNG ({ANZAHL_ZIEHUNGEN:,} Szenarien)\")\n",
" print(\"=\" * 88)\n",
" print(f\"Bedarfsverteilung: Mittelwert {bedarf.mean():.1f} | \"\n",
" f\"Median {np.median(bedarf):.1f} | \"\n",
" f\"95%-Quantil {np.percentile(bedarf, 95):.1f} | \"\n",
" f\"Maximum {bedarf.max():.1f}\")\n",
" print(\"Der Median liegt deutlich unter dem Mittelwert - die Verteilung ist\")\n",
" print(\"rechtsschief. Genau hier fuehrt Planung mit dem Mittelwert in die Irre.\\n\")\n",
"\n",
" print(f\"{'Kapazitaet':>10} | {'Erw. Kosten':>12} | {'Median':>10} | \"\n",
" f\"{'95%-Quantil':>12} | {'Unterdeckung':>12}\")\n",
" print(\"-\" * 88)\n",
"\n",
" kandidaten = [150, 200, 225, 250, 300, 350, 400]\n",
" ergebnisse = []\n",
" for kapazitaet in kandidaten:\n",
" kosten = kosten_fuer(kapazitaet, bedarf)\n",
" p_unterdeckung = float(np.mean(bedarf > kapazitaet))\n",
" ergebnisse.append((kapazitaet, kosten.mean(), p_unterdeckung))\n",
" print(f\"{kapazitaet:>10} | {kosten.mean():>12,.0f} | \"\n",
" f\"{np.median(kosten):>10,.0f} | {np.percentile(kosten, 95):>12,.0f} | \"\n",
" f\"{p_unterdeckung*100:>11.1f} %\")\n",
"\n",
" beste = min(ergebnisse, key=lambda t: t[1])\n",
" print(\"-\" * 88)\n",
" print(f\"Bester Kandidat: Kapazitaet {beste[0]} mit erwarteten Kosten \"\n",
" f\"{beste[1]:,.0f} EUR\")\n",
"\n",
" # --- Feinsuche ueber ein Raster ---------------------------------------\n",
" raster = np.arange(100, 500, 5)\n",
" erwartete = np.array([kosten_fuer(k, bedarf).mean() for k in raster])\n",
" optimum = raster[int(np.argmin(erwartete))]\n",
" print(f\"Feinsuche (Raster 100..500): Optimum bei Kapazitaet {optimum}, \"\n",
" f\"Kosten {erwartete.min():,.0f} EUR\")\n",
"\n",
" # --- Vergleich mit der naiven Mittelwertplanung ----------------------\n",
" naiv = int(round(bedarf.mean()))\n",
" kosten_naiv = kosten_fuer(naiv, bedarf).mean()\n",
" kosten_opt = kosten_fuer(optimum, bedarf).mean()\n",
" print(\"\\n--- Fluch des Durchschnitts, gemessen ---\")\n",
" print(f\" Planung mit Mittelwert ({naiv}): {kosten_naiv:,.0f} EUR\")\n",
" print(f\" Monte-Carlo-Optimum ({optimum}): {kosten_opt:,.0f} EUR\")\n",
" print(f\" Mehrkosten der naiven Planung: {kosten_naiv - kosten_opt:,.0f} EUR \"\n",
" f\"({(kosten_naiv/kosten_opt - 1)*100:.1f} %)\")\n",
" print(\"=\" * 88)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Zweistufige stochastische Programmierung\n",
"\n",
"`Stochastische_Optimierung.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Stochastische_Optimierung.py\n",
"\"\"\"\n",
"Kapitel Unsicherheit: Two-Stage Stochastic Programming mit CVXPY.\n",
"\n",
"Eigenschaften:\n",
" * Die Analyse am Ende wird BERECHNET statt fest verdrahtet\n",
" * Vergleich gegen drei Alternativen: Mittelwert, Worst Case, perfekte Voraussicht\n",
" * Kennzahl EVPI (Wert perfekter Information) wird ausgewiesen\n",
"\"\"\"\n",
"\n",
"import cvxpy as cp\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"SZENARIEN = [\"Ruhig\", \"Volatil\", \"Crash\"]\n",
"WAHRSCHEINLICHKEIT = np.array([0.50, 0.30, 0.20])\n",
"BEDARF = np.array([100.0, 250.0, 500.0])\n",
"\n",
"KOSTEN_VORAB = 40.0\n",
"KOSTEN_SPOT = 120.0\n",
"KOSTEN_LEERLAUF = 5.0\n",
"\n",
"S = len(SZENARIEN)\n",
"\n",
"\n",
"def loese_stochastisch():\n",
" \"\"\"Zweistufiges Modell: eine Vorabentscheidung, szenarioabhaengige Korrektur.\"\"\"\n",
" x = cp.Variable(nonneg=True, name=\"Basiskapazitaet\") # Stufe 1\n",
" y_spot = cp.Variable(S, nonneg=True, name=\"Spot_Zukauf\") # Stufe 2\n",
" y_leer = cp.Variable(S, nonneg=True, name=\"Leerlauf\") # Stufe 2\n",
"\n",
" # Kopplung: Basis + Zukauf - Leerlauf == Bedarf (je Szenario)\n",
" nebenbedingungen = [x + y_spot[s] - y_leer[s] == BEDARF[s] for s in range(S)]\n",
"\n",
" erwartete_korrektur = sum(\n",
" WAHRSCHEINLICHKEIT[s] * (KOSTEN_SPOT * y_spot[s] + KOSTEN_LEERLAUF * y_leer[s])\n",
" for s in range(S))\n",
"\n",
" problem = cp.Problem(cp.Minimize(KOSTEN_VORAB * x + erwartete_korrektur),\n",
" nebenbedingungen)\n",
" problem.solve()\n",
" return problem, x, y_spot, y_leer\n",
"\n",
"\n",
"def kosten_bei(kapazitaet):\n",
" \"\"\"Erwartete Gesamtkosten fuer eine fest vorgegebene Kapazitaet.\"\"\"\n",
" unter = np.maximum(BEDARF - kapazitaet, 0.0)\n",
" ueber = np.maximum(kapazitaet - BEDARF, 0.0)\n",
" je_szenario = KOSTEN_VORAB * kapazitaet + KOSTEN_SPOT * unter + KOSTEN_LEERLAUF * ueber\n",
" return float(WAHRSCHEINLICHKEIT @ je_szenario)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" problem, x, y_spot, y_leer = loese_stochastisch()\n",
" kapazitaet = float(x.value)\n",
" mittelwert = float(WAHRSCHEINLICHKEIT @ BEDARF)\n",
"\n",
" print(\"=\" * 82)\n",
" print(\" STOCHASTISCHE TWO-STAGE OPTIMIERUNG (KAPAZITAETSPLANUNG)\")\n",
" print(\"=\" * 82)\n",
" print(f\"Status: {problem.status}\")\n",
" print(f\"Erwarteter Bedarf (Mittelwert): {mittelwert:.1f} Einheiten\")\n",
" print(f\"Optimale Stufe-1-Kapazitaet x*: {kapazitaet:.1f} Einheiten\")\n",
" print(f\"Minimale erwartete Gesamtkosten: {problem.value:,.2f} EUR\\n\")\n",
"\n",
" tabelle = pd.DataFrame({\n",
" \"Szenario\": SZENARIEN,\n",
" \"Wahrsch.\": [f\"{p*100:.0f} %\" for p in WAHRSCHEINLICHKEIT],\n",
" \"Bedarf\": BEDARF,\n",
" \"Basis genutzt\": [min(kapazitaet, b) for b in BEDARF],\n",
" \"Spot-Zukauf\": np.round(y_spot.value, 1),\n",
" \"Leerlauf\": np.round(y_leer.value, 1),\n",
" \"Kosten (EUR)\": [f\"{KOSTEN_VORAB*kapazitaet + KOSTEN_SPOT*y_spot.value[s] + KOSTEN_LEERLAUF*y_leer.value[s]:,.0f}\"\n",
" for s in range(S)],\n",
" })\n",
" print(tabelle.to_string(index=False))\n",
"\n",
" # --- Vergleich mit Alternativstrategien (berechnet, nicht behauptet) --\n",
" print(\"\\n\" + \"-\" * 82)\n",
" print(\"Vergleich verschiedener Planungsstrategien:\")\n",
" print(f\"{'Strategie':<34} {'Kapazitaet':>11} {'Erw. Kosten':>14} {'Mehrkosten':>13}\")\n",
" print(\"-\" * 82)\n",
"\n",
" optimal = problem.value\n",
" strategien = [\n",
" (\"Stochastisch optimal\", kapazitaet),\n",
" (\"Naiv: Mittelwert einsetzen\", mittelwert),\n",
" (\"Vorsichtig: Worst Case abdecken\", float(BEDARF.max())),\n",
" (\"Optimistisch: Bestfall\", float(BEDARF.min())),\n",
" ]\n",
" for name, kap in strategien:\n",
" kosten = kosten_bei(kap)\n",
" print(f\"{name:<34} {kap:>11.1f} {kosten:>14,.0f} \"\n",
" f\"{kosten - optimal:>+13,.0f}\")\n",
"\n",
" # --- EVPI: Was waere perfekte Voraussicht wert? ----------------------\n",
" # Bei perfekter Information wuerde man je Szenario genau den Bedarf kaufen.\n",
" kosten_perfekt = float(WAHRSCHEINLICHKEIT @ (KOSTEN_VORAB * BEDARF))\n",
" evpi = optimal - kosten_perfekt\n",
" print(\"-\" * 82)\n",
" print(f\"Kosten bei perfekter Voraussicht: {kosten_perfekt:>10,.0f} EUR\")\n",
" print(f\"Wert perfekter Information (EVPI): {evpi:>10,.0f} EUR \"\n",
" f\"({evpi/optimal*100:.1f} % der Kosten)\")\n",
" print(\" -> So viel duerfte eine perfekte Bedarfsprognose hoechstens kosten.\")\n",
"\n",
" # --- Automatische Interpretation --------------------------------------\n",
" print(\"-\" * 82)\n",
" if kapazitaet > mittelwert + 1e-6:\n",
" print(f\"Analyse: Der Solver waehlt {kapazitaet:.0f} Einheiten und damit MEHR als\")\n",
" print(f\"den Mittelwert ({mittelwert:.0f}), weil Unterdeckung ({KOSTEN_SPOT:.0f} EUR)\")\n",
" print(f\"deutlich teurer ist als Leerlauf ({KOSTEN_LEERLAUF:.0f} EUR).\")\n",
" elif kapazitaet < mittelwert - 1e-6:\n",
" print(f\"Analyse: Der Solver waehlt {kapazitaet:.0f} und damit WENIGER als den\")\n",
" print(f\"Mittelwert ({mittelwert:.0f}) - Leerlauf ist hier teurer als Zukauf.\")\n",
" else:\n",
" print(\"Analyse: Kapazitaet entspricht dem Mittelwert (symmetrische Kosten).\")\n",
" print(\"=\" * 82)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Robuste Optimierung: gegen den Worst Case absichern\n",
"\n",
"`Robuste_Optimierung.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Robuste_Optimierung.py\n",
"\"\"\"\n",
"Kapitel Unsicherheit: Robuste Portfolio-Optimierung.\n",
"\n",
"Vergleicht drei Haltungen zur Unsicherheit:\n",
" (a) nominal - vertraut den Punktschaetzungen blind\n",
" (b) robust - sichert gegen Box-Unsicherheit ab (Worst Case)\n",
" (c) stochastisch - optimiert den Erwartungswert ueber Szenarien\n",
"\n",
"und misst den \"Preis der Robustheit\": Wie viel Ertrag kostet die Absicherung\n",
"im Normalfall - und wie viel Verlust erspart sie im Ernstfall?\n",
"\"\"\"\n",
"\n",
"import cvxpy as cp\n",
"import numpy as np\n",
"\n",
"ASSETS = [\"Aktien Welt\", \"Anleihen\", \"Rohstoffe\", \"Immobilien\"]\n",
"MU_SCHAETZUNG = np.array([0.085, 0.030, 0.055, 0.060]) # Punktschaetzung\n",
"UNSICHERHEIT = np.array([0.040, 0.008, 0.045, 0.025]) # +/- delta je Titel\n",
"VOLA = np.array([0.17, 0.05, 0.22, 0.12])\n",
"KORR = np.array([\n",
" [1.00, -0.15, 0.35, 0.55],\n",
" [-0.15, 1.00, -0.05, 0.10],\n",
" [0.35, -0.05, 1.00, 0.25],\n",
" [0.55, 0.10, 0.25, 1.00],\n",
"])\n",
"SIGMA = np.diag(VOLA) @ KORR @ np.diag(VOLA)\n",
"LAMBDA = 4.0 # Risikoaversion\n",
"N = len(ASSETS)\n",
"\n",
"\n",
"def optimiere(mu_effektiv):\n",
" \"\"\"Standard-Mean-Variance mit vorgegebenem Renditevektor.\"\"\"\n",
" w = cp.Variable(N, nonneg=True)\n",
" ziel = cp.Maximize(mu_effektiv @ w - 0.5 * LAMBDA * cp.quad_form(w, SIGMA))\n",
" problem = cp.Problem(ziel, [cp.sum(w) == 1])\n",
" problem.solve()\n",
" return w.value\n",
"\n",
"\n",
"def kennzahlen(w, mu):\n",
" ertrag = float(mu @ w)\n",
" risiko = float(np.sqrt(w @ SIGMA @ w))\n",
" return ertrag, risiko\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 88)\n",
" print(\" ROBUSTE vs. NOMINALE PORTFOLIO-OPTIMIERUNG\")\n",
" print(\"=\" * 88)\n",
" print(f\"{'Asset':<14} {'Erw. Rendite':>14} {'Unsicherheit':>14} \"\n",
" f\"{'Worst Case':>12} {'Volatilitaet':>13}\")\n",
" print(\"-\" * 88)\n",
" for i, name in enumerate(ASSETS):\n",
" print(f\"{name:<14} {MU_SCHAETZUNG[i]*100:>13.1f} % \"\n",
" f\"{'+/- ' + format(UNSICHERHEIT[i]*100, '.1f') + ' %':>14} \"\n",
" f\"{(MU_SCHAETZUNG[i]-UNSICHERHEIT[i])*100:>11.1f} % \"\n",
" f\"{VOLA[i]*100:>12.1f} %\")\n",
"\n",
" # (a) nominal: vertraut den Schaetzungen\n",
" w_nominal = optimiere(MU_SCHAETZUNG)\n",
" # (b) robust: rechnet mit dem Worst Case der Box-Unsicherheitsmenge\n",
" w_robust = optimiere(MU_SCHAETZUNG - UNSICHERHEIT)\n",
"\n",
" print(\"\\n\" + \"-\" * 88)\n",
" print(f\"{'':<14} {'nominal':>22} {'robust':>22}\")\n",
" print(\"-\" * 88)\n",
" for i, name in enumerate(ASSETS):\n",
" print(f\"{name:<14} {w_nominal[i]*100:>21.1f} % {w_robust[i]*100:>21.1f} %\")\n",
"\n",
" # --- Bewertung in beiden Welten --------------------------------------\n",
" mu_worst = MU_SCHAETZUNG - UNSICHERHEIT\n",
" e_nom_gut, r_nom = kennzahlen(w_nominal, MU_SCHAETZUNG)\n",
" e_rob_gut, r_rob = kennzahlen(w_robust, MU_SCHAETZUNG)\n",
" e_nom_schlecht, _ = kennzahlen(w_nominal, mu_worst)\n",
" e_rob_schlecht, _ = kennzahlen(w_robust, mu_worst)\n",
"\n",
" print(\"\\n\" + \"-\" * 88)\n",
" print(f\"{'Bewertung':<34} {'nominales Portfolio':>22} {'robustes Portfolio':>22}\")\n",
" print(\"-\" * 88)\n",
" print(f\"{'Ertrag, wenn Schaetzung stimmt':<34} {e_nom_gut*100:>21.2f} % \"\n",
" f\"{e_rob_gut*100:>21.2f} %\")\n",
" print(f\"{'Ertrag im Worst Case':<34} {e_nom_schlecht*100:>21.2f} % \"\n",
" f\"{e_rob_schlecht*100:>21.2f} %\")\n",
" print(f\"{'Volatilitaet':<34} {r_nom*100:>21.2f} % {r_rob*100:>21.2f} %\")\n",
"\n",
" print(\"\\n\" + \"-\" * 88)\n",
" print(f\"Preis der Robustheit (Ertragsverzicht im Normalfall): \"\n",
" f\"{(e_nom_gut - e_rob_gut)*100:+.2f} Prozentpunkte\")\n",
" print(f\"Nutzen der Robustheit (Vorteil im Worst Case): \"\n",
" f\"{(e_rob_schlecht - e_nom_schlecht)*100:+.2f} Prozentpunkte\")\n",
" verhaeltnis = ((e_rob_schlecht - e_nom_schlecht)\n",
" / max(e_nom_gut - e_rob_gut, 1e-9))\n",
" print(f\"Verhaeltnis Nutzen/Preis: {verhaeltnis:.2f}\")\n",
" print(\" -> Werte > 1 bedeuten: Die Absicherung bringt im Ernstfall mehr,\")\n",
" print(\" als sie im Normalfall kostet.\")\n",
" print(\"=\" * 88)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}