#!/usr/bin/env python3 """Trailing breiter (Richtung M15) — haelt der M5-Befund auf M1-Aufloesung? ANLASS (2026-08-11, User: „sollten wir das Trailing auf M15 setzen?"). Auslöser war ein Trade, der nach ~1×ATR Ruecklauf auf Einstand ausgestoppt wurde — bei Trail 1,0×ATR_M5 ist das zwangslaeufig. ⚠⚠ WAS SCHON GEMESSEN IST (und die Frage NICHT beantwortet): · `backtest_exit_atr_tf.py` (08.08.) hat den GESAMTEN Exit auf ATR_M15 gestellt → schlechter in beiden Haelften, Tail-Risiko verdreifacht. ⚠ Aber `simulate()` nimmt EINEN atr fuer SL, Trail, TP und Breakeven — dort wurde also alles mitverbreitert. Das ist NICHT „nur das Trailing". · `backtest_trailmult.py` (31.07.) hat `mult` bei FIXEM SL 2,0 gesweept → enger ist monoton besser (1,0: +80/+1360 · 1,5: −220/+1305 · 2,0: −394/+1198). Das trifft die Frage — aber es lief auf **M5-Bars**. ⚠⚠ UND GENAU DA SITZT DER ZWEIFEL: `analyze_trail_resolution.py` (05.08.) hat gezeigt, dass eine Bar-Simulation **enge Trails ueberschaetzt und weite unterschaetzt**; das Vorzeichen kippt bei ~0,3×ATR. Bei 1,5×ATR meldete M5 −0,038, M1 dagegen **+0,028** — ein Vorzeichenwechsel. Der Sweep, der „enger ist besser" sagt, ist also mit einer Verzerrung GEGEN weite Trails gemessen. DIESE MESSUNG kontrolliert genau das: · **dieselben Einstiege**, derselbe ATR (aus M5) → der Trail-Abstand ist in DOLLAR identisch, nur die Aufloesung des Exits unterscheidet sich. · Zeitgrenzen in WALL-CLOCK gleich gehalten: `max_hold` 200 M5-Bars = 1000 M1-Bars, `timestop` 24 = 120. ⚠ Der Altbestand liess 200 stehen und kappte M1-Trades damit nach 200 statt 1000 Minuten. · zwei Populationen (Squeeze + Welle), zwei Haelften, Echtkosten. · `mult = 1.86` ist die „Trailing auf M15"-Variante: Median ATR_M15/ATR_M5. ⚠ ENTSCHEIDUNGSREGEL, VORAB: ein breiterer Trail wird nur empfohlen, wenn er auf **M1-Aufloesung** in BEIDEN Haelften UND auf BEIDEN Populationen besser ist als 1,0 — und die Nachbarwerte nicht kippen. Aufruf: python backtest_trail_tf.py [n_M1] (Default 80000) """ from __future__ import annotations import bisect import sys import MetaTrader5 as mt5 from core.exit_model import LIVE, simulate from core.squeeze_scan import atr_series, scan _MULTS = (0.5, 0.8, 1.0, 1.3, 1.5, 1.86, 2.0, 2.5) _M5_IN_M1 = 5 def hole(tf, n): b = mt5.copy_rates_from_pos(SYM, tf, 0, n) if b is None: raise SystemExit(f"keine Bars fuer {tf}") return ([int(x["time"]) for x in b], [float(x["high"]) for x in b], [float(x["low"]) for x in b], [float(x["close"]) for x in b], [float(x["spread"]) * POINT for x in b]) def kennz(Rs): n = len(Rs) if not n: return " —" w = sum(1 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) return (f"{n:>5}{100*w/n:>7.0f} %{sum(Rs)/n:>+9.3f}" f"{(g/v if v else 9.99):>7.2f}{sum(Rs):>+9.1f}") def main(): global SYM, POINT nM1 = 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) POINT = mt5.symbol_info(SYM).point T5, H5, L5, C5, SP5 = hole(mt5.TIMEFRAME_M5, 20000) T1, H1, L1, C1, _ = hole(mt5.TIMEFRAME_M1, nM1) mt5.shutdown() A5 = atr_series(H5, L5, C5) # gemeinsames Fenster — nur wo BEIDE Zeitebenen Daten haben t_von, t_bis = max(T5[0], T1[0]), min(T5[-1], T1[-1]) lo = next(i for i, t in enumerate(T5) if t >= t_von) hi = max(i for i, t in enumerate(T5) if t <= t_bis) # ── Einstiege sammeln: Squeeze (Auto-Pfad) ──────────────────────────── sq_ein: list = [] scan(H5, L5, C5, A5, SP5, POINT, lo, hi, 2.5, on_entry=lambda j, d, lvl, atr, komp: ( sq_ein.append((j, d, lvl, atr)) if komp else None)) # ── und Welle (EMA-Kreuz, die manuelle Population) ──────────────────── def ema(v, p): k = 2 / (p + 1); o = [v[0]] for x in v[1:]: o.append(o[-1] + k * (x - o[-1])) return o ef, es = ema(C5, 12), ema(C5, 50) wv_ein = [] vor = 0 for i in range(lo, hi): a = A5[i] if not a or a < 0.06: continue s = 1 if ef[i] > es[i] else -1 if s != vor and vor != 0: wv_ein.append((i, s, C5[i], a)) vor = s print("=" * 104) print(f" TRAIL-BREITE × AUFLOESUNG — {SYM}") print(f" gemeinsames Fenster: {len(T1)} M1-Bars, {hi-lo} M5-Bars") print(f" Squeeze-Einstiege {len(sq_ein)} · Wellen-Einstiege {len(wv_ein)}") print(" ⚠ Gleiche Einstiege, gleicher ATR (M5) → Trail-Abstand in DOLLAR") print(" identisch; NUR die Aufloesung des Exits unterscheidet sich.") print(" Zeitgrenzen wall-clock gleich (200 M5-Bars = 1000 M1-Bars).") print("=" * 104) for name, ein in (("SQUEEZE", sq_ein), ("WELLE", wv_ein)): mid = len(ein) // 2 print(f"\n ══ {name} ══") print(f" {'mult':<7}{'':>3}{'Haelfte':<9}{'n':>5}{'Treffer':>9}{'OeR':>9}" f"{'PF':>7}{'SumR':>9} ||{'n':>5}{'Treffer':>9}{'OeR':>9}{'PF':>7}{'SumR':>9}") print(f" {'':<19}{'--- Exit auf M5 ---':^39} {'--- Exit auf M1 ---':^39}") for m in _MULTS: p5 = LIVE.with_(mult=m) p1 = LIVE.with_(mult=m, timestop_bars=LIVE.timestop_bars * _M5_IN_M1, max_hold=LIVE.max_hold * _M5_IN_M1) for lbl, teil in (("H1", ein[:mid]), ("H2", ein[mid:])): r5, r1 = [], [] for j, d, px, atr in teil: kost = (SP5[j] if SP5[j] > 0 else 0.0225) / atr if j + 1 >= len(C5) - LIVE.max_hold - 2: continue r5.append(simulate(px, d, atr, H5, L5, C5, j + 1, p5) - kost) k = bisect.bisect_left(T1, T5[j]) if not (0 < k < len(C1) - p1.max_hold - 2): continue r1.append(simulate(px, d, atr, H1, L1, C1, k + 1, p1) - kost) print(f" {m:<7.2f}{'':>3}{lbl:<9}{kennz(r5)} ||{kennz(r1)}") print("\n " + "-" * 100) print(" ⚠ REGEL (vorab): breiter nur, wenn auf M1-Aufloesung in BEIDEN Haelften") print(" UND auf BEIDEN Populationen besser als 1,0 — Nachbarn duerfen nicht kippen.") print(" ⚠ M1-Historie ist beim Broker hart gedeckelt → kuerzeres Fenster als 80k M5.") return 0 if __name__ == "__main__": raise SystemExit(main())