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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d75789ac72
commit
c9f1e8d039
@@ -357,25 +357,40 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
Berliner Stunde (nicht Signal-Edge wie `backtest_hourly_split.py`), 80k M5-Bars
|
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
|
(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).
|
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**
|
**ALLE VIER Tagesblöcke kippen zwischen den Hälften** — es gibt überhaupt kein
|
||||||
(H1 +0,55 / H2 −2,90 Zehntel-Cent je Bar), Nachmittag 14–18 ist zwar
|
Tageszeit-Muster (Zehntel-Cent je Bar):
|
||||||
vorzeichen-robust, aber **NEGATIV in beiden** (−0,23/−2,10) — also das Gegenteil
|
| Block | H1 | H2 | |
|
||||||
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.
|
| Vormittag 08–12 | +0,06 | −0,43 | kippt |
|
||||||
⚠ **Größenordnung macht es ohnehin unhandelbar:** die stärkste robuste Stunde
|
| Nachmittag 14–18 | −1,26 | +1,96 | kippt |
|
||||||
(23:00, +2,2/+3,4) sammelt über eine volle Stunde ~0,03–0,04 $ — der Spread allein
|
| Abend 19–23 | −0,39 | +1,67 | kippt |
|
||||||
|
| Nacht 00–07 | +0,42 | −1,30 | kippt |
|
||||||
|
Robust auf: 2/5/20/21/23 Uhr · robust ab: 14/18/19 Uhr — verstreute Einzelstunden
|
||||||
|
aus 24 Tests, kein Vormittag/Nachmittag-Muster.
|
||||||
|
⚠ **Größenordnung macht es ohnehin unhandelbar:** die stärkste belastbare Stunde
|
||||||
|
(20:00, +1,9/+3,3) sammelt über eine volle Stunde ~0,02–0,04 $ — der Spread allein
|
||||||
ist **0,0225 $**. Selbst wenn die Drift echt wäre, ist sie kleiner als die Kosten.
|
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)
|
⚠ Std 23 sticht mit +33,0 in H2 hervor, hat aber nur n=648 (22 Uhr: 1062, sonst
|
||||||
= dünne Sonntag-Open-/Feiertagsstunde, Ausreißer-verdächtig.
|
~1750) — das ist der Marktschluss-Rand, kein Befund.
|
||||||
⚠⚠ **Methodik-Lehre (eigener Fehler, dokumentiert):** der ERSTE Lauf meldete
|
⚠⚠ **ZWEI eigene Messfehler auf dem Weg dorthin — beide dokumentiert, weil sie
|
||||||
„Vormittag 08–12 robust negativ" — Artefakt eines **selbst eingebauten ATR-Filters**
|
Fehlerklassen sind, keine Ausrutscher:**
|
||||||
(`atr < 0,06` übersprungen + ATR-Normierung). In den ruhigen Morgenstunden der
|
**(1) Selbst gebauter ATR-Filter (04.08.).** Der erste Lauf meldete „Vormittag
|
||||||
älteren Periode liegt der ATR unter dem Floor → von 1.740 Bars je Stunde blieben
|
robust negativ" — Artefakt von `atr < 0,06` übersprungen + ATR-Normierung. In den
|
||||||
**138–391** übrig, und zwar ausgerechnet die ungewöhnlich volatilen Morgen.
|
ruhigen Morgenstunden der älteren Periode liegt der ATR unter dem Floor → von
|
||||||
Genau die Stunden, um die es ging, waren eine verzerrte Teilstichprobe. Gleiche
|
1.740 Bars je Stunde blieben **138–391** übrig, und zwar die ungewöhnlich
|
||||||
Fehlerklasse wie das ORB-Selektions-Artefakt 2026-07-17 (ATR-Skip statt Floor).
|
volatilen Morgen. Gleiche Fehlerklasse wie das ORB-Selektions-Artefakt 2026-07-17.
|
||||||
**Regel: ein Filter, der Bars nach einer Eigenschaft verwirft, die mit der
|
**Regel: ein Filter, der Bars nach einer Eigenschaft verwirft, die mit der
|
||||||
untersuchten Achse korreliert, ERZEUGT den Befund.**
|
untersuchten Achse korreliert, ERZEUGT den Befund.**
|
||||||
|
**(2) Zeitzone um 3 h verschoben (05.08., beim Squeeze-Nachgang aufgefallen).**
|
||||||
|
`datetime.fromtimestamp(rates["time"], UTC+3)` behandelt den Bar-Zeitstempel als
|
||||||
|
echten UTC-Epoch — MT5 liefert dort aber bereits die **Broker-Wallclock**. Ergebnis:
|
||||||
|
jede Stunde war **3 Stunden zu spät** einsortiert; der zweite Lauf meldete deshalb
|
||||||
|
„Nachmittag robust negativ", was in Wahrheit der Abend war. **Richtig:**
|
||||||
|
`fromtimestamp(ts, UTC).replace(tzinfo=UTC+3).astimezone(Berlin)` (Helfer `_berlin`
|
||||||
|
im Skript). ⚠ Der Rest des Projekts war NICHT betroffen (geprüft) — `core/gaps.py`
|
||||||
|
& Co. nutzen die richtige Konvention; der Fehler steckte nur in diesem neuen Skript.
|
||||||
|
**Plausibilitätstest, der ihn entlarvt hätte: die dünnen Stunden (n≈650/1060) müssen
|
||||||
|
am Marktschluss liegen (22/23 Uhr), nicht mitten in der Nacht.**
|
||||||
- **Tageszeit-Gate = AUS geschaltet (User-Vorgabe 2026-07-22, `[trading] dead_hours=`
|
- **Tageszeit-Gate = AUS geschaltet (User-Vorgabe 2026-07-22, `[trading] dead_hours=`
|
||||||
leer — bewusst GEGEN die Messung).** Jetzt **config-steuerbar** (`wave.set_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`
|
Komma-Liste von Stunden; leer=aus). Gemessener Default im Code = `0,1,2,3,4,5,6,7,12,16`
|
||||||
@@ -1417,6 +1432,31 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
|
|||||||
aus (dann auch nachts, unbeaufsichtigt). Einen engen Schutz-%-Stop NUR für Squeeze
|
aus (dann auch nachts, unbeaufsichtigt). Einen engen Schutz-%-Stop NUR für Squeeze
|
||||||
wurde bewusst NICHT gebaut (enger %-Stop unter Margin-Sizing wird vom Squeeze-
|
wurde bewusst NICHT gebaut (enger %-Stop unter Margin-Sizing wird vom Squeeze-
|
||||||
Rauschen ausgelöst, −48,94-€-Tag — der Squeeze braucht Raum).
|
Rauschen ausgelöst, −48,94-€-Tag — der Squeeze braucht Raum).
|
||||||
|
⚠⚠ **FALLBEISPIEL 05.08.2026 — der Guard hat 5 Ausbrüche geblockt, darunter den
|
||||||
|
Einstieg direkt vor einem 1,8-$-Rutsch (User-Frage „warum ist der Squeeze nicht
|
||||||
|
eingestiegen?").** Nachgerechnet mit der ECHTEN `_squeeze_one` auf den M5-Bars
|
||||||
|
(nicht aus dem Log geschlossen — der Squeeze-ZUSTAND wird nirgends geloggt, nur der
|
||||||
|
Entry): zwischen 02:00 und 07:30 Berlin feuerte 5× `active`, **alle in 0–7 Uhr**.
|
||||||
|
Der Bot war ab 01:15 flat, die Nachtsperre war also der ALLEINIGE Blocker (kein
|
||||||
|
Cooldown, keine offene Position). Kursverlauf: Hoch **76,373** um 03:30 → Tief
|
||||||
|
**74,548** um 04:40 = **−1,825 $ in 70 min**; der Ausbruch **04:00 SHORT @ 76,11**
|
||||||
|
lag praktisch auf dem Hoch.
|
||||||
|
**Mit dem kanonischen Exit** (`exit_model.LIVE`, Echtkosten aus dem Bar-Spread —
|
||||||
|
bewusst NICHT „maximale Auslenkung", s. die Korrektur bei der 15-Minuten-Regel):
|
||||||
|
02:00 SHORT **+3,28 R** · 03:00 LONG −0,24 · 04:00 SHORT **+1,36 R** · 06:15 LONG
|
||||||
|
−0,60 · 07:30 SHORT −0,24 → **Σ +3,56 R ≈ +95 € bei 2,66 Lots**.
|
||||||
|
⚠ **Trotzdem kein Argument gegen den Guard:** n=5 an EINEM Tag, und **92 % davon
|
||||||
|
stecken in zwei Trades** — ohne den 02:00er bleiben +0,29 R ≈ +12 €. Ein Einzeltag
|
||||||
|
taugt nicht als Beleg, auch nicht als widerlegender (dieselbe Lehre wie am 01.08.).
|
||||||
|
Die Guard-Begründung ruht auf 2 Halbjahren; dazu kommt der nicht-ökonomische Grund
|
||||||
|
(keine unbeaufsichtigten Nacht-Trades), der von Zahlen gar nicht berührt wird.
|
||||||
|
✅ **Ein messbarer Ansatz ergibt sich aber:** die Nacht-Begründung ist eine
|
||||||
|
**Kosten**-Aussage (Spread÷ATR 0,32–0,50). In dieser Nacht lagen die realen Kosten
|
||||||
|
bei **0,11–0,22×ATR**, also klar UNTER der Falle — weil der ATR hoch war. Ein
|
||||||
|
**kosten-basiertes statt stunden-basiertes Gate** („blocke, wenn Spread/ATR > X")
|
||||||
|
würde genau solche Nächte durchlassen und ruhige weiter sperren. **Ungemessen** —
|
||||||
|
vor dem Bau 2-Stichproben-Test gegen das Stunden-Gate; und es hebelt den
|
||||||
|
Beaufsichtigungs-Grund aus, ist also zusätzlich eine User-Entscheidung.
|
||||||
**Stop-&-Reverse = AUS (Default, `[trading] auto_squeeze_reverse=false`,
|
**Stop-&-Reverse = AUS (Default, `[trading] auto_squeeze_reverse=false`,
|
||||||
`_squeeze_reverse`; User-Vorgabe 2026-07-17):** War kurz scharf, dann abgeschaltet —
|
`_squeeze_reverse`; User-Vorgabe 2026-07-17):** War kurz scharf, dann abgeschaltet —
|
||||||
nur der **flat-Entry ist gemessen-validiert**, der Reverse ist unbelegt, verdoppelt
|
nur der **flat-Entry ist gemessen-validiert**, der Reverse ist unbelegt, verdoppelt
|
||||||
|
|||||||
+19
-4
@@ -31,7 +31,22 @@ import MetaTrader5 as mt5
|
|||||||
|
|
||||||
_BERLIN = ZoneInfo("Europe/Berlin")
|
_BERLIN = ZoneInfo("Europe/Berlin")
|
||||||
_BROKER = timezone(timedelta(hours=3)) # Pepperstone: UTC+3 ganzjährig
|
_BROKER = timezone(timedelta(hours=3)) # Pepperstone: UTC+3 ganzjährig
|
||||||
_ATRMIN = 0.06
|
_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):
|
def _atr_series(H, L, C, p=14):
|
||||||
@@ -59,7 +74,7 @@ def auswerten(bars, H, L, C, A, lo, hi):
|
|||||||
out = defaultdict(list)
|
out = defaultdict(list)
|
||||||
for i in range(max(lo, 1), hi):
|
for i in range(max(lo, 1), hi):
|
||||||
# Broker-Epoch → echte Berliner Stunde (DST-korrekt)
|
# Broker-Epoch → echte Berliner Stunde (DST-korrekt)
|
||||||
t = datetime.fromtimestamp(int(bars[i]["time"]), _BROKER).astimezone(_BERLIN)
|
t = _berlin(bars[i]["time"])
|
||||||
out[t.hour].append(C[i] - C[i - 1])
|
out[t.hour].append(C[i] - C[i - 1])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -75,8 +90,8 @@ def main():
|
|||||||
C = [float(b["close"]) for b in bars]
|
C = [float(b["close"]) for b in bars]
|
||||||
A = _atr_series(H, L, C)
|
A = _atr_series(H, L, C)
|
||||||
N = len(C); mid = N // 2
|
N = len(C); mid = N // 2
|
||||||
t0 = datetime.fromtimestamp(int(bars[0]["time"]), _BROKER).astimezone(_BERLIN)
|
t0 = _berlin(bars[0]["time"])
|
||||||
t1 = datetime.fromtimestamp(int(bars[-1]["time"]), _BROKER).astimezone(_BERLIN)
|
t1 = _berlin(bars[-1]["time"])
|
||||||
|
|
||||||
h1 = auswerten(bars, H, L, C, A, 0, mid)
|
h1 = auswerten(bars, H, L, C, A, 0, mid)
|
||||||
h2 = auswerten(bars, H, L, C, A, mid, N)
|
h2 = auswerten(bars, H, L, C, A, mid, N)
|
||||||
|
|||||||
Reference in New Issue
Block a user