Files
AH-Oil-Trader/measurement_reminder.py
T
Axel HocksandClaude Opus 5 dca3c11bbd Gesamtpruefung 2026-08-11: drei Defekte behoben
Zweiter vollstaendiger Durchlauf nach docs/review-prompt.md.

BEFUND 1: rec_outcomes war seit dem Einbau (06.08.) still tot - 0
Zeilen in 5 Tagen. _run_analysis las s.get("pb_feats"), aber s ist der
MT5Data-Snapshot; pb_feats lebt im Wave-Snapshot. _atr also immer 0,0
und die Schreibbedingung nie erfuellt. Kein NameError (ruff sieht
nichts), und das umgebende except loggt auf DEBUG. Aufgefallen nur,
weil die Tabelle mit 0 Zeilen neben ihren Schwestern stand.
Generalisiert als measurement_reminder.telemetrie_puls(): jede
Telemetrie-Tabelle muss einen Puls haben. Erster Lauf meldet genau
diesen Fund, alle uebrigen gruen.

BEFUND 2: der Divergenz-Waechter war selbst driftend. Abschnitt A
verglich gegen 43 % WARTEN - seit 05.08. ausdruecklich ungueltig (das
misst _build allein); richtig sind 80,5 %. Und entry_room galt als
"live-only", obwohl es seit 04./05.08. modelliert wird.
A: +50,8 Pp Warnung -> +13,3 Pp OK. B: 63,9 % -> 5,0 %.

BEFUND 3: P(break) ist wieder kalibriert (Delta -1,7 Pp nach +14,3 Pp
am 07.08.) - stuetzt den Schluss, dass die gewanderte Basisrate ein
Zeitraum-Effekt war.

Sauber: alle Standardpruefungen, 0 fehlende Frontend-IDs, mt5_lock,
Broker-Zeit, Race-Klasse. Performance: Snapshot-Median 15,9 ms, alle
heissen Abfragen ueber Index - keine Optimierung noetig.

Kein toter Code geloescht: die vermeintlich verwaisten Snapshot-Felder
sind Diagnose-Oberflaeche (vier davon heute zur Deploy-Verifikation
benutzt).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:31:42 +02:00

636 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.",
# ⚠⚠ 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,53,0 je TF. Deshalb NICHT sofort zurückgebaut, "
"sondern ein sauberes Fenster mit harter Latte.",
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' "
"AND pnl IS NOT NULL AND entry_time >= "
"strftime('%s','2026-08-01')"),
"need": 20, "unit": "Squeeze-Trades seit 01.08.",
"cmd": "ABBRUCHREGEL: Verhältnis (ØGewinn/ØVerlust) < 1,0 ODER PF < 1 "
"→ `auto_squeeze=false` (B5-Rückbau, kein Ego). Sonst weiterlaufen "
"lassen. Zahlen: /api/squeeze_monitor bzw. Statistik-Tab.",
},
{
"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 36: tragen die freigegebenen Stunden 0/1/2/7?",
"why": "Am 05.08. wurde die Auto-Squeeze-Nachtsperre von 07 auf 36 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 "
"810 % 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.",
},
{
"key": "auto_signal", "test": "backtest_auto_signal.py",
"validated": "false",
"why": "ABGESCHALTET 2026-08-01. Gemessen in H1 durchgehend negativ (alle 8 "
"Varianten, n=7181167/Hälfte), H2 positiv = Regime-Kippen. Zusätzlich: "
"beide Auto-Pfade konkurrieren um den EINEN Positions-Slot — läuft er "
"mit, ist die B5-Abbruchprüfung des Auto-Squeeze nicht sauber messbar. "
"⚠ Dieser Schlüssel lebt AUCH in runtime_state.json (UI-Toggle) und wird "
"von dort überschrieben — genau so lief er am 31.07. unbemerkt weiter, "
"obwohl die ini 'false' sagte.",
},
{
"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 07 + 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.).
_PULS = [
# (Tabelle, Zeitspalte, erwartete Kadenz in Minuten, wozu)
("recommendations", "timestamp", 1, "Empfehlungs-Log"),
("candles_m1", "time", 1, "M1-Kerzen-Sammlung"),
("verdict_votes", "ts", 1, "Modul-Stimmen"),
("m15_states", "ts", 60, "M15-Karte Selbstmessung"),
("cone_checks", "ts", 60, "Kegel-Kalibrierung (ACI)"),
("pbreak_predictions", "ts", 60, "P(break)-Prognose-Tracking"),
("rec_outcomes", "ts", 180, "Selbst-Kalibrierung Empfehlung"),
]
def telemetrie_puls() -> list:
"""Welche Telemetrie-Tabelle schreibt nicht mehr? → Liste von Klartext-Zeilen."""
import sqlite3
import time as _t
out = []
try:
db = sqlite3.connect(DB)
except Exception:
return out
jetzt = _t.time()
for tab, sp, kadenz, wozu in _PULS:
try:
n = db.execute(f"SELECT COUNT(*) FROM {tab}").fetchone()[0]
letzt = db.execute(f"SELECT MAX({sp}) FROM {tab}").fetchone()[0]
except Exception:
continue # Tabelle gibt es (noch) nicht
if not n:
out.append(f"{tab}: 0 Zeilen — der Logger hat NIE geschrieben ({wozu})")
continue
# ⚠ candles_m1.time ist ROHE Brokerzeit (UTC+3) — sonst 3 h Fehlalarm.
alter_min = (jetzt - (letzt - 10800 if tab == "candles_m1" else letzt)) / 60.0
if alter_min > kadenz * 20:
out.append(f"{tab}: letzte Zeile vor {alter_min/60:.1f} h "
f"(erwartet ~alle {kadenz} min) — {wozu}")
db.close()
return out
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:
print(f"\n ✅ alle {len(_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()