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>
130 lines
4.8 KiB
Python
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
|