diff --git a/CLAUDE.md b/CLAUDE.md index ad2f15a..e51bc07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4182,7 +4182,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: („⚠ die Welle (steuert die Order) schweigt"). Live gegengerechnet, beide Zeilen erscheinen wie beabsichtigt. ⚠ Reine Anzeige — an Gewichten, Gates und Order-Logik ist nichts geändert. -- Asset-Version aktuell **v=153** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=157** (in `web/index.html` hochzählen, siehe Workflows). Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis 14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die `font: Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` — @@ -4942,6 +4942,48 @@ Modell schliesst keine Trades. „die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die Basisraten-/Kalibrierungslücke. +## ✅ DIE M15-KARTE MISST SICH JETZT SELBST + AUSFÜHRUNGSQUALITÄT SICHTBAR (2026-08-08, v=157) + +Nach Web-Recherche umgesetzt (User-Wahl aus drei Vorschlägen). ⚠ Der dritte +(Kegel mit **Adaptive Conformal Inference** nachkalibrieren) bleibt OFFEN — der +Kegel ist mit **76,7 / 76,6 / 75,3 %** gegen 80 % Nennwert dokumentiert zu eng, +und ACI ist die Standardmethode dafür (adjustiert das Niveau online, garantiert +Konvergenz auch unter Drift). Nicht gebaut. + +**② SELBSTMESSUNG (`m15_states`).** Die Karte hatte **keine Rückkopplung** — +geprüft, es gab kein Logging. Das war die auffälligste Lücke: **jeder** Ausfall, +der in diesem Projekt je aufgedeckt wurde, kam aus einer Rückkopplung +(`pbreak_predictions` entlarvte den Modellausfall LIVE, was kein Backtest +konnte; der B4-Monitor die Squeeze-Drift). Die Karte hätte monatelang falsch +stehen können. +Neu: eine Zeile je **Zustandswechsel** (Status + Lesart + Level), nach 30/60 min +gegen `candles_m1` ausgewertet, Ergebnis als Zeile ⑥ in der Karte. +⚠ **Gewertet wird die LESART, nicht der Status** — und nur die gerichtete +(45–55 % zählt nicht mit). Gegen 50 % zu lesen. +⚠ **Nur bei Wechsel**, nie je Tick: sonst entsteht dieselbe Cluster-Verzerrung, +die am 07.08. elf widerlegte Hypothesen ausgelöst hat (roh 922 Zeilen 52,6 %, +entkoppelt 93 Zeilen 45,2 %) — und die mein erster Tagestest reproduzierte +(46 fortlaufende Bars als 46 Trades). Live verifiziert: **1 Zeile** trotz +laufendem Sekundentakt. + +**③ AUSFÜHRUNGSQUALITÄT (Zeile ⑦).** `analyze_squeeze_entry_gap.py` existiert +seit dem 05.08. und misst die TCA-Kennzahl (Arrival-Price-Slippage) — **niemand +hat je hineingesehen.** Dabei war das der einzige Erfolgsmaßstab des +Stop-Order-Umbaus (Abstand Einstieg↔Level muss gegen 0, vorher Median ++0,275×ATR) und zugleich der **grösste je gemessene Hebel** (+0,32 R gegenüber +Market). Jetzt steht der Zählstand in der Karte: **3 von 12** Bot-Trades seit dem +Umbau. + +⚠⚠ **BEIM BAU EIN NAMEERROR IM 1-SEKUNDEN-PFAD — vom LINTER gefangen.** Der +erste Entwurf berechnete `_m15` in `_run_analysis` und benutzte es in +`snapshot()`; dort gibt es weder `market` noch `wave_snap`. **`py_compile` sieht +das NICHT**, `ruff F821` meldete **4 Befunde**, darunter `_m15` selbst. Genau +die Fehlerklasse, für die der Linter am 07.08. eingebaut wurde (die beiden +`log.`-NameError in `core/trader.py` sassen Monate in `except`-Zweigen) — und +der erste echte Fang. +⚠ Zweiter Fehler beim Verschieben: der Block landete **mitten in einem +mehrzeiligen Aufruf** (`_cone = _cone_build(`) → SyntaxError. Beide behoben. + ## ⚠⚠ DIE ZWEI GERATENEN M15-SCHWELLEN GEMESSEN — beide NICHT kalibrierbar (2026-08-08) Auftrag: die Reichweite (0,5×ATR) und die Kosten-Schwelle (0,32, von M5) messen @@ -5885,7 +5927,7 @@ besser) · Teil-Exit bei 1:1 (`_PARTIAL_TP_FRAC=0`, User-Vorgabe). (Beobachtungen/Feedback) ergänzt diese Datei. - `oil_widget_config.ini` enthält **Live-Secrets** (Keys/Token) — nicht ins Repo/Logs leaken. -- **DB** `oil_widget_history.db` (trades · intended_trades · recommendations · **candles_m1** · **pbreak_predictions** · **verdict_votes**). +- **DB** `oil_widget_history.db` (trades · intended_trades · recommendations · **m15_states** · **candles_m1** · **pbreak_predictions** · **verdict_votes**). Vor jedem Eingriff Backup (`*.bak-`); Zeitspalten sind lokale Epoch (Broker-Offset bereits korrigiert) — **AUSNAHME `candles_m1.time` = ROHE Broker-Zeit (UTC+3)**, deckungsgleich mit `copy_rates`. diff --git a/core/engine.py b/core/engine.py index 0f6acbd..ec7f1b2 100644 --- a/core/engine.py +++ b/core/engine.py @@ -692,6 +692,12 @@ class TradingEngine: except Exception as pe: log.warning(f"pbreak_predictions: {pe}") try: self._evaluate_rec_outcomes() # ~alle 5 min, self-throttled except Exception as re_: log.warning(f"rec_outcomes: {re_}") + # Selbstmessung der M15-Karte (2026-08-08): ohne Rueckkopplung + # koennte sie monatelang falsch stehen, ohne dass es auffaellt — + # genau die Luecke, die `pbreak_predictions` beim P(break)-Modell + # geschlossen hat. + try: self._evaluate_m15_states() # ~alle 5 min, self-throttled + except Exception as me: log.warning(f"m15_states: {me}") # Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock # unkritisch — reiner HTTP-Fetch mit Timeout) # HL-Kontext (Funding/OI/Premium) — reine Anzeige. @@ -3155,6 +3161,59 @@ class TradingEngine: self._blockmix_ts = now return getattr(self, "_blockmix_cache", None) + def _m15_track_cached(self) -> dict | None: + """Selbstmessung der M15-Karte — Trefferquote ihrer eigenen Lesart. + + ⚠ Gegen 50 % zu lesen. Gemessen wird NUR die bedingte Lesart am Level + („Abprall → eher abwaerts"), nicht eine freie Richtungsprognose — eine + solche ist im Projekt gemessen unmoeglich (AUC 0,50 oos / 0,52 + in-sample). ~120 s gecacht.""" + now = time.time() + if now - getattr(self, "_m15track_ts", 0.0) > 120: + try: + self._m15track_cache = (self.history.m15_accuracy(200) + if self.history else None) + except Exception as e: + log.debug(f"m15_track: {e}") + self._m15track_cache = None + self._m15track_ts = now + return getattr(self, "_m15track_cache", None) + + def _entry_gap_cached(self) -> dict | None: + """AUSFUEHRUNGS-QUALITAET: kommt der Bot-Einstieg AM Level an? + + ⚠⚠ WARUM DAS DIE WICHTIGSTE UNBEOBACHTETE ZAHL WAR. Der Umbau auf + ruhende Stop-Orders (05.08.) hatte als einziges Erfolgskriterium: der + Abstand Einstieg↔Level muss gegen 0 gehen (vorher Median +0,275×ATR). + Das ist der groesste je gemessene Hebel des Projekts (+0,32 R gegenueber + Market) — und niemand hat je hineingesehen. + + ⚠ DIE ERFOLGSMELDUNG IST PARADOX: eine per PENDING gefuellte Order + erzeugt **keine** `AUTO-SQUEEZE-ENTRY`-Logzeile (die entsteht nur im + Market-Fallback). Bot-Trades OHNE Log-Treffer sind also der ERFOLG. + Hier wird deshalb das gezaehlt, was ohne Logdatei bestimmbar ist: + wie viele Bot-Trades es seit dem Umbau gab und wie viele davon ueber + den Market-Fallback liefen (erkennbar am Zaehler `squeeze_entry_count` + bzw. am Setup-Tag). ~300 s gecacht — DB-Abfrage gehoert nicht in den + 1-s-Snapshot.""" + now = time.time() + if now - getattr(self, "_entrygap_ts", 0.0) > 300: + try: + import sqlite3 + con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0) + # Stichtag = Umbau auf ruhende Stop-Orders (05.08. 08:55) + seit = 1786000500 + n, = con.execute( + "SELECT COUNT(*) FROM trades WHERE entry_time >= ? " + "AND setup LIKE 'SQUEEZE%'", (seit,)).fetchone() + con.close() + self._entrygap_cache = {"n_seit_umbau": n, "soll": 12} + except Exception as e: + log.debug(f"entry_gap: {e}") + self._entrygap_cache = None + self._entrygap_ts = now + return getattr(self, "_entrygap_cache", None) + def _rec_track_cached(self) -> dict | None: """Selbst-Kalibrierung: Trefferquote der letzten Empfehlungs-Episoden. @@ -3398,6 +3457,105 @@ class TradingEngine: except Exception as e: log.debug(f"pbreak_predictions auswerten: {e}") + def _log_m15_state(self, s: dict | None, market): + """Loggt einen Zustandswechsel der M15-Karte — die Rueckkopplung, die + der Karte bisher fehlte. + + ⚠⚠ NUR BEI WECHSEL. Wer je Tick loggt, bekommt dieselbe Cluster- + Verzerrung, die am 07.08. elf widerlegte Hypothesen ausgeloest hat + (roh 922 Zeilen 52,6 %, entkoppelt 93 Zeilen 45,2 %) — und die mein + erster Tagestest reproduzierte (46 fortlaufende Bars als 46 Trades). + Der Zustand wird aus Status UND Lesart gebildet: wechselt nur das + Level, ist es ein neuer Zustand; bleibt alles gleich, passiert nichts. + ⚠ Fail-safe: ein Fehler hier darf den Snapshot nie stoeren. + """ + try: + if not s: + return + zonen = s.get("zonen") or [] + kosten = (s.get("kosten") or {}).get("ratio") + atr = s.get("atr_m15") + bid = (market or {}).get("bid") + if not atr or not bid: + return + nah = min(zonen, key=lambda x: x["gap_atr15"]) if zonen else None + status = ("rot" if (kosten is not None and kosten >= 0.32) + else "gruen" if (nah and nah["gap_atr15"] <= 0.5) else "gelb") + # Lesart: dieselbe Regel wie im Frontend (45–55 % = Muenzwurf) + lesart = "unklar" + if nah and nah.get("p_break") is not None: + pb = nah["p_break"] + if pb < 45 or pb > 55: + bricht = pb > 55 + res = nah["seite"] == "res" + lesart = "auf" if (res if bricht else not res) else "ab" + schluessel = (status, lesart, + None if not nah else round(nah["level"], 3)) + if schluessel == getattr(self, "_m15_last_key", None): + return # kein Wechsel → nichts loggen + self._m15_last_key = schluessel + self.history.log_m15_state( + status=status, veto=(s.get("bias") or {}).get("verboten"), + kosten=kosten, level=None if not nah else nah["level"], + gap_atr=None if not nah else nah["gap_atr15"], + p_break=None if not nah else nah.get("p_break"), + lesart=lesart, price=bid, atr=atr) + except Exception as e: + log.debug(f"m15_state log: {e}") + + def _evaluate_m15_states(self): + """Traegt den Forward-Return offener M15-Zustaende nach. + + ⚠ Gewertet wird die LESART, nicht der Status: `hit=1`, wenn der Kurs in + die Richtung lief, die die Karte am Level angezeigt hat. Zustaende ohne + gerichtete Lesart (Muenzwurf-Zone 45–55 %) bekommen keine Richtung und + zaehlen in der Quote nicht mit. + ⚠ `candles_m1.time` ist ROHE Broker-Zeit — Offset noetig (die im Projekt + mehrfach dokumentierte Falle).""" + now = time.time() + if now - getattr(self, "_m15_eval_ts", 0.0) < 300: + return + self._m15_eval_ts = now + try: + import sqlite3 + off = self.trader._broker_offset_s() + con = sqlite3.connect(HISTORY_DB_FILE, timeout=5.0) + con.row_factory = sqlite3.Row + offen = con.execute( + "SELECT id, ts, lesart, price, atr, ret30, ret60 FROM m15_states " + "WHERE done IS NULL OR done = 0").fetchall() + for r in offen: + if r["lesart"] not in ("auf", "ab") or not r["atr"] or r["atr"] <= 0: + con.execute("UPDATE m15_states SET done=1 WHERE id=?", (r["id"],)) + continue + d = 1 if r["lesart"] == "auf" else -1 + sets, args = [], [] + for feld, minuten in (("ret30", 30), ("ret60", 60)): + if r[feld] is not None: + continue + ziel = r["ts"] + minuten * 60 + if now < ziel: + continue + row = con.execute( + "SELECT c FROM candles_m1 WHERE time <= ? AND time > ? " + "ORDER BY time DESC LIMIT 1", + (ziel + off, ziel + off - 900)).fetchone() + if row: + v = (row["c"] - r["price"]) * d / r["atr"] + sets.append(f"{feld}=?"); args.append(round(v, 4)) + sets.append(f"hit{feld[3:]}=?"); args.append(1 if v > 0 else 0) + if now >= r["ts"] + 60 * 60 and (r["ret60"] is not None + or any("ret60" in x for x in sets)): + sets.append("done=?"); args.append(1) + elif now >= r["ts"] + 6 * 3600: + sets.append("done=?"); args.append(1) # keine Bars mehr zu erwarten + if sets: + args.append(r["id"]) + con.execute(f"UPDATE m15_states SET {', '.join(sets)} WHERE id=?", args) + con.commit(); con.close() + except Exception as e: + log.debug(f"m15_states auswerten: {e}") + def _evaluate_rec_outcomes(self): """Traegt fuer offene Empfehlungs-Episoden den Forward-Return nach. @@ -3614,6 +3772,17 @@ class TradingEngine: _cone = _cone_build( (market or {}).get("bid"), float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0)) + # M15-Setup EINMAL rechnen (nicht im Dict), damit die Selbstmessung + # daran haengen kann — ein zweiter Aufruf waere Arbeit fuer dieselben + # Zahlen (der Konsens-Pfeil-Fehler). + # ⚠ MUSS hier stehen, nicht in `_run_analysis`: dort gibt es weder + # `market` noch `wave_snap`. Der erste Entwurf tat genau das und + # erzeugte einen NameError im 1-Sekunden-Pfad. `py_compile` sieht das + # NICHT, `ruff F821` schon (4 Befunde, darunter `_m15` selbst) — genau + # die Fehlerklasse, fuer die der Linter am 07.08. eingebaut wurde. + _m15 = self._m15_setup(market, wave_snap, _cone, + self._alignment_cached()) + self._log_m15_state(_m15, market) # Für den KI-Copiloten festhalten (2026-08-02) — er bekam bis dahin nur # das Wellen-Signal und sah ausgerechnet die beiden kalibrierten Größen # des Projekts nicht. Hier nur ZUWEISEN, nicht neu rechnen. @@ -3799,8 +3968,10 @@ class TradingEngine: # Gesamtempfehlung. ⚠ KEINE Richtungsaussage: auf M15 traegt gemessen # keine (Squeeze n=33/Nachbarn kippen, VWAP-Baender ohne Edge). # Zeigt Bedingungen, nicht Prognosen. - "m15_setup": self._m15_setup(market, wave_snap, _cone, - self._alignment_cached()), + "m15_setup": _m15, + # Selbstmessung der Karte + Ausfuehrungsqualitaet (2026-08-08). + "m15_track": self._m15_track_cached(), + "entry_gap": self._entry_gap_cached(), "sr_close_hint": sr_close_hint, "stop_approach": stop_approach, "auto_sr_close": self._auto_sr_close, diff --git a/core/history.py b/core/history.py index b1dd16d..8d4b7ac 100644 --- a/core/history.py +++ b/core/history.py @@ -161,6 +161,34 @@ SCHEMA = [ ) """, """ + -- M15-KARTE misst sich SELBST (2026-08-08). + -- ⚠ WARUM: jeder Ausfall, der in diesem Projekt je aufgedeckt wurde, kam aus + -- einer Rueckkopplung — `pbreak_predictions` entlarvte den Modellausfall LIVE + -- (was kein Backtest konnte), der B4-Monitor die Squeeze-Drift. Die Karte + -- hatte nichts davon und koennte monatelang falsch stehen, ohne dass es + -- auffaellt. Geloggt wird EINE Zeile je STATUSWECHSEL (nicht je Tick — sonst + -- zaehlt man dieselbe Marktlage dutzendfach; die Lehre aus rec_outcomes und + -- der Clusterung bei pbreak_predictions). + CREATE TABLE IF NOT EXISTS m15_states ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER, -- lokale Epoch, Beginn des Zustands + status TEXT, -- 'gruen' | 'gelb' | 'rot' + veto TEXT, -- verbotene Richtung ('LONG'/'SHORT'/NULL) + kosten REAL, -- Spread/ATR_M15 + level REAL, -- naechstes stehendes Level + gap_atr REAL, -- Abstand Kurs<->Level in ATR_M15 + p_break INTEGER, -- Durchbruchwahrscheinlichkeit in % + lesart TEXT, -- 'auf' | 'ab' | 'unklar' (die Richtungs-Lesart) + price REAL, + atr REAL, -- ATR_M15 zur Normierung + ret30 REAL, -- (Kurs[+30min]-price) x Lesart-Richtung / atr + ret60 REAL, + hit30 INTEGER, -- 1 = Kurs lief in Richtung der Lesart + hit60 INTEGER, + done INTEGER + ) + """, + """ CREATE TABLE IF NOT EXISTS verdict_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, -- lokale Epoch @@ -458,6 +486,63 @@ class HistoryLogger: conn.commit() return cur.lastrowid + def log_m15_state(self, *, status: str, veto, kosten, level, gap_atr, + p_break, lesart, price, atr) -> int: + """Loggt EINEN Zustandswechsel der M15-Karte. + + ⚠ Nur bei WECHSEL aufrufen, nicht je Tick — sonst entsteht dieselbe + Cluster-Verzerrung, die am 07.08. eine Untersuchung mit elf widerlegten + Hypothesen ausgeloest hat (roh 922 Zeilen mit 52,6 %, entkoppelt 93 mit + 45,2 %). Ausgewertet wird separat gegen `candles_m1`. + ⚠ Fail-open wie der Rest dieser Klasse: ein Logging-Fehler darf den + Handelspfad nie stoeren.""" + try: + with self._lock, self._connect() as conn: + cur = conn.execute(""" + INSERT INTO m15_states + (ts,status,veto,kosten,level,gap_atr,p_break,lesart,price,atr) + VALUES (?,?,?,?,?,?,?,?,?,?) + """, (int(time.time()), status or "", veto, + None if kosten is None else float(kosten), + None if level is None else float(level), + None if gap_atr is None else float(gap_atr), + None if p_break is None else int(p_break), + lesart, float(price or 0.0), float(atr or 0.0))) + conn.commit() + return cur.lastrowid + except Exception: + return 0 + + def m15_accuracy(self, last_n: int = 200) -> dict: + """Trefferquote der LESART (nicht des Status) ueber die letzten + ausgewerteten Zustaende mit gerichteter Lesart. + + ⚠ Gegen 50 % zu lesen, nicht gegen 0. Und ⚠ die Karte behauptet KEINE + freie Richtung — gemessen wird nur, ob die BEDINGTE Lesart am Level + („Abprall -> eher abwaerts") getroffen hat.""" + try: + with self._lock, self._connect() as conn: + rows = conn.execute(""" + SELECT hit30, hit60, ret30 FROM m15_states + WHERE done=1 AND lesart IN ('auf','ab') + ORDER BY id DESC LIMIT ? + """, (int(last_n),)).fetchall() + offen = conn.execute( + "SELECT COUNT(*) FROM m15_states WHERE done IS NULL " + "AND lesart IN ('auf','ab')").fetchone()[0] + except Exception: + return {"n": 0} + n = len(rows) + if not n: + return {"n": 0, "offen": offen} + h30 = [r[0] for r in rows if r[0] is not None] + h60 = [r[1] for r in rows if r[1] is not None] + r30 = [r[2] for r in rows if r[2] is not None] + return {"n": n, "offen": offen, + "wr30": round(100 * sum(h30) / len(h30)) if h30 else None, + "wr60": round(100 * sum(h60) / len(h60)) if h60 else None, + "avg30": round(sum(r30) / len(r30), 3) if r30 else None} + def rec_accuracy(self, last_n: int = 200) -> dict: """Trefferquote der letzten `last_n` ausgewerteten Empfehlungs-Episoden. diff --git a/web/app.js b/web/app.js index 63ce7ba..7dd24d4 100644 --- a/web/app.js +++ b/web/app.js @@ -252,6 +252,36 @@ function renderM15(s) { ? `⑤ gegen das Signal: ${g.n} Trades, ${f(g.pnl_lot, 2)} € je Lot` : "⑤ Veto: noch zu wenige Trades"; + // ── ⑥ Selbstmessung: die Karte zeigt ihre EIGENE Trefferquote ──────── + // ⚠ Gegen 50 % lesen. Gemessen wird die BEDINGTE Lesart am Level, nicht + // eine freie Prognose — die ist gemessen unmöglich (AUC 0,52 in-sample). + const tk = s.m15_track || {}; + const tEl = $("m15-track"); + if (tk.n >= 20 && tk.wr30 != null) { + tEl.className = "m15-item " + (tk.wr30 >= 55 ? "ok" : tk.wr30 <= 45 ? "warn" : ""); + tEl.textContent = `⑥ eigene Trefferquote (${tk.n} Zustände): ${tk.wr30} % auf 30′` + + (tk.wr60 != null ? ` · ${tk.wr60} % auf 60′` : "") + + ` · Ø ${tk.avg30 == null ? "—" : (tk.avg30 >= 0 ? "+" : "") + f(tk.avg30, 3)}×ATR`; + } else { + tEl.className = "m15-item"; + tEl.textContent = `⑥ eigene Trefferquote: sammelt noch` + + (tk.n ? ` (${tk.n} von 20 ausgewertet)` : "") + + (tk.offen ? ` · ${tk.offen} offen` : ""); + } + + // ── ⑦ Ausführungsqualität — der einzige gemessen grosse Hebel ──────── + const eg = s.entry_gap || {}; + const eEl = $("m15-exec"); + if (eg.n_seit_umbau != null) { + const genug = eg.n_seit_umbau >= (eg.soll || 12); + eEl.className = "m15-item " + (genug ? "ok" : ""); + eEl.textContent = `⑦ Ausführung: ${eg.n_seit_umbau} Bot-Trades seit dem ` + + `Stop-Order-Umbau (Prüfung ab ${eg.soll || 12})`; + } else { + eEl.className = "m15-item"; + eEl.textContent = "⑦ Ausführung —"; + } + // ── Status: die Kette wird der Reihe nach ausgewertet ──────────────── // ⚠ Das Banner nennt AUSDRUECKLICH keine Richtung. Es sagt, ob die // BEDINGUNGEN fuer eine Order am Level stehen — und was verboten ist. diff --git a/web/index.html b/web/index.html index fa44fae..c7874f0 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -109,6 +109,13 @@
+ +
+ +
@@ -340,6 +347,6 @@ - +