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:
Axel Hocks
2026-07-27 17:39:55 +02:00
co-authored by Claude Opus 4.8
parent 873b67a521
commit cd8ee5b01b
6 changed files with 148 additions and 5 deletions
+20 -3
View File
@@ -805,8 +805,25 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
Heuristik-TF nur im Speicher (kein `save_config` mehr) — sonst würde die
Secrets-`ini` bei jedem Switch neu geschrieben (Kommentar-Verlust/Churn). Beim
Neustart wählt die Heuristik den TF binnen 1 min neu.
- **Kein automatischer Tagesverlust-Schalter (Circuit Breaker):** vom User
abgelehnt — NICHT einbauen. Bei 90 %-Margin-Sizing bleibt Drawdown-Risiko hoch.
- **Circuit Breaker (Tagesverlust-Stopp) = AKTIV `[trading] daily_loss_limit_pct=8`
(User-Vorgabe 2026-07-27, im Rahmen des „Überlebens-Basis"-Pakets — vorher jahrelang
abgelehnt, mit dem Autonomie-Ziel reaktiviert):** erreicht der **Tages-P&L
(realisiert heute + offene Position)** pct % der Balance, schließt der Bot die
Position (`closed_by=circuit_breaker`) und sperrt **neue Auto-Trades bis zum
nächsten Tag** (`_check_auto_squeeze` prüft `_circuit_halted()`). `_check_circuit_
breaker` im `_pos_loop` (1 s, unter Startup-Grace pausiert), realisierter Tages-P&L
~30 s gecacht (`_day_realized_cached`), Berlin-Datum als Trip-Marker (`_circuit_
tripped_date`) → automatischer Reset am neuen Tag. Log 🛑 + Telegram (eigener,
nicht vom Close-Gate betroffen). Snapshot `circuit_breaker {enabled, limit_pct,
limit_eur, day_total, tripped}`, Frontend `#circuit-banner` (rot bei ausgelöst,
bernstein ab 60 % des Limits, v=115). ⚠ **80 %-Margin-Sizing bleibt** (User-
Entscheidung, 3.+ Mal bestätigt; `risk_pct=0`) → der 8 %-Tagesstopp schützt v. a.
gegen die **Verlust-Serie/-Kaskade**, NICHT vollständig gegen den Einzeltrade
(dessen 2×ATR-SL unter 80 % Margin allein schon >8 % verlieren kann). 0 = aus.
Mit 5 synthetischen Szenarien getestet (nicht-zu-früh · Trip+Close · kein
Doppel-Close · aus · Auto-Squeeze-Block). ⚠ Der frühere „NICHT einbauen"-Grund
gilt nicht mehr — Ziel hat sich zu **autonomem Handel** verschoben, und ein
unbeaufsichtigter Agent braucht die Notbremse im Code.
- **Notfall-Stop = AUS beim Öffnen (Stand 2026-07-24, `auto_emergency_margin_pct=0`,
`auto_emergency_pct=0`, `auto_emergency_loss=0`).** User-Vorgabe 2026-07-24: „gewinn
close und notfall close … nicht setzen beim Eröffnen" → alle drei Auto-Arm-Modi 0,
@@ -1197,7 +1214,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
Signal → **Hinweis-Dialog „trotzdem eröffnen?"** (überschreibbar). CLOSE bei
**negativem** P&L → Sicherheitsabfrage; im Plus/Breakeven direkt. Sonst keine
Token-Dialoge (`require_token=false`). Audio braucht 1× Nutzer-Tap.
- Asset-Version aktuell **v=114** (in `web/index.html` hochzählen, siehe Workflows).
- Asset-Version aktuell **v=115** (in `web/index.html` hochzählen, siehe Workflows).
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
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge`
+7
View File
@@ -271,6 +271,13 @@ DEFAULT_CONFIG = {
# sofort und vererbt sich per _emergency_remember auf alle Folge-Trades;
# real 2026-07-27 killte ein 1-€-Stop jeden Trade in Sekunden). 0 = kein Floor.
"emergency_min_eur": "3",
# Circuit Breaker (Tagesverlust-Stopp, User-Vorgabe 2026-07-27): erreicht der
# Tages-P&L (realisiert + offen) pct% der Balance, schließt der Bot die
# Position und sperrt neue Auto-Trades 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.
# 0 = aus.
"daily_loss_limit_pct": "8",
# Gewinn-mitnehmen automatisch beim Öffnen setzen (Kontowährung). 0 = aus
# (dann nur manuell im UI). Merkt den zuletzt gesetzten Wert.
"auto_takeprofit": "0",
+90
View File
@@ -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,
+20
View File
@@ -205,6 +205,25 @@ function renderStructure(s) {
}
}
// Circuit Breaker — roter Banner wenn ausgelöst, bernstein wenn nah (>60% des Limits).
function renderCircuitBreaker(cb) {
const el = $("circuit-banner");
if (!el) return;
if (!cb || !cb.enabled) { el.className = "circuit hidden"; el.textContent = ""; return; }
const limit = cb.limit_eur, day = cb.day_total;
if (cb.tripped) {
el.className = "circuit tripped";
el.textContent = `🛑 CIRCUIT BREAKER AKTIV — Tagesverlust ${fmt(day, 2)} € erreicht ` +
`(Limit ${fmt(limit, 0)} €). Keine Auto-Trades mehr heute.`;
} else if (limit && day < 0 && Math.abs(day) >= 0.6 * limit) {
el.className = "circuit near";
el.textContent = `⚠ Tagesverlust ${fmt(day, 2)} € — Stopp bei ${fmt(limit, 0)}` +
`(${Math.round(100 * Math.abs(day) / limit)} % erreicht)`;
} else {
el.className = "circuit hidden"; el.textContent = "";
}
}
function render(d) {
// ── Header ──────────────────────────────
$("conn").className = "dot " + (d.connected ? "on" : "off");
@@ -294,6 +313,7 @@ function render(d) {
// ── Gesamtempfehlung (aggregiert) ───────
renderVerdict(d.verdict || {}, d);
renderCircuitBreaker(d.circuit_breaker);
renderStructure(d.structure);
renderStatsCompact(d.stats_compact, d.market);
renderGaps(d.gaps, d.market);
+4 -2
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=114">
<link rel="stylesheet" href="style.css?v=115">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -40,6 +40,8 @@
</header>
<main id="view-dash" class="view">
<!-- Circuit Breaker (Tagesverlust-Stopp) — roter Banner, nur wenn ausgelöst/nah -->
<div id="circuit-banner" class="circuit hidden"></div>
<!-- Aktueller Trade (nur bei offener Position) — Anzeige + SL/TP/Close editierbar -->
<section class="card wide trade-bar hidden" id="trade-bar">
<h2 class="tb-title">aktueller Trade läuft</h2>
@@ -218,6 +220,6 @@
<div id="toast" class="toast hidden"></div>
<script src="lightweight-charts.standalone.production.js?v=114"></script>
<script src="app.js?v=114"></script>
<script src="app.js?v=115"></script>
</body>
</html>
+7
View File
@@ -134,6 +134,13 @@ main{display:grid;grid-template-columns:1fr 1fr;gap:11px;padding:11px}
.tb-btn:active{filter:brightness(1.3)}
/* ── Gesamtempfehlung ───────────────────── */
/* Circuit Breaker Banner (Tagesverlust-Stopp) — ganz oben, rot bei ausgelöst */
.circuit{margin:0 0 11px;padding:11px 14px;border-radius:var(--r);font-weight:700;
font-size:16px;text-align:center;letter-spacing:.2px}
.circuit.hidden{display:none}
.circuit.tripped{background:#3d1a1a;color:#ff6b6b;border:1px solid var(--down);
box-shadow:0 0 0 1px var(--down)}
.circuit.near{background:#2a1f00;color:var(--accent);border:1px solid #4a3600}
#card-verdict{border-color:var(--accent)}
.vd-head{display:flex;align-items:center;gap:14px;margin-bottom:10px}
.vd-ring{width:60px;height:60px;border-radius:50%;flex:0 0 auto;