#!/usr/bin/env python3
"""measurement_reminder.py — erinnert per Windows-Popup an FÄLLIGE Messungen.

Im Projekt sammeln mehrere Module still Daten für spätere Auswertungen
(`pbreak_predictions`, `verdict_votes`, `candles_m1`, AUTOSIG-/SQUEEZE-Trades …).
Ohne Erinnerung bleiben die liegen — genau das ist passiert: die P(break)-Prognose-
Genauigkeit war seit 2026-07-23 „später auswertbar" und hatte am 2026-07-30 bereits
2213 ausgewertete Zeilen.

Jeder Eintrag hat eine PRÜFBARE Fälligkeitsbedingung (Datenmenge oder Datum), nicht
nur ein Datum — so wird gemeldet, wenn die Messung wirklich belastbar ist.
Gemeldet wird je Messung genau einmal (Marker-Datei); ist sie erledigt, trägt man
sie hier aus oder löscht den Marker für eine Wiedervorlage.

Nutzung:  python measurement_reminder.py            (echt, mit Popup)
          python measurement_reminder.py --dry-run  (Statusliste, kein Popup/Marker)
          python measurement_reminder.py --status   (alle Punkte + Fortschritt)
"""
from __future__ import annotations
import os
import sqlite3
import sys
from datetime import datetime, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
DB = os.path.join(HERE, "oil_widget_history.db")
MARKER_DIR = os.path.join(HERE, ".reminders")
DRY = "--dry-run" in sys.argv
STATUS = "--status" in sys.argv


def _q(sql: str, default=0):
    try:
        with sqlite3.connect(DB, timeout=5) as c:
            r = c.execute(sql).fetchone()
        return r[0] if r and r[0] is not None else default
    except Exception:
        return default


def _days_since(iso: str) -> float:
    try:
        return (datetime.now() - datetime.fromisoformat(iso)).total_seconds() / 86400
    except Exception:
        return 0.0


