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>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
133
OR_HTML_04/programme/Data_Snooping.py
Normal file
133
OR_HTML_04/programme/Data_Snooping.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Data_Snooping.py
|
||||
"""
|
||||
Kapitel Handelsmaschine: Die Selbsttaeuschung, die man einem Backtest nicht ansieht.
|
||||
|
||||
Backtest_Fallen.py zeigt drei Fehler, die IM Backtest stecken: Lookahead,
|
||||
Survivorship, vergessene Kosten. Alle drei kann man einem Programm ansehen,
|
||||
wenn man es liest.
|
||||
|
||||
Die beiden anderen - Overfitting und Data Snooping - entstehen nicht im
|
||||
Backtest, sondern in der ARBEITSWEISE davor. Sie hinterlassen im Code keine
|
||||
Spur. Ein Backtest, der zwanzigmal angepasst wurde, sieht am Ende genauso
|
||||
sauber aus wie einer, der beim ersten Versuch funktioniert hat.
|
||||
|
||||
Dieses Programm macht den Effekt messbar. Es testet Strategien, die
|
||||
NACHWEISLICH keinerlei Prognosekraft haben (ihre taeglichen Ergebnisse sind
|
||||
Zufallszahlen mit Erwartungswert exakt null), und zeigt, wie gut die jeweils
|
||||
BESTE davon aussieht - allein als Funktion der Anzahl der Versuche.
|
||||
|
||||
Benoetigt: numpy, scipy
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from scipy import stats
|
||||
|
||||
HANDELSTAGE = 1260 # fuenf Jahre
|
||||
TAGESSCHWANKUNG = 0.010 # 1 % taeglich
|
||||
WIEDERHOLUNGEN = 300 # damit die Tabelle nicht vom Zufall abhaengt
|
||||
NIVEAU = 0.05
|
||||
|
||||
RNG = np.random.default_rng(42)
|
||||
|
||||
|
||||
def bewerte(ergebnisse: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Sharpe-Kennzahl und einseitiger p-Wert je Strategie.
|
||||
|
||||
ergebnisse hat die Form (Strategien, Handelstage). Der p-Wert prueft die
|
||||
Nullhypothese 'mittleres Tagesergebnis ist null' - also genau die Frage,
|
||||
die ein Backtest beantworten soll.
|
||||
"""
|
||||
mittel = ergebnisse.mean(axis=1)
|
||||
streuung = ergebnisse.std(axis=1, ddof=1)
|
||||
sharpe = mittel / streuung * np.sqrt(252)
|
||||
t_wert = mittel / (streuung / np.sqrt(ergebnisse.shape[1]))
|
||||
return sharpe, stats.t.sf(t_wert, ergebnisse.shape[1] - 1)
|
||||
|
||||
|
||||
def probiere(anzahl_strategien: int) -> tuple[float, float, float, float]:
|
||||
"""Liefert (beste Sharpe, ihr p-Wert, Anteil scheinbar signifikanter,
|
||||
Anteil, der auch die Bonferroni-Huerde nimmt) - gemittelt ueber
|
||||
WIEDERHOLUNGEN unabhaengige Durchgaenge."""
|
||||
beste_sharpe, beste_p, anteil_signifikant, ueberlebt_bonferroni = [], [], [], []
|
||||
|
||||
for _ in range(WIEDERHOLUNGEN):
|
||||
# Der springende Punkt: Erwartungswert exakt 0. Keine dieser
|
||||
# Strategien hat einen echten Vorteil - per Konstruktion.
|
||||
ergebnisse = RNG.normal(0.0, TAGESSCHWANKUNG,
|
||||
(anzahl_strategien, HANDELSTAGE))
|
||||
sharpe, p_werte = bewerte(ergebnisse)
|
||||
|
||||
beste = int(sharpe.argmax())
|
||||
beste_sharpe.append(float(sharpe[beste]))
|
||||
beste_p.append(float(p_werte[beste]))
|
||||
anteil_signifikant.append(float((p_werte < NIVEAU).mean()))
|
||||
# Bonferroni: Wer n Tests macht, muss die Huerde durch n teilen.
|
||||
ueberlebt_bonferroni.append(
|
||||
float((p_werte < NIVEAU / anzahl_strategien).mean()))
|
||||
|
||||
return (float(np.mean(beste_sharpe)), float(np.mean(beste_p)),
|
||||
float(np.mean(anteil_signifikant)),
|
||||
float(np.mean(ueberlebt_bonferroni)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 78)
|
||||
print(" WIE GUT SIEHT DIE BESTE VON N STRATEGIEN AUS,")
|
||||
print(" WENN KEINE EINZIGE ETWAS TAUGT?")
|
||||
print("=" * 78)
|
||||
print(f"{HANDELSTAGE} Handelstage, taegliche Ergebnisse mit Erwartungswert "
|
||||
f"EXAKT null.")
|
||||
print(f"Jede Zeile ist der Mittelwert aus {WIEDERHOLUNGEN} unabhaengigen "
|
||||
f"Durchgaengen.\n")
|
||||
|
||||
print(f"{'getestet':>9} {'beste Sharpe':>14} {'ihr p-Wert':>12} "
|
||||
f"{'scheinbar sign.':>16}")
|
||||
print("-" * 78)
|
||||
ergebnisse = {}
|
||||
for anzahl in (1, 10, 100, 1000):
|
||||
sharpe, p_wert, anteil, _ = probiere(anzahl)
|
||||
ergebnisse[anzahl] = (sharpe, p_wert, anteil)
|
||||
print(f"{anzahl:9d} {sharpe:14.2f} {p_wert:12.4f} {anteil * 100:15.1f} %")
|
||||
print("-" * 78)
|
||||
|
||||
sharpe_100, p_100, _ = ergebnisse[100]
|
||||
sharpe_1000, p_1000, _ = ergebnisse[1000]
|
||||
|
||||
print("\nLesen Sie die dritte Spalte von unten nach oben:")
|
||||
print(f" Wer {1000} Varianten durchprobiert, findet eine mit p = {p_1000:.4f} -")
|
||||
print(" ein Wert, den jede Zeitschrift als 'hochsignifikant' druckt.")
|
||||
print(f" Wer {100} probiert, findet p = {p_100:.4f} und eine Sharpe-Kennzahl")
|
||||
print(f" von {sharpe_100:.2f} - damit geht man zum Vorgesetzten.")
|
||||
print(" Und keine dieser Strategien hat auch nur den Hauch eines Vorteils.")
|
||||
print()
|
||||
print("Die LETZTE Spalte bleibt dagegen konstant bei rund 5 %. Das ist kein")
|
||||
print("Widerspruch, sondern die Definition: Ein Test zum 5-%-Niveau irrt in")
|
||||
print("5 % der Faelle. Was sich aendert, ist nicht der Anteil, sondern die")
|
||||
print("ANZAHL - und weil man nur die beste Strategie praesentiert, sieht man")
|
||||
print("von allen Fehlversuchen genau einen: den erfolgreichsten.")
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
print(" WAS MAN DAGEGEN TUN KANN")
|
||||
print("=" * 78)
|
||||
print("1. ZAEHLEN. Notieren Sie, wie viele Varianten Sie ausprobiert haben -")
|
||||
print(" auch die verworfenen, auch die 'nur mal schnell'. Ohne diese Zahl")
|
||||
print(" ist kein p-Wert und keine Sharpe-Kennzahl interpretierbar.")
|
||||
print()
|
||||
print("2. KORRIGIEREN. Die Bonferroni-Korrektur teilt die Huerde durch die")
|
||||
print(" Zahl der Versuche:")
|
||||
for anzahl in (10, 100, 1000):
|
||||
print(f" bei {anzahl:5d} Versuchen: p < {NIVEAU / anzahl:.5f} statt "
|
||||
f"p < {NIVEAU:.2f}")
|
||||
print(" Sie ist konservativ, aber sie ist ehrlich. Fuer Handelsstrategien")
|
||||
print(" gibt es verfeinerte Varianten (Deflated Sharpe Ratio nach Bailey")
|
||||
print(" und Lopez de Prado), die dasselbe Prinzip verfolgen.")
|
||||
print()
|
||||
print("3. ZURUECKHALTEN. Legen Sie einen Zeitraum beiseite, BEVOR Sie")
|
||||
print(" anfangen, und ruehren Sie ihn bis zum Schluss nicht an. Er ist")
|
||||
print(" der einzige Test, den Sie nicht durch Probieren verderben koennen -")
|
||||
print(" und er ist genau einmal verwendbar.")
|
||||
print("=" * 78)
|
||||
Loading…
Reference in a new issue