From 5f94917014dba58be8d5a0f050cdc186744c5911 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Mon, 3 Aug 2026 16:37:52 +0200 Subject: [PATCH] =?UTF-8?q?Kanal-Pullback=20ueber=20alle=20Zeitebenen=20ge?= =?UTF-8?q?messen=20=E2=80=94=2024/24=20negativ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anlass: User-Beobachtung im M5-Chart "der Kurs folgt dem Marktstrukturkanal und bricht die untere Linie nicht — koennen wir das nutzen?". Die vorhandene Messung (backtest_structure.py, 20.07.) lief NUR auf M30; M5 war ungetestet. Neues Skript backtest_structure_tf.py — bewusst mit dem KANONISCHEN Live-Exit (exit_model.LIVE: Trail 1,0 + Time-Stop + TP) statt der Exit-Kopie der alten Datei (Trail 1,5, kein Time-Stop). Die alte Datei bleibt unveraendert, weil die Migrations-Regel eine Umstellung nur bei identischen Zahlen erlaubt. Getestet: M5/M15/M30 x zwei Lesarten des M5-Kanals (60 Bars = 5 h "was man im Chart sieht" und 360 Bars = 30 h zeitgleich zu M30) x drei Regel-Varianten, 80k Bars je Zeitebene, 2 Haelften, Echtkosten. Ergebnis: 24 Zellen, ALLE negativ, PF 0,66-0,99. - Trefferquote 37-43 % ueber 1200-2400 Beruehrungen je Haelfte: der Kanalrand haelt in WENIGER als der Haelfte der Faelle. Die im Chart sichtbaren "Halter" sind Selektion. - M5 ist die SCHLECHTESTE Zeitebene (-0,201 vs -0,103 auf M30). - Keine Parameterfrage: 60-Bar- und 360-Bar-Kanal liefern identische Zahlen. Legacy-Recheck-Bonus: die M30-Zeile reproduziert den Befund vom 20.07. (-0,103/-0,019 gegen damals -0,05...-0,11) — der alte Schluss war NICHT durch das abweichende Exit-Modell verzerrt. Mechanismus: hinter einer Regressionslinie liegt nichts — keine Order, kein Pivot. Darin liegt der Unterschied zu P(break) auf einem echten S/R-Level. 19. verworfener Signal-Eingriff. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 30 ++++++++ backtest_structure_tf.py | 162 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 backtest_structure_tf.py diff --git a/CLAUDE.md b/CLAUDE.md index a077c0c..933b9b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1747,6 +1747,36 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Continuation = breakeven-negativ (−0,011/−0,005, Momentum-Klasse). Damit ist auch die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie TF-Ampel/Squeeze), steuert nichts. + - **NACHTEST über ALLE Zeitebenen = ERNEUT VERWORFEN (`backtest_structure_tf.py`, + 2026-08-03, User-Beobachtung im M5-Chart: „der Kurs folgt dem Kanal und bricht die + untere Linie nicht — können wir das nutzen?"):** Die alte Messung lief NUR auf M30, + M5 war streng genommen ungetestet. Jetzt mit dem **kanonischen Live-Exit** + (`exit_model.LIVE`: Trail 1,0 + Time-Stop + TP — die alte Datei nutzt eine Exit-KOPIE + mit Trail 1,5 OHNE Time-Stop) über M5/M15/M30, zwei Lesarten des M5-Kanals und drei + Regel-Varianten, 80k Bars je Zeitebene, 2 Hälften, Echtkosten: + | Zeitebene / Kanal | X=0,20 | X=0,30 | +Swing | + |---|---|---|---| + | **M5, 60 Bars** (=5 h, „was man im Chart sieht") | **−0,201/−0,017** | −0,170/−0,054 | −0,161/−0,082 | + | M5, 360 Bars (=30 h, zeitgleich zu M30) | −0,206/−0,017 | −0,179/−0,054 | −0,159/−0,082 | + | M15, 120 Bars | −0,032/−0,003 | −0,068/−0,034 | −0,043/−0,033 | + | M30, 60 Bars (die ALTE Messung) | −0,103/−0,019 | −0,094/−0,013 | −0,090/−0,024 | + **24 Zellen, ALLE negativ**, PF 0,66–0,99. Drei Befunde: + (a) **Trefferquote 37–43 %** über 1.200–2.400 Berührungen je Hälfte — der Kanalrand + hält in WENIGER als der Hälfte der Fälle. Die im Chart sichtbaren „Halter" sind + Selektion: die Durchbrüche sehen hinterher nicht mehr nach Kanal aus. + (b) **M5 ist die SCHLECHTESTE Zeitebene** (−0,201 vs. −0,103 auf M30) — feiner + auflösen erzeugt mehr Berührungen, und jede kostet Spread. + (c) **Keine Parameterfrage:** 60-Bar- und 360-Bar-Kanal liefern praktisch IDENTISCHE + Zahlen. Zwei so verschiedene Fenster mit gleichem Ergebnis → es liegt am Mechanismus. + ✅ **Legacy-Recheck-Bonus:** die M30-Zeile **reproduziert** den Befund vom 20.07. + (−0,103/−0,019 gegen damals −0,05…−0,11) — der alte Schluss war NICHT durch das + abweichende Exit-Modell verzerrt. + **Mechanismus:** Der Kanalrand ist eine Regressionslinie durch die letzten N Closes — + dahinter liegt **nichts**: keine Order, kein Pivot, keine Struktur. Er hält nur, + solange der Trend ohnehin läuft. Genau darin liegt der Unterschied zu **P(break) auf + einem echten S/R-Level** (Pivot = Stelle, an der real gehandelt wurde), das + out-of-sample kalibriert ist (AUC 0,65 bzw. 0,68–0,72). „Linie im Chart" ist nicht + „Struktur im Markt". **19. verworfener Signal-Eingriff.** - **⚠⚠ P(break) LIVE-AUSWERTUNG (`analyze_pbreak_live.py`, 2026-07-31, 2275 ausgewertete Vorhersagen seit 23.07.): DAS MODELL HÄLT LIVE NICHT, WAS DER BACKTEST VERSPRACH — B5-Fall.** Fällige Messung aus dem Reminder, jetzt erledigt. diff --git a/backtest_structure_tf.py b/backtest_structure_tf.py new file mode 100644 index 0000000..c301a63 --- /dev/null +++ b/backtest_structure_tf.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Marktstruktur-Kanal als SIGNAL — über MEHRERE Zeitebenen (2026-08-03). + +Anlass: User-Beobachtung im M5-Chart „der Kurs folgt dem Marktstrukturkanal und +bricht die untere Linie nicht — können wir das nutzen?". Die vorhandene Messung +(`backtest_structure.py`, 2026-07-20) lief ausschliesslich auf **M30** und fiel +in beiden Hälften durch. M5 ist damit streng genommen eine UNGETESTETE Variante. + +Unterschiede zu `backtest_structure.py` — bewusst, und beide relevant: + + 1. **Kanonischer Exit** (`core/exit_model.simulate`, LIVE) statt der dortigen + Exit-KOPIE. Jene nutzt Trail **1,5** und kennt weder Time-Stop noch TP; live + läuft seit 2026-07-31 Trail **1,0** MIT Time-Stop. Für eine NEUE Messung ist + der kanonische Kern richtig (die alte Datei bleibt unverändert — sie umzu- + stellen wäre laut Migrations-Regel nur erlaubt, wenn die Zahlen gleich blieben). + 2. **Zeitebene als Parameter**, plus zwei Lesarten des „M5-Kanals": + · `bars` — gleiche BAR-Anzahl wie auf M30 (=schnellerer, kürzerer Kanal; + das, was man im M5-Chart tatsächlich sieht) + · `time` — gleiche ZEIT-Spanne (Fenster ×6 auf M5) = derselbe Kanal, nur + feiner aufgelöst + Beide zu testen ist Pflicht: kippt das Vorzeichen zwischen ihnen, ist es + Rauschen (die ORB-/SMC-Lehre — Nachbar-Parameter müssen mithalten). + +Maßstab wie immer: beidhälftig positiv UND parameter-robust. Vergleich: +Squeeze ØR +0,14…+0,23 bei PF>1. + +Aufruf: python backtest_structure_tf.py [bars] +""" +import sys + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.structure import _pivots, _classify, _channel + +_ATRMIN = 0.06 +_COOL = 3 + + +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:<26} — (keine Trades)" + return (f" {lbl:<26} 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, lo, hi, rule, X, look, reg): + """Sequentielle 1-Positions-Sim mit dem KANONISCHEN Live-Exit.""" + def cost(i, atr): + return (SP[i] if SP[i] > 0 else 0.0225) / atr + + Rs = [] + i = max(lo, look) + prev_pos = None + while i < hi: + atr = A[i] + if not atr or atr < _ATRMIN: + i += 1; prev_pos = None; continue + ch = _channel(C[max(0, i - reg + 1):i + 1], atr) + if not ch: + i += 1; prev_pos = None; continue + pos, cdir = ch["pos"], ch["dir"] + d = 0 + if rule in ("A", "A2"): + # frischer Eintritt ins untere (Aufwärtskanal) bzw. obere (Abwärts) Drittel + if cdir == "up" and pos <= X and (prev_pos is None or prev_pos > X): + d = 1 + elif cdir == "down" and pos >= 1 - X and (prev_pos is None or prev_pos < 1 - X): + d = -1 + if d != 0 and rule == "A2": + pl = _pivots(H[max(0, i - look):i + 1], L[max(0, i - look):i + 1], 3) + lab = _classify(pl) + recent = [x["type"] for x in lab[-4:]] + ups = sum(1 for t in recent if t in ("HH", "HL")) + dns = sum(1 for t in recent if t in ("LH", "LL")) + strend = ("up" if ups >= 3 and ups > dns else + "down" if dns >= 3 and dns > ups else "range") + if (d > 0 and strend != "up") or (d < 0 and strend != "down"): + d = 0 + prev_pos = pos + if d == 0: + i += 1; continue + entry = C[i] + a = max(atr, _ATRMIN) + r, xj = simulate(entry, 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 + prev_pos = None + 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) + point = mt5.symbol_info(sym).point + TFS = [("M5", mt5.TIMEFRAME_M5), ("M15", mt5.TIMEFRAME_M15), + ("M30", mt5.TIMEFRAME_M30)] + data = {} + for lbl, tf in TFS: + b = mt5.copy_rates_from_pos(sym, tf, 0, n) + if b is not None and len(b) > 5000: + data[lbl] = ([float(x["high"]) for x in b], [float(x["low"]) for x in b], + [float(x["close"]) for x in b], + [float(x["spread"]) * point for x in b]) + mt5.shutdown() + + print("=" * 92) + print(f" Kanal-Pullback über Zeitebenen — {sym}, kanonischer Live-Exit " + f"(SL {LIVE.sl_atr}× · Trail {LIVE.mult}× · BE {LIVE.be}× · Time-Stop), Echtkosten") + print(" Regel: im Aufwärtskanal am UNTEREN Drittel long / im Abwärtskanal am OBEREN short") + print(" Maßstab: beidhälftig ØR>0 UND PF>1 UND Nachbar-Varianten halten mit") + print("=" * 92) + + # (reg, look) je Lesart. M30-Referenz = 60/220 wie in backtest_structure.py. + plans = [ + ("M5 Kanal 60 Bars (=5 h, 'was man im M5-Chart sieht')", "M5", 60, 220), + ("M5 Kanal 360 Bars (=30 h, zeitgleich zum M30-Kanal)", "M5", 360, 1320), + ("M15 Kanal 120 Bars (=30 h, Nachbar-Zeitebene)", "M15", 120, 440), + ("M30 Kanal 60 Bars (=30 h, die ALTE Messung)", "M30", 60, 220), + ] + for title, tfl, reg, look in plans: + if tfl not in data: + print(f"\n {title}: keine Daten"); continue + H, L, C, SP = data[tfl] + A = _atr_series(H, L, C) + N = len(C); mid = N // 2 + print(f"\n {title} [{N} Bars]") + for rl, X, sub in (("A", 0.20, "Kanal-Pullback X=0.20"), + ("A", 0.30, "Kanal-Pullback X=0.30"), + ("A2", 0.25, "+Swing-Filter X=0.25")): + s1 = st(run(H, L, C, A, SP, 0, mid, rl, X, look, reg)) + s2 = st(run(H, L, C, A, SP, mid, N - LIVE.max_hold - 1, rl, X, look, reg)) + print(f" · {sub}") + 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' if ok else 'fällt durch'}") + print() + + +if __name__ == "__main__": + main()