S/R-Auto-Close komplett entfernt + Circuit-Breaker als runder CB-Knopf (v=210)

(1) S/R-AUTO-CLOSE RAUS (User-Vorgabe: "die brauche ich nicht mehr").
Der Schalter stand seit dem 19.08. ohnehin auf AUS - abgeschaltet, nachdem
analyze_pbreak_live.py das P(break)-Modell live als INVERTIERT gemessen hatte
(AUC 0,452, Klassen monoton rueckwaerts). Ohne funktionierendes Gate ist der
S/R-Close faktisch der PAUSCHALE, und der ist 2x gemessen und verworfen
(Gewinner-Kappen). Die Entfernung vollzieht den gemessenen Stand nach.

WEG: _check_sr_close, set_sr_autoclose, set_sr_close_min_gain, die
Konstruktor-Felder, die %-Armierung des Gewinn-Close, drei Snapshot-Felder,
beide runtime_state-Schluessel, die Reichweiten- und Systemstatus-Zeilen,
/api/srclose und /api/srclose_min, der Knopf S/R, das Gewinn-Close-Feld, der
Toast "Bot hat am Level geschlossen".
BLEIBT: `_sr_close_hint` (App-Hinweis, P(break)-Chartlinien, Copilot-Kontext,
pbreak_predictions-Tracking), `stop_approach`, das Modell selbst und die
closed_by=sr_close-Historie in der DB. Live gegengeprueft: die drei Felder sind
aus dem Snapshot verschwunden, sr_close_hint und stop_approach sind da, beide
Endpunkte antworten 405.
Sicherung: .removed_backup/sr_auto_close_2026-08-21.py

⚠ Der Close-Alarm gilt jetzt schlicht ab jedem Gewinn (`minGain = 0`) - die
Schwelle gab es nur fuer den Auto-Close. Das ist der Zustand von vor 2026-07.

⚠ Der Test test_automatik_zeile_nennt_alle_schalter war MITGEZAEHLT und faellt
korrekt - er ist angepasst, nicht entfernt: jetzt zwei Schalter statt drei, und
die Negativ-Pruefungen ("SIG"/"S/R-Close" duerfen NICHT auftauchen) sind der
eigentliche Wert, denn ein wieder auftauchender Pfad waere ein stiller Rueckbau.

(2) CIRCUIT BREAKER als runder Knopf "CB" rechts neben BALANCE.
⚠ Farb-Logik wie vorgegeben: GRUEN = inaktiv (aus), ROT = aktiv (scharf).
Bewusst NICHT die uebliche "gruen = geschuetzt"-Lesart - hier signalisiert Rot,
dass eine Automatik eingreifen kann. Im Code und im Markup steht das dran, damit
es niemand spaeter "korrigiert".
⚠ VERSCHOBEN, nicht kopiert: zwei Elemente mit derselben ID waeren ein stiller
Fehler, weil getElementById nur das erste findet.

⚠ Beim Entfernen zwei eigene Fehler: (a) ein Slice ueber `rindex("try:")`
schnitt durch fremde Konstruktor-Bloecke und riss Kommentar-Fragmente heraus -
zurueckgesetzt und zeilenbasiert von hinten neu gemacht; (b) beim Toast-Block
eine Klammer zu viel entfernt, von `node --check` sofort gefangen.