# ── Die offenen Messungen ────────────────────────────────────────────────────
# have  = aktueller Stand · need = Schwelle · unit = Anzeigeeinheit
# cmd   = was zu tun ist, wenn fällig
CHECKS = [
    {
        # ⚠⚠ WIEDERVORLAGE aus dem Backtest vom 2026-08-12
        # (`backtest_srclose_reentry.py`). Die Frage des Users: wenn das
        # P(break)-Gate schliesst („Level haelt") und die Linie bricht doch —
        # lohnt der Wiedereinstieg? Ergebnis ueber 2 Halbjahre:
        #   0,3×ATR  H1 +0,119 [−0,138 … +0,387] · H2 +0,196 [+0,065 … +0,332]
        #   0,5×ATR  H1 −0,043               · H2 +0,043
        #   0,7×ATR  H1 +0,079               · H2 +0,014
        # Die vorab fixierte Regel ist verfehlt (H1s KI enthaelt die Null, und
        # der Nachbar 0,5 dreht H1 negativ). ABER: der OeR ist bei 0,3 in BEIDEN
        # Haelften positiv, und zwei gleichgerichtete Haelften sind mehr als eine
        # — „KI enthaelt 0" heisst nicht widerlegt, sondern nicht belegt.
        # ⚠ Der Backtest kann NICHT besser werden: H1 ist feste Historie, und die
        #   M5-Tiefe des Brokers ist bei 80k Bars gedeckelt. Was waechst, sind
        #   LIVE-Faelle — und die stehen bereits in `pbreak_predictions`
        #   (predicted='bounce' + outcome='break' = genau dieser Fall).
        # ⚠⚠ ENTKOPPELT ZAEHLEN — und das ist nicht kosmetisch. Beim Aufsetzen
        #   dieser Bedingung habe ich zuerst „roh 193 = entkoppelt 193" gemessen
        #   und daraus ~17 Faelle/Tag geschlossen. Falsch: `schluessel_level_stunde`
        #   ist eine FABRIK und muss AUFGERUFEN werden; uebergibt man sie selbst,
        #   ist jeder Schluessel ein neues Funktionsobjekt und es wird nichts
        #   dedupliziert. Richtig sind **55** Faelle = **~4,9/Tag**.
        #   (Die zwei echten Verwendungen in `analyze_divergence.py` rufen sie
        #   korrekt auf — nur die Ad-hoc-Auswertung war betroffen.)
        # SCHWELLE 500: loest einen wahren OeR ab ~0,118 auf (n > (1,96·1,35/OeR)²
        #   bei der gemessenen SD ~1,35) und deckt damit auch die UNTERE
        #   Backtest-Schaetzung (+0,12) ab. Bei ~4,9/Tag sind das **~3 Monate**.
        # ⚠ Bewusst NICHT kleiner: bei n=175 (~25 Tage) waere nur ein OeR ≥0,20
        #   aufloesbar — die Bedingung liefe ab, ohne entscheiden zu koennen.
        #   Genau dieser Fehler steckte in `pbreak_accuracy_v2` (Faktor 10 zu
        #   frueh faellig). Lieber spaet und entscheidbar als frueh und wertlos.
        "key": "srclose_reentry",
        "title": "Wiedereinstieg nach S/R-Close erneut prüfen (Live-Daten)",
        "why": "Backtest 2026-08-12 nicht entscheidbar: bei 0,3×ATR "
               "Bestätigung ØR +0,119/+0,196 — beidhälftig positiv, aber H1s "
               "95-%-KI enthält die Null und der Nachbar 0,5 kippt H1. Der "
               "Backtest kann nicht besser werden (H1 ist feste Historie); "
               "was wächst, sind Live-Fälle. Auswerten: die Fälle mit "
               "predicted='bounce' und outcome='break' gegen den kanonischen "
               "Exit rechnen (Vorlage: analyze_srclose_reentry.py) und gegen "
               "die Backtest-Erwartung +0,12…+0,20 halten.",
        "have": lambda: _q(
            "SELECT COUNT(*) FROM (SELECT DISTINCT ROUND(level,2), ts/3600 "
            "FROM pbreak_predictions WHERE predicted='bounce' "
            "AND outcome='break' AND ts >= 1785492000)"),   # 31.07.2026 12:00
        "need": 500,
        "unit": "entkoppelte Fälle 'Gate schliesst, Level bricht doch' ab 31.07.",
        "cmd": "python analyze_srclose_reentry.py   (bzw. auf pbreak_predictions erweitern)",
    },
    {
        # ERLEDIGT 2026-07-31 (analyze_pbreak_live.py, 2275 Vorhersagen): das Modell
        # hält live NICHT — AUC 0,539 statt 0,71, Kalibrierung sagt 8 % wo 39 %
        # eintreten, und die Bruchrate der geschlossenen Gruppe ist bei JEDER
        # Schwelle ~38 % → das Gate gatet nicht. Wiedervorlage mit HÖHERER Schwelle,
        # damit ein evtl. nachtrainiertes Modell auf frischen Daten geprüft wird.
        "key": "pbreak_accuracy_v2",
        "title": "P(break) erneut auswerten (nach Nachtraining)",
        "why": "Erste Auswertung 2026-07-31 fiel durch (AUC 0,539, Gate ohne "
               "Trennschärfe). Offene Baustelle: Touch-Zahl als 5. Merkmal in "
               "backtest_srclose_prob.py aufnehmen und neu fitten (live in beiden "
               "Hälften +6/+4 Pp für die 2.–4. Berührung, 5.+ fällt zurück). "
               "Danach mit frischen Live-Daten gegenprüfen.",
        # ⚠⚠ EINHEIT KORRIGIERT 2026-08-07 — die alte Bedingung zählte die FALSCHE
        # Größe. Sie stand auf 800 ROHEN Vorhersagen; die sind aber NICHT
        # unabhängig (dieselbe Position pendelt vielfach um dasselbe Level). Am
        # 07.08. war sie mit 878 „fällig" — entkoppelt (max. 1 je Level und
        # Stunde) waren es **87**, und das 95-%-Bootstrap-Intervall der AUC ging
        # von 0,434 bis 0,674: es enthält sowohl den Zufall (0,50) als auch die
        # Messlatte (0,654). Die Messung konnte also gar nichts entscheiden.
        # Die Abhängigkeit war im Skript seit jeher dokumentiert („entkoppelte
        # Sicht"), nur die Fälligkeit zählte daran vorbei — sie überschätzte die
        # Beweismenge um rund das Zehnfache.
        # 350 entkoppelte Zeilen ≈ halbe KI-Breite (0,12 → 0,06), erst dann sind
        # 0,50 und 0,65 überhaupt trennbar. Bei ~17/Tag sind das ~3 Wochen.
        "have": lambda: _q(
            "SELECT COUNT(*) FROM (SELECT DISTINCT ROUND(level,2), ts/3600 "
            "FROM pbreak_predictions WHERE outcome IS NOT NULL "
            "AND ts >= strftime('%s','2026-08-01'))"),
        "need": 350, "unit": "ENTKOPPELTE Vorhersagen seit 01.08. (max. 1 je Level und Stunde)",
        "cmd": "python analyze_pbreak_live.py",
    },
    {
        "key": "modul_audit_2",
        "title": "Modul-Audit: zweiter Durchgang",
        "why": "Am 2026-07-31 wurden an EINEM Tag 5 Deployment-Drift-Fälle (gemessen "
               "unter X, betrieben unter Y) UND 5 Modul-Inkonsistenzen gefunden — bei "
               "gezielter Suche in wenigen Stunden. Die Trefferquote spricht dafür, "
               "dass weitere existieren. `analyze_divergence.py` meldet die Klasse "
               "'Betrieb ≠ Messung' inzwischen selbst; die Klasse 'Modul-Inkonsistenz' "
               "(Gewichte, stale Quellen, tote Config, Sonderfälle) braucht dagegen "
               "einen bewussten Durchgang.",
        "have": lambda: _days_since("2026-07-31"),
        "need": 28, "unit": "Tage seit dem 1. Audit",
        "cmd": "python analyze_divergence.py 14  ·  dann Verdict-Gewichte gegen die "
               "Regel 'keine Aussage → Gewicht 0' prüfen, Config-Schlüssel gegen den "
               "Code, Level-Quellen gegen den aktuellen Kurs",
    },
    {
        "key": "verdict_votes",
        "title": "Verdict-Module auf Prädiktivität prüfen",
        "why": "`verdict_votes` loggt seit 2026-07-24 jede Modul-Stimme + Forward-"
               "Verlauf. Damit lässt sich datenbasiert entscheiden, welches Modul "
               "im Konsens bleibt, welches Gewicht bekommt — und welches raus muss "
               "(wie TU 2026-07-06). Auch die Doppelzählung M30/H1 wird hier messbar. "
               "⚠ OFFENE PUNKTE für diesen Lauf (Stand 2026-08-07, aus dem "
               "Zwischenstand über 15.544 Zeilen): "
               "(a) Liq-Trend (Gewicht 0,5, HL-abgeleitet) — +0,20/+0,32 über nur "
               "132 Episoden, also dünn. ⚠ Es stammt aus DERSELBEN Quelle wie das am "
               "06.08. entmachtete Orderbuch-Modul, und HL-Preisdaten laufen gemessen "
               "hinter Pepperstone her (Lead-Lag: 51 % Treffer nach 6 s). Als einziges "
               "verbliebenes HL-Modul mit Stimmrecht gehört es zuerst geprüft. "
               "(b) Muster (0,25) — +1,27/+0,67 bei n=88, zu dünn für ein Urteil. "
               "(c) H1 (1,5) — nur 19 Episoden, wechselt zu selten; trägt aber 38 % "
               "des Nadel-Einflusses, prädiktiv bisher NICHT beurteilbar. "
               "(d) Elliott und Orderbuch sind bereits entmachtet (used=False), werden "
               "aber WEITER geloggt — die Reihe darf für diese Messung nicht abreißen.",
        "have": lambda: _q("SELECT COUNT(*) FROM verdict_votes"),
        "need": 25000, "unit": "geloggte Verdicts (~3 Wochen)",
        "cmd": "python analyze_verdict_modules.py  (Episoden-Bildung beachten: "
               "die Rohzeilen zählen dieselbe Marktlage dutzendfach)",
    },
    # (ausgetragen 2026-08-01: „Auto-Signal-Entry B4" — der Pfad wurde ABGESCHALTET
    #  (`auto_signal=false`, Toggle + runtime_state). Gründe: gemessen H1 durchgehend
    #  negativ, und er konkurriert mit dem Auto-Squeeze um den EINEN Positions-Slot →
    #  solange beide laufen, ist die Squeeze-Erholung unten nicht sauber messbar.
    #  Wird er je reaktiviert, gilt wieder: ≥20 AUTOSIG-Trades, ØR gegen +0,14 halten.
    #  Der Zustand wird jetzt über CONFIG_DEPS['auto_signal'] überwacht.)
    {
        "key": "manual_ctx",
        "title": "Manuelle Trades: Stufe 2 — was trennt Gewinner von Verlierern?",
        "why": "Seit 2026-08-04 wird bei JEDEM Einstieg der volle Entscheidungszustand "
               "mitgeschrieben (16 `ctx_*`-Spalten: P(break) Ziel/Stop, Abstand zu S/R, "
               "Struktur, Squeeze, M30/H1, Spread/ATR, Session, block_reason, Konfidenz, "
               "ATR, Bias, Kegel-Position, Stunde). Vorher war die Marktlage eines "
               "manuellen Einstiegs NICHT rekonstruierbar — jede Aussage über die "
               "Diskretion des Users blieb Spekulation. ⚠ Die Vorprüfung zeigte: die "
               "sichtbaren Muster sind Einzeltage (Stunde 13:00 = +478 € über n=69, "
               "davon 80 % aus FÜNF Trades des 04.08.). Erst mit genug Kontext ist "
               "messbar, WORAN gute von schlechten Einstiegen zu unterscheiden sind.",
        "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE ctx_atr_m5 IS NOT NULL "
                           "AND pnl IS NOT NULL AND (setup IS NULL OR (setup NOT LIKE "
                           "'AUTOSIG%' AND setup NOT LIKE 'SQUEEZE%'))"),
        "need": 300, "unit": "manuelle Trades mit Kontext",
        "cmd": "Fit auf der ERSTEN Hälfte, validiert auf der ZWEITEN; AUC und "
               "Kalibrierung berichten (Methode wie backtest_pbreak_retrain.py). "
               "⚠ 300 reichen für eine ERSTE SICHTUNG, nicht für ein Urteil — mit "
               "~150 je Hälfte und mehreren Merkmalen ist die Overfit-Gefahr hoch; "
               "wenige Merkmale nehmen und das Ergebnis entsprechend vorsichtig lesen. "
               "Ziel ist ein HINWEIS im Order-Dialog ('deine Trades in dieser "
               "Konstellation: X % WR, n=Y'), ausdrücklich KEIN Auto-Entry.",
    },
    {
        # ⚠ VORAB festgelegte Abbruchregel (User-Zustimmung 2026-08-01) — bewusst VOR
        # der Beobachtung fixiert, damit die Latte hinterher nicht verschoben wird.
        "key": "squeeze_b5",
        "title": "Auto-Squeeze: B5-Abbruchprüfung (Latte vorab fixiert)",
        "why": "Live-Bilanz 17.–31.07.: n=31, ΣEUR −162,99, PF 0,49, Verhältnis 0,56 — "
               "während der B4-Monitor den EINSTIEG als intakt ausweist (Trefferquote "
               "live 45 % vs. Mechanik 46 %, ØR Mechanik +0,102). Die Lücke von ~0,34 R "
               "entstand also im EXIT, und alle drei Ursachen wurden am 31.07. behoben: "
               "P(break)-Modell nachtrainiert (schloss vorher 91 % aller Level-"
               "Berührungen = faktisch pauschaler S/R-Close), 15-Min-Regel aus, Trailing "
               "einheitlich 1,0 statt 1,5–3,0 je TF. Deshalb NICHT sofort zurückgebaut, "
               "sondern ein sauberes Fenster mit harter Latte.",
        "have": lambda: 20,          # ⚠ ENTSCHIEDEN — s. `cmd`
        "need": 20, "unit": "Squeeze-Trades seit 01.08. (ENTSCHIEDEN bei 11)",
        "cmd": "✅ ERLEDIGT 2026-08-17 — die Regel ist RECHNERISCH entschieden, "
               "nicht vertagt. Stand war 11/20: ØGewinn 13,62 gegen ØVerlust 94,15 "
               "= Verhältnis 0,14 (Latte 1,0), PF 0,39 (Latte 1,0), Σ −173,46 €. "
               "Nachgerechnet, was die 9 Rest-Trades leisten müssten: selbst im "
               "BESTEN Fall (9 von 9 Gewinnern, kein weiterer Verlust) bräuchte es "
               "Ø 165,72 € je Trade — das 12-fache des bisherigen Durchschnitts und "
               "2,1× den bisherigen REKORD (77,34 €). Mit Verlusten darunter steigt "
               "die Anforderung auf 175–223 €. ⚠ Die Latte ist damit nicht mehr "
               "erreichbar; auf 20 zu warten hätte den Zustand nie entschieden — "
               "und der Trader stand seit 11.08. ohnehin AUS, wodurch der Zähler "
               "eingefroren war (Deadlock). `auto_squeeze` bleibt FALSE. "
               "Wiederaufnahme wäre eine NEUE Entscheidung mit NEUER, vorab "
               "fixierter Latte — nicht die Fortsetzung dieser.",
    },
    {
        "key": "squeeze_entry_gap",
        "title": "Stop-Order-Umbau: kommt der Einstieg jetzt AM Level an?",
        "why": "Am 05.08. wurde der Auto-Squeeze von der Market-Order auf ruhende "
               "Stop-Orders am Ausbruchs-Level umgestellt. Grund: der Squeeze hat "
               "seinen GESAMTEN Edge im Einstiegspreis — am Level ØR +0,456/+0,611, "
               "hinterhergelaufen −0,158/−0,006 (`backtest_squeeze_entry.py`). Live "
               "lag der Abstand vorher im Median +0,275×ATR (Mittel +0,686, Max "
               "+4,339). Greift die Pending-Order nicht wie gedacht, ist der ganze "
               "Umbau wirkungslos — und das fiele sonst NICHT auf, weil der Bot "
               "weiterhin Trades macht.",
        # ⚠ Stichtag exakt auf den Umbau (05.08. 08:55 Berlin = 06:55 UTC), NICHT auf
        # Mitternacht: der Squeeze-Trade von 08:33 lief noch über die Market-Order und
        # würde die Zählung verfälschen. `strftime('%s', …)` liest UTC, `entry_time`
        # ist lokale Epoch — deshalb 07:00 UTC als sicherer Schnitt.
        # ⚠ Seit 05.08. laeuft AUCH der Signal-Pfad ueber Pending-Orders — beide
        # zaehlen, sonst wartet der Reminder auf eine Population, die vielleicht
        # nie kommt (der Squeeze feuert nur ~2-3x/Woche).
        "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE "
                           "(setup LIKE 'SQUEEZE%' OR setup LIKE 'AUTOSIG%') "
                           "AND entry_time >= strftime('%s','2026-08-05 07:00:00')"),
        "need": 12, "unit": "Bot-Trades (Squeeze+Signal) seit dem Umbau (05.08. 08:55)",
        "cmd": "python analyze_squeeze_entry_gap.py 60   →  Der Median-Abstand muss "
               "gegen 0 gehen und die Market-Log-Zeilen ('AUTO-SQUEEZE-ENTRY') "
               "sollten weitgehend verschwinden. Bleibt der Abstand bei ~0,3×ATR, "
               "füllt die Pending-Order nicht — dann Broker-Mindestabstand und die "
               "Log-Zeile 'Squeeze-Pending nicht platzierbar' prüfen.",
    },
    {
        "key": "night_window_0127",
        "title": "Nacht-Fenster 3–6: tragen die freigegebenen Stunden 0/1/2/7?",
        "why": "Am 05.08. wurde die Auto-Squeeze-Nachtsperre von 0–7 auf 3–6 verkürzt "
               "(`backtest_cost_gate.py`: keine Nachtstunde ist beidhälftig negativ, "
               "das alte Gate kostete ΣR −42/−243 R). Der Backtest modelliert aber "
               "KEINE Slippage — und genau die freigegebenen Stunden liegen in der "
               "dünnsten Liquidität (gemessener Tail: 0,75×ATR über den Stop hinaus). "
               "Das ist die einzige offene Bedrohung des Befunds, und sie lässt sich "
               "nur live prüfen.",
        "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' "
                           "AND pnl IS NOT NULL AND entry_time >= "
                           "strftime('%s','2026-08-05') AND "
                           "CAST(strftime('%H', entry_time, 'unixepoch', 'localtime') "
                           "AS INTEGER) IN (0,1,2,7)"),
        "need": 15, "unit": "Squeeze-Trades aus 0/1/2/7 Uhr seit 05.08.",
        "cmd": "Erwartung aus dem Backtest: diese Stunden gehören zu den besseren "
               "(Std 0 ist die STÄRKSTE des Tages, ØR +1,48/+1,51). Liegen sie live "
               "deutlich darunter oder im Minus, war die Slippage der Grund → "
               "`auto_squeeze_night_hours = 0,1,2,3,4,5,6,7` zurücksetzen.",
    },
    # (erledigt 2026-08-04: „M30-Level-Umstellung live gegenprüfen" — Ertrag JE LOT
    #  +17,53 → +36,92 €, Ø-Move je Trade 0,202 → 0,427 $, also verdoppelt wie im
    #  Backtest erwartet. ⚠ Normierung war entscheidend: unnormiert sah HEUTE wie der
    #  Treiber aus (Ø +55 € vs +19 €), je Lot ist heute aber praktisch identisch zum
    #  Rest (+38,81 vs +35,87) — der Unterschied war reine Positionsgröße.
    #  ⚠ NICHT der Level-Quelle allein zuzuordnen: am 31.07. kamen P(break)-Nachtraining
    #  und Schwelle 0,55→0,35 dazu; das Fenster mit NUR der Umstellung hat n=8.)
    # (erledigt 2026-07-30: Chartmuster-Kontrolltest gegen generischen Swing-Bruch —
    #  Muster schlagen die Kontrolle in beiden Hälften (+0,022/+0,108) → Gewicht 1,0)
    {
        "key": "m1_squeeze",
        "title": "M1-Squeeze (Variante B) backtesten",
        "why": "Der Squeeze läuft auf M5. Ob M1 früher/besser erkennt, ist offen — "
               "braucht genug eigene M1-History (candles_m1, Broker-History rollt weg).",
        "have": lambda: (lambda mn, mx: (mx - mn) / 86400 if mn else 0)(
            _q("SELECT MIN(time) FROM candles_m1"), _q("SELECT MAX(time) FROM candles_m1")),
        "need": 180, "unit": "Tage M1-History",
        "cmd": "backtest_breakout_squeeze.py auf M1-Basis aus candles_m1",
    },
    {
        "key": "news_score",
        "title": "News-Score auf Prädiktivität prüfen",
        "why": "`recommendations.news_score` wird seit 2026-07-24 wieder befüllt. "
               "Die Kalibrier-Messung hatte damals zu wenige Fälle (n=4/6). Mit "
               "genug Zeilen ist messbar, ob der News-Flow überhaupt etwas vorhersagt "
               "— aktuell ist er nur Kontext-Chip.",
        "have": lambda: _days_since("2026-07-24"),
        "need": 90, "unit": "Tage Sammlung",
        "cmd": "Forward-Return nach news_score-Bucket, 2 Hälften",
    },
]


