operations_research/OR_HTML_04/programme/Data_Snooping.py
dschlueter b7af2f1d9a 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

133 lines
5.8 KiB
Python

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