Pipeline A-G gruen, 97 Tests, 14 render*-Funktionen, deployt und am
ausgelieferten HTML/Snapshot verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-21 10:47:44 +02:00
co-authored by Claude Opus 5
parent ebf10da00e
commit 0dd2e524b7
6 changed files with 75 additions and 209 deletions
+13 -121
View File
@@ -444,14 +444,11 @@ class TradingEngine:
self._close_alert_ts = 0.0 # letzter CLOSE-Telegram-Push (5-min-Cooldown)
self._close_alert_ticket = None # Ticket, auf das sich der Cooldown bezieht
self._close_alert_count = 0 # wie oft der Flip-Close-Alarm gefeuert hat
self._auto_sr_close = self.cfg["trading"].get("auto_sr_close", "true").lower() == "true"
try:
self._sr_close_pbreak = float(self.cfg["trading"].get("sr_close_pbreak", "0.60"))
except (TypeError, ValueError):
self._sr_close_pbreak = 0.60
self._sr_close_pbreak = max(0.30, min(0.90, self._sr_close_pbreak)) # sinnvolles Band
self._sr_close_count = 0 # wie oft der Bot am S/R automatisch schloss
self._sr_close_last_ticket = None # verhindert Doppel-Auslösung je Ticket
# Autonomer Entry auf den gemessen-validierten Squeeze-Ausbruch (nur FLAT).
# ⚠ ECHTE Order — Default AUS (Autonomie nie stillschweigend an). `[trading]
# auto_squeeze=true` aktiviert. Dedup über (dir, level) des Ausbruchs.
@@ -575,24 +572,6 @@ class TradingEngine:
self._flip_close_count = 0
self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die
# Open→Instant-Close-Kaskade bei zu engem Notfall-Stop)
self._sr_close_min_gain_armed_ticket = None # Ticket, für das die %-Schwelle schon
# aus echter Margin berechnet wurde (Retry-Schutz,
# s. `_check_auto_close`)
try:
self._sr_close_min_gain = max(0.0, float(
self.cfg["trading"].get("sr_close_min_gain", "0")))
except (TypeError, ValueError):
self._sr_close_min_gain = 0.0
# Gewinn-Close automatisch auf X% des Einsatzes (Margin) DIESER Position setzen,
# sobald sie eröffnet wird (User-Vorgabe 2026-07-23) — überschreibt den manuell
# gemerkten/persistierten Wert bei jeder ECHT NEUEN Position (nicht beim
# Wiedererkennen nach Neustart, s. `_check_auto_close`). 0 = aus (dann bleibt der
# manuell gesetzte/gemerkte Wert wie bisher maßgeblich).
try:
self._sr_close_min_gain_pct = max(0.0, float(
self.cfg["trading"].get("sr_close_min_gain_pct", "1.0")))
except (TypeError, ValueError):
self._sr_close_min_gain_pct = 1.0
self._pb_state = None # geglättetes P(break) + Hysterese-Zone je Level
self._stop_pb_state = None # dito für die Gegen-/Stop-Seiten-Anzeige
# (gemessen `analyze_pbreak_flicker.py`: mom3 der
@@ -946,7 +925,6 @@ class TradingEngine:
self._check_slot_reichweite() # meldet ungedeckte 2. Position
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
self._check_flip_close() # Empfehlung dreht → schließen
self._check_sr_close()
# ZUERST die ruhenden Stop-Orders pflegen (setzen/stornieren),
# DANN der Market-Fallback: so kann eine bereits liegende Order
# den Ausbruch am Level abfangen, bevor nachgejagt würde.
@@ -1202,8 +1180,6 @@ class TradingEngine:
or (getattr(self, "_emergency_pct", 0) or 0) > 0
or (getattr(self, "_emergency_remember", 0) or 0) > 0):
offen.append("Notfall-Stop")
if getattr(self, "_auto_sr_close", False):
offen.append("S/R-Close")
if (getattr(self, "_adverse15_atr", 0) or 0) > 0:
offen.append("15-Min-Regel")
if getattr(self, "_flip_close", False):
@@ -1321,7 +1297,6 @@ class TradingEngine:
# bei offenem Trade kappte sonst die von Hand gesetzten 25 € auf 19,42).
self._emergency_loss = self._emergency_state_loss
self._takeprofit = self._emergency_state_tp
self._sr_close_min_gain_armed_ticket = ticket # wiederhergestellter Wert
# gilt als final — NICHT auf 1% zurücksetzen
self._emergency_margin_armed_ticket = ticket # dito für den margin-%-Stop
log.info(f"Auto-Close wiederhergestellt (Pos {ticket}): Stop "
@@ -1359,16 +1334,8 @@ class TradingEngine:
# umgesetzt, der Ticket-Wechsel-Zweig feuert nur 1×) — konnte den Trade zu
# früh (falscher, zu niedriger Alt-Wert) ODER dauerhaft mit falscher Schwelle
# laufen lassen.
if (self._sr_close_min_gain_pct > 0
and self._sr_close_min_gain_armed_ticket != ticket):
margin = ps.get("margin") or 0.0
if margin > 0:
self._sr_close_min_gain = round(
self._sr_close_min_gain_pct / 100.0 * margin, 2)
self._sr_close_min_gain_armed_ticket = ticket
self._save_runtime_state()
log.info(f"Gewinn-Close armiert (Pos {ticket}): "
f"+{self._sr_close_min_gain:.2f} ({self._sr_close_min_gain_pct:.0f}% Margin {margin:.2f})")
# ⚠ Die automatische %-Armierung des Gewinn-Close ist am 2026-08-21
# mit dem S/R-Auto-Close entfallen — sie diente ausschliesslich ihm.
# Notfall-Stop als % der Margin — gleiches Retry-Muster (margin-Wert kann im
# ersten Tick nach dem Öffnen noch 0 sein). Hat Vorrang vor Balance-%/Fixwert
# (die im "echt neue Position"-Zweig oben bewusst übersprungen wurden, wenn
@@ -1430,62 +1397,17 @@ class TradingEngine:
self._takeprofit = None
return
def _check_sr_close(self):
"""Automatischer S/R-Close (User-Vorgabe): schließt eine Position **im Plus**
selbstständig, wenn die gemessene Bedingung greift (Kurs klebt am gegenüber-
liegenden Level + schwacher Anlauf Durchbruch ~26 %, Close schlägt Laufen-
lassen; `backtest_srbreak.py`). Nur bei `auto_sr_close=true`, genau 1×/Ticket,
wirkt auch bei gesperrtem Handy. Fail-safe."""
if not self._auto_sr_close:
return
if self._in_startup_grace(): # Startup-Schonfrist: kein Sofort-Close nach Neustart
return
ps = self.trader.snapshot()
ticket = ps.get("ticket")
if not ticket:
self._sr_close_last_ticket = None
return
if ticket == self._sr_close_last_ticket: # bereits ausgelöst → nicht erneut
return
pnl = ps.get("pnl")
if pnl is None or pnl <= 0: # NUR bei Trade im Plus (User-Vorgabe)
return
# Optionaler Mindestgewinn (User-Vorgabe; Default 0=aus — gemessen ist ohne
# besser): unter der Schwelle wird am Level NICHT geschlossen, Trade läuft.
if self._sr_close_min_gain > 0 and pnl < self._sr_close_min_gain:
return
hint = self._sr_close_hint(self.data.snapshot(), ps, self.wave.snapshot())
if not hint or not hint.get("close"): # am Level, aber P(break) ≥ Schwelle → laufen lassen
return
self._sr_close_last_ticket = ticket # vor dem Close markieren (kein Doppel)
sym = self.data.symbol or ""
_confirm = hint.get("confirm")
log.info(f"🎯 S/R-AUTO-CLOSE: {sym} P&L {pnl:+.2f} — am Level {hint['level']}, "
f"Durchbruch nur ~{hint['p_break']}% (< {self._sr_close_pbreak:.0%})"
+ (f" — Bestätigung erst ab {_confirm}" if _confirm else "")
+ " → Gewinn sichern")
err = self.trader.close(reason="sr_close")
tg = self.cfg["telegram"]
tg_on = (tg.get("enabled", "false").lower() == "true" and tg.get("bot_token"))
if err:
log.error(f"S/R-Auto-Close fehlgeschlagen: {err}")
self._sr_close_last_ticket = None # Fehlschlag → nächster Tick erneut
if tg_on:
try: send_telegram(f"⚠ <b>S/R-Close FEHLGESCHLAGEN</b>\n{sym} P&L {pnl:+.2f} "
f"— bitte MANUELL schließen! ({err})",
tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"S/R-Close-Telegram (Fehler): {e}")
return
# Richtung + Zeit merken → Auto-Squeeze-Cooldown gegen Gleichrichtungs-Roundtrip
self._sr_close_dir = "LONG" if ps.get("order_type") == 0 else "SHORT"
self._sr_close_ts = time.time()
self._sr_close_count += 1
if tg_on:
try: send_telegram(f"🎯 <b>S/R-Auto-Close</b>\n{sym} bei P&L {pnl:+.2f} geschlossen "
f"— Kurs klebte am Level {hint['level']} mit schwachem Anlauf "
f"(Durchbruch ~{hint['p_break']}% unwahrscheinlich).",
tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"S/R-Close-Telegram: {e}")
# ⚠⚠ `_check_sr_close` ENTFERNT am 2026-08-21 (User-Vorgabe). Der Schalter
# stand seit dem 19.08. auf AUS — abgeschaltet, nachdem
# `analyze_pbreak_live.py` das P(break)-Modell live als INVERTIERT gemessen
# hatte (AUC 0,452, Klassen monoton rueckwaerts). Ohne funktionierendes
# Gate ist der S/R-Close faktisch der PAUSCHALE, und der ist 2x gemessen
# und verworfen (Gewinner-Kappen). Die Entfernung vollzieht den gemessenen
# Stand nach; sie schaltet nichts ab, was noch lief.
# ⚠ BLEIBT: `_sr_close_hint` (App-Hinweis, P(break)-Chartlinien,
# Copilot-Kontext, `pbreak_predictions`-Tracking), `stop_approach`, das
# Modell selbst und die `closed_by=sr_close`-Historie in der DB.
# Sicherung: `.removed_backup/sr_auto_close_2026-08-21.py`.
def _auto_guard(self, pfad: str) -> str | None:
"""Die NICHT richtungsabhängigen Gates für autonome Entries.
@@ -2174,24 +2096,6 @@ class TradingEngine:
tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"Adverse15-Telegram: {e}")
def set_sr_autoclose(self, on: bool) -> bool:
"""Automatischen S/R-Close an/aus (UI/Config). Rückgabe = neuer Zustand."""
self._auto_sr_close = bool(on)
log.info(f"Auto-S/R-Close {'AN' if self._auto_sr_close else 'AUS'}")
self._save_runtime_state()
return self._auto_sr_close
def set_sr_close_min_gain(self, value) -> float:
"""Mindestgewinn (EUR) für den S/R-Auto-Close. 0/leer = aus."""
try:
v = max(0.0, float(value)) if value not in (None, "") else 0.0
except (TypeError, ValueError):
v = 0.0
self._sr_close_min_gain = v
log.info(f"S/R-Close-Mindestgewinn {('+' + format(v, '.2f')) if v > 0 else 'aus'}")
self._save_runtime_state() # neustart-fest (Fix 2026-07-22 — war vergessen)
return self._sr_close_min_gain
def set_sltp(self, sl=None, tp=None) -> dict:
"""Manuelles SL/TP der offenen Position setzen. **Deaktiviert das Trailing**
(User-Vorgabe) sonst zöge es SL/TP beim nächsten Tick wieder um. Rückgabe
@@ -2731,10 +2635,6 @@ class TradingEngine:
# ⚠ `auto_signal` wird seit 2026-08-12 NICHT mehr gelesen (Pfad
# entfernt). Ein Altbestand in der Datei schadet nicht — er wird
# beim naechsten Schreiben ohnehin nicht mehr mitgeschrieben.
if "auto_sr_close" in st:
self._auto_sr_close = bool(st["auto_sr_close"])
if "sr_close_min_gain" in st:
self._sr_close_min_gain = max(0.0, float(st["sr_close_min_gain"] or 0))
if "manual_margin" in st:
from core.config import set_manual_margin as _set
self._manual_margin = _set(st["manual_margin"] or 0)
@@ -2783,8 +2683,6 @@ class TradingEngine:
self._cone_skalen = {int(k): float(v)
for k, v in (st["cone_skalen"] or {}).items()}
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-%={getattr(self, '_margin_pct', 0):.0f}")
except FileNotFoundError:
@@ -2798,8 +2696,6 @@ class TradingEngine:
tmp = self._runtime_state_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump({"auto_squeeze": self._auto_squeeze,
"auto_sr_close": self._auto_sr_close,
"sr_close_min_gain": self._sr_close_min_gain,
"manual_margin": getattr(self, "_manual_margin", 0.0),
"margin_pct": getattr(self, "_margin_pct", 0.0),
# Circuit-Breaker als PROZENTZAHL (nicht bool):
@@ -5258,10 +5154,7 @@ class TradingEngine:
"entry_gap": self._entry_gap_cached(),
"sr_close_hint": sr_close_hint,
"stop_approach": stop_approach,
"auto_sr_close": self._auto_sr_close,
"sr_close_min_gain": self._sr_close_min_gain,
"sr_close_pbreak": self._sr_close_pbreak,
"sr_close_count": self._sr_close_count,
# Schwelle der Close-Benachrichtigung — sichtbar, damit belegbar ist,
# dass sie im LAUFENDEN Prozess angekommen ist. „Erfolg melden, wo
# nichts geprueft wurde" ist im Projekt bereits viermal passiert.
@@ -5488,7 +5381,6 @@ class TradingEngine:
try:
an = lambda b: "AN" if b else "aus" # noqa: E731
z.append(f"Automatik: BRK {an(self._auto_squeeze)} · "
f"S/R-Close {an(self._auto_sr_close)} · "
f"Flip {an(getattr(self, '_flip_close', False))}")
except Exception as e:
z.append(f"Automatik: nicht lesbar ({e})")