# ── Config-abhängige Backtests ───────────────────────────────────────────────
# User-Frage 2026-07-31: „sollten wir alle verworfenen Backtests wöchentlich neu
# laufen lassen, da wir laufend die Config anpassen?"
#
# ⚠ NEIN — wöchentlich wäre eine Fehlalarm-Maschine: eine Woche M5 sind ~2000 Bars,
# und das Projekt hat dreimal erlebt, was daraus wird (Auto-Signal-Vorlauf auf 6k
# Bars zeigte „trägt", der 80k-Lauf drehte es; Std 13; ORB mit ØR +1,3/PF 10 als
# Selektions-Artefakt). Dazu der Mehrfachvergleich: 18 verworfene Ideen × 52 Wochen
# = 936 Tests/Jahr → bei 5 % Fehlalarmquote ~47 falsche „funktioniert jetzt!"/Jahr.
#
# ABER das Anliegen dahinter stimmt: einige Messungen sind gegen einen KONKRETEN
# Config-Wert kalibriert und werden ungültig, wenn der sich ändert. Das ist
# EREIGNIS-gesteuert, nicht kalendergesteuert — und genau das prüft dieser Block.
# (Die meisten verworfenen Tests sind config-UNABHÄNGIG: PDH/PDL, Volume Profile,
# Chartmuster, Liquidity Sweeps testen Signalklassen auf rohen Preisdaten.)
CONFIG_DEPS = [
    {
        # ⚠ Bewusste Abweichung von der Messung (User-Entscheidung 2026-08-07),
        # deshalb hier überwacht — wie `auto_sr_close`.
        "key": "auto_emergency_margin_pct", "test": "kontrafaktisch, 1022 Trades",
        "validated": "0",
        "why": "AKTIV auf 8 seit 2026-08-07 auf User-Wunsch. Kontrafaktisch über "
               "1022 echte Trades ist KEINE Schwelle besser als gar kein Stop: "
               "ohne Stop +358 € · 3 % −293 · 5 % −639 · 8 % −14 · 15 % −416 · "
               "30 % −331 (Δ gegen ohne). 8 % ist die mildeste Variante und "
               "praktisch neutral — sie schützt wenig und kostet wenig. Auch je "
               "Einstiegsart trägt keine Schwelle robust (bei 'ohne Signal' sieht "
               "8–10 % gut aus, aber beide Nachbarn kippen = Rauschen). "
               "⚠ Mechanismus aller Früh-Ausstiege hier: Trefferquote steigt, "
               "Ertrag fällt (Gewinner-Kappen) — fünfmal unabhängig gemessen. "
               "Zurück: auto_emergency_margin_pct = 0.",
    },
    {
        "key": "entry_room_atr", "test": "backtest_entryroom.py",
        "validated": "0.6",
        "why": "Das Raum-Gate ist auf diesen Schwellwert kalibriert (gemessen war 1,0 "
               "netto besser, 0,6 ist die bewusste Frequenz-Entscheidung). Es nutzt "
               "weiterhin die M5-`pb_levels` — eine Umstellung auf M30 wäre ungemessen.",
    },
    {
        "key": "sr_close_pbreak", "test": "backtest_pbreak_rvalue.py",
        "validated": "0.35",
        "why": "Seit dem Modell-Nachtraining 2026-07-31 (live-spiegelnde Stichprobe) "
               "ist 0,35 der in BEIDEN Hälften beste Wert gegen die Trailing-Baseline "
               "(H1 +357, H2 +1418 R). Bei 0,60 kippt H1. Die alte 0,55 gehörte zum "
               "alten Modell und ist mit den neuen _PB_W nicht mehr gültig.",
    },
    {
        "key": "breakout_k", "test": "backtest_breakout.py",
        "validated": "0.3",
        "why": "Split-validiert: 0,3 ist in BEIDEN Hälften das Beste; 0,5 und 1,0 waren "
               "in H1 netto NEGATIV. Ein anderer Wert ist nicht belegt.",
    },
    {
        "key": "risk_pct", "test": "backtest_sizing.py",
        "validated": "0",
        "why": "0 = margin-basiert. Wechsel auf >0 aktiviert einen anderen Sizing-Pfad "
               "(calc_lots_risk) — und koppelt den %-Notfall-Stop anders.",
    },
    {
        "key": "margin_buffer_pct", "test": "backtest_sizing.py",
        "validated": "95",
        "why": "Die Ruin-Simulation ist gegen den Margin-Anteil gerechnet (90 % = 36 % "
               "P(DD>80 %)). Ändert sich er, ändert sich das Ruin-Risiko.",
    },
    {
        "key": "adverse_15min_atr", "test": "backtest_adverse15_squeeze.py",
        "validated": "0",
        "why": "0 = AUS (2026-07-31). Nachgemessen mit dem kanonischen Exit über die "
               "ZEIT- und Schwellen-Achse (15/30/45/60 min × 0,5/0,8×ATR) auf BEIDEN "
               "Populationen: **alle 16 Kombinationen sind in beiden Hälften negativ**, "
               "nur monoton weniger schädlich je später/lockerer. Es gibt keine "
               "Einstellung, bei der die Regel hilft. Wiedereinschalten wäre nicht "
               "gedeckt — der Schutz läuft über Broker-SL 2×ATR, Trailing und Time-Stop.",
    },
    {
        "key": "auto_signal_min_conf", "test": "backtest_auto_signal.py",
        "validated": "75",
        "why": "Empirisch bester Punkt (NICHT weil höhere Konfidenz besser wäre — "
               "conf_pct ist unkalibriert). Nachbarwerte sind nicht besser.",
    },
    {
        "key": "reversal_enabled", "test": "backtest_reversal_angle.py",
        "validated": "false",
        "why": "AUS seit 2026-08-04. Der Reversal-Trigger ist in JEDER Variante "
               "beidhälftig negativ (IST −0,122/−0,031 · vorzeichenkorrigiert "
               "−0,173/−0,061 · ohne Winkelbedingung −0,099/−0,023) und hebelt dabei "
               "ZWEI gemessen positive Schutzmechanismen aus (Anti-Überdehnung und den "
               "M30-Gegen-Trend-Filter, Edge ×2). ⚠ Der Modul-Default in `wave_rec` ist "
               "bewusst True — die Backtests rufen dasselbe `_build`; abgeschaltet wird "
               "nur im Live-Pfad über diese Config.",
    },
    {
        # ⚠⚠ GEGENSTANDSLOS seit 2026-08-12: der Auto-Signal-Pfad ist KOMPLETT
        #    entfernt (Code, Endpunkt, Button, Snapshot-Felder). Der Schlüssel
        #    wird nirgends mehr gelesen — ein Wächter darüber könnte also nur
        #    noch Fehlalarm oder falsche Beruhigung liefern.
        #    Der Eintrag bleibt als STUMME Notiz stehen (`validated` = der
        #    Ist-Zustand), damit die Historie lesbar bleibt und niemand den
        #    Schlüssel versehentlich reaktiviert, ohne die Messlage zu kennen.
        "key": "auto_signal", "test": "backtest_signal_touchfill.py",
        "validated": "false",
        "why": "ENTFERNT 2026-08-12 (Pfad gelöscht, nicht nur abgeschaltet). "
               "Gemessen negativ auf BEIDEN Ausführungs-Achsen: Markt-Einstieg "
               "in allen 8 Feldern (backtest_auto_signal.py 30.07.), ruhende "
               "Order mit Touch-Fill −0,392/−0,338 in allen 6 Feldern und damit "
               "SCHLECHTER als Markt (backtest_signal_touchfill.py 06.08.). "
               "Ursache strukturell: das Bestätigungs-Level `_pend` WANDERT. "
               "⚠ Die ini-Schlüssel bleiben stehen, werden aber nicht mehr "
               "gelesen — Reaktivieren ginge nur über die Git-Historie bzw. "
               ".removed_backup/auto_signal_2026-08-12.py.",
    },
    {
        "key": "auto_squeeze", "test": "backtest_breakout_squeeze.py",
        "validated": "true",
        "why": "Der EINZIGE 2-Stichproben-validierte Auto-Einstieg (ØR +0,14…+0,23). "
               "Bleibt an, aber unter der vorab fixierten B5-Abbruchregel (s. Messung "
               "'squeeze_b5'). Ebenfalls über runtime_state.json überschreibbar.",
    },
    {
        "key": "auto_flip_close", "test": "backtest_flipclose2.py",
        "validated": "false",
        "why": "ABGESCHALTET 2026-08-05. Jede Variante in BEIDEN Halbjahren negativ "
               "(Live-Wert 0,5xATR: -92 R / -160 R); Mechanismus = Gewinner-Kappen, "
               "die nachlaufende EMA dreht oft mitten im Pullback. Die Live-Bilanz "
               "sprach dagegen (+284 EUR aus 4 Ausloesungen), aber +101 EUR davon "
               "steckten in EINEM Trade am Ausreissertag 04.08. Wird der Schalter "
               "wieder auf true gesetzt, laeuft ein gemessen negativer Exit auf "
               "echtem Geld - deshalb hier ueberwacht.",
    },
    {
        "key": "signal_pending_entry", "test": "backtest_signal_touchfill.py",
        "validated": "false",
        "why": "ABGESCHALTET 2026-08-06. Gebaut am 05.08. auf +0,39/+0,36 — diese "
               "Zahl unterstellt aber Fill NUR dort, wo der Bar-CLOSE das Level "
               "bricht. Eine liegende Order fuellt bei der ERSTEN BERUEHRUNG. Mit "
               "der realistischen Annahme ist der Pfad in ALLEN SECHS Feldern "
               "negativ (-0,18 bis -0,48, PF 0,38-0,71) und damit sogar schlechter "
               "als der Markt-Einstieg — er faengt an einem WANDERNDEN Level "
               "systematisch die Fehlausbrueche ein (441 statt 328 Fills). "
               "⚠ NICHT verwechseln mit `squeeze_pending_entry`: dort STEHT die Box "
               "still, und der gleiche Test ist bestanden (+0,124/+0,292). "
               "Wieder auf true zu setzen hiesse, SIG ueber den gemessen "
               "schlechtesten Einstiegsweg zu handeln.",
    },
    {
        "key": "auto_sr_close", "test": "backtest_pbreak_rvalue.py",
        "validated": "true",
        "why": "ABGESCHALTET 2026-08-06 auf User-Wunsch — und das ist eine Abweichung "
               "von der Messung, deshalb hier überwacht. Mit dem am 31.07. "
               "nachtrainierten P(break)-Modell schlägt der gegatete S/R-Close die "
               "Trailing-Baseline in BEIDEN Hälften (bei der Live-Schwelle 0,35: "
               "H1 -149 gegen -506 = +357 R, H2 +1308 gegen -110 = +1418 R). "
               "⚠ Nicht zu verwechseln mit dem PAUSCHALEN S/R-Close, der 2x verworfen "
               "wurde — das P(break)-Gate ist der Unterschied. Was weiter schützt: "
               "Broker-SL 2xATR, Trailing, Time-Stop. Der Hinweis in der App und die "
               "Chart-Linien bleiben (nur die automatische Ausführung entfällt). "
               "⚠ Lebt AUCH in runtime_state.json (UI-Toggle) und wird von dort "
               "überschrieben — beim Zurückschalten beide Stellen prüfen.",
    },
    {
        "key": "dead_hours", "test": "backtest_hourly_split.py / backtest_realcosts.py",
        "validated": "",
        "why": "Leer = Gate AUS (User-Vorgabe, bewusst GEGEN die Messung). Wird es "
               "wieder gefüllt, gelten die gemessenen Stunden 0–7 + 12 + 16.",
    },
]


