diff --git a/core/engine.py b/core/engine.py index 9498c64..4c34476 100644 --- a/core/engine.py +++ b/core/engine.py @@ -183,7 +183,9 @@ def _p_break(mom6: float, mom3: float, wt: float, dist: float) -> float: # EINE AUSNAHME mit Bypass: " 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",) +# ⚠ User-Vorgabe 2026-08-19: ALLE Trade-Closes UND -Opens melden. +# `close_notify_min_eur` steht dafür auf 0 (= jeder Close, keine Schwelle). +_TG_ERLAUBT = ("Trade geschlossen", "Trade eroeffnet") def send_telegram(text, *a, **kw): @@ -862,6 +864,8 @@ class TradingEngine: # ⚠ NACH `_check_auto_close`: erst dort wird der Flat-Übergang # erkannt und die Benachrichtigung vorgemerkt. self._check_close_notify() # Telegram bei jedem Close ab Schwelle + self._check_open_notify() # Telegram bei jeder Eröffnung + 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() @@ -945,6 +949,85 @@ class TradingEngine: return False return self._circuit_tripped_date == datetime.now(_BERLIN).date() + def _check_open_notify(self): + """Telegram bei JEDER Eröffnung — beide Slots (User-Vorgabe 2026-08-19). + + ⚠ Bewusst über BEIDE Trader: ein autonomer BRK-Entry soll nicht + unbemerkt bleiben — genau der Fall, für den der zweite Slot gebaut + wurde. Die Meldung nennt deshalb den Slot mit. + ⚠ 1× je Ticket (`_open_notify_gemeldet`); ohne den Merker feuerte sie im + 1-s-Takt weiter, solange die Position offen ist. + ⚠ In der Startup-Schonfrist wird nur GEMERKT, nicht gemeldet: nach einem + Neustart erkennt der Trader eine BESTEHENDE Position wieder, und das ist + keine Eröffnung. + """ + if not hasattr(self, "_open_notify_gemeldet"): + self._open_notify_gemeldet = set() + for tr, quelle in ((self.trader, "Slot 1"), (self.trader_brk, "BRK")): + try: + ps = tr.snapshot() or {} + tk = ps.get("ticket") + if not tk or tk in self._open_notify_gemeldet: + continue + self._open_notify_gemeldet.add(tk) + if self._in_startup_grace(): + continue + lg = ps.get("order_type") == 0 + zeilen = [ + f"📈 Trade eroeffnet ({quelle})", + f"{self.data.symbol or '?'} " + f"{'▲ LONG' if lg else '▼ SHORT'} " + f"{ps.get('lots')} Lots @ {ps.get('entry_price')}", + f"SL {ps.get('sl') or '—'} · TP {ps.get('tp') or '—'}", + ] + send_telegram("\n".join(zeilen)) + except Exception as e: + log.debug(f"_check_open_notify: {e}") + if len(self._open_notify_gemeldet) > 200: + self._open_notify_gemeldet = set(list(self._open_notify_gemeldet)[-50:]) + + # ⚠⚠ REICHWEITEN-WÄCHTER (2026-08-19). Vier Schutz-Checks kennen bislang NUR + # den ersten Slot: `_check_auto_close` (Notfall-Stop, hängt an 4 + # Ticket-Variablen), `_check_sr_close`, `_check_adverse15`, + # `_check_flip_close`. Solange sie AUS sind, ist das folgenlos — der + # BRK-Slot hat Broker-SL, `trail_brk` und den Circuit-Breaker. + # ⚠ Wird einer davon wieder eingeschaltet, WÄHREND eine BRK-Position + # läuft, gilt er stillschweigend nur für Slot 1: die zweite Position liefe + # ohne ihn, und im Dashboard sähe alles normal aus. Dieselbe Klasse wie + # Deployment-Drift Fall 8 — nicht die Strategie driftet, sondern ihre + # REICHWEITE. Bis die vier slot-fähig sind, macht dieser Wächter das Loch + # LAUT statt still (1× je Ticket, kein Dauerlärm). + def _check_slot_reichweite(self): + try: + tk = self.trader_brk.ticket + if not tk: + self._reichweite_gemeldet = None + return None + offen = [] + if ((getattr(self, "_emergency_margin_pct", 0) or 0) > 0 + 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): + offen.append("Flip-Close") + if not offen: + self._reichweite_gemeldet = None + return None + if getattr(self, "_reichweite_gemeldet", None) != tk: + self._reichweite_gemeldet = tk + log.warning("⚠ REICHWEITE: %s aktiv, gilt aber NUR für Slot 1 — " + "die BRK-Position T=%s läuft ohne (Broker-SL, " + "Trailing und Circuit-Breaker greifen weiter).", + ", ".join(offen), tk) + return offen + except Exception as e: + log.debug(f"_check_slot_reichweite: {e}") + return None + 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,