Files
AH-Oil-Trader/analyze_pbreak_live.py
Axel HocksandClaude Opus 5 908e20a9d0 P(break) v2 live geprueft: Kalibrierung daneben, Trennschaerfe unentschieden
Faellige B4-Messung nach dem Nachtrainieren vom 31.07. (878 Vorhersagen ab
01.08.). Drei Befunde.

1) Das Skript mischte zwei Modelle. Es trennte bei der M30-Umstellung (30.07.),
nicht beim Nachtrainieren (31.07.) - "GESAMT" las altes und neues Modell
zusammen. Derselbe Fehler wie am 02.08. in analyze_divergence.py. Neu:
MODELL_V2 mit demselben Stichtag wie die Faelligkeitsbedingung.

2) Kalibrierung messbar daneben. Die Basisrate ist gewandert: Training 37,8 %,
live 53,2 %, stabil ueber vier volle Tage (51,4 / 54,1 / 56,4 / 53,8 % bei
n=122-276). Ein Modell, dessen Niveau an 37,8 % verankert ist, muss in einem
53-%-Regime systematisch zu niedrig sagen - genau das passiert. Oberste Klasse
invertiert (69 % vorhergesagt, 17 % real).

3) Trennschaerfe NICHT entscheidbar - und das ist der wichtigere Befund.
Entkoppelt n=87, AUC 0,556, 95-%-Bootstrap-KI [0,434 ... 0,674]. Das Intervall
enthaelt den Zufall (0,50) UND die Messlatte (0,654).

Die Faelligkeitsbedingung zaehlte die falsche Groesse: 800 ROHE Vorhersagen,
die aber nicht unabhaengig sind (dieselbe Position pendelt um dasselbe Level).
Aus 878 rohen werden 87 entkoppelte - die Bedingung ueberschaetzte die
Beweismenge um rund das Zehnfache und meldete "faellig", obwohl die Messung
nichts entscheiden konnte. Jetzt 350 ENTKOPPELTE (halbiert die KI-Breite).
Stand 87/350.

Operativ kein Notfall: auto_sr_close ist seit 06.08. false, das Modell
schliesst keine Trades. Es speist nur Anzeige, Chart-Linien und
Copilot-Kontext.

Nichts umgestellt - ein drittes Fitten derselben Bauart wuerde denselben Weg
gehen; der Befund zeigt aufs Niveau, nicht zwingend auf die Rangfolge.

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

