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>
152 lines
6.6 KiB
Python
152 lines
6.6 KiB
Python
#!/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())
|