Close-Knopf je Slot, direkt vor der jeweiligen G/V-Anzeige
User-Wunsch. Zwei Slots heissen zwei Close-Moeglichkeiten - bisher gab es nur
den grossen CLOSE-Knopf, und der schliesst seit heute BEIDE. Neu je ein kleines
✕ vor "G/V" (Slot 1) und vor "G/V · BRK", rechtsbuendig gespiegelt.
BACKEND: `engine.close(slot=None)` - None/"" = beide (Verhalten des grossen
Knopfs, unveraendert), "main" = nur Slot 1, "brk" = nur der Breakout-Autotrader.
`POST /api/order {"side":"close","slot":"brk"}` reicht ihn durch; ohne `slot`
ist das Verhalten bitgleich zu vorher.
⚠ 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.
⚠⚠ EIGENER FEHLER auf dem Weg, gefunden bevor er live ging: mein erster Entwurf
las den P&L aus `_letzterSnap` - eine Variable, die es NICHT gab. Das ist exakt
der ReferenceError, der am 19.08. den halben Render abgeschossen hat, nur
diesmal im Klick-Pfad (er waere erst beim Druecken geknallt, also genau dann,
wenn man schliessen will). Behoben durch `letzterSnap`, das `_applySnapshot`
jetzt fuehrt - die Variable fehlte im Projekt tatsaechlich, Klick-Handler hatten
bisher keinen Zugriff auf den aktuellen Zustand.
Verifiziert: 2 Knoepfe im ausgelieferten HTML, span/div/button ausgeglichen,
keine doppelten IDs, node --check, Render-Probe, 91 Tests und die volle Pipeline
gruen. Deploy verifiziert (v=199).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
14b3fd4f64
commit
d881636e1b
+11
-2
@@ -2960,7 +2960,7 @@ class TradingEngine:
|
|||||||
self.trail.toggle(sym)
|
self.trail.toggle(sym)
|
||||||
return None
|
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).
|
"""Schliesst die offene Position — BEIDE Slots (Fix 2026-08-20).
|
||||||
|
|
||||||
⚠⚠ Bis heute lief das nur auf `self.trader`. Hielt der BRK-Slot eine
|
⚠⚠ 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.
|
Sind beide offen, werden beide geschlossen und Fehler gesammelt gemeldet.
|
||||||
"""
|
"""
|
||||||
fehler = []
|
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:
|
try:
|
||||||
if not (tr.snapshot() or {}).get("ticket"):
|
if not (tr.snapshot() or {}).get("ticket"):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -299,7 +299,9 @@ async def order(payload: dict = Body(...),
|
|||||||
elif side == "short":
|
elif side == "short":
|
||||||
fn = eng.open_short
|
fn = eng.open_short
|
||||||
elif side == "close":
|
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:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="side muss long|short|close sein")
|
raise HTTPException(status_code=400, detail="side muss long|short|close sein")
|
||||||
log.info(f"[WEB] Order angefordert: {side}")
|
log.info(f"[WEB] Order angefordert: {side}")
|
||||||
|
|||||||
+39
@@ -1577,8 +1577,16 @@ let lastRecvMs = 0;
|
|||||||
// Getrennt davon: wann kam zuletzt eine WS-NACHRICHT? Nur diese Uhr entscheidet,
|
// Getrennt davon: wann kam zuletzt eine WS-NACHRICHT? Nur diese Uhr entscheidet,
|
||||||
// ob der Poll uebersprungen wird (s. Kommentar in `sock.onmessage`).
|
// ob der Poll uebersprungen wird (s. Kommentar in `sock.onmessage`).
|
||||||
let lastWsMs = 0;
|
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) {
|
function _applySnapshot(d) {
|
||||||
if (typeof d.ts === "number" && d.ts <= lastTs) return; // schon Neueres gezeigt
|
if (typeof d.ts === "number" && d.ts <= lastTs) return; // schon Neueres gezeigt
|
||||||
|
letzterSnap = d;
|
||||||
if (typeof d.ts === "number") lastTs = d.ts;
|
if (typeof d.ts === "number") lastTs = d.ts;
|
||||||
lastRecvMs = Date.now();
|
lastRecvMs = Date.now();
|
||||||
render(d);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+6
-4
@@ -12,7 +12,7 @@
|
|||||||
⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard
|
⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard
|
||||||
und muss offline funktionieren. -->
|
und muss offline funktionieren. -->
|
||||||
<link rel="stylesheet" href="pico.min.css?v=1">
|
<link rel="stylesheet" href="pico.min.css?v=1">
|
||||||
<link rel="stylesheet" href="style.css?v=198">
|
<link rel="stylesheet" href="style.css?v=199">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -53,10 +53,12 @@
|
|||||||
<div class="px" id="px-hl"><label title="Roher Kurs des Hyperliquid-Öl-Perps — läuft 24/7, auch wenn Pepperstone geschlossen ist. Anderer Kontrakt, deshalb nicht identisch; der Tooltip am Wert nennt die umgerechnete Entsprechung.">HYPERLIQUID KURS</label><span id="hdr-hl">—</span></div>
|
<div class="px" id="px-hl"><label title="Roher Kurs des Hyperliquid-Öl-Perps — läuft 24/7, auch wenn Pepperstone geschlossen ist. Anderer Kontrakt, deshalb nicht identisch; der Tooltip am Wert nennt die umgerechnete Entsprechung.">HYPERLIQUID KURS</label><span id="hdr-hl">—</span></div>
|
||||||
<div class="px px-split" title="Links: Gewinn/Verlust der Position in Slot 1 (manuelle und M15-Trades). Rechts: die Position des Breakout-Autotraders, der seit dem Zwei-Slot-Umbau vom 19.08. einen EIGENEN Slot hat. Beide NETTO nach Quellensteuer; bei geschlossenem Markt eine Schaetzung auf Basis des Hyperliquid-Kurses (mit ≈ markiert). Die SUMME beider Slots steht im TAG P/L oben.">
|
<div class="px px-split" title="Links: Gewinn/Verlust der Position in Slot 1 (manuelle und M15-Trades). Rechts: die Position des Breakout-Autotraders, der seit dem Zwei-Slot-Umbau vom 19.08. einen EIGENEN Slot hat. Beide NETTO nach Quellensteuer; bei geschlossenem Markt eine Schaetzung auf Basis des Hyperliquid-Kurses (mit ≈ markiert). Die SUMME beider Slots steht im TAG P/L oben.">
|
||||||
<div class="px-half">
|
<div class="px-half">
|
||||||
<label>G/V</label><span id="hdr-pnl">—</span>
|
<label>G/V</label>
|
||||||
|
<span class="px-line"><button class="px-close" id="close-main" title="Nur die Position in Slot 1 schliessen (manuelle und M15-Trades). ⚠ Der grosse CLOSE-Knopf unten schliesst BEIDE Slots.">✕</button><span id="hdr-pnl">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-half rechts">
|
<div class="px-half rechts">
|
||||||
<label>G/V · BRK</label><span id="hdr-pnl-brk">—</span>
|
<label>G/V · BRK</label>
|
||||||
|
<span class="px-line"><button class="px-close" id="close-brk" title="Nur die Position des Breakout-Autotraders schliessen. ⚠ Der grosse CLOSE-Knopf unten schliesst BEIDE Slots.">✕</button><span id="hdr-pnl-brk">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,6 +418,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="toast" class="toast hidden"></div>
|
<div id="toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script src="app.js?v=198"></script>
|
<script src="app.js?v=199"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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{display:flex; flex-direction:column; min-width:0}
|
||||||
.px-split .px-half.rechts{align-items:flex-end; text-align:right}
|
.px-split .px-half.rechts{align-items:flex-end; text-align:right}
|
||||||
.px-split .px-half label{white-space:nowrap}
|
.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}
|
||||||
|
|||||||
Reference in New Issue
Block a user