Files
AH-Oil-Trader/analyze_pbreak_live.py
T
Axel HocksandClaude Opus 5 92995b08d7 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>
2026-07-31 08:39:31 +02:00

146 lines
5.5 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
"""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()