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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-04 17:31:58 +02:00
co-authored by Claude Opus 5
parent 33b6b8f629
commit 6af4c868bc
11 changed files with 300 additions and 7 deletions
+40
View File
@@ -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() {