#!/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)