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:
co-authored by
Claude Opus 5
parent
f81cc63dc7
commit
92995b08d7
@@ -1426,6 +1426,57 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
||||
Continuation = breakeven-negativ (−0,011/−0,005, Momentum-Klasse). Damit ist auch
|
||||
die Struktur-Idee empirisch durch — die Kachel bleibt Ist-Zustand-Anzeige (wie
|
||||
TF-Ampel/Squeeze), steuert nichts.
|
||||
- **⚠⚠ P(break) LIVE-AUSWERTUNG (`analyze_pbreak_live.py`, 2026-07-31, 2275 ausgewertete
|
||||
Vorhersagen seit 23.07.): DAS MODELL HÄLT LIVE NICHT, WAS DER BACKTEST VERSPRACH —
|
||||
B5-Fall.** Fällige Messung aus dem Reminder, jetzt erledigt.
|
||||
| | Gesamt | M5-Level (bis 29.07.) | M30-Level (ab 30.07.) | entkoppelt |
|
||||
|---|---|---|---|---|
|
||||
| n | 2275 | 1850 | 425 | 307 |
|
||||
| **AUC** | **0,539** | 0,570 | **0,399** | **0,443** |
|
||||
| Treffer Modell | 58,5 % | 58,7 % | 57,6 % | 56,7 % |
|
||||
| „immer Abprall" | 59,0 % | 57,7 % | 64,7 % | 59,9 % |
|
||||
| Überschuss | −0,5 Pp | +1,0 Pp | −7,1 Pp | −3,3 Pp |
|
||||
Backtest-Erwartung war **AUC 0,71**. **Kalibrierung systematisch nach unten verzerrt:**
|
||||
Bucket 0–19 % (n=1234, über die Hälfte!) sagt **8 % voraus, real 39,1 %**; Bucket
|
||||
65–100 % sagt 74,5 %, real 48,0 % (invertiert). Im Mittel prognostiziert das Modell
|
||||
**23 % Durchbruch, real sind es 41 %**, und es sagt nur in **8 % der Fälle „break"**.
|
||||
⚠ **DER KERNBEFUND — das Gate gatet nicht:** die **Bruchrate der GESCHLOSSENEN Gruppe
|
||||
ist bei JEDER Schwelle 38–40 %**, also identisch zur Basisrate (thr 20 → 39,1 % ·
|
||||
thr 35 → 37,8 % · thr 55 → 40,5 %). Es findet live keinerlei Auswahl statt. Bei der
|
||||
ini-Schwelle 0,55 werden **91,2 % aller Level-Berührungen geschlossen** → der
|
||||
„P(break)-gegatete Close" ist faktisch zum **pauschalen S/R-Close** degeneriert, und
|
||||
DER ist 2× gemessen und verworfen (`backtest_srclose.py`: WR verdoppelt, PF/ΣR in
|
||||
beiden Hälften schlechter = Gewinner-Kappen). **Das erklärt die kontrafaktische
|
||||
Live-Messung vom Vortag** (`analyze_srclose_live.py`: −8 €/Trade, 43 % klar zu früh
|
||||
geschlossen, nur 26 % klar richtig) — Symptom und Ursache passen zusammen.
|
||||
✅ **Pipeline gegen das Training geprüft, KEIN Bug gefunden:** `dist` ist in beiden
|
||||
entry-basiert (`abs(level−entry)/atr`, `backtest_srclose_prob.py:86`), `mom6`/`mom3`
|
||||
in beiden am Touch-Bar `jt`, `confirm`/`reject` dieselbe ±0,5×ATR-Definition, Timeout
|
||||
zählt in beiden als Abprall. Plausibelste Ursache = **andere Stichproben-Population**:
|
||||
live werden 2275 Berührungen geloggt, entkoppelt (max. 1 je Level und Stunde) bleiben
|
||||
nur **307** — dieselbe Position pendelt vielfach um dasselbe Level, live dominieren
|
||||
also Chop-am-Level-Fälle. ⚠ Das entlastet das Modell NICHT: entkoppelt ist die AUC mit
|
||||
0,443 sogar schlechter. ⚠ Der M30-Teil (AUC 0,399) ist mit n=425 über 2 Tage zu klein,
|
||||
um die Umstellung als Ursache zu belegen — die Wiedervorlage (≥30 S/R-Closes auf
|
||||
M30-Leveln) läuft weiter.
|
||||
**TOUCH-ZAHL als fehlendes Merkmal (User-Idee 2026-07-31 „je öfter der Kurs die Linie
|
||||
berührt, desto wahrscheinlicher der Durchbruch") — auf den Live-Daten BESTÄTIGT, aber
|
||||
mit Umkehr:** Bruchrate nach n-ter Berührung desselben Levels im 30-min-Fenster, in
|
||||
BEIDEN Live-Hälften dasselbe Muster — 1. Berührung 38,3/41,7 % · 2. 40,8/45,6 % ·
|
||||
3. 43,2/45,1 % · **4. 45,1/47,4 %** (monoton, +6/+4 Pp über Basis) · **5.+ fällt
|
||||
zurück auf 37,8/41,8 %** (UNTER Basis, und mit n≈1400 die größte Gruppe). Also ein
|
||||
**umgekehrtes U**, kein monotoner Anstieg: 2–4 Berührungen = Level wird mürbe, ab 5 in
|
||||
30 min = Range, das Level HÄLT. ⚠ Das Modell sieht davon **nichts** (sagt über alle
|
||||
Berührungszahlen konstant ~23 % voraus) → echte orthogonale Information. ⚠ Belastbarkeit
|
||||
begrenzt: die beiden „Hälften" liegen nur 4 Tage auseinander (kein Regimewechsel),
|
||||
Buckets n=82–139; und `backtest_srbreak.py` fand die Touch-Zahl früher „nicht robust"
|
||||
(allerdings ohne Fenster-Definition und ohne die 5+-Umkehr). **Nächster Schritt: als
|
||||
5. Merkmal in `backtest_srclose_prob.py` aufnehmen und über die 34.771 Touches / 2
|
||||
echten Halbjahre neu fitten** — dort ist messbar, ob die AUC steigt.
|
||||
**Empfehlung: `auto_sr_close=false`** (zurück auf reines
|
||||
Trailing = der gemessene Vergleichsmaßstab). Eine reine Schwellensenkung repariert es
|
||||
NICHT (sie ändert die Anzahl, nicht die Auswahl); 0,25–0,35 wäre nur ein Kompromiss,
|
||||
falls die Mechanik erhalten bleiben soll.
|
||||
- **P(Trendumkehr) = GEMESSEN, NICHT BERECHENBAR (`analyze_reversal.py`, 2026-07-31,
|
||||
User-Frage „können wir die Trendumkehr-Wahrscheinlichkeit berechnen?"):** Aufgebaut
|
||||
exakt wie das erfolgreiche P(break)-Modell — mechanisch scharfes **Barriere-Rennen**
|
||||
|
||||
@@ -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,71–0,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()
|
||||
+16
-8
@@ -50,14 +50,22 @@ def _days_since(iso: str) -> float:
|
||||
# cmd = was zu tun ist, wenn fällig
|
||||
CHECKS = [
|
||||
{
|
||||
"key": "pbreak_accuracy",
|
||||
"title": "P(break)-Prognose-Genauigkeit auswerten",
|
||||
"why": "Seit 2026-07-23 wird jede Level-Vorhersage mitgeloggt und gegen den "
|
||||
"echten Ausgang geprüft. Zeigt, ob das Modell live so trifft wie im "
|
||||
"Backtest (AUC 0,72) — und ob die M30-Umstellung es verbessert hat.",
|
||||
"have": lambda: _q("SELECT COUNT(*) FROM pbreak_predictions WHERE outcome IS NOT NULL"),
|
||||
"need": 500, "unit": "ausgewertete Vorhersagen",
|
||||
"cmd": "GET /api/pbreak_accuracy?period=all · history.pbreak_accuracy()",
|
||||
# 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",
|
||||
|
||||
Reference in New Issue
Block a user