Files
AH-Oil-Trader/tests/test_pending_fill.py
T
Axel HocksandClaude Opus 5 9b5b20e713 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>
2026-08-19 20:03:45 +02:00

91 lines
4.0 KiB
Python

"""`engine._check_pending_fill` — ordnet eine gefüllte Stop-Order dem Bot zu.
HINTERGRUND (2026-08-06): eine ruhende Stop-Order füllt IM BROKER, nicht über
`engine._open`. `trader._refresh_locked` adoptiert die Position dann per
magic-match und loggt sie OHNE `setup` — vier reale Squeeze-Trades waren dadurch
von manuellen nicht unterscheidbar, und B4-Monitor, `squeeze_b5` sowie
`squeeze_entry_gap` zählten **null**. Deployment-Drift Fall 8: nicht die Strategie
driftete, sondern ihre Beobachtbarkeit.
Die Zuordnung ist eindeutig, weil bei einem Pending-Fill die POSITIONS-Nummer
identisch mit der ORDER-Nummer ist (an allen vier Broker-Datensätzen verifiziert).
"""
def test_squeeze_fill_wird_getaggt(engine):
engine._pending_tickets = {49483981: "squeeze"}
engine.trader_brk.snap = {"ticket": 49483981, "order_type": 1,
"entry_price": 75.127}
engine.trader_brk.ticket = engine.trader_brk.snap.get("ticket")
engine._check_pending_fill()
assert engine._squeeze_entry_count == 1
assert engine._bot_open_ticket == 49483981
assert engine._bot_open_source == "auto_squeeze"
assert 49483981 not in engine._pending_tickets # Merker verbraucht
def test_unbekannte_quelle_wird_NICHT_getaggt(engine):
"""⚠ Ersetzt `test_signal_fill_long` (der Auto-Signal-Pfad ist am
2026-08-12 entfernt). Der Test wurde NICHT geloescht, sondern umgedreht:
seit es nur noch den Squeeze gibt, darf ein Fill mit einer FREMDEN Quelle
nicht stillschweigend als Squeeze durchgehen — das waere genau
Deployment-Drift Fall 8 (vier reale Trades landeten als `setup=NULL` und
waren von manuellen nicht unterscheidbar), nur mit umgekehrtem Vorzeichen:
ein Fremd-Trade wuerde die B4-/B5-Monitore verfaelschen."""
engine._pending_tickets = {777: "signal"} # Altbestand / unbekannt
engine.trader_brk.snap = {"ticket": 777, "order_type": 0, "entry_price": 75.2}
engine.trader_brk.ticket = engine.trader_brk.snap.get("ticket")
engine._check_pending_fill()
assert engine._squeeze_entry_count == 0
assert engine._bot_open_source is None
# ⚠ Der Merker bleibt STEHEN — nur ein sauber zugeordneter Fill verbraucht
# ihn. So wird derselbe Fall beim naechsten Tick erneut gemeldet, statt
# still zu verschwinden.
assert 777 in engine._pending_tickets
def test_fremde_position_wird_nicht_getaggt(engine):
"""Der gefährliche Fall: ein manueller Trade darf NICHT als Bot-Trade gelten —
sonst verfälscht er genau die Monitore, die der Fix reparieren soll."""
engine._pending_tickets = {111: "squeeze"}
engine.trader_brk.snap = {"ticket": 999, "order_type": 0, "entry_price": 75.0}
engine.trader_brk.ticket = engine.trader_brk.snap.get("ticket")
engine._check_pending_fill()
assert engine._squeeze_entry_count == 0
assert engine._bot_open_ticket is None
assert engine._pending_tickets == {111: "squeeze"} # bleibt liegen
def test_derselbe_fill_zaehlt_nur_einmal(engine):
"""Der `_pos_loop` läuft im Sekundentakt — ohne Dedup zählte ein Trade
hundertfach und der B4-Monitor wäre wertlos."""
engine._pending_tickets = {222: "squeeze"}
engine.trader_brk.snap = {"ticket": 222, "order_type": 1, "entry_price": 74.9}
for _ in range(5):
engine.trader_brk.ticket = engine.trader_brk.snap.get("ticket")
engine._check_pending_fill()
assert engine._squeeze_entry_count == 1
def test_flat_ist_ein_no_op(engine):
engine._pending_tickets = {333: "squeeze"}
engine.trader_brk.snap = {"ticket": None}
engine.trader_brk.ticket = engine.trader_brk.snap.get("ticket")
engine._check_pending_fill()
assert engine._squeeze_entry_count == 0
assert engine._pending_tickets == {333: "squeeze"}
def test_ohne_pending_merker_sofort_raus(engine):
"""Der häufigste Fall — es darf keine DB-Arbeit anfallen."""
engine.trader_brk.snap = {"ticket": 444, "order_type": 0}
engine.trader_brk.ticket = engine.trader_brk.snap.get("ticket")
engine._check_pending_fill()
assert engine._bot_open_ticket is None