diff --git a/.gitignore b/.gitignore index b36d44d..6a5b65a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,8 @@ desktop.ini # Reminder-Marker (gemeldete Messungen) .reminders/ + +# Laufprotokolle der Skript-Vollpruefung (2026-08-04, wegwerfbar) +run_all.log +run_all.err +rerun_rejected.* diff --git a/CLAUDE.md b/CLAUDE.md index 15de0ed..b0f67eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2804,6 +2804,59 @@ Copiloten aktiv verschlechtern. Offener Kandidat: sie MIT der gemessenen Lesart mitgeben („langer Docht oben; gemessen läuft der Kurs danach eher GEGEN die Kerze") — noch nicht gebaut. +## Vollprüfung aller Skripte + Nachrechnung der verworfenen Modelle (2026-08-04) + +**Auftrag:** „alle Skripte ausgiebig testen und die verworfenen Modelle erneut +gegenrechnen." Werkzeug: `run_all_backtests.py` (probiert die Aufruf-Signaturen +adaptiv durch — die sind nicht einheitlich, s. Audit unten). + +**Phase 1 — Lauffähigkeit: ✅ 87 von 87 Skripten laufen fehlerfrei** (8 000 Bars, +Timeout 900 s je Skript). Kein Defekt durch die heutigen `wave_rec`-Änderungen. + +**Phase 2a — `breakout_k`: die dokumentierte Begründung REPRODUZIERT NICHT.** +Offener Punkt aus dem Stufe-3-Refactor: k=0,3 war mit dem `LEGACY_BE10`-Exit +(Breakeven 1,0) validiert. Erstmals mit dem **kanonischen** Exit nachgerechnet +(`backtest_breakout_canon.py`, echte `_confirm_breakout`-Mechanik, 80k M5, +2 Halbjahre, Echtkosten): +| k | H1 ØR / ΣR | H2 ØR / ΣR | +|---|---|---| +| 0,0 | −0,180 / −569 | −0,054 / −250 | +| **0,3 (LIVE)** | **−0,170 / −356** | **−0,069 / −217** | +| 0,5 | −0,142 / −262 | −0,074 / −208 | +| 0,8 | −0,112 / −166 | −0,032 / −73 | +| 1,0 | −0,101 / −130 | −0,004 / −8 | +**k=0,5 · 0,8 · 1,0 schlagen den Live-Wert 0,3 in BEIDEN Hälften** (ΣR-Differenz +bis +226/+209). Die Aussage „0,3 ist in beiden Hälften das Beste" gilt mit dem +Live-Exit **nicht mehr**. +⚠ **Trotzdem KEINE Änderung empfohlen**, aus drei Gründen: (a) **alle** Werte sind +negativ (PF 0,70–0,99) — es ist eine Wahl zwischen Verlusten, kein Edge; +(b) das Optimum lag am Rand des Suchbereichs → Gegenprobe bis k=3,0 zeigt **kein +sauberes Optimum** (H2: −8 → −17 → −20 → −19, H1 schwankt) — es ist also weder +reine Handelsvermeidung noch ein echter Bestwert; (c) gemessen wurde das +**UNGEGATETE** Signal (ohne min_conf, HTF-Filter, Entry-Raum), live laufen die +davor. **Nächster Schritt wäre eine GEGATETE Neumessung**, bevor ein Live-Gate +angefasst wird, das 63 % der WARTEN-Zustände erzeugt. + +**Phase 2b — verworfene Modelle bei vollen Bars nachgefahren: ALLE bleiben verworfen.** +14 Klassen, keine einzige kippt ins Positive: +| Klasse | Ergebnis | +|---|---| +| ORB (EU/US, 15/30 min) | H1 −0,018…−0,177 · H2 +0,013…+0,280 → Regime-Kippen | +| Doppeltop (3 Toleranzen) | 3/3 fallen durch | +| Volume Profile (POC/HVN/LVN) | 3/3 fallen durch | +| Liquidity Sweeps (SMC) | alle Varianten fallen durch | +| Konsolidierung (False Break) | alle Varianten fallen durch | +| Handbuch (PDH/PDL, Asian Range, Discount/Premium) | alle fallen durch | +| Marktstruktur | 5/5 fallen durch | +| Momentum · Inter-Market · Chop-Gate · Reversal-Lockout · S/R-Close-Signal · MACD/ADX/RSI · HTF-Winkel | unverändert verworfen | +⚠ **Bemerkenswert:** Bei ~14 Wiederholungstests wäre bei 5 % Fehlalarmquote rund +**ein** falsch-positives Ergebnis zu erwarten gewesen — es gab **keins**. Das +stützt die Ablehnungen zusätzlich. Die Regel wurde VOR dem Lauf fixiert (ØR>0 und +PF>1 in BEIDEN Hälften, Nachbarparameter müssen mithalten). +⚠ Das ist ausdrücklich **kein Freibrief für regelmäßiges Nachrechnen** — die +Begründung gegen wöchentliche Läufe (Mehrfachvergleich, ~47 Fehlalarme/Jahr) +steht unverändert. Anlass war hier die *einmalige* Signaländerung. + ## Backtest-Audit 2026-08-04 — rechnen die Skripte noch korrekt? **Anlass:** User-Frage nach dem `angle=`-Fund in `backtest_auto_signal.py`. Prüfung diff --git a/backtest_breakout_canon.py b/backtest_breakout_canon.py new file mode 100644 index 0000000..5130a5b --- /dev/null +++ b/backtest_breakout_canon.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""`breakout_k` mit dem KANONISCHEN Exit nachgerechnet (2026-08-04). + +Offener Punkt aus CLAUDE.md: „**`breakout_k=0,3` ruht weiter auf dem +`LEGACY_BE10`-Exit** (Breakeven 1,0 statt live 1,3). Der Legacy-Recheck hat EIA +und Dead-Hours nachgerechnet, `breakout_k` nicht. Kandidat für den nächsten +Recheck." — Das wiegt schwer, weil es ein AKTIVES Gate ist: die erste +`block_reason`-Auswertung zeigte `breakout_pending` als Ursache von **63 %** +aller WARTEN-Zustände. + +Aufbau: echte `_confirm_breakout`-Mechanik (stateful `_pend`, Neuverankerung bei +Richtungswechsel/Timeout) auf dem EMA12/50-Signal, Entry erst nach Bestätigung. +Exit = `core/exit_model.LIVE` (SL 2,0 · Trail 1,0 · BE 1,3 · Time-Stop · TP), +Echtkosten = Bar-Spread/ATR, sequentielle 1-Positions-Sim, 2 Halbjahre. + +⚠ VORAB festgelegte Regel (gegen Mehrfachvergleich): ein anderer k-Wert wird nur +dann empfohlen, wenn er in BEIDEN Hälften mehr ΣR liefert als der Live-Wert 0,3 +UND seine Nachbarwerte mithalten. Ein einzelner Ausreißer zählt nicht. + +Aufruf: python backtest_breakout_canon.py [bars] +""" +import sys + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.wave_rec import _EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _ema_series + +_ATRMIN = 0.06 +_COOL = 3 +_PEND_TIMEOUT = 24 # Bars, bis eine unbestätigte Anwartschaft verfällt + + +def _atr_series(H, L, C, p=14): + t = [0.0] + for i in range(1, len(C)): + t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1]))) + if i else None for i in range(len(C))] + + +def st(Rs): + if not Rs: + return None + n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) + up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0) + return dict(n=n, wr=100 * w / n, oR=s / n, pf=(up / dn if dn > 0 else 9.99), sum=s) + + +def run(H, L, C, A, SP, EF, ES, lo, hi, k): + """k = geforderte Bewegung in ATR, bevor das Signal gehandelt wird. k=0 → aus.""" + def cost(i, atr): + return (SP[i] if SP[i] > 0 else 0.0225) / atr + + Rs = [] + pend = None # {"dir": ±1, "level": float, "bar": int} + i = max(lo, _EMA_SLOW + 5) + while i < hi: + atr = A[i] + if not atr or atr < _ATRMIN: + i += 1; continue + sep = (EF[i] - ES[i]) / atr + d = 0 if abs(sep) < _TREND_DEADBAND else (1 if sep > 0 else -1) + if d == 0: + pend = None; i += 1; continue + + if k <= 0: # Bestätigung aus → sofort handeln + trigger = True + else: + if pend is None or pend["dir"] != d: + pend = {"dir": d, "level": C[i], "bar": i} + i += 1; continue + if i - pend["bar"] > _PEND_TIMEOUT: # Timeout → neu verankern + pend = {"dir": d, "level": C[i], "bar": i} + i += 1; continue + fortschritt = (C[i] - pend["level"]) * d / atr + if fortschritt <= -k: # gegen die Richtung → neu + pend = {"dir": d, "level": C[i], "bar": i} + i += 1; continue + trigger = fortschritt >= k + if not trigger: + i += 1; continue + + r, xj = simulate(C[i], d, max(atr, _ATRMIN), H, L, C, i + 1, LIVE, + timestop=True, use_tp=True, ret_bar=True) + Rs.append(r - cost(i, max(atr, _ATRMIN))) + pend = None + i = xj + _COOL + return Rs + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + mt5.initialize() + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") + if mt5.symbol_info(c)), None) + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) + point = mt5.symbol_info(sym).point + mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars]; SP = [float(b["spread"]) * point for b in bars] + A = _atr_series(H, L, C) + EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW) + N = len(C); mid = N // 2 + + print("=" * 92) + print(f" breakout_k — KANONISCHER Exit — {sym} M5 ({N} Bars, 2 Halbjahre, Echtkosten)") + print(f" Exit: SL {LIVE.sl_atr}× · Trail {LIVE.mult}× · BE {LIVE.be}× · Time-Stop · TP") + print(f" ⚠ Die Validierung von k=0,3 stammt aus `backtest_breakout.py` mit BE 1,0") + print(f" (LEGACY_BE10) — hier erstmals mit dem Live-Exit gegengerechnet.") + print("=" * 92) + erg = {} + for k in (0.0, 0.2, 0.3, 0.5, 0.8, 1.0): + s1 = st(run(H, L, C, A, SP, EF, ES, 0, mid, k)) + s2 = st(run(H, L, C, A, SP, EF, ES, mid, N - LIVE.max_hold - 1, k)) + erg[k] = (s1, s2) + tag = " ← LIVE" if abs(k - 0.3) < 1e-9 else "" + print(f"\n k = {k:.1f}{tag}") + for lbl, s in (("H1 (alt)", s1), ("H2 (neu)", s2)): + if s: + print(f" {lbl} n={s['n']:>5} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} " + f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+7.0f}") + else: + print(f" {lbl} — keine Trades") + live = erg.get(0.3) + print("\n" + "=" * 92) + print(" Vergleich gegen den Live-Wert k=0,3 (ΣR-Differenz je Hälfte):") + for k, (s1, s2) in sorted(erg.items()): + if abs(k - 0.3) < 1e-9 or not (s1 and s2 and live[0] and live[1]): + continue + print(f" k={k:.1f}: H1 {s1['sum'] - live[0]['sum']:+7.0f} " + f"H2 {s2['sum'] - live[1]['sum']:+7.0f}" + + (" << in BEIDEN besser" if (s1['sum'] > live[0]['sum'] + and s2['sum'] > live[1]['sum']) else "")) + print("\n Regel (vorab): Wechsel nur, wenn ein k in BEIDEN Hälften besser ist") + print(" UND seine Nachbarn mithalten. Ein einzelner Ausreißer zählt nicht.") + print() + + +if __name__ == "__main__": + main() diff --git a/run_all_backtests.py b/run_all_backtests.py new file mode 100644 index 0000000..7c43ad0 --- /dev/null +++ b/run_all_backtests.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Führt ALLE Backtest-/Analyse-Skripte aus und protokolliert das Ergebnis. + +Zweck (User-Auftrag 2026-08-04): nach den Änderungen am Signal (Reversal aus, +`angle=`-Befund) prüfen, ob die Skripte noch laufen UND ob ihre Aussagen halten. + +⚠ Die Aufruf-Signaturen sind NICHT einheitlich. Die meisten nehmen eine Bar-Zahl +als erstes Argument, einige aber einen Timeframe (`backtest_signal.py M5 3000`, +`backtest_htf_filter.py M5 M30 3000`). Ein Aufruf mit der falschen Signatur +quittiert mit `KeyError`/`ValueError` — das ist KEIN Defekt und darf nicht als +solcher gezählt werden. Deshalb probiert der Runner adaptiv mehrere Varianten +und meldet erst dann einen Fehler, wenn KEINE davon durchläuft. + +Aufruf: python run_all_backtests.py [bars] [--only muster] +""" +from __future__ import annotations + +import pathlib +import subprocess +import sys +import time + +PY = sys.executable +HERE = pathlib.Path(__file__).parent + +# Skripte, die kein eigenständiger Lauf sind (Bibliothek/Helfer) oder extrem lange +# laufen und separat gefahren werden. +SKIP = { + "run_all_backtests.py", +} + + +def varianten(n: int) -> list[list[str]]: + """Aufruf-Varianten in der Reihenfolge, in der sie probiert werden.""" + return [ + [str(n)], # Standard: Bar-Zahl + ["M5", str(n)], # TF + Bar-Zahl + ["M5", "M30", str(n)], # zwei TFs + Bar-Zahl + [], # ohne Argumente (Defaults) + ] + + +def main() -> None: + n = 6000 + only = None + args = sys.argv[1:] + if args and args[0].isdigit(): + n = int(args[0]); args = args[1:] + if len(args) >= 2 and args[0] == "--only": + only = args[1] + + files = sorted(set(list(HERE.glob("backtest_*.py")) + list(HERE.glob("analyze_*.py")))) + files = [f for f in files if f.name not in SKIP] + if only: + files = [f for f in files if only in f.name] + + print(f"{len(files)} Skripte · {n} Bars · Start {time.strftime('%H:%M:%S')}") + print("=" * 96) + ok, fail, slow = [], [], [] + for i, f in enumerate(files, 1): + t0 = time.time() + erfolg = None + letzter_fehler = "" + for v in varianten(n): + try: + r = subprocess.run([PY, "-X", "utf8", f.name] + v, cwd=HERE, + capture_output=True, text=True, timeout=900, + encoding="utf-8", errors="replace") + except subprocess.TimeoutExpired: + letzter_fehler = "TIMEOUT (>900 s)" + continue + if r.returncode == 0: + erfolg = (v, r.stdout or "") + break + tail = [x for x in (r.stderr or "").splitlines() if x.strip()] + letzter_fehler = tail[-1][:110] if tail else f"exit {r.returncode}" + dt = time.time() - t0 + if erfolg: + v, out = erfolg + zeilen = [x for x in out.splitlines() if x.strip()] + sig = " ".join(v) or "(ohne)" + print(f"[{i:>2}/{len(files)}] OK {f.name:<36} {dt:>6.1f}s args={sig}") + ok.append((f.name, sig, dt, zeilen[-1][:100] if zeilen else "")) + if dt > 120: + slow.append((f.name, dt)) + else: + print(f"[{i:>2}/{len(files)}] FEHLER {f.name:<36} {dt:>6.1f}s {letzter_fehler}") + fail.append((f.name, letzter_fehler)) + + print("=" * 96) + print(f"OK: {len(ok)} FEHLER: {len(fail)} (Ende {time.strftime('%H:%M:%S')})") + if fail: + print("\nFEHLGESCHLAGEN:") + for name, err in fail: + print(f" {name:<36} {err}") + if slow: + print("\nLangsam (>120 s):") + for name, dt in sorted(slow, key=lambda x: -x[1]): + print(f" {name:<36} {dt:.0f}s") + + +if __name__ == "__main__": + main()