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