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:
co-authored by
Claude Opus 5
parent
2ea17ce3e0
commit
b8bb1d260f
@@ -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,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
|
||||
|
||||
+20
-1
@@ -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
@@ -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,
|
||||
|
||||
+11
-4
@@ -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);
|
||||
|
||||
|
||||
+6
-5
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#0d1117">
|
||||
<title>Oil · MT5</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="stylesheet" href="style.css?v=130">
|
||||
<link rel="stylesheet" href="style.css?v=131">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Modul-Menü (ganz oben) -->
|
||||
@@ -34,11 +34,12 @@
|
||||
<div class="px"><label>BALANCE</label><span id="balance">—</span></div>
|
||||
<div class="px"><label>TAG P/L</label><span id="day-pl">—</span></div>
|
||||
<div class="px"><label>EQUITY</label><span id="equity">—</span></div>
|
||||
<div class="px"><label id="hdr-price-lbl">KURS</label><span id="hdr-price">—</span></div>
|
||||
<div class="px"><label id="hdr-price-lbl">PEPPERSTONE KURS</label><span id="hdr-price">—</span></div>
|
||||
<!-- Hyperliquid-Kurs NEBEN dem Broker-Kurs (User 2026-08-01). HL handelt 24/7,
|
||||
Pepperstone nicht — am Wochenende der einzige lebende Kurs. Basis-korrigiert,
|
||||
also direkt vergleichbar. -->
|
||||
<div class="px" id="px-hl"><label>HL</label><span id="hdr-hl">—</span></div>
|
||||
also direkt vergleichbar. Beide Quellen AUSGESCHRIEBEN (User 2026-08-01):
|
||||
„KURS"/„HL" ließ offen, welcher Kurs woher kommt. -->
|
||||
<div class="px" id="px-hl"><label>HYPERLIQUID KURS</label><span id="hdr-hl">—</span></div>
|
||||
<div class="px"><label>G/V</label><span id="hdr-pnl">—</span></div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -248,6 +249,6 @@
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
||||
<script src="app.js?v=130"></script>
|
||||
<script src="app.js?v=131"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user