181 lines
7.4 KiB
Python
Raw Permalink 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
"""P(break) LIVE-Auswertung — hält das Modell, was der Backtest versprach?
Fällige Messung aus `measurement_reminder.py` (seit 2026-07-23 werden alle
Level-Vorhersagen mitgeloggt; Stand 2026-07-31: 2275 ausgewertete Zeilen).
BACKTEST-ERWARTUNG (die Messlatte, `backtest_srclose_prob.py` / `backtest_level_tf.py`):
AUC out-of-sample 0,710,72
Kalibrierung pred 23 % → real 25 % · 38→36 · 58→59 · 86→87
Basisrate P(break) ~37 % (M5-Level) bzw. 39,4 % (M30-Level)
GEPRÜFT WIRD:
1. Basisrate + Trefferquote gegen die TRIVIALE Regel („immer Abpraller sagen") —
das ist der eigentliche Maßstab, nicht die nackte Trefferquote.
2. Trennschärfe (AUC) — trennt das Modell hohe von niedrigen Bruchraten?
3. Kalibrierung — trifft die vorhergesagte Rate die echte?
4. Split vor/nach der M30-Level-Umstellung (2026-07-30).
5. Split nach Richtung.
⚠ ABHÄNGIGKEIT DER STICHPROBEN: derselbe Level wird oft mehrfach berührt; die Zeilen
sind daher NICHT unabhängig. Deshalb zusätzlich eine entkoppelte Sicht (max. eine
Vorhersage je Level und Stunde) — kippt ein Befund dort, ist er ein Cluster-Artefakt.
"""
import sqlite3
import sys
from datetime import datetime
DB = "oil_widget_history.db"
M30_SWITCH = datetime(2026, 7, 30).timestamp()
# ⚠⚠ MODELL-EPOCHE (ergänzt 2026-08-07). Am 31.07. wurde das P(break)-Modell
# NACHTRAINIERT (`backtest_pbreak_retrain.py`) und die Schwelle von 0,55 auf 0,35
# gesenkt. Die Tabelle `pbreak_predictions` enthält beides — Vorhersagen des ALTEN
# und des NEUEN Modells. Wer sie zusammen auswertet, misst eine Mischung und liest
# die Fehlkalibrierung des alten Modells als aktuellen Alarm.
# Derselbe Fehler wurde am 02.08. in `analyze_divergence.py` behoben (`_EPOCHS`);
# hier stand er noch. Stichtag identisch zur Fälligkeitsbedingung in
# `measurement_reminder.py` ("pbreak_accuracy_v2", ts >= 2026-08-01), damit
# Erinnerung und Auswertung dieselbe Grundmenge zählen.
MODELL_V2 = datetime(2026, 8, 1).timestamp()
def auc(ps, ys):
"""Rangbasiert, mit Bindungskorrektur."""
pairs = sorted(zip(ps, ys))
pos = sum(ys); neg = len(ys) - pos
if not pos or not neg:
return None
rank = 0.0; i = 0
while i < len(pairs):
j = i
while j + 1 < len(pairs) and pairs[j + 1][0] == pairs[i][0]:
j += 1
r = (i + j) / 2.0 + 1
for k in range(i, j + 1):
if pairs[k][1] == 1:
rank += r
i = j + 1
return (rank - pos * (pos + 1) / 2) / (pos * neg)
def report(rows, title):
"""rows = [(p_break_pct, is_break, predicted)]"""
n = len(rows)
if n < 30:
print(f"\n{title}: nur n={n} — zu wenig für eine Aussage")
return
br = sum(y for _, y, _ in rows) / n
acc = sum(1 for p, y, pr in rows
if (pr == "break") == (y == 1)) / n
# triviale Regel: immer die Mehrheitsklasse
trivial = max(br, 1 - br)
a = auc([p for p, _, _ in rows], [y for _, y, _ in rows])
print(f"\n{title} n={n}")
print(f" Basisrate Durchbruch {100*br:>5.1f} %")
print(f" Trefferquote Modell {100*acc:>5.1f} %")
print(f" ... triviale Regel {100*trivial:>5.1f} % "
f"(immer '{'break' if br > 0.5 else 'bounce'}')")
delta = 100 * (acc - trivial)
print(f" UEBERSCHUSS {delta:>+5.1f} Pp "
f"{'<-- Modell schlaegt die triviale Regel' if delta > 1 else ''}")
print(f" AUC (Trennschaerfe) {a:.3f}" if a else " AUC: n/a")
print(f" (Backtest-Erwartung 0,71)")
def calib(rows, title):
print(f"\n Kalibrierung — {title}")
print(f" {'P(break)':<14}{'n':>6}{'vorhergesagt':>14}{'real':>9}{'':>4}")
edges = [0, 20, 35, 50, 65, 101]
for lo, hi in zip(edges, edges[1:]):
sel = [(p, y) for p, y, _ in rows if lo <= p < hi]
if len(sel) < 25:
continue
pm = sum(p for p, _ in sel) / len(sel)
rm = 100 * sum(y for _, y in sel) / len(sel)
gap = rm - pm
flag = "OK" if abs(gap) <= 8 else ("zu niedrig" if gap > 0 else "zu hoch")
print(f" {lo:>3}{hi-1:<10}{len(sel):>6}{pm:>13.1f}%{rm:>8.1f}% {flag}")
def main():
db = sqlite3.connect(DB)
c = db.cursor()
c.execute("""SELECT ts, direction, level, p_break, predicted, outcome
FROM pbreak_predictions WHERE outcome IS NOT NULL
ORDER BY ts""")
raw = c.fetchall()
db.close()
if not raw:
print("keine ausgewerteten Vorhersagen"); return
rows = [(float(p), 1 if o == "break" else 0, pr) for _ts, _d, _l, p, pr, o in raw]
t0 = datetime.fromtimestamp(raw[0][0]); t1 = datetime.fromtimestamp(raw[-1][0])
print("=" * 84)
print(" P(break) — LIVE-AUSWERTUNG")
print(f" {len(rows)} ausgewertete Vorhersagen, {t0:%d.%m.%Y} bis {t1:%d.%m.%Y}")
print("=" * 84)
# ⚠ GESAMT mischt ALTES und NEUES Modell (s. MODELL_V2 oben) — bleibt als
# Historie stehen, ist aber NICHT der B4-Maßstab.
report(rows, "GESAMT ⚠ mischt altes + nachtrainiertes Modell")
calib(rows, "gesamt (gemischt)")
# ── DAS ist die fällige B4-Messung: nur das nachtrainierte Modell ──────
v2 = [(float(p), 1 if o == "break" else 0, pr)
for ts, _d, _l, p, pr, o in raw if ts >= MODELL_V2]
print("\n" + "=" * 84)
print(" NACHTRAINIERTES MODELL (ab 01.08.) — die fällige B4-Prüfung")
print(" Messlatte: AUC 0,654 (out-of-sample beim Nachtrainieren erreicht)")
print("=" * 84)
report(v2, "MODELL v2")
if len(v2) >= 30:
calib(v2, "Modell v2")
# entkoppelt auch hier — der Cluster-Effekt gilt unabhängig vom Modell
seen2 = set(); dd2 = []
for ts, _d, lvl, p, pr, o in raw:
if ts < MODELL_V2:
continue
key = (round(float(lvl or 0), 2), ts // 3600)
if key in seen2:
continue
seen2.add(key)
dd2.append((float(p), 1 if o == "break" else 0, pr))
report(dd2, "MODELL v2 · ENTKOPPELT (max. 1 je Level und Stunde)")
# ── entkoppelt: max. 1 Vorhersage je (Level, Stunde)
seen = set(); dedup = []
for ts, _d, lvl, p, pr, o in raw:
key = (round(float(lvl or 0), 2), ts // 3600)
if key in seen:
continue
seen.add(key)
dedup.append((float(p), 1 if o == "break" else 0, pr))
report(dedup, "ENTKOPPELT (max. 1 je Level und Stunde)")
# ── vor/nach der M30-Umstellung
pre = [(float(p), 1 if o == "break" else 0, pr)
for ts, _d, _l, p, pr, o in raw if ts < M30_SWITCH]
post = [(float(p), 1 if o == "break" else 0, pr)
for ts, _d, _l, p, pr, o in raw if ts >= M30_SWITCH]
report(pre, "M5-LEVEL (bis 29.07.)")
report(post, "M30-LEVEL (ab 30.07.)")
if len(post) >= 30:
calib(post, "nur M30-Level")
# ── nach Richtung
for d in ("LONG", "SHORT"):
sel = [(float(p), 1 if o == "break" else 0, pr)
for _ts, dd, _l, p, pr, o in raw if dd == d]
report(sel, f"Richtung {d}")
# ── Verteilung der Vorhersagen (wie oft sagt das Modell ueberhaupt 'break'?)
nb = sum(1 for _, _, pr in rows if pr == "break")
print(f"\n Prognose-Verteilung: 'break' {nb} ({100*nb/len(rows):.1f} %) · "
f"'bounce' {len(rows)-nb} ({100*(len(rows)-nb)/len(rows):.1f} %)")
if __name__ == "__main__":
main()