analyze_signal_live.py: die Richtungsaussage der Empfehlung ist ein Muenzwurf

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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-06 22:00:58 +02:00
co-authored by Claude Opus 5
parent 58af6eba43
commit 64eef14871
2 changed files with 192 additions and 0 deletions
+35
View File
@@ -270,6 +270,41 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
(das hatte ~0 Edge). Totband (Chop→WARTEN), Anti-Überdehnung (das hatte ~0 Edge). Totband (Chop→WARTEN), Anti-Überdehnung
(`_STRETCH_MAX=3,5×ATR`), +S/R-Konfidenz, +Session-Konfidenz. (TU **entfernt** (`_STRETCH_MAX=3,5×ATR`), +S/R-Konfidenz, +Session-Konfidenz. (TU **entfernt**
s. u.) 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 (4346 % 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 - **Volatilitäts-Squeeze-Breakout = ADDITIVER SETUP, UMGESETZT (User-Idee
„Breakouterkennung" 2026-07-14, gemessen `backtest_breakout_squeeze.py`):** „Breakouterkennung" 2026-07-14, gemessen `backtest_breakout_squeeze.py`):**
struktureller Ausbruch aus einer **Kompression** (Box = Spanne der letzten `_SQ_N`=10 struktureller Ausbruch aus einer **Kompression** (Box = Spanne der letzten `_SQ_N`=10
+157
View File
@@ -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())