Dritter Slot-Durchgang: TRAIL-Knopf und Wiedereinstiegs-Sperre waren blind

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 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-20 08:54:20 +02:00
co-authored by Claude Opus 5
parent 2b498fc20e
commit 5e476025d7
+50 -2
View File
@@ -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 OpenInstant-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)
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