Files
AH-Oil-Trader/measurement_reminder.py
T
Axel HocksandClaude Opus 5 0944b1c13a P(break) neu trainiert auf live-spiegelnder Stichprobe (Weg b) - eingebaut
Behebt die am selben Tag gefundene Merkmals-Diskrepanz: das alte Modell war
auf "Anlauf zu einem beim Entry FIXIERTEN Level" trainiert, live waehlt
_draw_levels das Level jede Sekunde neu -> mom3 live 0,15 statt 1,45 ->
Ausgabe immer ~23 % -> Gate seit Inbetriebnahme faktisch nie aktiv.

backtest_pbreak_retrain.py: Stichprobe spiegelt den Live-Pfad (Level
dynamisch mit Hysterese, gesampelt an JEDEM Bar im 0,15xATR-Band, keine
Anlauf-Bedingung). Kontrolle: Merkmalsmittel der Stichprobe (mom6 0,203 /
mom3 0,202) treffen die live rekonstruierten Werte (0,181 / 0,149).

  AUC auf dieser Stichprobe (H2, out-of-sample):
      ALT-Modell  0,368   (schlechter als Zufall, 19,2 % vs real 39,4 %)
      NEU-Modell  0,654   kalibriert

backtest_pbreak_rvalue.py (R-Ertrag, Echtkosten, Baseline = nur Trailing):
NEU schlaegt ALT in BEIDEN Haelften bei JEDER Schwelle. Gegen die Baseline
gewinnt es bei 0,35/0,45/0,55 in beiden (bei 0,60 kippt H1).
Bestwert 0,35: H1 -149 vs -506 (+357), H2 +1308 vs -110 (+1418).
Nebenbefund: das ALTE Modell war in H1 schlechter als gar kein Auto-Close.

EINGEBAUT: neue _PB_MU/_SD/_W (final auf allen 80k gefittet, n=76.542,
Basisrate 37,8 %, nach H1->H2-Validierung; alte Werte als Kommentar) +
sr_close_pbreak 0,55 -> 0,35, Config-Waechter-Anker mitgezogen.
Verifiziert: Chart-Linien zeigen 43 %/36 % statt 8-15 %.

ACHTUNG - invertiert eine alte Projekt-Regel: mom3-Gewicht dreht das
Vorzeichen (+1,4330 -> -0,5420). Alt: "kriecht ans Level -> 26 % Bruch, mit
Schwung -> laufen lassen". Neu: Anlauf-Schub -> 26,5 % (Level absorbiert
ihn und haelt), Schwung weg vom Level -> 51 %. Beides gilt in seiner
Population (fixiertes vs. dynamisches Level); fuer den Live-Pfad gilt die
neue Lesart.

Offen: auch NEU schliesst bei 0,35 noch ~94-97 % der Positionen (waehlt vor
allem den besseren Moment). Touch-Zahl bringt im neuen Modell nichts mehr
(0,652 vs 0,654) - die dynamische Level-Wahl erfasst den Effekt bereits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:57:16 +02:00

