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_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_ticket = None # Ticket, auf das sich der Cooldown bezieht
self._close_alert_count = 0 # wie oft der Flip-Close-Alarm gefeuert hat 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: try:
self._sr_close_pbreak = float(self.cfg["trading"].get("sr_close_pbreak", "0.60")) self._sr_close_pbreak = float(self.cfg["trading"].get("sr_close_pbreak", "0.60"))
except (TypeError, ValueError): except (TypeError, ValueError):
self._sr_close_pbreak = 0.60 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_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). # Autonomer Entry auf den gemessen-validierten Squeeze-Ausbruch (nur FLAT).
# ⚠ ECHTE Order — Default AUS (Autonomie nie stillschweigend an). `[trading] # ⚠ ECHTE Order — Default AUS (Autonomie nie stillschweigend an). `[trading]
# auto_squeeze=true` aktiviert. Dedup über (dir, level) des Ausbruchs. # auto_squeeze=true` aktiviert. Dedup über (dir, level) des Ausbruchs.
@@ -575,24 +572,6 @@ class TradingEngine:
self._flip_close_count = 0 self._flip_close_count = 0
self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die self._pos_close_dir = None # Art) → Squeeze-Wiedereinstiegs-Sperre (gegen die
# Open→Instant-Close-Kaskade bei zu engem Notfall-Stop) # 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._pb_state = None # geglättetes P(break) + Hysterese-Zone je Level
self._stop_pb_state = None # dito für die Gegen-/Stop-Seiten-Anzeige self._stop_pb_state = None # dito für die Gegen-/Stop-Seiten-Anzeige
# (gemessen `analyze_pbreak_flicker.py`: mom3 der # (gemessen `analyze_pbreak_flicker.py`: mom3 der
@@ -946,7 +925,6 @@ class TradingEngine:
self._check_slot_reichweite() # meldet ungedeckte 2. Position self._check_slot_reichweite() # meldet ungedeckte 2. Position
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades) self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
self._check_flip_close() # Empfehlung dreht → schließen self._check_flip_close() # Empfehlung dreht → schließen
self._check_sr_close()
# ZUERST die ruhenden Stop-Orders pflegen (setzen/stornieren), # ZUERST die ruhenden Stop-Orders pflegen (setzen/stornieren),
# DANN der Market-Fallback: so kann eine bereits liegende Order # DANN der Market-Fallback: so kann eine bereits liegende Order
# den Ausbruch am Level abfangen, bevor nachgejagt würde. # 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_pct", 0) or 0) > 0
or (getattr(self, "_emergency_remember", 0) or 0) > 0): or (getattr(self, "_emergency_remember", 0) or 0) > 0):
offen.append("Notfall-Stop") 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: if (getattr(self, "_adverse15_atr", 0) or 0) > 0:
offen.append("15-Min-Regel") offen.append("15-Min-Regel")
if getattr(self, "_flip_close", False): 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). # bei offenem Trade kappte sonst die von Hand gesetzten 25 € auf 19,42).
self._emergency_loss = self._emergency_state_loss self._emergency_loss = self._emergency_state_loss
self._takeprofit = self._emergency_state_tp self._takeprofit = self._emergency_state_tp
self._sr_close_min_gain_armed_ticket = ticket # wiederhergestellter Wert
# gilt als final — NICHT auf 1% zurücksetzen # gilt als final — NICHT auf 1% zurücksetzen
self._emergency_margin_armed_ticket = ticket # dito für den margin-%-Stop self._emergency_margin_armed_ticket = ticket # dito für den margin-%-Stop
log.info(f"Auto-Close wiederhergestellt (Pos {ticket}): 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 # umgesetzt, der Ticket-Wechsel-Zweig feuert nur 1×) — konnte den Trade zu
# früh (falscher, zu niedriger Alt-Wert) ODER dauerhaft mit falscher Schwelle # früh (falscher, zu niedriger Alt-Wert) ODER dauerhaft mit falscher Schwelle
# laufen lassen. # laufen lassen.
if (self._sr_close_min_gain_pct > 0 # ⚠ Die automatische %-Armierung des Gewinn-Close ist am 2026-08-21
and self._sr_close_min_gain_armed_ticket != ticket): # mit dem S/R-Auto-Close entfallen — sie diente ausschliesslich ihm.
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})")
# Notfall-Stop als % der Margin — gleiches Retry-Muster (margin-Wert kann im # 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 # ersten Tick nach dem Öffnen noch 0 sein). Hat Vorrang vor Balance-%/Fixwert
# (die im "echt neue Position"-Zweig oben bewusst übersprungen wurden, wenn # (die im "echt neue Position"-Zweig oben bewusst übersprungen wurden, wenn
@@ -1430,62 +1397,17 @@ class TradingEngine:
self._takeprofit = None self._takeprofit = None
return return
def _check_sr_close(self): # ⚠⚠ `_check_sr_close` ENTFERNT am 2026-08-21 (User-Vorgabe). Der Schalter
"""Automatischer S/R-Close (User-Vorgabe): schließt eine Position **im Plus** # stand seit dem 19.08. auf AUS — abgeschaltet, nachdem
selbstständig, wenn die gemessene Bedingung greift (Kurs klebt am gegenüber- # `analyze_pbreak_live.py` das P(break)-Modell live als INVERTIERT gemessen
liegenden Level + schwacher Anlauf Durchbruch ~26 %, Close schlägt Laufen- # hatte (AUC 0,452, Klassen monoton rueckwaerts). Ohne funktionierendes
lassen; `backtest_srbreak.py`). Nur bei `auto_sr_close=true`, genau 1×/Ticket, # Gate ist der S/R-Close faktisch der PAUSCHALE, und der ist 2x gemessen
wirkt auch bei gesperrtem Handy. Fail-safe.""" # und verworfen (Gewinner-Kappen). Die Entfernung vollzieht den gemessenen
if not self._auto_sr_close: # Stand nach; sie schaltet nichts ab, was noch lief.
return # ⚠ BLEIBT: `_sr_close_hint` (App-Hinweis, P(break)-Chartlinien,
if self._in_startup_grace(): # Startup-Schonfrist: kein Sofort-Close nach Neustart # Copilot-Kontext, `pbreak_predictions`-Tracking), `stop_approach`, das
return # Modell selbst und die `closed_by=sr_close`-Historie in der DB.
ps = self.trader.snapshot() # Sicherung: `.removed_backup/sr_auto_close_2026-08-21.py`.
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}")
def _auto_guard(self, pfad: str) -> str | None: def _auto_guard(self, pfad: str) -> str | None:
"""Die NICHT richtungsabhängigen Gates für autonome Entries. """Die NICHT richtungsabhängigen Gates für autonome Entries.
@@ -2174,24 +2096,6 @@ class TradingEngine:
tg["bot_token"], tg["chat_id"]) tg["bot_token"], tg["chat_id"])
except Exception as e: log.warning(f"Adverse15-Telegram: {e}") 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: def set_sltp(self, sl=None, tp=None) -> dict:
"""Manuelles SL/TP der offenen Position setzen. **Deaktiviert das Trailing** """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 (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 # ⚠ `auto_signal` wird seit 2026-08-12 NICHT mehr gelesen (Pfad
# entfernt). Ein Altbestand in der Datei schadet nicht — er wird # entfernt). Ein Altbestand in der Datei schadet nicht — er wird
# beim naechsten Schreiben ohnehin nicht mehr mitgeschrieben. # 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: if "manual_margin" in st:
from core.config import set_manual_margin as _set from core.config import set_manual_margin as _set
self._manual_margin = _set(st["manual_margin"] or 0) self._manual_margin = _set(st["manual_margin"] or 0)
@@ -2783,8 +2683,6 @@ class TradingEngine:
self._cone_skalen = {int(k): float(v) self._cone_skalen = {int(k): float(v)
for k, v in (st["cone_skalen"] or {}).items()} for k, v in (st["cone_skalen"] or {}).items()}
log.info(f"Runtime-State geladen: BRK={'AN' if self._auto_squeeze else 'aus'} · " 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={'auto' if not getattr(self, '_manual_margin', 0) else f'{self._manual_margin:.2f}'}"
f" · Einsatz-%={getattr(self, '_margin_pct', 0):.0f}") f" · Einsatz-%={getattr(self, '_margin_pct', 0):.0f}")
except FileNotFoundError: except FileNotFoundError:
@@ -2798,8 +2696,6 @@ class TradingEngine:
tmp = self._runtime_state_path + ".tmp" tmp = self._runtime_state_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f: with open(tmp, "w", encoding="utf-8") as f:
json.dump({"auto_squeeze": self._auto_squeeze, 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), "manual_margin": getattr(self, "_manual_margin", 0.0),
"margin_pct": getattr(self, "_margin_pct", 0.0), "margin_pct": getattr(self, "_margin_pct", 0.0),
# Circuit-Breaker als PROZENTZAHL (nicht bool): # Circuit-Breaker als PROZENTZAHL (nicht bool):
@@ -5258,10 +5154,7 @@ class TradingEngine:
"entry_gap": self._entry_gap_cached(), "entry_gap": self._entry_gap_cached(),
"sr_close_hint": sr_close_hint, "sr_close_hint": sr_close_hint,
"stop_approach": stop_approach, "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_pbreak": self._sr_close_pbreak,
"sr_close_count": self._sr_close_count,
# Schwelle der Close-Benachrichtigung — sichtbar, damit belegbar ist, # Schwelle der Close-Benachrichtigung — sichtbar, damit belegbar ist,
# dass sie im LAUFENDEN Prozess angekommen ist. „Erfolg melden, wo # dass sie im LAUFENDEN Prozess angekommen ist. „Erfolg melden, wo
# nichts geprueft wurde" ist im Projekt bereits viermal passiert. # nichts geprueft wurde" ist im Projekt bereits viermal passiert.
@@ -5488,7 +5381,6 @@ class TradingEngine:
try: try:
an = lambda b: "AN" if b else "aus" # noqa: E731 an = lambda b: "AN" if b else "aus" # noqa: E731
z.append(f"Automatik: BRK {an(self._auto_squeeze)} · " 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))}") f"Flip {an(getattr(self, '_flip_close', False))}")
except Exception as e: except Exception as e:
z.append(f"Automatik: nicht lesbar ({e})") z.append(f"Automatik: nicht lesbar ({e})")
-19
View File
@@ -346,15 +346,6 @@ async def set_sltp(payload: dict = Body(...),
return res return res
@app.post("/api/srclose")
async def toggle_srclose(x_auth_token: str | None = Header(default=None)):
"""Automatischen S/R-Close (P(break)-Regel) an/aus."""
_require_auth(x_auth_token)
eng = app.state.engine
state = eng.set_sr_autoclose(not eng._auto_sr_close)
return {"ok": True, "enabled": state}
@app.post("/api/autom15") @app.post("/api/autom15")
async def toggle_autom15(x_auth_token: str | None = Header(default=None)): async def toggle_autom15(x_auth_token: str | None = Header(default=None)):
"""M15-Karten-Auto-Entry an/aus (User-Test 2026-08-19, gemessen negativ).""" """M15-Karten-Auto-Entry an/aus (User-Test 2026-08-19, gemessen negativ)."""
@@ -464,16 +455,6 @@ async def set_margin_pct(body: dict,
# Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git-Historie. # Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git-Historie.
@app.post("/api/srclose_min")
async def srclose_min(payload: dict = Body(...),
x_auth_token: str | None = Header(default=None)):
"""Mindestgewinn (EUR) für den S/R-Auto-Close setzen. 0/leer = aus."""
_require_auth(x_auth_token)
get_logger("server").info(f"[WEB] Mindestgewinn angefordert: {payload.get('value')!r}")
cur = app.state.engine.set_sr_close_min_gain(payload.get("value"))
return {"ok": True, "min_gain": cur}
@app.post("/api/trail") @app.post("/api/trail")
async def toggle_trail(x_auth_token: str | None = Header(default=None)): async def toggle_trail(x_auth_token: str | None = Header(default=None)):
_require_auth(x_auth_token) _require_auth(x_auth_token)
+10 -6
View File
@@ -46,14 +46,18 @@ def _text(z):
def test_automatik_zeile_nennt_alle_schalter(): def test_automatik_zeile_nennt_alle_schalter():
"""⚠ Seit dem 2026-08-12 sind es DREI — der Auto-Signal-Pfad (SIG) ist """⚠ Seit dem 2026-08-21 sind es ZWEI. Entfernt wurden der Auto-Signal-Pfad
entfernt. Der Test zaehlt sie bewusst einzeln auf: verschwindet einer (SIG, am 12.08.) und der S/R-Auto-Close (am 21.08., User-Vorgabe — er stand
stillschweigend aus der Zeile, faellt es hier auf und nicht erst, wenn seit dem 19.08. ohnehin aus, weil das P(break)-Modell live INVERTIERT misst).
monatelang niemand merkt, dass ein Schalter aus ist (real geschehen: Der Test zaehlt sie bewusst einzeln auf: verschwindet einer stillschweigend
`auto_sr_close` stand 5 Tage aus, gemessener Wert +350,81 EUR).""" aus der Zeile, faellt es hier auf und nicht erst, wenn monatelang niemand
merkt, dass ein Schalter aus ist.
⚠ Die beiden Negativ-Pruefungen sind der eigentliche Wert: ein entfernter
Pfad, der wieder auftaucht, waere ein stiller Rueckbau."""
z = _text(_Stub(brk=True, src=True, flip=False)._system_status()) z = _text(_Stub(brk=True, src=True, flip=False)._system_status())
assert "BRK AN" in z and "S/R-Close AN" in z and "Flip aus" in z assert "BRK AN" in z and "Flip aus" in z
assert "SIG" not in z, "der entfernte Pfad darf nicht wieder auftauchen" assert "SIG" not in z, "der entfernte Pfad darf nicht wieder auftauchen"
assert "S/R-Close" not in z, "der S/R-Auto-Close ist entfernt (21.08.)"
def test_toter_logger_wird_gemeldet(): def test_toter_logger_wird_gemeldet():
+18 -56
View File
@@ -94,7 +94,7 @@ function beepCloseAlert() {
// unterscheidbar). Sofort beim Signal-Wechsel + WIEDERHOLUNG alle 30 s, solange // unterscheidbar). Sofort beim Signal-Wechsel + WIEDERHOLUNG alle 30 s, solange
// der Button blinkt (Empfehlung aktiv & flat) — einmaliger Piep war überhörbar. // der Button blinkt (Empfehlung aktiv & flat) — einmaliger Piep war überhörbar.
const _REC_REBEEP_MS = 30000; const _REC_REBEEP_MS = 30000;
let _lastRecBeep = 0, _lastRecSignal = null, _audioHintShown = false, _lastSrCloseCount = null; let _lastRecBeep = 0, _lastRecSignal = null, _audioHintShown = false;
// ── Laufzeit-Uhr des offenen Trades ─────────────────────────────────────────── // ── Laufzeit-Uhr des offenen Trades ───────────────────────────────────────────
// Öffnungszeit kommt als echte Epoch (Broker-Offset serverseitig korrigiert) im // Öffnungszeit kommt als echte Epoch (Broker-Offset serverseitig korrigiert) im
@@ -1519,16 +1519,13 @@ function render(d) {
+ " Entries). ACHTUNG: gemessen negativ -- reiner Test." + " Entries). ACHTUNG: gemessen negativ -- reiner Test."
: "Auto-Entry auf die gruene M15-Karte AUS (gemessen negativ)."; : "Auto-Entry auf die gruene M15-Karte AUS (gemessen negativ).";
} }
const srb = $streng("btn-sr");
if (srb) {
srb.classList.toggle("on", !!d.auto_sr_close);
srb.title = d.auto_sr_close
? "S/R-Auto-Close AN: schliesst am Level, wenn P(break) unter der Schwelle liegt."
: "S/R-Auto-Close AUS -- der Bot schliesst nicht mehr selbst am Level.";
}
const cbb = $streng("btn-cb"); const cbb = $streng("btn-cb");
if (cbb) { if (cbb) {
cbb.classList.toggle("on", !!cb.enabled); // ⚠ Vorgabe des Nutzers: GRUEN = inaktiv (aus), ROT = aktiv (scharf).
// Bewusst nicht die uebliche "gruen = geschuetzt"-Lesart — Rot
// signalisiert hier, dass eine Automatik eingreifen kann.
cbb.classList.toggle("aktiv", !!cb.enabled);
cbb.classList.toggle("inaktiv", !cb.enabled);
cbb.title = cb.enabled cbb.title = cb.enabled
? "Circuit Breaker AN: schliesst bei " + fmt(cb.limit_eur, 2) + " € (" ? "Circuit Breaker AN: schliesst bei " + fmt(cb.limit_eur, 2) + " € ("
+ cb.limit_pct + " % der Balance). Heute: " + fmt(cb.day_total, 2) + " €." + cb.limit_pct + " % der Balance). Heute: " + fmt(cb.day_total, 2) + " €."
@@ -1559,7 +1556,9 @@ function render(d) {
// Deckt sich mit dem Auto-Close-Gate im Backend (unter der Schwelle schließt der // Deckt sich mit dem Auto-Close-Gate im Backend (unter der Schwelle schließt der
// Bot ohnehin nicht → dann auch kein Alarm/Ton). // Bot ohnehin nicht → dann auch kein Alarm/Ton).
const posLong = p.order_type === 0; const posLong = p.order_type === 0;
const minGain = d.sr_close_min_gain || 0; // ⚠ Der Mindestgewinn ist mit dem S/R-Auto-Close entfallen; der
// Close-Alarm gilt jetzt schlicht ab jedem Gewinn (wie vor 2026-07).
const minGain = 0;
const gainOK = hasPos && (p.pnl || 0) > minGain; const gainOK = hasPos && (p.pnl || 0) > minGain;
const flipClose = gainOK && const flipClose = gainOK &&
((posLong && w.signal === "SHORT") || (!posLong && w.signal === "LONG")); ((posLong && w.signal === "SHORT") || (!posLong && w.signal === "LONG"));
@@ -1567,7 +1566,8 @@ function render(d) {
// schwacher Anlauf → Level hält ~74 % → Close schlägt Laufenlassen. Text-Hinweis // schwacher Anlauf → Level hält ~74 % → Close schlägt Laufenlassen. Text-Hinweis
// in der Position-Kachel immer; Blink/Ton nur über dem Mindestgewinn. // in der Position-Kachel immer; Blink/Ton nur über dem Mindestgewinn.
const srHint = d.sr_close_hint || null; // vorhanden = Kurs am Level const srHint = d.sr_close_hint || null; // vorhanden = Kurs am Level
const srAuto = !!d.auto_sr_close; // ⚠ Der Bot schliesst seit dem 2026-08-21 NICHT mehr selbst am Level.
const srAuto = false;
const srClose = !!(srHint && srHint.close); // P(break) < Schwelle → schließen const srClose = !!(srHint && srHint.close); // P(break) < Schwelle → schließen
const closeAlert = flipClose || (gainOK && srClose); const closeAlert = flipClose || (gainOK && srClose);
// ⚠ Der Alarm gilt der Position in Slot 1 (dort entsteht `sr_close_hint` und // ⚠ Der Alarm gilt der Position in Slot 1 (dort entsteht `sr_close_hint` und
@@ -1686,23 +1686,9 @@ function render(d) {
flipEl.className = "news-conflict hidden"; flipEl.textContent = ""; flipEl.className = "news-conflict hidden"; flipEl.textContent = "";
} }
} }
// ⚠ Der Auto-Close-Schalter der Trade-Leiste ist am 2026-08-20 entfernt // ⚠ Am 2026-08-21 entfernt: der Auto-Close-Schalter der Trade-Leiste, das
// (User-Wunsch) — er war doppelt: der Knopf S/R in der Aktionsleiste // Mindestgewinn-Feld und die Toast-Meldung "Bot hat am Level
// schaltet dasselbe und ist auch sichtbar, wenn KEINE Position offen ist. // geschlossen" — es gibt keinen Auto-Close am Level mehr.
// `srAuto` wird weiter gebraucht (Knopf `btn-sr`, Hinweistexte).
const srMin = d.sr_close_min_gain || 0;
const smi = $("srmin-input");
if (smi && document.activeElement !== smi) {
smi.value = srMin > 0 ? String(srMin) : "";
}
// Einblendung: der Bot hat am S/R automatisch geschlossen (Zähler stieg)
const src = d.sr_close_count;
if (src != null) {
if (_lastSrCloseCount != null && src > _lastSrCloseCount) {
toast("🎯 Bot hat am Level geschlossen (schwacher Anlauf, Gewinn gesichert)", true);
}
_lastSrCloseCount = src;
}
// ── Letzte Trades ─────────────────────── // ── Letzte Trades ───────────────────────
// 5 → 10 (User-Wunsch 2026-08-04). Das Backend liefert bereits 10 // 5 → 10 (User-Wunsch 2026-08-04). Das Backend liefert bereits 10
@@ -2116,37 +2102,13 @@ _updateMuteBtn();
// Toggle-Buttons (AUTO/TRAIL/S-R-Close) — ohne Bestätigung // Toggle-Buttons (AUTO/TRAIL/S-R-Close) — ohne Bestätigung
$("btn-cb").onclick = () => toggle("api/circuit", "Circuit Breaker"); $("btn-cb").onclick = () => toggle("api/circuit", "Circuit Breaker");
$("btn-sr").onclick = () => toggle("api/srclose", "S/R-Auto-Close");
$("btn-m15").onclick = () => toggle("api/autom15", "M15-Auto-Entry (gemessen negativ!)"); $("btn-m15").onclick = () => toggle("api/autom15", "M15-Auto-Entry (gemessen negativ!)");
// (🎯 SIG entfernt 2026-08-12 — s. renderOrderButtons oben) // (🎯 SIG entfernt 2026-08-12 — s. renderOrderButtons oben)
$("btn-trail").onclick = () => toggle("api/trail", "Trailing"); $("btn-trail").onclick = () => toggle("api/trail", "Trailing");
// Mindestgewinn für den S/R-Auto-Close (0/leer = aus) // ⚠ Der Mindestgewinn-Sender ist am 2026-08-21 mit dem S/R-Auto-Close
async function setSrMin() { // entfallen — die Schwelle gab es nur fuer ihn (Endpoint, Feld und
const raw = $("srmin-input").value; // Backend-Zustand sind ebenfalls weg).
const v = raw === "" ? 0 : parseFloat(raw);
if (isNaN(v) || v < 0) { toast("Bitte Betrag ≥ 0 eingeben (0 = aus)", false); return; }
const tok = await ensureToken();
if (tok === null) return;
$("srmin-input").blur();
try {
const r = await fetch("api/srclose_min", { method: "POST",
headers: { "X-Auth-Token": tok, "Content-Type": "application/json" },
body: JSON.stringify({ value: v }) });
if (r.status === 401) { clearToken(); throw new Error("401"); }
toast(v > 0 ? `Mindestgewinn +${v} gesetzt` : "Mindestgewinn aus", true);
} catch (e) {
toast(e.message === "401" ? "Token abgelaufen — neu eingeben" : "Netzwerkfehler", false);
}
}
// Feld sitzt in der Button-Zeile: setzt beim Verlassen/Enter automatisch
$("srmin-input").addEventListener("change", setSrMin);
// Enter NUR blurren → das change-Event feuert setSrMin genau EINMAL (und nur, wenn
// der Wert wirklich geändert wurde). Vorher: keydown + change = doppelter Send,
// und Enter auf leerem Feld schickte ungewollt 0 (Fix 2026-07-22).
$("srmin-input").addEventListener("keydown", e => {
if (e.key === "Enter") { e.preventDefault(); e.target.blur(); }
});
// Einsatz-Felder ENTFERNT (User 2026-08-19). Das BACKEND bleibt vollstaendig: // Einsatz-Felder ENTFERNT (User 2026-08-19). Das BACKEND bleibt vollstaendig:
// beide Endpunkte sind weiter erreichbar, die Werte leben in // beide Endpunkte sind weiter erreichbar, die Werte leben in
@@ -2519,7 +2481,7 @@ async function closeSlot(slot, label, pnl) {
// und der Browser zeigte Altes. Statt das jedes Mal von Hand zu diagnostizieren, // und der Browser zeigte Altes. Statt das jedes Mal von Hand zu diagnostizieren,
// sagt die Seite es jetzt selbst. // sagt die Seite es jetzt selbst.
// ⚠ Bewusst SICHTBAR statt nur Konsole: die Konsole sieht am Handy niemand. // ⚠ Bewusst SICHTBAR statt nur Konsole: die Konsole sieht am Handy niemand.
const _JS_V = "208"; const _JS_V = "210";
{ {
const htmlV = document.body && document.body.dataset const htmlV = document.body && document.body.dataset
? document.body.dataset.htmlV : null; ? document.body.dataset.htmlV : null;
+19 -7
View File
@@ -12,9 +12,9 @@
⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard ⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard
und muss offline funktionieren. --> und muss offline funktionieren. -->
<link rel="stylesheet" href="pico.min.css?v=1"> <link rel="stylesheet" href="pico.min.css?v=1">
<link rel="stylesheet" href="style.css?v=208"> <link rel="stylesheet" href="style.css?v=210">
</head> </head>
<body data-html-v="208"> <body data-html-v="210">
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
<nav id="tabs"> <nav id="tabs">
<button class="tab active" data-view="dash">Dashboard</button> <button class="tab active" data-view="dash">Dashboard</button>
@@ -43,6 +43,12 @@
</div> </div>
<div class="price-row"> <div class="price-row">
<div class="px"><label title="Kontostand ohne die offene Position.">BALANCE</label><span id="balance"></span></div> <div class="px"><label title="Kontostand ohne die offene Position.">BALANCE</label><span id="balance"></span></div>
<!-- ⚠⚠ Farb-Logik wie vom Nutzer vorgegeben: GRUEN = Breaker AUS
(inaktiv, nichts begrenzt), ROT = Breaker AN (aktiv, ein
Tagesverlust-Limit ist scharf). Das ist bewusst NICHT die
uebliche "gruen = geschuetzt"-Lesart — hier signalisiert Rot,
dass eine Automatik eingreifen kann. -->
<button class="cb-dot" id="btn-cb" title="Circuit Breaker (Tagesverlust-Stopp) an/aus">CB</button>
<div class="px"><label title="Realisiertes Ergebnis von heute. Brutto — die Quellensteuer wird größtenteils erstattet und ist hier nicht abgezogen.">TAG P/L</label><span id="day-pl"></span></div> <div class="px"><label title="Realisiertes Ergebnis von heute. Brutto — die Quellensteuer wird größtenteils erstattet und ist hier nicht abgezogen.">TAG P/L</label><span id="day-pl"></span></div>
<div class="px"><label title="Kontostand einschließlich der offenen Position.">EQUITY</label><span id="equity"></span></div> <div class="px"><label title="Kontostand einschließlich der offenen Position.">EQUITY</label><span id="equity"></span></div>
<div class="px"><label id="hdr-price-lbl" title="Der Kurs, zu dem tatsächlich gehandelt wird. Steht er länger als 3 Minuten still, gilt der Markt als geschlossen und das Label zeigt ZU.">PEPPERSTONE KURS</label><span id="hdr-price"></span></div> <div class="px"><label id="hdr-price-lbl" title="Der Kurs, zu dem tatsächlich gehandelt wird. Steht er länger als 3 Minuten still, gilt der Markt als geschlossen und das Label zeigt ZU.">PEPPERSTONE KURS</label><span id="hdr-price"></span></div>
@@ -98,8 +104,8 @@
Broker-TP NICHT kennt. --> Broker-TP NICHT kennt. -->
<span class="tb-field gain"><label title="TP3 — die Obergrenze des 80-%-Kegels auf 120 Minuten (core/cone.py). Out-of-sample geprüft: die 80-%-Bänder treffen real 77 %. ⚠ Das ist eine Aussage über die REICHWEITE, nicht über die Richtung — weiter kommt der Kurs in dieser Zeit selten, nicht: er kommt dorthin.">TP3 · Kegel 120</label> <span class="tb-field gain"><label title="TP3 — die Obergrenze des 80-%-Kegels auf 120 Minuten (core/cone.py). Out-of-sample geprüft: die 80-%-Bänder treffen real 77 %. ⚠ Das ist eine Aussage über die REICHWEITE, nicht über die Richtung — weiter kommt der Kurs in dieser Zeit selten, nicht: er kommt dorthin.">TP3 · Kegel 120</label>
<span class="tb-val" id="tb-tp3"></span></span> <span class="tb-val" id="tb-tp3"></span></span>
<span class="tb-field gain"><label title="Mindestgewinn in Euro, unter dem der Auto-Close am Level NICHT schließt. Leer oder 0 = aus.">Gewinn-Close</label> <!-- ⚠ "Gewinn-Close" entfernt am 2026-08-21 mit dem S/R-Auto-Close:
<input id="srmin-input" type="number" inputmode="decimal" step="1" placeholder="€"></span> die Schwelle gab es NUR fuer ihn. -->
<span class="tb-field stop"><label title="Harte Verlustgrenze in Euro — der Server schließt sofort, wenn sie erreicht ist. Wirkt auch bei gesperrtem Handy. ⚠ Ein gesetzter Wert wird auf den nächsten Trade übernommen, bis er gelöscht wird.">Notfall-Close</label> <span class="tb-field stop"><label title="Harte Verlustgrenze in Euro — der Server schließt sofort, wenn sie erreicht ist. Wirkt auch bei gesperrtem Handy. ⚠ Ein gesetzter Wert wird auf den nächsten Trade übernommen, bis er gelöscht wird.">Notfall-Close</label>
<input id="emg-input" type="number" inputmode="decimal" step="1" placeholder="€"></span> <input id="emg-input" type="number" inputmode="decimal" step="1" placeholder="€"></span>
</div> </div>
@@ -421,14 +427,20 @@
Close-Alarm (nur Slot 1, dort entsteht der Anlass). --> Close-Alarm (nur Slot 1, dort entsteht der Anlass). -->
<!-- BRK-Schalter ENTFERNT, dafuer der Circuit-Breaker (User 2026-08-19). <!-- BRK-Schalter ENTFERNT, dafuer der Circuit-Breaker (User 2026-08-19).
Der Auto-Squeeze-Zustand bleibt in der Breakout-Karte sichtbar. --> Der Auto-Squeeze-Zustand bleibt in der Breakout-Karte sichtbar. -->
<button class="btn tog" id="btn-cb" title="Circuit Breaker (Tagesverlust-Stopp) an/aus">🛑 CIRCUIT BREAKER</button> <!-- ⚠ Der CIRCUIT-BREAKER-Knopf sitzt seit dem 2026-08-21 als runder
"CB" rechts neben BALANCE (User-Wunsch). VERSCHOBEN, nicht kopiert:
zwei Elemente mit derselben ID waeren ein stiller Fehler, weil
`getElementById` nur das erste findet. -->
<!-- S/R-Auto-Close auch in der Aktionsleiste (User 2026-08-19). Den <!-- S/R-Auto-Close auch in der Aktionsleiste (User 2026-08-19). Den
Schalter gab es nur in der Trade-Leiste -- die ist aber NUR bei Schalter gab es nur in der Trade-Leiste -- die ist aber NUR bei
offener Position sichtbar, also genau dann nicht, wenn man ihn vor offener Position sichtbar, also genau dann nicht, wenn man ihn vor
einem Trade setzen will. Der alte Knopf bleibt; beide lesen einem Trade setzen will. Der alte Knopf bleibt; beide lesen
`auto_sr_close` aus demselben Snapshot und koennen nicht `auto_sr_close` aus demselben Snapshot und koennen nicht
auseinanderlaufen. --> auseinanderlaufen. -->
<button class="btn tog" id="btn-sr" title="Auto-Close am S/R-Level an/aus (P(break)-gegatet: schliesst nur, wenn das Level wahrscheinlich haelt)">S/R</button> <!-- ⚠ Knopf S/R entfernt am 2026-08-21 (User-Vorgabe). Der Auto-Close am
Level ist samt Endpoint und Backend-Logik weg; der HINWEIS in der
Meldungen-Karte, die P(break)-Chartlinien und das
`pbreak_predictions`-Tracking bleiben. -->
<!-- M15-Karten-Auto-Entry (User-Test 2026-08-19). ⚠ GEGEN DIE MESSUNG: <!-- M15-Karten-Auto-Entry (User-Test 2026-08-19). ⚠ GEGEN DIE MESSUNG:
backtest_m15_auto.py -> Lesart beidhaelftig negativ (OeR -0,162/ backtest_m15_auto.py -> Lesart beidhaelftig negativ (OeR -0,162/
-0,094, PF 0,72/0,84), KI ohne Null, schlechter als die -0,094, PF 0,72/0,84), KI ohne Null, schlechter als die
@@ -473,6 +485,6 @@
</div> </div>
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<script src="app.js?v=208"></script> <script src="app.js?v=210"></script>
</body> </body>
</html> </html>
+15
View File
@@ -757,3 +757,18 @@ main{padding-bottom:4.1111rem} /* Platz für die fixe Leiste */
border-radius:var(--r-sm); color:var(--bright); cursor:pointer; border-radius:var(--r-sm); color:var(--bright); cursor:pointer;
font-size:1.1111rem; line-height:1; padding:0.3333rem 0.5556rem} font-size:1.1111rem; line-height:1; padding:0.3333rem 0.5556rem}
.pt-img[data-bild]{cursor:zoom-in} .pt-img[data-bild]{cursor:zoom-in}
/* ── Runder Circuit-Breaker-Knopf neben BALANCE (User 2026-08-21) ──────────
⚠ Farb-Logik ist die VORGABE des Nutzers: gruen = Breaker AUS (inaktiv),
rot = Breaker AN (aktiv/scharf). Bewusst nicht die uebliche
"gruen = geschuetzt"-Lesart — hier heisst Rot: eine Automatik kann eingreifen. */
.cb-dot{
width:2.0556rem; height:2.0556rem; border-radius:50%; flex:0 0 auto;
border:1px solid var(--border); cursor:pointer; align-self:center;
font:700 0.6667rem/1 var(--mono); letter-spacing:.5px;
display:flex; align-items:center; justify-content:center;
background:var(--surface-2); color:var(--dim); transition:filter .15s;
}
.cb-dot:active{filter:brightness(1.3)}
.cb-dot.inaktiv{background:#0d2a16; border-color:#1a4731; color:#3fb950}
.cb-dot.aktiv{background:#2d0d0d; border-color:#5c1a1a; color:#f85149}