From 5e476025d76eb566a1807fa7aede9dd0e228b522 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 20 Aug 2026 08:54:20 +0200 Subject: [PATCH] Dritter Slot-Durchgang: TRAIL-Knopf und Wiedereinstiegs-Sperre waren blind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-Auftrag (dritte Runde). Diesmal die Bereiche ausserhalb der `self.trader.`-Zugriffe geprueft. ZWEI neue Funde, beide behoben: ⑤ toggle_trail() schaltete NUR `self.trail`. Hielt allein der BRK-Slot eine Position, liess sich dessen Trailing per Dashboard weder ein- noch ausschalten. ⚠ Das ist keine Kosmetik: 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 %). Neu: haelt NUR BRK etwas, wird dessen Trailing geschaltet; sonst (auch bei zwei offenen Positionen) das von Slot 1 - die Position, die der Nutzer selbst geoeffnet hat. ⑥ Die Wiedereinstiegs-Sperre sah den BRK-Slot NICHT. `_check_auto_close` setzt `_pos_close_ts`/`_pos_close_dir` aus `self.trader.snapshot()`; schloss eine BRK-Position, blieb die Sperre unarmiert und der Squeeze durfte sofort in dieselbe Richtung neu eroeffnen. Genau die Open->Instant-Close-Kaskade, gegen die _SQUEEZE_REENTRY_COOLDOWN_S am 27.07. gebaut wurde. Neu `_check_brk_close_cooldown` im _pos_loop. ⚠ Bewusst DIESELBEN Felder wie Slot 1: die Sperre ist eine Aussage ueber den MARKT ("in diese Richtung gerade nicht"), nicht ueber einen Slot. GEPRUEFT UND SAUBER in diesem Durchgang: - core/trailing.py hat KEINE Klassen-Variablen und keine veraenderlichen Modul-Globalen -> die zwei TrailingManager-Instanzen sind vollstaendig unabhaengig. Das war die groesste offene Sorge. - _check_close_notify arbeitet ueber history.trade_by_ticket, also DB-basiert und slot-unabhaengig. ⚠ WEITER OFFEN, bewusst nicht angefasst: `set_sltp()` wirkt nur auf Slot 1 (die SL/TP-Felder koennen eine BRK-Position nicht anpassen) - das ist eine UI-Entscheidung, welches Feld zu welchem Slot gehoert, und braucht eine Vorgabe statt eines stillen Fixes. Ebenso `_check_auto_m15`, das `self.trader.open_long` VOR dem `_open`-Aufruf bindet und damit `_slot()` umgeht (funktioniert derzeit nur, weil beide dasselbe liefern). 91 Tests gruen, Pipeline inkl. der neuen Stufe E gruen, Deploy verifiziert. Co-Authored-By: Claude Opus 5 --- core/engine.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) 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