Provider-Failover bei hartem Fehlschlag + Einsatz-Prozentfeld im Dashboard

1) LLM-Failover (core/agent.py): ein hart gescheiterter Provider wird zeitweise
uebersprungen, analyze() nimmt den naechsten aus der Kette. Hart = 401/402/403/
404 (Key/Guthaben/Modell) -> 30 min, nicht erreichbar -> 5 min. Timeout/429/5xx
loesen BEWUSST keinen Wechsel aus (voruebergehend; sonst kostet jede Lastspitze
die volle Timeout-Summe aller Anbieter). Neu im Snapshot: agent.provider_dead
mit Restminuten - der DeepSeek-Ausfall stand vorher nur im Log und blieb
deshalb 18 h unbemerkt. 9 Szenarien getestet.

2) Einsatz-Prozentfeld (web, v=137): margin_buffer_pct hatte bisher keine UI.
Neues Feld "Einsatz %" neben "Einsatz EUR", POST /api/marginpct ->
engine.set_margin_pct -> config.set_margin_buffer, Snapshot margin_pct,
neustart-fest. Der feste EUR-Betrag hat Vorrang; das Prozentfeld wird dann
ausgegraut, damit nicht unklar bleibt was gilt. Auf 1-99 % geklemmt.
Ende-zu-Ende getestet (50, 150->99, 0 und -5 abgelehnt, 95, persistiert).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-05 08:38:17 +02:00
co-authored by Claude Opus 5
parent 7e9b0196a5
commit cc4f1a0863
7 changed files with 244 additions and 26 deletions
+31 -7
View File
@@ -91,14 +91,25 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
(`glm-4.5-flash`) ist kostenlos**, antwortet in ~2,3 s (DeepSeek ~6 s) und wird im
Projekt ohnehin für `daily_levels` genutzt; live verifiziert („[zai] NEUTRAL (30%)").
Kimi (`kimi-k2.6`) antwortet ebenfalls (7,6 s), ist aber kostenpflichtig.
⚠⚠ **Der eigentliche Befund: die Fallback-Kette hat NICHT gegriffen.** `_active_
provider` prüft in `avail` nur das **Key-FORMAT** (`startswith("sk-")`), nicht ob der
Key noch funktioniert. Ein Provider ohne Guthaben gilt damit als „verfügbar" und
**blockiert die gesamte Kette** (`deepseek→kimi→zai→local`) — der Copilot lief
⚠⚠ **Der eigentliche Befund: die Fallback-Kette hatte NICHT gegriffen.** `_active_
provider` prüfte in `avail` nur das **Key-FORMAT** (`startswith("sk-")`), nicht ob der
Key noch funktioniert. Ein Provider ohne Guthaben galt damit als „verfügbar" und
**blockierte die gesamte Kette** (`deepseek→kimi→zai→local`) — der Copilot lief
**~18 h still ins Leere** (29 Fehlversuche ab 04.08. 14:52), ohne dass irgendetwas
außer einer WARNING-Zeile passierte. **Offen, noch nicht gebaut:** bei hartem
Fehlschlag (401/402) den Provider für X Minuten als tot markieren und den nächsten
nehmen. Ohne das wiederholt sich dasselbe, sobald das z.ai-Kontingent endet.
außer einer WARNING-Zeile passierte.
**BEHOBEN am selben Tag (`agent._mark_dead`/`_provider_chain`/`_chain_raw`):** ein
hart gescheiterter Provider wird zeitweise übersprungen und `analyze()` nimmt den
nächsten aus der Kette. **Hart = 401/402/403/404** (Key/Guthaben/Modell) → **30 min**;
**nicht erreichbar** (z. B. Ollama aus) → 5 min. **Bewusst NICHT hart: Timeout, 429,
5xx** — das sind vorübergehende Zustände; wer darauf wechselt, verliert bei jeder
Lastspitze den bevorzugten Anbieter und zahlt pro Zyklus die volle Timeout-Summe
ALLER Anbieter (90120 s je Stück). Sichtbar im Snapshot als
**`agent.provider_dead {provider: Restminuten}`** — der Ausfall stand vorher nur im
Log, genau deshalb blieb er 18 h unbemerkt. `_dead` läuft absichtlich **ohne**
`self._lock` (`snapshot()` hält ihn bereits, nicht reentrant → Deadlock; dict-Zugriffe
sind unter dem GIL atomar). Mit **9 Szenarien getestet** (402→Fallback · zweiter Zyklus
überspringt direkt · zwei tote Anbieter · Timeout/429 lösen KEINEN Wechsel aus ·
ConnectionError 5 min · alle tot → sauberer Fehler · Erholung nach Ablauf · Snapshot).
Zurück auf DeepSeek nach Aufladen: `provider = deepseek`.
- **LLM-Provider (Historie):** **DeepSeek (`[deepseek]`) war der aktive
Copilot-Provider** (`[agent] provider=deepseek`, 2026-07-20 bis 2026-08-05). `api.deepseek.com`,
@@ -3218,6 +3229,19 @@ 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).
**Einsatz in PROZENT (2026-08-05, v=137):** das Gegenstück zum festen Betrag —
`margin_buffer_pct` (live 95) hatte bisher **keine UI** und war nur in der ini
änderbar. Neu: Feld **„Einsatz %"** (`#marginpct-input`) direkt neben „Einsatz €" in
der Order-Leiste · `POST /api/marginpct {value}``engine.set_margin_pct`
`config.set_margin_buffer` · Snapshot `margin_pct` · neustart-fest.
**VORRANG ist sichtbar gemacht:** steht links ein fester €-Betrag, wird das
Prozent-Feld **ausgegraut** (`.mg-field.muted`, gestrichelter Rand) und der Tooltip
sagt, dass der Prozentsatz dann nur noch die Obergrenze ist — zwei scheinbar
gleichrangige Felder ohne Hinweis wären die schlechtere Lösung.
⚠ Geklemmt auf **199 %** (`set_margin_buffer`); 100 % ließe keinen Puffer für
Spread/Swap. Ende-zu-Ende getestet: 50 → 50 · 150 → auf 99 geklemmt · 0 und 5
abgelehnt · 95 → 95, in `runtime_state.json` 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,
+93 -13
View File
@@ -145,6 +145,17 @@ def _has_cjk(rec: dict) -> bool:
return any("" <= c <= "鿿" for c in txt)
def _req_conn_errors() -> tuple:
"""Exception-Klassen für „Anbieter nicht erreichbar". `requests` wird im Modul
absichtlich nur lazy importiert (die Provider-Aufrufe tun das ebenso), deshalb
hier zur Laufzeit auflösen. Fehlt requests, greift der Zweig einfach nicht."""
try:
import requests
return (requests.exceptions.ConnectionError,)
except Exception:
return ()
# JSON-Schema für strukturierte Ausgabe (Claude: output_config.format erzwingt es)
_SCHEMA = {
"type": "object",
@@ -189,6 +200,11 @@ class TradingAgent:
self.tg_push = (ac.get("telegram", "false") or "false").lower() == "true"
self._lock = threading.Lock()
# Provider → Zeitpunkt, bis zu dem er als tot gilt (s. `_mark_dead`).
# Absichtlich OHNE `self._lock`: `_active_provider` wird aus `snapshot()`
# heraus aufgerufen, das den Lock bereits hält (nicht reentrant → Deadlock).
# dict-get/set sind unter dem GIL atomar, mehr braucht es hier nicht.
self._dead: dict[str, float] = {}
self._busy = False
self._last: dict | None = None # letzte Beurteilung (geparst)
self._error: str | None = None
@@ -216,7 +232,43 @@ class TradingAgent:
except Exception:
return ""
# ── Ausfall-Erkennung (2026-08-05) ───────────────────────────────────────
# ⚠ ANLASS: `avail` unten prüft nur das Key-FORMAT. Ein Key OHNE GUTHABEN gilt
# damit als „verfügbar" und blockiert die gesamte Fallback-Kette. Real lief der
# Copilot ~18 h ins Leere (DeepSeek `HTTP 402 Insufficient Balance`, 29 Fehl-
# versuche ab 04.08. 14:52) — sichtbar nur als WARNING-Zeile, ohne dass je auf
# zai/kimi umgeschaltet wurde. `_dead` schliesst diese Lücke: ein hart
# gescheiterter Provider wird zeitweise übersprungen.
_DEAD_HARD_S = 1800.0 # 401/402/403/404 = Key/Guthaben/Modell kaputt → 30 min
_DEAD_CONN_S = 300.0 # nicht erreichbar (z. B. Ollama aus) → 5 min
# BEWUSST NICHT als tot markiert: Timeout, 429 und 5xx. Das sind vorübergehende
# Zustände; wer darauf den Provider wechselt, verliert bei jeder Lastspitze den
# konfigurierten (i. d. R. besten) Anbieter.
def _mark_dead(self, prov: str, exc: Exception) -> str | None:
"""Klassifiziert einen Fehlschlag. → Grundtext, wenn als tot markiert."""
code = getattr(getattr(exc, "response", None), "status_code", None)
if code in (401, 402, 403, 404):
grund = {401: "Key ungültig", 402: "kein Guthaben",
403: "Zugriff verweigert", 404: "Modell/Endpoint fehlt"}[code]
dauer, txt = self._DEAD_HARD_S, f"HTTP {code} ({grund})"
elif isinstance(exc, _req_conn_errors()):
dauer, txt = self._DEAD_CONN_S, "nicht erreichbar"
else:
return None
self._dead[prov] = time.time() + dauer
return f"{txt} — für {dauer/60:.0f} min übersprungen"
def _provider_chain(self) -> list:
"""Die Fallback-Reihenfolge, OHNE aktuell tote Provider."""
return [p for p in self._chain_raw() if self._dead.get(p, 0.0) <= time.time()]
def _active_provider(self) -> str | None:
"""Erster verfügbarer Provider der Kette (tote übersprungen)."""
ch = self._provider_chain()
return ch[0] if ch else None
def _chain_raw(self) -> list:
"""Bevorzugt den konfigurierten Provider, fällt sonst der Reihe nach
auf einen verfügbaren zurück. 'local' (Ollama) gilt als verfügbar,
sobald ein Modellname gesetzt ist — Erreichbarkeit wird erst beim
@@ -240,10 +292,7 @@ class TradingAgent:
"kimi": ["kimi", "deepseek", "zai", "local"],
"deepseek": ["deepseek", "kimi", "zai", "local"],
}
for p in orders.get(self.provider, default):
if avail[p]:
return p
return None
return [p for p in orders.get(self.provider, default) if avail[p]]
def is_configured(self) -> bool:
return self._active_provider() is not None
@@ -623,9 +672,17 @@ class TradingAgent:
}
# ── Hauptlauf (blockierend — im Hintergrund-Thread aufrufen) ─────────────
def _call(self, prov: str, prompt: str) -> str:
return (self._call_ollama(prompt) if prov == "local"
else self._call_zai(prompt) if prov == "zai"
else self._call_kimi(prompt) if prov == "kimi"
else self._call_deepseek(prompt) if prov == "deepseek"
else self._call_claude(prompt) if prov == "claude"
else self._call_openai(prompt) if prov == "openai"
else self._call_gemini(prompt))
def analyze(self) -> bool:
prov = self._active_provider()
if prov is None:
if self._active_provider() is None:
with self._lock:
self._error = ("Kein Provider (config.ini [ollama] model / "
"[anthropic] / [gemini] / [openai])")
@@ -634,6 +691,7 @@ class TradingAgent:
if self._busy:
return False
self._busy = True
prov = None
try:
ctx = self._gather_context()
prompt = ("Aktueller Systemzustand (JSON):\n"
@@ -641,13 +699,30 @@ class TradingAgent:
+ "\n\nGib deine Beurteilung als JSON zurück. "
"Alle Texte auf DEUTSCH, nur lateinische Schrift "
"(keine chinesischen Zeichen).")
raw = (self._call_ollama(prompt) if prov == "local"
else self._call_zai(prompt) if prov == "zai"
else self._call_kimi(prompt) if prov == "kimi"
else self._call_deepseek(prompt) if prov == "deepseek"
else self._call_claude(prompt) if prov == "claude"
else self._call_openai(prompt) if prov == "openai"
else self._call_gemini(prompt))
# ── Fallback-Kette (2026-08-05): bei einem HARTEN Fehlschlag (Key/
# Guthaben/Modell) den Provider markieren und den NÄCHSTEN nehmen.
# ⚠ Nur bei hart — nach Timeout/429/5xx wird abgebrochen statt die
# ganze Kette durchzuprobieren, sonst kostet eine Lastspitze bei jedem
# Zyklus die volle Timeout-Summe aller Anbieter (hier bis 90120 s je
# Stück) und der bevorzugte Anbieter geht unnötig verloren.
raw = None
for kandidat in self._provider_chain():
prov = kandidat
try:
raw = self._call(prov, prompt)
break
except Exception as e:
grund = self._mark_dead(prov, e)
if grund is None:
raise # vorübergehend → nicht wechseln
naechster = next(iter(self._provider_chain()), None)
log.warning(f"Provider '{prov}' ausgefallen: {grund}"
+ (f" — weiter mit '{naechster}'" if naechster
else " — KEIN Provider mehr verfügbar"))
if raw is None:
with self._lock:
self._error = "alle Provider ausgefallen (Key/Guthaben prüfen)"
return False
parsed = self._parse(raw)
# CN-/lokale Modelle (Qwen, z.ai, Kimi, DeepSeek) driften gelegentlich ins
# Chinesische → einmal mit verschärfter Anweisung neu versuchen.
@@ -688,4 +763,9 @@ class TradingAgent:
"busy": self._busy,
"provider": self._active_provider(),
"configured": self.is_configured(),
# Aktuell übersprungene Anbieter + Restminuten. Macht den Ausfall
# sichtbar, statt ihn (wie bisher) nur ins Log zu schreiben — genau
# das hat die 18 h DeepSeek-Ausfall unbemerkt bleiben lassen.
"provider_dead": {p: round((t - time.time()) / 60.0, 1)
for p, t in self._dead.items() if t > time.time()},
}
+31 -2
View File
@@ -22,6 +22,7 @@ import MetaTrader5 as mt5
from core.config import (
HISTORY_DB_FILE, MAGIC, load_config, set_margin_buffer, set_risk_per_trade,
get_margin_buffer as _cfg_margin_buffer,
)
# ⚠ `parse_zones` wurde am 2026-07-31 hier entfernt (letzter Nutzer war `_sr_levels`,
# s. dort): die `[zones]`-Config stammte aus einem ~10 $ tieferen Kursregime und
@@ -1845,10 +1846,15 @@ class TradingEngine:
if "manual_margin" in st:
from core.config import set_manual_margin as _set
self._manual_margin = _set(st["manual_margin"] or 0)
if "margin_pct" in st:
from core.config import set_margin_buffer as _setp, get_margin_buffer
_setp(float(st["margin_pct"] or 0) or 95.0)
self._margin_pct = round(get_margin_buffer() * 100.0, 1)
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"Einsatz={'auto' if not getattr(self, '_manual_margin', 0) else f'{self._manual_margin:.2f}'}")
f"Einsatz={'auto' if not getattr(self, '_manual_margin', 0) else f'{self._manual_margin:.2f}'}"
f" · Einsatz-%={getattr(self, '_margin_pct', 0):.0f}")
except FileNotFoundError:
pass
except Exception as e:
@@ -1863,7 +1869,8 @@ class TradingEngine:
"auto_signal": self._auto_signal,
"auto_sr_close": self._auto_sr_close,
"sr_close_min_gain": self._sr_close_min_gain,
"manual_margin": getattr(self, "_manual_margin", 0.0)}, f)
"manual_margin": getattr(self, "_manual_margin", 0.0),
"margin_pct": getattr(self, "_margin_pct", 0.0)}, f)
os.replace(tmp, self._runtime_state_path)
except Exception as e:
log.warning(f"runtime_state speichern: {e}")
@@ -2781,6 +2788,25 @@ class TradingEngine:
self._save_runtime_state()
return v
def set_margin_pct(self, pct: float) -> float:
"""Einsatz als PROZENT der freien Margin (User-Wunsch 2026-08-05).
Das ist die zweite Hälfte des Einsatz-Felds: `set_manual_margin` gibt einen
FESTEN Betrag vor, das hier den ANTEIL. Beides existierte bisher nur in der
`ini` (`margin_buffer_pct`, live 95) — die Prozent-Schraube hatte keine UI.
⚠ VORRANG: ein fester Betrag (`manual_margin > 0`) schlägt den Prozentsatz —
`calc_lots` prüft ihn zuerst. Der Prozentsatz bleibt dabei die Obergrenze
(freie Margin × pct), ein zu hoher Fixbetrag wird darauf gedeckelt.
⚠ Wirkt auf ALLE neuen Positionen, auch die autonomen. Neustart-fest.
⚠ `set_margin_buffer` klemmt auf 199 % — 100 % wäre ohnehin unhaltbar
(kein Puffer für Spread/Swap → Margin-Call beim Öffnen)."""
from core.config import set_margin_buffer as _set, get_margin_buffer
_set(pct)
self._margin_pct = round(get_margin_buffer() * 100.0, 1)
self._save_runtime_state()
return self._margin_pct
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
@@ -3342,6 +3368,9 @@ class TradingEngine:
"manual_stats": self._manual_stats_cached(),
# Manuell vorgegebene Einsatz-Margin (0 = automatisch, s. set_manual_margin)
"manual_margin": getattr(self, "_manual_margin", 0.0),
# Einsatz als % der freien Margin (`margin_buffer_pct`). Greift, solange
# kein fester Betrag gesetzt ist — der hat Vorrang (s. set_margin_pct).
"margin_pct": round(_cfg_margin_buffer() * 100.0, 1),
# Live-Handelskosten relativ zur Vola (2026-07-24): Spread/ATR(M5).
# Gemessen (`backtest_realcosts.py`): Ø 0,265×ATR, Nacht 0,320,50 =
# Kostenfalle, 1517 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die
+20
View File
@@ -364,6 +364,26 @@ async def set_manual_margin(body: dict,
return {"ok": True, "value": val}
@app.post("/api/marginpct")
async def set_margin_pct(body: dict,
x_auth_token: str | None = Header(default=None)):
"""Einsatz als PROZENT der freien Margin setzen (User-Wunsch 2026-08-05).
Gegenstück zu `/api/manualmargin` (fester Betrag). ⚠ Ein gesetzter FESTER
Betrag hat Vorrang; der Prozentsatz bleibt dann nur noch die Obergrenze.
Wird auf 199 % geklemmt — 100 % ließe keinen Puffer für Spread/Swap."""
_require_auth(x_auth_token)
try:
v = float(body.get("value") or 0)
except (TypeError, ValueError):
return {"ok": False, "error": "ungültiger Wert"}
if v <= 0:
return {"ok": False, "error": "Prozentsatz muss > 0 sein"}
val = app.state.engine.set_margin_pct(v)
get_logger("server").info(f"[WEB] Einsatz-Prozentsatz: {val:.0f} % der freien Margin")
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).
+54
View File
@@ -908,6 +908,24 @@ function render(d) {
mgi.value = mm > 0 ? String(mm) : "";
mgi.classList.toggle("on", mm > 0);
}
// Einsatz in Prozent der freien Margin. Ausgegraut, sobald links ein fester
// Betrag steht — der hat Vorrang, und ohne den Hinweis wäre unklar, was gilt.
const mpi = $("marginpct-input");
if (mpi && document.activeElement !== mpi) {
const mp = d.margin_pct || 0;
mpi.value = mp > 0 ? String(mp) : "";
const fest = (d.manual_margin || 0) > 0;
mpi.classList.toggle("on", mp > 0 && !fest);
const wrap = $("marginpct-wrap");
if (wrap) {
wrap.classList.toggle("muted", fest);
wrap.title = fest
? `Ohne Wirkung: links steht ein fester Einsatz (${fmt(d.manual_margin, 2)} €). `
+ `Der Prozentsatz (${mp} %) bleibt nur die Obergrenze.`
: "Einsatz als Prozent der freien Margin (199). "
+ "Greift, solange links kein fester Betrag steht.";
}
}
// Einblendung: der Bot hat am S/R automatisch geschlossen (Zähler stieg)
const src = d.sr_close_count;
if (src != null) {
@@ -1307,6 +1325,42 @@ async function setManualMargin() {
});
})();
// Einsatz in PROZENT der freien Margin (User-Wunsch 2026-08-05) — das Gegenstück
// zum festen Betrag oben. Steuert `margin_buffer_pct`, das bisher nur in der ini
// stand. ⚠ Ein gesetzter fester Betrag hat Vorrang; darauf weist der Toast hin.
async function setMarginPct() {
const el = $("marginpct-input"); if (!el) return;
const v = parseFloat(el.value);
if (isNaN(v) || v < 1 || v > 99) {
toast("Bitte 199 % eingeben (100 % ließe keinen Puffer für Spread/Swap)", false);
return;
}
const tok = await ensureToken();
if (tok === null) return;
el.blur();
try {
const r = await fetch("api/marginpct", { 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();
if (j.ok === false) { toast(j.error || "abgelehnt", false); return; }
const fest = parseFloat(($("margin-input") || {}).value || "0") > 0;
toast(`Einsatz: ${fmt(j.value ?? v, 0)} % der freien Margin`
+ (fest ? " — greift erst, wenn der feste Betrag links leer ist" : ""), true);
} catch (e) {
toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false);
}
}
(() => {
const el = $("marginpct-input");
if (!el) return; // null-sicher (Cache-Regel)
el.addEventListener("change", setMarginPct);
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() {
+11 -4
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=136">
<link rel="stylesheet" href="style.css?v=137">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -239,10 +239,17 @@
(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>
<span class="mg-field" title="Feste Einsatz-Margin in € — leer/0 = automatisch (dann gilt der Prozentsatz rechts). 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>
<!-- Prozent-Gegenstück (User 2026-08-05). Steuert `margin_buffer_pct`, das
bisher nur in der ini stand. ⚠ Der feste €-Betrag hat VORRANG — steht er,
wird dieses Feld ausgegraut, damit nicht unklar bleibt, was gilt. -->
<span class="mg-field" id="marginpct-wrap" title="Einsatz als Prozent der freien Margin (199). Greift, solange links kein fester Betrag steht.">
<label for="marginpct-input">Einsatz %</label>
<input id="marginpct-input" type="number" inputmode="decimal" step="5" min="1" max="99" placeholder="95">
</span>
<button class="btn long" data-side="long">▲ LONG</button>
<button class="btn short" data-side="short">▼ SHORT</button>
<button class="btn close" data-side="close">✕ CLOSE</button>
@@ -267,6 +274,6 @@
<div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=136"></script>
<script src="app.js?v=137"></script>
</body>
</html>
+4
View File
@@ -528,3 +528,7 @@ main{padding-bottom:74px} /* Platz für die fixe Leiste */
/* aktiv gesetzt = sichtbar hervorgehoben, damit ein fester Einsatz nicht
unbemerkt bestehen bleibt */
.mg-field input.on { border-color: #d29922; color: #d29922; }
/* Prozent-Feld ohne Wirkung, weil links ein FESTER Betrag steht — sichtbar
zurückgenommen, statt zwei scheinbar gleichrangige Felder zu zeigen. */
.mg-field.muted { opacity: .45; }
.mg-field.muted input { border-style: dashed; }