From 6af4c868bc0502fff4409d438d6f16816b0acc42 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Tue, 4 Aug 2026 17:31:58 +0200 Subject: [PATCH] Feste Einsatz-Margin (UI-Feld) + lokale Sprachausgabe via Piper EINSATZ-FELD (User-Wunsch): Laufzeitwert MANUAL_MARGIN in core/config.py (set/get_manual_margin, Muster wie set_margin_buffer). 0 = automatisch wie bisher (margin_buffer_pct % der freien Margin), > 0 = Position wird auf genau diesen Margin-Einsatz gerechnet. - calc_lots setzt es um und DECKELT auf freie Margin x Buffer; ein zu hoher Wunschwert wird begrenzt und geloggt statt an den Broker durchgereicht. Reicht der Betrag nicht fuers Mindestlot -> 0.0 + Warnung, Trade sauber abgelehnt. - Vorrang vor dem Risiko-Modus in trader._send_locked: eine ausdrueckliche Groessenvorgabe schlaegt die Rechenregel. - Wirkt auf ALLE neuen Positionen, auch die autonomen. - UI-Feld "Einsatz" in der ORDER-Leiste (nicht in der Trade-Leiste: die ist nur bei offener Position sichtbar, der Einsatz muss vorher einstellbar sein), bernstein umrandet solange gesetzt. Enter blurrt nur, change sendet einmal. - POST /api/manualmargin, Snapshot manual_margin, neustart-fest ueber runtime_state.json. Ende-zu-Ende getestet (250 -> Snapshot -> 0 -> persistiert). - Lot-Logik isoliert geprueft: 200 EUR -> 0,58 Lots; 5000 EUR -> gedeckelt. SPRACHAUSGABE: Piper lokal (tools/speak.py). Windows-TTS funktioniert zwar, aber es ist KEINE deutsche Stimme installiert - weder SAPI5 noch OneCore (nur David/Zira/Mark, en-US); deutscher Text kaeme mit englischer Aussprache. Add-WindowsCapability scheitert ohne Adminrechte. Piper gewaehlt: laeuft lokal (passt zum Rest - WireGuard-only, Secrets in der ini, keine Cloud), kostet nichts, aus PowerShell/Python aufrufbar. Stimme thorsten-medium (de_DE), Real-Time-Faktor 0,08 (4,3 s Audio in 0,36 s). Das ZIP entpackt eine Ebene tiefer als erwartet (tools/piper/piper/piper.exe) - speak.py SUCHT Binary und Modell statt Pfade fest zu verdrahten. Der erste Entwurf scheiterte daran; der Fail-safe meldete die Pfade sauber statt stumm zu bleiben. Drei Modi getestet (Argument, Pipe, --wav). tools/piper/ ist gitignored (Binaries + 60-MB-Modell), speak.py versioniert. v=136. Co-Authored-By: Claude Opus 5 --- .gitignore | 3 ++ CLAUDE.md | 41 +++++++++++++++++++- core/config.py | 26 +++++++++++++ core/engine.py | 24 +++++++++++- core/mt5_utils.py | 21 +++++++++- core/trader.py | 7 +++- server.py | 20 ++++++++++ tools/speak.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++ web/app.js | 40 +++++++++++++++++++ web/index.html | 12 +++++- web/style.css | 16 ++++++++ 11 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 tools/speak.py diff --git a/.gitignore b/.gitignore index 6e4da7e..5de5b46 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ gated.log gated.err fib.log fib.err + +# Piper (lokale Sprachausgabe) — Binaries und Stimmmodell, ~80 MB +tools/piper/ diff --git a/CLAUDE.md b/CLAUDE.md index 420971c..6de49c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2503,7 +2503,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=135** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=136** (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` — @@ -3027,6 +3027,45 @@ PF>1 in BEIDEN Hälften, Nachbarparameter müssen mithalten). Begründung gegen wöchentliche Läufe (Mehrfachvergleich, ~47 Fehlalarme/Jahr) steht unverändert. Anlass war hier die *einmalige* Signaländerung. +## Feste Einsatz-Margin + lokale Sprachausgabe (2026-08-04) + +**Einsatz-Feld** (User-Wunsch „Feld, mit dem ich meine eingesetzte Margin selber +eingeben kann"): `[trading]`-unabhängiger Laufzeitwert `MANUAL_MARGIN` in +`core/config.py` (`set/get_manual_margin`, Muster wie `set_margin_buffer`). +**0 = automatisch** (bisheriges Verhalten: `margin_buffer_pct` % der freien Margin), +**> 0 = die Position wird auf genau diesen Margin-Einsatz gerechnet.** +· `calc_lots` setzt es um und **deckelt auf die freie Margin × Buffer** — ein zu +hoher Wunschwert wird begrenzt und geloggt, nicht an den Broker durchgereicht +(sonst Reject mit kryptischem retcode statt sauberer Meldung). Reicht der Betrag +nicht fürs Mindestlot → 0.0 + Warnung, der Trade wird sauber abgelehnt. +· **Vorrang vor dem Risiko-Modus** (`trader._send_locked`): eine ausdrückliche +Größenvorgabe schlägt die Rechenregel. Bei `risk_pct=0` (live) ohnehin der Fall. +· ⚠ **Wirkt auf ALLE neuen Positionen — auch die autonomen** (Squeeze/Signal). +· UI: Feld **„Einsatz"** in der **Order-Leiste** (`#margin-input`, v=136), bernstein +umrandet solange gesetzt. ⚠ **Bewusst NICHT in der Trade-Leiste** — die ist nur bei +OFFENER Position sichtbar, der Einsatz muss aber VOR dem Öffnen einstellbar sein. +Enter blurrt nur, `change` sendet einmal (gleicher Fix wie beim Mindestgewinn). +· `POST /api/manualmargin {value}` · Snapshot `manual_margin` · neustart-fest über +`runtime_state.json`. Ende-zu-Ende getestet (250 → Snapshot → 0 → persistiert). + +**Sprachausgabe: Piper, lokal** (`tools/speak.py`, `tools/piper/`). Anlass: die +Windows-Sprachausgabe funktioniert zwar, aber auf diesem Rechner ist **keine +deutsche Stimme installiert** — weder SAPI5 noch OneCore (nur David/Zira/Mark, +alle en-US). Deutscher Text käme mit englischer Aussprache. `Add-WindowsCapability` +scheitert ohne Adminrechte (elevated Fenster nötig). +Piper gewählt, weil es **lokal** läuft (passt zum Rest: WireGuard-only, Secrets in +der ini, keine Cloud-Abhängigkeit), **nichts kostet** (nach der 200-$/Monat-Absage +bei X relevant) und aus PowerShell/Python aufrufbar ist. Stimme *thorsten-medium* +(de_DE), Real-Time-Faktor **0,08** — 4,3 s Audio in 0,36 s. +⚠ **Das ZIP entpackt eine Ebene tiefer als erwartet** (`tools/piper/piper/piper.exe`) +— `speak.py` **sucht** die Binärdatei und das Modell deshalb, statt Pfade fest zu +verdrahten. Der erste Entwurf scheiterte genau daran; der Fail-safe hat die Pfade +sauber gemeldet statt stumm zu bleiben. +Aufruf: `python tools/speak.py "Text"` · Pipe · `--wav datei` (nur erzeugen). +Rückgabe 1 bei fehlendem Piper → ein Aufrufer im Bot läuft weiter, statt abzustürzen. +⚠ `tools/piper/` ist **gitignored** (Binaries + 60-MB-Modell gehören nicht ins Repo); +`speak.py` selbst ist versioniert. + ## Modul „Manuelle Trades" + voller Entscheidungskontext (Stufe 1, 2026-08-04) **User-Frage:** „Kann der Bot aus meinen manuellen Trades lernen — evtl. ein neues diff --git a/core/config.py b/core/config.py index 0e67649..a86bf64 100644 --- a/core/config.py +++ b/core/config.py @@ -58,6 +58,32 @@ def get_margin_buffer() -> float: return MARGIN_BUFFER +# ── Manuell vorgegebene Einsatz-Margin (User-Wunsch 2026-08-04) ─────────────── +# 0 = AUS → automatische Größe wie bisher (MARGIN_BUFFER % der freien Margin bzw. +# risiko-basiert bei RISK_PER_TRADE > 0). > 0 = die Position wird auf GENAU diesen +# Margin-Einsatz in Kontowährung gerechnet. +# ⚠ Die freie Margin bleibt IMMER die Obergrenze — ein zu hoher Wunschwert wird +# gedeckelt, nicht durchgereicht (sonst Broker-Reject mit kryptischem retcode). +MANUAL_MARGIN = 0.0 + + +def set_manual_margin(eur: float) -> float: + """Feste Einsatz-Margin in Kontowährung setzen. 0 = automatisch.""" + global MANUAL_MARGIN + try: + MANUAL_MARGIN = max(0.0, float(eur or 0)) + except (TypeError, ValueError): + MANUAL_MARGIN = 0.0 + log.info(f"Einsatz-Margin: " + + (f"{MANUAL_MARGIN:.2f} (manuell)" if MANUAL_MARGIN > 0 + else "automatisch")) + return MANUAL_MARGIN + + +def get_manual_margin() -> float: + return MANUAL_MARGIN + + def set_risk_per_trade(pct: float) -> float: """Setzt das Risiko pro Trade zur Laufzeit (% der Equity). 0 = aus (Fallback margin-basiert). Sinnvoll 0,5–3 %.""" diff --git a/core/engine.py b/core/engine.py index 9d4f647..b4f7c3b 100644 --- a/core/engine.py +++ b/core/engine.py @@ -1815,9 +1815,13 @@ class TradingEngine: self._auto_sr_close = bool(st["auto_sr_close"]) if "sr_close_min_gain" in st: self._sr_close_min_gain = max(0.0, float(st["sr_close_min_gain"] or 0)) + if "manual_margin" in st: + from core.config import set_manual_margin as _set + self._manual_margin = _set(st["manual_margin"] or 0) log.info(f"Runtime-State geladen: BRK={'AN' if self._auto_squeeze else 'aus'} · " f"S/R-Close={'AN' if self._auto_sr_close else 'aus'} · " - f"MinGewinn={self._sr_close_min_gain:+.2f}") + f"MinGewinn={self._sr_close_min_gain:+.2f} · " + f"Einsatz={'auto' if not getattr(self, '_manual_margin', 0) else f'{self._manual_margin:.2f}'}") except FileNotFoundError: pass except Exception as e: @@ -1831,7 +1835,8 @@ class TradingEngine: json.dump({"auto_squeeze": self._auto_squeeze, "auto_signal": self._auto_signal, "auto_sr_close": self._auto_sr_close, - "sr_close_min_gain": self._sr_close_min_gain}, f) + "sr_close_min_gain": self._sr_close_min_gain, + "manual_margin": getattr(self, "_manual_margin", 0.0)}, f) os.replace(tmp, self._runtime_state_path) except Exception as e: log.warning(f"runtime_state speichern: {e}") @@ -2736,6 +2741,19 @@ class TradingEngine: # ══════════════════════════════════════════════════════════════════════ _SNAP_TTL_S = 0.8 # geteilter Snapshot gilt so lange als frisch + def set_manual_margin(self, eur: float) -> float: + """Feste Einsatz-Margin in Kontowährung (User-Wunsch 2026-08-04). + 0 = automatisch (wie bisher `margin_buffer_pct` % der freien Margin). + + ⚠ Wirkt auf ALLE neuen Positionen — manuelle wie autonome (Squeeze/Signal). + Die freie Margin bleibt Obergrenze; `calc_lots` deckelt und loggt das. + Neustart-fest über `runtime_state.json`.""" + from core.config import set_manual_margin as _set + v = _set(eur) + self._manual_margin = v + self._save_runtime_state() + return v + def _manual_stats_cached(self) -> dict | None: """Kennzahlen der manuellen Trades für die Karte „Manuelle Trades" (2026-08-04). Gleiches Cache-Muster wie `_alignment_cached` — die @@ -3295,6 +3313,8 @@ class TradingEngine: # (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße # Snapshot-Pfad nicht je Abruf die DB aggregiert. "manual_stats": self._manual_stats_cached(), + # Manuell vorgegebene Einsatz-Margin (0 = automatisch, s. set_manual_margin) + "manual_margin": getattr(self, "_manual_margin", 0.0), # Live-Handelskosten relativ zur Vola (2026-07-24): Spread/ATR(M5). # Gemessen (`backtest_realcosts.py`): Ø 0,265×ATR, Nacht 0,32–0,50 = # Kostenfalle, 15–17 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die diff --git a/core/mt5_utils.py b/core/mt5_utils.py index 516c624..123cf0e 100644 --- a/core/mt5_utils.py +++ b/core/mt5_utils.py @@ -13,7 +13,8 @@ from contextlib import contextmanager import MetaTrader5 as mt5 from core.config import ( - get_margin_buffer, SL_TF, SL_LOOKBACK, SL_WINDOW, SL_BUFFER_TICKS, + get_margin_buffer, get_manual_margin, + SL_TF, SL_LOOKBACK, SL_WINDOW, SL_BUFFER_TICKS, ) from core.logger import get_logger @@ -76,6 +77,24 @@ def calc_lots(sym: str, price: float, otype: int) -> float: if not mpl or mpl <= 0: return 0.0 step = si.volume_step + # ── Manuell vorgegebene Einsatz-Margin (User-Wunsch 2026-08-04) ────────── + # Ist sie gesetzt, wird die Position auf GENAU diesen Betrag gerechnet statt + # auf einen Prozentsatz der freien Margin. Die freie Margin bleibt Obergrenze: + # ein zu hoher Wunschwert wird gedeckelt, nicht an den Broker durchgereicht + # (sonst Reject mit kryptischem retcode statt sauberer Meldung). + _man = get_manual_margin() + if _man > 0: + _leistbar = acc.margin_free * get_margin_buffer() + _ziel = min(_man, _leistbar) + lots = round(((_ziel / mpl) // step) * step, 4) + if lots < si.volume_min: + log.warning(f"Einsatz-Margin {_man:.2f} reicht nicht für das Mindestlot " + f"({si.volume_min}) — Margin je Lot {mpl:.2f}") + return 0.0 + if _ziel < _man: + log.warning(f"Einsatz-Margin auf {_ziel:.2f} gedeckelt " + f"(gewünscht {_man:.2f}, frei×Buffer {_leistbar:.2f})") + return min(lots, si.volume_max) lots = round(((acc.margin_free * get_margin_buffer() / mpl) // step) * step, 4) # Margin-Guard: reicht die freie Margin nicht mal fürs Mindestvolumen, 0.0 # zurückgeben → Caller meldet sauber „Lot-Fehler" statt Broker-Reject mit diff --git a/core/trader.py b/core/trader.py index 234b525..f3c48a0 100644 --- a/core/trader.py +++ b/core/trader.py @@ -14,7 +14,7 @@ import MetaTrader5 as mt5 from core.config import ( DEVIATION, MAGIC, SL_BUFFER_TICKS, INIT_SL_FALLBACK, INIT_TP_RR, - INIT_SL_MIN_ATR, INIT_SL_MAX_ATR, get_risk_per_trade, + INIT_SL_MIN_ATR, INIT_SL_MAX_ATR, get_risk_per_trade, get_manual_margin, ) from core.mt5_utils import ( mt5_lock, get_tick, get_filling, calc_lots, calc_lots_risk, @@ -142,6 +142,11 @@ class TradeManager: sl, _tp, sl_src = self._calc_sl_tp(sym, otype, price) risk = get_risk_per_trade() sl_dist = abs(price - sl) if sl else None + # Eine manuell gesetzte Einsatz-Margin hat VORRANG vor dem Risiko-Modus + # (2026-08-04): sie ist eine ausdrückliche Größenvorgabe des Users, keine + # Rechenregel. `calc_lots` setzt sie um und deckelt auf die freie Margin. + if get_manual_margin() > 0: + risk = 0.0 if risk > 0: # Risiko-Modus: KEIN stiller Fallback auf Margin-Sizing (75 % wäre ein # Vielfaches des gewollten Risikos). Klappt die Risiko-Rechnung nicht diff --git a/server.py b/server.py index ebbd5ba..95e4bbd 100644 --- a/server.py +++ b/server.py @@ -344,6 +344,26 @@ async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)): return {"ok": True, "enabled": state} +@app.post("/api/manualmargin") +async def set_manual_margin(body: dict, + x_auth_token: str | None = Header(default=None)): + """Feste Einsatz-Margin in Kontowährung setzen (User-Wunsch 2026-08-04). + `{"value": 0}` = automatisch (wie bisher `margin_buffer_pct` % der freien Margin). + + ⚠ Wirkt auf ALLE neuen Positionen — auch die autonomen. Die freie Margin bleibt + Obergrenze; `calc_lots` deckelt einen zu hohen Wunschwert und loggt das.""" + _require_auth(x_auth_token) + try: + v = float(body.get("value") or 0) + except (TypeError, ValueError): + return {"ok": False, "error": "ungültiger Wert"} + eng = app.state.engine + val = eng.set_manual_margin(v) + get_logger("server").info( + f"[WEB] Einsatz-Margin: {'automatisch' if val <= 0 else f'{val:.2f}'}") + return {"ok": True, "value": val} + + @app.post("/api/autosignal") async def toggle_autosignal(x_auth_token: str | None = Header(default=None)): """Autonomen Entry auf die EMPFEHLUNG an/aus (User-Wunsch 2026-07-30). diff --git a/tools/speak.py b/tools/speak.py new file mode 100644 index 0000000..78f4a69 --- /dev/null +++ b/tools/speak.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""speak.py — deutsche Sprachausgabe über Piper (lokal, offline, kostenlos). + +Warum Piper und nicht die Windows-Sprachausgabe: auf diesem Rechner ist KEINE +deutsche SAPI-/OneCore-Stimme installiert (nur David/Zira/Mark, alle en-US) — +deutscher Text käme mit englischer Aussprache heraus. Piper bringt eine eigene +deutsche Stimme mit, läuft komplett lokal (passt zum Rest des Setups: WireGuard- +only, Secrets in der ini, keine Cloud-Abhängigkeit) und kostet nichts. + +Aufruf: + python tools/speak.py "Der Text" + echo "Der Text" | python tools/speak.py + python tools/speak.py --datei meldung.txt + +Rückgabe 0 = gesprochen, 1 = Fehler (fehlt Piper/Modell → Hinweis auf stderr). +Fail-safe gedacht: ein Aufrufer im Bot soll bei fehlender Sprachausgabe +weiterlaufen, nicht abstürzen. +""" +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _finde(name: str, *starts: str) -> str: + """Erste Datei `name` unterhalb der Startpfade. ⚠ Das Piper-ZIP entpackt eine + Ebene tiefer als erwartet (`tools/piper/piper/piper.exe`) — fest verdrahtete + Pfade brechen deshalb, sobald jemand anders entpackt oder das Layout sich mit + einer neuen Version ändert. Suchen ist hier robuster als raten.""" + for s in starts: + if os.path.isfile(s): + return s + for wurzel, _dirs, dateien in os.walk(s): + if name in dateien: + return os.path.join(wurzel, name) + return "" + + +PIPER = _finde("piper.exe", os.path.join(HERE, "piper")) +MODELL = _finde("de_DE-thorsten-medium.onnx", os.path.join(HERE, "piper")) + + +def sprich(text: str, behalten: str | None = None) -> int: + text = (text or "").strip() + if not text: + return 0 + if not PIPER or not MODELL: + print(f"Piper oder Stimme nicht gefunden unter {os.path.join(HERE, 'piper')}\n" + f" piper.exe: {PIPER or 'FEHLT'}\n Stimme: {MODELL or 'FEHLT'}", + file=sys.stderr) + return 1 + wav = behalten or os.path.join(tempfile.gettempdir(), "speak_out.wav") + try: + subprocess.run([PIPER, "--model", MODELL, "--output_file", wav], + input=text.encode("utf-8"), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + check=True, timeout=120) + except Exception as e: + print(f"Piper-Aufruf fehlgeschlagen: {e}", file=sys.stderr) + return 1 + if behalten: # nur erzeugen, nicht abspielen + return 0 + # Abspielen über den .NET-SoundPlayer — kein zusätzliches Paket nötig. + ps = (f"(New-Object Media.SoundPlayer '{wav}').PlaySync()") + try: + subprocess.run(["powershell", "-NoProfile", "-Command", ps], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + check=True, timeout=300) + except Exception as e: + print(f"Wiedergabe fehlgeschlagen: {e}", file=sys.stderr) + return 1 + return 0 + + +def main() -> None: + p = argparse.ArgumentParser(description="Deutsche Sprachausgabe via Piper") + p.add_argument("text", nargs="*", help="zu sprechender Text") + p.add_argument("--datei", help="Text aus Datei lesen") + p.add_argument("--wav", help="nur WAV erzeugen, nicht abspielen") + a = p.parse_args() + if a.datei: + with open(a.datei, encoding="utf-8") as f: + txt = f.read() + elif a.text: + txt = " ".join(a.text) + else: + txt = sys.stdin.read() # Pipe-Betrieb + sys.exit(sprich(txt, a.wav)) + + +if __name__ == "__main__": + main() diff --git a/web/app.js b/web/app.js index d686e0e..f34bdef 100644 --- a/web/app.js +++ b/web/app.js @@ -900,6 +900,14 @@ function render(d) { if (smi && document.activeElement !== smi) { smi.value = srMin > 0 ? String(srMin) : ""; } + // Feste Einsatz-Margin (0/leer = automatisch). Nicht überschreiben, während + // getippt wird — sonst löscht der 1-s-Render die Eingabe unter den Fingern weg. + const mgi = $("margin-input"); + if (mgi && document.activeElement !== mgi) { + const mm = d.manual_margin || 0; + mgi.value = mm > 0 ? String(mm) : ""; + mgi.classList.toggle("on", mm > 0); + } // Einblendung: der Bot hat am S/R automatisch geschlossen (Zähler stieg) const src = d.sr_close_count; if (src != null) { @@ -1267,6 +1275,38 @@ $("srmin-input").addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } }); +// Feste Einsatz-Margin (User-Wunsch 2026-08-04). Leer/0 = automatisch. +// ⚠ Wirkt auf ALLE neuen Positionen, auch die autonomen — deshalb sagt der Toast +// ausdrücklich, was gilt, statt nur „gespeichert". +async function setManualMargin() { + const el = $("margin-input"); if (!el) return; + const raw = el.value; + const v = raw === "" ? 0 : parseFloat(raw); + if (isNaN(v) || v < 0) { toast("Bitte Betrag ≥ 0 eingeben (0 = automatisch)", false); return; } + const tok = await ensureToken(); + if (tok === null) return; + el.blur(); + try { + const r = await fetch("api/manualmargin", { method: "POST", + headers: { "X-Auth-Token": tok, "Content-Type": "application/json" }, + body: JSON.stringify({ value: v }) }); + if (r.status === 401) { clearToken(); throw new Error("401"); } + const j = await r.json(); + toast(v > 0 ? `Einsatz fest: ${fmt(j.value ?? v, 2)} € je Position` + : "Einsatz wieder automatisch (95 % der freien Margin)", true); + } catch (e) { + toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false); + } +} +(() => { + const el = $("margin-input"); + if (!el) return; // null-sicher (Cache-Regel) + el.addEventListener("change", setManualMargin); + el.addEventListener("keydown", e => { + if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } + }); +})(); + // Notfall-Stop: manueller Verlust-Wert, bei dem der Server sofort schließt. // Wieder eingeblendet 2026-07-22 (Backend/Persistenz waren die ganze Zeit aktiv). async function setEmg() { diff --git a/web/index.html b/web/index.html index 81b3613..ca7fe5a 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -235,6 +235,14 @@