#!/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 = [ { # 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.", "have": lambda: _q("SELECT COUNT(*) FROM pbreak_predictions " "WHERE outcome IS NOT NULL AND ts >= strftime('%s','2026-08-01')"), "need": 800, "unit": "Vorhersagen seit 01.08.", "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.", "have": lambda: _q("SELECT COUNT(*) FROM verdict_votes"), "need": 25000, "unit": "geloggte Verdicts (~3 Wochen)", "cmd": "analog analyze_verdict_calibration.py, aber auf verdict_votes", }, { "key": "autosig_b4", "title": "Auto-Signal-Entry: B4-Monitor (trägt er live?)", "why": "Gemessen NICHT tragfähig (H1 −0,05…−0,18, H2 positiv = Regime-Kippen), " "auf ausdrücklichen Wunsch trotzdem gebaut. Erwartung: im aktuellen " "Regime ~+0,14 R/Trade, im ungünstigen −0,05. Driftet es negativ weg → " "`auto_signal=false` (B5-Rückbau).", "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'AUTOSIG%' " "AND pnl IS NOT NULL"), "need": 20, "unit": "AUTOSIG-Trades", "cmd": "Statistik-Tab · setup LIKE 'AUTOSIG%' gegen ØR +0,14 halten", }, { "key": "m30_levels", "title": "M30-Level-Umstellung live gegenprüfen", "why": "Seit 2026-07-30 kommen S/R-Level + P(break) aus M30 statt M5 " "(Backtest: R-Ertrag ~2×, AUC 0,721). Live gegenprüfen: bringt der " "S/R-Auto-Close jetzt mehr pro Trade als vorher?", "have": lambda: _q("SELECT COUNT(*) FROM trades WHERE closed_by='sr_close' " "AND pnl IS NOT NULL AND exit_time >= " "strftime('%s','2026-07-30')"), "need": 30, "unit": "S/R-Closes seit der Umstellung", "cmd": "sr_close-Trades vor/nach 2026-07-30 vergleichen (ØEUR, Trefferquote)", }, # (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 = [ { "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": "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.", }, ] def _cfg_now() -> dict: """Liest NUR die überwachten [trading]-Keys. ⚠ Die ini enthält Live-Secrets — hier wird bewusst nichts anderes gelesen oder ausgegeben.""" import configparser 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 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']}") 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()