Fix: Broker-Offset bei geschlossenem Markt (-9,5 h statt +3 h)

`_broker_offset_s()` leitete die Zeitzone aus `tick.time - now` ab. Das misst
die Zeitzone nur bei FRISCHEM Tick; steht der Markt, misst es die Veraltung.
Real am 01.08. (letzter Tick Fr 23:54): -34210 s -> gerundet -34200 (-9,5 h)
statt +10800. Die Pruefung [-12h,+14h] liess das durch.

Folgen (die Laufzeit-Uhr war nur das Sichtbare):
- open_time 12,5 h in der Zukunft -> Laufzeit 0:00
- Time-Stop-Alter (trailing.py:504) negativ -> 120-min-Stop haette nicht ausgeloest
- deal.time-Umrechnung beim externen Close, alle MQL5-Chart-Anker

Fix: Offset nur noch aus frischem Tick bestimmen, sonst letzter gueltiger Wert
(vorbelegt UTC+3 = dieselbe Konstante wie engine.broker_utc_offset). Frische am
bereits bekannten Offset gemessen -> nicht zirkulaer. Schwelle 900 s < halbes
Rundungsraster. Zeitzonen-Wechsel wird uebernommen und geloggt.

Ausserdem:
- market_closed greift sofort nach Neustart: _last_bid_move_ts wird aus dem
  echten Tick-Alter vorbelegt statt bei null zu starten (vorher 180 s blind)
- pollSnapshot: der stille catch umschloss render() -> Render-Fehler wurden
  lautlos verschluckt, das Dashboard fror ohne Konsolen-Fehler ein. Jetzt ist
  nur noch der fetch im try.
- Header-Labels ausgeschrieben: "PEPPERSTONE KURS" / "HYPERLIQUID KURS"
- v=131, CLAUDE.md (Deployment-Drift Fall 4)

