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:
co-authored by
Claude Opus 5
parent
58af6eba43
commit
64eef14871
@@ -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())
|
||||
Reference in New Issue
Block a user