"""M15-Alert: die ENTPRELLUNG ist der ganze Bau — also wird sie geprueft. ANLASS (2026-08-14, User: „wenn es eine Empfehlung gibt (gruen), schicke eine Telegram-Nachricht"). ⚠⚠ Roh gemessen springt die Karte **85x am Tag** auf gruen (`m15_states`, 3 Tage nach der Hysterese vom 12.08.). Ohne Entprellung waere die Meldung Spam und damit wertlos — genau die Sorte Funktion, die man nach zwei Tagen stummschaltet. Die Stufen und ihre gemessene Wirkung: Flanke allein 85,0/Tag + Level+Richtung als Schluessel 23,7/Tag + 30 min Mindestabstand 13,3/Tag + Fenster 8-20 Uhr 7,7/Tag Deshalb liegt der Schwerpunkt der Tests auf dem, was NICHT gesendet werden darf. """ from __future__ import annotations import time from types import SimpleNamespace import pytest class _Fake: """Minimal-Engine mit genau den Feldern, die `_check_m15_alert` anfasst.""" def __init__(self, **kw): from core.engine import TradingEngine self._check_m15_alert = TradingEngine._check_m15_alert.__get__(self) self._m15_alert = kw.get("an", True) self._m15_alert_cd = kw.get("cd", 1800.0) self._m15_alert_hours = kw.get("hours", (0, 24)) self._m15_alert_war = False self._m15_alert_ts = 0.0 self._m15_alert_key = None self.cfg = {"telegram": {"enabled": "true", "bot_token": "t", "chat_id": "c"}} def _karte(status="gruen", lesart="auf", level=82.069, pb=69): return {"status": status, "lesart": lesart, "nah": {"seite": "res", "level": level, "gap_atr15": 0.48, "p_break": pb}, "bias": {"verboten": "SHORT"}, "kosten": {"ratio": 0.11}} @pytest.fixture def gesendet(monkeypatch): """Faengt jeden Telegram-Versand ab — kein Netz im Test (conftest-Regel).""" raus = [] monkeypatch.setattr("core.engine.send_telegram", lambda text, *a, **k: raus.append(text)) return raus def test_gruene_flanke_sendet(gesendet): e = _Fake() e._check_m15_alert(_karte()) assert len(gesendet) == 1 assert "Setup bereit" in gesendet[0] def test_zustand_sendet_nur_EINMAL(gesendet): """⚠ Die wichtigste Zeile: gruen bleibt gruen ueber viele Ticks. Ohne Flankenerkennung kaeme im Sekundentakt eine Nachricht.""" e = _Fake() for _ in range(50): e._check_m15_alert(_karte()) assert len(gesendet) == 1 def test_flankenerkennung_ISOLIERT(gesendet): """⚠⚠ DIESER Test existiert wegen einer Mutationsprobe (2026-08-14): das Entfernen der Flankenerkennung liess ALLE anderen Tests gruen — weil der Cooldown und der 4-h-Schluessel dasselbe mit abfangen. Zwei Schutzschichten, die sich gegenseitig maskieren, sind genau das Muster, das im Projekt beim Fill-Dedup schon dokumentiert ist. Isoliert wird sie so: Cooldown AUS und ein Level, das bei jedem Tick leicht wandert (real der Normalfall — der Kurs laeuft, der naechste Pivot rutscht). Damit greift WEDER Cooldown NOCH Schluessel; bleibt nur die Flanke. """ e = _Fake(cd=0.0) for i in range(30): e._check_m15_alert(_karte(level=82.0 + i * 0.01)) assert len(gesendet) == 1, ( "ohne Flankenerkennung kaeme hier eine Nachricht je Tick") def test_flackern_am_selben_level_sendet_nicht_erneut(gesendet): """gruen -> gelb -> gruen am SELBEN Level ist dasselbe Setup, das flackert. Gemessen der Haupttreiber: 85 -> 23,7/Tag allein durch diesen Schluessel.""" e = _Fake() for _ in range(10): e._check_m15_alert(_karte()) e._check_m15_alert(_karte(status="gelb")) assert len(gesendet) == 1 def test_neues_level_sendet_trotz_cooldown_nicht(gesendet): """⚠ Der Mindestabstand hat VORRANG vor dem neuen Level — sonst genuegt ein um einen Cent verschobenes Level, um die Entprellung auszuhebeln.""" e = _Fake() e._check_m15_alert(_karte(level=82.069)) e._check_m15_alert(_karte(status="gelb")) e._check_m15_alert(_karte(level=83.500)) # anderes Level, aber sofort assert len(gesendet) == 1 def test_neues_level_sendet_nach_cooldown(gesendet): e = _Fake(cd=0.0) e._check_m15_alert(_karte(level=82.069)) e._check_m15_alert(_karte(status="gelb")) e._check_m15_alert(_karte(level=83.500)) assert len(gesendet) == 2 def test_gleiches_level_wiederholt_nicht_vor_vier_stunden(gesendet): """Ohne Cooldown, aber gleicher Schluessel -> erst nach _ALERT_NEU_S wieder.""" e = _Fake(cd=0.0) e._check_m15_alert(_karte(level=82.069)) e._check_m15_alert(_karte(status="gelb")) e._check_m15_alert(_karte(level=82.069)) assert len(gesendet) == 1 def test_ausserhalb_des_stundenfensters_still(gesendet): e = _Fake(hours=(23, 24)) # jetzt ist praktisch nie drin if time.localtime().tm_hour == 23: pytest.skip("Testzeit liegt zufaellig im Fenster") e._check_m15_alert(_karte()) assert gesendet == [] def test_unklare_lesart_sendet_nicht(gesendet): """45-55 % ist ein Muenzwurf — daraus eine Meldung zu machen waere genau der Fehler, den die Karte an drei Stellen schon gemacht hat.""" e = _Fake() e._check_m15_alert(_karte(lesart="unklar")) assert gesendet == [] def test_gelb_und_rot_senden_nicht(gesendet): e = _Fake() e._check_m15_alert(_karte(status="gelb")) e._check_m15_alert(_karte(status="rot")) assert gesendet == [] def test_abgeschaltet_sendet_nicht(gesendet): e = _Fake(an=False) e._check_m15_alert(_karte()) assert gesendet == [] def test_telegram_deaktiviert_sendet_nicht(gesendet): e = _Fake() e.cfg["telegram"]["enabled"] = "false" e._check_m15_alert(_karte()) assert gesendet == [] def test_text_nennt_die_gemessene_lesart_und_warnt(gesendet): """Die Zahl muss WORTGLEICH zur Karte und zum Chart sein (v1.35-Fix), und der Text darf keine Prognose behaupten.""" e = _Fake() e._check_m15_alert(_karte(pb=33)) t = gesendet[0] assert "67 % Abprall" in t, "P(Abprall) wie in Karte und Chart" assert "keine Richtungsprognose" in t assert "erlaubt wäre LONG" in t # Veto SHORT -> erlaubt LONG def test_kein_absturz_ohne_daten(gesendet): e = _Fake() e._check_m15_alert(None) e._check_m15_alert({}) e._check_m15_alert({"status": "gruen", "lesart": "auf"}) # `nah` fehlt assert isinstance(gesendet, list) if __name__ == "__main__": raise SystemExit(pytest.main([__file__, "-v"]))