From 3314b6e9b1b3864c6a32f9643f3d04f3d5e4e62a Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Fri, 31 Jul 2026 18:15:09 +0200 Subject: [PATCH] Flip-Close bekommt Nachrang gegenueber dem S/R-Close (gemessen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-Frage "sollten wir den S/R close auch deaktivieren?" -> nein, die beiden sind gegensaetzlich gemessen. Aber sie konkurrieren seit dem Einbau um dieselben Trades, und die S/R-Freigabe lief OHNE Flip-Close. backtest_exit_combo.py (NEU, 80k M5, 2 Halbjahre, Echtkosten, Exit-Kern aus core/exit_model.py, 5 Varianten auf IDENTISCHEN Entries): nur S/R-Close H1 +59 · H2 -14 (535/284 S/R-Closes) nur Flip-Close H1 -1 · H2 -43 BEIDE H1 +56 · H2 -50 (507/252) BEIDE, Flip nachrangig H1 +62 · H2 -49 (516/260) KOLLISION BELEGT: der Flip nahm dem S/R-Close 28 bzw. 32 Trades weg (5-11 %) - genau die, fuer die dieser validiert ist. Mit Nachrang ist es in BEIDEN Haelften besser als ohne. Umsetzung: liegt ein S/R-ZIEL <=0,6xATR entfernt (dieselbe Schwelle wie der Live-Hinweis), haelt der Flip sich zurueck. [trading] auto_flip_close_subordinate (Default true). Mit 4 Szenarien getestet: in Reichweite, ausser Reichweite, kein Level, Nachrang aus. EHRLICH DAZU: auch mit Nachrang kostet der Flip in H2 rund 35 R gegenueber "nur S/R-Close" (-49 vs -14). Der Nachrang begrenzt den Schaden, er dreht ihn nicht um. METHODIK-FEHLER BEIM BAU, KORRIGIERT: der erste Lauf liess die sequentielle Sim nach dem Exit bei xb+1 weiterlaufen - dadurch hatte JEDE Variante eine ANDERE Trade-Folge und die Zahlen waren nicht vergleichbar. Jetzt feste, geteilte Entry-Liste wie in backtest_pbreak_rvalue.py. (Die zunaechst gemeldeten Deltas -195/-144 stammten aus diesem konfundierten Lauf.) Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 24 ++++++ backtest_exit_combo.py | 177 +++++++++++++++++++++++++++++++++++++++++ core/engine.py | 20 +++++ 3 files changed, 221 insertions(+) create mode 100644 backtest_exit_combo.py diff --git a/CLAUDE.md b/CLAUDE.md index 96f9252..89aa950 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -596,6 +596,30 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: bindend gewesen. Für die Neumessung bekam `exit_model.simulate()` einen **`stop_when`-Hook** (+ `ret_bar`), damit die Phasen-Mechanik NICHT wieder kopiert werden musste; Rückwärtskompatibilität verifiziert (ohne Hook bitgenau identisch). + **NACHRANG gegenüber dem S/R-Close (gemessen `backtest_exit_combo.py`, + 2026-07-31, `auto_flip_close_subordinate=true`):** Beide Exits konkurrieren um + dieselben Trades — wer zuerst feuert, gewinnt. Die S/R-Freigabe lief OHNE + Flip-Close, die Baseline existierte also nach dem Einbau nicht mehr. Gemessen auf + **IDENTISCHEN Entries** (5 Varianten, 2 Halbjahre, Echtkosten, Exit-Kern): + | Variante | Δ H1 | Δ H2 | S/R-Closes H1/H2 | + |---|---|---|---| + | nur S/R-Close | +59 | −14 | 535 / 284 | + | nur Flip-Close | −1 | −43 | — | + | BEIDE | +56 | −50 | **507 / 252** | + | **BEIDE, Flip nachrangig** | **+62** | **−49** | **516 / 260** | + **Kollision belegt:** der Flip nahm dem S/R-Close 28 bzw. 32 Trades weg (5–11 %) — + genau die, für die dieser validiert ist. Mit Nachrang ist es in BEIDEN Hälften + besser als ohne. Umsetzung: liegt ein S/R-ZIEL ≤0,6×ATR entfernt (dieselbe + Schwelle wie der Live-Hinweis), hält der Flip sich zurück. Mit 4 Szenarien + getestet (in/außer Reichweite · kein Level · Nachrang aus). + ⚠ **Ehrlich dazu:** auch MIT Nachrang kostet der Flip in H2 rund 35 R gegenüber + „nur S/R-Close" (−49 vs −14). Der Nachrang begrenzt den Schaden, er dreht ihn + nicht um. Abschalten des Features bleibt `auto_flip_close=false`. + ⚠ **Methodik-Fehler beim Bau, korrigiert:** der erste Lauf ließ die sequentielle + Sim nach dem Exit bei `xb+1` weiterlaufen — dadurch hatte JEDE Variante eine + ANDERE Trade-Folge (sobald eine früher aussteigt, verschiebt sich alles + Nachfolgende) und die Zahlen waren nicht vergleichbar. Jetzt feste, geteilte + Entry-Liste wie in `backtest_pbreak_rvalue.py`. Umsetzung `engine._check_flip_close` im `_pos_loop`: nur im PLUS, 1× je Ticket, Startup-Schonfrist beachtet, Log 🔄 + Telegram, Snapshot `flip_close`/ `flip_close_min_r`/`flip_close_count`. Mit **10 synthetischen Szenarien** getestet diff --git a/backtest_exit_combo.py b/backtest_exit_combo.py new file mode 100644 index 0000000..e39b0d4 --- /dev/null +++ b/backtest_exit_combo.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""S/R-Close und Flip-Close im DIREKTEN Wettbewerb (User-Frage 2026-07-31). + +WARUM: Seit dem Einbau des Auto-Flip-Close konkurrieren zwei Exits um dieselben +Trades — **wer zuerst feuert, gewinnt.** Beide sind einzeln gemessen, aber gegen +UNTERSCHIEDLICHE Baselines: + S/R-Close (neues Modell, Schwelle 0,35): H1 +357 · H2 +1418 gegen reines Trailing + Flip-Close (0,5×ATR): H1 −92 · H2 −160 gegen reines Trailing +Die S/R-Messung lief OHNE Flip-Close. Die Baseline, gegen die der validierte Exit +freigegeben wurde, existiert seit dem Einbau nicht mehr. + +BEFÜRCHTUNG: Ein Trade läuft ins Ziel-Level, wäre dort (P(break) niedrig) gut +geschlossen worden — aber die nachlaufende EMA dreht 0,5×ATR vorher und der +Flip-Close nimmt ihn früher und schlechter raus. + +GEMESSEN WERDEN 5 VARIANTEN auf IDENTISCHEN Entries (sequentiell, 1 Position, +Echtkosten, Exit-Kern aus `core/exit_model.py`): + 1 nur Trailing (Baseline) + 2 nur S/R-Close (der validierte Stand vor heute) + 3 nur Flip-Close (das neue Feature allein) + 4 BEIDE (was seit heute live ist) + 5 BEIDE, Flip NACHRANGIG (Flip nur, wenn KEIN S/R-Ziel in Reichweite) +Variante 5 ist der Vorschlag (b): den validierten Exit vor dem unvalidierten schützen. + +Alles über den `stop_when`-Hook des gemeinsamen Exit-Kerns — die Phasen-Mechanik +wird NICHT kopiert (diese Kopiererei hat im Projekt vier divergierende Exit-Modelle +erzeugt). +""" +import sys +from bisect import bisect_left + +import MetaTrader5 as mt5 + +from core.analysis import calc_trend_angle +from core.exit_model import LIVE, simulate +from core.engine import _p_break +from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD, + _REVERSAL_STRETCH, _STRETCH_MAX) +from backtest_pbreak_retrain import (_ema_series, _atr_series, build_levels, + _cluster, pick_level, _ZONE, _M30) + +_ATRMIN = 0.12 +_PBRK = 0.35 # live `sr_close_pbreak` +_FLIP_MIN_R = 0.5 # live `auto_flip_close_min_r` +_NEAR_ATR = 0.6 # „S/R-Ziel in Reichweite" (wie der Live-Hinweis) + + +def signal_series(H, L, C, EF, ES, AT): + TH = _REVERSAL_STRETCH + sig = [0] * len(C) + for i in range(_N_BARS, len(C)): + 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 + if stretch <= -TH and ad >= _ANGLE_DEAD: sig[i] = 1 + elif stretch >= TH and ad <= -_ANGLE_DEAD: sig[i] = -1 + elif abs(stretch) < _STRETCH_MAX: sig[i] = 1 if ef > es else -1 if ef < es else 0 + return sig + + +def entries(sig, AT, lo, hi, cool=12): + """Einstiegs-Liste EINMAL berechnen — alle Varianten teilen sie sich. + + ⚠ WICHTIG (Korrektur 2026-07-31): ein sequentieller Lauf, der nach dem Exit bei + `xb+1` weitermacht, erzeugt für JEDE Variante eine ANDERE Trade-Folge — sobald + eine früher aussteigt, verschiebt sich alles Nachfolgende. Die Varianten wären + dann nicht vergleichbar (Konfundierung). Deshalb feste Entries mit Cooldown, + wie in `backtest_pbreak_rvalue.py`. + """ + out = [] + i = lo + while i < hi: + if sig[i] != 0: + out.append((i, sig[i], max(AT[i] or 0, _ATRMIN))) + i += cool + else: + i += 1 + return out + + +def run(ent, sig, H, L, C, AT, EF, ES, SP, LV, sr=False, flip=False, + flip_subordinate=False, stats=None): + """`stats` sammelt, WER geschlossen hat (für die Kollisions-Analyse).""" + Rs = [] + for (i, d, atr) in ent: + entry = C[i] + cost = (SP[i] if SP[i] > 0 else 0.0225) / atr + state = {"lvl": None, "who": None} + + def hook(j, prof_atr, _d=d, _e=entry, _st=state): + aj = max(AT[j] or 0, _ATRMIN) + lv = LV[min(j // _M30, len(LV) - 1)] + level = pick_level(_cluster(lv, aj), C[j], _d, aj, _st["lvl"]) + _st["lvl"] = level + near = level is not None and abs(C[j] - level) <= _NEAR_ATR * aj + # ── S/R-Close: AM Level (±0,15×ATR) und P(break) unter der Schwelle + if sr and level is not None and abs(C[j] - level) <= _ZONE * aj: + m6 = (C[j] - C[max(0, j - 6)]) * _d / aj + m3 = (C[j] - C[max(0, j - 3)]) * _d / aj + wt = 1.0 if (EF[j] - ES[j]) * _d > 0 else 0.0 + dist = abs(level - _e) / aj + if _p_break(m6, m3, wt, dist) < _PBRK and prof_atr > 0: + _st["who"] = "sr"; return True + # ── Flip-Close: Signal dreht, mind. min_r im Plus + if flip and sig[j] == -_d and prof_atr > _FLIP_MIN_R: + if flip_subordinate and near: + return False # S/R-Ziel in Reichweite → Vorrang für den S/R + _st["who"] = "flip"; return True + return False + + R = simulate(entry, d, atr, H, L, C, i + 1, LIVE, + stop_when=(hook if (sr or flip) else None)) + Rs.append(R - cost) + if stats is not None: + stats[state["who"] or "exit"] = stats.get(state["who"] or "exit", 0) + 1 + return Rs + + +def st(Rs): + if not Rs: + return " —" + n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) + g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0) + return (f"n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={(g/l if l > 0 else 9.99):>4.2f} " + f"ΣR={s:>+6.0f}") + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + mt5.initialize(); sym = None + for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): + if mt5.symbol_info(c): + sym = c; break + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) + si = mt5.symbol_info(sym); point = si.point + 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]; SP = [float(b["spread"]) * point 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) + sig = signal_series(H, L, C, EF, ES, AT) + mid = len(C) // 2 + halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C) - LIVE.max_hold - 1)] + + print("=" * 98) + print(f" S/R-CLOSE vs FLIP-CLOSE im Wettbewerb — {sym} M5 ({len(C)} Bars), Echtkosten") + print(f" Exit-Kern core/exit_model.py · Trail {LIVE.mult} · P(break)<{_PBRK} · " + f"Flip ab {_FLIP_MIN_R}×ATR") + print("=" * 98) + ENT = {lbl: entries(sig, AT, a, b) for lbl, a, b in halves} + for lbl, _, _ in halves: + print(f" {lbl}: {len(ENT[lbl])} Entries (fuer ALLE Varianten identisch)") + + variants = [("1 nur Trailing (Baseline)", dict()), + ("2 nur S/R-Close", dict(sr=True)), + ("3 nur Flip-Close", dict(flip=True)), + ("4 BEIDE (seit heute live)", dict(sr=True, flip=True)), + ("5 BEIDE, Flip NACHRANGIG", dict(sr=True, flip=True, + flip_subordinate=True))] + base = {} + for name, kw in variants: + print(f"\n{name}") + for lbl, a, b in halves: + stats = {} + Rs = run(ENT[lbl], sig, H, L, C, AT, EF, ES, SP, LV, stats=stats, **kw) + s = sum(Rs) + if not kw: + base[lbl] = s + who = " · ".join(f"{k}={v}" for k, v in sorted(stats.items())) + print(f" {lbl} {st(Rs)} Δ Basis {s - base.get(lbl, 0):+.0f} [{who}]") + + +if __name__ == "__main__": + main() diff --git a/core/engine.py b/core/engine.py index d4fccee..1dd4471 100644 --- a/core/engine.py +++ b/core/engine.py @@ -327,6 +327,9 @@ class TradingEngine: self._flip_close_min_r = float(_tf_cfg.get("auto_flip_close_min_r", "0.5")) except (TypeError, ValueError): self._flip_close_min_r = 0.5 + # Nachrang gegenüber dem S/R-Close (gemessen besser, s. `_check_flip_close`) + self._flip_close_subordinate = \ + _tf_cfg.get("auto_flip_close_subordinate", "true").lower() == "true" self._flip_close_done_ticket = None self._flip_close_count = 0 self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die @@ -1488,6 +1491,23 @@ class TradingEngine: return else: return # ohne ATR keine Schwelle → nicht schließen + # ── NACHRANG gegenüber dem S/R-Close (gemessen `backtest_exit_combo.py`, + # 2026-07-31): liegt ein S/R-ZIEL in Reichweite (≤0,6×ATR, dieselbe + # Schwelle wie der Live-Hinweis), hat der **validierte** Exit Vorrang und + # der Flip hält sich zurück. Grund: der Flip nahm dem S/R-Close messbar + # Trades weg (535→507 in H1, 284→252 in H2 = 5–11 %) — und zwar genau + # die, für die dieser validiert ist. Mit Nachrang ist es in BEIDEN + # Hälften besser als ohne (H1 +62 statt +56, H2 −49 statt −50). + # `auto_flip_close_subordinate=false` schaltet den Nachrang ab. + if self._flip_close_subordinate: + try: + dl = self._draw_levels() + tgt = (dl.get("res") or [None])[0] if is_long \ + else (dl.get("sup") or [None])[0] + if tgt is not None and abs(tgt - bid) <= 0.6 * max(atr, 0.06): + return # S/R-Ziel in Reichweite → Vorrang dort + except Exception: + pass self._flip_close_done_ticket = ticket log.info(f"🔄 AUTO-FLIP-CLOSE: Empfehlung dreht auf {sig} — " f"{'LONG' if is_long else 'SHORT'} bei P&L {pnl:+.2f} € "