BRK bekommt einen EIGENEN Positions-Slot - unabhaengig von manuell und M15

User-Vorgabe: "der brk trade soll komplett unabhaengig von m15 oder manuellen
trades laufen". Entwurf: docs/brk-eigener-slot.md

DER BEFUND, DER DEN UMBAU KLEIN MACHT: TrailingManager.__init__ nimmt bereits
SEINEN Trader entgegen und liest ausschliesslich ueber dessen snapshot() - es
haengt also nicht an einer globalen Position. Damit entfaellt der
29-Stellen-Umbau in engine.py. Stattdessen:

    engine.trader      - TradeManager()                 - trail      - manuell + M15
    engine.trader_brk  - TradeManager(nur_ticket=True)  - trail_brk  - nur BRK

Der validierte Pfad bleibt damit vollstaendig unangetastet - kein
Regressionsrisiko auf der gemessen besseren Population (+1,56 gegen -4,71/Lot).

DIE ZWEI KRITISCHEN STELLEN, beide gebaut:
(1) ADOPTION. TradeManager.refresh() greift per Default jede Position auf dem
    Symbol (gewollt: ein von Hand eroeffneter Trade bekommt so den Schutz-Stack).
    Mit zwei Managern wuerden sich BEIDE dieselbe Position schnappen. Neu:
    `nur_ticket=True` sucht sich nichts, und der adoptierende Manager bekommt
    ueber `fremde_tickets` eine Sperrliste (BRK-Ticket + liegende Pendings).
(2) DER FILL. Eine ruhende Order fuellt IM BROKER; der ticket-gebundene Slot
    kann sie nicht finden. Neu `TradeManager.bind(ticket, sym)`, aufgerufen aus
    _check_pending_fill - es sieht direkt am Broker nach, welches der liegenden
    Tickets zu einer Position geworden ist.

WEITER GEAENDERT: _check_auto_squeeze und _manage_squeeze_pending lesen den
EIGENEN Slot (eine manuelle oder M15-Position storniert die Pendings nicht mehr
- real am 19.08. waren es 5 Fenster mit zusammen ~2 min am Markt, 3 davon vom
M15-Trader); trail_brk wird in _price_loop mit Preisen versorgt (ohne das liefe
BRK in der gemessen DURCHGEFALLENEN SL-only-Variante); Snapshot-Feld
`position_brk` macht den zweiten Slot sichtbar - ohne das waere es
Deployment-Drift Fall 8 in neuer Form (nicht die Strategie driftet, sondern
ihre Beobachtbarkeit).

TESTS: 91 gruen. tests/test_pending_fill.py auf den BRK-Slot umgestellt - die
Aussagen bleiben unveraendert (fremde Quelle wird NICHT getaggt, derselbe Fill
zaehlt nur einmal), nur der Slot ist ein anderer. Die Tests wurden NICHT
abgeschwaecht; sie sichern weiter Deployment-Drift Fall 8.

LIVE VERIFIZIERT: Slot 1 fuehrt die offene Position (T=50349215) unveraendert
weiter, Slot 2 ist leer und wartet auf einen Squeeze. Deploy ueber
tools/deploy.py --feld position_brk, alle 5 Schritte.

