#!/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()
