Trailing breiter (Richtung M15) gemessen: faellt durch

User-Frage nach einem Trade, der bei Trail 1,0xATR_M5 nach rund einer
ATR Ruecklauf auf Einstand ausgestoppt wurde.

Die Frage war nicht schon beantwortet: backtest_exit_atr_tf.py (08.08.)
stellte den GESAMTEN Exit auf ATR_M15 (simulate nimmt EINEN atr fuer
SL, Trail, TP und Breakeven - daher das verdreifachte Tail), und
backtest_trailmult.py (31.07.) traf die Frage, lief aber auf M5-Bars.
Genau davor warnt analyze_trail_resolution.py: eine Bar-Simulation
ueberschaetzt enge Trails und unterschaetzt weite - bei 1,5xATR kippte
dort sogar das Vorzeichen. Der Sweep war also mit einer Verzerrung
GEGEN weite Trails gemessen.

Diese Messung kontrolliert das: gleiche Einstiege, gleicher ATR (M5),
Trail-Abstand in Dollar identisch, nur die Exit-Aufloesung variiert;
Zeitgrenzen wall-clock gleich (200 M5-Bars = 1000 M1-Bars, Time-Stop
24 = 120 - der Altbestand kappte M1-Trades nach 200 statt 1000 min).

Ergebnis: 1,86 ("M15") faellt in H1 auf BEIDEN Populationen und BEIDEN
Aufloesungen klar durch (Treffer 25-30 %, PF 0,52-0,64).

Wichtiger Nebenertrag: der Aufloesungs-Einwand ist entkraeftet. Die
Differenz M1-M5 ist bei ENGEN Trails gross und verschwindet bei
weiten. Die feinere Aufloesung bestraft enge Trails, sie belohnt weite
nicht. Der Vorzeichenwechsel vom 05.08. kam aus willkuerlichen
Einstiegen und reproduziert mit echten nicht.

Grenzen: Squeeze n=46/47 je Haelfte, M1-Historie ~55 Tage - die zwei
Haelften sind zwei Haelften EINES Regimes. Keine Aenderung, Trail
bleibt 1,0xATR_M5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-11 22:07:29 +02:00
co-authored by Claude Opus 5
parent 4eb92bf192
commit 661b99cc88
2 changed files with 204 additions and 0 deletions
+151
View File
@@ -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())