#!/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()