Trade-Leiste: Laufzeit-Uhr des offenen Trades (v=118)

- 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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-30 13:40:41 +02:00
co-authored by Claude Opus 4.8
parent e9653a842a
commit 919ed7c3ee
5 changed files with 71 additions and 7 deletions
+13 -2
View File
@@ -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:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge`
+20 -2
View File
@@ -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),
)
+29
View File
@@ -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;
}
}
+5 -3
View File
@@ -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=117">
<link rel="stylesheet" href="style.css?v=118">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -44,8 +44,10 @@
<div id="circuit-banner" class="circuit hidden"></div>
<!-- Aktueller Trade (nur bei offener Position) — Anzeige + SL/TP/Close editierbar -->
<section class="card wide trade-bar hidden" id="trade-bar">
<h2 class="tb-title">aktueller Trade läuft</h2>
<h2 class="tb-title">aktueller Trade läuft <span class="tb-runtime" id="tb-runtime"></span></h2>
<div class="tb-row">
<span class="tb-field"><label>Laufzeit</label>
<b class="tb-val" id="tb-dur"></b></span>
<span class="tb-field"><label>Richtung</label>
<b class="tb-val tb-dir" id="tb-dir"></b></span>
<span class="tb-field"><label>Lots</label>
@@ -226,6 +228,6 @@
<div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=117"></script>
<script src="app.js?v=118"></script>
</body>
</html>
+4
View File
@@ -112,6 +112,10 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
#card-trail.hidden{display:none} /* Trailing-Kachel ausgeblendet, 2026-07-22 */
.tb-title{margin:0 0 8px;font-size:14px;letter-spacing:1.5px;text-transform:uppercase;
color:var(--dim);font-weight:600}
/* Laufzeit-Uhr: im Titel mitlaufend (kompakt) + eigenes Feld in der Zeile */
.tb-runtime{margin-left:8px;color:var(--bright);font-family:var(--mono);
letter-spacing:.5px;text-transform:none}
.tb-field .tb-val.warn{color:var(--accent);border-color:#4a3600}
.tb-row{display:flex;flex-wrap:wrap;gap:10px}
/* EINHEITLICHES Feld-Design: Anzeige-Boxen (Richtung/Lots/Einsatz) und Eingabe-
Felder (SL/TP/Gewinn-Close/Notfall-Close) sehen identisch aus (Label über Box). */