Diesmal systematisch statt punktuell: alle Methoden von TradingEngine per AST
klassifiziert, welche Slot-Objekte sie beruehren. 36 Funktionen fassen einen
Slot an, 20 davon NUR Slot 1.
⚠⚠⚠ LUECKE 1 (gravierend): EINE FRISCH GEFUELLTE BRK-POSITION HATTE KEIN
TRAILING. trail_brk.toggle() wurde an genau ZWEI Stellen gerufen - _rebind_brk
(nach Neustart) und toggle_trail (der Knopf). Der frische Pending-Fill kam in
keiner davon vor, und das ist der haeufigste Weg, auf dem eine BRK-Position
entsteht (7 von 8 Trades seit dem Pending-Umbau). Sie lief damit nur mit dem
Broker-SL - exakt die Variante, die backtest_brk_slonly.py verworfen hat (alle
zehn KI enthalten die Null, Trefferquote 40 -> 21 %).
Von aussen unsichtbar: die Position sieht in jeder Anzeige normal aus, ihr
fehlt nur der Schutz. Ironie: _rebind_brk loggt "Trailing wiederhergestellt" -
wiederhergestellt wurde etwas, das fuer einen frischen Fill nie an war.
⚠⚠ LUECKE 2: der Market-Fallback schaltete das FALSCHE Trailing ein. _open rief
pauschal self.trail.toggle(). Zwei Schaeden auf einmal: die BRK-Position bleibt
ungeschuetzt UND auf der manuellen Position wird das Trailing wieder
eingeschaltet, auch wenn der Nutzer es dort bewusst abgeschaltet hatte (nach
einer SL-Handeingabe). Sein handgesetzter Stop waere weitergezogen worden.
Noch nicht eingetreten, weil alle acht Fallback-Versuche des 20.08. schon an
der Order scheiterten.
⚠⚠ LUECKE 3: ein geschlossener BRK-Trade loeste KEINE Telegram-Meldung aus.
_close_notify_pending wird in _check_auto_close armiert, das self.trader liest.
Wiegt schwer, weil seit dem 19.08. NUR NOCH "Trade geschlossen" durch den
Telegram-Filter kommt - der autonome Pfad haette vollstaendig still gehandelt.
Behoben in _check_brk_close_cooldown, das den BRK-Flat-Uebergang ohnehin kennt.
Dazu die Reichweiten-Warnung korrigiert: sie sagte "Broker-SL, Trailing und
Circuit-Breaker greifen weiter" - das Trailing tat es eben NICHT.
ABGESICHERT auf zwei Ebenen, beide mutationsgeprueft:
tests/test_pending_fill.py +2: der Fill MUSS trail_brk einschalten und DARF
das Trailing von Slot 1 nicht anfassen. Mutation -> Test faellt.
Pipeline-Stufe E, Regel E: jeder Pfad, der eine BRK-Position eroeffnet
(_check_pending_fill, _open), MUSS trail_brk erwaehnen. Mutation -> Exit 1.
Die Test-Fixture bekam dafuer erstmals BEIDE Trailing-Attrappen - ohne die war
der gefaehrlichste Slot-Fehler ueberhaupt nicht pruefbar.
GEPRUEFT UND SAUBER: Circuit-Breaker, close(), _check_open_notify, _price_loop,
_write_levels_file, snapshot, die Fill-Bindung, die _pos_loop-Reihenfolge.
BEWUSST NICHT GEAENDERT (Slot-1-only, aber alle Features AUS): _check_auto_close,
_check_sr_close, _check_adverse15, _check_flip_close, _check_auto_m15, set_sltp,
_check_close_alert. _check_slot_reichweite meldet es, sobald eines eingeschaltet
wird. Zweislot-faehig waere ein Umbau der Notfall-Zustandsmaschine - offen.
⚠⚠ ZWEI CONFIG-WERTE SIND AUF 0 ZURUECKGEDRIFTET (nur berichtet):
close_notify_min_eur dokumentiert 50 -> ist 0 (Telegram ganz still)
auto_emergency_margin_pct dokumentiert 8 -> ist 0
Bei close_notify_min_eur = 0 nuetzt auch Luecke 3 nichts - die Vormerkung haengt
an _close_notify_min > 0. Der Bot meldet derzeit gar keinen Trade-Abschluss.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.7 KiB
Python
150 lines
5.7 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()
|
|
# ⚠⚠ BEIDE Trailing-Manager — sonst ist der gefaehrlichste Slot-Fehler
|
|
# ueberhaupt nicht pruefbar: bis zum 2026-08-20 hat der Pending-Fill das
|
|
# BRK-Trailing NICHT eingeschaltet, die Position lief nur mit dem
|
|
# Broker-SL (gemessen durchgefallen, `backtest_brk_slonly.py`). Die
|
|
# Attrappe merkt sich lediglich, OB `toggle()` gerufen wurde — die
|
|
# Trailing-Mechanik selbst gehoert nicht in diesen Test.
|
|
class _FakeTrail:
|
|
def __init__(self):
|
|
self.enabled = False
|
|
self.setup = None
|
|
def set_setup(self, s):
|
|
self.setup = s
|
|
def toggle(self, sym):
|
|
self.enabled = not self.enabled
|
|
return self.enabled
|
|
def deactivate(self):
|
|
was, self.enabled = self.enabled, False
|
|
return was
|
|
e.trail = _FakeTrail()
|
|
e.trail_brk = _FakeTrail()
|
|
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
|