From 9ad1b6d8d45016121880d3b23a5188bbed8adb53 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Fri, 31 Jul 2026 07:51:27 +0200 Subject: [PATCH] backtest_trailing_sr.py: S/R-verankertes Trailing gemessen - nicht gebaut User-Idee "Trailing TP und SL auf die S/R-Linien anpassen". Zerfaellt in drei Teile; Initial-SL an S/R und fester TP am Gegenlevel waren schon verworfen, der TRAILING-Teil war offen (bisher nur ATR-Abstaende getestet). Aufbau wie backtest_trailing.py (echte Phasen-Mechanik, Breakeven 1,3), Level = M30-Pivots k=3 KAUSAL (Pivot erst 3 M30-Bars spaeter bekannt), Deckel [0,5..2,0]xATR gegen den -11,4xATR-Fall aus backtest_sl_method.py. 80k Bars, ~19,7k Signale je Haelfte. Ergebnis: schadet nicht, traegt aber zu wenig. 3 von 5 Varianten schlagen die Basis in BEIDEN Haelften (Hybrid "engerer Stop" +0,006/+0,006, WR 37->43 %). Trotzdem nicht gebaut: (a) Effekt 0,006-0,014 R/Trade = eine Groessenordnung unter der B3-Huerde; (b) nicht parameter-robust - Puffer 0,10 besteht, 0,25 faellt durch, 0,50 besteht wieder. Positiv: Worst-Case bleibt in ALLEN Varianten -2,00xATR, der Deckel funktioniert - S/R-Verankerung fuegt kein Tail-Risiko hinzu. Die Kontrollvariante "TP am Gegenlevel" reproduziert den alten Befund exakt (WR 40->54 %, SigmaR H2 +1261->+740 = Gewinner-Kappen) -> Simulation gegen backtest_srclose.py validiert, TP-Teil endgueltig erledigt. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 24 ++++ backtest_trailing_sr.py | 251 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 backtest_trailing_sr.py diff --git a/CLAUDE.md b/CLAUDE.md index e17eff7..e3ccdaf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -680,6 +680,30 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: nur X% → Bot schließt" bzw. grün „Durchbruch X% wahrscheinlich → läuft weiter". Abschalten `auto_sr_close=false` / `set_sr_autoclose(False)`; Schwelle via `sr_close_pbreak`. Kein pauschaler S/R-Close (2× verworfen) — nur mit P(break)-Gate. + - **S/R-verankertes TRAILING = GEMESSEN, NICHT gebaut (`backtest_trailing_sr.py`, + 2026-07-31, User-Idee „Trailing TP und SL auf die S/R-Linien anpassen"):** Die Idee + zerfällt in drei Teile — Initial-SL an S/R (schon verworfen, s. u.), fester TP am + Gegenlevel (schon 2× verworfen, `backtest_srclose.py`) und **Trailing-SL ans Level + nachziehen** = der einzig offene Teil (bisher wurden nur ATR-Abstände variiert). + Aufbau identisch zu `backtest_trailing.py` (echte Phasen-Mechanik, Breakeven 1,3), + Level = **M30-Pivots k=3, kausal** (Pivot erst 3 M30-Bars später bekannt), Deckel + **[0,5…2,0]×ATR** gegen den −11,4×ATR-Fall; 80k Bars, ~19,7k Signale je Hälfte. + **Ergebnis: schadet nicht, trägt aber zu wenig.** Basis (ATR-Trail 1,5) H1 −0,020 / + H2 +0,064; **3 von 5 Varianten schlagen sie in BEIDEN Hälften** (Puffer 0,10: + +0,009/+0,004 · Puffer 0,50: +0,003/+0,014 · **Hybrid „engerer von beiden": + +0,006/+0,006**, WR 37→43 % bzw. 40→43 %). **Trotzdem nicht gebaut**, zwei Gründe: + (a) **Effektgröße 0,006–0,014 R/Trade** = eine Größenordnung unter der B3-Hürde + (~0,1×ATR netto); (b) **nicht parameter-robust** — Puffer 0,10 besteht, **0,25 fällt + durch** (H1 −0,023), 0,50 besteht wieder; ein Parameter, dessen Nachbar das Vorzeichen + kippt, ist Rauschen (dasselbe Kriterium, an dem ORB und die SMC-Sweeps starben — + beim Squeeze waren ALLE 8 Kombis positiv). ✅ Positiv-Befund: **der Worst-Case bleibt + in ALLEN Varianten −2,00×ATR** — der Deckel funktioniert, S/R-Verankerung fügt kein + Tail-Risiko hinzu (das war die Sorge aus `backtest_sl_method.py`). ⚠ **Die + KONTROLLVARIANTE „TP am Gegenlevel" reproduziert den alten Befund exakt** (WR 40→54 %, + ΣR H2 +1261→+740 = Gewinner-Kappen) — damit ist die Simulation gegen + `backtest_srclose.py` validiert und der TP-Teil endgültig erledigt. **Offener + Nachtest:** dieselbe Messung NUR auf Squeeze-Entries (runner-abhängig → dort könnte + ein Level-Stop mehr bewegen als über alle Wave-Signale gemittelt). - **S/R-verankerter SL = KEIN Edge (gemessen, `backtest_sl_method.py`):** Pivot+Band vs. „S/R ∓ ATR/0,10-Puffer" vs. fest 2,0×ATR — **gedeckelt alle identisch** (Ø-R 0,196–0,201, PF 1,38–1,39, Worst −2,2×ATR): der **Band-Cap diff --git a/backtest_trailing_sr.py b/backtest_trailing_sr.py new file mode 100644 index 0000000..a70dcd2 --- /dev/null +++ b/backtest_trailing_sr.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""S/R-verankertes TRAILING statt fixem ATR-Abstand (User-Idee 2026-07-31). + +FRAGE: Der Live-Trail zieht den SL stur `mult×ATR` hinter das Hoch. Wäre es besser, +ihn hinter das **nächste S/R-Level** zu legen (LONG: knapp unter die Unterstützung)? +Und/oder einen TP ans Gegenlevel zu setzen? + +⚠ ABGRENZUNG — zwei der drei Teil-Ideen sind SCHON gemessen und durchgefallen: + * **Initial-SL an S/R** → `backtest_sl_method.py`: gedeckelt identisch zu fix + 2,0×ATR (Ø-R 0,196–0,201), der Band-Cap dominiert; ungedeckelt Worst −11,4×ATR. + * **Fester TP am Gegenlevel** → `backtest_srclose.py` (2×): verdoppelt die WR, + verschlechtert aber PF/ΣR in BEIDEN Hälften = Gewinner-Kappen. + * **TRAILING-SL an S/R** = hier NEU gemessen (bisher nur ATR-Abstände getestet, + `backtest_trailing.py`). + +Deshalb wird der TP-Teil hier NUR als Kontrollvariante mitgeführt (Erwartung: fällt +durch — dient als Sanity-Check, dass die Sim mit dem alten Befund übereinstimmt). + +AUFBAU (identisch zu `backtest_trailing.py`, damit die Zahlen vergleichbar sind): +echte Phasen-Mechanik (Init→Trail→Lock, HW-Ratsche, Breakeven-Boden 1,3), Signal-Set +Trend+Reversal, 2 Halbjahre. Level-Quelle = **M30-Pivots** (k=3) — dieselbe Quelle, +auf die der Live-Bot am 2026-07-30 umgestellt hat; kausal (ein Pivot ist erst 3 M30- +Bars später bekannt). + +Urteil nur „trägt", wenn eine Variante in BEIDEN Hälften ≥ Live-Basis liegt UND der +Worst-Case nicht davonläuft (die Lehre aus `backtest_sl_method.py`). +""" +import sys +from bisect import bisect_left + +import MetaTrader5 as mt5 + +from core.analysis import calc_trend_angle +from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD, + _REVERSAL_STRETCH, _STRETCH_MAX) + +_MAXH = 200; _ATRMIN = 0.12; _TP_INIT = 3.5 +_LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2 +_BE = 1.3 # Breakeven-Boden (live) +_M30 = 6 # M5-Bars je M30-Bar +_PIV_K = 3 # Pivot-Fenster (wie live `pb_levels30`) +_PIV_LOOKBACK = 50 # letzte N M30-Bars (wie live) +# Sicherheits-Deckel für den S/R-Trail: ohne den droht der −11,4×ATR-Fall aus +# `backtest_sl_method.py` (Level zu weit weg = Kontokiller unter Margin-Sizing). +_SL_MIN_ATR = 0.5 +_SL_MAX_ATR = 2.0 + + +def _ema_series(v, p): + k = 2.0 / (p + 1); o = []; e = v[0] + for i, x in enumerate(v): + e = x if i == 0 else x * k + e * (1 - k) + o.append(e) + return o + + +def _atr_series(H, L, C, p=14): + t = [0.0] + for i in range(1, len(C)): + t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [] + for i in range(len(C)): + if not i: + out.append(None); continue + w = t[max(1, i - p + 1):i + 1] + out.append(sum(w) / max(1, len(w))) + return out + + +def build_levels(H, L): + """M30-Pivots (k=3) je M5-Bar-Index — KAUSAL. + + Liefert `lv[m]` = sortierte Level-Liste, die zum Beginn von M30-Bar `m` bereits + bekannt ist (ein Pivot bei M30-Index p ist erst ab p+k bestätigt). + """ + nm = len(H) // _M30 + mh = [max(H[i * _M30:(i + 1) * _M30]) for i in range(nm)] + ml = [min(L[i * _M30:(i + 1) * _M30]) for i in range(nm)] + # Pivot bei p → bekannt ab p + _PIV_K + born: list[list[float]] = [[] for _ in range(nm)] + for p in range(_PIV_K, nm - _PIV_K): + seg_h = mh[p - _PIV_K:p + _PIV_K + 1] + seg_l = ml[p - _PIV_K:p + _PIV_K + 1] + if mh[p] == max(seg_h): + if p + _PIV_K < nm: + born[p + _PIV_K].append(mh[p]) + if ml[p] == min(seg_l): + if p + _PIV_K < nm: + born[p + _PIV_K].append(ml[p]) + # rollierendes Fenster der letzten _PIV_LOOKBACK M30-Bars + lv: list[list[float]] = [] + from collections import deque + win: deque = deque() + cur: list[float] = [] + for m in range(nm): + win.append(born[m]) + cur.extend(born[m]) + while len(win) > _PIV_LOOKBACK: + old = win.popleft() + for x in old: + try: + cur.remove(x) + except ValueError: + pass + lv.append(sorted(cur)) + return lv + + +def _sr_below(levels, px): + i = bisect_left(levels, px) + return levels[i - 1] if i > 0 else None + + +def _sr_above(levels, px): + i = bisect_left(levels, px) + return levels[i] if i < len(levels) else None + + +def sim(entry, d, atr, H, L, C, j0, LV, mode="atr", mult=1.5, + buf=0.25, trail_start=0.3, use_sr_tp=False): + """Phasen-Trailing. `mode`: + 'atr' — Live: SL = HW − mult×ATR + 'sr' — SL = nächstes Level in Gegenrichtung ∓ buf×ATR (auf [0,5..2,0]×ATR gedeckelt) + 'hybrid' — der ENGERE von beiden (schützt Gewinne früher) + 'sr_wide'— der WEITERE von beiden (gibt dem Trade mehr Luft) + """ + sl = entry - d * 2.0 * atr # Initial-SL wie live (2,0×ATR) + tp = entry + d * _TP_INIT * atr + if use_sr_tp: + lv = LV[min(j0 // _M30, len(LV) - 1)] + t = _sr_above(lv, entry) if d > 0 else _sr_below(lv, entry) + if t is not None: + tp = t + hw = entry; rank = 0 + end = min(j0 + _MAXH, len(C) - 1) + for j in range(j0, end + 1): + hi, lo = H[j], L[j] + if (lo <= sl) if d > 0 else (hi >= sl): + return (sl - entry) * d / atr # pessimistisch: SL vor TP im selben Bar + if (hi >= tp) if d > 0 else (lo <= tp): + return (tp - entry) * d / atr + hw = max(hw, hi) if d > 0 else min(hw, lo) + profit = (hw - entry) * d + ph = 0 if profit < trail_start * atr else (1 if profit < _LOCK_START * atr else 2) + if ph < rank: + ph = rank + rank = ph + if ph == 0: + continue + m = mult if ph == 1 else max(_LOCK_MIN, mult * _LOCK_SCALE) + cand_atr = hw - d * m * atr + cand = cand_atr + if mode != "atr": + lv = LV[min(j // _M30, len(LV) - 1)] + lev = _sr_below(lv, C[j]) if d > 0 else _sr_above(lv, C[j]) + if lev is not None: + cand_sr = lev - d * buf * atr + # Deckel: nie enger als 0,5×ATR (Rauschen) / weiter als 2,0×ATR (Tail) + dist = (C[j] - cand_sr) * d + if dist < _SL_MIN_ATR * atr: + cand_sr = C[j] - d * _SL_MIN_ATR * atr + elif dist > _SL_MAX_ATR * atr: + cand_sr = C[j] - d * _SL_MAX_ATR * atr + if mode == "sr": + cand = cand_sr + elif mode == "hybrid": # der engere = höher (LONG) + cand = max(cand_atr, cand_sr) if d > 0 else min(cand_atr, cand_sr) + elif mode == "sr_wide": + cand = min(cand_atr, cand_sr) if d > 0 else max(cand_atr, cand_sr) + if profit >= _BE * atr or ph == 2: + cand = max(cand, entry) if d > 0 else min(cand, entry) + sl = max(sl, cand) if d > 0 else min(sl, cand) + return (C[end] - entry) * d / atr + + +def st(Rs): + if not Rs: + return " -" + n = len(Rs); w = sum(1 for r in Rs if r > 0) + g = sum(r for r in Rs if r > 0); ls = -sum(r for r in Rs if r < 0) + return (f"WR={100*w/n:>3.0f}% Ø-R={sum(Rs)/n:+.3f} PF={(g/ls if ls>0 else 99):>4.2f} " + f"Worst={min(Rs):+.2f} ΣR={sum(Rs):+.0f}") + + +def signals(H, L, C, ES, EF, AT, lo, hi, step): + TH = _REVERSAL_STRETCH; out = [] + for i in range(max(lo, _N_BARS), min(hi, len(C) - _MAXH - 1), step): + atr = AT[i] + if not atr or atr <= 0: + continue + atr = max(atr, _ATRMIN); es = ES[i]; ef = EF[i] + stretch = (C[i] - es) / atr + ang = calc_trend_angle(C[i - _ANGLE_LR - 2:i], _ANGLE_LR); ad = ang - 90.0 + d = 0 + if stretch <= -TH and ad >= _ANGLE_DEAD: + d = 1 + elif stretch >= TH and ad <= -_ANGLE_DEAD: + d = -1 + elif abs(stretch) < _STRETCH_MAX: + d = 1 if ef > es else -1 if ef < es else 0 + if d: + out.append((i, d, atr)) + return out + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + step = int(sys.argv[2]) if len(sys.argv) > 2 else 2 + mt5.initialize(); sym = None + for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): + if mt5.symbol_info(c): + sym = c; break + bars = None + for req in (n, 100000, 80000, 60000, 40000): + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, req) + if bars is not None and len(bars) > 2000: + break + mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars] + EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C) + LV = build_levels(H, L) + mid = len(C) // 2 + halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))] + print("=" * 96) + print(f" S/R-VERANKERTES TRAILING — {sym} M5 ({len(C)} Bars, step {step})") + print(f" Level = M30-Pivots k={_PIV_K}, Fenster {_PIV_LOOKBACK} M30-Bars (kausal), " + f"Deckel [{_SL_MIN_ATR}..{_SL_MAX_ATR}]×ATR") + print("=" * 96) + SIG = {lbl: signals(H, L, C, ES, EF, AT, a, b, step) for lbl, a, b in halves} + for lbl, _, _ in halves: + print(f" {lbl}: {len(SIG[lbl])} Signale") + + variants = [ + ("LIVE ATR-Trail 1,5 (Basis)", dict(mode="atr", mult=1.5)), + ("S/R-Trail Puffer 0,10", dict(mode="sr", buf=0.10)), + ("S/R-Trail Puffer 0,25", dict(mode="sr", buf=0.25)), + ("S/R-Trail Puffer 0,50", dict(mode="sr", buf=0.50)), + ("Hybrid (engerer von beiden)", dict(mode="hybrid", buf=0.25)), + ("Hybrid weit (weiterer)", dict(mode="sr_wide", buf=0.25)), + ("KONTROLLE: TP am Gegenlevel", dict(mode="atr", mult=1.5, use_sr_tp=True)), + ] + for name, kw in variants: + print(f"\n{name}") + for lbl, _, _ in halves: + Rs = [sim(C[i], d, atr, H, L, C, i + 1, LV, **kw) for (i, d, atr) in SIG[lbl]] + print(f" {lbl} {st(Rs)}") + + +if __name__ == "__main__": + main()