From 47c596ed70ea3f432144e128f72cbe3cc458915c Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Mon, 10 Aug 2026 09:09:27 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 72 ++++++++++++++++++ core/engine.py | 37 +++++++++- core/wave_rec.py | 65 ++++++++++++---- tests/test_room_info.py | 159 ++++++++++++++++++++++++++++++++++++++++ web/app.js | 50 +++++++++++++ web/index.html | 11 ++- 6 files changed, 375 insertions(+), 19 deletions(-) create mode 100644 tests/test_room_info.py diff --git a/CLAUDE.md b/CLAUDE.md index 26feee1..5a2e2c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5220,6 +5220,78 @@ nicht führbar — und Schritt 4 ist der EINZIGE belastbare Nachweis, dass neuer Python-Code läuft. Jetzt mit **Punktpfaden**; ein Feld mit Wert `None` zählt als vorhanden (geprüft wird der Schlüssel, nicht der Inhalt). +### ⚠⚠ GATE ③b — DIE KARTE PRÜFTE „LEVEL NAH", NIE „RAUM DAHINTER" (2026-08-10) + +**Anlass: Trade #49671601, −87,03 €** (User: „SL geschlossen trotz LONG-Empfehlung"). +⚠ **Die Prämisse stimmte nicht — es gab keine LONG-Empfehlung.** 138 Zeilen +zwischen Ein- und Ausstieg, **100 % WARTEN**: `entry_room` 76 % · `breakout_pending` +19 % · `min_conf` 5 %, **kein einziges durchgelassenes Richtungssignal**. Gesehen +wurde `verboten: SHORT` — ein **Veto**. +⚠⚠ Damit ist es der **dritte** Fall derselben Umwandlung in dieser Karte: am 08.08. +machte der **Code** aus dem Verbot eine Richtung (13 LONG-Episoden an einem +fallenden Tag), am 10.08. früh die **Geometrie** (Gate ④), jetzt die **Lesart**. +Wenn eine Anzeige dreimal zur selben Fehldeutung einlädt, liegt es an der Anzeige. + +⚠⚠ **DER EIGENTLICHE BEFUND: zwei Bauteile lesen DIESELBE Tatsache mit +umgekehrtem Vorzeichen.** +| Bauteil | bei „Level sehr nah" | Beleg | +|---|---|---| +| **`entry_room`** (Signal-Motor) | **WARTEN** — Ertrag am Level gedeckelt | ØR −0,119/−0,067 · **PF 0,42/0,66** | +| **Gate ③** (M15-Karte) | **GRÜN** — Level in Reichweite | Ausführungs-Edge der ruhenden Order | +✅ **Beide sind gemessen und beide haben recht — über VERSCHIEDENE Trades:** +`entry_room` misst den **Markt**-Einstieg mit dem Level als *Hindernis voraus*, +die Karte die **ruhende Order AM** Level. Der Unterschied ist real. +**Die Karte zeigte nur die grüne Hälfte** — die Block-Grund-Zeile war beim +Entfernen der Gesamtempfehlung mit verschwunden. + +**Der Trade war beim Einstieg entschieden:** Widerstand **0,07×ATR = 0,008 $** +voraus, Stop **0,560 $ = 4,7×ATR_M5** zurück (CRV ~1:67), **MFE +0,016 $** — nie +im Plus. `ctx_atr_m5 = 0,12` = **der ATR-Floor**, der Markt stand still; der Stop +kam trotzdem aus ATR_M15 (bei manuellen Trades Absicht, M5 nur für Auto-Squeeze +seit 05.08.). −87 € = `0,568 $ × 1,77 Lots × ~87,6 €` — reine Geometrie, kein +Ausreißer. ⚠ Stunde 6 „vor DE-Open": autonom hätte der Bot dort nicht eröffnet. + +**Ist es ein Muster? Gemessen (77 Trades mit `ctx_*` seit 04.08., je Lot):** +| Block beim Einstieg | n | WR | EUR/Lot | +|---|---|---|---| +| **`entry_room`** | **29** | 55 % | **−11,45** | +| `min_conf` | 16 | 44 % | −16,08 | +| `htf_counter` | 10 | 80 % | +22,83 | +| `breakout_pending` | 9 | 89 % | +17,68 | + +Direkt auf der Raum-Achse (Abstand zum Level in der **eigenen** Trade-Richtung): +| | n | WR | EUR/Lot | 95-%-KI | +|---|---|---|---|---| +| Raum < 0,6 (geblockt) | 24 | **62 %** | −9,91 | [−29,4 … +6,6] ⚠ | +| Raum ≥ 0,6 (frei) | 52 | **62 %** | +3,24 | [−5,8 … +13,5] ⚠ | +⚠⚠ **Beide Intervalle enthalten die Null — mit n=76 ist das NICHT belegt**, nur +konsistent mit dem über 80k Bars gemessenen Gate. ✅ Die schärfste Zeile ist die +**identische Trefferquote 62 % gegen 62 %**: die Richtungswahl war gleich gut, der +Unterschied liegt **allein in der Asymmetrie** (Gewinn am Level gedeckelt, Verlust +läuft bis zum Stop) — die dokumentierte Kern-Leckage an neuer Stelle. + +✅ **GEBAUT: Zeile ③b** (`#m15-motor`, v=160) — zeigt den aktuellen `block_reason` +im Klartext **plus den Raum je Richtung** („③b Motor blockt: gegen den M30-Trend · +Raum ▲ 0,17×ATR ⚠ / ▼ 0,07×ATR ⚠ (Gate 0,6)"). Beide Richtungen, weil die Karte +bewusst keine vorgibt. +⚠⚠ **Das Status-Banner bleibt BEWUSST unverändert.** Es auf 🟡 zu ziehen wäre eine +Strategie-Änderung an einer gemessenen Gate-Kette — und die einzige Live-Zahl dafür +(−9,91 gegen +3,24 €/Lot) hat ein KI, das die Null enthält. **Sichtbar machen, +nicht bevormunden**; die Order-Buttons bleiben ohnehin bedienbar (Übersteuerungen +liefen gemessen 68 % WR). +✅ **`wave_rec.room_info(cur)` ist EINE Implementierung für Gate UND Anzeige** — +`_room_gate` rechnet jetzt darüber. Ein Nachbau wäre die Divergenz-Falle, die im +Projekt dreimal zugeschlagen hat (`sim_run`/`_ema_series` in ≥6 Skripten · der +nachgebaute Level-Cluster · das fünfte Exit-Modell). +⚠ Quelle ist trainingsgleich **M5**, nicht die M30-Level der Karte — `entry_room_atr +=0,6` ist auf M5 kalibriert (die drei divergierenden Level-Quellen sind Absicht). +✅ **Bitgleichheit bewiesen** (`tests/test_room_info.py`, 6 Tests): die alte Fassung +liegt wörtlich eingefroren im Test und läuft über **2.000 Zufallslagen × 3 Signale** +gegen die neue — identisch, und das Gate feuerte dabei >300× (ein Test, in dem es +nie feuert, belegt nichts). ⚠ Der gefährlichste Fall hat einen eigenen Test: +`frei` MUSS auf dem **ungerundeten** Abstand entschieden werden — bei 0,5951 würde +die gerundete 0,60 den Fall fälschlich durchlassen. + ## ⚠⚠ EXIT-ABSTAND AUS M15-ATR = FÄLLT DURCH (2026-08-08) Zweiter Teil der M15-Idee: M5-Einstieg behalten, nur den EXIT gröber führen. diff --git a/core/engine.py b/core/engine.py index fdaa44d..4cae6d9 100644 --- a/core/engine.py +++ b/core/engine.py @@ -3879,7 +3879,7 @@ class TradingEngine: # NICHT, `ruff F821` schon (4 Befunde, darunter `_m15` selbst) — genau # die Fehlerklasse, fuer die der Linter am 07.08. eingebaut wurde. _m15 = self._m15_setup(market, wave_snap, _cone, - self._alignment_cached()) + self._alignment_cached(), wave_signal) self._log_m15_state(_m15, market) self._log_cone_check(_cone) # Für den KI-Copiloten festhalten (2026-08-02) — er bekam bis dahin nur @@ -4281,7 +4281,8 @@ class TradingEngine: except Exception: return None - def _m15_setup(self, market, wave_snap, cone, alignment) -> dict | None: + def _m15_setup(self, market, wave_snap, cone, alignment, + wave_signal=None) -> dict | None: """SETUP-BEREITSCHAFT auf M15 — der Ersatz für die entfernte Gesamtempfehlung. ⚠⚠ BEWUSST KEINE RICHTUNGS-EMPFEHLUNG. Gemessen (2026-08-08) trägt auf @@ -4360,6 +4361,38 @@ class TradingEngine: "p_break": round(100 * p)}) out["zonen"] = zonen + # ── 3b) MOTOR-ZUSTAND + RAUM ───────────────────────────────── + # ⚠⚠ ANLASS 2026-08-10 (Trade #49671601, −87 €): die Karte zeigte + # GRÜN, während der Signal-Motor zu 76 % mit `entry_room` blockte. + # Das ist kein Widerspruch im Motor, sondern eine LÜCKE in der + # Karte: dieselbe geometrische Tatsache — „ein Level ist sehr nah" + # — wird von beiden Seiten mit UMGEKEHRTEM Vorzeichen gelesen: + # · Gate ③ (Karte): Level in Reichweite → man kann eine ruhende + # Order dort platzieren → GUT + # · entry_room (Motor): das Ziel-Level ist zu nah → Ertrag + # gedeckelt, Kosten fressen ihn → SCHLECHT + # (gemessen ØR −0,119/−0,067 · PF 0,42/0,66) + # Beide sind gemessen und beide haben recht — über VERSCHIEDENE + # Trades (Markt-Einstieg mit dem Level als Hindernis gegen ruhende + # Order AM Level). Die Karte verschwieg nur die zweite Hälfte. + # ⚠ REINE ANZEIGE. Es wird nichts geblockt und keine Richtung + # ausgegeben — der Raum kommt für BEIDE Richtungen, weil der Mensch + # in jede handeln kann. Hartes Blockieren ist im Projekt verworfen + # (Übersteuerungen des Users liefen gemessen 68 % WR). + # ⚠ `room_info` ist DIESELBE Funktion, die das Gate entscheidet — + # kein Nachbau (die Divergenz-Falle, dreimal dokumentiert). + wsig = wave_signal or {} + try: + raum = self.wave.room_info(bid) + except Exception: + raum = None + out["motor"] = { + "signal": wsig.get("signal"), + "block": wsig.get("block"), + "grund": (wsig.get("reasons") or [None])[0], + "conf_pct": wsig.get("conf_pct"), + "raum": raum} + # ── 4) REICHWEITE — der Kegel (kalibriert: 80 %-Band trifft 77 %) lv = (cone or {}).get("levels") or [] b80 = [x for x in lv if x.get("band") == 80] diff --git a/core/wave_rec.py b/core/wave_rec.py index 86e526b..a5d2ea3 100644 --- a/core/wave_rec.py +++ b/core/wave_rec.py @@ -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 — 79–82 % 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'} " diff --git a/tests/test_room_info.py b/tests/test_room_info.py new file mode 100644 index 0000000..8b220d7 --- /dev/null +++ b/tests/test_room_info.py @@ -0,0 +1,159 @@ +"""`room_info` darf das entry_room-Gate NICHT veraendert haben. + +ANLASS (2026-08-10): der Raum zum Gegenlevel lag bisher nur als Fliesstext in +`reasons` und war nicht auswertbar. Fuer die Anzeige in der M15-Karte wurde er +herausgezogen — als EINE Implementierung fuer Gate und Anzeige, nicht als zweite +Kopie (die Divergenz-Falle, die im Projekt dreimal zugeschlagen hat). + +⚠⚠ MIGRATIONS-REGEL: eine Refaktorierung ist nur dann eine, wenn das Verhalten +bitgenau gleich bleibt. Die ALTE Fassung liegt dafuer woertlich eingefroren hier +im Test (aus `git show`) und laeuft gegen die neue. + +⚠ Der gefaehrlichste Fall ist die RUNDUNG: `dist_atr` wird auf 2 Stellen +gerundet ausgewiesen. Wuerde `frei` auf dem gerundeten Wert entschieden, kippte +ein Grenzfall (0,5951 → 0,60 → faelschlich frei). Genau dafuer gibt es +`test_grenzfall_rundung` — ohne den waere der Fehler unsichtbar, weil er nur +etwa jeden hundertsten Fall trifft. +""" +from __future__ import annotations + +import random + +import pytest + +from core.wave_rec import WaveRecommender + + +class _Stub: + """Minimal-Recommender: nur die drei Felder, die beide Methoden lesen.""" + + def __init__(self, x, ph, pl, atr): + self._entry_room_atr = x + self._pb_levels = {"ph": ph, "pl": pl} + self._pb_feats = {"atr": atr} + + room_info = WaveRecommender.room_info + _room_gate = WaveRecommender._room_gate + + +# ── die ALTE Fassung, woertlich eingefroren (Stand vor dem Umbau) ───────── +def _alt_room_gate(s, rec, cur): + x = s._entry_room_atr + sig = rec.get("signal") + if x <= 0 or sig == "WARTEN": + return rec + lv = s._pb_levels or {} + pf = s._pb_feats or {} + atr5 = pf.get("atr") + if not atr5: + 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: + return rec + dist = (lvl - cur) * d / atr5 + if dist >= x: + return rec + 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'} " + f"{lvl:.2f} nur {dist:.2f}×ATR entfernt (Gate {x:.1f}) — " + f"Ertrag gedeckelt, Kosten fressen den Edge"]} + + +def _rec(sig): + return {"signal": sig, "conf_pct": 70, "score": 1.0, "setup": "WAVE", + "regime": None, "rsi": None, "reasons": ["Test"]} + + +def test_bitgleich_ueber_zufallsfaelle(): + """2.000 Zufallslagen — alte und neue Fassung muessen IDENTISCH entscheiden. + + ⚠ Die Level werden eng um den Kurs gestreut, damit die Gate-Grenze wirklich + oft getroffen wird. Liegen alle Level weit weg, blockt nie etwas und der + Test belegt nichts (dieselbe Falle wie bei `test_squeeze_scan`, wo erst die + Vola-Wechsel die Bedingungen durchlaufen liessen).""" + r = random.Random(4711) + getroffen = 0 + for _ in range(2000): + cur = 79.0 + r.uniform(-2, 2) + atr = r.choice([0.06, 0.12, 0.2, 0.35]) + x = r.choice([0.0, 0.3, 0.6, 1.0]) + ph = [round(cur + r.uniform(-1.0, 1.0), 3) for _ in range(r.randint(0, 5))] + pl = [round(cur + r.uniform(-1.0, 1.0), 3) for _ in range(r.randint(0, 5))] + s = _Stub(x, ph, pl, atr) + for sig in ("LONG", "SHORT", "WARTEN"): + neu = s._room_gate(_rec(sig), cur) + alt = _alt_room_gate(s, _rec(sig), cur) + assert neu == alt, f"Abweichung bei {sig} cur={cur} atr={atr} x={x}" + if neu.get("block") == "entry_room": + getroffen += 1 + # Ein Test, in dem das Gate nie feuert, belegt nichts. + assert getroffen > 300, f"Gate feuerte nur {getroffen}x — Testdaten zu lasch" + + +def test_grenzfall_rundung(): + """dist = 0,5951 liegt UNTER dem Gate 0,6 — gerundet waere es 0,60. + + Wuerde `frei` auf dem gerundeten Wert entschieden, liesse das Gate diesen + Fall faelschlich durch. Das ist der einzige Weg, wie der Umbau das Verhalten + haette veraendern koennen.""" + cur, atr, x = 79.0, 0.1, 0.6 + lvl = cur + 0.05951 # dist = 0,5951 + s = _Stub(x, [round(lvl, 5)], [], atr) + info = s.room_info(cur) + assert info["long"]["dist_atr"] == 0.6, "Anzeige rundet wie erwartet" + assert info["long"]["frei"] is False, "…aber die ENTSCHEIDUNG nutzt den Rohwert" + assert s._room_gate(_rec("LONG"), cur)["block"] == "entry_room" + + +# ── die neue Anzeige-Faehigkeit ────────────────────────────────────────── +def test_beide_richtungen_werden_geliefert(): + """Die Karte gibt keine Richtung vor → sie braucht den Raum fuer beide.""" + cur = 79.0 + s = _Stub(0.6, [79.30], [78.20], 0.2) + info = s.room_info(cur) + assert info["long"]["level"] == 79.30 + assert info["long"]["dist_atr"] == 1.5 and info["long"]["frei"] is True + assert info["short"]["level"] == 78.20 + assert info["short"]["dist_atr"] == 4.0 and info["short"]["frei"] is True + assert info["gate"] == 0.6 and info["atr_m5"] == 0.2 + + +def test_der_reale_fall_vom_10_08(): + """Trade #49671601: BUY, Widerstand praktisch AUF dem Kurs. + + ctx_dist_res_atr war −0,07 bei ATR_M5 0,12 (dem Floor) — der Widerstand lag + rund 0,008 $ voraus, der Stop 0,560 $ zurueck. Genau diese Lage muss die + Karte kuenftig ANZEIGEN.""" + cur, atr = 79.065, 0.12 + s = _Stub(0.6, [79.073], [78.99], atr) + info = s.room_info(cur) + assert info["long"]["frei"] is False # kein Raum nach oben + assert info["long"]["dist_atr"] < 0.1 + assert s._room_gate(_rec("LONG"), cur)["block"] == "entry_room" + + +def test_freie_bahn_ohne_level(): + """Kein Level voraus = bestes gemessenes Segment → frei, nicht geblockt.""" + s = _Stub(0.6, [], [], 0.2) + info = s.room_info(79.0) + assert info["long"] == {"level": None, "dist_atr": None, "frei": True} + assert s._room_gate(_rec("LONG"), 79.0).get("block") is None + + +def test_ohne_atr_kein_absturz(): + """Kaltstart: keine pb_feats → fail-open, kein Gate, keine Exception.""" + s = _Stub(0.6, [79.3], [78.2], None) + info = s.room_info(79.0) + assert info["atr_m5"] is None and info["long"] is None + assert s._room_gate(_rec("LONG"), 79.0).get("block") is None + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-v"])) diff --git a/web/app.js b/web/app.js index e7df4bd..0e4d2e3 100644 --- a/web/app.js +++ b/web/app.js @@ -256,6 +256,56 @@ function renderM15(s) { ? "④ erwartete Spanne · " + r.map(x => `${x.minutes}′ ${f(x.lo, 2)}–${f(x.hi, 2)}`).join(" · ") : "④ erwartete Spanne —"; } + // ── ③b RAUM + Motor-Zustand ────────────────────────────────────────── + // ⚠⚠ ANLASS: Trade #49671601 (10.08., −87 €). Die Karte stand auf GRÜN, + // während der Signal-Motor zu 76 % mit `entry_room` blockte — der Widerstand + // lag 0,07×ATR voraus, der Stop 4,7×ATR zurück. Die Karte prüfte, ob ein + // Level NAH ist, nie ob RAUM dahinter ist. Genau das steht jetzt hier. + // ⚠ Reine Anzeige: keine Richtung, kein Blockieren der Buttons. + const mo = s.motor || {}; + const rm = mo.raum || {}; + const moEl = $("m15-motor"); + const seite = (o, lbl) => { + if (!o) return null; + if (o.level == null) return `${lbl} frei`; + return `${lbl} ${o.dist_atr}×ATR${o.frei ? "" : " ⚠"}`; + }; + if (mo.block) { + // Der Motor blockt — das ist die Information, die gefehlt hat. + const gr = { + entry_room: "kein Raum zum Gegenlevel", + breakout_pending: "Bestätigung läuft noch", + min_conf: `Konfidenz ${mo.conf_pct ?? "?"} % zu schwach`, + htf_counter: "gegen den M30-Trend", + stretch: "überdehnt", + deadband: "Totband — kein Trend", + eia: "EIA-Blackout", + dead_hour: "gesperrte Stunde", + stale: "Daten veraltet", + no_data: "keine Daten" + }[mo.block] || mo.block; + moEl.className = "m15-item " + (mo.block === "entry_room" ? "warn" : ""); + moEl.textContent = `③b Motor blockt: ${gr}`; + } else if (mo.signal) { + moEl.className = "m15-item ok"; + moEl.textContent = `③b Motor frei · ${mo.signal}`; + } else { + moEl.className = "m15-item"; + moEl.textContent = "③b Motor —"; + } + const teile = [seite(rm.long, "▲"), seite(rm.short, "▼")].filter(Boolean); + if (teile.length) { + moEl.textContent += ` · Raum ${teile.join(" / ")}` + + (rm.gate ? ` (Gate ${rm.gate})` : ""); + } + moEl.title = "Was der Signal-Motor GERADE blockt, plus der Raum zum nächsten " + + "Gegenlevel je Richtung (M5 — dieselbe Rechnung wie das Gate selbst, kein " + + "Nachbau). Unter 0,6×ATR ist der Ertrag am Level gedeckelt: gemessen " + + "ØR −0,119/−0,067 bei PF 0,42/0,66 — Trefferquote hoch und trotzdem " + + "Verlust. ⚠ Gate ③ liest dieselbe Nähe als GUT (ruhende Order AM Level), " + + "entry_room als SCHLECHT (Markt-Einstieg mit dem Level als Hindernis). " + + "Beides stimmt — für verschiedene Trades. Blockt nichts."; + rEl.title = "Reine Geometrie auf dem Kegel: liegt das Ziel im 80-%-Band des " + "Horizonts? Das Ziel wird NUR in der von ② erlaubten Richtung gebildet — " + "ein rein geometrisches Ziel zeigte am 10.08. auf die verbotene Seite. " diff --git a/web/index.html b/web/index.html index 0c90408..78597d5 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -109,6 +109,13 @@
+ +