#!/usr/bin/env python3 """P(break) LIVE-Auswertung — hält das Modell, was der Backtest versprach? Fällige Messung aus `measurement_reminder.py` (seit 2026-07-23 werden alle Level-Vorhersagen mitgeloggt; Stand 2026-07-31: 2275 ausgewertete Zeilen). BACKTEST-ERWARTUNG (die Messlatte, `backtest_srclose_prob.py` / `backtest_level_tf.py`): AUC out-of-sample 0,71–0,72 Kalibrierung pred 23 % → real 25 % · 38→36 · 58→59 · 86→87 Basisrate P(break) ~37 % (M5-Level) bzw. 39,4 % (M30-Level) GEPRÜFT WIRD: 1. Basisrate + Trefferquote gegen die TRIVIALE Regel („immer Abpraller sagen") — das ist der eigentliche Maßstab, nicht die nackte Trefferquote. 2. Trennschärfe (AUC) — trennt das Modell hohe von niedrigen Bruchraten? 3. Kalibrierung — trifft die vorhergesagte Rate die echte? 4. Split vor/nach der M30-Level-Umstellung (2026-07-30). 5. Split nach Richtung. ⚠ ABHÄNGIGKEIT DER STICHPROBEN: derselbe Level wird oft mehrfach berührt; die Zeilen sind daher NICHT unabhängig. Deshalb zusätzlich eine entkoppelte Sicht (max. eine Vorhersage je Level und Stunde) — kippt ein Befund dort, ist er ein Cluster-Artefakt. """ import sqlite3 import sys from datetime import datetime DB = "oil_widget_history.db" M30_SWITCH = datetime(2026, 7, 30).timestamp() # ⚠⚠ MODELL-EPOCHE (ergänzt 2026-08-07). Am 31.07. wurde das P(break)-Modell # NACHTRAINIERT (`backtest_pbreak_retrain.py`) und die Schwelle von 0,55 auf 0,35 # gesenkt. Die Tabelle `pbreak_predictions` enthält beides — Vorhersagen des ALTEN # und des NEUEN Modells. Wer sie zusammen auswertet, misst eine Mischung und liest # die Fehlkalibrierung des alten Modells als aktuellen Alarm. # Derselbe Fehler wurde am 02.08. in `analyze_divergence.py` behoben (`_EPOCHS`); # hier stand er noch. Stichtag identisch zur Fälligkeitsbedingung in # `measurement_reminder.py` ("pbreak_accuracy_v2", ts >= 2026-08-01), damit # Erinnerung und Auswertung dieselbe Grundmenge zählen. MODELL_V2 = datetime(2026, 8, 1).timestamp() def auc(ps, ys): """Rangbasiert, mit Bindungskorrektur.""" pairs = sorted(zip(ps, ys)) pos = sum(ys); neg = len(ys) - pos if not pos or not neg: return None rank = 0.0; i = 0 while i < len(pairs): j = i while j + 1 < len(pairs) and pairs[j + 1][0] == pairs[i][0]: j += 1 r = (i + j) / 2.0 + 1 for k in range(i, j + 1): if pairs[k][1] == 1: rank += r i = j + 1 return (rank - pos * (pos + 1) / 2) / (pos * neg) def report(rows, title): """rows = [(p_break_pct, is_break, predicted)]""" n = len(rows) if n < 30: print(f"\n{title}: nur n={n} — zu wenig für eine Aussage") return br = sum(y for _, y, _ in rows) / n acc = sum(1 for p, y, pr in rows if (pr == "break") == (y == 1)) / n # triviale Regel: immer die Mehrheitsklasse trivial = max(br, 1 - br) a = auc([p for p, _, _ in rows], [y for _, y, _ in rows]) print(f"\n{title} n={n}") print(f" Basisrate Durchbruch {100*br:>5.1f} %") print(f" Trefferquote Modell {100*acc:>5.1f} %") print(f" ... triviale Regel {100*trivial:>5.1f} % " f"(immer '{'break' if br > 0.5 else 'bounce'}')") delta = 100 * (acc - trivial) print(f" UEBERSCHUSS {delta:>+5.1f} Pp " f"{'<-- Modell schlaegt die triviale Regel' if delta > 1 else ''}") print(f" AUC (Trennschaerfe) {a:.3f}" if a else " AUC: n/a") print(f" (Backtest-Erwartung 0,71)") def calib(rows, title): print(f"\n Kalibrierung — {title}") print(f" {'P(break)':<14}{'n':>6}{'vorhergesagt':>14}{'real':>9}{'':>4}") edges = [0, 20, 35, 50, 65, 101] for lo, hi in zip(edges, edges[1:]): sel = [(p, y) for p, y, _ in rows if lo <= p < hi] if len(sel) < 25: continue pm = sum(p for p, _ in sel) / len(sel) rm = 100 * sum(y for _, y in sel) / len(sel) gap = rm - pm flag = "OK" if abs(gap) <= 8 else ("zu niedrig" if gap > 0 else "zu hoch") print(f" {lo:>3}–{hi-1:<10}{len(sel):>6}{pm:>13.1f}%{rm:>8.1f}% {flag}") def main(): db = sqlite3.connect(DB) c = db.cursor() c.execute("""SELECT ts, direction, level, p_break, predicted, outcome FROM pbreak_predictions WHERE outcome IS NOT NULL ORDER BY ts""") raw = c.fetchall() db.close() if not raw: print("keine ausgewerteten Vorhersagen"); return rows = [(float(p), 1 if o == "break" else 0, pr) for _ts, _d, _l, p, pr, o in raw] t0 = datetime.fromtimestamp(raw[0][0]); t1 = datetime.fromtimestamp(raw[-1][0]) print("=" * 84) print(" P(break) — LIVE-AUSWERTUNG") print(f" {len(rows)} ausgewertete Vorhersagen, {t0:%d.%m.%Y} bis {t1:%d.%m.%Y}") print("=" * 84) # ⚠ GESAMT mischt ALTES und NEUES Modell (s. MODELL_V2 oben) — bleibt als # Historie stehen, ist aber NICHT der B4-Maßstab. report(rows, "GESAMT ⚠ mischt altes + nachtrainiertes Modell") calib(rows, "gesamt (gemischt)") # ── DAS ist die fällige B4-Messung: nur das nachtrainierte Modell ────── v2 = [(float(p), 1 if o == "break" else 0, pr) for ts, _d, _l, p, pr, o in raw if ts >= MODELL_V2] print("\n" + "=" * 84) print(" NACHTRAINIERTES MODELL (ab 01.08.) — die fällige B4-Prüfung") print(" Messlatte: AUC 0,654 (out-of-sample beim Nachtrainieren erreicht)") print("=" * 84) report(v2, "MODELL v2") if len(v2) >= 30: calib(v2, "Modell v2") # entkoppelt auch hier — der Cluster-Effekt gilt unabhängig vom Modell seen2 = set(); dd2 = [] for ts, _d, lvl, p, pr, o in raw: if ts < MODELL_V2: continue key = (round(float(lvl or 0), 2), ts // 3600) if key in seen2: continue seen2.add(key) dd2.append((float(p), 1 if o == "break" else 0, pr)) report(dd2, "MODELL v2 · ENTKOPPELT (max. 1 je Level und Stunde)") # ── entkoppelt: max. 1 Vorhersage je (Level, Stunde) seen = set(); dedup = [] for ts, _d, lvl, p, pr, o in raw: key = (round(float(lvl or 0), 2), ts // 3600) if key in seen: continue seen.add(key) dedup.append((float(p), 1 if o == "break" else 0, pr)) report(dedup, "ENTKOPPELT (max. 1 je Level und Stunde)") # ── vor/nach der M30-Umstellung pre = [(float(p), 1 if o == "break" else 0, pr) for ts, _d, _l, p, pr, o in raw if ts < M30_SWITCH] post = [(float(p), 1 if o == "break" else 0, pr) for ts, _d, _l, p, pr, o in raw if ts >= M30_SWITCH] report(pre, "M5-LEVEL (bis 29.07.)") report(post, "M30-LEVEL (ab 30.07.)") if len(post) >= 30: calib(post, "nur M30-Level") # ── nach Richtung for d in ("LONG", "SHORT"): sel = [(float(p), 1 if o == "break" else 0, pr) for _ts, dd, _l, p, pr, o in raw if dd == d] report(sel, f"Richtung {d}") # ── Verteilung der Vorhersagen (wie oft sagt das Modell ueberhaupt 'break'?) nb = sum(1 for _, _, pr in rows if pr == "break") print(f"\n Prognose-Verteilung: 'break' {nb} ({100*nb/len(rows):.1f} %) · " f"'bounce' {len(rows)-nb} ({100*(len(rows)-nb)/len(rows):.1f} %)") if __name__ == "__main__": main()