Files
AH-Oil-Trader/tests/conftest.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

130 lines
4.8 KiB
Python

"""Gemeinsame Fixtures für die Szenario-Tests.
⚠⚠ DREI HARTE REGELN, damit diese Suite jederzeit gefahrlos läuft:
1. **Nie die Live-DB anfassen.** Jeder Test bekommt eine frische Temp-Datei;
`HistoryLogger.__init__` ruft `_init_db()` und legt das komplette Schema an.
Die 15,7-MB-Live-DB zu kopieren wäre nicht nur langsam, sondern auch
unzuverlässig — echte Zeilen haben mir am 06.08. einen Test verfälscht
(echte `candles_m1` im vermeintlich leeren Fenster).
2. **Kein MT5, kein laufender Server.** Alles, was den Broker berührt, wird
gestubbt. Die Tests prüfen die LOGIK, nicht die Anbindung.
3. **Keine Netzwerkzugriffe.**
Die Engine wird über `__new__` ohne `__init__` erzeugt: der echte Konstruktor
baut MT5-Verbindung, Threads und Config auf. Für die geprüften Methoden reichen
wenige Attribute — was fehlt, fällt im Test sofort als AttributeError auf und ist
damit dokumentiert, statt still zu passieren.
"""
import sys
import types
import logging
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from core.candle_logger import CandleLogger # noqa: E402
from core.engine import TradingEngine # noqa: E402
from core.history import HistoryLogger # noqa: E402
@pytest.fixture(autouse=True, scope="session")
def _kein_produktions_log():
"""VIERTE Grundregel der Suite: Tests schreiben NICHT ins Produktions-Log.
⚠⚠ ANLASS (2026-08-14, real passiert): beim Bau des M15-Alerts standen nach
den Testläufen **über 150** `📨 M15-Alert`-Zeilen in `oil_widget.log` — dazu
`Close-Push #4711` mit dem synthetischen Ticket aus `test_close_buchung.py`.
Ich habe daraufhin zuerst geglaubt, die Entprellung sei live defekt, und
musste die Herkunft mühsam an der Level-Folge (82.00, 82.01, …) nachweisen.
In einem Projekt, dessen Forensik am Log hängt (der 18-h-Ausfall des
Copiloten, der stille `positions_get`-Fehlbuchung), sind erfundene Zeilen
im Produktions-Log ein echtes Risiko — nicht nur Unordnung.
`core/logger.py` haengt einen `RotatingFileHandler` an den Logger `oil`,
sobald `core.engine` importiert wird. Hier wird er fuer die Dauer der
Sitzung entfernt und danach zurueckgegeben — die Log-AUFRUFE laufen weiter
(sie gehoeren zum getesteten Code), nur ihr Ziel entfaellt.
"""
lg = logging.getLogger("oil")
weg = [h for h in lg.handlers if isinstance(h, logging.FileHandler)]
for h in weg:
lg.removeHandler(h)
yield
for h in weg:
lg.addHandler(h)
@pytest.fixture
def db(tmp_path):
"""Frische, vollständig angelegte Test-Datenbank.
⚠ `candles_m1` gehört NICHT zum `HistoryLogger`-Schema, sondern wird vom
`CandleLogger` angelegt (`core/candle_logger.py`). Beide werden hier über
ihre ECHTEN Konstruktoren aufgerufen — ein von Hand nachgebautes Test-Schema
würde irgendwann vom Produktivstand abweichen, und der Test prüfte dann etwas
anderes als das, was live läuft.
"""
p = tmp_path / "test.db"
HistoryLogger(p) # legt trades, recommendations, rec_outcomes, …
CandleLogger(str(p)) # legt candles_m1
return p
@pytest.fixture
def history(db):
return HistoryLogger(db)
class FakeTrader:
"""Minimaler Trader-Ersatz: liefert genau den Snapshot, den der Test setzt."""
def __init__(self):
self.snap: dict = {}
self.offset = 3 * 3600
# ⚠ Der BRK-Slot ist strikt ticket-gebunden; `_check_pending_fill`
# prueft `ticket is None`, um zu entscheiden, ob es binden muss.
# Der Test setzt es direkt, damit kein MT5 gebraucht wird.
self.ticket = None
def snapshot(self):
return self.snap
def _broker_offset_s(self, sym=None):
return self.offset
@pytest.fixture
def engine(history):
"""Nackte Engine ohne `__init__` — nur die Attribute, die geprüft werden."""
e = TradingEngine.__new__(TradingEngine)
e.history = history
e.trader = FakeTrader()
# Zweiter, strikt ticket-gebundener Slot fuer BRK (Umbau 2026-08-19).
e.trader_brk = FakeTrader()
e.data = types.SimpleNamespace(symbol="XTIUSD")
e.cfg = {"telegram": {"enabled": "false"}}
# Zustand der geprüften Pfade
e._pending_tickets = {}
e._pending_tagged = set()
e._bot_open_ticket = None
e._bot_open_source = None
e._squeeze_entry_count = 0
e._signal_entry_count = 0
e._rec_epi_sig = None
e._rec_eval_ts = 0.0
return e
@pytest.fixture
def db_umleiten(db, monkeypatch):
"""Leitet das modulweite `HISTORY_DB_FILE` der Engine auf die Test-DB um.
⚠ Nötig, weil `_evaluate_rec_outcomes` eine EIGENE sqlite3-Verbindung öffnet
(bewusst — die Auswertung läuft außerhalb des HistoryLogger-Locks).
"""
import core.engine as CE
monkeypatch.setattr(CE, "HISTORY_DB_FILE", str(db))
return db