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
+53
View File
@@ -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 `frei` MUSS auf dem **ungerundeten** Abstand entschieden werden — bei 0,5951 würde
die gerundete 0,60 den Fall fälschlich durchlassen. 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 2530 %, PF 0,520,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) ## ⚠⚠ 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. Zweiter Teil der M15-Idee: M5-Einstieg behalten, nur den EXIT gröber führen.
+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())