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:
co-authored by
Claude Opus 5
parent
33b6b8f629
commit
6af4c868bc
@@ -59,3 +59,6 @@ gated.log
|
|||||||
gated.err
|
gated.err
|
||||||
fib.log
|
fib.log
|
||||||
fib.err
|
fib.err
|
||||||
|
|
||||||
|
# Piper (lokale Sprachausgabe) — Binaries und Stimmmodell, ~80 MB
|
||||||
|
tools/piper/
|
||||||
|
|||||||
@@ -2503,7 +2503,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=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
|
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` —
|
||||||
@@ -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)
|
Begründung gegen wöchentliche Läufe (Mehrfachvergleich, ~47 Fehlalarme/Jahr)
|
||||||
steht unverändert. Anlass war hier die *einmalige* Signaländerung.
|
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)
|
## Modul „Manuelle Trades" + voller Entscheidungskontext (Stufe 1, 2026-08-04)
|
||||||
|
|
||||||
**User-Frage:** „Kann der Bot aus meinen manuellen Trades lernen — evtl. ein neues
|
**User-Frage:** „Kann der Bot aus meinen manuellen Trades lernen — evtl. ein neues
|
||||||
|
|||||||
@@ -58,6 +58,32 @@ def get_margin_buffer() -> float:
|
|||||||
return MARGIN_BUFFER
|
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:
|
def set_risk_per_trade(pct: float) -> float:
|
||||||
"""Setzt das Risiko pro Trade zur Laufzeit (% der Equity). 0 = aus (Fallback
|
"""Setzt das Risiko pro Trade zur Laufzeit (% der Equity). 0 = aus (Fallback
|
||||||
margin-basiert). Sinnvoll 0,5–3 %."""
|
margin-basiert). Sinnvoll 0,5–3 %."""
|
||||||
|
|||||||
+22
-2
@@ -1815,9 +1815,13 @@ class TradingEngine:
|
|||||||
self._auto_sr_close = bool(st["auto_sr_close"])
|
self._auto_sr_close = bool(st["auto_sr_close"])
|
||||||
if "sr_close_min_gain" in st:
|
if "sr_close_min_gain" in st:
|
||||||
self._sr_close_min_gain = max(0.0, float(st["sr_close_min_gain"] or 0))
|
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'} · "
|
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"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:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1831,7 +1835,8 @@ class TradingEngine:
|
|||||||
json.dump({"auto_squeeze": self._auto_squeeze,
|
json.dump({"auto_squeeze": self._auto_squeeze,
|
||||||
"auto_signal": self._auto_signal,
|
"auto_signal": self._auto_signal,
|
||||||
"auto_sr_close": self._auto_sr_close,
|
"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)
|
os.replace(tmp, self._runtime_state_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"runtime_state speichern: {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
|
_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:
|
def _manual_stats_cached(self) -> dict | None:
|
||||||
"""Kennzahlen der manuellen Trades für die Karte „Manuelle Trades"
|
"""Kennzahlen der manuellen Trades für die Karte „Manuelle Trades"
|
||||||
(2026-08-04). Gleiches Cache-Muster wie `_alignment_cached` — die
|
(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
|
# (2026-08-04). Rein deskriptiv; ~120 s gecacht, damit der heiße
|
||||||
# Snapshot-Pfad nicht je Abruf die DB aggregiert.
|
# Snapshot-Pfad nicht je Abruf die DB aggregiert.
|
||||||
"manual_stats": self._manual_stats_cached(),
|
"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).
|
# Live-Handelskosten relativ zur Vola (2026-07-24): Spread/ATR(M5).
|
||||||
# Gemessen (`backtest_realcosts.py`): Ø 0,265×ATR, Nacht 0,32–0,50 =
|
# Gemessen (`backtest_realcosts.py`): Ø 0,265×ATR, Nacht 0,32–0,50 =
|
||||||
# Kostenfalle, 15–17 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die
|
# Kostenfalle, 15–17 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die
|
||||||
|
|||||||
+20
-1
@@ -13,7 +13,8 @@ from contextlib import contextmanager
|
|||||||
import MetaTrader5 as mt5
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
from core.config import (
|
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
|
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:
|
if not mpl or mpl <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
step = si.volume_step
|
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)
|
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
|
# Margin-Guard: reicht die freie Margin nicht mal fürs Mindestvolumen, 0.0
|
||||||
# zurückgeben → Caller meldet sauber „Lot-Fehler" statt Broker-Reject mit
|
# zurückgeben → Caller meldet sauber „Lot-Fehler" statt Broker-Reject mit
|
||||||
|
|||||||
+6
-1
@@ -14,7 +14,7 @@ import MetaTrader5 as mt5
|
|||||||
from core.config import (
|
from core.config import (
|
||||||
DEVIATION, MAGIC,
|
DEVIATION, MAGIC,
|
||||||
SL_BUFFER_TICKS, INIT_SL_FALLBACK, INIT_TP_RR,
|
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 (
|
from core.mt5_utils import (
|
||||||
mt5_lock, get_tick, get_filling, calc_lots, calc_lots_risk,
|
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)
|
sl, _tp, sl_src = self._calc_sl_tp(sym, otype, price)
|
||||||
risk = get_risk_per_trade()
|
risk = get_risk_per_trade()
|
||||||
sl_dist = abs(price - sl) if sl else None
|
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:
|
if risk > 0:
|
||||||
# Risiko-Modus: KEIN stiller Fallback auf Margin-Sizing (75 % wäre ein
|
# Risiko-Modus: KEIN stiller Fallback auf Margin-Sizing (75 % wäre ein
|
||||||
# Vielfaches des gewollten Risikos). Klappt die Risiko-Rechnung nicht
|
# Vielfaches des gewollten Risikos). Klappt die Risiko-Rechnung nicht
|
||||||
|
|||||||
@@ -344,6 +344,26 @@ async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)):
|
|||||||
return {"ok": True, "enabled": state}
|
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")
|
@app.post("/api/autosignal")
|
||||||
async def toggle_autosignal(x_auth_token: str | None = Header(default=None)):
|
async def toggle_autosignal(x_auth_token: str | None = Header(default=None)):
|
||||||
"""Autonomen Entry auf die EMPFEHLUNG an/aus (User-Wunsch 2026-07-30).
|
"""Autonomen Entry auf die EMPFEHLUNG an/aus (User-Wunsch 2026-07-30).
|
||||||
|
|||||||
@@ -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()
|
||||||
+40
@@ -900,6 +900,14 @@ function render(d) {
|
|||||||
if (smi && document.activeElement !== smi) {
|
if (smi && document.activeElement !== smi) {
|
||||||
smi.value = srMin > 0 ? String(srMin) : "";
|
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)
|
// Einblendung: der Bot hat am S/R automatisch geschlossen (Zähler stieg)
|
||||||
const src = d.sr_close_count;
|
const src = d.sr_close_count;
|
||||||
if (src != null) {
|
if (src != null) {
|
||||||
@@ -1267,6 +1275,38 @@ $("srmin-input").addEventListener("keydown", e => {
|
|||||||
if (e.key === "Enter") { e.preventDefault(); e.target.blur(); }
|
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.
|
// Notfall-Stop: manueller Verlust-Wert, bei dem der Server sofort schließt.
|
||||||
// Wieder eingeblendet 2026-07-22 (Backend/Persistenz waren die ganze Zeit aktiv).
|
// Wieder eingeblendet 2026-07-22 (Backend/Persistenz waren die ganze Zeit aktiv).
|
||||||
async function setEmg() {
|
async function setEmg() {
|
||||||
|
|||||||
+10
-2
@@ -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=135">
|
<link rel="stylesheet" href="style.css?v=136">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Modul-Menü (ganz oben) -->
|
<!-- Modul-Menü (ganz oben) -->
|
||||||
@@ -235,6 +235,14 @@
|
|||||||
|
|
||||||
<!-- Aktionsleiste (Trade-Steuerung) -->
|
<!-- Aktionsleiste (Trade-Steuerung) -->
|
||||||
<nav id="actions">
|
<nav id="actions">
|
||||||
|
<!-- Einsatz-Margin manuell vorgeben (User 2026-08-04). Leer/0 = automatisch
|
||||||
|
(margin_buffer_pct % der freien Margin). ⚠ BEWUSST hier und nicht in der
|
||||||
|
Trade-Leiste: die ist nur bei OFFENER Position sichtbar, der Einsatz muss
|
||||||
|
aber VOR dem Öffnen einstellbar sein. Wirkt auf ALLE neuen Positionen. -->
|
||||||
|
<span class="mg-field" title="Feste Einsatz-Margin in € — leer/0 = automatisch (95 % der freien Margin). Gilt auch für autonome Trades.">
|
||||||
|
<label for="margin-input">Einsatz</label>
|
||||||
|
<input id="margin-input" type="number" inputmode="decimal" step="10" min="0" placeholder="auto">
|
||||||
|
</span>
|
||||||
<button class="btn long" data-side="long">▲ LONG</button>
|
<button class="btn long" data-side="long">▲ LONG</button>
|
||||||
<button class="btn short" data-side="short">▼ SHORT</button>
|
<button class="btn short" data-side="short">▼ SHORT</button>
|
||||||
<button class="btn close" data-side="close">✕ CLOSE</button>
|
<button class="btn close" data-side="close">✕ CLOSE</button>
|
||||||
@@ -259,6 +267,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=135"></script>
|
<script src="app.js?v=136"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -512,3 +512,19 @@ main{padding-bottom:74px} /* Platz für die fixe Leiste */
|
|||||||
font-size: 13px; font-family: var(--mono);
|
font-size: 13px; font-family: var(--mono);
|
||||||
}
|
}
|
||||||
.mt-note { font-size: 12px; line-height: 1.45; }
|
.mt-note { font-size: 12px; line-height: 1.45; }
|
||||||
|
|
||||||
|
/* ── Feste Einsatz-Margin in der Order-Leiste (2026-08-04) ────────────────
|
||||||
|
Sitzt bewusst bei den Order-Buttons, nicht in der Trade-Leiste: die ist nur
|
||||||
|
bei offener Position sichtbar, der Einsatz muss aber VORHER einstellbar sein. */
|
||||||
|
.mg-field { display: inline-flex; flex-direction: column; gap: 2px; margin-right: 8px; }
|
||||||
|
.mg-field label { font-size: 11px; color: var(--dim); line-height: 1; }
|
||||||
|
.mg-field input {
|
||||||
|
width: 74px; padding: 5px 7px;
|
||||||
|
background: var(--surface-2); color: var(--bright);
|
||||||
|
border: 1px solid var(--border); border-radius: var(--r-sm);
|
||||||
|
font-family: var(--mono); font-size: 14px; text-align: right;
|
||||||
|
}
|
||||||
|
.mg-field input:focus { outline: none; border-color: #1f6feb; }
|
||||||
|
/* aktiv gesetzt = sichtbar hervorgehoben, damit ein fester Einsatz nicht
|
||||||
|
unbemerkt bestehen bleibt */
|
||||||
|
.mg-field input.on { border-color: #d29922; color: #d29922; }
|
||||||
|
|||||||
Reference in New Issue
Block a user