diff --git a/CLAUDE.md b/CLAUDE.md index 2f2855d..13e9fd5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -725,6 +725,28 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: eigene Konstante `_MIN_CONF_DISPLAY=70` in `engine.py`. Die `intended_trades`-DB- Tabelle + `history.intended_*`-Methoden bleiben (historische Daten, kein Schreiber mehr aktiv). Manuelles Handeln bleibt der validierte Edge auf dem Wave-Signal. + - **RE-TEST 2026-07-30 (User-Wunsch „Trades bei Empfehlung automatisch eröffnen") = + ERNEUT VERWORFEN, gemessen `backtest_auto_signal.py`:** Auto-Entry auf dem Wave- + Signal mit den HEUTIGEN Gates, echte `_build`-Logik (kein Nachbau), M30-/H1-Trend + aus M5 aggregiert und in `_build` gefüttert, sequentielle 1-Positions-Sim, Live-Exit + (SL 2,0/Trail 1,5/BE 1,3/Time-Stop 120 min), Echtkosten (Bar-Spread/ATR), 80k M5-Bars + in 2 Halbjahren. **ALLE 8 Varianten fallen durch** (Konf-Schwelle 55/65/70/75 × + Nacht-Sperre an/aus): **H1 durchgehend NEGATIV** (ØR −0,076…−0,182, PF 0,78–0,90), + **H2 durchgehend positiv** (+0,112…+0,167) → klassisches Regime-Kippen, n=718–1167 + je Hälfte (keine Stichproben-Frage). ⚠ **Kontroll-Beleg, dass es am SIGNAL liegt und + nicht an der Sim:** der Squeeze läuft durch DIESELBE Exit-/Kosten-Maschinerie und ist + in H1 **+0,187/PF 1,29** — die Simulation funktioniert also, nur das Wave-Signal trägt + in H1 nicht. Deckt sich exakt mit dem Live-Dry-Run (WR 29 %/PF 0,64). Nebenbefunde: + (a) höhere Konfidenz hilft NICHT monoton (75 schlechter als 65) — bestätigt + `analyze_verdict_calibration.py` (conf_pct ist NICHT kalibriert, in H2 invertiert), + ein „nur bei hoher Konfidenz"-Auto-Entry ist damit auch erledigt; (b) die Nacht-Sperre + verbessert konsistent (H1 −0,172→−0,123), reicht aber nicht; (c) ⚠ **Methodik-Lehre:** + ein Vorlauf mit nur 6k Bars zeigte „Konf≥65+Nacht TRÄGT" — der 80k-Lauf drehte das + komplett (n war 60–99/Hälfte = Rauschen). **Kleine Stichprobe hätte hier zum Fehl- + Einbau geführt.** Warum manuelle „MIT Signal"-Trades trotzdem profitabel sind + (+118 €/WR 56 %, `analyze_entries`-Auswertung 2026-07-30): **menschliche AUSWAHL** + unter den Signalen (Kontext/Tageszeit/Marktlage), nicht „alle nehmen" — die + Grundgesamtheit ALLER Signale ist der Verlierer. **14. verworfener Signal-Eingriff.** - **Auto-Squeeze-Entry = LIVE AUTONOM (User-Vorgabe 2026-07-16, `[trading] auto_squeeze=true`, Default FALSE):** der Bot eröffnet **selbständig eine echte Order**, sobald der **gemessen-validierte** Squeeze-Ausbruch feuert (`wave.squeeze. diff --git a/backtest_auto_signal.py b/backtest_auto_signal.py new file mode 100644 index 0000000..00b7ec2 --- /dev/null +++ b/backtest_auto_signal.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""backtest_auto_signal.py — Auto-Entry auf die EMPFEHLUNG (B2, User-Wunsch +2026-07-30 „Trades bei Empfehlung automatisch eröffnen"). + +Kernfrage: Trägt ein AUTONOMER Entry auf dem Wave-Signal (mit den HEUTIGEN Gates) +netto in BEIDEN Halbjahren — und hält er gegen den einzigen validierten Auto-Entry +(Squeeze: ØR +0,14…+0,23, PF>1)? + +Vorgeschichte (hohe Beweislast!): Der Wave-Auto-Trader lief live als Dry-Run und +war ein klarer VERLIERER (WR 29 % / PF 0,64 vs. manuell 56–76 %) → am 2026-07-23 +komplett entfernt. Gegenbefund: die MANUELLEN „MIT Signal"-Trades des Users sind +profitabel (+118 €, WR 56 %) — aber das ist MENSCHLICHE AUSWAHL unter den Signalen, +nicht „alle nehmen". Genau diese Differenz misst dieses Skript. + +Methodik (bewusst pessimistisch, wie backtest_breakout_squeeze.py): + · Signal = die ECHTE Logik `WaveRecommender._build` (kein Nachbau), inkl. + Totband, Anti-Überdehnung, Konfidenz-Gate 55, Reversal-Zweig + · HTF = M30- UND H1-Trend aus den M5-Bars aggregiert und in `_build` + gefüttert (`htf_trend`/`h1_trend`) → der live aktive M30-Gegen- + Trend-Filter und der H1-Konfluenz-Bonus wirken mit + · Entry = am Bar-CLOSE des Signal-Bars, nur wenn FLAT (sequentiell, 1 Position) + · Exit = Live-Modell: SL 2,0×ATR · Trailing 1,5 ab 0,3 · Breakeven 1,3 · + Time-Stop 120 min (24 M5-Bars) wenn nie Trail-Start erreicht + · Kosten = echter Bar-Spread/ATR (Ø ~0,265×ATR — NICHT die alte 0,1-Annahme) + · 2 Halbjahre (H1/H2) — eine Variante gilt nur als „trägt", wenn sie in BEIDEN + positiv ist (Dead-Hour-Lektion) + +NICHT simuliert (bewusst): Entry-Raum-Gate + ATR-Breakout-Bestätigung (beide +brauchen Live-State/pb_levels, s. CLAUDE.md — Backtests bleiben gate-frei) sowie +das EIA-Blackout-Gate. Beide würden Trades ENTFERNEN, die Ergebnisse hier sind +also eher optimistisch als zu streng. + +Aufruf: python backtest_auto_signal.py [n_bars] (Default 40000 M5-Bars) +""" +from __future__ import annotations +import sys +from datetime import datetime, timezone, timedelta + +import MetaTrader5 as mt5 + +from core.wave_rec import (WaveRecommender, _atr, _ema_last, + _EMA_FAST, _EMA_SLOW, _HTF_DEADBAND) + +# ── Exit-Parameter = die LIVE-Werte (core/trailing.py) ──────────────────────── +_SL_ATR = 2.0 # Initial-SL (Band 1,8–2,2 → Ziel 2,0) +_TRAIL = 1.5 # Trailing-Abstand ×ATR (M5-Mult) +_TRAIL_ON = 0.3 # Trail-Start ab diesem Profit ×ATR +_BE_ON = 1.3 # Breakeven-Floor ab diesem Profit ×ATR +_TIMESTOP = 24 # Time-Stop: 120 min = 24 M5-Bars in Phase „Init" +_MAXH = 288 # Sim-Horizont (24 h auf M5) +_ATRMIN = 0.06 # ATR-Floor wie live (_ATR_MIN in trailing.py) +_NIGHT = (0, 1, 2, 3, 4, 5, 6, 7) # Berlin-Stunden = gemessene Kostenfalle +_HTF_N = 6 # 6 M5-Bars = 1 M30-Bar +_H1_N = 12 # 12 M30-Bars … (H1 = 12 M5-Bars) + + +class _NeutralTU: + """TU ist aus der Empfehlung entfernt — im Test neutral (kein Einfluss).""" + def snapshot(self): + return {"intervals": {}} + + +def _atr_series(H, L, C, p=14): + tr = [0.0] + for i in range(1, len(C)): + tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [None] * len(C) + run = 0.0 + for i in range(1, len(C)): + run += tr[i] + if i > p: + run -= tr[i - p] + out[i] = run / min(i, p) + return out + + +def _ema_series(vals, period): + k = 2.0 / (period + 1) + out = [vals[0]] + for v in vals[1:]: + out.append(v * k + out[-1] * (1.0 - k)) + return out + + +def _htf_signs(H, L, C, step): + """Aggregiert die M5-Bars zu einer höheren TF (step M5-Bars je HTF-Bar) und + liefert je M5-Index das Trend-Vorzeichen (+1/-1/0, Totband _HTF_DEADBAND×ATR) + — LOOK-AHEAD-frei: an Index i zählen nur HTF-Bars, die vor i abgeschlossen sind.""" + # HTF-Bars bilden + hc, hh, hl = [], [], [] + for s in range(0, len(C) - step + 1, step): + seg_h = H[s:s + step]; seg_l = L[s:s + step] + hh.append(max(seg_h)); hl.append(min(seg_l)); hc.append(C[s + step - 1]) + if len(hc) < _EMA_SLOW + 5: + return [0] * len(C) + ef = _ema_series(hc, _EMA_FAST); es = _ema_series(hc, _EMA_SLOW) + hatr = _atr_series(hh, hl, hc) + signs_htf = [] + for j in range(len(hc)): + a = hatr[j] + if j < _EMA_SLOW + 2 or not a: + signs_htf.append(0); continue + d = ef[j] - es[j] + signs_htf.append(0 if abs(d) < _HTF_DEADBAND * a else (1 if d > 0 else -1)) + # auf M5-Index abbilden: bei i ist der letzte ABGESCHLOSSENE HTF-Bar (i//step)-1 + out = [0] * len(C) + for i in range(len(C)): + j = i // step - 1 + out[i] = signs_htf[j] if 0 <= j < len(signs_htf) else 0 + return out + + +def _sim(entry, d, atr, H, L, C, j0): + """Live-Exit-Modell. Rückgabe R = Profit/ATR (brutto, ohne Kosten). + Phasen-Ratsche wie live: Time-Stop greift nur, solange nie Trail-Start erreicht.""" + eff = entry - d * _SL_ATR * atr + hw = entry + trail_started = False + end = min(j0 + _MAXH, len(C) - 1) + exit_px = C[end] + for j in range(j0, end + 1): + hi, lo = H[j], L[j] + # SL/Trail-Stop zuerst (konservativ: Stop schlägt Weiterlaufen im selben Bar) + if (lo <= eff) if d > 0 else (hi >= eff): + exit_px = eff; break + hw = max(hw, hi) if d > 0 else min(hw, lo) + prof = (C[j] - entry) * d + if prof >= _TRAIL_ON * atr: + trail_started = True + cand = hw - d * _TRAIL * atr + if prof >= _BE_ON * atr: + cand = max(cand, entry) if d > 0 else min(cand, entry) + eff = max(eff, cand) if d > 0 else min(eff, cand) + # Time-Stop: nach 120 min noch in „Init" (nie Trail-Start) → raus + if not trail_started and (j - j0) >= _TIMESTOP: + exit_px = C[j]; break + return (exit_px - entry) * d / atr + + +def _rep(name, Rs, ref=None): + if not Rs: + print(f" {name:<34} -"); 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) + pf = up / dn if dn > 0 else 9.99 + worst = min(Rs) + print(f" {name:<34} n={n:>4} WR={100*w/n:>3.0f}% " + f"ØR={s/n:+.3f} PF={pf:.2f} ΣR={s:+.0f} worst={worst:+.1f}") + return s / n + + +def scan(bars, H, L, C, SP, A, HTF, H1, EF, ES, w, lo_i, hi_i, min_conf, skip_night): + """Sequentielle 1-Positions-Simulation: nimmt das ERSTE Signal, wenn flat.""" + Rs = [] + i = max(lo_i, _EMA_SLOW + 20) + hi_i = min(hi_i, len(C) - _MAXH - 2) + while i < hi_i: + atr = A[i] + if not atr or atr < _ATRMIN: + i += 1; continue + if skip_night: + # Broker-Zeit ist UTC+3, Berlin (Sommer) UTC+2 → Berlin = Broker − 1 h + hb = (datetime.fromtimestamp(int(bars[i]["time"]), timezone.utc).hour - 1) % 24 + if hb in _NIGHT: + i += 1; continue + ef, es = EF[i], ES[i] + rec, _snap = w._build(ef, es, C[i], atr, "M5", 5, + htf_trend=HTF[i], h1_trend=H1[i]) + sig = rec.get("signal") + if sig not in ("LONG", "SHORT") or (rec.get("conf_pct") or 0) < min_conf: + i += 1; continue + d = 1 if sig == "LONG" else -1 + entry = C[i] + cost = (SP[i] if SP[i] > 0 else 0.0225) / atr + R = _sim(entry, d, atr, H, L, C, i + 1) - cost + Rs.append(R) + # nach dem Trade weiterspringen: der Exit fand irgendwo in [i+1, i+_MAXH] + # statt — konservativ mit dem Time-Stop-Fenster weiterrücken (kein Overlap) + i += _TIMESTOP + return Rs + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 40000 + if not mt5.initialize(): + print(f"MT5-Init fehlgeschlagen: {mt5.last_error()}"); sys.exit(1) + sym = None + for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): + if mt5.symbol_info(c): + sym = c; break + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n + _MAXH + 60) + si = mt5.symbol_info(sym); point = si.point + mt5.shutdown() + if bars is None or len(bars) < 5000: + print("Zu wenige Bars."); sys.exit(1) + + 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) + HTF = _htf_signs(H, L, C, _HTF_N) # M30-Trend je M5-Bar + H1 = _htf_signs(H, L, C, _HTF_N * 2) # H1-Trend je M5-Bar (12 M5-Bars) + + w = WaveRecommender(_NeutralTU(), mt5.TIMEFRAME_M5) + # EMA-Serien vorab (identisch zur Live-Rekursion, nur ohne Fenster-Neuaufbau) + EF = _ema_series(C, _EMA_FAST) + ES = _ema_series(C, _EMA_SLOW) + + N = len(C); mid = N // 2 + print("=" * 92) + print(f" AUTO-ENTRY auf die EMPFEHLUNG — {sym} M5 ({N} Bars) · echte _build-Logik") + print(f" Exit=live(SL2,0/Trail1,5/BE1,3/Time-Stop120min) · Kosten=Bar-Spread/ATR") + print(f" Referenz (validierter Auto-Entry Squeeze): ØR +0,14…+0,23 · PF>1") + print("=" * 92) + + verdict = {} + for label, lo, hi in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N)): + print(f"\n{label}:") + for mc in (55, 65, 70, 75): + for night in (False, True): + tag = f"Konf>={mc}" + (" · Nacht-Sperre" if night else "") + Rs = scan(bars, H, L, C, SP, A, HTF, H1, EF, ES, w, lo, hi, mc, night) + oR = _rep(tag, Rs) + verdict.setdefault((mc, night), []).append(oR) + + print("\n" + "=" * 92) + print(" URTEIL (trägt nur, wenn ØR in BEIDEN Hälften > 0 UND im Squeeze-Band):") + any_ok = False + for (mc, night), vals in sorted(verdict.items()): + if len(vals) != 2 or any(v is None for v in vals): + continue + ok = all(v > 0 for v in vals) + band = all(v >= 0.14 for v in vals) + mark = "✅ TRÄGT" if (ok and band) else ("⚠ positiv, unter Squeeze-Band" if ok else "❌ fällt durch") + if ok and band: + any_ok = True + tag = f"Konf>={mc}" + (" +Nacht-Sperre" if night else "") + print(f" {tag:<28} H1 ØR {vals[0]:+.3f} · H2 ØR {vals[1]:+.3f} {mark}") + print("\n " + ("→ mindestens eine Variante trägt: Einbau vertretbar (klein starten, B4-Monitor)." + if any_ok else + "→ KEINE Variante trägt beidhälftig im Squeeze-Band → NICHT einbauen.")) + + +if __name__ == "__main__": + main()