operations_research/bilder_04/erzeuge_var_vs_cvar.py
2026-09-09 07:37:07 +02:00

169 lines
7.3 KiB
Python

#!/usr/bin/env python3
# erzeuge_var_vs_cvar.py
"""
Erzeugt das Subadditivitaets-Bild zum Kapitel CVaR:
bilder_04/kap_cvar_subadditivitaet.svg
ERGAENZT kap_cvar_var_vergleich.svg, ersetzt es nicht: Jenes zeigt, WAS die
beiden Kennzahlen sind - eine Schwelle und ein Mittelwert. Dieses zeigt, warum
der Unterschied zaehlt.
Gerechnet wird das Gegenbeispiel aus Teil 2 von VaR_CVaR_Demo.py: zwei
unabhaengige Anleihen, je 100 EUR Nominal, Ausfallwahrscheinlichkeit 4 %,
sonst 2 EUR Kupon. Einzeln meldet der VaR fuer jede Anleihe einen GEWINN von
2 EUR - denn 4 % liegen unter der 5-%-Schwelle, der Ausfall faellt also aus der
Betrachtung heraus. Im Portfolio steigt die Wahrscheinlichkeit mindestens eines
Ausfalls auf 7,8 % und damit UEBER die Schwelle; der VaR springt auf 98 EUR.
Das ist die Aussage des Bildes: Der VaR behauptet, Streuung habe das Risiko um
102 EUR ERHOEHT. Der CVaR, der den ganzen Rand mittelt statt ihn abzuschneiden,
kommt auf 101,33 EUR gegenueber 159,24 EUR - Streuung senkt das Risiko, wie es
sein muss.
Die Ziehungsreihenfolge des Buchprogramms wird nachgespielt: default_rng(2026),
und Teil 1 des Programms zieht nichts, also sind die beiden Anleihen die ersten
Ziehungen. Deshalb stehen im Bild genau die Zahlen der abgedruckten Tabelle;
pruefe_gegen_buch() bricht ab, wenn nicht.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_var_vs_cvar.py
Benoetigt: numpy, matplotlib
"""
from __future__ import annotations
import os
import sys
import numpy as np
HIER = os.path.dirname(os.path.abspath(__file__))
BASIS = os.path.dirname(HIER)
sys.path.insert(0, HIER)
sys.path.insert(0, os.path.join(
BASIS, "Operations_Research_mit_Python_Version_04_Programme"))
from stil_04 import FARBEN, speichere # noqa: E402
import matplotlib.pyplot as plt # noqa: E402
from VaR_CVaR_Demo import cvar_rockafellar, var_quantil # noqa: E402
# Instanz wortgleich zu Teil 2 von VaR_CVaR_Demo.py.
SAAT = 2026
ZIEHUNGEN = 2_000_000
AUSFALLWAHRSCHEINLICHKEIT = 0.04
VERLUST_BEI_AUSFALL = 100.0
KUPON = 2.0
# Die im Kapitel abgedruckte Tabelle; nur zum Gegenpruefen.
TABELLE_IM_BUCH = {
"Anleihe A allein": (-2.00, 79.59),
"Anleihe B allein": (-2.00, 79.66),
"Portfolio A+B": (98.00, 101.33),
"Summe der Einzelwerte": (-4.00, 159.24),
}
def rechne() -> dict[str, tuple[float, float]]:
"""Dieselben Ziehungen wie im Buchprogramm - siehe Modulkommentar."""
rng = np.random.default_rng(SAAT)
verlust_a = np.where(rng.random(ZIEHUNGEN) < AUSFALLWAHRSCHEINLICHKEIT,
VERLUST_BEI_AUSFALL, -KUPON)
verlust_b = np.where(rng.random(ZIEHUNGEN) < AUSFALLWAHRSCHEINLICHKEIT,
VERLUST_BEI_AUSFALL, -KUPON)
werte = {}
for name, v in (("Anleihe A allein", verlust_a),
("Anleihe B allein", verlust_b),
("Portfolio A+B", verlust_a + verlust_b)):
werte[name] = (var_quantil(v), cvar_rockafellar(v))
werte["Summe der Einzelwerte"] = tuple(
werte["Anleihe A allein"][i] + werte["Anleihe B allein"][i]
for i in (0, 1))
return werte
def pruefe_gegen_buch(werte) -> None:
"""Die Tafel muss dieselben Zahlen zeigen wie die Tabelle im Kapitel."""
for name, (soll_var, soll_cvar) in TABELLE_IM_BUCH.items():
ist_var, ist_cvar = werte[name]
if abs(ist_var - soll_var) > 0.005 or abs(ist_cvar - soll_cvar) > 0.005:
raise SystemExit(
f"Subadditivitaets-Bild: '{name}' ergibt VaR {ist_var:.2f} / "
f"CVaR {ist_cvar:.2f}, das Kapitel druckt {soll_var:.2f} / "
f"{soll_cvar:.2f} ab.")
# Die Aussage selbst - nicht nur die Zahlen.
if not werte["Portfolio A+B"][0] > werte["Summe der Einzelwerte"][0]:
raise SystemExit("Subadditivitaets-Bild: Der VaR verletzt die "
"Subadditivitaet hier nicht - dann zeigt die Tafel "
"das Gegenteil ihrer Überschrift.")
if not werte["Portfolio A+B"][1] <= werte["Summe der Einzelwerte"][1]:
raise SystemExit("Subadditivitaets-Bild: Der CVaR ist hier nicht "
"subadditiv. Das widerspricht dem Kapiteltext.")
def euro(wert: float) -> str:
return f"{wert:,.2f}".replace(",", "@").replace(".", ",").replace("@", ".")
def zeichne(werte) -> None:
figur, achsen = plt.subplots(1, 2, figsize=(10.2, 4.4))
namen = ["Anleihe A allein", "Anleihe B allein",
"Summe der Einzelwerte", "Portfolio A+B"]
y = np.arange(len(namen))[::-1]
for achse, index, titel, urteil, farbe in (
(achsen[0], 0, "VaR 95 %",
"Portfolio teurer als die Summe —\nSubadditivität verletzt",
FARBEN["fehler"]),
(achsen[1], 1, "CVaR 95 %",
"Portfolio billiger als die Summe —\nsubadditiv, also kohärent",
FARBEN["gut"])):
wert = [werte[n][index] for n in namen]
farben = [FARBEN["gedaempft"], FARBEN["gedaempft"],
FARBEN["zweit"], farbe]
achse.barh(y, wert, height=0.6, color=farben, zorder=3)
# Balken muessen bei null beginnen, sonst luegt ihre Laenge. Der Rand
# links richtet sich nach der Spannweite, damit die Zahl an einem
# negativen Balken nicht an der Achsenbeschriftung klebt.
spanne = max(wert) - min(0.0, min(wert))
achse.set_xlim(min(0.0, min(wert)) - 0.20 * spanne, max(wert) * 1.22)
for stelle, w in enumerate(wert):
abstand = 0.015 * spanne
achse.text(w + (abstand if w >= 0 else -abstand), y[stelle],
euro(w), va="center", ha="left" if w >= 0 else "right",
fontsize=9, color=FARBEN["text"], zorder=4)
achse.axvline(0, color=FARBEN["text"], linewidth=1.0, zorder=2)
achse.set_yticks(y)
achse.set_yticklabels(namen if index == 0 else [], fontsize=9,
color=FARBEN["text"])
achse.set_title(titel, fontsize=11, color=FARBEN["text"], pad=34)
achse.set_xlabel("Verlust in € (negativ = Gewinn)")
achse.grid(axis="y", visible=False)
# Das Urteil ueber der Achse, nicht in ihr - sonst stoesst es an die
# Zahl am obersten Balken.
achse.text(0.5, 1.008, urteil, transform=achse.transAxes,
fontsize=8.5, color=farbe, ha="center", va="bottom",
fontweight="bold")
figur.suptitle("Zwei unabhängige Anleihen, je 4 % Ausfallrisiko — "
"und zwei Urteile über dieselbe Diversifikation",
fontsize=11, color=FARBEN["text"], y=1.02)
speichere(figur, "kap_cvar_subadditivitaet")
if __name__ == "__main__":
werte = rechne()
print(f" {'':<24} {'VaR 95%':>10} {'CVaR 95%':>10}")
for name in ("Anleihe A allein", "Anleihe B allein", "Portfolio A+B",
"Summe der Einzelwerte"):
print(f" {name:<24} {werte[name][0]:>10.2f} {werte[name][1]:>10.2f}")
pruefe_gegen_buch(werte)
print(" stimmt mit der abgedruckten Tabelle überein")
print(f" VaR: Portfolio {werte['Portfolio A+B'][0]:.2f} > Summe "
f"{werte['Summe der Einzelwerte'][0]:.2f} — verletzt")
print(f" CVaR: Portfolio {werte['Portfolio A+B'][1]:.2f} ≤ Summe "
f"{werte['Summe der Einzelwerte'][1]:.2f} — kohärent")
zeichne(werte)