diff --git a/CLAUDE.md b/CLAUDE.md index 5add8c1..881fab4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4942,6 +4942,53 @@ Modell schliesst keine Trades. „die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die Basisraten-/Kalibrierungslücke. +## ⚠⚠ ECHTES CME-VOLUMEN (CL=F) TRÄGT NICHT MEHR ALS DIE CFD-TICK-ZÄHLUNG (2026-08-08) + +Anlass: User-Idee eines Telegram-Alerts auf WTI-Volumen-Spikes (`CL=F` via +yfinance). Die berechtigte Frage dahinter: das Projekt kennt bisher nur +`tick_volume` vom CFD — eine Zählung von Preisänderungen bei einem +**Market-Maker**-Broker, kein gehandeltes Volumen. Echtes NYMEX-Kontraktvolumen +wäre die erste Datenquelle, die **nicht aus dem Preis abgeleitet** ist. + +**Gemessen (`backtest_cme_volumen.py`): beide Quellen auf DEMSELBEN Fenster** +(28.05.–07.08.26, 13.855 CME- gegen 14.178 CFD-Bars), gleiche Methode, +Überschuss über die Drift, zwei Hälften. +⚠ Bewusst NICHT gegen den dokumentierten Wert −0,117/−0,204 gestellt — der +stammt aus einem anderen Zeitraum. Genau dieser Vergleich („kurzes Fenster gegen +Langfrist-Mittel") war am 07.08. der Messfehler, der elf Hypothesen ausgelöst hat. +| Volumen | CME H1 / H2 | CFD H1 / H2 | +|---|---|---| +| 1,5–2,0× | +0,116 / **−0,203** | −0,011 / +0,152 | +| 2,0–2,5× | −0,221 / −0,282 | −0,101 / **+0,589** | +| **≥2,5×** | **+0,102 / +0,096** | −0,267 / **−0,881** | +**Trefferquote in ALLEN Zellen 44–53 %** — Münzwurf, in beiden Quellen. +Kein monotoner Zusammenhang, und die Vorzeichen kippen zwischen den Hälften. +Die einzige beidhälftig konsistente Zelle (CME ≥2,5×) ist mit +**[−0,234 … +0,428] bzw. [−0,265 … +0,461]** klar Rauschen. + +⚠⚠ **EHRLICHE GRENZE: meine CFD-Kontrolle REPRODUZIERT den alten Befund NICHT** — +und der Grund ist ein Populations-Unterschied, kein Widerspruch. +`backtest_candles.py` maß „2× Volumen" **bedingt auf ein Kerzen-MUSTER** (langer +Körper / Marubozu), hier läuft es unbedingt über alle Kerzen. Der alte Befund +ist damit weder bestätigt noch widerlegt; er gilt weiter für seine Population. +⚠ 60 Tage = EIN Regime (yfinance deckelt 5-min-Bars). Ein ✅ hieße hier „nicht +ausgeschlossen"; das ❌ ist belastbar. + +✅ **Konsequenz: kein Volumen-Spike-SIGNAL.** Die Datenquelle funktioniert +(CL=F liefert echte Kontrakte, ~4 s je Abruf), sie trägt nur keine +Richtungsinformation. Ein Alert ist allenfalls als **Aufmerksamkeits-Hinweis** +vertretbar („hier passiert etwas"), ausdrücklich NICHT als Richtungsaussage — +und dann mit derselben Ehrlichkeit beschriftet wie die Kerzen-Anatomie-Karte. +⚠ **Telegram-KANÄLE als Eingabe (Bookmap/ATAS) = verworfen, ohne Messung:** sie +transportieren **Screenshots**. Ein Orderbuch-Zustand ist vorbei, sobald ihn +jemand abfotografiert. Dasselbe Argument wie bei der X-API — und der vorhandene +Textstrom (News-Sentiment) ist bereits gemessen nicht robust prädiktiv. +⚠ **Nebenbefund zum eingereichten Skript:** das `.iloc[0]`-Muster ist **korrekt** +(yfinance liefert MultiIndex-Spalten), nicht wie zunächst von mir vermutet ein +Fehler. Echte Mängel wären: kein Dedup (die „letzte geschlossene Kerze" bleibt +5 min stehen → 5 Alerts je Spike), kein Handelszeit-Gate, voller Tagesabruf im +60-s-Takt. + ## ⚠⚠ VWAP-BÄNDER AUF M15 = VERWORFEN (2026-08-08) — die letzte ungetestete Zutat Anlass: User-Wunsch nach einem **M15-Daytrading-Modul** auf Profi-Methodik diff --git a/backtest_cme_volumen.py b/backtest_cme_volumen.py new file mode 100644 index 0000000..d03f212 --- /dev/null +++ b/backtest_cme_volumen.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Verhält sich ECHTES CME-Volumen anders als die CFD-Tick-Zählung? + +ANLASS (2026-08-08, User-Idee „Volume-Spike-Alert auf CL=F"). Das Projekt hat +„2× Volumen = echt" schon gemessen (`backtest_candles.py`): Überschuss +**−0,117 / −0,204** — *monoton in die FALSCHE Richtung*. Mehr Volumen bedeutet +eine STÄRKERE Gegenbewegung; der Spike ist die Klimax-Kerze, und der Kurs läuft +danach GEGEN sie. Ein naiver „viel Volumen → mitgehen"-Alert stupst also +messbar falsch. + +⚠ ABER: jene Messung lief auf `tick_volume` des **CFD** — das ist eine Zählung +von Preisänderungen, KEIN gehandeltes Volumen. Der Broker ist Market Maker; was +dort als „Volumen" erscheint, hat mit den Kontrakten an der NYMEX nur mittelbar +zu tun. Genau das ist die offene Frage: trägt ECHTES Börsenvolumen (CL=F, CME) +eine andere Information? + +⚠⚠ DESHALB LAUFEN BEIDE QUELLEN AUF DEMSELBEN FENSTER UND MIT DERSELBEN METHODE. +Der dokumentierte Wert −0,117/−0,204 stammt aus einem anderen Zeitraum und von +anderen Bars — ihn direkt danebenzustellen wäre derselbe Fehler wie am 07.08., +als eine 6-Tage-Live-Stichprobe gegen ein 208-Tage-Mittel gehalten wurde. +Verglichen wird nur CME gegen CFD, Zeitpunkt für Zeitpunkt. + +METHODE (wie `backtest_candles.py`): + · Volumen-Verhältnis = V[i] / Ø(V[i−20 … i−1]) + · Forward-Return über 12 Bars (60 min), in ×ATR, VORZEICHEN = Kerzenrichtung + · gelesen als ÜBERSCHUSS über die Drift des Zeitraums — sonst liest man in + einem Trendfenster die Drift als Edge (die Lehre von `analyze_tageszeit.py`) + · zwei Hälften des Fensters + +⚠⚠ GRENZE, DIE VORAB ZU NENNEN IST: yfinance liefert für 5-min-Bars maximal +~60 Tage. Beide Hälften liegen damit im SELBEN Regime — genau die Konstellation, +an der ORB und die Dead-Hour-Stunde 13 gescheitert sind. **Ein ✅ heißt hier +„nicht ausgeschlossen", NICHT „bestätigt".** Ein ❌ ist dagegen belastbar. + +Aufruf: python backtest_cme_volumen.py +""" +from __future__ import annotations + +import datetime as dt + +import MetaTrader5 as mt5 + +_FWD = 12 # 12 M5-Bars = 60 min +_LOOK = 20 # Fenster fuer das Durchschnittsvolumen +_BUCKETS = ((0, 1.5), (1.5, 2.0), (2.0, 2.5), (2.5, 99)) +_BROKER_OFF = 10800 # Brokerzeit = UTC+3 + + +def atr_series(H, L, C, p=14): + tr = [0.0] + for i in range(1, len(C)): + tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [None] + for i in range(1, len(C)): + seg = tr[max(1, i - p + 1):i + 1] + out.append(sum(seg) / len(seg)) + return out + + +def hole_cme(): + """CL=F 5-min von yfinance. ⚠ MultiIndex-Spalten sind hier NORMAL — das + `.iloc[0]`-Muster im User-Skript ist deshalb korrekt, kein Fehler.""" + import yfinance as yf + df = yf.download("CL=F", interval="5m", period="60d", + progress=False, auto_adjust=False) + if df is None or df.empty: + return None + reihen = [] + for ts, r in df.iterrows(): + def f(sp): + v = r[sp] + return float(v.iloc[0] if hasattr(v, "iloc") else v) + try: + reihen.append((int(ts.timestamp()), f("High"), f("Low"), + f("Close"), f("Volume"))) + except Exception: + continue + return reihen + + +def hole_cfd(von_ts, bis_ts): + """SpotCrude M5 aus MT5, auf dasselbe Zeitfenster beschnitten. + ⚠ `time` ist Broker-Wallclock (UTC+3) — fuer den Abgleich in echte + UTC-Epoch umrechnen (die im Projekt mehrfach dokumentierte Falle).""" + 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, 30000) + mt5.shutdown() + if bars is None: + return None, sym + out = [] + for b in bars: + t = int(b["time"]) - _BROKER_OFF + if von_ts - 300 <= t <= bis_ts + 300: + out.append((t, float(b["high"]), float(b["low"]), + float(b["close"]), float(b["tick_volume"]))) + return out, sym + + +def auswerten(reihen, name): + """Ueberschuss ueber die Drift je Volumen-Bucket, in zwei Haelften.""" + T = [r[0] for r in reihen]; H = [r[1] for r in reihen] + L = [r[2] for r in reihen]; C = [r[3] for r in reihen] + V = [r[4] for r in reihen] + AT = atr_series(H, L, C) + mid = len(C) // 2 + print(f"\n ── {name} ── {len(C)} Bars, " + f"{dt.datetime.utcfromtimestamp(T[0]):%d.%m.} bis " + f"{dt.datetime.utcfromtimestamp(T[-1]):%d.%m.}") + + for hn, lo, hi in (("H1", _LOOK + 15, mid), ("H2", mid, len(C) - _FWD - 1)): + # Drift des Fensters: mittlerer Forward-Return OHNE Richtungsvorzeichen + drift = [] + for i in range(lo, hi): + a = AT[i] + if a and a > 0: + drift.append((C[i + _FWD] - C[i]) / a) + d_mean = sum(drift) / len(drift) if drift else 0.0 + print(f" {hn} Drift des Fensters: {d_mean:+.3f}×ATR " + f"(darueber wird gelesen)") + print(f" {'Volumen':<14}{'n':>7}{'Ueberschuss':>14}{'Treffer':>10}") + for b_lo, b_hi in _BUCKETS: + werte = [] + for i in range(lo, hi): + a = AT[i] + if not a or a <= 0: + continue + vs = V[max(0, i - _LOOK):i] + if len(vs) < _LOOK: + continue + m = sum(vs) / len(vs) + if m <= 0: + continue + q = V[i] / m + if not (b_lo <= q < b_hi): + continue + d = 1 if C[i] > (H[i] + L[i]) / 2 else -1 # Kerzenrichtung + r = (C[i + _FWD] - C[i]) * d / a + werte.append(r - d * d_mean) # Ueberschuss + if len(werte) < 25: + print(f" {f'{b_lo}–{b_hi}x':<14}{len(werte):>7}{'zu wenig':>14}") + continue + m = sum(werte) / len(werte) + tr = 100 * sum(1 for x in werte if x > 0) / len(werte) + print(f" {f'{b_lo}–{b_hi}x':<14}{len(werte):>7}{m:>+14.3f}{tr:>9.0f}%") + + +def main(): + print("=" * 84) + print(" ECHTES CME-VOLUMEN gegen CFD-TICK-ZAEHLUNG — gleiches Fenster, gleiche Methode") + print("=" * 84) + cme = hole_cme() + if not cme or len(cme) < 500: + print(f" CL=F lieferte zu wenig ({0 if not cme else len(cme)} Bars)"); return + cfd, sym = hole_cfd(cme[0][0], cme[-1][0]) + if not cfd or len(cfd) < 500: + print(f" {sym} lieferte zu wenig ({0 if not cfd else len(cfd)} Bars)"); return + + print(f"\n Fenster: {dt.datetime.utcfromtimestamp(cme[0][0]):%d.%m.%Y} bis " + f"{dt.datetime.utcfromtimestamp(cme[-1][0]):%d.%m.%Y} (UTC)") + print(f" CL=F (CME, echte Kontrakte): {len(cme)} Bars") + print(f" {sym} (CFD, tick_volume): {len(cfd)} Bars") + print("\n ⚠ ~60 Tage = EIN Regime. Ein ✅ heisst 'nicht ausgeschlossen',") + print(" NICHT 'bestaetigt'. Ein ❌ ist dagegen belastbar.") + print("\n Erwartung aus der Vormessung (CFD, anderer Zeitraum): der Ueberschuss") + print(" wird mit steigendem Volumen NEGATIVER (Klimax, Kurs laeuft dagegen).") + + auswerten(cme, "CL=F — echtes CME-Kontraktvolumen") + auswerten(cfd, f"{sym} — CFD-tick_volume (Kontrolle)") + + +if __name__ == "__main__": + main()