Verifiziert: market_closed sofort True, tick_age gegen die Rohwerte 0 s
Abweichung, open_time -> 31.07. 22:54 (Laufzeit +15,8 h).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-01 14:44:25 +02:00
co-authored by Claude Opus 5
parent 2ea17ce3e0
commit b8bb1d260f
5 changed files with 149 additions and 20 deletions
+20 -1
View File
@@ -3031,11 +3031,30 @@ class TradingEngine:
# und das Alter hebt sich rechnerisch auf (Ergebnis: 347 s).
# Verwendet wird daher der direkte Beleg: **wann hat sich der Kurs zuletzt
# BEWEGT?** Kein Zeitzonen-, Kalender- oder Feiertagswissen nötig.
# ⚠ NACHTRAG 2026-08-01: Punkt (2) galt nur, solange `_broker_offset_s()`
# den Offset aus einem MÖGLICHERWEISE STALEN Tick ableitete. Seit dem Fix
# dort (Offset nur aus frischem Tick, sonst letzter gültiger Wert) ist die
# Rechnung nicht mehr zirkulär — deshalb dient das echte Tick-Alter jetzt
# als STARTWERT der Stillstands-Uhr. Ohne ihn begann die Uhr bei jedem
# Neustart bei null und der Bot brauchte 180 s, um ein längst geschlossenes
# Wochenende zu bemerken (real: Reload direkt nach `restart_server.bat`
# zeigte „KURS" statt „KURS · ZU" und den eingefrorenen Broker-G/V).
# Laufender Betrieb bleibt bewusst bewegungs-basiert: das erkennt auch
# einen eingefrorenen Feed, dessen Zeitstempel weiterlaufen.
_now = time.time()
try:
_b = (market or {}).get("bid")
if _b is not None:
if self._last_bid is None or abs(float(_b) - self._last_bid) > 1e-9:
if self._last_bid is None:
self._last_bid = float(_b)
_srv = (market or {}).get("tick_server_ts")
_age0 = 0.0
if _srv:
_a = _now + self.trader._broker_offset_s() - float(_srv)
if 0.0 <= _a <= 14 * 86400: # plausibel: bis ~2 Wochen Stillstand
_age0 = _a
self._last_bid_move_ts = _now - _age0
elif abs(float(_b) - self._last_bid) > 1e-9:
self._last_bid = float(_b)
self._last_bid_move_ts = _now
except Exception:
+46 -7
View File
@@ -50,6 +50,16 @@ def _retcode_msg(res) -> str:
return _RETCODE_MSG.get(rc, f"Order-Fehler (retcode={rc})")
# ── Broker-Zeitzone ───────────────────────────────────────────────────────────
# Pepperstone liefert Broker-Zeit UTC+3 (ganzjährig) — dieselbe Konstante nutzt
# `engine.get_bars` bereits für die Chart-Umrechnung (`broker_utc_offset`).
_BROKER_OFF_DEFAULT_S = 3 * 3600
# Nur ein FRISCHER Tick darf den Offset (neu) bestimmen. Begründung s.
# `_broker_offset_s` — die Schwelle liegt unter dem halben Rundungsraster
# (1800/2 = 900 s), damit die 30-min-Rundung immer auf den echten Offset fällt.
_BROKER_TICK_FRESH_S = 900
class TradeManager:
def __init__(self):
self.ticket = self.order_type = None
@@ -57,6 +67,11 @@ class TradeManager:
self.sl = self.tp = self.margin = 0.0
self.open_time = 0.0 # Öffnungszeit als ECHTE Epoch (Broker-Offset korrigiert)
self.symbol = None
# Zuletzt als GÜLTIG erkannter Broker-Offset (s. `_broker_offset_s`).
# Vorbelegt mit der bekannten Zeitzone, damit auch ein Start bei
# geschlossenem Markt (Wochenende) sofort richtig rechnet.
self._boff = _BROKER_OFF_DEFAULT_S
self._boff_logged = None
self.last_error = ""
self._lock = threading.Lock()
self.history: 'HistoryLogger | None' = None
@@ -503,9 +518,26 @@ class TradeManager:
(z.B. UTC+3 → 10800). MT5 liefert deal.time/tick.time in
Broker-Zeit, NICHT in UTC — ohne Korrektur landen Timestamps
3 h verschoben in der DB.
Außerhalb der Handelszeiten kann der letzte Tick alt sein →
Ergebnis wird auf plausiblen Bereich [-12h, +14h] geprüft,
sonst 0 (keine Korrektur).
⚠ BUGFIX 2026-08-01 (Wochenende, real gemessen): `tick.time - now`
misst die Zeitzone NUR, solange der Tick frisch ist. Steht der Markt,
friert `tick.time` ein und die Differenz misst die **Veraltung**.
Real am 01.08. um 11:25: letzter Tick Fr 23:54 → `tick.time - now =
-34210` → gerundet **-34200 (9,5 h)** statt +10800. Die alte
Plausibilitätsprüfung `[-12h, +14h]` ließ das durch, weil eine
Wochenend-Veraltung genau in dieses Fenster fällt.
Folgen waren real sichtbar: `open_time` 12,5 h in der ZUKUNFT
(Laufzeit-Uhr stand auf 0:00), und still betroffen waren auch
Time-Stop-Alter (`trailing`), `deal.time`-Umrechnung beim externen
Close und alle MQL5-Chart-Anker (Kanal/Liquiditäts-Trendlinie/
Trade-Marker).
Neu: Der Offset wird **nur von einem frischen Tick** (neu) bestimmt;
sonst gilt der zuletzt gültige Wert (`self._boff`, vorbelegt mit der
bekannten Zeitzone UTC+3). Die Frische wird am bereits bekannten
Offset gemessen — nicht am Tick selbst, das wäre zirkulär.
Ein echter Zeitzonen-Wechsel (DST des Brokers) wird beim ersten
frischen Tick übernommen und geloggt.
"""
try:
# `sym` explizit übergebbar: beim Adoptieren einer Position ist
@@ -514,12 +546,19 @@ class TradeManager:
sym = sym or self.symbol
tick = mt5.symbol_info_tick(sym) if sym else None
if tick and tick.time:
off = round((tick.time - time.time()) / 1800) * 1800
if -12 * 3600 <= off <= 14 * 3600:
return int(off)
now = time.time()
off = round((tick.time - now) / 1800) * 1800
# Veraltung, gemessen am bisher bekannten Offset (nicht zirkulär).
stale = (now + self._boff) - tick.time
if stale <= _BROKER_TICK_FRESH_S and -12 * 3600 <= off <= 14 * 3600:
if off != self._boff and self._boff_logged != off:
log.info(f"Broker-Zeitzone neu erkannt: "
f"{self._boff / 3600:+.1f} h → {off / 3600:+.1f} h")
self._boff_logged = off
self._boff = int(off)
except Exception:
pass
return 0
return int(self._boff)
def _log_external_close(self, ticket: int,
fallback_pnl: float | None = None,