Circuit Breaker: Tagesverlust-Stopp (daily_loss_limit_pct=8) — Überlebens-Basis
User-Ziel autonomes Handeln → Notbremse im Code (vorher abgelehnt, reaktiviert). Erreicht der Tages-P&L (realisiert+offen) -8% der Balance, schließt der Bot die Position und sperrt neue Auto-Trades bis zum naechsten Tag. _check_circuit_breaker im _pos_loop (Startup-Grace, ~30s realized-cache, Berlin-Datum-Reset), blockt Auto-Squeeze, Snapshot + roter Frontend-Banner (v=115). 80%-Margin bleibt (User). Mit 5 Szenarien getestet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
873b67a521
commit
cd8ee5b01b
@@ -298,6 +298,19 @@ class TradingEngine:
|
||||
except (TypeError, ValueError):
|
||||
self._emergency_margin_pct = 0.0
|
||||
self._emergency_margin_armed_ticket = None # Retry-Schutz wie bei sr_close_min_gain
|
||||
# ── Circuit Breaker (Tagesverlust-Stopp, User-Vorgabe 2026-07-27) ──────────
|
||||
# `[trading] daily_loss_limit_pct` > 0: erreicht der TAGES-P&L (realisiert heute
|
||||
# + offene Position) −pct% der Balance, schließt der Bot die Position und
|
||||
# eröffnet KEINE neuen Auto-Trades mehr bis zum nächsten Tag. Überlebens-Netz
|
||||
# gegen Verlust-Kaskaden (früher abgelehnt; mit dem Autonomie-Ziel reaktiviert
|
||||
# — unter 80 %-Margin schützt es v. a. gegen die Serie, nicht den Einzeltrade,
|
||||
# dessen 2×ATR-SL allein schon >pct% verlieren kann). 0 = aus.
|
||||
try:
|
||||
self._cb_limit_pct = max(0.0, float(self.cfg["trading"].get("daily_loss_limit_pct", "0") or 0))
|
||||
except (TypeError, ValueError):
|
||||
self._cb_limit_pct = 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
|
||||
# bei P&L ≥ +Wert. Gemerkter Wert über Trades hinweg (0 = Auto-Arm aus).
|
||||
self._takeprofit: float | None = None
|
||||
@@ -476,6 +489,7 @@ class TradingEngine:
|
||||
try:
|
||||
if self.data.symbol:
|
||||
self.trader.refresh(self.data.symbol)
|
||||
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
|
||||
self._check_auto_close()
|
||||
self._check_sr_close()
|
||||
self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch
|
||||
@@ -585,6 +599,71 @@ class TradingEngine:
|
||||
Bot keinen laufenden Trade selbst (Daten noch nicht stabil nach Neustart)."""
|
||||
return time.time() < self._no_autoclose_until
|
||||
|
||||
# ── Circuit Breaker (Tagesverlust-Stopp) ─────────────────────────────────────
|
||||
def _day_realized_cached(self) -> float:
|
||||
"""Realisierter Tages-P&L (heute geschlossene Trades), gecacht ~30 s —
|
||||
damit die SQLite-Abfrage nicht jeden 1-s-Tick läuft."""
|
||||
now = time.time()
|
||||
if now - self._cb_realized[0] < 30:
|
||||
return self._cb_realized[1]
|
||||
try:
|
||||
v = float((self.history.stats_overview("today") or {}).get("total_pnl") or 0.0)
|
||||
except Exception:
|
||||
v = self._cb_realized[1]
|
||||
self._cb_realized = (now, v)
|
||||
return v
|
||||
|
||||
def _circuit_halted(self) -> bool:
|
||||
"""True, wenn der Circuit Breaker HEUTE (Berlin) bereits ausgelöst hat →
|
||||
keine neuen Auto-Trades bis zum nächsten Tag."""
|
||||
if self._cb_limit_pct <= 0 or not self._circuit_tripped_date:
|
||||
return False
|
||||
return self._circuit_tripped_date == datetime.now(_BERLIN).date()
|
||||
|
||||
def _check_circuit_breaker(self):
|
||||
"""Tagesverlust-Stopp (User-Vorgabe 2026-07-27): erreicht der Tages-P&L
|
||||
(realisiert heute + offene Position) −`daily_loss_limit_pct`% der Balance,
|
||||
schließt der Bot die Position und sperrt neue Auto-Trades bis zum nächsten
|
||||
Tag. Wirkt auch bei gesperrtem Handy. Fail-safe."""
|
||||
if self._cb_limit_pct <= 0:
|
||||
return
|
||||
if self._in_startup_grace(): # nach Neustart nicht auf halb-init Daten feuern
|
||||
return
|
||||
today = datetime.now(_BERLIN).date()
|
||||
if self._circuit_tripped_date and self._circuit_tripped_date != today:
|
||||
self._circuit_tripped_date = None # neuer Tag → Reset
|
||||
log.info("Circuit Breaker zurückgesetzt (neuer Handelstag)")
|
||||
if self._circuit_tripped_date == today:
|
||||
return # heute schon ausgelöst
|
||||
bal = float(self.data.balance or 0.0)
|
||||
if bal <= 0:
|
||||
return
|
||||
ps = self.trader.snapshot()
|
||||
open_pnl = float(ps.get("pnl") or 0.0)
|
||||
day_total = self._day_realized_cached() + open_pnl
|
||||
limit_eur = self._cb_limit_pct / 100.0 * bal
|
||||
if day_total > -limit_eur:
|
||||
return
|
||||
self._circuit_tripped_date = today
|
||||
log.warning(f"🛑 CIRCUIT BREAKER: Tages-P&L {day_total:+.2f} ≤ -{limit_eur:.2f} € "
|
||||
f"({self._cb_limit_pct:.0f}% von {bal:.0f}) → Position schließen, "
|
||||
f"keine Auto-Trades mehr heute")
|
||||
sym = self.data.symbol or ""
|
||||
tg = self.cfg["telegram"]
|
||||
tg_on = (tg.get("enabled", "false").lower() == "true" and tg.get("bot_token"))
|
||||
if ps.get("ticket"):
|
||||
err = self.trader.close(reason="circuit_breaker")
|
||||
if err:
|
||||
log.error(f"Circuit-Breaker-Close fehlgeschlagen: {err}")
|
||||
if tg_on:
|
||||
try:
|
||||
send_telegram(f"🛑 <b>CIRCUIT BREAKER</b>\n{sym} Tagesverlust {day_total:+.2f} € "
|
||||
f"≤ -{limit_eur:.2f} € ({self._cb_limit_pct:.0f}% der Balance). "
|
||||
f"Position geschlossen, keine Auto-Trades mehr heute.",
|
||||
tg["bot_token"], tg["chat_id"])
|
||||
except Exception as e:
|
||||
log.warning(f"Circuit-Breaker-Telegram: {e}")
|
||||
|
||||
def _check_auto_close(self):
|
||||
"""Server-seitiger Auto-Close (wirkt auch bei gesperrtem Handy): Notfall-Stop
|
||||
(P&L ≤ −Schwelle) UND Gewinn-mitnehmen (P&L ≥ +Ziel). Beide werden bei einer
|
||||
@@ -762,6 +841,8 @@ class TradingEngine:
|
||||
armiert beim Öffnen automatisch (2 % Konto, via `_check_auto_close`). Fail-safe."""
|
||||
if not self._auto_squeeze:
|
||||
return
|
||||
if self._circuit_halted(): # Tagesverlust-Stopp aktiv → heute keine Auto-Trades
|
||||
return
|
||||
try:
|
||||
sq = (self.wave.snapshot() or {}).get("squeeze") or {}
|
||||
if sq.get("state") != "active":
|
||||
@@ -1921,6 +2002,14 @@ class TradingEngine:
|
||||
open_pnl = float(position.get("pnl") or 0.0)
|
||||
day_pl = {"realized": round(day_realized, 2), "open": round(open_pnl, 2),
|
||||
"total": round(day_realized + open_pnl, 2)}
|
||||
# Circuit Breaker — Status für die UI (Tagesverlust-Stopp)
|
||||
_bal = float(self.data.balance or 0.0)
|
||||
_cb_limit_eur = round(self._cb_limit_pct / 100.0 * _bal, 2) if self._cb_limit_pct > 0 else None
|
||||
circuit_breaker = {
|
||||
"enabled": self._cb_limit_pct > 0, "limit_pct": self._cb_limit_pct,
|
||||
"limit_eur": _cb_limit_eur, "day_total": day_pl["total"],
|
||||
"tripped": self._circuit_halted(),
|
||||
}
|
||||
sr_close_hint = self._sr_close_hint(market, position, wave_snap)
|
||||
stop_approach = self._stop_approach_hint(market, position, wave_snap)
|
||||
return {
|
||||
@@ -1940,6 +2029,7 @@ class TradingEngine:
|
||||
"session": session_state(),
|
||||
"position": position,
|
||||
"day_pl": day_pl,
|
||||
"circuit_breaker": circuit_breaker,
|
||||
"emergency_loss": self._emergency_loss,
|
||||
"emergency_remember": self._emergency_remember,
|
||||
"emergency_pct": self._emergency_pct,
|
||||
|
||||
Reference in New Issue
Block a user