# ── TELEMETRIE-PULS ───────────────────────────────────────────────────────────
# ⚠⚠ ANLASS (2026-08-11, Review-Durchgang 4): `rec_outcomes` — die
#    Selbst-Kalibrierung der Empfehlung — stand seit dem Einbau am 06.08. bei
#    **0 Zeilen**. Ursache war ein falsches dict (`s.get("pb_feats")` auf dem
#    MT5Data- statt dem Wave-Snapshot) → `_atr` immer 0 → die Schreibbedingung
#    nie erfüllt. **Kein NameError** (ruff sieht nichts), und das umgebende
#    `except` loggt auf DEBUG, der Logger steht auf INFO. Fünf Tage still tot.
#    Aufgefallen NUR, weil die Tabelle mit 0 Zeilen neben ihren Schwestern stand.
# ➜ Diese Prüfung generalisiert den Fund: jede Telemetrie-Tabelle muss einen
#   PULS haben. Schweigt eine, während die anderen schreiben, ist der Logger tot.
# ⚠ Die Toleranz ist grosszügig (Faktor 20 der erwarteten Kadenz), damit ein
#   ruhiger Markt keinen Fehlalarm auslöst — ein Wächter, der oft warnt, wird
#   ignoriert (Lehre vom 02.08.).
# ⚠ Die Implementierung liegt in `core/history.py` (DB-Schicht) — BEIDE Nutzer
#   (dieses Popup und der Tagesreport in `engine._send_daily_report`) rufen
#   dieselbe Funktion. Zwei Kopien waeren die Divergenz-Falle, die im Projekt
#   dreimal zugeschlagen hat. Richtungsregel: Skripte importieren `core`.
def telemetrie_puls() -> list:
    """Welche Telemetrie-Tabelle schreibt nicht mehr? → Klartext-Zeilen."""
    try:
        from core.history import HistoryLogger
        return HistoryLogger(DB).telemetrie_puls()
    except Exception as e:
        return [f"Puls-Pruefung nicht moeglich: {e}"]


