365 lines
17 KiB
Text
365 lines
17 KiB
Text
|
|
{
|
||
|
|
"cells": [
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"# Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten\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": [
|
||
|
|
"## 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"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
}
|