P(break) Live-Auswertung: Modell haelt nicht - das Gate gatet nicht (B5)

analyze_pbreak_live.py, 2275 ausgewertete Vorhersagen seit 23.07.

  AUC 0,539 (Backtest-Erwartung 0,71) · entkoppelt 0,443 · M30-Teil 0,399
  Trefferquote 58,5 % vs 59,0 % fuer "immer Abprall" -> Ueberschuss -0,5 Pp
  Kalibrierung: Bucket 0-19 % sagt 8 % voraus, real 39,1 % (Faktor 5);
  Bucket 65-100 % sagt 74,5 %, real 48,0 % (invertiert)

KERNBEFUND: die Bruchrate der GESCHLOSSENEN Gruppe ist bei JEDER Schwelle
38-40 %, identisch zur Basisrate. Es findet live keine Auswahl statt. Bei
der ini-Schwelle 0,55 werden 91,2 % aller Beruehrungen geschlossen -> der
"P(break)-gegatete Close" ist faktisch der pauschale S/R-Close, und DER ist
2x gemessen und verworfen (backtest_srclose.py). Erklaert die
kontrafaktische Messung vom Vortag (-8 EUR/Trade, 43 % zu frueh).

Pipeline gegen das Training geprueft, KEIN Bug: dist in beiden
entry-basiert, mom6/mom3 in beiden am Touch-Bar, confirm/reject dieselbe
+-0,5xATR-Definition, Timeout zaehlt in beiden als Abprall. Plausibelste
Ursache: andere Stichproben-Population (2275 roh -> 307 entkoppelt, live
dominieren Chop-am-Level-Faelle). Entlastet das Modell aber nicht -
entkoppelt ist die AUC sogar schlechter.

TOUCH-ZAHL (User-Idee): auf Live-Daten in BEIDEN Haelften bestaetigt, aber
mit Umkehr - 1. Beruehrung 38,3/41,7 %, 4. 45,1/47,4 % (monoton, +6/+4 Pp),
aber 5.+ faellt auf 37,8/41,8 % zurueck (unter Basis, groesste Gruppe).
Umgekehrtes U: 2-4 Beruehrungen machen das Level muerbe, ab 5 in 30 min ist
es eine Range und haelt. Das Modell sieht davon nichts (~23 % konstant) ->
echte orthogonale Information. Belastbarkeit begrenzt (Haelften nur 4 Tage
auseinander, n=82-139). Naechster Schritt: als 5. Merkmal in
backtest_srclose_prob.py aufnehmen, ueber 34.771 Touches / 2 Halbjahre
neu fitten.

Empfehlung: auto_sr_close=false. Eine reine Schwellensenkung repariert es
nicht (aendert die Anzahl, nicht die Auswahl).

Reminder: Punkt erledigt, Wiedervorlage als pbreak_accuracy_v2 (800
Vorhersagen ab 01.08., nach einem Nachtraining).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-31 08:39:31 +02:00
co-authored by Claude Opus 5
parent f81cc63dc7
commit 92995b08d7
3 changed files with 212 additions and 8 deletions
+145
View File
@@ -0,0 +1,145 @@
#!/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()
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)
report(rows, "GESAMT")
calib(rows, "gesamt")
# ── 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()