From 919ed7c3ee1599d3dcc3068918a27c6fea4450da Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 30 Jul 2026 13:40:41 +0200 Subject: [PATCH] Trade-Leiste: Laufzeit-Uhr des offenen Trades (v=118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - trader: open_time (echte Epoch, Broker-Offset korrigiert) aus position.time, im Snapshot; nur 1x je Position berechnet, beim Close auf 0. - _broker_offset_s(sym=None): Symbol explizit uebergebbar -- beim Adoptieren ist self.symbol noch nicht gesetzt (war Offset 0 -> Zeit 3h in der Zukunft); zusaetzlich Selbstheilung bei Zukunftswerten. - app.js/index.html/style.css: Feld #tb-dur + Titel-Uhr #tb-runtime, eigener 1-s-Ticker (flüssig zwischen Snapshots), ab 2h bernstein (Time-Stop-Naehe). - Verifiziert: Live-Trade 48898382 open_time == Log-Zeile 13:22:48. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 15 +++++++++++++-- core/trader.py | 22 ++++++++++++++++++++-- web/app.js | 29 +++++++++++++++++++++++++++++ web/index.html | 8 +++++--- web/style.css | 4 ++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 312b0ac..2f2855d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -950,7 +950,18 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: (Kurs/PnL/Felder/Logs) = `--mono`. Karten mit Elevation-Schatten, Fokus-Ringe auf Feldern, Transitions auf Tabs/Buttons. Neutrale Hex-Literale → Tokens (Konsistenz). - **Trade-Leiste** (`#trade-bar`, über der Gesamtempfehlung, nur bei offener Position, - 2026-07-15): eine Zeile Richtung (▲LONG/▼SHORT) · Lots · Einsatz · **3 Eingabefelder + 2026-07-15): **Laufzeit-Uhr** (2026-07-30, User-Wunsch „Uhr … wie lange diese schon + läuft"): `trader.snapshot()` liefert `open_time` = Öffnungszeit als **echte Epoch** + (Broker-Offset korrigiert, aus `position.time`); Frontend zeigt sie als Feld + `#tb-dur` **und** kompakt im Kachel-Titel (`#tb-runtime`, „⏱ 1:23:45"). Der Ticker + läuft **eigenständig 1×/s** (`_tickTradeDuration`), zählt also flüssig zwischen den + Snapshots; ab **2 h** bernstein (Time-Stop-Nähe: 120 min in Phase „Init"). ⚠ Zwei + Fallen beim Bau: (1) `_broker_offset_s()` liest `self.symbol`, die beim **Adoptieren** + einer Position noch NICHT gesetzt ist → Offset 0 → Zeit lag 3 h in der Zukunft; die + Funktion nimmt jetzt `sym` als Parameter (`_broker_offset_s(pos_sym)`). (2) Selbst- + heilung: liegt `open_time` >60 s in der Zukunft, wird sie neu berechnet; beim Close + auf 0 zurückgesetzt (kein stale Wert für den Folge-Trade). Berechnung nur 1× je + Position (nicht je 1-s-Refresh). Weiter in der Zeile: Richtung (▲LONG/▼SHORT) · Lots · Einsatz · **3 Eingabefelder `.tb-field` (Label über Feld, rot=`.stop`/grün=`.gain`): SL · TP · Gewinn-Close (`#srmin-input`)** (Notfall-Close-Feld **2026-07-17 entfernt** — Notfall-Stop ganz aus). SL/TP: Enter/Verlassen → @@ -1298,7 +1309,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=117** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=118** (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` — diff --git a/core/trader.py b/core/trader.py index 65a497c..80fbdb1 100644 --- a/core/trader.py +++ b/core/trader.py @@ -55,6 +55,7 @@ class TradeManager: self.ticket = self.order_type = None self.entry_price = self.lots = self.pnl = self.cur_price = 0.0 self.sl = self.tp = self.margin = 0.0 + self.open_time = 0.0 # Öffnungszeit als ECHTE Epoch (Broker-Offset korrigiert) self.symbol = None self.last_error = "" self._lock = threading.Lock() @@ -241,6 +242,7 @@ class TradeManager: self.ticket = None; self.order_type = None self.entry_price = 0.0; self.lots = 0.0 self.pnl = 0.0; self.cur_price = 0.0; self.sl = self.tp = self.margin = 0.0 + self.open_time = 0.0 self._swap = 0.0; self._tick_size = None; self._tick_value = None return "" return "Close: " + _retcode_msg(res) @@ -366,12 +368,14 @@ class TradeManager: own = [p for p in all_pos if getattr(p, "magic", 0) == MAGIC] pick = own[0] if own else all_pos[0] pos_sym = getattr(pick, "symbol", sym) + _boff = self._broker_offset_s(pos_sym) with self._lock: self.ticket = pick.ticket; self.order_type = pick.type self.entry_price = pick.price_open; self.symbol = pos_sym self.lots = pick.volume; self.pnl = pick.profit self.sl = float(getattr(pick, "sl", 0.0) or 0.0) self.tp = float(getattr(pick, "tp", 0.0) or 0.0) + self.open_time = float(getattr(pick, "time", 0) or 0) - _boff # Adoptierter Trade ohne SL → Schutz-SL nachrüsten (Lock gehalten) if not self.sl: psl, _ptp, _ps = self._calc_sl_tp(pos_sym, pick.type, @@ -438,6 +442,7 @@ class TradeManager: self.ticket = None; self.order_type = None self.entry_price = 0.0; self.pnl = 0.0; self.lots = 0.0 self.cur_price = 0.0; self.sl = self.tp = self.margin = 0.0 + self.open_time = 0.0 self._swap = 0.0; self._commission = 0.0 self._tick_size = None; self._tick_value = None return @@ -456,6 +461,13 @@ class TradeManager: margin = float(_m) if _m else 0.0 except Exception: margin = 0.0 + # Öffnungszeit nur EINMAL je Position berechnen (spart den Offset-Tick-Call je + # 1-s-Refresh) — ändert sich nicht, solange die Position offen ist. + _open_time = None + # Neu berechnen, wenn noch unbekannt ODER offensichtlich falsch (in der + # Zukunft = Offset war beim Adoptieren nicht ermittelbar → Selbstheilung). + if not self.open_time or self.open_time > now + 60: + _open_time = float(getattr(p, "time", 0) or 0) - self._broker_offset_s(sym) with self._lock: self.lots = p.volume self._swap = swap @@ -464,6 +476,8 @@ class TradeManager: self.sl = float(getattr(p, "sl", 0.0) or 0.0) self.tp = float(getattr(p, "tp", 0.0) or 0.0) self.margin = margin + if _open_time is not None: + self.open_time = _open_time self.cur_price = (tick.bid if p.type == mt5.ORDER_TYPE_BUY else tick.ask) if tick else p.price_current if si: @@ -483,7 +497,7 @@ class TradeManager: diff = (cur - ep) if otype == mt5.ORDER_TYPE_BUY else (ep - cur) return diff / ts * tv * lots + swap - def _broker_offset_s(self) -> int: + def _broker_offset_s(self, sym: str | None = None) -> int: """ Broker-Serverzeit minus UTC in Sekunden, auf 30 min gerundet (z.B. UTC+3 → 10800). MT5 liefert deal.time/tick.time in @@ -494,7 +508,10 @@ class TradeManager: sonst 0 (keine Korrektur). """ try: - sym = self.symbol + # `sym` explizit übergebbar: beim Adoptieren einer Position ist + # `self.symbol` noch nicht gesetzt → sonst kein Tick → Offset 0 und + # die Öffnungszeit bliebe rohe Broker-Zeit (3 h in der Zukunft). + 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 @@ -706,4 +723,5 @@ class TradeManager: entry_price=self.entry_price, lots=self.lots, pnl=self.pnl, cur_price=self.cur_price, sl=self.sl, tp=self.tp, margin=self.margin, + open_time=(self.open_time if self.ticket else 0.0), ) diff --git a/web/app.js b/web/app.js index 439fc32..d51ae84 100644 --- a/web/app.js +++ b/web/app.js @@ -50,6 +50,30 @@ function beepCloseAlert() { // der Button blinkt (Empfehlung aktiv & flat) — einmaliger Piep war überhörbar. const _REC_REBEEP_MS = 30000; let _lastRecBeep = 0, _lastRecSignal = null, _audioHintShown = false, _lastSrCloseCount = null; + +// ── Laufzeit-Uhr des offenen Trades ─────────────────────────────────────────── +// Öffnungszeit kommt als echte Epoch (Broker-Offset serverseitig korrigiert) im +// Snapshot (`position.open_time`); der Ticker läuft eigenständig jede Sekunde, +// damit die Uhr auch zwischen den Snapshots (4 s Poll / 1 s WS) flüssig zählt. +let _tradeOpenTime = 0; +function _fmtDuration(sec) { + sec = Math.max(0, Math.floor(sec)); + const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; + const p2 = (n) => String(n).padStart(2, "0"); + return h > 0 ? `${h}:${p2(m)}:${p2(s)}` : `${m}:${p2(s)}`; +} +function _tickTradeDuration() { + const el = document.getElementById("tb-dur"), hd = document.getElementById("tb-runtime"); + if (!el) return; + if (!_tradeOpenTime) { el.textContent = "—"; if (hd) hd.textContent = ""; return; } + const sec = Date.now() / 1000 - _tradeOpenTime; + const txt = _fmtDuration(sec); + el.textContent = txt; + if (hd) hd.textContent = "⏱ " + txt; + // Ab 2 h dezent hervorheben (Time-Stop greift bei 120 min in Phase „Init") + el.className = "tb-val" + (sec >= 7200 ? " warn" : ""); +} +setInterval(_tickTradeDuration, 1000); function beepRecommendation(sig, isNew) { if (_muted) return; // auch kein Sperr-Toast if (!_audioCtx || _audioCtx.state !== "running") { @@ -386,12 +410,17 @@ function render(d) { dEl.textContent = long ? "▲ LONG" : "▼ SHORT"; $("tb-lots").textContent = fmt(p.lots, 2); $("tb-margin").textContent = p.margin ? cs + fmt(p.margin, 2) : "—"; + // Laufzeit: Öffnungszeit merken (echte Epoch, Broker-Offset korrigiert) und + // sekündlich per Ticker anzeigen — unabhängig vom Snapshot-Intervall. + _tradeOpenTime = p.open_time ? Number(p.open_time) : 0; + _tickTradeDuration(); // SL/TP-Felder nur befüllen, wenn nicht gerade bearbeitet (kein Tipp-Konflikt) const slEl = $("tb-sl"), tpEl = $("tb-tp"); if (document.activeElement !== slEl) slEl.value = p.sl ? fmt(p.sl, 2) : ""; if (document.activeElement !== tpEl) tpEl.value = p.tp ? fmt(p.tp, 2) : ""; } else { tb.classList.add("hidden"); + _tradeOpenTime = 0; } } diff --git a/web/index.html b/web/index.html index b476ba3..67b3442 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -44,8 +44,10 @@