analyze_squeeze_entry_gap.py fuehrt die zwei noetigen Quellen zusammen - das Ausbruchs-Level steht NUR im Log, der tatsaechliche Fill NUR in trades - und splittet am Umbau-Stichtag. Die Erfolgsmeldung ist paradox: eine per Pending gefuellte Order erzeugt KEINE AUTO-SQUEEZE-ENTRY-Logzeile (die entsteht nur im Market-Fallback), Squeeze-Trades ohne Log-Treffer sind also der Erfolg. Das Skript weist sie deshalb getrennt aus. Als Messung squeeze_entry_gap im measurement_reminder.py hinterlegt (>=12 Squeeze-Trades ab dem Umbau). Stichtag exakt 05.08. 08:55 statt Mitternacht - der 08:33-Trade lief noch ueber die Market-Order und haette die Zaehlung verfaelscht (0/12 statt faelschlich 1/12). Zahlen-Korrektur: die zuerst dokumentierten +0,306 / +0,741 xATR stammten aus einer Scratchpad-Auswertung mit FESTEM Stundenversatz. DST-korrekt ueber zoneinfo sind es +0,275 / +0,686 (n=34 unveraendert). In CLAUDE.md, engine.py, trader.py und der ini nachgezogen; die Schlussfolgerung aendert sich nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
487 lines
25 KiB
Python
487 lines
25 KiB
Python
#!/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,5–3,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.
|
||
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'SQUEEZE%' "
|
||
"AND entry_time >= strftime('%s','2026-08-05 07:00:00')"),
|
||
"need": 12, "unit": "Squeeze-Trades 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 = [
|
||
{
|
||
"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=718–1167/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 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.
|
||
|
||
⚠⚠ 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()
|