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:
co-authored by
Claude Opus 4.8
parent
e9653a842a
commit
919ed7c3ee
@@ -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
|
(Kurs/PnL/Felder/Logs) = `--mono`. Karten mit Elevation-Schatten, Fokus-Ringe auf
|
||||||
Feldern, Transitions auf Tabs/Buttons. Neutrale Hex-Literale → Tokens (Konsistenz).
|
Feldern, Transitions auf Tabs/Buttons. Neutrale Hex-Literale → Tokens (Konsistenz).
|
||||||
- **Trade-Leiste** (`#trade-bar`, über der Gesamtempfehlung, nur bei offener Position,
|
- **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
|
`.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).
|
(`#srmin-input`)** (Notfall-Close-Feld **2026-07-17 entfernt** — Notfall-Stop ganz aus).
|
||||||
SL/TP: Enter/Verlassen →
|
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
|
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
|
||||||
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
|
||||||
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
|
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
|
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
|
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` —
|
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` —
|
||||||
|
|||||||
+20
-2
@@ -55,6 +55,7 @@ class TradeManager:
|
|||||||
self.ticket = self.order_type = None
|
self.ticket = self.order_type = None
|
||||||
self.entry_price = self.lots = self.pnl = self.cur_price = 0.0
|
self.entry_price = self.lots = self.pnl = self.cur_price = 0.0
|
||||||
self.sl = self.tp = self.margin = 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.symbol = None
|
||||||
self.last_error = ""
|
self.last_error = ""
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
@@ -241,6 +242,7 @@ class TradeManager:
|
|||||||
self.ticket = None; self.order_type = None
|
self.ticket = None; self.order_type = None
|
||||||
self.entry_price = 0.0; self.lots = 0.0
|
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.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
|
self._swap = 0.0; self._tick_size = None; self._tick_value = None
|
||||||
return ""
|
return ""
|
||||||
return "Close: " + _retcode_msg(res)
|
return "Close: " + _retcode_msg(res)
|
||||||
@@ -366,12 +368,14 @@ class TradeManager:
|
|||||||
own = [p for p in all_pos if getattr(p, "magic", 0) == MAGIC]
|
own = [p for p in all_pos if getattr(p, "magic", 0) == MAGIC]
|
||||||
pick = own[0] if own else all_pos[0]
|
pick = own[0] if own else all_pos[0]
|
||||||
pos_sym = getattr(pick, "symbol", sym)
|
pos_sym = getattr(pick, "symbol", sym)
|
||||||
|
_boff = self._broker_offset_s(pos_sym)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.ticket = pick.ticket; self.order_type = pick.type
|
self.ticket = pick.ticket; self.order_type = pick.type
|
||||||
self.entry_price = pick.price_open; self.symbol = pos_sym
|
self.entry_price = pick.price_open; self.symbol = pos_sym
|
||||||
self.lots = pick.volume; self.pnl = pick.profit
|
self.lots = pick.volume; self.pnl = pick.profit
|
||||||
self.sl = float(getattr(pick, "sl", 0.0) or 0.0)
|
self.sl = float(getattr(pick, "sl", 0.0) or 0.0)
|
||||||
self.tp = float(getattr(pick, "tp", 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)
|
# Adoptierter Trade ohne SL → Schutz-SL nachrüsten (Lock gehalten)
|
||||||
if not self.sl:
|
if not self.sl:
|
||||||
psl, _ptp, _ps = self._calc_sl_tp(pos_sym, pick.type,
|
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.ticket = None; self.order_type = None
|
||||||
self.entry_price = 0.0; self.pnl = 0.0; self.lots = 0.0
|
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.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._swap = 0.0; self._commission = 0.0
|
||||||
self._tick_size = None; self._tick_value = None
|
self._tick_size = None; self._tick_value = None
|
||||||
return
|
return
|
||||||
@@ -456,6 +461,13 @@ class TradeManager:
|
|||||||
margin = float(_m) if _m else 0.0
|
margin = float(_m) if _m else 0.0
|
||||||
except Exception:
|
except Exception:
|
||||||
margin = 0.0
|
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:
|
with self._lock:
|
||||||
self.lots = p.volume
|
self.lots = p.volume
|
||||||
self._swap = swap
|
self._swap = swap
|
||||||
@@ -464,6 +476,8 @@ class TradeManager:
|
|||||||
self.sl = float(getattr(p, "sl", 0.0) or 0.0)
|
self.sl = float(getattr(p, "sl", 0.0) or 0.0)
|
||||||
self.tp = float(getattr(p, "tp", 0.0) or 0.0)
|
self.tp = float(getattr(p, "tp", 0.0) or 0.0)
|
||||||
self.margin = margin
|
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
|
self.cur_price = (tick.bid if p.type == mt5.ORDER_TYPE_BUY
|
||||||
else tick.ask) if tick else p.price_current
|
else tick.ask) if tick else p.price_current
|
||||||
if si:
|
if si:
|
||||||
@@ -483,7 +497,7 @@ class TradeManager:
|
|||||||
diff = (cur - ep) if otype == mt5.ORDER_TYPE_BUY else (ep - cur)
|
diff = (cur - ep) if otype == mt5.ORDER_TYPE_BUY else (ep - cur)
|
||||||
return diff / ts * tv * lots + swap
|
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
|
Broker-Serverzeit minus UTC in Sekunden, auf 30 min gerundet
|
||||||
(z.B. UTC+3 → 10800). MT5 liefert deal.time/tick.time in
|
(z.B. UTC+3 → 10800). MT5 liefert deal.time/tick.time in
|
||||||
@@ -494,7 +508,10 @@ class TradeManager:
|
|||||||
sonst 0 (keine Korrektur).
|
sonst 0 (keine Korrektur).
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
tick = mt5.symbol_info_tick(sym) if sym else None
|
||||||
if tick and tick.time:
|
if tick and tick.time:
|
||||||
off = round((tick.time - time.time()) / 1800) * 1800
|
off = round((tick.time - time.time()) / 1800) * 1800
|
||||||
@@ -706,4 +723,5 @@ class TradeManager:
|
|||||||
entry_price=self.entry_price, lots=self.lots,
|
entry_price=self.entry_price, lots=self.lots,
|
||||||
pnl=self.pnl, cur_price=self.cur_price,
|
pnl=self.pnl, cur_price=self.cur_price,
|
||||||
sl=self.sl, tp=self.tp, margin=self.margin,
|
sl=self.sl, tp=self.tp, margin=self.margin,
|
||||||
|
open_time=(self.open_time if self.ticket else 0.0),
|
||||||
)
|
)
|
||||||
|
|||||||
+29
@@ -50,6 +50,30 @@ function beepCloseAlert() {
|
|||||||
// der Button blinkt (Empfehlung aktiv & flat) — einmaliger Piep war überhörbar.
|
// der Button blinkt (Empfehlung aktiv & flat) — einmaliger Piep war überhörbar.
|
||||||
const _REC_REBEEP_MS = 30000;
|
const _REC_REBEEP_MS = 30000;
|
||||||
let _lastRecBeep = 0, _lastRecSignal = null, _audioHintShown = false, _lastSrCloseCount = null;
|
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) {
|
function beepRecommendation(sig, isNew) {
|
||||||
if (_muted) return; // auch kein Sperr-Toast
|
if (_muted) return; // auch kein Sperr-Toast
|
||||||
if (!_audioCtx || _audioCtx.state !== "running") {
|
if (!_audioCtx || _audioCtx.state !== "running") {
|
||||||
@@ -386,12 +410,17 @@ function render(d) {
|
|||||||
dEl.textContent = long ? "▲ LONG" : "▼ SHORT";
|
dEl.textContent = long ? "▲ LONG" : "▼ SHORT";
|
||||||
$("tb-lots").textContent = fmt(p.lots, 2);
|
$("tb-lots").textContent = fmt(p.lots, 2);
|
||||||
$("tb-margin").textContent = p.margin ? cs + fmt(p.margin, 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)
|
// SL/TP-Felder nur befüllen, wenn nicht gerade bearbeitet (kein Tipp-Konflikt)
|
||||||
const slEl = $("tb-sl"), tpEl = $("tb-tp");
|
const slEl = $("tb-sl"), tpEl = $("tb-tp");
|
||||||
if (document.activeElement !== slEl) slEl.value = p.sl ? fmt(p.sl, 2) : "";
|
if (document.activeElement !== slEl) slEl.value = p.sl ? fmt(p.sl, 2) : "";
|
||||||
if (document.activeElement !== tpEl) tpEl.value = p.tp ? fmt(p.tp, 2) : "";
|
if (document.activeElement !== tpEl) tpEl.value = p.tp ? fmt(p.tp, 2) : "";
|
||||||
} else {
|
} else {
|
||||||
tb.classList.add("hidden");
|
tb.classList.add("hidden");
|
||||||
|
_tradeOpenTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -6,7 +6,7 @@
|
|||||||
<meta name="theme-color" content="#0d1117">
|
<meta name="theme-color" content="#0d1117">
|
||||||
<title>Oil · MT5</title>
|
<title>Oil · MT5</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="stylesheet" href="style.css?v=117">
|
<link rel="stylesheet" href="style.css?v=118">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -44,8 +44,10 @@
|
|||||||
<div id="circuit-banner" class="circuit hidden"></div>
|
<div id="circuit-banner" class="circuit hidden"></div>
|
||||||
<!-- Aktueller Trade (nur bei offener Position) — Anzeige + SL/TP/Close editierbar -->
|
<!-- Aktueller Trade (nur bei offener Position) — Anzeige + SL/TP/Close editierbar -->
|
||||||
<section class="card wide trade-bar hidden" id="trade-bar">
|
<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">
|
<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>
|
<span class="tb-field"><label>Richtung</label>
|
||||||
<b class="tb-val tb-dir" id="tb-dir">—</b></span>
|
<b class="tb-val tb-dir" id="tb-dir">—</b></span>
|
||||||
<span class="tb-field"><label>Lots</label>
|
<span class="tb-field"><label>Lots</label>
|
||||||
@@ -226,6 +228,6 @@
|
|||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="lightweight-charts.standalone.production.js?v=114"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 */
|
#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;
|
.tb-title{margin:0 0 8px;font-size:14px;letter-spacing:1.5px;text-transform:uppercase;
|
||||||
color:var(--dim);font-weight:600}
|
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}
|
.tb-row{display:flex;flex-wrap:wrap;gap:10px}
|
||||||
/* EINHEITLICHES Feld-Design: Anzeige-Boxen (Richtung/Lots/Einsatz) und Eingabe-
|
/* EINHEITLICHES Feld-Design: Anzeige-Boxen (Richtung/Lots/Einsatz) und Eingabe-
|
||||||
Felder (SL/TP/Gewinn-Close/Notfall-Close) sehen identisch aus (Label über Box). */
|
Felder (SL/TP/Gewinn-Close/Notfall-Close) sehen identisch aus (Label über Box). */
|
||||||
|
|||||||
Reference in New Issue
Block a user