User-Fund: Karte zeigte "verboten SHORT" (= LONG erlaubt) UND 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. Strukturell steckte dahinter ein Fade-Trade (Gegenlevel als Ziel = Mean Reversion) in einer Kette, deren Gate 2 ein Trendfilter ist - zwei unvereinbare Konzepte. Damit derselbe Fehler, wegen dem die Gesamtempfehlung entfernt wurde: aus einem VERBOT wird ueber die Hintertuer wieder eine RICHTUNG. Fix nach der gemessenen Population (backtest_m15_gates.py): Einstieg am naechstgelegenen Level, Richtung = die vom HTF erlaubte. Ein Ziel nur, wenn es jenseits dieses Einstiegs in der erlaubten Richtung liegt; sonst "kein Ziel" mit Grund statt einer erfundenen Zahl. Richtung steht jetzt im Anzeigetext. - tests/test_m15_ziel.py (6 Tests, Invariante ueber 32 Konstellationen) - Mutationsprobe: alte Zeile zurueck -> 4 von 6 Tests fallen - deploy.py --feld unterstuetzt jetzt Punktpfade (m15_setup.ziel_grund) - v=159 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
5.5 KiB
Python
126 lines
5.5 KiB
Python
"""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"]))
|