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:
co-authored by
Claude Opus 5
parent
7e9b0196a5
commit
cc4f1a0863
@@ -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 (90–120 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 **1–99 %** (`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
@@ -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 90–120 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
@@ -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 1–99 % — 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,32–0,50 =
|
||||
# Kostenfalle, 15–17 Uhr ~0,13 = günstig. Reine Anzeige (Chip) — die
|
||||
|
||||
@@ -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 1–99 % 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
@@ -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 (1–99). "
|
||||
+ "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 1–99 % 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
@@ -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 (1–99). 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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user