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
+66 -3
View File
@@ -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:<weight> 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,53,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