#!/usr/bin/env python3

# Backtest_Fallen.py
"""
Kapitel Handelsmaschine: Wie leicht man sich selbst betruegt.

Demonstriert an SIMULIERTEN Daten (also ohne jede echte Prognosekraft),
wie gross die scheinbare Ueberrendite durch typische Backtest-Fehler wird.
Weil die Daten reines Rauschen sind, ist JEDE positive Ueberrendite ein Artefakt.
"""

import numpy as np
import pandas as pd


def erzeuge_zufallsmarkt(tage=1260, titel=15, seed=7):
    """Reines Rauschen: kein Titel hat echte Prognosekraft."""
    rng = np.random.default_rng(seed)
    renditen = rng.normal(0.0003, 0.015, size=(tage, titel))
    index = pd.bdate_range("2020-01-01", periods=tage)
    return pd.DataFrame(renditen, index=index,
                        columns=[f"T{i:02d}" for i in range(titel)])


def strategie(renditen, lookahead=False, survivorship=False,
              ohne_kosten=False, gebuehr=0.0015):
    """Einfache Momentum-Strategie mit optional eingebauten Fehlern."""
    fenster = 60
    kapital = [100.0]
    gewichte = np.ones(renditen.shape[1]) / renditen.shape[1]

    daten = renditen
    if survivorship:
        # FEHLER: nur die im Nachhinein besten Titel ins Universum lassen
        beste = renditen.sum().nlargest(renditen.shape[1] // 2).index
        daten = renditen[beste]
        gewichte = np.ones(len(beste)) / len(beste)

    for t in range(fenster, len(daten) - 1):
        if t % 21 == 0:                                  # monatlich
            if lookahead:
                # FEHLER: Signal nutzt die Rendite von MORGEN
                signal = daten.iloc[t + 1].values
            else:
                signal = daten.iloc[t - fenster:t].mean().values

            neue = (signal > np.median(signal)).astype(float)
            neue = neue / neue.sum() if neue.sum() > 0 else gewichte
            if not ohne_kosten:
                kapital[-1] *= (1 - np.abs(neue - gewichte).sum() * gebuehr)
            gewichte = neue

        kapital.append(kapital[-1] * (1 + float(gewichte @ daten.iloc[t + 1].values)))
    return np.array(kapital)


def cagr(verlauf, jahre):
    return (verlauf[-1] / verlauf[0]) ** (1 / jahre) - 1


if __name__ == "__main__":
    renditen = erzeuge_zufallsmarkt()
    jahre = len(renditen) / 252
    markt = 100 * (1 + renditen.mean(axis=1)).cumprod().values

    print("=" * 84)
    print("  DIE FUENF SELBSTTAEUSCHUNGEN - GEMESSEN AN REINEM RAUSCHEN")
    print("=" * 84)
    print("Die Daten sind zufaellig erzeugt. Es gibt KEINE echte Prognosekraft.")
    print("Jede Ueberrendite unten ist daher ein reines Artefakt.\n")
    print(f"{'Variante':<44} {'CAGR':>9} {'ggue. Markt':>13}")
    print("-" * 84)

    basis = cagr(markt, jahre)
    print(f"{'Markt (Gleichgewichtung, Referenz)':<44} {basis*100:>8.2f} % "
          f"{0.0:>12.2f} %")

    varianten = [
        ("Ehrlich: Signal aus Vergangenheit, mit Kosten", {}),
        ("FEHLER 1: Lookahead (Signal kennt morgen)", {"lookahead": True}),
        ("FEHLER 2: Survivorship (nur Gewinner im Universum)", {"survivorship": True}),
        ("FEHLER 3: Transaktionskosten ignoriert", {"ohne_kosten": True}),
        ("FEHLER 1+2+3 kombiniert", {"lookahead": True, "survivorship": True,
                                     "ohne_kosten": True}),
    ]
    for name, argumente in varianten:
        verlauf = strategie(renditen, **argumente)
        wert = cagr(verlauf, jahre)
        print(f"{name:<44} {wert*100:>8.2f} % {(wert-basis)*100:>+12.2f} %")

    print("-" * 84)
    print("FEHLER 4 (Overfitting) und FEHLER 5 (Data Snooping) lassen sich so nicht")
    print("zeigen - sie entstehen erst durch WIEDERHOLTES Probieren. Faustregel:")
    print("Wer 20 Strategien testet, findet auch in reinem Rauschen eine mit")
    print("Signifikanz auf dem 5-%-Niveau. Genau das ist der Punkt.")
    print("=" * 84)
