#!/usr/bin/env python3
"""P(break): liegt der Live-Fehler am NIVEAU (Basisrate) oder an der RANGFOLGE?

ANLASS (2026-08-07). Die B4-Prüfung des am 31.07. nachtrainierten Modells zeigt
zwei getrennte Dinge:
  · KALIBRIERUNG messbar daneben — Training-Basisrate 37,8 %, live 53,2 %,
    stabil über vier volle Tage. Alle unteren Klassen „zu niedrig".
  · TRENNSCHÄRFE nicht entscheidbar — entkoppelt n=87, AUC 0,556,
    95-%-KI [0,434 … 0,674] (enthält Zufall UND Messlatte).
Die Vermutung lautet deshalb: das Modell RANKT brauchbar, aber sein NIVEAU ist an
eine Basisrate genagelt, die wandert. Ein drittes Fitten derselben Bauart würde
daran nichts ändern — es würde nur einen neuen festen Achsenabschnitt lernen.

⚠ VORFRAGE, die über alles entscheidet: schwankt die Basisrate im Backtest-Zeitraum
überhaupt? Ist sie dort konstant, lässt sich das Problem an diesen Daten NICHT
studieren, und jede Variante wäre in-sample-Kosmetik. Wird zuerst geprüft und bei
Bedarf ausdrücklich gesagt.

GEPRÜFT WERDEN DREI VARIANTEN, alle auf derselben live-spiegelnden Stichprobe:
  A) BASIS      — 4 Merkmale, fester Achsenabschnitt (der heutige Stand)
  B) +REGIME    — 5. Merkmal: ATR relativ zum eigenen gleitenden Median
                  ⚠ RELATIV, nicht absolut. Ein absoluter ATR-Schwellwert wäre
                  ein Selektions-Artefakt (Lehre aus `analyze_tageszeit.py` und
                  dem ORB-Fehlschlag) und würde über Regime hinweg nicht gelten.
  C) ROLL-ACHSE — Merkmale wie A, aber der Achsenabschnitt wird laufend an die
                  zuletzt BEOBACHTETE Basisrate angepasst.
                  ⚠ KAUSAL: ein Ausgang steht erst `_BRK_W` Bars später fest,
                  deshalb Nachlauf. Ohne den wäre es Look-ahead — genau der
                  Fehler, der am 06.08. Momentum und SIG scheinbar rettete.

⚠⚠ ENTSCHEIDUNGSREGEL, VORAB FESTGELEGT (sonst verschiebt sich hinterher die Latte):
Eine Variante gilt nur dann als besser, wenn sie in BEIDEN Fit-Richtungen
(H1→H2 UND H2→H1)
    (1) den Kalibrierungsfehler senkt (mittlerer |vorhergesagt − real| über die
        Klassen, gewichtet nach n), UND
    (2) die AUC NICHT verschlechtert (Toleranz 0,005), UND
    (3) bei den Nachbar-Parametern nicht kippt (Fenster/Nachlauf).
Sonst: kein Einbau. Zwei Richtungen, weil eine einzelne Richtung schon einmal in
die Irre führte (`backtest_metalabel.py`, 05.08.).
"""
import sys

import numpy as np
import MetaTrader5 as mt5

from backtest_pbreak_retrain import (_ATRMIN, _BRK_W, _N_BARS, _atr_series,
                                     _ema_series, auc, build_levels, collect,
                                     fit_logreg, old_model)
from core.wave_rec import _EMA_FAST, _EMA_SLOW

_REG_FENSTER = 288      # gleitender ATR-Median (~1 Tag auf M5) für das Regime
_ROLL_N = 400           # wie viele zurückliegende Ausgänge die Achse nachführt
_ROLL_LAG = _BRK_W      # Nachlauf: so lange braucht ein Ausgang, bis er feststeht


