M15-Karte: Gate 3b zeigt Motor-Block + RAUM je Richtung

Anlass Trade #49671601 (-87,03 EUR). Die Praemisse "SL trotz LONG-
Empfehlung" stimmte nicht: 138 Zeilen zwischen Ein- und Ausstieg,
100 % WARTEN (entry_room 76 %, breakout_pending 19 %, min_conf 5 %).
Gesehen wurde "verboten: SHORT" - ein Veto. Dritter Fall derselben
Veto->Richtung-Umwandlung in dieser Karte (Code 08.08., Geometrie
10.08. frueh, jetzt die Lesart).

Der eigentliche Befund: entry_room und Gate 3 lesen DIESELBE Tatsache
("ein Level ist nah") mit umgekehrtem Vorzeichen. Beide gemessen,
beide richtig - ueber verschiedene Trades (Markt-Einstieg mit dem
Level als Hindernis vs. ruhende Order AM Level). Die Karte zeigte nur
die gruene Haelfte; die Block-Grund-Zeile war mit der Gesamtempfehlung
verschwunden.

Gebaut: Zeile 3b (#m15-motor) mit block_reason im Klartext + Raum je
Richtung. Reine Anzeige. Das Status-Banner bleibt bewusst unveraendert
- es auf gelb zu ziehen waere eine Strategie-Aenderung, und die einzige
Live-Zahl dafuer (-9,91 vs +3,24 EUR/Lot, n=76) hat ein KI, das die
Null enthaelt.

- wave_rec.room_info(): EINE Implementierung fuer Gate und Anzeige,
  _room_gate rechnet jetzt darueber (kein Nachbau)
- tests/test_room_info.py: Bitgleichheit ueber 2.000 Zufallslagen x 3
  Signale, Gate feuerte >300x; Grenzfall-Test fuer die Rundung
- v=160

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-10 09:09:27 +02:00
co-authored by Claude Opus 5
parent 5474c2be61
commit 47c596ed70
6 changed files with 375 additions and 19 deletions
+50 -15
View File
@@ -612,32 +612,67 @@ class WaveRecommender:
"regime": None, "rsi": None, "block": "breakout_pending",
"reasons": [f"warte auf Breakout (+{k:.1f}×ATR {sig}, noch {need:.2f})"]}
def room_info(self, cur):
"""Abstand zum Gegenlevel JE RICHTUNG — die Zahl hinter dem entry_room-Gate.
⚠⚠ Warum das hier steht und nicht anderswo nachgebaut wird: die Zahl lag
bisher NUR als Fließtext in `reasons` und war damit nicht auswertbar. Eine
zweite Berechnung in Engine oder Frontend wäre genau die Divergenz, die im
Projekt schon dreimal zugeschlagen hat (`sim_run`/`_ema_series` als Kopie in
≥6 Skripten · der nachgebaute Level-Cluster · das fünfte Exit-Modell).
**Eine Implementierung, zwei Nutzer:** `_room_gate` (Entscheidung) und
`engine._m15_setup` (Anzeige).
⚠ BEIDE Richtungen, weil die Karte bewusst keine Richtung vorgibt — der
Mensch kann in jede handeln und braucht den Raum für die, die er wählt.
⚠ Quelle ist trainingsgleich **M5** (`_pb_levels`/`_pb_feats`), NICHT die
M30-Level der Karte: `entry_room_atr=0.6` ist auf M5 kalibriert. Die drei
divergierenden Level-Quellen sind Absicht, aber eine Stolperfalle.
"""
lv = getattr(self, "_pb_levels", None) or {}
pf = getattr(self, "_pb_feats", None) or {}
atr5 = pf.get("atr")
x = self._entry_room_atr
out = {"gate": x, "atr_m5": atr5, "long": None, "short": None}
if not atr5 or not cur:
return out
for name, d in (("long", 1), ("short", -1)):
if d > 0:
cands = [p for p in (lv.get("ph") or []) if p > cur]
lvl = min(cands) if cands else None
else:
cands = [p for p in (lv.get("pl") or []) if p < cur]
lvl = max(cands) if cands else None
if lvl is None: # freie Bahn (bestes gemessenes Segment)
out[name] = {"level": None, "dist_atr": None, "frei": True}
continue
dist = (lvl - cur) * d / atr5
# ⚠ `frei` MUSS auf dem UNGERUNDETEN Wert entschieden werden — sonst
# kippt ein Grenzfall (0,5951 → gerundet 0,60 → fälschlich frei)
# und das Gate verhielte sich anders als vor dem Umbau.
out[name] = {"level": round(lvl, 3), "dist_atr": round(dist, 2),
"frei": bool(dist >= x)}
return out
def _room_gate(self, rec, cur):
"""Entry-Raum-Gate: Signal → WARTEN, wenn das Gegenlevel < X×ATR_M5 entfernt
ist (gemessen `backtest_entryroom.py`: Raum <0,6×ATR in BEIDEN Hälften klar
negativ — 7982 % WR, aber PF<1 = Klein-Close-Falle). Level/ATR trainings-
gleich aus `_pb_levels`/`_pb_feats` (M5). Fail-open: ohne Daten kein Gate."""
gleich aus `_pb_levels`/`_pb_feats` (M5). Fail-open: ohne Daten kein Gate.
⚠ Rechnet seit 2026-08-10 über `room_info` — Verhalten unverändert
(bitgenau geprüft in `tests/test_room_info.py`)."""
x = self._entry_room_atr
sig = rec.get("signal")
if x <= 0 or sig == "WARTEN":
return rec
lv = getattr(self, "_pb_levels", None) or {}
pf = getattr(self, "_pb_feats", None) or {}
atr5 = pf.get("atr")
if not atr5:
info = self.room_info(cur)
if not info.get("atr_m5"):
return rec
d = 1 if sig == "LONG" else -1
if d > 0:
cands = [p for p in (lv.get("ph") or []) if p > cur]
lvl = min(cands) if cands else None
else:
cands = [p for p in (lv.get("pl") or []) if p < cur]
lvl = max(cands) if cands else None
if lvl is None: # freie Bahn (bestes gemessenes Segment)
return rec
dist = (lvl - cur) * d / atr5
if dist >= x:
seite = info["long"] if d > 0 else info["short"]
if seite is None or seite["level"] is None or seite["frei"]:
return rec
lvl, dist = seite["level"], seite["dist_atr"]
return {"signal": "WARTEN", "conf_pct": 0, "score": 0.0, "setup": "WAVE",
"regime": None, "rsi": None, "block": "entry_room",
"reasons": [f"kein Raum: {'Widerstand' if d > 0 else 'Support'} "