Files
Axel HocksandClaude Opus 5 0966caf1b8 G/V-Trennung geprueft: Adoptions-Sperre riss bei JEDEM Neustart auf
User: "ueberpruefe ob wirklich die G/V und G/V BRK Anzeigen stimmen, besonders
bei gleichzeitig laufenden Trades. Die Anzeige darf jeweils nur den eigenen
Slot anzeigen."

FRONTEND SAUBER: #hdr-pnl liest nur d.position, #hdr-pnl-brk nur
d.position_brk - keine Summe, kein Fallback. Aber das ist die falsche Stelle
zum Suchen: stehen im Backend beide Felder auf derselben Position, kann keine
Anzeige das mehr richten. Geprueft wurde deshalb die AUSWAHL in
TradeManager._refresh_locked.

⚠⚠ FUND 1 - die Sperrliste kannte nur zwei von DREI Quellen eines BRK-Tickets:
  trader_brk.ticket     nur im Speicher
  _pending_tickets      nur im Speicher
  _brk_restore          FEHLTE - und nur das ueberlebt den Neustart
Nach einem Neustart sind die ersten beiden leer, und die Reihenfolge im
_pos_loop besiegelt es: trader.refresh() laeuft DREI Zeilen vor _rebind_brk().
Slot 1 greift die BRK-Position also, bevor sie zurueckgebunden werden kann.
_rebind_brk erkennt das zwar und bricht ab - damit ist der Schaden aber nur
festgestellt, nicht behoben: die BRK-Position haengt am falschen Slot, ihr G/V
steht links statt rechts, und "G/V BRK" zeigt "—". Danach sieht alles normal
aus, was es besonders schwer sichtbar macht.
Behoben ueber getattr(..., None), also unabhaengig davon, WANN das Attribut
gesetzt wird - genau die Reihenfolge-Falle, die in zwei Tagen zweimal
zugeschlagen hat.

 FUND 2 - zwei verschiedene Rechnungen nebeneinander. Bei geschlossenem
Markt zeigt Slot 1 seit dem 01.08. eine HL-Schaetzung (pnl_hl), der BRK-Slot
bekam die nie und zeigte den EINGEFRORENEN Broker-Wert, ohne dass man es der
Anzeige ansieht. Neu pnl_hl_brk, mit ≈ markiert. Geprueft statt angenommen,
dass _tick_size/_tick_value am BRK-Slot gesetzt sind.

 day_pl.open summiert korrekt BEIDE Slots - die Summe gehoert dorthin, die
Trennung in die beiden G/V-Felder.

BELEG - tests/test_slot_gv.py, 6 Tests auf dem ECHTEN TradeManager (Broker
gestubbt): zwei Positionen gleichzeitig je im eigenen Slot · Slot 1 adoptiert
die BRK-Position nicht · Pending-Fill wird nicht weggeschnappt · der BRK-Slot
adoptiert nie · plus die Regressionsprobe, die den alten Zustand festhaelt.
NEUE PIPELINE-STUFE E (tools/check_slots.py): die Sperrliste muss alle drei
Namen erwaehnen. Mutationsprobe: Merker entfernt -> Exit 1.
⚠⚠ Ehrlich zur Arbeitsteilung: der pytest belegt den MECHANISMUS, baut die
Lambda aber selbst nach und bleibt bei der Mutation gruen. Erst die statische
Stufe belegt, dass die ENGINE sie richtig verdrahtet. Keine der beiden allein
haette gereicht.

⚠⚠ NEBENBEFUND, korrigiert: mein Schreib-Helfer hat SECHS Dateien still von
LF auf CRLF gedreht (io.open(...,"w") uebersetzt auf Windows). Mein erster
Check mit `grep -c $'\r$'` meldete faelschlich 0 - erst die BYTE-Zaehlung zeigte
8959 CR in CLAUDE.md. Alle sechs auf LF zurueckgesetzt. core.autocrlf=true
haette es im Repo normalisiert, im Arbeitsbaum aber nicht.

