#!/usr/bin/env python3 """Auto-Flip-Close NEU gemessen — mit dem ECHTEN Exit (User-Wunsch 2026-07-31). FRAGE: „Schließe den Trade automatisch beim Close-Signal" — also: dreht die Empfehlung gegen die offene Position, sofort raus (heute nur Alarm + Ton). ⚠ DAS WURDE SCHON EINMAL VERWORFEN (`backtest_flipclose.py`, 2026-07-16): keine Variante hielt in beiden Hälften, H2 kostete 17–32 % des Ertrags („Gewinner-Kappen", weil die nachlaufende EMA oft mitten im Pullback dreht). WARUM TROTZDEM NEU: Beim Stufe-3-Refactor (2026-07-31) kam heraus, dass das alte Skript ein **viertes, eigenes Exit-Modell** benutzt, das dem Live-System in mindestens fünf Punkten widerspricht: Trail hart 1,5 (live seit heute **1,0**) KEINE Lock-Phase (live: ab 3,5×ATR enger) KEIN Time-Stop (live: 120 min in Phase Init) KEIN Initial-TP (live: 3,5×ATR) _MAXH 288 (live/kanonisch 200) + Phasen-Entscheidung über den BAR-CLOSE statt über das High-Water. Ein Urteil auf dieser Grundlage ist für den Live-Betrieb nicht bindend. HIER: derselbe sequentielle 1-Positions-Aufbau, aber der Exit kommt aus `core/exit_model.simulate()` — inklusive des neuen `stop_when`-Hooks für den Flip, damit die Phasen-Mechanik NICHT noch einmal kopiert wird (genau diese Kopiererei hat die vier divergierenden Modelle erzeugt). Echtkosten, 2 Halbjahre. VARIANTEN: Flip erst ab minR Gewinn (0 / 0,3 / 0,5×ATR) sowie „auch im Minus". Urteil „bauen" nur, wenn eine Variante in BEIDEN Hälften ≥ Basis liegt. """ import sys import MetaTrader5 as mt5 from core.analysis import calc_trend_angle from core.exit_model import LIVE, simulate from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD, _REVERSAL_STRETCH, _STRETCH_MAX) _ATRMIN = 0.12 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 signal_series(H, L, C, EF, ES, AT): """Richtung je Bar (+1/−1/0) — dieselbe Regel wie im Live-Signal.""" 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 run(sig, H, L, C, AT, SP, lo, hi, flip=False, min_r=0.0, flip_neg=False): """Sequentiell: eine Position, Entry bei Signal, Exit aus dem GEMEINSAMEN Kern.""" Rs = [] i = lo while i < hi: d = sig[i] if d == 0: i += 1; continue atr = max(AT[i] or 0, _ATRMIN) cost = (SP[i] if SP[i] > 0 else 0.0225) / atr hook = None if flip: def hook(j, prof_atr, _d=d): if sig[j] != -_d: return False return flip_neg or prof_atr > min_r R, xb = simulate(C[i], d, atr, H, L, C, i + 1, LIVE, stop_when=hook, ret_bar=True) Rs.append(R - cost) i = xb + 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} Worst={min(Rs):+.2f}") 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) 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("=" * 96) print(f" AUTO-FLIP-CLOSE — {sym} M5 ({len(C)} Bars), Echtkosten") print(f" Exit aus core/exit_model.py: SL {LIVE.sl_atr} · Trail {LIVE.mult} · " f"BE {LIVE.be} · Lock {LIVE.lock_start} · Time-Stop {LIVE.timestop_bars}") print(" (das alte Urteil von 2026-07-16 fusste auf einem abweichenden Exit)") print("=" * 96) variants = [("Basis — kein Flip-Close", dict(flip=False)), ("Flip ab 0,0×ATR Gewinn", dict(flip=True, min_r=0.0)), ("Flip ab 0,3×ATR Gewinn", dict(flip=True, min_r=0.3)), ("Flip ab 0,5×ATR Gewinn", dict(flip=True, min_r=0.5)), ("Flip AUCH im Minus", dict(flip=True, flip_neg=True))] base = {} for name, kw in variants: print(f"\n{name}") for lbl, a, b in halves: Rs = run(sig, H, L, C, AT, SP, a, b, **kw) s = sum(Rs) if not kw.get("flip"): base[lbl] = s delta = f" Δ vs Basis {s - base.get(lbl, 0):+.0f}" if kw.get("flip") else "" print(f" {lbl} {st(Rs)}{delta}") if __name__ == "__main__": main()