_PULS = ()   # Rueckwaerts-Kompatibilitaet fuer die Zaehl-Ausgabe unten


def _cfg_now() -> dict:
    """Liest NUR die überwachten [trading]-Keys. ⚠ Die ini enthält Live-Secrets —
    hier wird bewusst nichts anderes gelesen oder ausgegeben.

    ⚠⚠ WICHTIG (Fix 2026-08-01): Die ini ist NICHT die Quelle der Wahrheit für die
    UI-Schalter. `runtime_state.json` wird beim Start über die ini gelegt (engine
    `_load_runtime_state`) — der Wächter muss denselben Vorrang abbilden, sonst meldet
    er „alles auf dem gemessenen Stand", während live etwas anderes läuft.
    Real passiert: `auto_signal` stand in der ini auf `false`, lief aber wochenlang,
    weil ein Dashboard-Toggle `true` persistiert hatte — 4 der 7 Bot-Trades vom 31.07.
    stammten aus diesem Pfad, der gemessen in H1 negativ ist.
    """
    import configparser
    import json
    out: dict = {}
    try:
        c = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
        c.read(os.path.join(HERE, "oil_widget_config.ini"), encoding="utf-8")
        t = c["trading"] if c.has_section("trading") else {}
        for d in CONFIG_DEPS:
            out[d["key"]] = (t.get(d["key"], "") or "").strip()
    except Exception:
        pass
    # runtime_state.json überschreibt — exakt wie im Engine-Start.
    try:
        with open(os.path.join(HERE, "runtime_state.json"), encoding="utf-8") as f:
            rt = json.load(f) or {}
        for k, v in rt.items():
            if k in out:
                out[k] = ("true" if v else "false") if isinstance(v, bool) else str(v)
    except Exception:
        pass
    return out


