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
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
# erzeuge_var_cvar.py
|
|
|
|
|
"""
|
|
|
|
|
Erzeugt das VaR/CVaR-Diagramm zum Kapitel CVaR:
|
|
|
|
|
|
|
|
|
|
bilder_04/kap_cvar_var_vergleich.svg statisch, fuer das PDF
|
|
|
|
|
|
|
|
|
|
Gezeigt wird die Verteilung simulierter Tagesrenditen mit beiden Kennzahlen:
|
|
|
|
|
Der VaR ist die SCHWELLE, unter die nur 5 % der Tage fallen; der CVaR ist der
|
|
|
|
|
MITTELWERT genau dieser 5 %. Der Abstand zwischen beiden Linien ist die
|
|
|
|
|
Information, die der VaR wegwirft.
|
|
|
|
|
|
|
|
|
|
Zur Instanz: Die Renditen stammen aus einer t-Verteilung mit drei
|
|
|
|
|
Freiheitsgraden, auf Standardabweichung 1 normiert und dann auf eine realistische
|
|
|
|
|
Tagesvolatilitaet skaliert - dasselbe Fat-Tail-Modell, mit dem Teil 1 von
|
|
|
|
|
VaR_CVaR_Demo.py rechnet. Die beiden Kennzahlen werden mit den Funktionen aus
|
|
|
|
|
demselben Programm bestimmt, insbesondere der CVaR ueber Rockafellar-Uryasev
|
|
|
|
|
statt ueber den naheliegenden, aber falschen Mittelwert aller Werte jenseits des
|
|
|
|
|
VaR.
|
|
|
|
|
|
|
|
|
|
Die Zahlen, die das Skript ausgibt, stehen so auch im Kapiteltext. Wer die
|
|
|
|
|
Instanz aendert, muss beides nachziehen - deshalb druckt das Skript sie.
|
|
|
|
|
|
|
|
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
|
|
|
|
python3 bilder_04/erzeuge_var_cvar.py
|
|
|
|
|
|
|
|
|
|
Benoetigt: numpy, scipy, matplotlib
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
import matplotlib
|
|
|
|
|
matplotlib.use("Agg")
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
import numpy as np
|
|
|
|
|
from scipy import stats
|
|
|
|
|
|
|
|
|
|
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
|
|
|
|
|
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
|
|
|
|
|
|
|
|
|
|
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
BILDER = os.path.join(BASIS, "bilder_04")
|
|
|
|
|
|
|
|
|
|
INDIGO, AMBER, ROT = "#4338ca", "#b45309", "#be123c"
|
|
|
|
|
|
|
|
|
|
TAGE = 20_000 # simulierte Handelstage
|
|
|
|
|
FREIHEITSGRADE = 3 # Fat-Tail-Modell des Kapitels
|
|
|
|
|
TAGESVOLA = 0.014 # 1,4 % Tagesschwankung - realistisch fuer ein Aktienportfolio
|
|
|
|
|
TAGESDRIFT = 0.0003 # leicht positiver Erwartungswert
|
|
|
|
|
ALPHA = 0.95
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Kennzahlen: wortgleich zu VaR_CVaR_Demo.py -----------------------------
|
|
|
|
|
|
|
|
|
|
def var_quantil(verluste: np.ndarray, alpha: float = ALPHA) -> float:
|
|
|
|
|
"""VaR = Quantil der Verlustverteilung (Verluste positiv, Gewinne negativ)."""
|
|
|
|
|
return float(np.quantile(verluste, alpha))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cvar_rockafellar(verluste: np.ndarray, alpha: float = ALPHA) -> float:
|
|
|
|
|
"""CVaR ueber min_gamma { gamma + 1/(1-alpha) * E[max(Verlust - gamma, 0)] }.
|
|
|
|
|
|
|
|
|
|
Der naheliegende Weg "Mittelwert aller Werte >= VaR" ist falsch, sobald die
|
|
|
|
|
Verteilung Atome hat. Hier hat sie keine, aber die Formel bleibt dieselbe
|
|
|
|
|
wie im Buchprogramm - und nur so ist der Wert im Bild derselbe wie dort.
|
|
|
|
|
"""
|
|
|
|
|
kandidaten = np.unique(verluste)
|
|
|
|
|
return float(min(g + np.mean(np.maximum(verluste - g, 0.0)) / (1.0 - alpha)
|
|
|
|
|
for g in kandidaten))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def renditen(saat: int = 2026) -> np.ndarray:
|
|
|
|
|
"""Tagesrenditen aus dem Fat-Tail-Modell des Kapitels."""
|
|
|
|
|
rng = np.random.default_rng(saat)
|
|
|
|
|
verteilung = stats.t(df=FREIHEITSGRADE)
|
|
|
|
|
roh = verteilung.rvs(size=TAGE, random_state=rng) / verteilung.std()
|
|
|
|
|
return TAGESDRIFT + TAGESVOLA * roh
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AUSSCHNITT = 8.0 # Prozent - der sichtbare Bereich der x-Achse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def zeichne(werte: np.ndarray, var: float, cvar: float) -> None:
|
|
|
|
|
"""Zeichnet die Verteilung mit beiden Kennzahlen.
|
|
|
|
|
|
|
|
|
|
Die x-Achse ist bewusst auf +/- 8 % beschnitten. Eine t-Verteilung mit drei
|
|
|
|
|
Freiheitsgraden hat keine endliche Woelbung: Einzelne Tage liegen bei +70 %,
|
|
|
|
|
und ungeschnitten quetscht sich die gesamte Verteilung zu einem Strich
|
|
|
|
|
zusammen. Wie viele Tage ausserhalb liegen, steht im Bild - sie sind nicht
|
|
|
|
|
verschwiegen, sondern gerade der Punkt.
|
|
|
|
|
"""
|
|
|
|
|
figur, achse = plt.subplots(figsize=(8.0, 4.6))
|
|
|
|
|
prozent = werte * 100
|
|
|
|
|
sichtbar = prozent[np.abs(prozent) <= AUSSCHNITT]
|
|
|
|
|
achse.hist(sichtbar, bins=90, color=INDIGO, alpha=0.75,
|
|
|
|
|
edgecolor="white", linewidth=0.3)
|
|
|
|
|
achse.set_xlim(-AUSSCHNITT, AUSSCHNITT)
|
|
|
|
|
|
|
|
|
|
achse.axvspan(-AUSSCHNITT, -var * 100, color=ROT, alpha=0.08, zorder=0)
|
|
|
|
|
achse.axvline(-var * 100, color=AMBER, linewidth=2.0, linestyle="--")
|
|
|
|
|
achse.axvline(-cvar * 100, color=ROT, linewidth=2.0)
|
|
|
|
|
|
|
|
|
|
def komma(wert: float, stellen: int = 2) -> str:
|
|
|
|
|
return f"{wert:.{stellen}f}".replace(".", ",")
|
|
|
|
|
|
|
|
|
|
# Platz oberhalb der Verteilung schaffen, damit keine Beschriftung in den
|
|
|
|
|
# Balken liegt - die Aussage steht sonst buchstaeblich im Weg.
|
|
|
|
|
hoehe = achse.get_ylim()[1]
|
|
|
|
|
achse.set_ylim(0, hoehe * 1.30)
|
|
|
|
|
|
|
|
|
|
achse.annotate(f"VaR 95 % = {komma(var * 100)} %\ndie Schwelle",
|
|
|
|
|
(-var * 100, hoehe * 1.24), textcoords="offset points",
|
|
|
|
|
xytext=(8, 0), fontsize=9, color=AMBER, va="top")
|
|
|
|
|
achse.annotate(f"CVaR 95 % = {komma(cvar * 100)} %\nder Mittelwert dahinter",
|
|
|
|
|
(-cvar * 100, hoehe * 1.24), textcoords="offset points",
|
|
|
|
|
xytext=(-8, 0), fontsize=9, color=ROT, va="top", ha="right")
|
|
|
|
|
|
|
|
|
|
anteil = float(np.mean(werte <= -var))
|
|
|
|
|
achse.annotate(f"die schlechtesten\n{anteil * 100:.0f} % der Tage",
|
|
|
|
|
((-AUSSCHNITT - var * 100) / 2, hoehe * 0.55),
|
|
|
|
|
fontsize=8.5, color="#475569", ha="center")
|
|
|
|
|
|
|
|
|
|
draussen = int(np.sum(np.abs(prozent) > AUSSCHNITT))
|
|
|
|
|
schlimmster = float(prozent.min())
|
|
|
|
|
achse.annotate(f"{draussen} Tage liegen außerhalb dieses Ausschnitts,\n"
|
|
|
|
|
f"der schlechteste bei {komma(schlimmster, 1)} %.\n"
|
|
|
|
|
f"Genau sie zählt der VaR nicht mit.",
|
|
|
|
|
xy=(0.985, 0.80), xycoords="axes fraction", fontsize=8.5,
|
|
|
|
|
color="#475569", ha="right", va="top")
|
|
|
|
|
|
|
|
|
|
achse.set_xlabel("Tagesrendite des Portfolios (%)")
|
|
|
|
|
achse.set_ylabel("Häufigkeit (von 20 000 Tagen)")
|
|
|
|
|
achse.set_title("Der VaR ist eine Schwelle, der CVaR ein Mittelwert",
|
|
|
|
|
fontsize=11)
|
|
|
|
|
achse.grid(axis="y", linestyle=":", alpha=0.45)
|
|
|
|
|
for rand in ("top", "right"):
|
|
|
|
|
achse.spines[rand].set_visible(False)
|
|
|
|
|
figur.tight_layout()
|
|
|
|
|
|
|
|
|
|
os.makedirs(BILDER, exist_ok=True)
|
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE PNG-ZWEITFASSUNGEN SIND WEG. Jedes Diagramm lag doppelt vor, als SVG
und als PNG, und kein einziges src=/href= in der Website zeigte je auf ein
PNG. Der Build kopierte sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB,
die bei jeder Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
FUND 2: spiegle_bilder() legte leere Verzeichnisse auf dem Webserver an. Der
Dateifilter arbeitete korrekt, aber os.walk durchlief auch __pycache__/, und
os.makedirs() erzeugte es am Ziel. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. Excel_Bruecke.py SCHREIBT NEBEN DAS SKRIPT statt ins
Arbeitsverzeichnis. Es benutzte blanke relative Namen; wer es aus der
Repository-Wurzel startete, verstreute dort produktionsmix.xlsx und
produktionsmix_ergebnis.xlsx. Jetzt wie die vier anderen schreibenden
Programme ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
|
|
|
for endung in ("svg",):
|
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
|
|
|
pfad = os.path.join(BILDER, f"kap_cvar_var_vergleich.{endung}")
|
|
|
|
|
figur.savefig(pfad, format=endung, dpi=160,
|
|
|
|
|
metadata={"Date": None} if endung == "svg" else None)
|
|
|
|
|
print(f"geschrieben: {pfad}")
|
|
|
|
|
plt.close(figur)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
werte = renditen()
|
|
|
|
|
verluste = -werte
|
|
|
|
|
var = var_quantil(verluste)
|
|
|
|
|
cvar = cvar_rockafellar(verluste)
|
|
|
|
|
schlimmster = float(verluste.max())
|
|
|
|
|
print(f"{TAGE} simulierte Tage, t(df={FREIHEITSGRADE}), "
|
|
|
|
|
f"Tagesvola {TAGESVOLA * 100:.1f} %")
|
|
|
|
|
print(f"VaR 95 % = {var * 100:.2f} % (Schwelle)")
|
|
|
|
|
print(f"CVaR 95 % = {cvar * 100:.2f} % (Mittel der schlechtesten 5 %)")
|
|
|
|
|
print(f"schlimmster Einzeltag: {schlimmster * 100:.2f} %")
|
|
|
|
|
print(f"Verhaeltnis CVaR/VaR: {cvar / var:.2f}")
|
|
|
|
|
zeichne(werte, var, cvar)
|