Flip-Close bekommt Nachrang gegenueber dem S/R-Close (gemessen)

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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-31 18:15:09 +02:00
co-authored by Claude Opus 5
parent fea91fddb3
commit 3314b6e9b1
3 changed files with 221 additions and 0 deletions
+24
View File
@@ -596,6 +596,30 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
bindend gewesen. Für die Neumessung bekam `exit_model.simulate()` einen bindend gewesen. Für die Neumessung bekam `exit_model.simulate()` einen
**`stop_when`-Hook** (+ `ret_bar`), damit die Phasen-Mechanik NICHT wieder kopiert **`stop_when`-Hook** (+ `ret_bar`), damit die Phasen-Mechanik NICHT wieder kopiert
werden musste; Rückwärtskompatibilität verifiziert (ohne Hook bitgenau identisch). 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 (511 %) —
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, Umsetzung `engine._check_flip_close` im `_pos_loop`: nur im PLUS, 1× je Ticket,
Startup-Schonfrist beachtet, Log 🔄 + Telegram, Snapshot `flip_close`/ Startup-Schonfrist beachtet, Log 🔄 + Telegram, Snapshot `flip_close`/
`flip_close_min_r`/`flip_close_count`. Mit **10 synthetischen Szenarien** getestet `flip_close_min_r`/`flip_close_count`. Mit **10 synthetischen Szenarien** getestet
+177
View File
@@ -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()
+20
View File
@@ -327,6 +327,9 @@ class TradingEngine:
self._flip_close_min_r = float(_tf_cfg.get("auto_flip_close_min_r", "0.5")) self._flip_close_min_r = float(_tf_cfg.get("auto_flip_close_min_r", "0.5"))
except (TypeError, ValueError): except (TypeError, ValueError):
self._flip_close_min_r = 0.5 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_done_ticket = None
self._flip_close_count = 0 self._flip_close_count = 0
self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die
@@ -1488,6 +1491,23 @@ class TradingEngine:
return return
else: else:
return # ohne ATR keine Schwelle → nicht schließen 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 = 511 %) — 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 self._flip_close_done_ticket = ticket
log.info(f"🔄 AUTO-FLIP-CLOSE: Empfehlung dreht auf {sig}" log.info(f"🔄 AUTO-FLIP-CLOSE: Empfehlung dreht auf {sig}"
f"{'LONG' if is_long else 'SHORT'} bei P&L {pnl:+.2f}" f"{'LONG' if is_long else 'SHORT'} bei P&L {pnl:+.2f}"