133 lines
5.8 KiB
Python
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)
|