Live verifiziert: Slot 1 T=50396824 (+10,34), BRK flat, day_pl.open 10,34 -
kein gemeinsames Ticket. Deploy mit --feld pnl_hl_brk, alle 5 Schritte gruen.
⚠ Noch nicht beobachtet: ein Neustart MIT offener BRK-Position - der Fall, den
der Fix adressiert.

v=201.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 13:07:16 +02:00

160 lines
6.1 KiB
Python

"""Zeigen G/V und G/V BRK wirklich NUR ihren eigenen Slot? (2026-08-20)
ANLASS (User): "ueberpruefe ob wirklich die G/V und G/V BRK Anzeigen stimmen,
besonders bei gleichzeitig laufenden Trades. Die Anzeige darf jeweils nur den
eigenen Slot anzeigen."
Geprueft wird die STELLE, an der die Trennung entstehen muss - die Auswahl der
Position in `TradeManager._refresh_locked`. Das Frontend liest nur `d.position`
bzw. `d.position_brk`; wenn schon dort dieselbe Position steht, kann keine
Anzeige das noch richten.
DIE GEFAEHRLICHE STELLE ist die ADOPTION: der Haupt-Trader greift jede Position
auf dem Symbol, damit ein von Hand eroeffneter Trade den Schutz-Stack bekommt.
Genau das darf er beim BRK-Ticket NICHT - dafuer gibt es `fremde_tickets`.
Diese Sperrliste wird hier in EXAKT der Form nachgebaut, in der die Engine sie
setzt (core/engine.py, `self.trader.fremde_tickets = ...`).
"""
import types
import pytest
from core import trader as trader_mod
from core.trader import TradeManager
class _Pos:
def __init__(self, ticket, lots=1.0, entry=80.0, pnl=0.0, magic=0):
self.ticket, self.volume, self.price_open = ticket, lots, entry
self.profit, self.magic = pnl, magic
self.type, self.symbol, self.time = 0, "SpotCrude", 1_700_000_000
self.sl, self.tp = 79.0, 82.0
@pytest.fixture
def welt(monkeypatch):
"""Stub-Broker mit einer frei setzbaren Positionsliste."""
offen = {}
def positions_get(**kw):
if "ticket" in kw:
p = offen.get(int(kw["ticket"]))
return (p,) if p else ()
return tuple(offen.values())
fake = types.SimpleNamespace(
positions_get=positions_get,
order_calc_margin=lambda *a, **k: 700.0,
order_send=lambda *a, **k: None,
ORDER_TYPE_BUY=0, ORDER_TYPE_SELL=1,
TRADE_ACTION_SLTP=6, TRADE_RETCODE_DONE=10009,
symbol_info_tick=lambda s: None,
symbol_info=lambda s: None,
)
monkeypatch.setattr(trader_mod, "mt5", fake)
monkeypatch.setattr(trader_mod, "MAGIC", 4711, raising=False)
class _Lock:
def __enter__(self): return True
def __exit__(self, *a): return False
monkeypatch.setattr(trader_mod, "mt5_lock", lambda *a, **k: _Lock())
return offen
def _slots(pending=None, brk_restore=None):
"""Beide Manager wie in `TradingEngine.__init__` verdrahtet."""
haupt, brk = TradeManager(), TradeManager(nur_ticket=True)
haupt._broker_offset_s = lambda sym=None: 0
brk._broker_offset_s = lambda sym=None: 0
pend = dict(pending or {})
haupt.fremde_tickets = lambda: (
({brk.ticket} if brk.ticket else set()) | set(pend)
| ({brk_restore} if brk_restore else set())
)
return haupt, brk, pend
def _gv(haupt, brk):
"""Genau das, was `snapshot()` ans Frontend gibt."""
a, b = haupt.snapshot(), brk.snapshot()
return (a.get("ticket"), a.get("pnl")), (b.get("ticket"), b.get("pnl"))
# --------------------------------------------------------------------------
def test_zwei_positionen_jede_im_eigenen_slot(welt):
"""Der Normalfall, nach dem der User fragt: BEIDE Slots gleichzeitig."""
haupt, brk, _ = _slots()
welt[100] = _Pos(100, pnl=-12.50, magic=4711) # BRK, gefuellte Pending
brk.bind(100, "SpotCrude")
welt[200] = _Pos(200, pnl=+31.00, magic=4711) # manueller Trade
haupt.refresh("SpotCrude")
(t1, p1), (t2, p2) = _gv(haupt, brk)
assert t1 == 200 and p1 == pytest.approx(31.00), "Slot 1 hat die falsche Position"
assert t2 == 100 and p2 == pytest.approx(-12.50), "BRK hat die falsche Position"
assert t1 != t2, "BEIDE Anzeigen zeigen dasselbe Ticket"
def test_haupt_adoptiert_die_brk_position_NICHT(welt):
"""Nur BRK offen, Slot 1 flat -> Slot 1 muss LEER bleiben."""
haupt, brk, _ = _slots()
welt[100] = _Pos(100, pnl=-5.0, magic=4711)
brk.bind(100, "SpotCrude")
haupt.refresh("SpotCrude")
(t1, _), (t2, _) = _gv(haupt, brk)
assert t1 is None, "Slot 1 hat die BRK-Position adoptiert -> G/V zeigt sie doppelt"
assert t2 == 100
def test_pending_fill_wird_nicht_vorher_adoptiert(welt):
"""Der Fill liegt am Broker, bevor `_check_pending_fill` laeuft.
Die Positions-Nummer ist dann identisch mit der Order-Nummer - deshalb
haelt `_pending_tickets` die Luecke zu.
"""
haupt, brk, _ = _slots(pending={100: "squeeze"})
welt[100] = _Pos(100, pnl=+3.0, magic=4711) # gerade gefuellt
haupt.refresh("SpotCrude") # laeuft ZUERST im _pos_loop
assert haupt.snapshot().get("ticket") is None, (
"Slot 1 hat den BRK-Pending-Fill adoptiert, bevor der BRK-Slot ihn "
"uebernehmen konnte")
assert brk.bind(100, "SpotCrude")
assert brk.snapshot().get("ticket") == 100
def test_brk_slot_adoptiert_niemals(welt):
"""`nur_ticket=True` darf sich NIE etwas greifen - auch nicht bei magic."""
_, brk, _ = _slots()
welt[200] = _Pos(200, pnl=+9.0, magic=4711)
brk.refresh("SpotCrude")
assert brk.snapshot().get("ticket") is None
def test_neustart_ohne_merker_in_der_sperrliste_BRICHT(welt):
"""REGRESSIONSPROBE: so sah es aus, bevor `_brk_restore` mit sperrte.
Nach einem Neustart ist `trader_brk.ticket` None und `_pending_tickets`
leer. Steht der Merker NICHT in der Sperrliste, greift Slot 1 die
BRK-Position - und das passiert im `_pos_loop` schon eine Zeile VOR
`_rebind_brk()`.
"""
haupt, brk, _ = _slots(brk_restore=None) # <- alter Zustand
welt[100] = _Pos(100, pnl=-40.0, magic=4711)
haupt.refresh("SpotCrude")
assert haupt.snapshot().get("ticket") == 100, (
"Der Test bildet den alten Zustand nicht mehr ab")
def test_neustart_mit_merker_bleibt_getrennt(welt):
"""Mit dem Merker in der Sperrliste ueberlebt die Trennung den Neustart."""
haupt, brk, _ = _slots(brk_restore=100)
welt[100] = _Pos(100, pnl=-40.0, magic=4711)
haupt.refresh("SpotCrude")
assert haupt.snapshot().get("ticket") is None, (
"Slot 1 hat die BRK-Position trotz Merker adoptiert")
assert brk.bind(100, "SpotCrude") # _rebind_brk kommt danach
(t1, _), (t2, _) = _gv(haupt, brk)
assert t1 is None and t2 == 100