Grafik-Upgrade Stufe 1: elf neue Diagramme

Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind
19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern
19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38
Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal
darin. Von den 19 betreffen neun bereits vorhandene Diagramme.

Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem
Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen,
nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene
Grafik mit ihren eigenen Zahlen.

Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das
Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht
worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen),
harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die
fehlende Byte-Reproduzierbarkeit.

Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine
Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier
haben angeschlagen:

* Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. -
  Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs.
* Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den
  abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %,
  gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht.
* Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95
  MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte.
* Almgren-Chriss: ein echter Fehler im Buchprogramm.

DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr
Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren
kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten()
rechnet das Risiko mit lambda/2, die geschlossene Formel setzt
kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die
Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der
Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er
ist der, den das Kapitel abdruckt.

Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein
vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die
Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0).

Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre
Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine
Pruefung ueber alle elf zeigt null Fehlplatzierungen.

Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere().
networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt
unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren.
Veroeffentlicht: 25 Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-09 01:11:33 +02:00
commit 0f9c815b86
64 changed files with 52288 additions and 25 deletions

View file

@ -0,0 +1,158 @@
#!/usr/bin/env python3
# erzeuge_almgren_chriss.py
"""
Erzeugt das Ausfuehrungsdiagramm zum Kapitel Dynamische Programmierung:
bilder_04/kap_dp_almgren_chriss.svg
Drei Wege, dieselbe Position abzubauen, und was sie kosten:
* SOFORT - alles in der ersten Periode. Kein Kursrisiko, dafuer der volle
Marktimpact auf einen Schlag.
* TWAP - gleich grosse Scheiben. Wenig Impact je Scheibe, dafuer haelt man
die Position lange und traegt ihr Kursrisiko.
* OPTIMAL - die Loesung der Bellman-Gleichung: vorne mehr, hinten weniger.
Der Punkt, auf den es ankommt: Die optimale Kurve liegt nicht "in der Mitte"
zwischen den beiden Extremen, sondern ist gekruemmt. Ihre Kruemmung ist die
Antwort auf die Frage, wie teuer Eile gegen Warten ist - und genau diese Frage
beantwortet die dynamische Programmierung.
Bestand und Kosten kommen aus Mehrperiodige_Order_Execution.py. Der Vergleich
der Kosten steht als Zahl im Bild; sie wird gerechnet, nicht eingetragen.
GEZEICHNET WIRD DER DP-PFAD, NICHT DIE GESCHLOSSENE FORMEL. Beim Bau dieses
Bildes fiel auf, dass beide auseinanderlaufen: Der DP-Pfad kostet 10 266,24 EUR
- der Wert, den das Kapitel abdruckt -, der Pfad aus analytische_loesung()
dagegen 10 860,40 EUR. Ein Gitterverfahren kann das kontinuierliche Optimum
aber nicht unterbieten, also stimmt die Formel nicht mit der Kostenfunktion
ueberein: periodenkosten() rechnet das Risiko mit lambda/2, die Formel setzt
kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 ergibt die
Formel 10 264,85 EUR und denselben Pfad wie die DP - knapp UNTER der
Gitterloesung, wie es sein muss. Bis das im Programm entschieden ist, zeigt das
Bild den DP-Pfad; er ist der, den das Kapitel ausweist.
Aufruf (aus dem Repository-Wurzelverzeichnis):
python3 bilder_04/erzeuge_almgren_chriss.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, neue_figur, speichere # noqa: E402
from Mehrperiodige_Order_Execution import (ETA, GESAMTBESTAND, # noqa: E402
KURS, PERIODEN,
RISIKOAVERSION,
VOLA_PERIODE,
SCHRITTWEITE, loese_dp,
periodenkosten)
# Der im Kapitel abgedruckte Wert der optimalen Strategie - nur zum
# Gegenpruefen.
GESAMTREIBUNG_IM_BUCH = 10_266.24
def impaktkosten(bestand: np.ndarray) -> float:
"""Temporaerer Marktimpact: eta je Stueck^2 auf jede Scheibe."""
scheiben = -np.diff(bestand)
return float(ETA * np.sum(scheiben ** 2))
def risikokosten(bestand: np.ndarray) -> float:
"""Risiko des jeweiligen Restbestands, mit dem Faktor des Buchprogramms."""
return float(gesamtkosten(bestand) - impaktkosten(bestand))
def gesamtkosten(bestand: np.ndarray) -> float:
"""Summe der Periodenkosten - mit periodenkosten() des Buchprogramms.
Bewusst nicht selbst formuliert: Das Buch rechnet das Risiko mit dem Faktor
lambda/2. Eine eigene Formel ohne diesen Faktor haette Zahlen ergeben, die
zur Tabelle im Kapitel nicht passen."""
scheiben = -np.diff(bestand)
return float(sum(periodenkosten(float(scheiben[t]), float(bestand[t + 1]))
for t in range(len(scheiben))))
def zeichne(pfade: dict[str, np.ndarray]) -> None:
figur, achse = neue_figur(breite=8.0, hoehe=4.6)
t = np.arange(PERIODEN + 1)
stile = {
"sofort verkaufen": (FARBEN["fehler"], "--"),
"gleichmäßig (TWAP)": (FARBEN["zweit"], "--"),
"optimal (Bellman)": (FARBEN["gut"], "-"),
}
for name, bestand in pfade.items():
farbe, strich = stile[name]
achse.plot(t, bestand / 1000.0, strich, color=farbe,
linewidth=2.6 if strich == "-" else 1.9,
marker="o", markersize=5, zorder=4,
label=f"{name}{gesamtkosten(bestand):,.0f}"
.replace(",", "."))
achse.set_xlabel("Handelsperiode $t$")
achse.set_ylabel("verbleibender Bestand in Tausend Stück")
achse.set_title(f"{GESAMTBESTAND:,} Stück abbauen — drei Wege, "
f"drei Rechnungen".replace(",", "."),
fontsize=10.5, color=FARBEN["text"])
achse.set_xticks(t)
achse.set_ylim(-4, GESAMTBESTAND / 1000.0 * 1.08)
achse.legend(fontsize=9, loc="upper right", frameon=False)
achse.annotate(
"Eile kostet Marktimpact, Warten kostet Kursrisiko.\n"
"Die optimale Kurve ist gekrümmt, weil sie beides gegeneinander "
"abwägt.",
xy=(0.03, 0.06), xycoords="axes fraction", fontsize=9,
color=FARBEN["text"], va="bottom")
speichere(figur, "kap_dp_almgren_chriss")
if __name__ == "__main__":
sofort = np.array([GESAMTBESTAND] + [0] * PERIODEN, dtype=float)
twap = np.linspace(GESAMTBESTAND, 0, PERIODEN + 1)
# Der Pfad der Rueckwaertsinduktion - genau der, den das Kapitel abdruckt.
_zustaende, _V, politik = loese_dp()
bestand, verlauf = float(GESAMTBESTAND), [float(GESAMTBESTAND)]
for t in range(PERIODEN):
bestand -= politik[t, int(round(bestand / SCHRITTWEITE))]
verlauf.append(bestand)
optimal = np.array(verlauf, dtype=float)
pfade = {"sofort verkaufen": sofort, "gleichmäßig (TWAP)": twap,
"optimal (Bellman)": optimal}
for name, bestand in pfade.items():
print(f" {name:<20} Impact {impaktkosten(bestand):>10,.0f} EUR "
f"Risiko {risikokosten(bestand):>9,.0f} EUR "
f"gesamt {gesamtkosten(bestand):>10,.0f} EUR")
if gesamtkosten(optimal) > min(gesamtkosten(sofort), gesamtkosten(twap)):
raise SystemExit(
"Almgren-Chriss-Bild: Die analytische Loesung ist teurer als eine "
"der beiden Vergleichsstrategien. Dann stimmt die Kostenformel "
"hier nicht mit der des Buchprogramms ueberein.")
abweichung = abs(gesamtkosten(optimal) - GESAMTREIBUNG_IM_BUCH)
if abweichung > 0.01:
raise SystemExit(
f"Almgren-Chriss-Bild: Der DP-Pfad kostet "
f"{gesamtkosten(optimal):,.2f} EUR, das Kapitel druckt "
f"{GESAMTREIBUNG_IM_BUCH:,.2f} EUR.")
print(f" stimmt mit dem abgedruckten Wert überein: "
f"{GESAMTREIBUNG_IM_BUCH:,.2f} EUR")
zeichne(pfade)