Files
AH-Oil-Trader/measurement_reminder.py
T
Axel HocksandClaude Opus 5 516f1eaa7f Roast-Punkte 1-3: Erfolgskontrolle erweitert, flip_close sichtbar, WARTEN-Massstab
korrigiert - und ein eigener Messfehler gefunden

(3) backtest_dist_gated.py: der 43-%-Massstab war UNGUELTIG. backtest_dist.py
misst _build ALLEIN, ohne Breakout-Bestaetigung und ohne Entry-Raum-Gate - genau
die beiden sind live die groessten Blocker. Ueber 80k Bars: nur _build 77,9 %
gerichtet, voller Live-Stack 19,5 %, live 7,2 %. Der reale Abstand ist 12,3 Pp,
nicht 50. Damit ist der "93 statt 43 %"-Alarm entschaerft, der den TF-Churn-Fix
ausgeloest hat; die dortige Erfolgskontrolle ist hinfaellig. Ein Rest bleibt
(min_conf live 16,3 % gegen 0,9 % in der Sim) und ist als naechster Ansatzpunkt
notiert.

⚠ Dabei EIGENEN Fehler gefunden: set_entry_room(0.6) fehlte in DREI heute neu
gebauten Skripten - _entry_room_atr ist im Konstruktor 0.0, _room_gate ist dann
ein No-op. Aufgefallen, weil entry_room in der Verteilung mit 0 % auftauchte
statt mit 35 %. Alle drei korrigiert und die SIG-Messung WIEDERHOLT: Population
aendert sich stark (n 967->295 / 1327->524), die Schlussfolgerung nicht - Market
in 7 von 8 Feldern negativ, am Level bestehen alle vier Schwellen
(OeR +0,392/+0,356, PF 2,37/2,39), Slippage-Test haelt bis 0,20 xATR. Die
heutige Bau-Entscheidung ist gedeckt.

(2) auto_flip_close ist jetzt sichtbar (#flip-note, v=139) - mit Schwelle,
Zaehler UND der Messung im Text ("gemessen in beiden Halbjahren negativ"). Ein
blanker Zaehler haette wie ein Erfolg ausgesehen. Der Exit bleibt an.

(1) analyze_squeeze_entry_gap.py erfasst jetzt BEIDE Bot-Pfade (Squeeze +
Signal), Reminder entsprechend erweitert - sonst wartet er auf eine Population,
die vielleicht nie kommt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:45:36 +02:00

491 lines
26 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.",
"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",
},
# (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 = [
{
"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": "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.",
},
]
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']}")
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()