158 lines
6.5 KiB
Python
158 lines
6.5 KiB
Python
|
|
#!/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)
|