#!/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 def _zaehle_setup(muster: str) -> int: """Geschlossene Trades mit `setup LIKE muster` (fuer die Abbruchregeln).""" try: import sqlite3 with sqlite3.connect(DB) as c: return c.execute( "SELECT COUNT(*) FROM trades WHERE setup LIKE ?" " AND exit_time IS NOT NULL AND pnl IS NOT NULL", (muster,) ).fetchone()[0] except Exception: return 0 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.", # ⚠⚠ EINHEIT KORRIGIERT 2026-08-17 — sie zählte die FALSCHE Größe. # Vorher: 25.000 ZEILEN. Die sind bei Minutentakt in 12 Tagen erreicht, # die auswertbare Einheit sind aber EPISODEN — und davon gab es je # Modul nur 50 (H1) bis 2.712 (Orderbuch). Dieselbe Klasse wie bei # `pbreak_accuracy_v2`, die roh statt entkoppelt zählte und die # Beweismenge um das Zehnfache überschätzte. # Gezählt werden jetzt Headline-WECHSEL als billiger Episoden-Proxy. # ⚠ Die Episodenzahl JE MODUL ist kleiner (17.08.: H1 nur 50 bei 254 # Headline-Episoden) — deshalb liegt die Schwelle höher als das Ziel # je Modul. Und der Vorbehalt des Skripts bleibt: wenige Wochen sind # EIN Regime, also Sichtung statt Urteil. "have": lambda: _q( "SELECT COUNT(*) FROM (SELECT headline, LAG(headline) OVER " "(ORDER BY ts) prev FROM verdict_votes) WHERE headline IS NOT prev"), "need": 400, "unit": "Headline-Episoden (Proxy; je Modul deutlich weniger)", "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.", }, { # Diese Regel MUSS hier stehen: sie wurde am 19.08. vorab fixiert, und # genau so eine Regel blieb bei B5 wochenlang unbemerkt, weil der Zaehler # nirgends lief. Ohne Ueberwachung ist eine Abbruchregel keine Regel. "key": "m15_abbruch", "title": "M15-Auto-Entry: Abbruchpruefung (Latte vorab fixiert)", "why": "Am 19.08. auf User-Wunsch GEGEN die Messung gebaut. " "backtest_m15_auto.py: die Karte ist in BEIDEN Halbjahren negativ " "(Lesart -0,162/-0,094; die jetzt gehandelte ERLAUBTE Richtung " "entspricht Kontrolle C mit -0,085/-0,030). Zusaetzlich ist der " "gebaute MARKT-Einstieg schlechter als der gemessene " "Level-Einstieg. Erwartung: die Latte greift.", "have": lambda: _zaehle_setup("AUTOM15%"), "need": 20, "unit": "AUTOM15_*-Trades", "cmd": "LATTE (vorab fixiert, NICHT verschiebbar): Verhaeltnis " "(OeGewinn/OeVerlust) < 1,0 ODER PF < 1 -> auto_m15 = false " "(Knopf M15). Dieselbe Latte wie squeeze_b5. Rechnung analog, " "nur setup LIKE 'AUTOM15%'.", }, { # ⚠ 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_m15", "test": "backtest_m15_auto.py", "soll": "false", "why": "Gemessen negativ (Lesart -0,162/-0,094; die gehandelte erlaubte " "Richtung entspricht Kontrolle C, -0,085/-0,030 -- ebenfalls " "negativ). Steht er auf 'true', laeuft ein gemessen negativer " "Auto-Entry auf echtem Geld. Das soll sichtbar bleiben.", }, { "key": "auto_squeeze", "test": "backtest_breakout_squeeze.py", # ⚠⚠ ERWARTUNG GEDREHT 2026-08-17: stand auf "true" und meldete deshalb # seit dem 11.08. taeglich eine Abweichung — obwohl FALSE inzwischen # der begruendete Zustand ist. Ein Waechter, der dauerhaft warnt, wird # ignoriert (dieselbe Lehre wie beim Divergenz-Waechter, 6 Tage # Dauerwarnung). Die B5-Regel ist am 17.08. RECHNERISCH gefallen: # Verhaeltnis 0,14 / PF 0,39 bei 11 Trades, und selbst 9 perfekte # Rest-Trades braeuchten Ø 165,72 EUR = 2,1x den bisherigen Rekord. "validated": "false", "why": "Der einzige 2-Stichproben-validierte Auto-Einstieg (ØR +0,14…+0,23) " "— aber die vorab fixierte B5-Abbruchregel ist am 2026-08-17 " "GEFALLEN (Verhaeltnis 0,14 gegen Latte 1,0, PF 0,39 gegen 1,0, " "Latte rechnerisch unerreichbar). Deshalb ist FALSE jetzt der " "validierte Zustand. ⚠ Ein Wiedereinschalten waere eine NEUE " "Entscheidung mit NEUER, vorab fixierter Latte — dann gehoert " "dieser Eintrag zurueck auf 'true' gedreht.", }, { "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 _auto_squeeze_an() -> bool: """Laeuft der Auto-Squeeze? (ini + runtime_state, Vorrang wie beim Start)""" try: from core.live_config import LiveConfig return bool(LiveConfig.laden().auto_squeeze) except Exception: return True # im Zweifel NICHT als ruhend melden # ── RUHENDE Messungen ──────────────────────────────────────────────────────── # ⚠⚠ ANLASS (2026-08-18): zwei Messungen brauchen SQUEEZE-Trades — und # `auto_squeeze` ist seit dem 11.08. aus, seit dem 17.08. auch begruendet # (B5-Regel gefallen). Ihre Zaehler stehen seither still: 8/12 und 3/15. # Als „sammelt (67 %)" angezeigt suggerieren sie einen Fortschritt, den es # nicht gibt — sie laufen NIE ab. # RUHEND heisst: die Messung ist nicht langsam, sie ist BLOCKIERT — und die # Blockade ist eine ENTSCHEIDUNG, keine Datenfrage. Wer den Trader wieder # einschaltet, laesst beide automatisch weiterlaufen; hier ist nichts zu tun. RUHT = { "squeeze_entry_gap": ( _auto_squeeze_an, "braucht SQUEEZE-Trades — `auto_squeeze` ist AUS (B5-Regel am 17.08. " "gefallen). Zaehler steht seit dem 11.08. still."), "night_window_0127": ( _auto_squeeze_an, "braucht SQUEEZE-Trades aus 0/1/2/7 Uhr — `auto_squeeze` ist AUS. " "Zaehler steht seit dem 11.08. still."), } def ruht(key: str): """(True, Grund) wenn die Messung blockiert ist, sonst (False, '').""" e = RUHT.get(key) if not e: return False, "" pruef, grund = e try: return (not pruef()), grund except Exception: return False, "" 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: _sch, _ = ruht(c["key"]) pending.append((c, have, "RUHT" if _sch else "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: if st == "RUHT": _, _grund = ruht(c["key"]) print(f"\n ⏸ {c['title']} [RUHT]") print(f" {have:.0f}/{c['need']} {c['unit']} — steht STILL") print(f" ⚠ {_grund}") continue 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()