diff --git a/CLAUDE.md b/CLAUDE.md index 3fe2867..6641b7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5425,6 +5425,59 @@ nie feuert, belegt nichts). ⚠ Der gefährlichste Fall hat einen eigenen Test: `frei` MUSS auf dem **ungerundeten** Abstand entschieden werden — bei 0,5951 würde die gerundete 0,60 den Fall fälschlich durchlassen. +## ⚠⚠ NUR DAS TRAILING BREITER (Richtung M15)? = FÄLLT DURCH, und der +## Auflösungs-Einwand trägt nicht (`backtest_trail_tf.py`, 2026-08-11) + +**Anlass:** User „sollten wir das Trailing auf M15 setzen?" nach einem Trade, der +bei Trail 1,0×ATR_M5 nach rund **1×ATR Rücklauf auf Einstand** ausgestoppt wurde +(#49850541 — bei genau diesem Abstand ist das zwangsläufig). + +⚠ **Warum die Frage NICHT schon beantwortet war.** `backtest_exit_atr_tf.py` +(08.08.) stellte den **gesamten** Exit auf ATR_M15 — `simulate()` nimmt EINEN +`atr` für SL, Trail, TP **und** Breakeven, dort wurde also alles mitverbreitert +(daher das verdreifachte Tail). `backtest_trailmult.py` (31.07.) traf die Frage +(fixer SL 2,0, nur `mult` gesweept), lief aber auf **M5-Bars** — und genau davor +warnt `analyze_trail_resolution.py` (05.08.): eine Bar-Simulation **überschätzt +enge Trails und unterschätzt weite**, bei 1,5×ATR kippte dort sogar das +Vorzeichen (M5 −0,038 gegen M1 **+0,028**). Der Sweep, auf dem „enger ist besser" +ruht, war also mit einer Verzerrung **GEGEN** weite Trails gemessen. + +**Diese Messung kontrolliert das:** dieselben Einstiege, derselbe ATR (aus M5) → +Trail-Abstand in **Dollar identisch**, nur die Exit-Auflösung unterscheidet sich; +Zeitgrenzen **wall-clock gleich** (200 M5-Bars = 1000 M1-Bars, Time-Stop 24 = 120 +— der Altbestand liess 200 stehen und kappte M1-Trades nach 200 statt 1000 min). +Zwei Populationen, Echtkosten. +| mult | Squeeze M5 H1/H2 | **Squeeze M1** | Welle M5 | **Welle M1** | +|---|---|---|---|---| +| 0,80 | +0,123 / +0,557 | **+0,077 / +0,292** | −0,131 / +0,022 | −0,237 / −0,183 | +| **1,00 (live)** | +0,019 / +0,495 | **−0,067 / +0,206** | −0,242 / +0,031 | −0,302 / −0,127 | +| 1,50 | −0,155 / +0,624 | −0,095 / +0,446 | −0,275 / +0,063 | −0,405 / −0,112 | +| **1,86 („M15")** | **−0,355 / +0,557** | **−0,327 / +0,379** | −0,297 / +0,053 | **−0,392 / −0,067** | +**1,86 fällt in H1 auf BEIDEN Populationen und BEIDEN Auflösungen klar durch** +(Trefferquote 25–30 %, PF 0,52–0,64). Regel nicht ansatzweise erfüllt. + +✅✅ **DER EIGENTLICHE ERTRAG: der Auflösungs-Einwand ist entkräftet — er wirkt +nur bei ENGEN Trails.** Δ (M1 − M5) auf der Welle: **−0,092/−0,236 bei 0,50**, +−0,060/−0,158 bei 1,00, −0,095/−0,120 bei 1,86; auf dem Squeeze bei 2,00 +**+0,049/−0,016 = praktisch null**. Bei weiten Trails verschwindet der +Unterschied — logisch, ein weiter Stop ist nicht auflösungsempfindlich. +⚠ **Das PRÄZISIERT den Befund vom 05.08.**: die feinere Auflösung **bestraft enge +Trails**, sie **belohnt weite nicht**. Der dortige Vorzeichenwechsel bei 1,5 kam +aus n=597 **willkürlichen** Einstiegen (jeder 20. Bar, immer LONG) — mit echten +Einstiegen reproduziert er nicht. + +⚠ **Grenzen, ehrlich:** Squeeze nur **n=46/47** je Hälfte; die M1-Historie deckt +~55 Tage, die „zwei Hälften" sind also **zwei Hälften EINES Regimes**, nicht zwei +Regime. Ein ✅ wäre hier wenig wert — das ❌ bei 1,86 ist es, weil es in **jeder** +Zelle auftritt. Die Wellen-Population ist in diesem Fenster durchgehend negativ +und kann deshalb gar nicht diskriminieren. +⚠ **Nebenbefund, ausdrücklich KEINE Empfehlung:** auf M1-Auflösung sieht **0,8 +besser aus als 1,0** (Squeeze +0,077/+0,292 gegen −0,067/+0,206) — also eher +*enger*, die Gegenrichtung der Frage. Für eine Umstellung reicht das nicht: ein +Regime, dünne Stichprobe, und die Sim modelliert **keine Exit-Slippage**, die +enge Trails härter trifft (real bis 0,75×ATR über den Stop hinaus). +**Keine Änderung — Trail bleibt 1,0×ATR_M5.** + ## ⚠⚠ EXIT-ABSTAND AUS M15-ATR = FÄLLT DURCH (2026-08-08) Zweiter Teil der M15-Idee: M5-Einstieg behalten, nur den EXIT gröber führen. diff --git a/backtest_trail_tf.py b/backtest_trail_tf.py new file mode 100644 index 0000000..13b1625 --- /dev/null +++ b/backtest_trail_tf.py @@ -0,0 +1,151 @@ +#!/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())