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
+55 -17
View File
@@ -4368,27 +4368,65 @@ class TradingEngine:
for x in b80] or None
# ── 4b) ZIEL-ERREICHBARKEIT — reine GEOMETRIE auf dem Kegel.
# Ziel eines Trades am nahen Level ist das GEGENÜBERLIEGENDE.
# Gefragt wird nur: passt dieser Abstand noch in das 80-%-Band
# des jeweiligen Horizonts? ⚠ KEINE Bruch-Annahme — die im
# Vorschlag mitgedachte „je länger, desto eher bricht es"-Regel
# ist gemessen NICHT robust (Touch-Zahl: umgekehrtes U auf 4
# Gefragt wird nur: passt der Abstand noch in das 80-%-Band des
# jeweiligen Horizonts? ⚠ KEINE Bruch-Annahme — die im Vorschlag
# mitgedachte „je länger, desto eher bricht es"-Regel ist
# gemessen NICHT robust (Touch-Zahl: umgekehrtes U auf 4
# Live-Tagen, im Backtest +0,010 AUC und nach dem Nachtrainieren
# gar nichts mehr). Hier steckt deshalb nur Arithmetik.
if len(zonen) == 2 and bid and b80:
#
# ⚠⚠ FEHLER BEHOBEN 2026-08-10 (User-Fund „Kurs 79, Ziel 78,7 — bei
# LONG?"). Der erste Entwurf nahm als Ziel schlicht das ENTFERNTERE
# der beiden Level (`max` nach gap) — also **rein geometrisch, ohne
# die erlaubte Richtung zu prüfen**. Real zeigte die Karte damit
# `verboten=SHORT` UND ein Ziel 0,15 $ UNTER dem Kurs: die Arithmetik
# war korrekt, aber für die VERBOTENE Richtung.
# Strukturell steckte dahinter die stillschweigende Annahme eines
# **Fade-Trades** (am Level einsteigen, Gegenlevel als Ziel = Mean
# Reversion), während Gate ② ein **Trendfilter** ist. Zwei
# unvereinbare Handelskonzepte in einer Kette — und damit derselbe
# Fehler, wegen dem die Gesamtempfehlung entfernt wurde: aus einem
# VERBOT wird über die Hintertür wieder eine RICHTUNG.
#
# Jetzt gilt die gemessene Population aus `backtest_m15_gates.py`:
# Einstieg am **nächstgelegenen** Level, Richtung = die vom HTF
# erlaubte. Ein Ziel wird nur ausgewiesen, wenn es JENSEITS dieses
# Einstiegs in der erlaubten Richtung liegt.
# ⚠ Ohne Veto (M30 flach) gibt es KEIN Ziel — ein einzelnes Ziel
# zu zeigen wäre wieder eine Richtungsaussage.
# ⚠ `_draw_levels` liefert bewusst nur die NÄCHSTE Linie je Seite.
# Beim Ausbruchs-Einstieg (nächstes Level == Ziel-Seite) ist das
# nächste Ziel damit schlicht NICHT berechenbar — dann sagt die
# Karte das, statt eine Zahl zu erfinden.
out["ziel"] = None
d_ok = (1 if verboten == "SHORT" else
-1 if verboten == "LONG" else 0)
if len(zonen) == 2 and bid and b80 and d_ok:
nah = min(zonen, key=lambda x: x["gap_atr15"])
fern = max(zonen, key=lambda x: x["gap_atr15"])
strecke = abs(fern["level"] - bid)
out["ziel"] = {
"level": fern["level"], "strecke": round(strecke, 3),
"horizonte": [{"minutes": x.get("minutes"),
"drin": bool(x.get("lo") is not None
and x.get("hi") is not None
and x["lo"] <= fern["level"] <= x["hi"])}
for x in b80],
"von": nah["level"]}
kand = next((z for z in zonen
if z["seite"] == ("res" if d_ok > 0 else "sup")), None)
if kand is None:
grund = "kein Level in der erlaubten Richtung"
elif (kand["level"] - nah["level"]) * d_ok > 0:
strecke = abs(kand["level"] - bid)
out["ziel"] = {
"level": kand["level"], "strecke": round(strecke, 3),
"horizonte": [{"minutes": x.get("minutes"),
"drin": bool(x.get("lo") is not None
and x.get("hi") is not None
and x["lo"] <= kand["level"] <= x["hi"])}
for x in b80],
"von": nah["level"],
"richtung": "LONG" if d_ok > 0 else "SHORT"}
grund = None
else:
grund = ("Ausbruch — Einstieg am "
+ ("Widerstand" if nah["seite"] == "res" else "Support")
+ f" {nah['level']:.3f}, nächstes Ziel dahinter nicht berechenbar")
out["ziel_grund"] = grund
else:
out["ziel"] = None
out["ziel_grund"] = (None if d_ok else
"kein Veto — Richtung offen, kein Ziel bestimmbar")
# ── 4c) VOLA-REGIME als Slippage-HINWEIS (kein Gate, s. Docstring)
out["vola"] = self._vola_regime(atr15)