def live_model(feat):
    """Das HEUTE laufende Modell — Gewichte direkt aus `core/engine.py`, damit die
    Kontrolle nicht von einer Kopie abhängt (Deployment-Drift wäre sonst genau
    hier möglich: gemessen mit anderen Zahlen als betrieben)."""
    from core.engine import _PB_MU, _PB_SD, _PB_W
    z = _PB_W[0]
    for k in range(4):
        z += _PB_W[k + 1] * (feat[k] - _PB_MU[k]) / _PB_SD[k]
    return 1 / (1 + np.exp(-z))


def kalib_fehler(p, y, kanten=(0.0, 0.2, 0.35, 0.5, 0.65, 1.01)):
    """Mittlerer |vorhergesagt − real| über die Klassen, nach n gewichtet.
    Das ist die Größe, die live danebenlag — nicht die AUC."""
    ges = 0.0
    n_ges = 0
    for a, b in zip(kanten, kanten[1:]):
        m = (p >= a) & (p < b)
        k = int(m.sum())
        if k < 20:
            continue
        ges += k * abs(p[m].mean() - y[m].mean())
        n_ges += k
    return ges / n_ges if n_ges else float("nan")


def zeig(titel, p, y):
    print(f"    {titel:<34} AUC {auc(p, y):.3f}   Ø-Vorhersage {100*p.mean():>5.1f} % "
          f"vs. real {100*y.mean():>5.1f} %   Kalibr.-Fehler {100*kalib_fehler(p, y):>5.2f} Pp")


def regime_reihe(AT):
    """ATR relativ zum eigenen gleitenden Median — regime-normiert, damit der Wert
    über verschiedene Vola-Niveaus dasselbe bedeutet. Kausal (nur Vergangenheit)."""
    out = [1.0] * len(AT)
    puffer = []
    for i, a in enumerate(AT):
        if a:
            puffer.append(a)
            if len(puffer) > _REG_FENSTER:
                puffer.pop(0)
            med = float(np.median(puffer))
            out[i] = (a / med) if med > 0 else 1.0
    return out


