Files
AH-Oil-Trader/analyze_verdict_modules.py
Axel HocksandClaude Opus 5 71aec2e240 Korrektur der eigenen Verdict-Auswertung: die Gewichtung ist NICHT invertiert
Der Anteil "ueber alle Zeilen" mischt Gewichtung und Haeufigkeit. Entscheidend
fuer "ist die Gewichtung kaputt?" ist der Anteil, WENN ein Modul spricht:

  Welle 44,4 % · H1 39,3 % · Squeeze 37,3 % · M30 31,9 % · Elliott 26,2 % ·
  KI 16,2 %

Die Welle dominiert also mit dem hoechsten Wert im Feld, genau wie entworfen -
ihre 3,8 % im Alltag sind schlicht die 91 % gewollte Stille der Gates. Meine
vorherige Formulierung ("Gewichtung invertiert") war zu scharf und ist
korrigiert.

Was bleibt: H1 und Elliott haben in beiden Spalten fast denselben Wert, weil sie
praktisch nie schweigen - ein permanenter Hintergrund von ~65 %, gegen den die
episodischen Module anschieben muessen. Elliott ist dabei ueber 1.163 Episoden
flach (49 % Treffer).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:23:10 +02:00

208 lines
9.3 KiB
Python
Raw Permalink 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
"""Welches Verdict-Modul sagt tatsächlich etwas voraus? (2026-08-05)
FRAGE (User): „Bewerte die Signale, die in die Gesamtempfehlung einfließen."
Die Tabelle `verdict_votes` wird seit 2026-07-24 genau dafür geschrieben — bisher
hat sie niemand ausgewertet.
⚠⚠ ZWEI FALLSTRICKE, beide hier adressiert:
1. **Autokorrelation.** Die Tabelle schreibt ~1 Zeile/Minute. Ein Modul, das drei
Stunden lang LONG sagt, erzeugt 180 Zeilen — das ist EINE Marktlage, nicht 180
Beobachtungen. Wer roh zählt, hält 20 Episoden für 286 Datenpunkte (genau dieser
Fehler ist bei der Copilot-Sichtung am 2026-08-02 aufgefallen). Deshalb werden
aufeinanderfolgende gleiche Stimmen zu **EPISODEN** zusammengefasst und nur die
ERSTE Zeile je Episode gewertet.
2. **Kurzer Zeitraum.** 24.07.05.08. = ~12 Tage, EIN Regime. Der Zwei-Stichproben-
Split ist hier bestenfalls ein Konsistenz-Check, KEIN Robustheitsnachweis. Das
Ergebnis ist eine SICHTUNG; die Reminder-Schwelle steht bewusst bei 25.000
Verdicts (~3 Wochen), erreicht sind ~13.500.
METHODIK: Forward-Return in Stimmen-Richtung, ATR-normiert, aus MT5-M5-Bars
(NICHT aus `candles_m1` — das deckt nur ~14 Tage und liefert für ältere Zeitstempel
still denselben Randwert; dieser Fehler ist bei der News-Messung am 03.08. passiert).
Gegen die **Drift-Null** gelesen: in einem Aufwärts-Regime sieht jedes LONG-Modul
gut aus, deshalb wird der Mittelwert ALLER Bars als Nulllinie abgezogen.
Aufruf: python analyze_verdict_modules.py [minuten]
"""
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
_MODULE = [("wave", "Welle", 3.0), ("m30", "M30", 1.5), ("h1", "H1", 1.5),
("ki", "KI-Copilot", 1.0), ("elliott", "Elliott", 1.0),
("squeeze", "Squeeze", 2.0), ("pattern", "Muster", 0.25),
("orderbook", "Orderbuch", 0.5), ("liqtrend", "Liq-Trend", 0.5)]
def _berlin_ep(ts):
"""Bar-Zeitstempel → lokale Epoch (DST-korrekt, s. CLAUDE.md 05.08.)."""
return (datetime.fromtimestamp(int(ts), _UTC)
.replace(tzinfo=_BROKER).astimezone(_BERLIN).timestamp())
def main():
horizont = int(sys.argv[1]) if len(sys.argv) > 1 else 60 # Minuten
mt5.initialize()
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
if mt5.symbol_info(c)), None)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, 20000)
mt5.shutdown()
EP = [_berlin_ep(b["time"]) for b in bars]
C = [float(b["close"]) for b in bars]
H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
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))]
A = [None] + [sum(tr[max(1, i - 13):i + 1]) / len(tr[max(1, i - 13):i + 1])
for i in range(1, len(C))]
schritt = max(1, horizont // 5)
def fwd(ep):
"""Forward-Return in ×ATR, oder None wenn nicht sauber abgedeckt."""
j = bisect.bisect_left(EP, ep)
if j <= 20 or j + schritt >= len(C):
return None
if abs(EP[j] - ep) > 900: # >15 min Lücke → keine Deckung
return None
a = A[j]
if not a or a <= 0:
return None
return (C[j + schritt] - C[j]) / a
db = sqlite3.connect("oil_widget_history.db")
db.row_factory = sqlite3.Row
rows = db.execute("SELECT * FROM verdict_votes ORDER BY ts").fetchall()
db.close()
if len(rows) < 500:
print("Zu wenige Zeilen."); return
# Drift-Null: Mittelwert ALLER abgedeckten Bars im Zeitraum
alle = [x for x in (fwd(r["ts"]) for r in rows) if x is not None]
drift = sum(alle) / len(alle) if alle else 0.0
t0, t1 = rows[0]["ts"], rows[-1]["ts"]
mitte = (t0 + t1) / 2
print("=" * 92)
print(f" VERDICT-MODULE — sagen sie etwas voraus? Horizont {horizont} min")
print(f" {datetime.fromtimestamp(t0, _BERLIN):%d.%m.%Y} bis "
f"{datetime.fromtimestamp(t1, _BERLIN):%d.%m.%Y} · {len(rows)} Zeilen · "
f"Drift-Null {drift:+.4f}×ATR")
print(" ⚠ ~12 Tage = EIN Regime. Sichtung, kein Urteil (Reminder-Schwelle: 25.000).")
print("=" * 92)
print(f" {'Modul':<13}{'Gew':>5}{'Episoden':>9}{'stumm':>7}"
f"{'Übersch. H1':>13}{'Übersch. H2':>13}{'Treffer':>9} Konsistenz")
print(" " + "-" * 90)
for feld, name, gew in _MODULE:
# Episoden bilden: aufeinanderfolgende GLEICHE Stimme = eine Beobachtung
# ⚠ Stimme UND Gewicht prüfen. `verdict_votes` speichert beides getrennt
# (`elliott` / `elliott_w`); eine Stimme mit Gewicht 0 zählt im Verdict NICHT.
# Erster Entwurf wertete nur die Stimme — Elliott sah damit aus wie ein Modul,
# das permanent mitredet (stumm 1 %), obwohl es real fast nie Gewicht hat.
wfeld = feld + "_w"
hat_w = wfeld in rows[0].keys()
eps, letzte, stumm = [], None, 0
for r in rows:
v = r[feld]
v = 0 if v is None else int(v)
if hat_w and not (r[wfeld] or 0) > 0:
v = 0 # Stimme ohne Gewicht = keine Stimme
if v == 0:
stumm += 1
letzte = 0
continue
if v != letzte:
eps.append((r["ts"], v))
letzte = v
werte = [(ts, v, fwd(ts)) for ts, v in eps]
werte = [(ts, v, f) for ts, v, f in werte if f is not None]
if len(werte) < 8:
print(f" {name:<13}{gew:>5.2f}{len(werte):>9}"
f"{100.0*stumm/len(rows):>6.0f}%{'—':>13}{'—':>13}{'—':>9} "
f"zu wenige Episoden")
continue
h1 = [(v * f - v * drift) for ts, v, f in werte if ts < mitte]
h2 = [(v * f - v * drift) for ts, v, f in werte if ts >= mitte]
alle_u = [(v * f - v * drift) for ts, v, f in werte]
tr_q = 100.0 * sum(1 for x in alle_u if x > 0) / len(alle_u)
m1 = sum(h1) / len(h1) if h1 else float("nan")
m2 = sum(h2) / len(h2) if h2 else float("nan")
kons = ("beide +" if (m1 > 0 and m2 > 0) else
"beide " if (m1 < 0 and m2 < 0) else "KIPPT")
if len(h1) < 5 or len(h2) < 5:
kons = "n zu klein"
print(f" {name:<13}{gew:>5.2f}{len(werte):>9}{100.0*stumm/len(rows):>6.0f}%"
f"{m1:>+13.4f}{m2:>+13.4f}{tr_q:>8.0f}% {kons}")
# ── Wieviel bewegt jedes Modul die Nadel WIRKLICH? ────────────────────
# Gewicht allein sagt wenig: ein Modul mit Gewicht 2,0, das zu 81 % schweigt,
# bewegt weniger als eines mit 1,0, das immer mitredet. Gemessen wird deshalb
# der ANTEIL am Gesamtgewicht, gemittelt ueber alle Zeilen.
print()
print("=" * 92)
print(" REALER EINFLUSS auf die Bias-Nadel (Anteil am Gesamtgewicht, Ø ueber alle Zeilen)")
print("=" * 92)
summe = {n: 0.0 for _, n, _ in _MODULE}
ges = 0.0
for r in rows:
w_row = {}
for feld, name, gew in _MODULE:
wfeld = feld + "_w"
v = r[feld]
v = 0 if v is None else int(v)
w = (r[wfeld] or 0.0) if wfeld in r.keys() else (gew if v != 0 else 0.0)
w_row[name] = w if v != 0 else 0.0
s = sum(w_row.values())
if s <= 0:
continue
ges += 1
for name, w in w_row.items():
summe[name] += w / s
# ⚠ Zwei GRUNDVERSCHIEDENE Fragen, die leicht verwechselt werden:
# (a) Anteil ueber ALLE Zeilen = wie stark praegt das Modul die Nadel im Alltag
# (b) Anteil NUR wenn es spricht = ob die Nominal-Gewichtung tut, was sie soll
# Ein Modul kann bei (a) winzig und bei (b) dominant sein — dann ist die
# Gewichtung KORREKT und das Modul schlicht selten. Ohne (b) haelt man ein
# gewolltes Schweigen faelschlich fuer eine Fehlgewichtung.
bedingt = {n: [0.0, 0] for _, n, _ in _MODULE}
for r in rows:
w_row = {}
for feld, name, gew in _MODULE:
wfeld = feld + "_w"
v = r[feld]
v = 0 if v is None else int(v)
w = (r[wfeld] or 0.0) if wfeld in r.keys() else (gew if v != 0 else 0.0)
w_row[name] = w if v != 0 else 0.0
s = sum(w_row.values())
if s <= 0:
continue
for name, w in w_row.items():
if w > 0:
bedingt[name][0] += w / s
bedingt[name][1] += 1
print(f" {'Modul':<13}{'alle Zeilen':>12}{'wenn es spricht':>18}")
for _, name, gew in _MODULE:
a = 100.0 * summe[name] / max(1, ges)
b_ = (100.0 * bedingt[name][0] / bedingt[name][1]) if bedingt[name][1] else 0.0
print(f" {name:<13}{a:>11.1f} %{b_:>17.1f} % " + "█" * int(round(b_ / 3)))
print(f" (n={int(ges)} Zeilen mit mindestens einer Stimme)")
print()
print(" LESART: Ueberschuss = Forward-Return in Stimmen-Richtung MINUS der")
print(" allgemeinen Drift. >0 heisst: das Modul trifft besser als ein Muenzwurf,")
print(" der einfach dem Markt folgt. 'stumm' = Anteil der Zeilen ohne Stimme")
print(" (Gewicht 0) - ein Modul, das fast immer schweigt, bewegt wenig.")
print()
if __name__ == "__main__":
main()