From f15a36138ff20b6abc02e66eba11651654d93632 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Wed, 19 Aug 2026 01:09:12 +0200 Subject: [PATCH] Konfidenz-Schwelle und M15-Selektivitaet gemessen: keine Aenderung (2) conf_pct trennt im Markt-Modus NICHT: der Bandverlauf ist nicht gleichgerichtet (H1 bestes Band 0-44, schlechtestes 55-64), und die Schwelle bewegt nichts -- >=0 liefert H1 -0,189, >=55 liefert -0,197. 11,4 % Signalmenge fuer 0,017 R. Mit ruhender Order trennt die obere Haelfte: >=75 schlaegt die Live-Schwelle im OeR beidhaelftig (-0,001/+0,147 gegen -0,070/+0,081), aber die Summe faellt (+82,8 gegen +146,1). Regel verlangte beides -> nicht geaendert, Regel NICHT nachtraeglich gelockert. 27. verworfener Signal-Eingriff. Dritte unabhaengige Bestaetigung des Ausfuehrungs-Hebels am selben Tag: Level gegen Markt +0,127/+0,096 R, groessengleich zu backtest_entryroom_level. (3) M15-Karte steht 37,8 % der ZEIT gruen (in Wechseln gerechnet waeren es 49 % -- gruene Phasen sind kuerzer). Handlungsrelevanter Zustand entkoppelt 54,1 % / 51,5 %, beide KI enthalten die 50 %. 205 von 350 unabhaengigen Faellen, also in gut zwei Wochen entscheidbar. Die Hysterese vom 12.08. wirkt (53 -> 54,1 %). Gate 3b ins Banner zu ziehen ist durch den Entry-Raum-Lauf schon beantwortet: diese Dimension trennt nicht. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 77 ++++++++++++++++++ backtest_conf_gated.py | 156 ++++++++++++++++++++++++++++++++++++ backtest_entryroom_level.py | 1 + 3 files changed, 234 insertions(+) create mode 100644 backtest_conf_gated.py diff --git a/CLAUDE.md b/CLAUDE.md index 60916f8..8420d66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7434,6 +7434,83 @@ 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. +## ⚠⚠ KONFIDENZ-SCHWELLE 55 GEMESSEN: SIE TRENNT NICHTS — keine Änderung +## (`backtest_conf_gated.py`, 2026-08-19) + +Punkt (2) der Verbesserungsliste. `min_conf` blockt live **11,4 %**. Die 55 +stammen aus `backtest_conf.py` — gemessen an `_build` **allein**, ohne +Breakout-Bestätigung und Raum-Gate (also eine andere Population, 77,9 % gegen +19,5 % gerichtet) und mit älterem Exit. Neu: voller Gate-Stack, `_MIN_CONF` +für die Messung auf 0, kanonischer Exit, Echtkosten, 2 Halbjahre, 95-%-KI, +**beide Ausführungs-Achsen**. + +**A) Markt — der Verlauf ist NICHT gleichgerichtet, also Rauschen:** + +| Band | H1 ØR | H2 ØR | +|---|---|---| +| 0–44 | **−0,014** (bestes H1!) | −0,072 | +| 45–54 | −0,141 | −0,082 | +| **55–64** | **−0,259** (schlechtestes) | −0,046 | +| 65–74 | −0,196 | −0,017 | +| 75–100 | −0,180 | +0,018 | + +⚠⚠ **Die Schwelle bewegt gar nichts:** ≥ 0 liefert H1 −0,189, ≥ 55 liefert +**−0,197** — also minimal SCHLECHTER. Über alle fünf Schwellen liegt der ØR in +H1 zwischen −0,180 und −0,197. **11,4 % der Signalmenge werden für eine +Wirkung von 0,017 R weggeworfen.** + +**B) ruhende Order am Level — hier trennt die OBERE Hälfte, aber:** + +| Schwelle | H1 ØR / ΣR | H2 ØR / ΣR | +|---|---|---| +| ≥ 0 | −0,062 / −83,6 | +0,078 / **+165,6** | +| **≥ 55 (live)** | **−0,070 / −82,1** | **+0,081 / +146,1** | +| ≥ 65 | −0,074 / −77,8 | +0,103 / +133,0 | +| **≥ 75** | **−0,001 / −0,5** | **+0,147 / +82,8** | + +✅ **≥ 75 schlägt die Live-Schwelle im ØR in BEIDEN Hälften** (−0,001 gegen +−0,070 · +0,147 gegen +0,081) und ist die einzige Zelle, in der H1 den +Breakeven erreicht. ❌ **Aber ΣR fällt** (+82,8 gegen +146,1) — die vorab +fixierte Regel verlangte **beides**. **Nicht geändert.** +⚠ Das ist wieder der dokumentierte Spezifikations-Konflikt aus +`backtest_cost_gate.py`: ØR und ΣR laufen mechanisch gegeneinander, wenn die +Varianten verschieden viele Trades handeln (416 gegen 1.175). Die Regel wurde +**nicht nachträglich gelockert**; beide Spalten stehen deshalb oben. + +✅ **Dritte unabhängige Bestätigung des Ausführungs-Hebels am selben Tag:** bei +≥ 55 liefert der Level-Einstieg −0,070/+0,081 gegen −0,197/−0,015 am Markt = +**+0,127 / +0,096 R**, größengleich zu `backtest_entryroom_level.py` +(+0,109/+0,088). **Die Achse trägt reproduzierbar, das Signal darunter nicht.** +**27. verworfener Signal-Eingriff.** + +## ⚠ M15-KARTE: SELEKTIVITÄT GEMESSEN — weiter kein Urteil möglich (2026-08-19) + +Punkt (3). Ausgewertet ab dem Hysterese-Stichtag 12.08. (5.979 Wechsel). +⚠ **In ZEIT statt in Wechseln gerechnet** — die Wechsel-Zahl (49 % grün) +überschätzt, weil grüne Phasen kürzer sind: + +| Zustand | Anteil der Zeit | +|---|---| +| gelb | **62,2 %** (104,0 h) | +| **grün** | **37,8 %** (63,1 h) | +| rot | 0,0 % (0,1 h) | + +| handlungsrelevant (grün + gerichtet) | Treffer | n | 95-%-KI | +|---|---|---|---| +| 30 min roh | 51,2 % | 2.869 | [49,3 … 53,0] | +| **30 min entkoppelt** | **54,1 %** | **205** | **[47,3 … 60,5]** ⚠ | +| 60 min entkoppelt | 51,5 % | 204 | [44,1 … 58,3] ⚠ | + +**Beide Intervalle enthalten die 50 %** — unverändert **nicht belegt**, aber +auch nicht widerlegt. Aus 2.869 rohen Zeilen bleiben **205** unabhängige; die +Latte von 350 (wie bei `pbreak_accuracy_v2`) ist zu **59 %** erreicht, also in +gut zwei Wochen entscheidbar. ✅ Die Hysterese vom 12.08. wirkt: der +entkoppelte Wert stieg von 53 auf 54,1 %. +⚠⚠ **Die naheliegende Schärfung ist damit schon beantwortet:** Gate ③b +(Entry-Raum) ins Banner zu ziehen würde auf einer Dimension filtern, die am +selben Tag als **nicht trennend** gemessen wurde — was `entry_room` +durchlässt, ist selbst beidhälftig negativ. **Nichts geändert.** + ## ⚠⚠ ENTRY-RAUM-GATE GEGEN AUSFÜHRUNG = NICHT GEBAUT — aber der Lauf ## zeigt etwas Wichtigeres (`backtest_entryroom_level.py`, 2026-08-19) diff --git a/backtest_conf_gated.py b/backtest_conf_gated.py new file mode 100644 index 0000000..ca37c36 --- /dev/null +++ b/backtest_conf_gated.py @@ -0,0 +1,156 @@ +"""Trennt `conf_pct` ueberhaupt? Und ist 55 die richtige Schwelle? (2026-08-19) + +FRAGE (User, Punkt (2) der Verbesserungsliste). `min_conf` blockt live **11,4 %** +aller Empfehlungen. Die Schwelle 55 stammt aus `backtest_conf.py`: Band 40-54 % +gemessen negativ (-0,025), >= 55 % positiv (+0,058). + +WARUM DAS NEU ZU MESSEN IST -- drei Gruende, jeder fuer sich hinreichend: + 1. `backtest_conf.py` misst `_build` ALLEIN, ohne Breakout-Bestaetigung und + ohne Entry-Raum-Gate. Genau die beiden sind live die groessten Blocker + (dokumentiert 2026-08-05: 77,9 % gerichtet allein vs. 19,5 % im vollen + Stack). Es ist also eine andere Population. + 2. Es nutzt ein aelteres Exit-Modell, nicht `exit_model.LIVE`. + 3. `analyze_verdict_calibration.py` hat `conf_pct` als **nicht kalibriert** + ausgewiesen und in H2 sogar INVERTIERT (85-100 % -> 40 % Trefferquote). + Ob es wenigstens im ERTRAG trennt, ist damit offen. + +AUFBAU. Voller Live-Gate-Stack, aber `_MIN_CONF` auf 0 gesetzt, damit auch die +heute geblockten Signale sichtbar werden; danach nach Konfidenz-Baendern +aufgeteilt. Kanonischer Exit, Echtkosten, 2 Halbjahre, 95-%-KI je Zelle. +Zusaetzlich beide Ausfuehrungs-Achsen (Markt / ruhende Order am Level), weil +`backtest_entryroom_level.py` am selben Tag gezeigt hat, dass die Achse +beidhaelftig +0,09 bis +0,11 R traegt. + +REGEL VORAB FIXIERT: + - Die Schwelle wird nur verschoben, wenn ein anderer Schnitt in BEIDEN + Haelften mehr OeR UND mehr Summe liefert und die Nachbarbaender mithalten. + - Zusaetzliche Aussage: trennt conf ueberhaupt? Das verlangt einen in beiden + Haelften GLEICHGERICHTETEN Verlauf ueber die Baender -- ein Band, das in H1 + steigt und in H2 faellt, ist Rauschen. + +GRENZE: kein sequentieller Ein-Slot-Lauf (verglichen werden Populationen). +Exit-Slippage ist nicht modelliert. + +Aufruf: python backtest_conf_gated.py [bars] +""" +import sys + +import MetaTrader5 as mt5 + +import core.wave_rec as WR +from backtest_breakout_gated import _PIV_K, _TU, _atr_series, _htf_sign +from backtest_cost_gate import kennzahlen +from backtest_entryroom_level import _GATE, _K, handeln, sammle +from core.analysis import calc_trend_angle +from core.exit_model import LIVE +from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW, + _ema_series) + +BAENDER = [(0, 45), (45, 55), (55, 65), (65, 75), (75, 101)] + + +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 " ~0")) + return ("n={:>4} WR={:>3.0f}% OeR={:+.3f} SR={:+8.1f} PF={:.2f}{}" + .format(k["n"], k["wr"], k["oer"], k["sr"], k["pf"], m)) + print(" {:<16} H1 {}".format(lbl, f(k1))) + print(" {:<16} 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 ...", 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) + w.set_dead_hours([]) + w.set_breakout_k(_K) + + # Gate oeffnen, damit die heute geblockten Baender ueberhaupt entstehen. + # ⚠ Modul-Konstante, wirkt prozessweit -- deshalb hier und nirgends sonst. + alt = WR._MIN_CONF + WR._MIN_CONF = 0 + print("=" * 100) + print(" KONFIDENZ-SCHWELLE im vollen Gate-Stack -- {} M5 ({} Bars)".format(sym, N)) + print(" _MIN_CONF {} -> 0 fuer die Messung ({} Bars pro Haelfte)".format(alt, mid)) + print("=" * 100) + try: + 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) + finally: + WR._MIN_CONF = alt + print(" Signale ohne Konfidenz-Gate: H1 {} H2 {}".format(len(s1), len(s2))) + + for modus, titel in (("markt", "A) MARKT am Bar-Close (= wie die Sim von 2026-07-16)"), + ("level", "B) ruhende Order AM Level, Fill bei BERUEHRUNG")): + print("") + print(" " + titel) + print(" " + "-" * 96) + for lo, hi in BAENDER: + def rs(sigs): + out = [] + for s in sigs: + c = s.get("conf") + if c is None or not (lo <= c < hi): + continue + r = handeln(s, H, L, C, SP, ende, modus, 0.0, 12) + if r is not None: + out.append(r) + return out + zeig("conf {}-{}".format(lo, hi - 1), kennzahlen(rs(s1)), kennzahlen(rs(s2))) + print("") + + # Kumulativ: was liefert die Schwelle X, wenn man alles >= X handelt? + print(" Schwellen-Vergleich (alles >= X gehandelt)") + for x in (0, 45, 55, 65, 75): + def rs2(sigs): + out = [] + for s in sigs: + c = s.get("conf") + if c is None or c < x: + continue + r = handeln(s, H, L, C, SP, ende, modus, 0.0, 12) + if r is not None: + out.append(r) + return out + tag = " <- LIVE" if x == alt else "" + zeig(">= {}{}".format(x, tag), kennzahlen(rs2(s1)), kennzahlen(rs2(s2))) + print("") + print("=" * 100) + + +if __name__ == "__main__": + main() diff --git a/backtest_entryroom_level.py b/backtest_entryroom_level.py index 005335e..7546c82 100644 --- a/backtest_entryroom_level.py +++ b/backtest_entryroom_level.py @@ -114,6 +114,7 @@ def sammle(w, H, L, C, A, EF, ES, AD, HTF, H1, PH, PL, lo, hi): # 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], + "conf": rec.get("conf_pct"), # additiv, s. backtest_conf_gated.py "lvl": ri.get("level"), "dist": ri.get("dist_atr"), "frei": bool(ri.get("frei"))}) pend = None