"""Close-Benachrichtigung: feuert sie genau dann, wenn sie soll? ANLASS (2026-08-11, User: „ich bekomme keine Telegram-Nachrichten mehr"). Der Kanal war intakt — eine Testnachricht wurde zugestellt —, aber ALLE NEUN sendenden Ausloeser waren aus. Jede Abschaltung war fuer sich begruendet; zusammen ergaben sie eine Stille, die niemand beschlossen hatte. ⚠ Der frueher entfernte Trade-Abschluss-Push scheiterte an fehlender SCHWELLE (jeder Close, ~17/Tag). Genau die wird hier geprueft — und der Renner-Fall, in dem die Engine dem DB-Schreiber einen Tick voraus ist. """ from __future__ import annotations import pytest import core.engine as eng from core.engine import TradingEngine class _Hist: def __init__(self, trade): self.trade = trade self.aufrufe = 0 def trade_by_ticket(self, ticket): self.aufrufe += 1 return self.trade class _Stub: """Minimal-Engine — nur was `_check_close_notify` wirklich liest.""" _check_close_notify = TradingEngine._check_close_notify def __init__(self, trade, min_eur=50.0, an=True): self.history = _Hist(trade) self._close_notify_min = min_eur self._close_notify_pending = [4711, 0] self.cfg = {"telegram": {"enabled": "true" if an else "false", "bot_token": "T", "chat_id": "C"}} def _trade(pnl=-87.03, fertig=True): return {"ticket": 4711, "symbol": "SpotCrude", "direction": "BUY", "lots": 1.77, "entry_price": 79.065, "exit_price": 78.497, "entry_time": 1000, "exit_time": 7180 if fertig else None, "pnl": pnl if fertig else None, "closed_by": "sl", "setup": "WAVE"} @pytest.fixture(autouse=True) def _abfangen(monkeypatch): """`send_telegram` abfangen — im Test geht NICHTS ins Netz.""" gesendet = [] monkeypatch.setattr(eng, "send_telegram", lambda msg, tok, cid: gesendet.append(msg)) return gesendet def test_ueber_schwelle_wird_gesendet(_abfangen): """Der reale −87,03-€-SL-Trade vom 10.08. — der kam bisher wortlos.""" s = _Stub(_trade(-87.03)) s._check_close_notify() assert len(_abfangen) == 1 t = _abfangen[0] assert "-87.03" in t and "Broker-SL" in t and "🔴" in t assert s._close_notify_pending is None def test_unter_schwelle_bleibt_still(_abfangen): """Genau der Grund, warum der alte Push als Spam entfernt wurde.""" s = _Stub(_trade(12.40)) s._check_close_notify() assert _abfangen == [] assert s._close_notify_pending is None, "Vormerkung trotzdem aufraeumen" def test_gewinn_bekommt_gruenes_icon(_abfangen): s = _Stub(_trade(+229.41)) s._check_close_notify() assert "🟢" in _abfangen[0] and "+229.41" in _abfangen[0] def test_db_noch_nicht_fertig_wird_erneut_versucht(_abfangen): """⚠ Der gefaehrlichste Fall: der Flat-Zustand kommt aus `trader.snapshot()` und ist dem DB-Schreiber gelegentlich einen Tick voraus. `exit_time IS NULL` heisst „noch nicht fertig", NICHT „kein Trade" — wer das verwechselt, meldet einen Close ohne P&L.""" s = _Stub(_trade(fertig=False)) s._check_close_notify() assert _abfangen == [] assert s._close_notify_pending == [4711, 1], "muss vorgemerkt BLEIBEN" # naechster Tick: DB ist durch s.history.trade = _trade(-87.03) s._check_close_notify() assert len(_abfangen) == 1 and s._close_notify_pending is None def test_gibt_nach_30_versuchen_auf(_abfangen): """Ein Fremd-Trade ohne DB-Zeile darf nicht ewig gepollt werden.""" s = _Stub(None) for _ in range(31): s._check_close_notify() assert _abfangen == [] assert s._close_notify_pending is None def test_telegram_aus_sendet_nicht(_abfangen): s = _Stub(_trade(-200.0), an=False) s._check_close_notify() assert _abfangen == [] def test_ohne_vormerkung_passiert_nichts(_abfangen): s = _Stub(_trade(-200.0)) s._close_notify_pending = None s._check_close_notify() assert _abfangen == [] and s.history.aufrufe == 0, "keine DB-Last ohne Anlass" if __name__ == "__main__": raise SystemExit(pytest.main([__file__, "-v"]))