#!/usr/bin/env python3 """Reversal-Trigger: IST-Verhalten gegen vorzeichenkorrigierte Variante (2026-08-04). Anlass: Beim Nachgehen der User-Frage „warum SHORT im starken M5-Aufwärtstrend?" kam heraus, dass die Reversal-Bedingung in `wave_rec._build` GEGEN ihre eigene Dokumentation läuft. `calc_trend_angle` liefert laut Docstring 0° = starker AUFWÄRTStrend 90° = seitwärts 180° = starker ABWÄRTStrend → `ad = angle − 90`: ad < 0 = STEIGEND, ad > 0 = FALLEND. Der Code macht: stretch >= +3.0 und ad <= -2 → SHORT (Kommentar: „Winkel ab") stretch <= -3.0 und ad >= +2 → LONG (Kommentar: „Winkel auf") Mit obiger Konvention heißt das: SHORT bei überkauft UND WEITER STEIGENDEM Winkel; LONG bei überverkauft UND WEITER FALLENDEM. CLAUDE.md beschreibt aber „Einstieg IN Winkel-Richtung" — also genau umgekehrt. ⚠ Warum das nicht einfach umgedreht wurde: sämtliche Messungen (`backtest_bounce.py`, der Legacy-Recheck, `backtest_bounce_reliability.py`) rufen dasselbe `_build` — sie beschreiben also das IST-Verhalten. Ein Vorzeichenwechsel machte aus einem gemessenen Setup ein ungemessenes. Deshalb hier beide Varianten gegeneinander, plus eine Kontrolle ohne Winkelbedingung (trägt der Winkel überhaupt etwas bei?). Aufbau: M5 (Basis-TF), kanonischer Live-Exit (`core/exit_model.LIVE`), Echtkosten (Bar-Spread/ATR), sequentielle 1-Positions-Sim, 2 Halbjahre. Nur der TRIGGER wird verglichen — ohne Konfidenz-Gate/Raum-Gate, sonst mischen sich Effekte. Maßstab: beidhälftig ØR>0 und PF>1. Aufruf: python backtest_reversal_angle.py [bars] """ 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 _ANGLE_DEAD, _ANGLE_LR, _EMA_SLOW, _REVERSAL_STRETCH _ATRMIN = 0.06 _COOL = 3 def _ema(vals, p): k = 2.0 / (p + 1) out = [vals[0]] for v in vals[1:]: out.append(out[-1] + k * (v - out[-1])) return out 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]))) return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1]))) if i else None for i in range(len(C))] def st(Rs): if not Rs: return None n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0) return dict(n=n, wr=100 * w / n, oR=s / n, pf=(up / dn if dn > 0 else 9.99), sum=s) def line(lbl, s): if not s: return f" {lbl:<22} — (keine Trades)" return (f" {lbl:<22} n={s['n']:>5} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} " f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+7.0f}") def run(H, L, C, A, SP, E50, AD, lo, hi, variant): def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr Rs = [] i = max(lo, _EMA_SLOW + _ANGLE_LR + 5) while i < hi: atr = A[i] if not atr or atr < _ATRMIN: i += 1; continue stretch = (C[i] - E50[i]) / atr ad = AD[i] d = 0 if variant == "IST": # wie der Live-Code heute if stretch <= -_REVERSAL_STRETCH and ad >= _ANGLE_DEAD: d = 1 elif stretch >= _REVERSAL_STRETCH and ad <= -_ANGLE_DEAD: d = -1 elif variant == "KORR": # vorzeichenkorrigiert = dokumentierte Absicht if stretch <= -_REVERSAL_STRETCH and ad <= -_ANGLE_DEAD: d = 1 # überverkauft + Winkel STEIGT = gedreht elif stretch >= _REVERSAL_STRETCH and ad >= _ANGLE_DEAD: d = -1 # überkauft + Winkel FÄLLT = gedreht else: # KONTROLLE: nur Überdehnung, kein Winkel if stretch <= -_REVERSAL_STRETCH: d = 1 elif stretch >= _REVERSAL_STRETCH: d = -1 if d == 0: i += 1; continue a = max(atr, _ATRMIN) r, xj = simulate(C[i], d, a, H, L, C, i + 1, LIVE, timestop=True, use_tp=True, ret_bar=True) Rs.append(r - cost(i, a)) i = xj + _COOL return Rs def main(): n = 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) bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) point = mt5.symbol_info(sym).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] A = _atr_series(H, L, C) E50 = _ema(C, _EMA_SLOW) # Winkel EINMAL je Bar (variantenunabhängig), exakt wie im Live-Code aufgerufen AD = [0.0] * len(C) for i in range(_ANGLE_LR + 2, len(C)): AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR) - 90.0 N = len(C); mid = N // 2 print("=" * 88) print(f" Reversal-Trigger: IST vs. vorzeichenkorrigiert — {sym} M5 ({N} Bars)") print(f" Kanonischer Exit (SL {LIVE.sl_atr}× · Trail {LIVE.mult}× · BE {LIVE.be}×" f" · Time-Stop), Echtkosten, 2 Halbjahre") print(f" Trigger: |stretch| ≥ {_REVERSAL_STRETCH}×ATR, Winkel-Totband ±{_ANGLE_DEAD}°") print("=" * 88) beschreibung = { "IST": "IST (Live heute): überkauft + Winkel STEIGT → SHORT", "KORR": "KORRIGIERT (Doku): überkauft + Winkel FÄLLT → SHORT", "KTRL": "KONTROLLE: nur Überdehnung, Winkel ignoriert", } for v in ("IST", "KORR", "KTRL"): s1 = st(run(H, L, C, A, SP, E50, AD, 0, mid, v)) s2 = st(run(H, L, C, A, SP, E50, AD, mid, N - LIVE.max_hold - 1, v)) print(f"\n {beschreibung[v]}") print(line("H1 (alt)", s1)) print(line("H2 (neu)", s2)) ok = (s1 and s2 and s1["oR"] > 0 and s2["oR"] > 0 and s1["pf"] > 1 and s2["pf"] > 1) print(f" → {'ROBUST (beide Hälften positiv)' if ok else 'fällt durch'}") print() if __name__ == "__main__": main()