diff --git a/core/engine.py b/core/engine.py index d5ee9e3..2d9fa46 100644 --- a/core/engine.py +++ b/core/engine.py @@ -868,6 +868,7 @@ class TradingEngine: # BRK-Slot mitfuehren (strikt ticket-gebunden, adoptiert nichts) self.trader_brk.refresh(self.data.symbol) self._rebind_brk() # nach Neustart zurueckbinden + self._check_brk_close_cooldown() # Sperre auch bei BRK-Close self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst) self._check_auto_close() # ⚠ NACH `_check_auto_close`: erst dort wird der Flat-Übergang @@ -994,6 +995,34 @@ class TradingEngine: log.warning(f"⚠ DB-Nachtrag T={tk} nach {n} Versuchen aufgegeben " f"— der Trade bleibt setup=NULL und fehlt in B4/B5.") + def _check_brk_close_cooldown(self): + """Wiedereinstiegs-Sperre auch für BRK-Closes armieren (Fix 2026-08-20). + + ⚠⚠ `_check_auto_close` setzt `_pos_close_ts`/`_pos_close_dir` aus + `self.trader.snapshot()` — es sieht den BRK-Slot also NICHT. Schloss eine + BRK-Position, blieb die Sperre unarmiert, und der Squeeze durfte sofort + in dieselbe Richtung neu eröffnen. Genau die Open→Instant-Close-Kaskade, + gegen die `_SQUEEZE_REENTRY_COOLDOWN_S` am 27.07. gebaut wurde — damals + killte ein zu enger Notfall-Stop den Entry nach 16 s, der Squeeze war + noch `active`, und es ging sofort wieder los (doppelter Spread). + ⚠ Dieselben Felder wie Slot 1, bewusst: die Sperre ist eine Aussage über + den MARKT („in diese Richtung gerade nicht"), nicht über einen Slot. + """ + try: + tk = getattr(self.trader_brk, "ticket", None) + vorher = getattr(self, "_brk_letzte", None) + if tk: + self._brk_letzte = (tk, (self.trader_brk.snapshot() or {}).get("order_type")) + return + if vorher: + self._brk_letzte = None + self._pos_close_ts = time.time() + self._pos_close_dir = "LONG" if vorher[1] == 0 else "SHORT" + log.info("BRK-Slot geschlossen (T=%s) — Wiedereinstiegs-Sperre " + "%s armiert.", vorher[0], self._pos_close_dir) + except Exception as e: + log.debug(f"_check_brk_close_cooldown: {e}") + def _rebind_brk(self): """BRK-Position nach einem Neustart zurückbinden (Fix 2026-08-20). @@ -1198,6 +1227,12 @@ class TradingEngine: self._emergency_loss = None self._takeprofit = None return + # ⚠⚠ Der BRK-Slot wird hier NICHT erfasst: `ps` kommt aus + # `self.trader.snapshot()`. Schliesst eine BRK-Position, armiert die + # Wiedereinstiegs-Sperre also NICHT — und der Squeeze duerfte sofort + # in dieselbe Richtung neu eroeffnen. Genau die Open->Instant-Close- + # Kaskade, gegen die die Sperre am 27.07. gebaut wurde. + # `_check_brk_close_cooldown` (im `_pos_loop`) schliesst das. self._open_pos_dir = "LONG" if ps.get("order_type") == 0 else "SHORT" if ticket != self._last_pos_ticket: # neue/wiedererkannte Position self._last_pos_ticket = ticket @@ -2950,10 +2985,23 @@ class TradingEngine: return "; ".join(fehler) if fehler else None def toggle_trail(self) -> bool: + """TRAIL-Knopf — trifft den Slot, der tatsaechlich eine Position haelt. + + ⚠⚠ Bis 2026-08-20 schaltete er NUR `self.trail`. Hielt allein der + BRK-Slot eine Position, liess sich dessen Trailing per Dashboard weder + ein- noch ausschalten — und ohne `trail_brk` laeuft ein BRK-Trade in der + gemessen DURCHGEFALLENEN SL-only-Variante (`backtest_brk_slonly.py`: + alle zehn KI enthalten die Null, Trefferquote faellt 40 -> 21 %). + ⚠ Haelt NUR der BRK-Slot etwas, wird dessen Trailing geschaltet; sonst + (auch bei zwei offenen Positionen) das von Slot 1 — das ist die + Position, die der Nutzer selbst geoeffnet hat. + """ sym = self.data.symbol - if sym: - return self.trail.toggle(sym) - return self.trail.enabled + if not sym: + return self.trail.enabled + nur_brk = (bool(getattr(self.trader_brk, "ticket", None)) + and not (self.trader.snapshot() or {}).get("ticket")) + return (self.trail_brk if nur_brk else self.trail).toggle(sym) def _sr_levels(self, market: dict) -> dict: """Nächste Widerstandslinie ÜBER Preis (Short) und Unterstützung UNTER