Konfidenzintervalle (scipy-Bootstrap) — und sie korrigieren den Befund vom 06.08.

backtest_cost_gate.ci95(): 95-%-Bootstrap-KI des Mittelwerts (Perzentil, 2000
Resamples, random_state fest -> reproduzierbar, ab n>=20). kennzahlen() liefert
es als neuen Schluessel `ci` — rein additiv, alle 11 Aufrufer lesen weiter nur
n/wr/oer/sr/pf, keine dokumentierte Zahl verschiebt sich (geprueft).

Sofortiger Ertrag: der erste Lauf relativiert das `3 von 7 tragen` vom 06.08.
Auf der entscheidenden Spalte C (Beruehrungs-Fill):

  KONTROLLE Squeeze  H1 [+0,037…+0,229]   H2 [+0,225…+0,366]
  PDH / PDL          H1 [-0,032…+0,544]⚠  H2 [+0,155…+0,764]
  Equal H/L          H1 [+0,001…+0,425]   H2 [-0,035…+0,356]⚠
  Asian Range        H1 [-0,067…+0,357]⚠  H2 [-0,060…+0,332]⚠

Nur der Squeeze hat in BEIDEN Haelften ein Intervall klar ueber null. Bei Asian
Range enthalten beide die Null — als Ueberlebender war das zu stark formuliert.
Formulierung auf `1 gesichert, 2 unklar` korrigiert.

Zwei verschiedene Fragen, beide noetig: die 2-Stichproben-Regel prueft die
Stabilitaet des VORZEICHENS ueber Regime, das KI die Unterscheidbarkeit vom
RAUSCHEN innerhalb einer Stichprobe. Hier war Ersteres erfuellt, Letzteres
nicht. Ein KI, das die Null enthaelt, heisst nicht widerlegt, sondern nicht
belegt — und zwei gleichgerichtete Haelften sind zusammen mehr Evidenz als jede
fuer sich.

Grenze dokumentiert: der Bootstrap unterstellt unabhaengige Ziehungen. Fuer die
sequentielle Sim mit EINEM Slot passt das; fuer ueberlappende Fenster
(analyze_hl_funding.py) ist er zu optimistisch — dort braeuchte es einen
Block-Bootstrap. ci95 ist dafuer ausdruecklich NICHT gedacht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-07 00:25:02 +02:00
co-authored by Claude Opus 5
parent e69907edbe
commit 79f3e8efe1
3 changed files with 105 additions and 4 deletions
+40
View File
@@ -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). Anteil, WR-Verfall, PF<1, Einzelverlust). **Wöchentlich** laufen lassen (B4/B5).
- **Vor „fertig":** immer `python -m py_compile <datei>`; JS grob via Klammern- - **Vor „fertig":** immer `python -m py_compile <datei>`; JS grob via Klammern-
Balance prüfen (kein node im Env). 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).** - **⚠⚠ 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 Wird Code über ein Shell-**Heredoc** geschrieben, macht die Shell aus einem
gemeinten `\n` einen **echten Zeilenumbruch mitten im String-Literal**. Real an 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 vier Varianten (bestätigt 2026-07-17). ✅ Die drei Überlebenden sind
**nachbar-robust** (PDH/PDL über die Gültigkeitsdauer, Asian Range über das **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). 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 ⚠⚠ **Das sind KEINE drei neuen Signale.** PDH/PDL, Asian-Range-Extrem, Equal
Highs/Lows und die Squeeze-Box sind **dasselbe**: ein waagerechtes Level aus Highs/Lows und die Squeeze-Box sind **dasselbe**: ein waagerechtes Level aus
früheren Kursextremen, gebrochen, mit ruhender Stop-Order betreten und mit dem früheren Kursextremen, gebrochen, mit ruhender Stop-Order betreten und mit dem
+53 -4
View File
@@ -165,21 +165,70 @@ def sequentiell(K, H, L, C, pred, lo, hi):
return rs 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=1530, „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): def kennzahlen(rs):
if not rs: if not rs:
return None return None
n = len(rs); s = sum(rs) 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) 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, 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): def f(k):
if not k: if not k:
return f"{'':>32}" return f"{'':>32}"
return (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} " s = (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} "
f"{k['pf']:>5.2f} {k['sr']:>+8.1f}") 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)}") print(f" {lbl:<26}{f(k1)} |{f(k2)}")
+12
View File
@@ -258,6 +258,18 @@ def main():
if c1["n"] < _MIN_N or c2["n"] < _MIN_N: if c1["n"] < _MIN_N or c2["n"] < _MIN_N:
z += " (duenn)" z += " (duenn)"
print(z) 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"): if ok and not name.startswith("KONTROLLE"):
treffer.append(name) treffer.append(name)