diff --git a/core/engine.py b/core/engine.py index 2ff0137..0bca046 100644 --- a/core/engine.py +++ b/core/engine.py @@ -2960,7 +2960,7 @@ class TradingEngine: self.trail.toggle(sym) return None - def close(self) -> str | None: + def close(self, slot: str | None = None) -> str | None: """Schliesst die offene Position — BEIDE Slots (Fix 2026-08-20). ⚠⚠ Bis heute lief das nur auf `self.trader`. Hielt der BRK-Slot eine @@ -2973,7 +2973,16 @@ class TradingEngine: Sind beide offen, werden beide geschlossen und Fehler gesammelt gemeldet. """ fehler = [] - for tr, name in ((self.trader, "Slot 1"), (self.trader_brk, "BRK")): + # ⚠ `slot` waehlt gezielt: None/"" = BEIDE (Verhalten des CLOSE-Knopfs in + # der Aktionsleiste), "main" = nur Slot 1, "brk" = nur der + # Breakout-Autotrader. Die zwei kleinen Knoepfe an den G/V-Anzeigen + # nutzen die gezielte Form (User-Wunsch 2026-08-20). + paare = [(self.trader, "Slot 1"), (self.trader_brk, "BRK")] + if slot == "main": + paare = paare[:1] + elif slot == "brk": + paare = paare[1:] + for tr, name in paare: try: if not (tr.snapshot() or {}).get("ticket"): continue diff --git a/server.py b/server.py index 77fcf2b..e6b5fbf 100644 --- a/server.py +++ b/server.py @@ -299,7 +299,9 @@ async def order(payload: dict = Body(...), elif side == "short": fn = eng.open_short elif side == "close": - fn = eng.close + # ⚠ optionaler `slot`: None = beide, "main" = Slot 1, "brk" = Breakout. + _slot = (payload or {}).get("slot") if isinstance(payload, dict) else None + fn = (lambda: eng.close(_slot)) if _slot else eng.close else: raise HTTPException(status_code=400, detail="side muss long|short|close sein") log.info(f"[WEB] Order angefordert: {side}") diff --git a/web/app.js b/web/app.js index dc2b563..f7f068c 100644 --- a/web/app.js +++ b/web/app.js @@ -1577,8 +1577,16 @@ let lastRecvMs = 0; // Getrennt davon: wann kam zuletzt eine WS-NACHRICHT? Nur diese Uhr entscheidet, // ob der Poll uebersprungen wird (s. Kommentar in `sock.onmessage`). let lastWsMs = 0; +// ⚠ Letzter gerenderter Snapshot — gebraucht von Klick-Handlern, die den +// AKTUELLEN Zustand kennen muessen (z. B. der P&L fuer die Sicherheitsabfrage +// der Slot-Close-Knoepfe). Vorher gab es das nicht; mein erster Entwurf hat +// eine Variable `_letzterSnap` ERFUNDEN — genau der ReferenceError, der am +// 19.08. den halben Render abgeschossen hat, nur diesmal im Klick-Pfad. +let letzterSnap = null; + function _applySnapshot(d) { if (typeof d.ts === "number" && d.ts <= lastTs) return; // schon Neueres gezeigt + letzterSnap = d; if (typeof d.ts === "number") lastTs = d.ts; lastRecvMs = Date.now(); render(d); @@ -2308,3 +2316,34 @@ async function setMgGlobal() { }); }); } + + +// --- Close je Slot (User-Wunsch 2026-08-20) -------------------------------- +// ⚠ Der grosse CLOSE-Knopf unten bleibt der Hauptweg und schliesst BEIDE Slots. +// Diese zwei sind fuer den Fall, dass genau EINE Position weg soll. +// ⚠ Sicherheitsabfrage NUR im Minus — dieselbe Regel wie beim grossen Knopf: +// im Plus direkt schliessen, im Minus nachfragen (hartes Blockieren ist im +// Projekt verworfen, aber ein versehentlicher Klick soll kein Geld kosten). +async function closeSlot(slot, label, pnl) { + if (pnl != null && pnl < 0 && + !confirm(`${label} steht bei ${pnl.toFixed(2)} € im MINUS — wirklich schliessen?`)) { + return; + } + const tok = await ensureToken(); + if (tok === null) return; + try { + const r = await fetch("api/order", { method: "POST", + headers: { "X-Auth-Token": tok, "Content-Type": "application/json" }, + body: JSON.stringify({ side: "close", slot: slot }) }); + if (r.status === 401) { clearToken(); throw new Error("401"); } + const j = await r.json(); + toast(j.ok ? `${label} geschlossen` : (j.error || "Close fehlgeschlagen"), !!j.ok); + } catch (e) { + toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false); + } +} +{ + const a = $streng("close-main"), b = $streng("close-brk"); + if (a) a.onclick = () => closeSlot("main", "Slot 1", letzterSnap && letzterSnap.position ? letzterSnap.position.pnl : null); + if (b) b.onclick = () => closeSlot("brk", "BRK", letzterSnap && letzterSnap.position_brk ? letzterSnap.position_brk.pnl : null); +} diff --git a/web/index.html b/web/index.html index 929e0e7..126a92d 100644 --- a/web/index.html +++ b/web/index.html @@ -12,7 +12,7 @@ ⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard und muss offline funktionieren. --> - + @@ -53,10 +53,12 @@
- + +
- + +
@@ -416,6 +418,6 @@ - + diff --git a/web/style.css b/web/style.css index 44cec17..a457126 100644 --- a/web/style.css +++ b/web/style.css @@ -688,3 +688,17 @@ main{padding-bottom:4.1111rem} /* Platz für die fixe Leiste */ .px-split .px-half{display:flex; flex-direction:column; min-width:0} .px-split .px-half.rechts{align-items:flex-end; text-align:right} .px-split .px-half label{white-space:nowrap} + +/* ── Close-Knopf je Slot, VOR der G/V-Anzeige (User-Wunsch 2026-08-20) ──── + ⚠ Bewusst klein und zurueckhaltend: der grosse CLOSE-Knopf unten bleibt der + Hauptweg (schliesst BEIDE Slots). Diese zwei sind fuer den Fall, dass genau + EINE der beiden Positionen weg soll. */ +.px-line{display:flex; align-items:center; gap:0.3333rem} +.px-half.rechts .px-line{flex-direction:row-reverse} +.px-close{ + background:transparent; border:1px solid var(--border); border-radius:var(--r-sm); + color:var(--dim); cursor:pointer; font-size:0.7222rem; line-height:1; + padding:0.1667rem 0.3333rem; font-family:inherit; +} +.px-close:hover{color:#f85149; border-color:#5c1a1a} +.px-close[disabled]{opacity:.3; cursor:default}