diff --git a/CLAUDE.md b/CLAUDE.md index 6de49c2..21b4b87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -352,6 +352,30 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Richtung → +12, „⭐⭐ Konfluenz M30+H1"; H1 dagegen → −8. Per Backtest belegt: Ø-Edge ×3,5 (+0,016 nur-M30 → +0,056 M30+H1). `_htf_sign` holt zusätzlich H1-Bars. Mess-Tool: `python backtest_improve.py 8000`. +- **„Morgens fällt der Kurs, nachmittags steigt er" = WIDERLEGT (gemessen + `analyze_tageszeit.py`, 2026-08-04, User-Beobachtung):** ROHE Kursdrift je + Berliner Stunde (nicht Signal-Edge wie `backtest_hourly_split.py`), 80k M5-Bars + (17.06.25–04.08.26), DST-korrekt via `zoneinfo`, 2 Hälften, Überschuss gegen den + jeweiligen Tagesschnitt (sonst färbt das Regime alle Stunden ein: H1 fiel, H2 stieg). + **Beide Hälften der Behauptung fallen durch:** Vormittag 08–12 **kippt** + (H1 +0,55 / H2 −2,90 Zehntel-Cent je Bar), Nachmittag 14–18 ist zwar + vorzeichen-robust, aber **NEGATIV in beiden** (−0,23/−2,10) — also das Gegenteil + der Vermutung. Robust auf: 0/2/5/8/15/23 Uhr · robust ab: 17/21/22 Uhr — kein + Vormittag/Nachmittag-Muster, sondern verstreute Einzelstunden aus 24 Tests. + ⚠ **Größenordnung macht es ohnehin unhandelbar:** die stärkste robuste Stunde + (23:00, +2,2/+3,4) sammelt über eine volle Stunde ~0,03–0,04 $ — der Spread allein + ist **0,0225 $**. Selbst wenn die Drift echt wäre, ist sie kleiner als die Kosten. + ⚠ Std 2:00 sticht mit +33,0 in H2 hervor, hat dort aber nur n=648 (vs. ~1740 sonst) + = dünne Sonntag-Open-/Feiertagsstunde, Ausreißer-verdächtig. + ⚠⚠ **Methodik-Lehre (eigener Fehler, dokumentiert):** der ERSTE Lauf meldete + „Vormittag 08–12 robust negativ" — Artefakt eines **selbst eingebauten ATR-Filters** + (`atr < 0,06` übersprungen + ATR-Normierung). In den ruhigen Morgenstunden der + älteren Periode liegt der ATR unter dem Floor → von 1.740 Bars je Stunde blieben + **138–391** übrig, und zwar ausgerechnet die ungewöhnlich volatilen Morgen. + Genau die Stunden, um die es ging, waren eine verzerrte Teilstichprobe. Gleiche + Fehlerklasse wie das ORB-Selektions-Artefakt 2026-07-17 (ATR-Skip statt Floor). + **Regel: ein Filter, der Bars nach einer Eigenschaft verwirft, die mit der + untersuchten Achse korreliert, ERZEUGT den Befund.** - **Tageszeit-Gate = AUS geschaltet (User-Vorgabe 2026-07-22, `[trading] dead_hours=` leer — bewusst GEGEN die Messung).** Jetzt **config-steuerbar** (`wave.set_dead_hours`, Komma-Liste von Stunden; leer=aus). Gemessener Default im Code = `0,1,2,3,4,5,6,7,12,16` diff --git a/analyze_tageszeit.py b/analyze_tageszeit.py new file mode 100644 index 0000000..aa27a60 --- /dev/null +++ b/analyze_tageszeit.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Kursdrift nach Tageszeit — steigt WTI nachmittags und fällt morgens? (2026-08-04) + +User-Beobachtung: „der Kurs sinkt morgens eher und steigt nachmittags." + +⚠ Das ist eine ANDERE Frage als `backtest_hourly_split.py`. Dort wurde gemessen, +wie gut das SIGNAL je Stunde abschneidet (Edge nach Kosten). Hier geht es um die +rohe Richtungsdrift des Kurses selbst — unabhängig von jedem Signal. + +Methodik und ihre Fallstricke, alle bewusst adressiert: + 1. **Gesamt-Drift herausrechnen.** Fiel der Markt im Zeitraum, sehen ALLE Stunden + negativ aus. Deshalb wird zusätzlich der Überschuss gegen den Tagesmittelwert + ausgewiesen — nur der ist eine Tageszeit-Aussage. + 2. **Zwei Stichproben.** 24 Stunden = 24 Tests; bei 5 % Fehlalarmquote sieht rund + eine Stunde zufällig „signifikant" aus. Nur was in BEIDEN Hälften dasselbe + Vorzeichen hat, zählt. + 3. **KEIN ATR-Filter, KEINE ATR-Normierung** — der erste Entwurf hatte beides und + erzeugte damit genau den Befund, den er messen sollte (s. `auswerten`). + 4. **Echte Zeitzone.** Broker-Zeit ist UTC+3 (ganzjährig), Berlin wechselt + zwischen UTC+1 und +2 — über eine lange Historie ist ein fester Versatz von + 1 h falsch. Umrechnung daher über `zoneinfo`. + +Aufruf: python analyze_tageszeit.py [bars] +""" +import sys +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +_BERLIN = ZoneInfo("Europe/Berlin") +_BROKER = timezone(timedelta(hours=3)) # Pepperstone: UTC+3 ganzjährig +_ATRMIN = 0.06 + + +def _atr_series(H, L, C, p=14): + t = [0.0] + for i in range(1, len(C)): + t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1]))) + if i else None for i in range(len(C))] + + +def auswerten(bars, H, L, C, A, lo, hi): + """→ {stunde: [Rendite je Bar in DOLLAR]} + + ⚠⚠ KEIN ATR-Filter und KEINE ATR-Normierung mehr (Korrektur 2026-08-04). + Der erste Entwurf übersprang Bars mit `atr < 0,06` und teilte durch den + rollierenden M5-ATR. Beides klang vernünftig, erzeugte aber eine massive + **Selektionsverzerrung**: in den ruhigen Nacht-/Morgenstunden der älteren + Periode liegt der ATR unter dem Floor, also blieben dort nur die + ungewöhnlich VOLATILEN Tage übrig — von 1.740 vorhandenen Bars je Stunde + kamen nur 138–391 in die Auswertung. Genau die Stunden, um die es geht, + waren damit eine verzerrte Teilstichprobe. + Jetzt: rohe Dollar-Rendite je Bar, ALLE Bars. Die Regime-Unterschiede + zwischen den Hälften fängt der Abzug des jeweiligen Tagesmittels ab. + """ + out = defaultdict(list) + for i in range(max(lo, 1), hi): + # Broker-Epoch → echte Berliner Stunde (DST-korrekt) + t = datetime.fromtimestamp(int(bars[i]["time"]), _BROKER).astimezone(_BERLIN) + out[t.hour].append(C[i] - C[i - 1]) + return out + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + 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, n) + mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars] + A = _atr_series(H, L, C) + N = len(C); mid = N // 2 + t0 = datetime.fromtimestamp(int(bars[0]["time"]), _BROKER).astimezone(_BERLIN) + t1 = datetime.fromtimestamp(int(bars[-1]["time"]), _BROKER).astimezone(_BERLIN) + + h1 = auswerten(bars, H, L, C, A, 0, mid) + h2 = auswerten(bars, H, L, C, A, mid, N) + + def mittel(d, s): + v = d.get(s) or [] + return (sum(v) / len(v), len(v)) if v else (0.0, 0) + + # Tagesmittel je Hälfte — dagegen wird der Überschuss gerechnet + alle1 = [x for v in h1.values() for x in v] + alle2 = [x for v in h2.values() for x in v] + m1 = sum(alle1) / len(alle1) if alle1 else 0.0 + m2 = sum(alle2) / len(alle2) if alle2 else 0.0 + + print("=" * 86) + print(f" KURSDRIFT NACH TAGESZEIT — {sym} M5, {N} Bars") + print(f" {t0:%d.%m.%Y} bis {t1:%d.%m.%Y} · Berliner Zeit (DST-korrekt) · 2 Hälften") + print(f" Werte = Ø Rendite je 5-Min-Bar in US-Dollar (×1000 = Zehntel-Cent)") + print(f" Gesamt-Drift je Bar: H1 {m1*1000:+.3f} · H2 {m2*1000:+.3f} (wird abgezogen)") + print("=" * 86) + print(f" {'Std':>3} {'n H1':>6} {'H1 roh':>9} {'H1 Übersch.':>12} " + f"{'n H2':>6} {'H2 roh':>9} {'H2 Übersch.':>12} Urteil") + print("-" * 86) + robust_auf, robust_ab = [], [] + for s in range(24): + a1, n1 = mittel(h1, s); a2, n2 = mittel(h2, s) + if n1 < 50 or n2 < 50: + continue + u1 = (a1 - m1) * 1000; u2 = (a2 - m2) * 1000 + if u1 > 0 and u2 > 0: + urteil = "beide AUF"; robust_auf.append((s, u1, u2)) + elif u1 < 0 and u2 < 0: + urteil = "beide AB"; robust_ab.append((s, u1, u2)) + else: + urteil = "kippt" + print(f" {s:>3} {n1:>6} {a1*1000:>+9.3f} {u1:>+12.3f} " + f"{n2:>6} {a2*1000:>+9.3f} {u2:>+12.3f} {urteil}") + + print("\n" + "=" * 86) + print(" ROBUST (gleiches Vorzeichen des Überschusses in BEIDEN Hälften)") + print("=" * 86) + print(" aufwärts: " + (", ".join(f"{s}:00 ({u1:+.2f}/{u2:+.2f})" + for s, u1, u2 in robust_auf) or "keine")) + print(" abwärts : " + (", ".join(f"{s}:00 ({u1:+.2f}/{u2:+.2f})" + for s, u1, u2 in robust_ab) or "keine")) + # Direkter Test der Behauptung: Vormittag gegen Nachmittag + def block(d, m, von, bis): + v = [x for s in range(von, bis) for x in (d.get(s) or [])] + return ((sum(v) / len(v) - m) * 1000, len(v)) if v else (0.0, 0) + print("\n Die Behauptung direkt geprüft (Überschuss gegen den Tagesschnitt):") + for lbl, von, bis in (("Vormittag 08–12", 8, 12), ("Nachmittag 14–18", 14, 18), + ("Abend 19–23", 19, 23), ("Nacht 00–07", 0, 7)): + b1, c1 = block(h1, m1, von, bis); b2, c2 = block(h2, m2, von, bis) + ok = "robust" if (b1 > 0) == (b2 > 0) else "KIPPT" + print(f" {lbl:<18} H1 {b1:>+7.3f} (n={c1:>6}) " + f"H2 {b2:>+7.3f} (n={c2:>6}) {ok}") + print("\n ⚠ 24 Stunden = 24 Tests: bei 5 % Fehlalarmquote sieht rund EINE Stunde") + print(" zufällig auffällig aus. Nur beidhälftig gleiche Vorzeichen zählen —") + print(" und auch die sind ohne Kosten gerechnet (Spread ~0,265×ATR!).") + print() + + +if __name__ == "__main__": + main()