Files
AH-Oil-Trader/analyze_signal_live.py
T
Axel HocksandClaude Opus 5 64eef14871 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>
2026-08-06 22:00:58 +02:00

158 lines
6.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
"""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())