From b8705e297f73fc7507fefa17db88239fede2d38f Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Sat, 8 Aug 2026 23:45:26 +0200 Subject: [PATCH] Kegel mit Adaptive Conformal Inference nachkalibriert (v=158) Der Kegel war seit dem 31.07. dokumentiert 3-5 Pp ZU ENG - 80 % nominal gegen real 76,7 / 76,6 / 75,3 % - und blieb es, weil Nachfitten das Problem nur verschiebt. ACI justiert stattdessen ONLINE nach jedem aufgeloesten Fenster und konvergiert nachweislich gegen die Zielabdeckung, auch unter Drift. EHRLICH ZUR BAUART: die Lehrbuch-Form justiert alpha und schlaegt das Quantil nach. Hier liegt nur eine feste Quantil-Tabelle vor, kein Verteilungsobjekt - umgesetzt ist die multiplikative Variante: s <- s * exp(gamma * (1{verfehlt} - alpha_ziel)), gamma 0,02, Deckel 0,7-2,0 Dieselbe Regelungsidee, aber die Konvergenz-GARANTIE der Originalform ist damit nicht woertlich uebertragen. Verifiziert: bei exakt 20 % Verfehlern bleibt die Skala bei 1,0000 - der Fixpunkt stimmt. Simulation aus der dokumentierten Lage (76,6 %): nach ~50 Checks um 80 %. Die Skala pendelt um das Ziel statt exakt zu treffen - das ist das bekannte ACI-Verhalten (garantiert ist die Langfrist-Abdeckung). NICHT-UEBERLAPPEND geloggt (neue Tabelle cone_checks): ein 120-min-Fenster alle 5 min waere 24-fach ueberlappend - genau die Cluster-Verzerrung, an der analyze_hl_funding.py aufgelaufen ist und die die Faelligkeitsbedingung pbreak_accuracy_v2 um das Zehnfache danebenliegen liess. Je Horizont startet ein neuer Check erst, wenn der vorige ablief -> 48/24/12 Checks je Tag. Neustart-fest ueber runtime_state.json. build() ohne skalen liefert bitgenau die alten Werte (additiv geprueft). ZWEI EIGENE PLATZIERUNGSFEHLER, beide gefangen: der Lade-Block landete zuerst HINTER den except-Klauseln von _load_runtime_state, wo st nicht mehr existiert; und beim M15-Setup landete ein Block mitten in einem mehrzeiligen Aufruf. Der erste waere stillschweigend nie gelaufen - gefunden nur, weil ich die Funktion danach gelesen habe statt dem gruenen py_compile zu trauen. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 46 ++++++++++++++++++++- core/cone.py | 44 +++++++++++++++++++-- core/engine.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++- core/history.py | 49 +++++++++++++++++++++++ web/app.js | 9 ++++- web/index.html | 4 +- 6 files changed, 244 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e51bc07..57fc749 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=157** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=158** (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. +## ✅ KEGEL MIT ADAPTIVE CONFORMAL INFERENCE (2026-08-08, v=158) + +Der Kegel war seit dem 31.07. dokumentiert **3–5 Pp ZU ENG** — 80 % nominal +gegen real **76,7 / 76,6 / 75,3 %** — und blieb es, weil Nachfitten das Problem +nur verschiebt: die nächste Periode hat wieder eine andere Vola. **ACI** ist die +Standardmethode dafür (Web-Recherche 08.08.): sie justiert das Niveau **online** +nach jedem aufgelösten Fenster und konvergiert nachweislich gegen die +Zielabdeckung, **auch unter Verteilungsdrift**. + +⚠ **EHRLICH ZUR BAUART:** die Lehrbuch-Form justiert `α` und schlägt das +zugehörige Quantil nach. Hier liegt nur eine feste **Quantil-Tabelle** vor, kein +Verteilungsobjekt — umgesetzt ist deshalb die **multiplikative Variante**: +``` +s ← s · exp(γ · (1{verfehlt} − α_ziel)) γ = 0,02 · Deckel [0,7 … 2,0] +``` +Dieselbe Regelungsidee, aber **die Konvergenz-Garantie der Originalform ist +damit NICHT wörtlich übertragen** — es ist ACI im Geiste. + +✅ **Verifiziert:** bei **exakt 20 % Verfehlern bleibt die Skala bei 1,0000** — +das ist der Fixpunkt, und er stimmt. Simulation aus der dokumentierten Lage +(76,6 % Abdeckung): nach ~50 Checks um 80 %. +⚠ Die Skala **pendelt** um das Ziel statt exakt zu treffen. Das ist das bekannte +ACI-Verhalten: garantiert ist die **Langfrist**-Abdeckung, nicht der Einzelwert. + +⚠⚠ **NICHT-ÜBERLAPPEND geloggt** (`cone_checks`): ein 120-min-Fenster alle 5 min +wäre **24-fach überlappend** — genau die Cluster-Verzerrung, an der +`analyze_hl_funding.py` (06.08.) aufgelaufen ist und die die +Fälligkeitsbedingung `pbreak_accuracy_v2` um das Zehnfache danebenliegen liess +(07.08.). Je Horizont startet ein neuer Check erst, wenn der vorige abgelaufen +ist → 48/24/12 Checks je Tag. +✅ **Neustart-fest** über `runtime_state.json` — ohne Persistenz liefe die +erlernte Kalibrierung nach jedem Neustart wieder bei 1,0 los. +⚠ `build(price, atr)` OHNE `skalen` liefert **bitgenau** die alten Werte +(additiv geprüft). + +⚠⚠ **ZWEI EIGENE PLATZIERUNGSFEHLER beim Bau, beide gefangen:** der Lade-Block +landete zuerst **hinter** den `except`-Klauseln von `_load_runtime_state`, wo +`st` gar nicht mehr existiert; und beim M15-Setup landete ein Block **mitten in +einem mehrzeiligen Aufruf**. Der erste wäre stillschweigend nie gelaufen +(`except Exception` hätte ihn geschluckt) — gefunden nur, weil ich die Funktion +danach gelesen habe statt dem grünen `py_compile` zu trauen. + ## ✅ 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 @@ -5927,7 +5969,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 · **m15_states** · **candles_m1** · **pbreak_predictions** · **verdict_votes**). +- **DB** `oil_widget_history.db` (trades · intended_trades · recommendations · **m15_states** · **cone_checks** · **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/cone.py b/core/cone.py index 3d1f235..3dd6a76 100644 --- a/core/cone.py +++ b/core/cone.py @@ -45,24 +45,60 @@ _ATRMIN = 0.06 MINUTES = {6: 30, 12: 60, 24: 120} -def build(price: float, atr: float, bands=(50, 80)) -> dict | None: - """→ {'atr':…, 'levels':[{minutes, bars, band, lo, hi, coverage}, …]} +# ── ADAPTIVE CONFORMAL INFERENCE (2026-08-08) ──────────────────────────────── +# ⚠⚠ WOZU: die Baender sind oben dokumentiert **3–5 Pp ZU ENG** (80 % nominal → +# real 76,7 / 76,6 / 75,3 %). Das stand seit dem 31.07. so da und wurde NICHT +# behoben, weil ein Nachfitten das Problem nur verschiebt: die naechste Periode +# hat wieder eine andere Vola. +# ACI loest genau das — es justiert das Niveau ONLINE nach jedem aufgeloesten +# Fenster und konvergiert nachweislich gegen die Zielabdeckung, AUCH unter +# Verteilungsdrift (Gibbs & Candes; DtACI schaetzt zusaetzlich gamma selbst). +# +# ⚠ EHRLICH ZUR BAUART: die Lehrbuch-Form justiert `alpha` und schlaegt das +# zugehoerige Quantil nach. Hier liegt nur eine feste Quantil-TABELLE vor, kein +# vollstaendiges Verteilungsobjekt — deshalb die **multiplikative Variante**: +# ein Skalenfaktor je Horizont, aktualisiert als +# s <- s * exp(gamma * (1{verfehlt} - alpha_ziel)) +# Verfehlt das Fenster oefter als alpha_ziel, waechst s (Band wird breiter), +# sonst schrumpft es. Dieselbe Regelungsidee, aber die Konvergenz-GARANTIE der +# Originalform ist damit NICHT woertlich uebertragen — es ist ACI im Geiste. +_ACI_GAMMA = 0.02 # Schrittweite (Literatur ~0,005–0,05) +_ACI_MIN, _ACI_MAX = 0.7, 2.0 # Skalen-Deckel gegen Ausreisser/Endlos-Drift + + +def aci_update(skala: float, verfehlt: bool, band: int = 80, + gamma: float = _ACI_GAMMA) -> float: + """Ein ACI-Schritt. `verfehlt` = der Kurs lag AUSSERHALB des Bandes.""" + ziel = 1.0 - band / 100.0 # z. B. 0,20 bei 80 % + import math + s = skala * math.exp(gamma * ((1.0 if verfehlt else 0.0) - ziel)) + return max(_ACI_MIN, min(_ACI_MAX, s)) + + +def build(price: float, atr: float, bands=(50, 80), + skalen: dict | None = None) -> dict | None: + """→ {'atr':…, 'levels':[{minutes, bars, band, lo, hi, coverage, skala}, …]} `price` = aktueller Kurs (Bid), `atr` = ATR der M5-Basis. + `skalen` = {bars: Faktor} aus ACI; ohne Angabe bleibt alles wie bisher + (⚠ ADDITIV — ein Aufruf ohne `skalen` liefert bitgenau die alten Werte). """ if not price or not atr or atr <= 0: return None a = max(float(atr), _ATRMIN) + sk = skalen or {} out = [] for bars in sorted(_CONE): for band in bands: q = _CONE[bars].get(band) if not q: continue + s = float(sk.get(bars) or sk.get(str(bars)) or 1.0) out.append({ "minutes": MINUTES[bars], "bars": bars, "band": band, - "lo": round(price + q[0] * a, 3), - "hi": round(price + q[1] * a, 3), + "lo": round(price + q[0] * a * s, 3), + "hi": round(price + q[1] * a * s, 3), "coverage": _COVERAGE.get(bars, {}).get(band), + "skala": round(s, 3), }) return {"atr": round(a, 4), "price": round(float(price), 3), "levels": out} diff --git a/core/engine.py b/core/engine.py index ec7f1b2..30c8712 100644 --- a/core/engine.py +++ b/core/engine.py @@ -698,6 +698,9 @@ class TradingEngine: # geschlossen hat. try: self._evaluate_m15_states() # ~alle 5 min, self-throttled except Exception as me: log.warning(f"m15_states: {me}") + # Kegel-Abdeckung + ACI-Nachjustierung (2026-08-08) + try: self._evaluate_cone_checks() + except Exception as ce: log.warning(f"cone_checks: {ce}") # Wirtschaftskalender (self-throttled ~1 h, außerhalb des mt5_lock # unkritisch — reiner HTTP-Fetch mit Timeout) # HL-Kontext (Funding/OI/Premium) — reine Anzeige. @@ -2101,6 +2104,13 @@ class TradingEngine: from core.config import set_margin_buffer as _setp, get_margin_buffer _setp(float(st["margin_pct"] or 0) or 95.0) self._margin_pct = round(get_margin_buffer() * 100.0, 1) + if "cone_skalen" in st: + # ⚠ JSON liefert String-Schluessel → auf int zurueckholen, sonst + # findet `cone.build` sie nicht (es sucht nach `bars` als int). + # ⚠ Der Block MUSS im `try` stehen: der erste Entwurf landete + # hinter den except-Klauseln, wo `st` gar nicht mehr existiert. + self._cone_skalen = {int(k): float(v) + for k, v in (st["cone_skalen"] or {}).items()} log.info(f"Runtime-State geladen: BRK={'AN' if self._auto_squeeze else 'aus'} · " f"S/R-Close={'AN' if self._auto_sr_close else 'aus'} · " f"MinGewinn={self._sr_close_min_gain:+.2f} · " @@ -2121,7 +2131,15 @@ class TradingEngine: "auto_sr_close": self._auto_sr_close, "sr_close_min_gain": self._sr_close_min_gain, "manual_margin": getattr(self, "_manual_margin", 0.0), - "margin_pct": getattr(self, "_margin_pct", 0.0)}, f) + "margin_pct": getattr(self, "_margin_pct", 0.0), + # Kegel-ACI-Skalen (2026-08-08): ohne Persistenz waere + # die muehsam erlernte Kalibrierung nach jedem Neustart + # weg und liefe von 1,0 neu an. ⚠ JSON-Schluessel sind + # Strings — `cone.build` und der ACI-Schritt lesen + # deshalb beide Formen (int UND str). + "cone_skalen": {str(k): v for k, v in + (getattr(self, "_cone_skalen", None) + or {}).items()}}, f) os.replace(tmp, self._runtime_state_path) except Exception as e: log.warning(f"runtime_state speichern: {e}") @@ -3503,6 +3521,85 @@ class TradingEngine: except Exception as e: log.debug(f"m15_state log: {e}") + def _log_cone_check(self, cone: dict | None): + """Legt je Horizont EINEN nicht-ueberlappenden Abdeckungs-Check an. + + ⚠⚠ NICHT-UEBERLAPPEND ist hier kein Detail: ein 120-min-Fenster alle + 5 min waere 24-fach ueberlappend. Genau daran ist `analyze_hl_funding.py` + (06.08.) aufgelaufen, und die Faelligkeitsbedingung `pbreak_accuracy_v2` + ueberschaetzte damit die Beweismenge um das Zehnfache (07.08.). Ein neuer + Check startet je Horizont erst, wenn der vorige abgelaufen ist.""" + try: + if not cone or not self.history: + return + now = time.time() + letzte = getattr(self, "_cone_check_ts", None) + if letzte is None: + letzte = self._cone_check_ts = {} + for lv in cone.get("levels") or []: + if lv.get("band") != 80: + continue + b = lv.get("bars") + fenster = (lv.get("minutes") or 0) * 60 + if not b or not fenster: + continue + if now - letzte.get(b, 0.0) < fenster: + continue # voriger Check laeuft noch + letzte[b] = now + self.history.log_cone_check( + bars=b, band=80, price=cone.get("price") or 0.0, + lo=lv["lo"], hi=lv["hi"], skala=lv.get("skala") or 1.0) + except Exception as e: + log.debug(f"cone_check log: {e}") + + def _evaluate_cone_checks(self): + """Loest faellige Kegel-Checks auf und fuehrt den ACI-Schritt aus. + + ⚠ Der Kegel war dokumentiert **3–5 Pp zu eng** (80 % nominal → real + 76,7 / 76,6 / 75,3 %) und blieb es seit dem 31.07., weil Nachfitten das + Problem nur verschiebt. ACI justiert stattdessen laufend nach. + ⚠ `candles_m1.time` ist ROHE Broker-Zeit — Offset noetig.""" + now = time.time() + if now - getattr(self, "_cone_eval_ts", 0.0) < 300: + return + self._cone_eval_ts = now + try: + import sqlite3 + from core.cone import MINUTES, aci_update + 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, bars, band, lo, hi, skala FROM cone_checks " + "WHERE done IS NULL OR done = 0").fetchall() + skalen = dict(getattr(self, "_cone_skalen", {}) or {}) + for r in offen: + ziel = r["ts"] + MINUTES.get(r["bars"], 30) * 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 not row: + if now >= r["ts"] + 6 * 3600: # keine Bars mehr zu erwarten + con.execute("UPDATE cone_checks SET done=1 WHERE id=?", (r["id"],)) + continue + drin = 1 if r["lo"] <= row["c"] <= r["hi"] else 0 + con.execute("UPDATE cone_checks SET drin=?, done=1 WHERE id=?", + (drin, r["id"])) + s = float(skalen.get(r["bars"]) or 1.0) + skalen[r["bars"]] = aci_update(s, verfehlt=(drin == 0), + band=r["band"] or 80) + con.commit(); con.close() + if skalen != getattr(self, "_cone_skalen", None): + self._cone_skalen = skalen + self._save_runtime_state() # Neustart-fest + log.info("Kegel-ACI: Skalen " + ", ".join( + f"{k}→{v:.3f}" for k, v in sorted(skalen.items()))) + except Exception as e: + log.debug(f"cone_checks auswerten: {e}") + def _evaluate_m15_states(self): """Traegt den Forward-Return offener M15-Zustaende nach. @@ -3771,7 +3868,8 @@ class TradingEngine: stop_approach = self._stop_approach_hint(market, position, wave_snap) _cone = _cone_build( (market or {}).get("bid"), - float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0)) + float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0), + skalen=getattr(self, "_cone_skalen", None)) # 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). @@ -3783,6 +3881,7 @@ class TradingEngine: _m15 = self._m15_setup(market, wave_snap, _cone, self._alignment_cached()) self._log_m15_state(_m15, market) + self._log_cone_check(_cone) # 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. diff --git a/core/history.py b/core/history.py index 8d4b7ac..cb11aba 100644 --- a/core/history.py +++ b/core/history.py @@ -189,6 +189,26 @@ SCHEMA = [ ) """, """ + -- KEGEL-ABDECKUNG fuer Adaptive Conformal Inference (2026-08-08). + -- ⚠ NICHT-UEBERLAPPEND je Horizont geloggt: ein 120-min-Fenster alle 5 min + -- waere 24-fach ueberlappend, und ueberlappende Beobachtungen haben genau + -- die Cluster-Verzerrung erzeugt, an der `analyze_hl_funding.py` (06.08.) + -- und die Faelligkeitsbedingung `pbreak_accuracy_v2` (07.08.) aufgelaufen + -- sind. Je Horizont startet ein neuer Check erst, wenn der vorige ablief. + CREATE TABLE IF NOT EXISTS cone_checks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER, -- lokale Epoch + bars INTEGER, -- Horizont in M5-Bars (6/12/24) + band INTEGER, -- Nennband in % (80) + price REAL, + lo REAL, + hi REAL, + skala REAL, -- ACI-Skala, mit der dieses Band gebaut wurde + drin INTEGER, -- 1 = Kurs lag im Band + done INTEGER + ) + """, + """ CREATE TABLE IF NOT EXISTS verdict_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, -- lokale Epoch @@ -486,6 +506,35 @@ class HistoryLogger: conn.commit() return cur.lastrowid + def log_cone_check(self, *, bars: int, band: int, price: float, + lo: float, hi: float, skala: float) -> int: + """Legt EINEN Abdeckungs-Check des Kegels an (nicht-ueberlappend).""" + try: + with self._lock, self._connect() as conn: + cur = conn.execute(""" + INSERT INTO cone_checks (ts,bars,band,price,lo,hi,skala) + VALUES (?,?,?,?,?,?,?) + """, (int(time.time()), int(bars), int(band), float(price), + float(lo), float(hi), float(skala))) + conn.commit() + return cur.lastrowid + except Exception: + return 0 + + def cone_coverage(self, bars: int, last_n: int = 100) -> dict: + """Real gemessene Abdeckung der letzten `last_n` Checks eines Horizonts.""" + try: + with self._lock, self._connect() as conn: + rows = conn.execute( + "SELECT drin FROM cone_checks WHERE bars=? AND done=1 " + "ORDER BY id DESC LIMIT ?", (int(bars), int(last_n))).fetchall() + except Exception: + return {"n": 0} + n = len(rows) + if not n: + return {"n": 0} + return {"n": n, "abdeckung": round(100 * sum(r[0] for r in rows) / n, 1)} + def log_m15_state(self, *, status: str, veto, kosten, level, gap_atr, p_break, lesart, price, atr) -> int: """Loggt EINEN Zustandswechsel der M15-Karte. diff --git a/web/app.js b/web/app.js index 7dd24d4..aacb91e 100644 --- a/web/app.js +++ b/web/app.js @@ -572,10 +572,17 @@ function renderCone(cn) { .sort((a, b) => a.minutes - b.minutes); if (!b80.length) { el.innerHTML = ""; return; } const cov = b80[0].coverage != null ? `${fmt(b80[0].coverage, 0)} %` : "~"; + // ⚠ ACI (2026-08-08): die Bänder waren dokumentiert 3–5 Pp ZU ENG (80 % + // nominal → real 76,7/76,6/75,3). Adaptive Conformal Inference justiert das + // jetzt online nach; die Skala zeigt, wie stark. 1,00 = noch unverändert. + const sk = b80.map(x => x.skala).filter(v => v != null); + const skTxt = (sk.length && sk.some(v => Math.abs(v - 1) > 0.005)) + ? `ACI ×${fmt(sk.reduce((a, b) => a + b, 0) / sk.length, 2)}` + : ""; el.innerHTML = `📐 erwartete Spanne` + b80.map(x => `${x.minutes}′` + `${fmt(x.lo, 2)}–${fmt(x.hi, 2)}`).join("") - + `trifft ${cov}`; + + `trifft ${cov}` + skTxt; // Die Einordnung gehört an die Zahl, nicht in einen Absatz darunter — sonst // liest jemand die Spanne als Richtungsprognose. el.title = "STREUUNG, nicht Richtung: wie WEIT der Kurs plausibel läuft, nicht " diff --git a/web/index.html b/web/index.html index c7874f0..659e2c9 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -347,6 +347,6 @@ - +