diff --git a/core/engine.py b/core/engine.py index 36c0d08..f720d62 100644 --- a/core/engine.py +++ b/core/engine.py @@ -607,6 +607,10 @@ class TradingEngine: self._cb_remember = self._cb_limit_pct or 8.0 except (TypeError, ValueError): self._cb_limit_pct = 0.0 + # Einsatz je autonomem Pfad (0 = globaler `margin_buffer_pct`). + # Wird von `runtime_state.json` ueberschrieben, falls dort gesetzt. + self._margin_brk = 0.0 + self._margin_m15 = 0.0 self._circuit_tripped_date = None # Berlin-Datum, an dem der Breaker griff self._cb_realized = (0.0, 0.0) # (ts, realisierter Tages-P&L) gecacht ~30 s # Gewinn-mitnehmen (Take-Profit) — symmetrisch zum Notfall-Stop: schließt @@ -2284,6 +2288,10 @@ class TradingEngine: self._manual_margin = _set(st["manual_margin"] or 0) if "auto_m15" in st: self._auto_m15 = bool(st["auto_m15"]) + if "margin_brk" in st: + self._margin_brk = max(0.0, min(99.0, float(st["margin_brk"] or 0))) + if "margin_m15" in st: + self._margin_m15 = max(0.0, min(99.0, float(st["margin_m15"] or 0))) if "cb_remember" in st: self._cb_remember = float(st["cb_remember"] or 8.0) if "cb_limit_pct" in st: @@ -2323,6 +2331,8 @@ class TradingEngine: # 0 = aus. So ueberlebt auch die Hoehe den Neustart. "cb_limit_pct": getattr(self, "_cb_limit_pct", 0.0), "cb_remember": getattr(self, "_cb_remember", 8.0), + "margin_brk": getattr(self, "_margin_brk", 0.0), + "margin_m15": getattr(self, "_margin_m15", 0.0), "auto_m15": getattr(self, "_auto_m15", False), # Kegel-ACI-Skalen (2026-08-08): ohne Persistenz waere # die muehsam erlernte Kalibrierung nach jedem Neustart @@ -2599,7 +2609,31 @@ class TradingEngine: # bewusst KEINE globale Umstellung. Strukturell begruendet: Breakout- und # Pullback-Einstiege haben verschiedene Nachlauf-Dynamik. _atr_tf = mt5.TIMEFRAME_M5 if source == "auto_squeeze" else None - err = fn(sym, _atr_tf) + # ⚠⚠ EINSATZ JE PFAD (User-Wunsch 2026-08-19). Quellen-abhaengig + # wie die SL-Zeitebene direkt darueber, und aus demselben Grund: BRK und + # M15 sind verschiedene Populationen mit verschieden gemessener Guete + # (autonom -4,71 EUR/Lot gegen Mensch +1,56). 0 = globaler Wert. + # ⚠ Das ist KEINE gleichzeitige Aufteilung der Margin — der Bot haelt + # EINE Position. Es legt fest, wie gross die Position wird, WENN der + # jeweilige Pfad eroeffnet. Eine echte Reservierung braucht den + # ticket-basierten Umbau (42 Einzel-Positions-Annahmen). + _mg = {"auto_squeeze": getattr(self, "_margin_brk", 0.0), + "auto_m15": getattr(self, "_margin_m15", 0.0)}.get(source, 0.0) + _mg_alt = None + if _mg > 0: + import core.config as _cfgmod + _mg_alt = _cfgmod.MARGIN_BUFFER + # ⚠ direkt setzen statt ueber set_margin_buffer(): das loggt bei + # JEDEM Trade eine Zeile und wuerde den globalen Wert ueberschreiben. + _cfgmod.MARGIN_BUFFER = max(0.01, min(0.99, _mg / 100.0)) + log.info(f"[ORDER] Einsatz fuer {source}: {_mg:.0f}% " + f"(global {_mg_alt*100:.0f}%)") + try: + err = fn(sym, _atr_tf) + finally: + if _mg_alt is not None: + import core.config as _cfgmod2 + _cfgmod2.MARGIN_BUFFER = _mg_alt if err: return err # Vom BOT eröffnete Trades markieren → nur für die greift die 15-Min-Regel @@ -3394,6 +3428,32 @@ class TradingEngine: self._save_runtime_state() return v + def set_auto_margin(self, brk: float, m15: float) -> dict: + """Einsatz-Prozentsatz JE AUTONOMEM PFAD (User-Wunsch 2026-08-19). + + 0 = der globale Wert (`margin_buffer_pct`, live 40) gilt — also das + bisherige Verhalten. > 0 = dieser Pfad eroeffnet mit genau diesem Anteil + der freien Margin. + + ⚠⚠ DAS IST KEINE RESERVIERUNG. Der Bot haelt EINE Position; es kann + also nie BRK und M15 gleichzeitig geben. Das Feld steuert die GROESSE der + jeweiligen Position, nicht einen parallel vorgehaltenen Topf. Eine echte + Reservierung verlangt den ticket-basierten Umbau (42 Einzel-Positions- + Annahmen in `engine.py`, 25 in `trader.py`) — bis dahin wuerde der + Schutz-Stack (Trailing, Notfall-Stop, S/R-Close, Time-Stop) an der + FALSCHEN der beiden Positionen haengen. + + ⚠ Manuelle Trades bleiben unberuehrt — fuer die gilt weiter der globale + Prozentsatz. Das ist Absicht: der Mensch ist die gemessen bessere + Population (+1,56 gegen -4,71 EUR/Lot), seine Groesse gehoert ihm. + Neustart-fest ueber `runtime_state.json`.""" + self._margin_brk = max(0.0, min(99.0, float(brk or 0))) + self._margin_m15 = max(0.0, min(99.0, float(m15 or 0))) + log.info(f"Einsatz je Pfad: BRK {self._margin_brk:.0f}% · " + f"M15 {self._margin_m15:.0f}% (0 = global)") + self._save_runtime_state() + return {"brk": self._margin_brk, "m15": self._margin_m15} + def set_margin_pct(self, pct: float) -> float: """Einsatz als PROZENT der freien Margin (User-Wunsch 2026-08-05). @@ -4488,6 +4548,9 @@ class TradingEngine: # 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), + # Einsatz je autonomem Pfad; 0 = der globale `margin_pct` gilt. + "margin_brk": getattr(self, "_margin_brk", 0.0), + "margin_m15": getattr(self, "_margin_m15", 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/server.py b/server.py index ffb3024..77fcf2b 100644 --- a/server.py +++ b/server.py @@ -412,6 +412,28 @@ async def set_manual_margin(body: dict, return {"ok": True, "value": val} +@app.post("/api/automargin") +async def set_auto_margin(body: dict, + x_auth_token: str | None = Header(default=None)): + """Einsatz-Prozentsatz JE AUTONOMEM PFAD (User-Wunsch 2026-08-19). + + `{"brk": 50, "m15": 25}` — 0 heisst "globaler Wert" (bisheriges Verhalten). + ⚠ KEINE gleichzeitige Reservierung: der Bot haelt EINE Position, es kann + also nie BRK und M15 nebeneinander geben. Das Feld steuert die GROESSE der + jeweiligen Position. Manuelle Trades bleiben beim globalen Prozentsatz.""" + _require_auth(x_auth_token) + try: + brk = float(body.get("brk") or 0) + m15 = float(body.get("m15") or 0) + except (TypeError, ValueError): + return {"ok": False, "error": "ungueltiger Wert"} + if brk < 0 or m15 < 0 or brk > 99 or m15 > 99: + return {"ok": False, "error": "erlaubt sind 0 (=global) bis 99"} + r = app.state.engine.set_auto_margin(brk, m15) + log.info(f"[WEB] Einsatz je Pfad: BRK {r['brk']:.0f}% · M15 {r['m15']:.0f}%") + return {"ok": True, **r} + + @app.post("/api/marginpct") async def set_margin_pct(body: dict, x_auth_token: str | None = Header(default=None)): diff --git a/web/app.js b/web/app.js index 933ea31..48e0689 100644 --- a/web/app.js +++ b/web/app.js @@ -169,6 +169,13 @@ function beepRecommendation(sig, isNew) { function renderBrk(d) { const box = $streng("card-brk"); if (!box) return; + // Einsatz je Pfad einspielen. ⚠ NICHT, solange das Feld den Fokus hat -- + // sonst ueberschreibt der 1-s-Snapshot die laufende Eingabe (dieselbe Falle + // wie beim Mindestgewinn-Feld). + [["am-brk", d.margin_brk], ["am-m15", d.margin_m15]].forEach(([id, v]) => { + const el = $streng(id); + if (el && document.activeElement !== el) el.value = (v > 0) ? v : ""; + }); const b5 = d.squeeze_b5; if (!b5) { box.classList.add("hidden"); return; } box.classList.remove("hidden"); @@ -2012,3 +2019,41 @@ document.addEventListener("visibilitychange", () => { }); window.addEventListener("pageshow", _wakeRefresh); window.addEventListener("focus", _wakeRefresh); + +// --- Einsatz je autonomem Pfad (User-Wunsch 2026-08-19) -------------------- +// ⚠ LEER heisst hier ausdruecklich 0 = "globaler Wert", NICHT "unveraendert". +// Deshalb `$streng` und eine explizite NaN-Behandlung: mit dem Auffang-`$` +// waere `.value` undefined -> NaN -> JSON.stringify macht daraus null, und +// der Server saehe einen anderen Wert als der Nutzer eingegeben hat. +async function setAutoMargin() { + const eb = $streng("am-brk"), em = $streng("am-m15"); + if (!eb || !em) { toast("Eingabefelder fehlen — Seite neu laden", false); return; } + const brk = eb.value.trim() === "" ? 0 : parseFloat(eb.value); + const m15 = em.value.trim() === "" ? 0 : parseFloat(em.value); + if (isNaN(brk) || isNaN(m15) || brk < 0 || m15 < 0 || brk > 99 || m15 > 99) { + toast("Erlaubt sind 0 (= global) bis 99", false); return; + } + const tok = await ensureToken(); + if (tok === null) return; + eb.blur(); em.blur(); + try { + const r = await fetch("api/automargin", { method: "POST", + headers: { "X-Auth-Token": tok, "Content-Type": "application/json" }, + body: JSON.stringify({ brk: brk, m15: m15 }) }); + if (r.status === 401) { clearToken(); throw new Error("401"); } + const j = await r.json(); + if (!j.ok) { toast(j.error || "abgelehnt", false); return; } + toast(`Einsatz: BRK ${brk || "global"} · M15 ${m15 || "global"}`, true); + } catch (e) { + toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false); + } +} +["am-brk", "am-m15"].forEach(id => { + const el = $streng(id); + if (!el) return; + el.addEventListener("change", setAutoMargin); + // Enter NUR blurren → `change` feuert dann genau EINMAL (Fix-Muster 22.07.). + el.addEventListener("keydown", e => { + if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } + }); +}); diff --git a/web/index.html b/web/index.html index 8ecb943..b6822ba 100644 --- a/web/index.html +++ b/web/index.html @@ -12,7 +12,7 @@ ⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard und muss offline funktionieren. --> - + @@ -170,6 +170,17 @@
Squeeze jetzt
Liegende Orders
B5-Regel
+
+ Einsatz je Pfad + + + / + + % + +
@@ -381,6 +392,6 @@ - +