diff --git a/CLAUDE.md b/CLAUDE.md index f789892..60916f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7434,6 +7434,61 @@ vollständig. ⚠ **Verifikation der Zustellung steht aus** — sie erfolgt mit dem Report am nächsten Morgen um 07:30. Die Bausteine sind einzeln live gegengeprüft. +## ⚠⚠ ENTRY-RAUM-GATE GEGEN AUSFÜHRUNG = NICHT GEBAUT — aber der Lauf +## zeigt etwas Wichtigeres (`backtest_entryroom_level.py`, 2026-08-19) + +Anlass: User-Frage nach Verbesserungen der Empfehlung. `entry_room` blockt +über 7 Tage **60,6 %** (gerichtet nur **2,7 %**) und ist damit der mit Abstand +größte Blocker — kalibriert aber am 2026-07-16 mit **Markt**-Einstieg. Seit dem +05./06.08. ist belegt, dass eine **ruhende Order AM Level** etwas anderes ist: +Markt-Einstieg unter einem Widerstand = in die Wand kaufen, BUY_STOP am +Widerstand = Ausbruch daraus. **Gegenteilige Trades, das Gate blockt beide.** +Gemessen wurde deshalb die GEBLOCKTE Gruppe getrennt (Muster `backtest_htf_vola`), +80k M5, voller Gate-Stack, `room_info` = die echte Gate-Methode, Fill bei +BERÜHRUNG, kanonischer Exit, Echtkosten, 95-%-KI. + +| Gruppe | H1 ØR / PF | H2 ØR / PF | +|---|---|---| +| GEBLOCKT · Markt | −0,228 / 0,64 | +0,008 / 1,01 | +| **GEBLOCKT · Level (ruhend)** | **−0,119 / 0,81** | **+0,096 / 1,18** | +| durchgelassen · Markt | *n=1* | *n=3* | +| **freie Bahn · Markt (= was heute durchkommt)** | **−0,145 / 0,75** | **−0,071 / 0,87** | + +❌ **Regel verfehlt** — H1 bleibt in **allen** Varianten negativ, auch über alle +vier Nachbarn (Puffer 0,00/0,05/0,10 × Gültigkeit 6/12/24). H2 allein würde +bestehen (KI [+0,015 … +0,180]) — klassisches Regime-Kippen. **Nichts gebaut.** + +✅ **BEFUND 1 — die Ausführung trägt auch hier, beidhälftig:** auf **denselben** +Signalen ist die ruhende Order besser als der Markt, **+0,109 (H1) / +0,088 (H2)** +R je Trade, Trefferquote 35→40 % bzw. 43→48 %. Gleiche Richtung und +Größenordnung wie beim Squeeze (+0,32) — nur reicht es hier nicht, um H1 ins +Plus zu drehen. **Der Ausführungs-Hebel ist real, das Signal darunter trägt nicht.** + +⚠⚠ **BEFUND 2 — DAS GATE IST IN WAHRHEIT BINÄR, und das war unbekannt.** Von +4.258 Signalen fallen **4** (!) in „Level vorhanden, aber weit genug“. Alles +andere ist entweder geblockt (2.624) oder **freie Bahn ohne jedes Level** +(1.630). Das Gate fragt praktisch nur: *gibt es überhaupt ein M5-Pivot in +Trade-Richtung?* — passend zum dokumentierten Median-Abstand von **0,61×ATR**, +der exakt auf der Schwelle 0,60 liegt. + +⚠⚠⚠ **BEFUND 3 — was das Gate DURCHLÄSST, ist selbst beidhälftig negativ** +(−0,145 / −0,071, PF 0,75/0,87). In H2 ist es sogar **invertiert**: die +geblockte Gruppe steht bei +0,008, die durchgelassene bei −0,071 — dort blockt +das Gate die BESSERE Hälfte. In H1 ist es umgekehrt richtig. **Das Gate +sortiert Verluste um, es wählt keine profitable Population aus.** +⚠ Daraus folgt **nicht** „Gate raus“ — ohne Gate käme die in H1 klar schlechtere +Gruppe dazu. Es folgt: **an dieser Stellschraube ist nichts zu gewinnen**, weil +die Grundgesamtheit keinen Edge hat. Deckt sich mit der Selbstmessung +`rec_outcomes` (49,2 % / 48,3 % Treffer, n=61, 13.–18.08.), die den +Münzwurf-Befund von `analyze_signal_live.py` unabhängig **live** reproduziert. + +⚠ Abgrenzung zu `backtest_entryroom.py` (2026-07-16): dort lief der **S/R-Close- +Exit**, hier der kanonische — die absoluten Zahlen sind nicht vergleichbar, +belastbar ist der **interne** Vergleich (alle Gruppen, gleicher Exit). +⚠ Nicht modelliert: Exit-Slippage; und es ist **kein** sequentieller Ein-Slot-Lauf +(verglichen werden Populationen, nicht Kontostände). +**26. verworfener Signal-Eingriff.** + ## Gesamtprüfung 2026-08-11 (nach `docs/review-prompt.md`) Zweiter vollständiger Durchlauf. **Drei echte Defekte**, alle behoben; keine diff --git a/backtest_entryroom_level.py b/backtest_entryroom_level.py new file mode 100644 index 0000000..005335e --- /dev/null +++ b/backtest_entryroom_level.py @@ -0,0 +1,273 @@ +"""Traegt der ENTRY-RAUM-Filter noch, wenn man am Level einsteigt? (2026-08-19) + +FRAGE (User: "hast Du Ideen wie ich die Empfehlung noch verbessern koennte?"). +`entry_room` ist mit 60,6 % der mit ABSTAND groesste Blocker der Empfehlung +(gerichtet sind nur 2,7 %). Kalibriert wurde er am 2026-07-16 mit +`backtest_entryroom.py` -- dort steigt die Sim am BAR-CLOSE ein, also per MARKT. +Die Begruendung lautet folgerichtig: Gegenlevel nah => Ertrag gedeckelt => +nicht handeln (PF 0,42/0,66, sauber gemessen). + +Am 05./06.08. hat sich die Ausfuehrung geaendert und das Gate wurde NICHT +nachgezogen. Seither ist belegt, dass eine RUHENDE ORDER AM LEVEL etwas +grundlegend anderes ist als ein Markt-Einstieg mit dem Level als Hindernis +voraus (Squeeze: -0,158 => +0,124/+0,292). + + Markt-Einstieg unter einem Widerstand = in die Wand hineinkaufen + BUY_STOP am Widerstand = Ausbruch aus der Wand heraus + +Das sind GEGENTEILIGE Trades, und das Gate kann sie nicht unterscheiden -- +es blockt beide. + +GEMESSEN wird deshalb die von `entry_room` GEBLOCKTE Gruppe getrennt, genau +wie `backtest_htf_vola.py` es fuer den M30-Filter gemacht hat: + A) Markt am Bar-Close (was die Sim von 2026-07-16 unterstellt) + B) ruhende Stop-Order AM blockierenden Level, Fill bei BERUEHRUNG + +Das Level ist ein M5-Pivot aus der Vergangenheit, es STEHT also -- das ist der +Unterschied zu `_pend` (wandert, `backtest_signal_touchfill.py` faellt durch) +und dieselbe Eigenschaft wie die Squeeze-Box (steht, haelt). + +REGEL VORAB FIXIERT -- gebaut wird nur, wenn ALLES zutrifft: + (1) geblockte Gruppe mit Level-Einstieg: OeR > 0 UND PF > 1 in BEIDEN Haelften + (2) 95-%-KI schliesst in BEIDEN Haelften die Null aus + (3) die Nachbarn (Puffer, Gueltigkeitsdauer) kippen nicht + (4) n >= 100 je Haelfte + +GRENZEN, vorab benannt: + - Kein sequentieller EIN-Slot-Lauf. Verglichen werden zwei POPULATIONEN + (geblockt/durchgelassen), nicht zwei Kontostaende; dafuer ist der OeR je + Trade mit KI die richtige Statistik. Ein Slot-Lauf braeuchte eine + Vorrang-Entscheidung und misst dann Selektion statt Gate. + - Exit-Slippage ist NICHT modelliert (senkt alle Varianten, die haeufiger + handelnden staerker). + - Der ATR fuer die R-Normierung ist in A und B derselbe (Signal-Bar), sonst + waeren die R-Einheiten nicht vergleichbar. + +Aufruf: python backtest_entryroom_level.py [bars] +""" +import sys +from bisect import bisect_left, bisect_right + +import MetaTrader5 as mt5 + +from backtest_breakout_gated import (_ATRMIN, _ATR_FLOOR_PB, _COOL, _PIV_K, + _PIV_LOOK, _TIMEOUT_BARS, _TU, + _atr_series, _htf_sign) +from backtest_cost_gate import kennzahlen +from core.analysis import calc_trend_angle +from core.exit_model import LIVE, simulate +from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW, + _ema_series) + +_K = 0.3 # breakout_k, Live-Wert +_GATE = 0.6 # entry_room_atr, Live-Wert + + +def sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, lo, hi): + """Alle Signale NACH _build + _confirm_breakout, VOR dem Raum-Gate. + + Geliefert wird je Signal die Rohlage; das Gate wird hier bewusst NICHT + angewandt -- seine Entscheidung ist der Gegenstand der Messung. + """ + out = [] + pend = None + i = max(lo, _PIV_LOOK + _EMA_SLOW + 5) + while i < hi: + atr = A[i] + if not atr or atr < _ATRMIN: + i += 1 + continue + w._pb_feats = {"atr": max(atr, _ATR_FLOOR_PB)} + _a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIV_K) + _a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIV_K) + w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]], + "pl": [C[j] for j in PL[_a2:_b2]]} + rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5, + htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i]) + sig = rec.get("signal") + + if _K > 0 and sig in ("LONG", "SHORT"): + d = 1 if sig == "LONG" else -1 + if pend is None or pend["dir"] != d: + pend = {"dir": d, "level": C[i] + d * _K * atr, + "invalid": C[i] - d * _K * atr, "confirmed": False, "bar": i} + if not pend["confirmed"]: + broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"]) + gegen = (C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"]) + if broke: + pend["confirmed"] = True + else: + if gegen or (i - pend["bar"]) > _TIMEOUT_BARS: + pend = {"dir": d, "level": C[i] + d * _K * atr, + "invalid": C[i] - d * _K * atr, + "confirmed": False, "bar": i} + i += 1 + continue + elif sig == "WARTEN": + pend = None + + if sig not in ("LONG", "SHORT"): + i += 1 + continue + + d = 1 if sig == "LONG" else -1 + # ECHTE Methode, nicht nachgebaut -- dieselbe Zahl, die das Gate benutzt. + ri = w.room_info(C[i])[("long" if d > 0 else "short")] or {} + out.append({"i": i, "d": d, "atr": max(atr, _ATRMIN), "px": C[i], + "lvl": ri.get("level"), "dist": ri.get("dist_atr"), + "frei": bool(ri.get("frei"))}) + pend = None + i += _COOL + return out + + +def handeln(sig, H, L, C, SP, ende, modus, puffer, gueltig): + """R eines Signals. modus 'markt' = Bar-Close, 'level' = ruhende Stop-Order.""" + i, d, atr = sig["i"], sig["d"], sig["atr"] + + def cost(j): + return (SP[j] if SP[j] > 0 else 0.0225) / atr + + if modus == "markt": + entry, j = C[i], i + else: + lvl = sig["lvl"] + if lvl is None: + return None # freie Bahn: kein Level zum Liegen + entry = lvl + d * puffer * atr + # Stop-Order MUSS auf der richtigen Marktseite liegen (Lehre 06.08.: + # ohne diese Pruefung fuellt die Sim zu laengst passierten Preisen). + if (d > 0 and entry <= C[i]) or (d < 0 and entry >= C[i]): + return None + j = None + for x in range(i + 1, min(i + 1 + gueltig, ende)): + if (H[x] >= entry) if d > 0 else (L[x] <= entry): + j = x + break + if j is None: + return None # nicht gefuellt = kein Trade + if j + 1 >= ende: + return None + r = simulate(entry, d, atr, H, L, C, j + 1, LIVE, timestop=True, use_tp=True) + return r - cost(j) + + +def gruppen(sigs, H, L, C, SP, ende, puffer, gueltig): + g = {"blk_markt": [], "blk_level": [], "frei_markt": [], + "durch_markt": [], "durch_level": []} + for s in sigs: + rm = handeln(s, H, L, C, SP, ende, "markt", puffer, gueltig) + rl = handeln(s, H, L, C, SP, ende, "level", puffer, gueltig) + if s["frei"] or s["lvl"] is None: + if rm is not None: + g["frei_markt"].append(rm) + continue + blk = (s["dist"] is not None and s["dist"] < _GATE) + if rm is not None: + g["blk_markt" if blk else "durch_markt"].append(rm) + if rl is not None: + g["blk_level" if blk else "durch_level"].append(rl) + return g + + +def zeig(lbl, k1, k2): + def f(k): + if not k: + return " --" + ci = k.get("ci") + m = "" + if ci: + m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1]) + + ("" if ci[0] * ci[1] > 0 else " ~KI enthaelt 0")) + return ("n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}" + .format(k["n"], k["wr"], k["oer"], k["pf"], m)) + print(" {:<28} H1 {}".format(lbl, f(k1))) + print(" {:<28} H2 {}".format("", f(k2))) + + +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) + EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW) + N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1 + + print("Vorberechnung (Winkel, HTF-Trends, Pivots) ...", flush=True) + AD = [90.0] * N + for i in range(_ANGLE_LR + 2, N): + AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR) + HTF = [0] * N; H1 = [0] * N + for i in range(N): + if A[i]: + HTF[i] = _htf_sign(C, i, 6, A[i]) + H1[i] = _htf_sign(C, i, 12, A[i]) + PH = [j for j in range(_PIV_K, N - _PIV_K) + if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])] + PL = [j for j in range(_PIV_K, N - _PIV_K) + if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])] + + w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5) + w.set_reversal_enabled(False) + w.set_entry_room(_GATE) # Live-Konfiguration, nicht Default + w.set_dead_hours([]) + w.set_breakout_k(_K) + + print("=" * 96) + print(" ENTRY-RAUM-GATE gegen AUSFUEHRUNG -- {} M5 ({} Bars, 2 Halbjahre)" + .format(sym, N)) + print(" Gate {}x ATR_M5 - breakout_k {} - Reversal AUS - Exit kanonisch" + .format(_GATE, _K)) + print("=" * 96) + + print("Signale sammeln (voller Gate-Stack, VOR dem Raum-Gate) ...", flush=True) + s1 = sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, 0, mid) + s2 = sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, mid, ende) + print(" H1 {} - H2 {} Signale vor dem Raum-Gate".format(len(s1), len(s2))) + + PU, GU = 0.0, 12 + g1 = gruppen(s1, H, L, C, SP, ende, PU, GU) + g2 = gruppen(s2, H, L, C, SP, ende, PU, GU) + + print("") + print(" HAUPTLAUF (Puffer {}x ATR - Order gueltig {} Bars = 1 h)".format(PU, GU)) + print(" " + "-" * 92) + for key, lbl in (("blk_markt", "GEBLOCKT - Markt"), + ("blk_level", "GEBLOCKT - Level (ruhend)"), + ("durch_markt", "durchgel. - Markt (=heute)"), + ("durch_level", "durchgel. - Level (ruhend)"), + ("frei_markt", "freie Bahn - Markt")): + zeig(lbl, kennzahlen(g1[key]), kennzahlen(g2[key])) + print("") + + print(" NACHBARN (Regel 3) -- nur die geblockte Gruppe am Level") + print(" " + "-" * 92) + for pu, gu in ((0.05, 12), (0.10, 12), (0.0, 6), (0.0, 24)): + a = gruppen(s1, H, L, C, SP, ende, pu, gu)["blk_level"] + b = gruppen(s2, H, L, C, SP, ende, pu, gu)["blk_level"] + zeig("Puffer {:.2f} - gueltig {}".format(pu, gu), + kennzahlen(a), kennzahlen(b)) + print("") + + k1, k2 = kennzahlen(g1["blk_level"]), kennzahlen(g2["blk_level"]) + ok = bool(k1 and k2 and k1["n"] >= 100 and k2["n"] >= 100 + and k1["oer"] > 0 and k2["oer"] > 0 + and k1["pf"] > 1 and k2["pf"] > 1 + and k1["ci"] and k2["ci"] + and k1["ci"][0] > 0 and k2["ci"][0] > 0) + print("=" * 96) + print(" URTEIL nach der vorab fixierten Regel: {}" + .format("ERFUELLT -- weiterverfolgen" if ok + else "NICHT erfuellt -- nichts bauen")) + print("=" * 96) + + +if __name__ == "__main__": + main()