#!/usr/bin/env python3 """Squeeze-Einstieg auf M5, EXIT-Abstände aus dem M15-ATR — trägt das? ANLASS (2026-08-08). Der M15-EINSTIEG ist gemessen durchgefallen (`backtest_squeeze_m15.py`: n=33 in H1, Nachbarn kippen). Offen bleibt die andere Hälfte der Idee: den validierten M5-Einstieg behalten und nur den EXIT gröber führen. Dafür spricht die Kostenseite (Median Spread/ATR: M15 **0,104** gegen M5 0,193) und der dokumentierte Befund, dass der Verlust-Hebel im Exit und im Sizing liegt, nicht im Signal. ⚠ IM CODE STEHT DIESE FRAGE SEIT DEM 31.07. OFFEN — `core/trailing.py`: „Falls die Staffelung je zurück soll: erst `backtest_trailing.py` auf M15-/M30-BASIS-Signalen laufen lassen — **bisher misst er nur M5**." Live nimmt das Trailing den ATR der WELLEN-Zeitebene als Abstandsmaß; die Heuristik landet häufig auf M15/M30 (31.07.: 19 von 33 Phasen). Welcher ATR das sein SOLLTE, ist nie gemessen worden. ⚠⚠ DIE FALLE, DIE DIESE MESSUNG SONST WERTLOS MACHT: `simulate()` normiert das Ergebnis mit DEMSELBEN ATR, den es für SL/Trail/TP benutzt (R = Gewinn/ATR). Mit dem ~1,8× größeren M15-ATR würde dieselbe Dollar-Bewegung zu einem kleineren R — die Varianten wären **nicht vergleichbar**, und die M15-Variante sähe mechanisch schlechter aus. Beide werden deshalb in **M5-Risikoeinheiten** zurückgerechnet: R_m5 = R_ergebnis × ATR_verwendet / ATR_M5. ⚠ NICHT dasselbe wie `backtest_sl_basis.py` (05.08.): dort wurde nur der INITIAL-SL variiert (Ergebnis: Wash beim Squeeze, Tail-Risiko ×3, daraufhin `_calc_sl_tp(atr_tf=M5)` für Squeeze-Trades gebaut). Hier hängt der GESAMTE Exit am ATR — SL, Trail-Abstand, Breakeven, Lock-Schwellen und TP. AUFBAU: feste, geteilte Entry-Liste (Squeeze auf M5, dokumentierte Parameter Box 10 / k 0,1 / Floor 0,12), Touch-Fill, Echtkosten, kanonischer Exit-Kern, 2 Halbjahre. M15-ATR kausal aus den M5-Bars aggregiert. ⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: die M15-Variante muss in BEIDEN Hälften mehr ΣR (in M5-Einheiten) liefern UND darf den Worst-Case nicht verschlechtern. Ein breiterer Exit kauft Ertrag sonst mit Tail-Risiko — genau die Abwägung, die bei `backtest_sl_basis.py` gegen die Umstellung sprach. Aufruf: python backtest_exit_atr_tf.py [n_M5_bars] (Default 80000) """ from __future__ import annotations import sys import MetaTrader5 as mt5 from core.exit_model import LIVE, simulate from core.squeeze_scan import atr_series, scan _FLOOR = 0.12 # dokumentierter M5-Floor (backtest_squeeze_touchfill.py) _M15 = 3 # 3 M5-Bars = 1 M15-Bar def atr_m15_auf_m5(H, L, C): """ATR aus M15-Bars, auf den M5-Index abgebildet. ⚠ LOOK-AHEAD-frei: an M5-Index i zählt nur der letzte ABGESCHLOSSENE M15-Bar. Dieselbe Konstruktion wie `_htf_signs` in `backtest_auto_signal`.""" hh, hl, hc = [], [], [] for s in range(0, len(C) - _M15 + 1, _M15): hh.append(max(H[s:s + _M15])); hl.append(min(L[s:s + _M15])) hc.append(C[s + _M15 - 1]) a15 = atr_series(hh, hl, hc) out = [None] * len(C) for i in range(len(C)): j = i // _M15 - 1 out[i] = a15[j] if 0 <= j < len(a15) else None return out def kz(Rs, label): if not Rs: return f" {label:<26} n=0" n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0) return (f" {label:<26} n={n:>4} Treffer={100*w/n:>3.0f}% ØR={s/n:+.3f} " f"PF={(g/v if v > 0 else 9.99):>4.2f} ΣR={s:>+6.0f} Worst={min(Rs):+.2f}") 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] A5 = atr_series(H, L, C) A15 = atr_m15_auf_m5(H, L, C) mid = len(C) // 2 gueltig = [(a15 / a5) for a5, a15 in zip(A5, A15) if a5 and a15 and a5 > 0] gueltig.sort() print("=" * 98) print(f" EXIT-ABSTAND aus M5-ATR gegen M15-ATR — {sym}, {len(C)} M5-Bars") print(f" Verhältnis ATR_M15/ATR_M5: Median {gueltig[len(gueltig)//2]:.2f}×") print(" ⚠ Beide Varianten in M5-RISIKOEINHEITEN gerechnet, sonst unvergleichbar") print("=" * 98) for nm, lo, hi in (("H1", 40, mid), ("H2", mid, len(C) - LIVE.max_hold - 2)): # ── EINE feste Entry-Liste (der Einstieg ist in beiden Varianten gleich) eintraege = [] # ⚠ NUR komprimierte Ausbrueche = die Squeeze-Population. `exit_fn` # allein taugt dafuer NICHT: es wird bei JEDEM Box-Ausbruch gerufen und # lieferte still die Kontrollgruppe (519 statt 132 Einstiege). def sammeln(j, d, lvl, atr, komprimiert, _l=eintraege): if komprimiert: _l.append((lvl, d, j + 1)) scan(H, L, C, A5, SP, 1.0, lo, hi, 2.5, box_n=10, k=0.1, atr_min=_FLOOR, max_hold=LIVE.max_hold, on_entry=sammeln) print(f"\n ── {nm} ── {len(eintraege)} feste Squeeze-Einstiege, " f"beide Varianten identisch") for label, quelle in (("M5-ATR (heute)", A5), ("M15-ATR", A15)): Rs = [] for entry, d, j0 in eintraege: i = j0 - 1 a5 = A5[i] a = quelle[i] if not a or not a5 or a5 <= 0: continue a = max(a, _FLOOR) R = simulate(entry, d, a, H, L, C, j0, LIVE) # ⚠ zurueck in M5-Einheiten: R × verwendeter ATR ÷ ATR_M5 r_m5 = R * a / max(a5, _FLOOR) kosten = (SP[i] if SP[i] > 0 else 0.0225) / max(a5, _FLOOR) Rs.append(r_m5 - kosten) print(kz(Rs, label)) if __name__ == "__main__": main()