diff --git a/CLAUDE.md b/CLAUDE.md index 1dd68f2..49ef42d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6161,6 +6161,49 @@ Neumessung. Sie bleiben unverändert und reproduzierbar; wo ihre Schlussfolgerun relevant wird, gehört sie live-treu **neu gerechnet** (Muster: `backtest_auto_signal_norev.py`). +## ⚠⚠ DAS FLACKERN DER M15-KARTE — Ursache gemessen, Hysterese eingebaut (2026-08-12) + +Die Karte wechselte den Zustand **alle 12 Sekunden** (~1.030/Tag). Das ist der +Grund, warum ihre Selbstmessung so stark geclustert war (roh 60 %, entkoppelt +53 %). + +⚠⚠ **MEIN ERSTER VERDACHT WAR FALSCH — und die Messung war schnell.** Ich hatte +das Level im Wechsel-Schlüssel (`round(level, 3)`) im Verdacht. Ausgezählt über +3.525 Zeilen: +| Was ändert sich zwischen zwei Zeilen? | Anteil | +|---|---| +| **`status`** | 32,9 % | +| **`lesart`** | 32,7 % | +| lesart+level | 20,3 % | +| **level allein** | **7,6 %** | +Das Level aus dem Schlüssel zu nehmen brächte 3.525 → 3.248 Zeilen — praktisch +nichts. **Der Zustand selbst zappelt**: `gap ≤ 0,5` und das 45/55-Band von +P(break) sind stetige Grössen, die um ihre Schwelle pendeln. + +✅ **Das Projekt hatte die Lösung schon** (`analyze_pbreak_flicker.py`, 14.07.: +Zeit-Glättung + **Hysterese ±5 Pp**). Die Karte umging sie, weil sie P(break) +selbst neu rechnet. Jetzt dieselbe Hysterese auf beide Schwellen: +· **Status**: grün ab `gap ≤ 0,50`, zurück auf gelb erst **über 0,60** +· **Lesart**: gerichtet erst ausserhalb **40/60**, zurück auf „unklar" erst + innerhalb **45/55** +Simuliert an 4.000 Ticks direkt auf den Schwellen: **3.084 → 1.103 Wechsel += 64 % weniger.** + +⚠⚠ **ZWEITER, GRÖSSERER BEFUND: Status und Lesart wurden an DREI Stellen +unabhängig gebildet** — Frontend, `_log_m15_state` und die Karte selbst. **Sie +wichen bereits ab**: der Logger kannte den „kein Veto"-Fall des Frontends nicht, +hat also einen anderen Zustand protokolliert als angezeigt wurde. Genau die +Divergenz-Falle, die im Projekt schon dreimal zugeschlagen hat (`sim_run` in ≥6 +Skripten · der nachgebaute Level-Cluster · das fünfte Exit-Modell). +✅ Jetzt **EINE Quelle**: `_m15_setup` liefert `status`/`lesart`/`nah` im +Snapshot, Logger und Frontend lesen sie. Anzeige und Messung können nicht mehr +auseinanderlaufen. + +⚠ **Die alten 3.525 Zeilen sind mit den neuen NICHT vergleichbar** — sie wurden +unter dem flackernden Regime geschrieben. Die Selbstmessung beginnt faktisch neu; +das ist kein Verlust, weil sie vorher ohnehin kein Urteil zuliess (KI enthielt +die 50). Stichtag: 12.08. + ## ⚠⚠ DIE M15-KARTE HAT SICH ZUM ERSTEN MAL SELBST GEMESSEN (2026-08-12) Anlass: User-Frage „könntest du mir auch eine **M5** · Setup-Bereitschaft bauen? diff --git a/core/engine.py b/core/engine.py index c3f3ecf..af96d88 100644 --- a/core/engine.py +++ b/core/engine.py @@ -3574,17 +3574,19 @@ class TradingEngine: bid = (market or {}).get("bid") if not atr or not bid: return - nah = min(zonen, key=lambda x: x["gap_atr15"]) if zonen else None - status = ("rot" if (kosten is not None and kosten >= 0.32) - else "gruen" if (nah and nah["gap_atr15"] <= 0.5) else "gelb") - # Lesart: dieselbe Regel wie im Frontend (45–55 % = Muenzwurf) - lesart = "unklar" - if nah and nah.get("p_break") is not None: - pb = nah["p_break"] - if pb < 45 or pb > 55: - bricht = pb > 55 - res = nah["seite"] == "res" - lesart = "auf" if (res if bricht else not res) else "ab" + # ⚠⚠ Status und Lesart werden NICHT mehr hier nachgerechnet + # (2026-08-12). Sie kommen aus `_m15_setup` — EINE Quelle fuer + # Anzeige UND Messung, mit Hysterese gegen das Schwellen-Flattern. + # Vorher gab es drei unabhaengige Fassungen, und sie wichen bereits + # ab: dieser Logger kannte den „kein Veto"-Fall des Frontends nicht. + nah = s.get("nah") + status = s.get("status") + lesart = s.get("lesart") + if not status: + return + # ⚠ Das Level bleibt im Schluessel: ein anderes Level IST ein anderes + # Setup. Gemessen ist es ohnehin nicht der Treiber (nur 7,6 % der + # Wechsel gingen allein darauf zurueck). schluessel = (status, lesart, None if not nah else round(nah["level"], 3)) if schluessel == getattr(self, "_m15_last_key", None): @@ -4661,6 +4663,51 @@ class TradingEngine: out["ziel_grund"] = (None if d_ok else "kein Veto — Richtung offen, kein Ziel bestimmbar") + # ── 4d) STATUS + LESART mit HYSTERESE — EINMAL berechnet ──────── + # ⚠⚠ ANLASS (2026-08-12): die Karte wechselte den Zustand **alle 12 + # Sekunden** (~1.030 Wechsel/Tag). Gemessen an den Logzeilen war + # NICHT das Level die Ursache (nur 7,6 %), sondern **Schwellen- + # Flattern**: `gap ≤ 0,5` und das 45/55-Band von P(break) sind + # stetige Groessen, die um die Schwelle pendeln. + # Folge: die Selbstmessung war massiv geclustert — roh 60 %, + # entkoppelt 53 %. + # ✅ Das Projekt hat genau dieses Problem am 14.07. schon geloest + # (`analyze_pbreak_flicker.py`): Zeit-Glaettung + **Hysterese + # ±5 Pp**. Die Karte umging sie, weil sie P(break) selbst neu + # rechnet. Hier ist dieselbe Hysterese, auf beide Schwellen. + # ⚠⚠ UND: Status/Lesart wurden an DREI Stellen unabhaengig gebildet + # (Frontend, `_log_m15_state`, hier) — sie wichen bereits ab (der + # Logger kannte den „kein Veto"-Fall nicht). Jetzt EINE Quelle; + # Anzeige und Messung koennen nicht mehr auseinanderlaufen. + hy = getattr(self, "_m15_hyst", None) or {} + nah_z = min(zonen, key=lambda x: x["gap_atr15"]) if zonen else None + # (a) Status: gruen ab gap ≤ 0,50 — zurueck auf gelb erst > 0,60 + war_gruen = hy.get("status") == "gruen" + if kr is not None and kr >= 0.32: + status = "rot" + elif not nah_z: + status = "gelb" + else: + g = nah_z["gap_atr15"] + status = "gruen" if (g <= (0.60 if war_gruen else 0.50)) else "gelb" + # (b) Lesart: gerichtet erst ausserhalb 40/60, zurueck auf „unklar" + # erst innerhalb 45/55 (die dokumentierten ±5 Pp). + lesart = "unklar" + if nah_z and nah_z.get("p_break") is not None: + pb = nah_z["p_break"] + war_ger = hy.get("lesart") in ("auf", "ab") + lo_s, hi_s = (45, 55) if war_ger else (40, 60) + if pb < lo_s or pb > hi_s: + bricht = pb > hi_s if not war_ger else pb > 55 + res = nah_z["seite"] == "res" + lesart = "auf" if (res if bricht else not res) else "ab" + elif war_ger: + lesart = hy.get("lesart") # im Band: alten Zustand halten + self._m15_hyst = {"status": status, "lesart": lesart} + out["status"] = status + out["lesart"] = lesart + out["nah"] = nah_z + # ── 4c) VOLA-REGIME als Slippage-HINWEIS (kein Gate, s. Docstring) out["vola"] = self._vola_regime(atr15) diff --git a/web/app.js b/web/app.js index 24262a7..d557c0a 100644 --- a/web/app.js +++ b/web/app.js @@ -189,9 +189,16 @@ function renderM15(s) { + slipTxt; // ── Gate 3: Level in Reichweite ────────────────────────────────────── + // ⚠⚠ `zOk` kommt seit 2026-08-12 aus dem BACKEND (`s.status`), nicht mehr aus + // einem eigenen Schwellenvergleich hier. Grund: die Karte wechselte den + // Zustand alle 12 Sekunden — Schwellen-Flattern an `gap ≤ 0,5`. Das Backend + // rechnet es EINMAL mit Hysterese (grün ab 0,50, zurück erst über 0,60) und + // liefert Status und Lesart mit; Anzeige und Selbstmessung können damit nicht + // mehr auseinanderlaufen (sie taten es bereits: der Logger kannte den + // „kein Veto"-Fall nicht). const z = (s.zonen || []); const nah = z.filter(x => x.gap_atr15 != null && x.gap_atr15 <= 0.5); - const zOk = nah.length > 0; + const zOk = s.status ? (s.status === "gruen") : (nah.length > 0); $("m15-zone").className = "m15-item " + (z.length ? (zOk ? "ok" : "warn") : ""); // ⚠⚠ RICHTUNGS-LESART, aber BEDINGT auf das Level. Eine FREIE Prognose ist // gemessen unmoeglich (`analyze_reversal.py`: AUC 0,499–0,509 oos und diff --git a/web/index.html b/web/index.html index a21e124..a391173 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@