diff --git a/CLAUDE.md b/CLAUDE.md index 082b307..d4e707a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2143,7 +2143,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei **negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap. -- Asset-Version aktuell **v=130** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=131** (in `web/index.html` hochzählen, siehe Workflows). Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis 14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die `font: Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` — @@ -2190,8 +2190,70 @@ Und der G/V nutzt die vorhandene, korrekte Umrechnung **`trader.live_pnl(bid, as (`tick_size`/`tick_value` inkl. Swap und Währung), gefüttert mit dem HL-Kurs ± halbem Spread. ⚠ Bleibt eine **Schätzung** (anderer Kontrakt, Montags-Open-Spread unbekannt) — deshalb das `≈` in der Anzeige. -⚠ Nach einem Server-Neustart braucht die Erkennung 180 s, bis sie greift (der -Bewegungs-Zeitstempel startet beim ersten Snapshot). + +### ⚠⚠ `_broker_offset_s()` lieferte bei geschlossenem Markt −9,5 h statt +3 h (Fix 2026-08-01) + +Sackgasse (2) oben war **nur halb richtig** — die Zirkularität war nicht das eigentliche +Problem, sondern ein **echter Bug in `trader._broker_offset_s()`**, der weit über die +Anzeige hinausreichte. Gefunden bei der Frage „warum steht die Laufzeit auf 0:00?". + +**Mechanismus:** `off = round((tick.time − now)/1800)*1800` misst die Zeitzone nur, +solange der Tick FRISCH ist. Steht der Markt, friert `tick.time` ein und die Differenz +misst die **Veraltung**. Real gemessen am 01.08. um 11:25 (letzter Tick Fr 23:54:59): + +``` +tick.time − now = −34210 s → gerundet −34200 (−9,5 h) statt +10800 (+3 h) +``` + +Die alte Plausibilitätsprüfung `[−12 h … +14 h]` **ließ das durch** — eine +Wochenend-Veraltung fällt genau in dieses Fenster. Ein noch längerer Stillstand (Mo +früh, ~49 h) wäre rausgefallen und hätte 0 geliefert, also *auch* falsch. + +**Was still betroffen war** (die Laufzeit-Uhr war nur das Sichtbare): +`open_time` = `p.time − off` landete **12,5 h in der ZUKUNFT** → Laufzeit 0:00 · +Time-Stop-Alter in `trailing.py:504` wurde negativ → **der 120-min-Time-Stop hätte +nicht ausgelöst** · `deal.time`-Umrechnung beim externen Close · alle MQL5-Chart-Anker +(Kanal, Liquiditäts-Trendlinie, Trade-Marker). + +**Fix:** Der Offset wird **nur noch von einem frischen Tick** (neu) bestimmt, sonst gilt +der zuletzt gültige Wert (`self._boff`, vorbelegt mit `_BROKER_OFF_DEFAULT_S` = UTC+3 — +dieselbe Konstante nutzt `engine.get_bars` schon als `broker_utc_offset`). Die Frische +wird am **bereits bekannten** Offset gemessen (`stale = now + _boff − tick.time`), nicht +am Tick selbst — damit nicht zirkulär. Schwelle `_BROKER_TICK_FRESH_S = 900` liegt unter +dem halben Rundungsraster (1800/2), sodass die 30-min-Rundung immer auf den echten +Offset fällt. Ein echter Zeitzonen-Wechsel (Broker-DST) wird beim ersten frischen Tick +übernommen **und geloggt** (statt still zu passieren). + +⚠ **Einordnung: Deployment-Drift** (Fall 4 in der Tabelle unten) — entworfen unter „Tick +ist frisch", betrieben unter „Tick kann beliebig alt sein". Kein Backtest hätte das +gefunden; Backtests rechnen nie mit Broker-Wallclock. Bestätigt die dortige Erwartung, +dass die Liste nicht vollständig war. + +**Startwert der Stillstands-Uhr (gleicher Fix):** Weil der Offset jetzt verlässlich ist, +wird `_last_bid_move_ts` beim **ersten** Snapshot aus dem ECHTEN Tick-Alter vorbelegt +(`_now + _broker_offset_s() − tick_server_ts`, Plausibilität 0…14 Tage) statt bei null zu +beginnen. Vorher brauchte der Bot nach **jedem** `restart_server.bat` volle 180 s, um ein +längst geschlossenes Wochenende zu bemerken — real lief der User genau in dieses +Blindfenster („KURS" statt „KURS · ZU", G/V auf dem eingefrorenen Broker-Wert). +Im **laufenden** Betrieb bleibt die Erkennung bewusst bewegungs-basiert: das fängt auch +einen eingefrorenen Feed, dessen Zeitstempel weiterlaufen. +Verifiziert nach dem Neustart: `market_closed=True` sofort, `tick_age_s` 56 844 s +(= Rohwert-Gegenrechnung auf **0 s** genau), `open_time` → 31.07. 22:54, Laufzeit +15,8 h. + +### ⚠ Stiller `catch` im REST-Poll verschluckte JEDEN Render-Fehler (Fix 2026-08-01) + +`pollSnapshot()` hatte `try { fetch → _applySnapshot(d) } catch {}` — der `catch` sollte +Netz-Aussetzer schlucken, umschloss aber auch `render()`. Ein Render-Fehler wurde damit +**lautlos** verschluckt: der Poll feuerte weiter alle 4 s, jeder Durchlauf starb still, +das Dashboard fror auf dem letzten guten Stand ein **und die Konsole blieb leer**. Genau +diese Kombination (eingefrorene Anzeige ohne jede Fehlermeldung) hat die Fehlersuche +unnötig lang gemacht. Jetzt ist nur noch der `fetch` im `try`; Netzfehler bleiben still, +**Render-Fehler werden laut**. Ergänzt die Null-Sicher-Regel von oben: die verhindert den +Absturz, das hier macht ihn sichtbar, falls er doch passiert. + +**Beschriftung (User 2026-08-01):** Die Header-Labels heißen ausgeschrieben +**„PEPPERSTONE KURS"** und **„HYPERLIQUID KURS"** (vorher „KURS"/„HL" — ließ offen, +welcher Kurs woher kommt). Bei geschlossenem Markt: „PEPPERSTONE KURS · ZU". ## Web-API (server.py) `GET /api/snapshot` (inkl. `verdict`-Aggregat) `/api/health` `/api/logs` `/api/stats` `/api/news` `/api/bars?tf=&n=` (Charts, read-only) `/api/squeeze_monitor` (B4-Monitor, read-only, Engine-Cache 60 s) · `WS /ws` · @@ -2243,6 +2305,7 @@ Bedingungen BETRIEBEN als VALIDIERT. An EINEM Tag wurden drei Fälle gefunden: | 1 | P(break) | Anlauf zu **fixiertem** Level | **dynamisch** gewähltes Level | AUC 0,715 → **0,368** | | 2 | `breakout_k=0,3` | **feste** Zeitebene | TF wechselt 33×/Tag, löscht den Anker | 43 % → **93 % WARTEN** | | 3 | Trailing `mult` | **1,5** | **1,5–3,0** je nach TF | H1 ΣR −249 → **−1367**, Worst −1,50 → −2,50 | +| 4 | `_broker_offset_s()` | **frischem** Tick | Tick beliebig alt (Wochenende) | Offset **−9,5 h statt +3 h** → `open_time` 12,5 h in der Zukunft, **Time-Stop-Alter negativ** | Keiner wäre durch MEHR Backtesting gefunden worden — die Backtests waren korrekt. **Ursachen (strukturell):** (a) Backtests sind NACHBAUTEN, keine Nutzer des Live-Codes diff --git a/core/engine.py b/core/engine.py index 58c9907..5f30bd9 100644 --- a/core/engine.py +++ b/core/engine.py @@ -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: diff --git a/core/trader.py b/core/trader.py index 80fbdb1..55554bb 100644 --- a/core/trader.py +++ b/core/trader.py @@ -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, diff --git a/web/app.js b/web/app.js index ec79047..da062cd 100644 --- a/web/app.js +++ b/web/app.js @@ -491,7 +491,7 @@ function render(d) { // Rest normal weiter, nur die neuen Felder fehlen, bis die HTML nachgeladen ist. const lblEl = $("hdr-price-lbl"); if (lblEl) { - lblEl.textContent = d.market_closed ? "KURS · ZU" : "KURS"; + lblEl.textContent = d.market_closed ? "PEPPERSTONE KURS · ZU" : "PEPPERSTONE KURS"; lblEl.title = d.market_closed ? `Pepperstone geschlossen — letzter Tick vor ${Math.round((d.tick_age_s || 0) / 60)} min` : ""; @@ -903,10 +903,17 @@ function connect() { const POLL_MS = 4000; async function pollSnapshot() { if (document.hidden) return; // seite unsichtbar → spart Akku/Daten, Wake-Handler holt beim Zurückkommen sofort nach + let d; try { - const d = await (await fetch("api/snapshot")).json(); - _applySnapshot(d); - } catch (e) { /* still — WS-Reconnect kümmert sich, das ist nur der Fallback */ } + d = await (await fetch("api/snapshot")).json(); + } catch (e) { return; } // still — Netz-/WS-Aussetzer, dafür IST der Fallback da + // ⚠ NICHT mit ins try oben (Fix 2026-08-01): der Poll-`catch` umschloss früher + // auch `_applySnapshot` → `render`. Ein Render-Fehler wurde damit LAUTLOS + // verschluckt: der Poll feuerte weiter alle 4 s, jeder Durchlauf starb still, + // das Dashboard fror auf dem letzten guten Stand ein und die Konsole blieb + // leer. Genau diese Kombination hat eine Fehlersuche unnötig lang gemacht. + // Netzfehler bleiben still (die sind erwartbar), Render-Fehler werden laut. + _applySnapshot(d); } setInterval(pollSnapshot, POLL_MS); diff --git a/web/index.html b/web/index.html index 8abb623..7a5e281 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -34,11 +34,12 @@
-
+
-
+ also direkt vergleichbar. Beide Quellen AUSGESCHRIEBEN (User 2026-08-01): + „KURS"/„HL" ließ offen, welcher Kurs woher kommt. --> +
@@ -248,6 +249,6 @@ - +