"""Gate ④ der M15-Karte: das Ziel muss in der ERLAUBTEN Richtung liegen. ANLASS (2026-08-10, User-Fund): die Karte zeigte `verboten=SHORT` (also LONG erlaubt) UND gleichzeitig ein Ziel 0,15 $ UNTER dem Kurs. Die Arithmetik war korrekt — aber fuer die verbotene Richtung. Ursache: das Ziel war schlicht das ENTFERNTERE der beiden Level (`max` nach gap), ohne Richtungspruefung. ⚠ Die Tests pruefen BEIDE Richtungen. Ein Test, der nur zeigt „im Fehlerfall kommt kein Ziel", belegt nicht, dass der korrekte Fall noch funktioniert — genau daran ist am 07.08. die Neustart-Sperre fast gescheitert (Negativ-Test gruen, Positiv-Test rot). """ from __future__ import annotations import pytest from core.engine import TradingEngine class _Stub: """Minimal-Engine: nur die beiden Methoden, die `_m15_setup` wirklich ruft.""" def __init__(self, res, sup): self._res, self._sup = res, sup def _draw_levels(self): return {"res": [self._res] if self._res else [], "sup": [self._sup] if self._sup else []} def _vola_regime(self, atr15): return None def _setup(bid, res, sup, htf): """`_m15_setup` mit Stub-Engine aufrufen (die Methode ist eine reine Funktion).""" ws = {"tf_atr": {"M15": 0.25}, "htf_trend": htf, "h1_trend": htf, "pb_feats": {"atr": 0.2, "ema_diff": 0.05, "mom6": 0.1, "mom3": 0.1}} cone = {"levels": [{"band": 80, "minutes": m, "lo": bid - 1.0, "hi": bid + 1.0} for m in (30, 60, 120)]} return TradingEngine._m15_setup( _Stub(res, sup), {"bid": bid, "ask": bid + 0.03}, ws, cone, None) # ── Der reale Fehlerfall vom 10.08. ────────────────────────────────────── def test_kein_ziel_gegen_das_veto(): """LONG erlaubt, naechstes Level ist der Widerstand → Ausbruch, kein Ziel. Exakt die Live-Zahlen: bid 78,918 · res 79,033 (0,47xATR) · sup 78,768 (0,61xATR). Vorher stand hier `ziel=78.768` — 0,15 $ UNTER dem Kurs, bei verbotenem SHORT.""" s = _setup(78.918, 79.033, 78.768, htf=1) assert s["bias"]["verboten"] == "SHORT" # LONG ist erlaubt assert s["ziel"] is None, "Ziel zeigte in die verbotene Richtung" assert "Ausbruch" in (s["ziel_grund"] or "") def test_ziel_niemals_auf_der_verbotenen_seite(): """Die eigentliche Invariante — ueber viele Konstellationen geprueft. Wird ein Ziel ausgewiesen, MUSS es in der erlaubten Richtung vom Kurs liegen. Das ist die Aussage, die der Fehler verletzt hat.""" for htf, verboten in ((1, "SHORT"), (-1, "LONG")): for res_d in (0.05, 0.15, 0.30, 0.60): for sup_d in (0.05, 0.15, 0.30, 0.60): bid = 78.918 s = _setup(bid, bid + res_d, bid - sup_d, htf=htf) assert s["bias"]["verboten"] == verboten z = s["ziel"] if z is None: continue if verboten == "SHORT": # LONG erlaubt → Ziel OBEN assert z["level"] > bid, f"Ziel {z['level']} unter Kurs {bid}" assert z["richtung"] == "LONG" else: # SHORT erlaubt → Ziel UNTEN assert z["level"] < bid, f"Ziel {z['level']} ueber Kurs {bid}" assert z["richtung"] == "SHORT" # ── Der korrekte Fall MUSS erhalten bleiben ────────────────────────────── def test_fade_am_widerstand_bleibt_wenn_short_erlaubt(): """M30 ab → SHORT erlaubt, naechstes Level ist der Widerstand → Ziel Support. Das ist genau die Anzeige, die vorher zu sehen war — sie war nur unter dem FALSCHEN Veto zu sehen. Sie darf nicht mit weggefixt werden.""" s = _setup(78.918, 79.033, 78.768, htf=-1) assert s["bias"]["verboten"] == "LONG" assert s["ziel"] is not None assert s["ziel"]["level"] == 78.768 assert s["ziel"]["von"] == 79.033 assert s["ziel"]["richtung"] == "SHORT" def test_bounce_am_support_mit_long(): """Kurs dicht ueber dem Support, LONG erlaubt → Einstieg Support, Ziel Widerstand.""" s = _setup(78.80, 79.30, 78.77, htf=1) # sup 0,03 nah, res 0,50 fern assert s["ziel"] is not None assert s["ziel"]["von"] == 78.77 # Einstieg = naechstes Level assert s["ziel"]["level"] == 79.30 # Ziel = oben assert s["ziel"]["richtung"] == "LONG" def test_ohne_veto_kein_ziel(): """M30 flach → keine erlaubte Richtung → ein einzelnes Ziel waere wieder eine Richtungsaussage.""" s = _setup(78.918, 79.033, 78.768, htf=0) assert s["bias"]["verboten"] is None assert s["ziel"] is None assert "kein Veto" in (s["ziel_grund"] or "") def test_kegel_pruefung_bleibt_wirksam(): """Liegt das Ziel ausserhalb des 80-%-Bandes, muessen alle Horizonte `drin=False` melden — die Erreichbarkeits-Aussage selbst bleibt intakt.""" ws = {"tf_atr": {"M15": 0.25}, "htf_trend": 1, "h1_trend": 1, "pb_feats": {"atr": 0.2, "ema_diff": 0.05, "mom6": 0.1, "mom3": 0.1}} cone = {"levels": [{"band": 80, "minutes": m, "lo": 78.79, "hi": 78.81} for m in (30, 60, 120)]} s = TradingEngine._m15_setup( _Stub(79.30, 78.77), {"bid": 78.80, "ask": 78.83}, ws, cone, None) assert s["ziel"] is not None assert all(h["drin"] is False for h in s["ziel"]["horizonte"]) if __name__ == "__main__": raise SystemExit(pytest.main([__file__, "-v"]))