def config_drift() -> list:
    """→ [(dep, ist, soll)] für alle Keys, deren Wert von der Validierung abweicht."""
    now = _cfg_now()
    drift = []
    for d in CONFIG_DEPS:
        cur = now.get(d["key"])
        if cur is None:
            continue
        try:                      # numerisch vergleichen, damit 0.60 == 0.6
            same = abs(float(cur) - float(d["validated"])) < 1e-9
        except (TypeError, ValueError):
            same = cur.lower() == d["validated"].lower()
        if not same:
            drift.append((d, cur, d["validated"]))
    return drift


def _popup(title: str, text: str) -> None:
    if DRY or STATUS:
        print(f"[POPUP] {title}\n{text}")
        return
    try:
        import ctypes
        ctypes.windll.user32.MessageBoxW(0, text, title, 0x40 | 0x1000)
    except Exception as e:
        print(f"Popup fehlgeschlagen: {e}\n{text}")


def main() -> None:
    os.makedirs(MARKER_DIR, exist_ok=True)
    due, pending = [], []
    for c in CHECKS:
        try:
            have = float(c["have"]())
        except Exception:
            have = 0.0
        ready = have >= c["need"]
        marker = os.path.join(MARKER_DIR, c["key"])
        if ready and (STATUS or DRY or not os.path.exists(marker)):
            due.append((c, have))
        elif ready:
            pending.append((c, have, "gemeldet"))
        else:
            pending.append((c, have, "sammelt"))

    # ── Config-Drift: welche Messung ist durch eine Config-Änderung veraltet?
    # Marker enthält den Wert → ändert der User später erneut, wird neu gemeldet.
    drift = config_drift()
    drift_new = []
    for d, cur, val in drift:
        marker = os.path.join(MARKER_DIR, f"cfg_{d['key']}")
        seen = ""
        try:
            with open(marker, encoding="utf-8") as f:
                seen = f.read().strip()
        except Exception:
            pass
        if STATUS or DRY or seen != cur:
            drift_new.append((d, cur, val, marker))

    if STATUS or DRY:
        print("=" * 78)
        print("  AUSSTEHENDE MESSUNGEN — Status")
        print("=" * 78)
        for c, have in due:
            print(f"\n  ✅ FÄLLIG: {c['title']}")
            print(f"     {have:.0f}/{c['need']} {c['unit']}")
            print(f"     → {c['cmd']}")
        for c, have, st in pending:
            pct = min(100, have / c["need"] * 100) if c["need"] else 0
            print(f"\n  ⏳ {c['title']}  [{st}]")
            print(f"     {have:.0f}/{c['need']} {c['unit']}  ({pct:.0f} %)")
        if not due:
            print("\n  (nichts fällig)")
        print("\n" + "=" * 78)
        print("  CONFIG-VERANKERUNG — sind die Messungen noch gültig?")
        print("=" * 78)
        if not drift:
            print(f"\n  ✅ alle {len(CONFIG_DEPS)} überwachten Werte stehen auf dem "
                  f"Stand, gegen den gemessen wurde")
        for d, cur, val, _m in drift_new:
            print(f"\n  ⚠ {d['key']}: ist '{cur}' — gemessen/validiert gegen '{val}'")
            print(f"     betroffen: {d['test']}")
            print(f"     {d['why']}")
        print("\n" + "=" * 78)
        print("  TELEMETRIE-PULS — schreibt jeder Logger noch?")
        print("=" * 78)
        puls = telemetrie_puls()
        if not puls:
            from core.history import HistoryLogger as _HL
            print(f"\n  ✅ alle {len(_HL.PULS)} Telemetrie-Tabellen schreiben")
        for x in puls:
            print(f"\n  ⚠ {x}")
        return

    if not due and not drift_new:
        return
    lines = []
    for c, have in due:
        lines.append(f"• {c['title']}\n    {have:.0f}/{c['need']} {c['unit']}\n"
                     f"    {c['why']}\n    -> {c['cmd']}")
    for d, cur, val, _m in drift_new:
        lines.append(f"• CONFIG geaendert: {d['key']} = '{cur}' "
                     f"(validiert gegen '{val}')\n"
                     f"    {d['why']}\n    -> neu rechnen: {d['test']}")
    head = []
    if due:
        head.append(f"{len(due)} Messung(en) faellig")
    if drift_new:
        head.append(f"{len(drift_new)} Config-Abweichung(en)")
    text = ("Offene Punkte:\n\n" + "\n\n".join(lines)
            + "\n\n⚠ Verworfene Backtests NICHT woechentlich neu rechnen — eine Woche "
              "ist Rauschen (~2000 Bars) und 936 Tests/Jahr erzeugen Fehlalarme.\n"
              "Neu rechnen nur bei Config-Aenderung (oben) oder wenn genug WIRKLICH "
              "neue Daten fuer eine dritte Stichprobe da sind (~quartalsweise).\n\n"
            + "Status: python measurement_reminder.py --status")
    _popup("Oil-Bot: " + " + ".join(head), text)
    for c, _ in due:
        try:
            open(os.path.join(MARKER_DIR, c["key"]), "w").close()
        except Exception:
            pass
    for _d, cur, _val, marker in drift_new:
        try:
            with open(marker, "w", encoding="utf-8") as f:
                f.write(cur)
        except Exception:
            pass


if __name__ == "__main__":
    main()