⚠ OFFEN und bewusst NICHT in diesem Zug: Circuit-Breaker und Tages-P&L summieren
noch nicht BEIDE Slots, und die Dashboard-Zeile fuer den zweiten Slot fehlt. Mit
zwei Positionen sind bei 40 % je Position 80 % der Margin gebunden - der Breaker
(aktuell AUS) waere dann keine Kuer mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-19 20:03:45 +02:00
co-authored by Claude Opus 5
parent 815b6a77b8
commit 9b5b20e713
4 changed files with 131 additions and 11 deletions
+58 -3
View File
@@ -253,6 +253,25 @@ class TradingEngine:
self.trader = TradeManager()
self.trader.history = self.history
self.trail = TrailingManager(self.trader)
# ⚠⚠ ZWEITER, STRIKT TICKET-GEBUNDENER SLOT NUR FUER BRK
# (User-Vorgabe 2026-08-19: "der brk trade soll komplett unabhaengig
# von m15 oder manuellen trades laufen").
# Entwurf: docs/brk-eigener-slot.md
# Der Trick ist, dass `TrailingManager` bereits SEINEN Trader nimmt und
# nur ueber dessen `snapshot()` liest — es haengt also nicht an einer
# globalen Position. Damit entfaellt der 29-Stellen-Umbau: BRK bekommt
# einen eigenen Manager samt vollem Schutz-Stack, und der validierte
# Pfad (manuell + M15) bleibt unangetastet.
# ⚠ `nur_ticket=True` ist zwingend: ohne das wuerden sich BEIDE Manager
# dieselbe Position greifen (der Default adoptiert per Symbol).
self.trader_brk = TradeManager(nur_ticket=True)
self.trail_brk = TrailingManager(self.trader_brk)
# ⚠ Der adoptierende Manager darf den BRK-Slot und dessen noch offene
# Pending-Tickets NICHT greifen — sonst verwalten beide dieselbe
# Position (Trailing zweimal, Notfall-Stop am falschen Trade).
self.trader.fremde_tickets = lambda: (
{self.trader_brk.ticket} if self.trader_brk.ticket else set()
) | set(self._pending_tickets or {})
self.trail.set_notify(self._on_trail_event)
# (Dry-Run-Auto-Trader entfernt 2026-07-23 — war Verlierer im Backtest,
# UI-Toggle längst weg. `min_conf`-Anzeigekonstante bleibt, s. unten.)
@@ -755,6 +774,10 @@ class TradingEngine:
if s.get("bid") and s.get("ask") and self.data.symbol:
try:
self.trail.on_price(self.data.symbol, s["bid"], s["ask"])
# ⚠ Ohne diese Zeile liefe der BRK-Trade in der gemessen
# DURCHGEFALLENEN SL-only-Variante (backtest_brk_slonly.py:
# alle zehn KI enthalten die Null).
self.trail_brk.on_price(self.data.symbol, s["bid"], s["ask"])
except Exception as te:
log.warning(f"trail.on_price: {te}")
except Exception as e:
@@ -828,6 +851,8 @@ class TradingEngine:
try:
if self.data.symbol:
self.trader.refresh(self.data.symbol)
# BRK-Slot mitfuehren (strikt ticket-gebunden, adoptiert nichts)
self.trader_brk.refresh(self.data.symbol)
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
self._check_auto_close()
# ⚠ NACH `_check_auto_close`: erst dort wird der Flat-Übergang
@@ -1321,7 +1346,23 @@ class TradingEngine:
try:
if not self._pending_tickets:
return
ps = self.trader.snapshot()
# ⚠⚠ Der BRK-Slot ist STRIKT ticket-gebunden und adoptiert nichts —
# er kann den Fill also nicht selbst finden. Deshalb hier direkt
# am Broker nachsehen, welches der liegenden Tickets zu einer
# Position geworden ist, und es dem Slot ZUWEISEN.
# (Bei einem Pending-Fill ist die Positions-Nummer identisch mit
# der Order-Nummer — an vier echten Datensaetzen verifiziert.)
if self.trader_brk.ticket is None:
try:
with mt5_lock(timeout=5) as _got:
offen = {int(p.ticket) for p in (mt5.positions_get() or ())} if _got else set()
except Exception:
offen = set()
for _tk in list(self._pending_tickets):
if int(_tk) in offen and self.trader_brk.bind(int(_tk), self.data.symbol):
log.info(f"🧷 BRK-Slot uebernimmt Pending-Fill T={_tk}")
break
ps = self.trader_brk.snapshot()
tk = ps.get("ticket")
if not tk or tk in self._pending_tagged:
return
@@ -1427,7 +1468,10 @@ class TradingEngine:
if not sym:
return
try:
ps = self.trader.snapshot()
# ⚠ NUR der eigene Slot entscheidet — vorher stornierte JEDE
# offene Position die Pendings (real am 19.08.: 5 Fenster,
# zusammen ~2 min am Markt, 3 davon vom M15-Trader).
ps = self.trader_brk.snapshot()
atr = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0
ziel, quelle = self._pending_ziel()
# Orders sollen NUR liegen, wenn zusätzlich: flat · Schonfrist vorbei.
@@ -1510,7 +1554,9 @@ class TradingEngine:
# Nacht-Fenster, News-Blackout, Circuit-Breaker und der An/Aus-Schalter
# sitzen jetzt in `_squeeze_guard()` (oben geprüft) — EINE Quelle für
# Pending- und Market-Weg, s. dort.
ps = self.trader.snapshot()
# ⚠ EIGENER Slot: eine manuelle oder M15-Position blockt BRK
# nicht mehr (User-Vorgabe 2026-08-19).
ps = self.trader_brk.snapshot()
ticket = ps.get("ticket")
if ticket:
pos_dir = "LONG" if ps.get("order_type") == 0 else "SHORT"
@@ -4541,6 +4587,15 @@ class TradingEngine:
# Einsatz als % der freien Margin (`margin_buffer_pct`). Greift, solange
# kein fester Betrag gesetzt ist — der hat Vorrang (s. set_margin_pct).
"margin_pct": round(_cfg_margin_buffer() * 100.0, 1),
# ⚠⚠ ZWEITER Slot sichtbar machen. Ohne dieses Feld liefe eine
# offene BRK-Position unbeobachtet — genau Deployment-Drift Fall 8
# (dort driftete nicht die Strategie, sondern ihre Beobachtbarkeit).
"position_brk": (lambda ps: {
"ticket": ps.get("ticket"), "lots": ps.get("lots"),
"entry": ps.get("entry_price"), "pnl": ps.get("pnl"),
"sl": ps.get("sl"), "tp": ps.get("tp"),
"trail": bool(getattr(self.trail_brk, "enabled", False)),
} if ps.get("ticket") else None)(self.trader_brk.snapshot()),
# Einsatz je autonomem Pfad; 0 = der globale `margin_pct` gilt.
"margin_brk": getattr(self, "_margin_brk", 0.0),
"margin_m15": getattr(self, "_margin_m15", 0.0),