From d05290531910af95259ed5bf15465949dd019400 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Tue, 4 Aug 2026 11:15:21 +0200 Subject: [PATCH] Reversal-Winkelbedingung gemessen: alle drei Varianten fallen durch Anlass: User-Frage "warum SHORT im starken M5-Aufwaertstrend?". Antwort war ein WAVE_REV_SHORT (3,25xATR ueberdehnt). Beim Nachrechnen kam heraus, dass die Winkelbedingung gegen ihre eigene Dokumentation laeuft: calc_trend_angle liefert 0 Grad = starker AUFWAERTStrend, 180 = ABWAERTS. ad = angle-90 heisst also: ad<0 = STEIGEND. Der Code triggert stretch>=+3.0 and ad<=-2 -> SHORT, kommentiert als "Winkel ab" - feuert also bei ueberkauft UND WEITER STEIGENDEM Winkel, waehrend CLAUDE.md "Einstieg IN Winkel-Richtung" beschreibt. Dasselbe Vorzeichen im Konfidenz-Block: ein SHORT bekommt "Winkel bestaetigt" (+5), waehrend der Kurs steigt. Mit synthetischen Reihen direkt nachgestellt. Erklaert zwei dokumentierte Befunde: ~50 % Richtungstreffer der Bounce-Anzeige und "blutet in starken Trends". Statt das Vorzeichen blind zu drehen (alle Messungen beruhen auf dem IST-Verhalten): backtest_reversal_angle.py, 80k M5, kanonischer Exit, Echtkosten, 2 Halbjahre, drei Varianten. IST -0,122 / -0,031 PF 0,79/0,94 n=1100/1615 KORRIGIERT -0,173 / -0,061 PF 0,68/0,89 n= 106/ 163 KONTROLLE -0,099 / -0,023 PF 0,82/0,96 n=1153/1693 (a) Die Vorzeichenkorrektur ist SCHLECHTER - gut, dass sie nicht blind eingebaut wurde. Sie feuert zudem kaum, weil bei 3xATR Ueberdehnung die 14-Bar- Regression fast immer noch steigt. (b) Die Kontrolle OHNE Winkel ist die beste der drei - die Winkelbedingung entfernt Trades, die im Schnitt besser waren als die behaltenen. (c) Der Trigger ist in jeder Variante beidhaelftig negativ. Gemessen wurde der ROHE Trigger ohne Konfidenz-/Raum-Gate; der Legacy-Recheck der gegateten Variante kam auf ~Breakeven (PF 0,93/1,06), dieselbe Richtung. Keine Messung stuetzt die dokumentierten "+0,185 / PF 1,35" mehr. Noch KEINE Strategieaenderung - das Abschalten ist eine User-Entscheidung. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 33 ++++++++ backtest_reversal_angle.py | 156 +++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 backtest_reversal_angle.py diff --git a/CLAUDE.md b/CLAUDE.md index 7f981f3..97aaf18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -441,6 +441,39 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: **und** M30-Filter bewusst auf. **Schwelle 3,5→3,0 gesenkt** (User wollte mehr Bounces; `backtest_bounce.py`): Ø-R +0,185, PF 1,35, Treffer 70 %, Worst −2×ATR (SL-gedeckelt), ~1,5× mehr Bounces als 3,5. + ⚠⚠ **WINKEL-BEDINGUNG LÄUFT GEGEN IHRE EIGENE DOKUMENTATION (gefunden 2026-08-04, + User-Frage „warum SHORT im starken M5-Aufwärtstrend?").** `calc_trend_angle` liefert + laut Docstring **0° = starker AUFWÄRTStrend, 90° = flach, 180° = ABWÄRTStrend** — + also invertiert. Damit heißt `ad = angle−90`: **ad < 0 = STEIGEND**, ad > 0 = fallend. + Der Code triggert aber `stretch >= +3.0 and ad <= -2` → SHORT, kommentiert als + „überkauft + Winkel ab". Tatsächlich feuert er bei überkauft **UND WEITER STEIGENDEM** + Winkel — also GEGEN den Winkel, während CLAUDE.md „Einstieg IN Winkel-Richtung" + beschreibt. Dasselbe Vorzeichen steckt im Konfidenz-Block (Zeile ~790): ein SHORT + bekommt „**Winkel bestätigt**" (+5), während der Kurs steigt. Mit synthetischen + Reihen direkt nachgestellt: überkauft + steigend → `WAVE_REV_SHORT`, Grund „Winkel + gedreht" + „Winkel bestätigt". **Erklärt zwei dokumentierte Befunde:** die ~50 % + Richtungstreffer der Bounce-Anzeige (`backtest_bounce_reliability.py`) und „blutet in + starken Trends". + ⚠⚠ **GEMESSEN (`backtest_reversal_angle.py`, 80k M5, kanonischer Exit, Echtkosten, + 2 Halbjahre) — ALLE drei Varianten fallen durch:** + | Variante | H1 ØR | H2 ØR | PF | n | + |---|---|---|---|---| + | **IST** (Live) überkauft + Winkel steigt → SHORT | −0,122 | −0,031 | 0,79/0,94 | 1100/1615 | + | **KORRIGIERT** (Doku-Absicht) Winkel fällt → SHORT | **−0,173** | **−0,061** | 0,68/0,89 | 106/163 | + | **KONTROLLE** ohne Winkelbedingung | **−0,099** | **−0,023** | 0,82/0,96 | 1153/1693 | + Drei Schlüsse: (a) **Die Vorzeichenkorrektur ist SCHLECHTER** — gut, dass sie nicht + blind eingebaut wurde; sie feuert zudem kaum (106 statt 1100 Trades, weil bei 3×ATR + Überdehnung die 14-Bar-Regression fast immer noch steigt). (b) **Die Kontrolle ohne + Winkel ist die BESTE der drei** — die Winkelbedingung entfernt Trades, die im Schnitt + besser waren als die behaltenen; sie trägt mit KEINEM Vorzeichen etwas bei. + (c) **Der Trigger ist in jeder Variante beidhälftig negativ.** + ⚠ Gemessen wurde der ROHE Trigger (ohne Konfidenz-/Raum-Gate); der Legacy-Recheck der + gegateten Variante kam auf ~Breakeven (PF 0,93/1,06) — dieselbe Richtung. **Keine + Messung stützt die dokumentierten „+0,185 / PF 1,35" mehr.** + ⚠ **Offene Entscheidung:** Das Setup hebelt zwei GEMESSEN POSITIVE Schutzmechanismen + aus (Anti-Überdehnung UND den M30-Gegen-Trend-Filter, Edge ×2). Ein Setup ohne eigenen + Edge, das validierte Filter umgeht, ist der schlechteste Fall — Abschalten liegt nahe, + ist aber eine Strategie-Entscheidung des Users. ⚠⚠ **DIESE ZAHLEN GELTEN NICHT MEHR** (Legacy-Recheck 2026-07-31, hier eingetragen am 2026-08-02): `backtest_bounce.py` nutzt den **vereinfachten Exit OHNE Kosten** (`LEGACY_SIMPLE`: kein Breakeven, kein Lock, kein Time-Stop). Mit dem **echten** diff --git a/backtest_reversal_angle.py b/backtest_reversal_angle.py new file mode 100644 index 0000000..b4d5f25 --- /dev/null +++ b/backtest_reversal_angle.py @@ -0,0 +1,156 @@ +#!/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()