Gate 4 der M15-Karte: Ziel nur noch in der ERLAUBTEN Richtung

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>
This commit is contained in:
Axel Hocks
2026-08-10 06:36:02 +02:00
co-authored by Claude Opus 5
parent b8705e297f
commit 5474c2be61
6 changed files with 293 additions and 30 deletions
+125
View File
@@ -0,0 +1,125 @@
"""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"]))