UI aufgeraeumt + Telegram auf Closes reduziert (v=177)

Vier User-Vorgaben:
1. Einsatz-Felder entfernt. Backend bleibt vollstaendig -- Folge: der
   Prozentsatz ist ohne UI nur ueber ini + Neustart aenderbar.
2. BRK-Schalter entfernt, dafuer STOPP (Circuit-Breaker). Bewusster Tausch mit
   Konsequenz: der An/Aus-Schalter des AUTONOMEN Pfads ist aus der App weg (nur
   noch POST bzw. ini), sein Zustand bleibt in der Breakout-Karte sichtbar.
   Dafuer ist die Notbremse bedienbar, die am 17.08. real ausgeloest hat.
   set_circuit_breaker + POST /api/circuit + runtime_state (cb_limit_pct,
   cb_remember -- ohne das Gedaechtnis waere die Prozentzahl nach einmal
   Ausschalten fuer immer weg).
3. Telegram nur noch bei geschlossenen Trades: EINE Filterstelle vor
   _send_telegram_roh statt 17 Einzeleingriffe, _TG_ERLAUBT = ('Trade
   geschlossen',). Umkehren = Liste erweitern. Tages-/Wochenreport wird nur auf
   dem Telegram-Weg still, die E-Mail laeuft weiter.
   EINE Ausnahme mit Bypass: '<ok_word> FEHLGESCHLAGEN' beim Notfall-Close --
   dort ist die Position NICHT zu und es braucht eine Handeingabe.

Verifiziert: /api/circuit vorher 405, nachher 200; Toggle aus->an->8 %/57,94 EUR
persistiert; 85 Tests, node --check, check_nfalle gruen; 0 verwaiste
JS-Referenzen; genau eine Instanz je Port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-19 17:03:30 +02:00
co-authored by Claude Opus 5
parent 32c064a1a6
commit e48ad80c0a
8 changed files with 5907 additions and 8425 deletions
+77 -2
View File
@@ -38,7 +38,7 @@ from core.wave_rec import WaveRecommender
from core.elliott import ElliottAnalyzer
from core.agent import TradingAgent
from core.news import NewsFetcher, NewsTranslator
from core.notify import send_telegram
from core.notify import send_telegram as _send_telegram_roh
from core.mailer import send_graph_mail
from core.mt5_utils import mt5_lock
from core.market_hours import session_state
@@ -166,6 +166,40 @@ def _p_break(mom6: float, mom3: float, wt: float, dist: float) -> float:
return 1.0 / (1.0 + math.exp(-max(-30.0, min(30.0, z))))
# --- Telegram-Gate (User-Vorgabe 2026-08-19) --------------------------------
# "entferne alle telegram benachrichtigungen. nur noch benachrichtigungen ueber
# geschlossene trades."
#
# BEWUSST EINE STELLE statt 17 Einzeleingriffe: die Sender bleiben stehen
# (Entry-Meldungen, Reports, Alarme), nur der Versand wird gefiltert. Kleiner
# Diff, trivial umkehrbar (Liste erweitern = wieder an) -- und beim
# Zurueckdrehen muessen nicht 17 Stellen einzeln wieder stimmen.
#
# Der TAGES- und WOCHENREPORT wird dadurch nur auf dem TELEGRAM-Weg still --
# die E-Mail laeuft weiter (er geht ohnehin an beide). Engste Lesart der
# Vorgabe: sie nennt ausdruecklich Telegram.
#
# EINE AUSNAHME mit Bypass: "<ok_word> FEHLGESCHLAGEN" beim Notfall-/
# Gewinn-Close. Dort ist die Position NICHT zu und es braucht eine
# Handeingabe -- Stille genau dort waere die gefaehrlichste Variante.
_TG_ERLAUBT = ("Trade geschlossen",)
def send_telegram(text, *a, **kw):
"""Gefilterter Versand -- nur Meldungen aus `_TG_ERLAUBT` gehen raus.
Rueckgabe False fuer Unterdruecktes, damit Aufrufer, die den Erfolg
auswerten (z. B. der Report-Versand-Marker), nichts faelschlich als
'zugestellt' verbuchen."""
try:
if not any(k in (text or "") for k in _TG_ERLAUBT):
return False
except Exception:
return False
return _send_telegram_roh(text, *a, **kw)
class TradingEngine:
"""Headless-Pendant zu OilWidget: hält die core-Objekte, fährt die
Refresh-Loops als Threads und liefert einen aggregierten Snapshot."""
@@ -555,6 +589,9 @@ class TradingEngine:
# 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))
# Toggle-Gedaechtnis: ohne das waere die Prozentzahl nach
# einmal Ausschalten verloren (s. `set_circuit_breaker`).
self._cb_remember = self._cb_limit_pct or 8.0
except (TypeError, ValueError):
self._cb_limit_pct = 0.0
self._circuit_tripped_date = None # Berlin-Datum, an dem der Breaker griff
@@ -1024,7 +1061,10 @@ class TradingEngine:
if err:
log.error(f"{ok_word} fehlgeschlagen: {err}")
if tg_on:
try: send_telegram(f"⚠ <b>{ok_word} FEHLGESCHLAGEN</b>\n{sym} P&L {pnl:+.2f} "
# BYPASS am Gate vorbei: die Position ist hier NICHT geschlossen und
# es braucht eine Handeingabe. Stille waere hier der
# gefaehrlichste Fall.
try: _send_telegram_roh(f"⚠ <b>{ok_word} FEHLGESCHLAGEN</b>\n{sym} P&L {pnl:+.2f} "
f"({level_txt}) — bitte MANUELL schließen! ({err})",
tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"Auto-Close-Telegram (Fehler): {e}")
@@ -1453,6 +1493,33 @@ class TradingEngine:
self._save_runtime_state()
return self._auto_squeeze
def set_circuit_breaker(self, on: bool) -> bool:
"""Tagesverlust-Stopp an/aus (User-Wunsch 2026-08-19, Dashboard-Schalter).
Der Breaker hat KEINEN eigenen An/Aus-Schalter, sondern lebt in
`_cb_limit_pct` (0 = aus). Ein Toggle muss die Prozentzahl also merken,
sonst waere sie nach einmal Ausschalten fuer immer weg. `_cb_remember`
haelt den letzten Wert > 0; Startwert ist die ini (`daily_loss_limit_pct`,
aktuell 8), Rueckfall 8, falls die ini je auf 0 stand.
WAS ER TUT UND WAS NICHT: er SCHLIESST die offene Position bei
Erreichen des Tageslimits (das wirkt auf ALLE Trades) und sperrt neue
AUTONOME Entries bis zum naechsten Tag (`_squeeze_guard`). Manuelle
Orders sperrt er NICHT gemessen sind nur 6,1 % der Trades autonom,
die schliessende Haelfte ist also die wirksame. Real ausgeloest am
17.08. 18:35 (-82,42 <= -81,45 EUR).
"""
if on:
self._cb_limit_pct = float(getattr(self, "_cb_remember", 0.0) or 8.0)
else:
if self._cb_limit_pct > 0:
self._cb_remember = self._cb_limit_pct
self._cb_limit_pct = 0.0
log.info(f"Circuit-Breaker {'AN' if self._cb_limit_pct > 0 else 'AUS'}"
f" ({self._cb_limit_pct:.0f} %)")
self._save_runtime_state()
return self._cb_limit_pct > 0
def _check_adverse15(self):
"""15-Minuten-Regel (User-Wunsch 2026-07-30): steht ein **vom Bot eröffneter**
Trade 15 min nach Entry mehr als `adverse_15min_atr`×ATR im Minus schließen
@@ -2094,6 +2161,10 @@ 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 "cb_remember" in st:
self._cb_remember = float(st["cb_remember"] or 8.0)
if "cb_limit_pct" in st:
self._cb_limit_pct = max(0.0, float(st["cb_limit_pct"] 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)
@@ -2125,6 +2196,10 @@ class TradingEngine:
"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):
# 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),
# Kegel-ACI-Skalen (2026-08-08): ohne Persistenz waere
# die muehsam erlernte Kalibrierung nach jedem Neustart
# weg und liefe von 1,0 neu an. ⚠ JSON-Schluessel sind