def roll_achse(p_roh, y, n_fenster=_ROLL_N, lag=_ROLL_LAG):
    """Verschiebt den Achsenabschnitt im Logit-Raum so, dass die mittlere
    Vorhersage der zuletzt BEOBACHTETEN Basisrate folgt.

    ⚠ Kausal: für Zeile i zählen nur Ausgänge bis i-lag. Vor dem ersten vollen
    Fenster bleibt die Vorhersage unverändert (kein Raten aus der Zukunft)."""
    p = np.clip(p_roh, 1e-6, 1 - 1e-6)
    logit = np.log(p / (1 - p))
    out = np.array(p_roh, dtype=float)
    for i in range(len(p)):
        a = i - lag - n_fenster
        b = i - lag
        if a < 0 or b <= a:
            continue
        beob = y[a:b].mean()
        erw = p_roh[a:b].mean()
        if not (0 < beob < 1) or not (0 < erw < 1):
            continue
        # Differenz der Logits der MITTELWERTE = Niveau-Korrektur
        delta = np.log(beob / (1 - beob)) - np.log(erw / (1 - erw))
        out[i] = 1 / (1 + np.exp(-(logit[i] + delta)))
    return out


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)
    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]
    EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
    AT = _atr_series(H, L, C); LV = build_levels(H, L)
    REG = regime_reihe(AT)
    mid = len(C) // 2

    print("=" * 104)
    print(f"  P(break) — NIVEAU oder RANGFOLGE?   {sym} M5, {len(C)} Bars")
    print("=" * 104)

    s1 = collect(_N_BARS, mid, H, L, C, EF, ES, AT, LV, REG)
    s2 = collect(mid, len(C), H, L, C, EF, ES, AT, LV, REG)
    if len(s1) < 500 or len(s2) < 500:
        print("zu wenige Stichproben"); return

    y1 = np.array([e["y"] for e in s1], float)
    y2 = np.array([e["y"] for e in s2], float)
    X1 = np.array([e["feat"] for e in s1]); X2 = np.array([e["feat"] for e in s2])

    # ── VORFRAGE: wandert die Basisrate in diesen Daten ueberhaupt? ────────
    print(f"\n  H1 n={len(s1)}  Basisrate {100*y1.mean():.1f} %")
    print(f"  H2 n={len(s2)}  Basisrate {100*y2.mean():.1f} %")
    print(f"  Live gemessen (ab 01.08.): 53,2 %   ·   ALT-Training: 37,8 %")
    print("\n  Basisrate in Zehnteln des Zeitraums (wandert sie?):")
    alle_y = np.concatenate([y1, y2])
    blk = len(alle_y) // 10
    raten = [100 * alle_y[i*blk:(i+1)*blk].mean() for i in range(10)]
    print("    " + " ".join(f"{r:5.1f}" for r in raten))
    print(f"    Spanne {min(raten):.1f} … {max(raten):.1f} %  "
          f"(Schwankung {max(raten)-min(raten):.1f} Pp)")
    if max(raten) - min(raten) < 8:
        print("    ⚠ Die Basisrate ist in diesen Daten weitgehend STABIL — der live")
        print("      beobachtete Drift (37,8 → 53,2 %) lässt sich hier nur begrenzt")
        print("      nachstellen. Die Ergebnisse unten sind entsprechend zu lesen.")

    # ── die drei Varianten, in BEIDEN Fit-Richtungen ───────────────────────
    print("\n" + "=" * 104)
    print("  VARIANTEN (vorab fixierte Regel: beide Richtungen besser kalibriert,")
    print("             AUC nicht schlechter, Nachbarn kippen nicht)")
    print("=" * 104)

    for name, (Xa, ya, Xb, yb, sa, sb) in (
            ("FIT H1 → TEST H2", (X1, y1, X2, y2, s1, s2)),
            ("FIT H2 → TEST H1", (X2, y2, X1, y1, s2, s1))):
        print(f"\n  {name}")
        w = fit_logreg(Xa, ya)
        pb = 1 / (1 + np.exp(-(np.hstack([np.ones((len(Xb), 1)), Xb]) @ w)))
        zeig("A) BASIS (fester Achsenabschnitt)", pb, yb)

        # B) +Regime als 5. Merkmal
        ra = np.array([[e["reg"]] for e in sa]); rb = np.array([[e["reg"]] for e in sb])
        w5 = fit_logreg(np.hstack([Xa, ra]), ya)
        p5 = 1 / (1 + np.exp(-(np.hstack([np.ones((len(Xb), 1)), Xb, rb]) @ w5)))
        zeig("B) + Regime (ATR/Median)", p5, yb)

        # C) rollierender Achsenabschnitt auf A
        pc = roll_achse(pb, yb)
        zeig(f"C) ROLL-ACHSE (n={_ROLL_N}, Nachlauf {_ROLL_LAG})", pc, yb)

        # Nachbarn von C
        for nf in (200, 800):
            zeig(f"   C-Nachbar n={nf}", roll_achse(pb, yb, n_fenster=nf), yb)

        # ── ZWEI Kontrollen, und sie sind NICHT dasselbe ──────────────────
        # ⚠ Beim ersten Lauf hatte ich `old_model` als „Modell wie live"
        # beschriftet — falsch. `old_model` sind die Gewichte VOR dem
        # Nachtrainieren (31.07.); live laufen seitdem `_PB_*` aus `engine.py`.
        p_old = np.array([old_model(e["feat"]) for e in sb])
        zeig("KONTROLLE 1: Modell VOR dem 31.07.", p_old, yb)
        p_live = np.array([live_model(e["feat"]) for e in sb])
        zeig("KONTROLLE 2: Modell wie HEUTE live", p_live, yb)


if __name__ == "__main__":
    main()
