Files
AH-Oil-Trader/tests/conftest.py
T
Axel HocksandClaude Opus 5 0386497a79 Slot-Pruefung 5. Durchgang: drei Luecken, eine davon gravierend
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>
2026-08-20 19:45:05 +02:00

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