Files
AH-Oil-Trader/analyze_tageszeit.py
Axel HocksandClaude Opus 5 c9f1e8d039 Zeitzonen-Bug in analyze_tageszeit.py behoben + Squeeze-Nachtsperre 05.08. dokumentiert
Zeitzone: datetime.fromtimestamp(rates["time"], UTC+3) behandelt den MT5-
Bar-Zeitstempel als echten UTC-Epoch - MT5 liefert dort aber bereits die
Broker-Wallclock. Jede Stunde war 3 h zu spaet einsortiert. Empirisch gegen
die Wallclock geprueft (letzte Bar 11:00 statt real 08:00). Helfer _berlin()
mit korrekter Konvention; Rest des Projekts war nicht betroffen (geprueft).

Neu gerechnet: ALLE VIER Tagesbloecke kippen zwischen den Haelften - es gibt
gar kein Tageszeit-Muster. Der zuvor gemeldete "Nachmittag robust negativ"
war der um 3 h verschobene Abend. Plausibilitaetstest, der es entlarvt haette:
die duennen Stunden (n~650) muessen am Marktschluss liegen, nicht nachts.

Squeeze 05.08.: 5 active-Ausbrueche zwischen 02:00 und 07:30 Berlin, alle von
auto_squeeze_skip_night geblockt; Bot war ab 01:15 flat, also alleiniger
Blocker. Mit kanonischem Exit + Echtkosten Sigma +3,56 R (~+95 EUR), davon
92 % in zwei Trades - n=5 an einem Tag ist kein Gegenbeleg zum Guard.
Messbarer Ansatz notiert: kosten- statt stundenbasiertes Gate.

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

159 lines
7.1 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
"""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
_UTC = timezone.utc
def _berlin(ts):
"""Bar-Zeitstempel → echte Berliner Zeit (DST-korrekt).
⚠⚠ FEHLERQUELLE, hier einmal falsch gemacht (Fix 2026-08-05): MT5 liefert
`rates["time"]` so, dass `fromtimestamp(ts, UTC)` bereits die BROKER-Wallclock
zeigt — die Zahl ist KEIN echter UTC-Epoch. Ein `fromtimestamp(ts, UTC+3)`
behandelt sie fälschlich als echten Epoch und landet **3 Stunden zu spät**.
Empirisch geprüft: letzte M5-Bar ergab so 11:00, während es real 08:00 war.
Richtig ist: die Wallclock als UTC lesen, dann als UTC+3 ETIKETTIEREN und
erst danach nach Berlin umrechnen.
"""
return (datetime.fromtimestamp(int(ts), _UTC)
.replace(tzinfo=_BROKER).astimezone(_BERLIN))
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 138391 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 = _berlin(bars[i]["time"])
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 = _berlin(bars[0]["time"])
t1 = _berlin(bars[-1]["time"])
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 0812", 8, 12), ("Nachmittag 1418", 14, 18),
("Abend 1923", 19, 23), ("Nacht 0007", 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()