From 64eef14871e0580423765775f060f854d9bff99d Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 6 Aug 2026 22:00:58 +0200 Subject: [PATCH] analyze_signal_live.py: die Richtungsaussage der Empfehlung ist ein Muenzwurf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-Beobachtung "die Empfehlungen heute waren fast immer gegenlaeufig" — vor einer Konzeptdebatte gemessen, und zwar an den TATSAECHLICH ausgegebenen recommendations-Zeilen (nach allen Live-Gates), nicht im Backtest. 20.365 Zeilen -> 1.071 Episoden (08.05.-06.08.), gegen die Drift der jeweiligen Haelfte gelesen. Trefferquote ueber alle sechs Felder 48,3-51,1 %. Bester Ueberschuss +0,085xATR bei 60 min — unter der B3-Huerde (0,1) und weit unter den realen M5-Kosten (0,265). Auf 120 min ist H2 mit -0,259 klar negativ. Der Anlasstag war typisch, kein Ausreisser: 8 von 10 Episoden auf 30 min gegenlaeufig; die fuenf Vormittags-SHORTs liefen gegen einen Tag, der von 75,5 auf 78 stieg. Wichtige Einordnung, damit daraus nicht der falsche Schluss gezogen wird: dasselbe Signal ist mit ruhender Order am Bestaetigungs-Level in beiden Haelften klar positiv (+0,35/+0,32, PF 2,2) und am Markt negativ. Der Ertrag kommt aus Einstiegspreis und Exit-Asymmetrie, nicht aus der Trefferquote — wie beim Squeeze (43-46 % Treffer, trotzdem positiv). Die Headline ist damit kein Kursurteil, sondern ein Ausloeser fuer eine Level-Order. Die Suche nach einem besseren Richtungssignal folgt daraus NICHT — die ist mit 23 verworfenen Eingriffen beantwortet. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 35 +++++++++ analyze_signal_live.py | 157 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 analyze_signal_live.py diff --git a/CLAUDE.md b/CLAUDE.md index 7c07439..e135817 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -270,6 +270,41 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: (das hatte ~0 Edge). Totband (Chop→WARTEN), Anti-Überdehnung (`_STRETCH_MAX=3,5×ATR`), +S/R-Konfidenz, +Session-Konfidenz. (TU **entfernt** — s. u.) +- **⚠⚠ DIE RICHTUNGS-AUSSAGE DER LIVE-EMPFEHLUNG IST EIN MÜNZWURF — an den LIVE + ausgegebenen Zeilen gemessen (`analyze_signal_live.py`, 2026-08-06, User-Frage + „die Empfehlungen heute waren fast immer gegenläufig, sollten wir das Konzept + überdenken?").** Kein Backtest: ausgewertet wurden die **tatsächlich + ausgegebenen** `recommendations`-Zeilen (also nach ALLEN Gates) gegen den echten + Kursverlauf. 20.365 Zeilen → **1.071 Episoden** (08.05.–06.08.; die Rohzeilen + zählen dieselbe Marktlage dutzendfach, deshalb Episoden), gegen die **Drift** + der jeweiligen Hälfte gelesen. + | Horizont | H1 Überschuss / Treffer | H2 Überschuss / Treffer | + |---|---|---| + | 30 min | +0,079×ATR / 49,7 % | +0,003×ATR / 51,0 % | + | 60 min | +0,085×ATR / 50,7 % | +0,034×ATR / 51,1 % | + | 120 min | +0,002×ATR / 50,5 % | **−0,259×ATR** / 48,3 % | + **Die Trefferquote liegt über alle sechs Felder zwischen 48,3 und 51,1 %.** Der + Überschuss über die Drift ist im besten Fall +0,085×ATR — das liegt **unter der + B3-Kostenhürde (0,1×ATR)** und weit unter den realen M5-Kosten (0,265×ATR). Als + **Richtungsprognose bezahlt das Signal nicht einmal seinen eigenen Spread.** + ✅ **Der Tag, um den es ging, war kein Ausreißer, sondern typisch:** 11 gerichtete + Episoden, auf 30 min **8 von 10 gegenläufig**. Die fünf Vormittags-SHORTs liefen + gegen einen Tag, der von 75,5 auf 78 stieg (11:54 SHORT: −5,09×ATR auf 120 min). + Das ist die nachlaufende EMA im Trendtag — sie shortet die Rücksetzer. + ⚠⚠ **DAS HEISST NICHT, DASS DAS SYSTEM WERTLOS IST — es verdient nur nicht mit + der Richtung.** Dasselbe Signal ist mit **ruhender Order am Bestätigungs-Level** + in BEIDEN Hälften klar positiv (`backtest_auto_signal_v3.py`: ØR +0,35/+0,32, + PF 2,2), am Markt dagegen negativ (−0,09/−0,11). Der Ertrag kommt aus + **Einstiegspreis und Exit-Asymmetrie**, nicht aus der Trefferquote — genau wie + beim Squeeze (43–46 % Treffer, trotzdem positiv, weil die Gewinner laufen). + ✅ **Konsequenz für die Lesart:** die Headline ist **kein Kursurteil**, sondern + ein **Auslöser, an einem Level eine Order zu platzieren**. Der Konfidenz-Ring + darf erst recht nicht als P(Erfolg) gelesen werden (unabhängig belegt: + `analyze_verdict_calibration.py`, in H2 sogar invertiert). + ⚠ **Was daraus NICHT folgt:** die Suche nach einem besseren Richtungssignal. + Die ist mit **23 verworfenen Eingriffen** über jede orthogonale Indikatorklasse + hinweg beantwortet (Momentum · Inter-Market · Volume Profile · SMC/Liquidity · + Chartmuster · Fibonacci · Kerzen · Marktstruktur · Meta-Labeling). - **Volatilitäts-Squeeze-Breakout = ADDITIVER SETUP, UMGESETZT (User-Idee „Breakouterkennung" 2026-07-14, gemessen `backtest_breakout_squeeze.py`):** struktureller Ausbruch aus einer **Kompression** (Box = Spanne der letzten `_SQ_N`=10 diff --git a/analyze_signal_live.py b/analyze_signal_live.py new file mode 100644 index 0000000..4d87036 --- /dev/null +++ b/analyze_signal_live.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Zeigen die LIVE ausgegebenen Empfehlungen in die richtige Richtung? + +ANLASS (2026-08-06, User): „Die Empfehlungen heute waren fast immer gegenläufig — +sollten wir das Konzept überdenken?" Bevor man ein Konzept verwirft, misst man es. + +Das ist bewusst KEIN Backtest: gemessen werden die **tatsächlich ausgegebenen** +Zeilen aus `recommendations` (also nach ALLEN Live-Gates: Totband, M30-Filter, +Anti-Überdehnung, min_conf, Breakout-Bestätigung, Entry-Raum, EIA …) gegen den +echten Kursverlauf danach. Was hier steht, ist das, was der User gesehen hat. + +DREI METHODIK-PUNKTE, ohne die das Ergebnis wertlos wäre: + 1) **Gegen die DRIFT lesen, nicht gegen null.** Fällt der Markt im Messzeitraum, + sieht JEDES Short gut aus. Ausgewiesen wird deshalb der Überschuss gegenüber + der mittleren Kursbewegung derselben Periode (Projekt-Standard seit + `backtest_candles.py`). + 2) **Episoden statt Zeilen.** Die Empfehlung wird ~1×/Minute geloggt; eine + Marktlage erzeugt dutzende identische Zeilen. Ungruppiert zählt man dieselbe + Situation vielfach und täuscht sich über die Stichprobengröße. + 3) **Zwei Hälften.** Eine Zahl über den Gesamtzeitraum verwischt genau den + Regimewechsel, um den es bei einem Trendfolger geht. + +Aufruf: python analyze_signal_live.py [horizont_min] +""" +import bisect +import sqlite3 +import sys +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +_BERLIN = ZoneInfo("Europe/Berlin") +_BROKER = timezone(timedelta(hours=3)) +_UTC = timezone.utc +_LUECKE_S = 300 # >5 min Abstand = neue Episode +_HORIZONTE = (30, 60, 120) + + +def _berlin(ts): + return (datetime.fromtimestamp(int(ts), _UTC) + .replace(tzinfo=_BROKER).astimezone(_BERLIN)) + + +def _atr(H, L, C, n=14): + tr = [0.0] + [max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])) + for i in range(1, len(C))] + out, s = [0.0] * len(C), 0.0 + for i in range(1, len(C)): + s += tr[i] + if i > n: + s -= tr[i - n] + out[i] = s / min(i, n) + return out + + +def main(): + hz = [int(sys.argv[1])] if len(sys.argv) > 1 else list(_HORIZONTE) + if not mt5.initialize(): + print("MT5 nicht erreichbar"); return 1 + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") + if mt5.symbol_info(c)), None) + b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, 30000) + mt5.shutdown() + H = [float(x["high"]) for x in b]; L = [float(x["low"]) for x in b] + C = [float(x["close"]) for x in b] + EP = [_berlin(x["time"]).timestamp() for x in b] + A = _atr(H, L, C) + + # ── Episoden bilden ────────────────────────────────────────────────── + db = sqlite3.connect("oil_widget_history.db") + db.row_factory = sqlite3.Row + rows = db.execute( + "SELECT timestamp ts, signal, conf_pct, setup FROM recommendations " + "WHERE signal IN ('LONG','SHORT') ORDER BY timestamp").fetchall() + epi, letzte = [], None + for r in rows: + if (letzte and r["signal"] == letzte["signal"] + and r["ts"] - letzte["ts"] <= _LUECKE_S): + letzte = r + continue + epi.append(r) + letzte = r + print("=" * 90) + print(" LIVE-EMPFEHLUNGEN gegen den echten Kursverlauf") + print("=" * 90) + print(f" {len(rows)} geloggte Zeilen → {len(epi)} Episoden " + f"({_berlin(EP[0] if False else rows[0]['ts']):%d.%m.} – " + f"{datetime.fromtimestamp(rows[-1]['ts'], _BERLIN):%d.%m.%Y})") + print(" ⚠ Ohne Episoden-Bildung wären es 20k 'Fälle' — dieselbe Marktlage,") + print(" vielfach gezählt. Das ist die Zahl, die zählt.") + + mitte = epi[len(epi) // 2]["ts"] + + def bar(ts): + j = bisect.bisect_right(EP, ts) - 1 + return j if 0 <= j < len(C) else None + + for minuten in hz: + h = minuten // 5 + print() + print("=" * 90) + print(f" HORIZONT {minuten} MINUTEN") + print("=" * 90) + # Drift-Null der jeweiligen Hälfte: mittlere Kursbewegung ueber h Bars + for lbl, lo, hi in (("H1 (erste Hälfte)", 0, mitte), + ("H2 (zweite Hälfte)", mitte, 1 << 62)): + grp = [e for e in epi if lo <= e["ts"] < hi] + js = [bar(e["ts"]) for e in grp] + paare = [(e, j) for e, j in zip(grp, js) + if j is not None and j + h < len(C) and A[j] > 0] + if len(paare) < 20: + print(f" {lbl:<20} zu wenige Episoden ({len(paare)})") + continue + # Drift ueber genau dieselben Bar-Indizes (nicht ueber den ganzen Chart) + drift = sum((C[j + h] - C[j]) / A[j] for _, j in paare) / len(paare) + sig = [((C[j + h] - C[j]) * (1 if e["signal"] == "LONG" else -1)) / A[j] + for e, j in paare] + m = sum(sig) / len(sig) + nL = sum(1 for e, _ in paare if e["signal"] == "LONG") + # Erwartung bei NULL Information = Drift × mittlere Richtung + d_mittel = (nL - (len(paare) - nL)) / len(paare) + null = drift * d_mittel + wr = 100.0 * sum(1 for x in sig if x > 0) / len(sig) + print(f" {lbl:<20} n={len(paare):<5} L/S {nL}/{len(paare)-nL} " + f"Ø {m:+.3f}×ATR Treffer {wr:.1f} %") + print(f" {'':<20} Drift-Null {null:+.3f} → " + f"**Überschuss {m - null:+.3f}×ATR**") + + # ── Der Tag, um den es ging ────────────────────────────────────────── + print() + print("=" * 90) + print(" HEUTE (der Anlass der Frage)") + print("=" * 90) + heute = datetime.now(_BERLIN).strftime("%Y-%m-%d") + grp = [e for e in epi + if datetime.fromtimestamp(e["ts"], _BERLIN).strftime("%Y-%m-%d") == heute] + if not grp: + print(" keine gerichteten Episoden heute") + for e in grp: + j = bar(e["ts"]) + if j is None or A[j] <= 0: + continue + t = datetime.fromtimestamp(e["ts"], _BERLIN) + aus = [] + for minuten in (30, 60, 120): + k = j + minuten // 5 + if k < len(C): + r = (C[k] - C[j]) * (1 if e["signal"] == "LONG" else -1) / A[j] + aus.append(f"{minuten}′ {r:+.2f}") + print(f" {t:%H:%M} {e['signal']:<6} conf {e['conf_pct'] or 0:<3} " + f"@ {C[j]:.3f} " + " · ".join(aus)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())