diff --git a/CLAUDE.md b/CLAUDE.md index bc3ce02..791efd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -259,6 +259,25 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Anteil, WR-Verfall, PF<1, Einzelverlust). **Wöchentlich** laufen lassen (B4/B5). - **Vor „fertig":** immer `python -m py_compile `; JS grob via Klammern- Balance prüfen (kein node im Env). +- **⚠⚠ KONFIDENZINTERVALLE statt Bauchgefühl (`backtest_cost_gate.ci95`, seit + 2026-08-07).** Das Projekt entschied bis dahin über **Punktschätzungen** plus die + 2-Stichproben-Regel; ob ein ØR von +0,12 bei n=30 überhaupt von null zu + unterscheiden war, musste jedes Mal im Kopf abgeschätzt werden. `kennzahlen()` + liefert jetzt zusätzlich **`ci`** = 95-%-Bootstrap-Intervall des Mittelwerts + (Perzentil, 2000 Resamples, `random_state` fest → reproduzierbar; ab n≥20, sonst + `None`). ⚠ **Rein additiv** — alle 11 Aufrufer lesen weiter nur `n/wr/oer/sr/pf`, + keine dokumentierte Zahl verschiebt sich. + ✅ **Sofortiger Ertrag:** der erste Lauf hat das Ergebnis vom 06.08. korrigiert + (s. „Verworfene Setups auf der Ausführungs-Achse" — aus „3 von 7 tragen" wird + „1 gesichert, 2 unklar"). + ⚠⚠ **GRENZE, die man kennen muss:** der Bootstrap unterstellt **unabhängige** + Ziehungen. Für die sequentielle Sim mit EINEM Slot stimmt das näherungsweise + (jeder Trade beginnt nach dem Exit des vorigen). Für **überlappende** + Beobachtungen ist er **zu optimistisch** — stündliche Werte mit 24-h-Horizont + teilen 23 von 24 Stunden, das effektive n ist ~n/24. Genau daran ist + `analyze_hl_funding.py` am 06.08. aufgelaufen (dort wird **entüberlappt** statt + gebootstrappt). Für solche Fälle bräuchte es einen **Block-Bootstrap** — + `ci95` ist dafür NICHT gedacht. - **⚠⚠ DIE `\n`-FALLE — und der Scan dagegen (`tools/check_nfalle.py`, 2026-08-07).** Wird Code über ein Shell-**Heredoc** geschrieben, macht die Shell aus einem gemeinten `\n` einen **echten Zeilenumbruch mitten im String-Literal**. Real an @@ -412,6 +431,27 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: vier Varianten (bestätigt 2026-07-17). ✅ Die drei Überlebenden sind **nachbar-robust** (PDH/PDL über die Gültigkeitsdauer, Asian Range über das Fenster, Equal H/L über Toleranz und Gültigkeit — 11 Zellen, keine kippt). + ⚠⚠ **NACHTRAG 2026-08-07 — mit 95-%-Konfidenzintervallen relativiert sich das + „3 von 7" deutlich.** Seit `backtest_cost_gate.ci95` (Bootstrap) hängt an jeder + ØR-Zahl ihr Intervall. Auf der entscheidenden Spalte C: + | Setup | H1 95-%-KI | H2 95-%-KI | + |---|---|---| + | **KONTROLLE Squeeze** | **[+0,037 … +0,229]** | **[+0,225 … +0,366]** | + | PDH / PDL | [−0,032 … +0,544] ⚠ enthält 0 | [+0,155 … +0,764] | + | Equal H/L | [+0,001 … +0,425] (haarscharf) | [−0,035 … +0,356] ⚠ enthält 0 | + | **Asian Range** | [−0,067 … +0,357] ⚠ | [−0,060 … +0,332] ⚠ | + **Nur der Squeeze hat in BEIDEN Hälften ein Intervall klar über null.** Bei + Asian Range enthalten **beide** die Null — als „Überlebender" war es zu stark + formuliert. + ⚠ **Zwei verschiedene Fragen, beide nötig:** die 2-Stichproben-Regel prüft, ob + das **Vorzeichen über Regime hinweg stabil** ist; das KI prüft, ob eine einzelne + Schätzung **überhaupt von Rauschen zu unterscheiden** ist. Hier war Ersteres + erfüllt und Letzteres nicht. Und: zwei Hälften, die beide in dieselbe Richtung + zeigen, sind zusammen mehr Evidenz als jede für sich — „KI enthält 0" heißt + also **nicht widerlegt**, sondern **nicht belegt**. + ✅ An der Schlussfolgerung ändert das nichts (es wurde ohnehin nichts gebaut), + aber die Formulierung „3 von 7 tragen" ist auf **„1 gesichert, 2 unklar"** zu + korrigieren. ⚠⚠ **Das sind KEINE drei neuen Signale.** PDH/PDL, Asian-Range-Extrem, Equal Highs/Lows und die Squeeze-Box sind **dasselbe**: ein waagerechtes Level aus früheren Kursextremen, gebrochen, mit ruhender Stop-Order betreten und mit dem diff --git a/backtest_cost_gate.py b/backtest_cost_gate.py index a5c157f..8bc6437 100644 --- a/backtest_cost_gate.py +++ b/backtest_cost_gate.py @@ -165,21 +165,70 @@ def sequentiell(K, H, L, C, pred, lo, hi): return rs +_BOOT_N = 2000 # Resamples — fuer ein 95-%-KI reichlich und schnell genug +_BOOT_MIN = 20 # darunter ist ein KI ohnehin nur ein Achselzucken + + +def ci95(rs): + """95-%-Konfidenzintervall des MITTELWERTS (Bootstrap, Perzentil-Methode). + + ⚠⚠ WOZU. Das Projekt entscheidet ueber Punktschaetzungen plus die + 2-Stichproben-Regel. Ob ein ØR von +0,12 bei n=30 ueberhaupt von null zu + unterscheiden ist, musste bisher jedes Mal im Kopf abgeschaetzt werden — am + 2026-08-06 gleich mehrfach (Buckets mit n=15–30, „Standardfehler ≈0,026", + die Slippage-Tabelle). Mit einem KI steht es einfach da, und ein Ergebnis, + dessen Intervall die Null enthaelt, ist als solches erkennbar. + + ⚠⚠ GRENZE, die man kennen MUSS: der Bootstrap unterstellt UNABHAENGIGE + Ziehungen. Fuer die sequentielle Sim mit EINEM Positions-Slot stimmt das + naeherungsweise — dort beginnt jeder Trade erst nach dem Exit des vorigen. + Fuer UEBERLAPPENDE Beobachtungen ist er ZU OPTIMISTISCH: stuendliche Werte + mit 24-h-Horizont teilen 23 von 24 Stunden, das effektive n ist ~n/24. Genau + daran ist `analyze_hl_funding.py` am 06.08. einmal aufgelaufen (dort wird + entueberlappt statt gebootstrappt). Fuer solche Faelle braeuchte es einen + BLOCK-Bootstrap — dieses `ci95` ist dafuer NICHT gedacht. + + `random_state` fest, damit derselbe Lauf dieselbe Zahl liefert. + Rueckgabe (lo, hi) oder None (zu wenige Werte / scipy fehlt). + """ + if not rs or len(rs) < _BOOT_MIN: + return None + try: + import numpy as np + from scipy.stats import bootstrap + r = bootstrap((np.asarray(rs, dtype=float),), np.mean, + n_resamples=_BOOT_N, confidence_level=0.95, + method="percentile", random_state=0) + return (float(r.confidence_interval.low), + float(r.confidence_interval.high)) + except Exception: + return None # nie den Backtest daran scheitern lassen + + def kennzahlen(rs): if not rs: return None n = len(rs); s = sum(rs) g = sum(x for x in rs if x > 0); v = -sum(x for x in rs if x < 0) + # ⚠ `ci` ist rein ADDITIV — alle 11 bestehenden Aufrufer lesen nur n/wr/oer/ + # sr/pf und bleiben unveraendert. Keine dokumentierte Zahl verschiebt sich. return {"n": n, "wr": 100.0 * sum(1 for x in rs if x > 0) / n, - "oer": s / n, "sr": s, "pf": (g / v) if v > 0 else float("inf")} + "oer": s / n, "sr": s, "pf": (g / v) if v > 0 else float("inf"), + "ci": ci95(rs)} -def zeile(lbl, k1, k2): +def zeile(lbl, k1, k2, ci=False): + """`ci=True` haengt das 95-%-KI des ØR an — und markiert mit `~`, wenn es die + Null enthaelt (dann ist das Vorzeichen nicht gesichert).""" def f(k): if not k: return f"{'—':>32}" - return (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} " - f"{k['pf']:>5.2f} {k['sr']:>+8.1f}") + s = (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} " + f"{k['pf']:>5.2f} {k['sr']:>+8.1f}") + if ci and k.get("ci"): + lo, hi = k["ci"] + s += f" [{lo:+.3f}…{hi:+.3f}]{'~' if lo <= 0 <= hi else ' '}" + return s print(f" {lbl:<26}{f(k1)} |{f(k2)}") diff --git a/backtest_verworfene_v2.py b/backtest_verworfene_v2.py index 0d51a74..36ef36e 100644 --- a/backtest_verworfene_v2.py +++ b/backtest_verworfene_v2.py @@ -258,6 +258,18 @@ def main(): if c1["n"] < _MIN_N or c2["n"] < _MIN_N: z += " (duenn)" print(z) + # ⚠⚠ 95-%-KI der ENTSCHEIDENDEN Spalte (C, Beruehrungs-Fill). Enthaelt es + # die Null, ist das Vorzeichen NICHT gesichert — und genau darauf ruht die + # ganze Entscheidung. Ohne diese Zahl sah „+0,142 / +0,134" nach einem + # Befund aus; mit ihr ist sofort sichtbar, ob n ausreicht. + unsicher = [] + for k, tag in ((c1, "H1"), (c2, "H2")): + if k.get("ci"): + lo, hi = k["ci"] + unsicher.append(f"{tag} [{lo:+.3f}…{hi:+.3f}]" + + ("⚠0" if lo <= 0 <= hi else "")) + if unsicher: + print(f" └ 95-%-KI: {' '.join(unsicher)}") if ok and not name.startswith("KONTROLLE"): treffer.append(name)