340 lines
15 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": "verdict_votes",
"title": "Verdict-Module auf Prädiktivität prüfen",
"why": "`verdict_votes` loggt seit 2026-07-24 jede Modul-Stimme + Forward-"
"Verlauf. Damit lässt sich datenbasiert entscheiden, welches Modul "
"im Konsens bleibt, welches Gewicht bekommt — und welches raus muss "
"(wie TU 2026-07-06). Auch die Doppelzählung M30/H1 wird hier messbar.",
"have": lambda: _q("SELECT COUNT(*) FROM verdict_votes"),
"need": 25000, "unit": "geloggte Verdicts (~3 Wochen)",
"cmd": "analog analyze_verdict_calibration.py, aber auf verdict_votes",
},
{
"key": "autosig_b4",
"title": "Auto-Signal-Entry: B4-Monitor (trägt er live?)",
"why": "Gemessen NICHT tragfähig (H1 0,05…−0,18, H2 positiv = Regime-Kippen), "
"auf ausdrücklichen Wunsch trotzdem gebaut. Erwartung: im aktuellen "
"Regime ~+0,14 R/Trade, im ungünstigen 0,05. Driftet es negativ weg → "
"`auto_signal=false` (B5-Rückbau).",
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE setup LIKE 'AUTOSIG%' "
"AND pnl IS NOT NULL"),
"need": 20, "unit": "AUTOSIG-Trades",
"cmd": "Statistik-Tab · setup LIKE 'AUTOSIG%' gegen ØR +0,14 halten",
},
{
"key": "m30_levels",
"title": "M30-Level-Umstellung live gegenprüfen",
"why": "Seit 2026-07-30 kommen S/R-Level + P(break) aus M30 statt M5 "
"(Backtest: R-Ertrag ~2×, AUC 0,721). Live gegenprüfen: bringt der "
"S/R-Auto-Close jetzt mehr pro Trade als vorher?",
"have": lambda: _q("SELECT COUNT(*) FROM trades WHERE closed_by='sr_close' "
"AND pnl IS NOT NULL AND exit_time >= "
"strftime('%s','2026-07-30')"),
"need": 30, "unit": "S/R-Closes seit der Umstellung",
"cmd": "sr_close-Trades vor/nach 2026-07-30 vergleichen (ØEUR, Trefferquote)",
},
# (erledigt 2026-07-30: Chartmuster-Kontrolltest gegen generischen Swing-Bruch —
# Muster schlagen die Kontrolle in beiden Hälften (+0,022/+0,108) → Gewicht 1,0)
{
"key": "m1_squeeze",
"title": "M1-Squeeze (Variante B) backtesten",
"why": "Der Squeeze läuft auf M5. Ob M1 früher/besser erkennt, ist offen — "
"braucht genug eigene M1-History (candles_m1, Broker-History rollt weg).",
"have": lambda: (lambda mn, mx: (mx - mn) / 86400 if mn else 0)(
_q("SELECT MIN(time) FROM candles_m1"), _q("SELECT MAX(time) FROM candles_m1")),
"need": 180, "unit": "Tage M1-History",
"cmd": "backtest_breakout_squeeze.py auf M1-Basis aus candles_m1",
},
{
"key": "news_score",
"title": "News-Score auf Prädiktivität prüfen",
"why": "`recommendations.news_score` wird seit 2026-07-24 wieder befüllt. "
"Die Kalibrier-Messung hatte damals zu wenige Fälle (n=4/6). Mit "
"genug Zeilen ist messbar, ob der News-Flow überhaupt etwas vorhersagt "
"— aktuell ist er nur Kontext-Chip.",
"have": lambda: _days_since("2026-07-24"),
"need": 90, "unit": "Tage Sammlung",
"cmd": "Forward-Return nach news_score-Bucket, 2 Hälften",
},
]
# ── Config-abhängige Backtests ───────────────────────────────────────────────
# User-Frage 2026-07-31: „sollten wir alle verworfenen Backtests wöchentlich neu
# laufen lassen, da wir laufend die Config anpassen?"
#
# ⚠ NEIN — wöchentlich wäre eine Fehlalarm-Maschine: eine Woche M5 sind ~2000 Bars,
# und das Projekt hat dreimal erlebt, was daraus wird (Auto-Signal-Vorlauf auf 6k
# Bars zeigte „trägt", der 80k-Lauf drehte es; Std 13; ORB mit ØR +1,3/PF 10 als
# Selektions-Artefakt). Dazu der Mehrfachvergleich: 18 verworfene Ideen × 52 Wochen
# = 936 Tests/Jahr → bei 5 % Fehlalarmquote ~47 falsche „funktioniert jetzt!"/Jahr.
#
# ABER das Anliegen dahinter stimmt: einige Messungen sind gegen einen KONKRETEN
# Config-Wert kalibriert und werden ungültig, wenn der sich ändert. Das ist
# EREIGNIS-gesteuert, nicht kalendergesteuert — und genau das prüft dieser Block.
# (Die meisten verworfenen Tests sind config-UNABHÄNGIG: PDH/PDL, Volume Profile,
# Chartmuster, Liquidity Sweeps testen Signalklassen auf rohen Preisdaten.)
CONFIG_DEPS = [
{
"key": "entry_room_atr", "test": "backtest_entryroom.py",
"validated": "0.6",
"why": "Das Raum-Gate ist auf diesen Schwellwert kalibriert (gemessen war 1,0 "
"netto besser, 0,6 ist die bewusste Frequenz-Entscheidung). Es nutzt "
"weiterhin die M5-`pb_levels` — eine Umstellung auf M30 wäre ungemessen.",
},
{
"key": "sr_close_pbreak", "test": "backtest_pbreak_rvalue.py",
"validated": "0.35",
"why": "Seit dem Modell-Nachtraining 2026-07-31 (live-spiegelnde Stichprobe) "
"ist 0,35 der in BEIDEN Hälften beste Wert gegen die Trailing-Baseline "
"(H1 +357, H2 +1418 R). Bei 0,60 kippt H1. Die alte 0,55 gehörte zum "
"alten Modell und ist mit den neuen _PB_W nicht mehr gültig.",
},
{
"key": "breakout_k", "test": "backtest_breakout.py",
"validated": "0.3",
"why": "Split-validiert: 0,3 ist in BEIDEN Hälften das Beste; 0,5 und 1,0 waren "
"in H1 netto NEGATIV. Ein anderer Wert ist nicht belegt.",
},
{
"key": "risk_pct", "test": "backtest_sizing.py",
"validated": "0",
"why": "0 = margin-basiert. Wechsel auf >0 aktiviert einen anderen Sizing-Pfad "
"(calc_lots_risk) — und koppelt den %-Notfall-Stop anders.",
},
{
"key": "margin_buffer_pct", "test": "backtest_sizing.py",
"validated": "95",
"why": "Die Ruin-Simulation ist gegen den Margin-Anteil gerechnet (90 % = 36 % "
"P(DD>80 %)). Ändert sich er, ändert sich das Ruin-Risiko.",
},
{
"key": "adverse_15min_atr", "test": "backtest_auto_signal.py (15-Min-Achse)",
"validated": "0.5",
"why": "0,5 war die beste Regel-Variante; 0,0 (sofort im Minus schliessen) war "
"die SCHLECHTESTE (WR bricht 39→31 %). Andere Werte sind ungemessen.",
},
{
"key": "auto_signal_min_conf", "test": "backtest_auto_signal.py",
"validated": "75",
"why": "Empirisch bester Punkt (NICHT weil höhere Konfidenz besser wäre — "
"conf_pct ist unkalibriert). Nachbarwerte sind nicht besser.",
},
{
"key": "dead_hours", "test": "backtest_hourly_split.py / backtest_realcosts.py",
"validated": "",
"why": "Leer = Gate AUS (User-Vorgabe, bewusst GEGEN die Messung). Wird es "
"wieder gefüllt, gelten die gemessenen Stunden 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."""
import configparser
out: dict = {}
try:
c = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
c.read(os.path.join(HERE, "oil_widget_config.ini"), encoding="utf-8")
t = c["trading"] if c.has_section("trading") else {}
for d in CONFIG_DEPS:
out[d["key"]] = (t.get(d["key"], "") or "").strip()
except Exception:
pass
return out
def config_drift() -> list:
"""→ [(dep, ist, soll)] für alle Keys, deren Wert von der Validierung abweicht."""
now = _cfg_now()
drift = []
for d in CONFIG_DEPS:
cur = now.get(d["key"])
if cur is None:
continue
try: # numerisch vergleichen, damit 0.60 == 0.6
same = abs(float(cur) - float(d["validated"])) < 1e-9
except (TypeError, ValueError):
same = cur.lower() == d["validated"].lower()
if not same:
drift.append((d, cur, d["validated"]))
return drift
def _popup(title: str, text: str) -> None:
if DRY or STATUS:
print(f"[POPUP] {title}\n{text}")
return
try:
import ctypes
ctypes.windll.user32.MessageBoxW(0, text, title, 0x40 | 0x1000)
except Exception as e:
print(f"Popup fehlgeschlagen: {e}\n{text}")
def main() -> None:
os.makedirs(MARKER_DIR, exist_ok=True)
due, pending = [], []
for c in CHECKS:
try:
have = float(c["have"]())
except Exception:
have = 0.0
ready = have >= c["need"]
marker = os.path.join(MARKER_DIR, c["key"])
if ready and (STATUS or DRY or not os.path.exists(marker)):
due.append((c, have))
elif ready:
pending.append((c, have, "gemeldet"))
else:
pending.append((c, have, "sammelt"))
# ── Config-Drift: welche Messung ist durch eine Config-Änderung veraltet?
# Marker enthält den Wert → ändert der User später erneut, wird neu gemeldet.
drift = config_drift()
drift_new = []
for d, cur, val in drift:
marker = os.path.join(MARKER_DIR, f"cfg_{d['key']}")
seen = ""
try:
with open(marker, encoding="utf-8") as f:
seen = f.read().strip()
except Exception:
pass
if STATUS or DRY or seen != cur:
drift_new.append((d, cur, val, marker))
if STATUS or DRY:
print("=" * 78)
print(" AUSSTEHENDE MESSUNGEN — Status")
print("=" * 78)
for c, have in due:
print(f"\n ✅ FÄLLIG: {c['title']}")
print(f" {have:.0f}/{c['need']} {c['unit']}")
print(f" → {c['cmd']}")
for c, have, st in pending:
pct = min(100, have / c["need"] * 100) if c["need"] else 0
print(f"\n{c['title']} [{st}]")
print(f" {have:.0f}/{c['need']} {c['unit']} ({pct:.0f} %)")
if not due:
print("\n (nichts fällig)")
print("\n" + "=" * 78)
print(" CONFIG-VERANKERUNG — sind die Messungen noch gültig?")
print("=" * 78)
if not drift:
print(f"\n ✅ alle {len(CONFIG_DEPS)} überwachten Werte stehen auf dem "
f"Stand, gegen den gemessen wurde")
for d, cur, val, _m in drift_new:
print(f"\n{d['key']}: ist '{cur}' — gemessen/validiert gegen '{val}'")
print(f" betroffen: {d['test']}")
print(f" {d['why']}")
return
if not due and not drift_new:
return
lines = []
for c, have in due:
lines.append(f"• {c['title']}\n {have:.0f}/{c['need']} {c['unit']}\n"
f" {c['why']}\n -> {c['cmd']}")
for d, cur, val, _m in drift_new:
lines.append(f"• CONFIG geaendert: {d['key']} = '{cur}' "
f"(validiert gegen '{val}')\n"
f" {d['why']}\n -> neu rechnen: {d['test']}")
head = []
if due:
head.append(f"{len(due)} Messung(en) faellig")
if drift_new:
head.append(f"{len(drift_new)} Config-Abweichung(en)")
text = ("Offene Punkte:\n\n" + "\n\n".join(lines)
+ "\n\n⚠ Verworfene Backtests NICHT woechentlich neu rechnen — eine Woche "
"ist Rauschen (~2000 Bars) und 936 Tests/Jahr erzeugen Fehlalarme.\n"
"Neu rechnen nur bei Config-Aenderung (oben) oder wenn genug WIRKLICH "
"neue Daten fuer eine dritte Stichprobe da sind (~quartalsweise).\n\n"
+ "Status: python measurement_reminder.py --status")
_popup("Oil-Bot: " + " + ".join(head), text)
for c, _ in due:
try:
open(os.path.join(MARKER_DIR, c["key"]), "w").close()
except Exception:
pass
for _d, cur, _val, marker in drift_new:
try:
with open(marker, "w", encoding="utf-8") as f:
f.write(cur)
except Exception:
pass
if __name__ == "__main__":
main()