M15-Karte: Flacker-Ursache gemessen, Hysterese + EINE Quelle (v=165)

Die Karte wechselte den Zustand alle 12 Sekunden (~1.030/Tag) - der
Grund, warum ihre Selbstmessung so stark geclustert war.

Mein erster Verdacht (das Level im Wechsel-Schluessel) war falsch.
Ausgezaehlt ueber 3.525 Zeilen: status 32,9 %, lesart 32,7 %,
level allein nur 7,6 %. Das Level zu entfernen braechte 3.525 -> 3.248,
also nichts. Der Zustand selbst zappelt: gap <= 0,5 und das 45/55-Band
von P(break) sind stetige Groessen um ihre Schwelle.

Das Projekt hatte die Loesung schon (analyze_pbreak_flicker.py, 14.07.:
Hysterese +-5 Pp) - die Karte umging sie, weil sie P(break) selbst neu
rechnet. Jetzt dieselbe Hysterese auf beide Schwellen: Status gruen ab
0,50 / zurueck erst ueber 0,60; Lesart gerichtet ab 40/60 / zurueck
erst innerhalb 45/55. Simuliert: 64 % weniger Wechsel.

Zweiter, groesserer Befund: Status und Lesart wurden an DREI Stellen
unabhaengig gebildet und wichen bereits ab - der Logger kannte den
"kein Veto"-Fall des Frontends nicht, protokollierte also einen
anderen Zustand als angezeigt wurde. Jetzt eine Quelle: _m15_setup
liefert status/lesart/nah im Snapshot, Logger und Frontend lesen sie.

Die alten 3.525 Zeilen sind mit den neuen nicht vergleichbar; die
Selbstmessung beginnt faktisch neu (Stichtag 12.08.). Kein Verlust -
sie liess vorher ohnehin kein Urteil zu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-12 09:37:12 +02:00
co-authored by Claude Opus 5
parent e882d795a6
commit 5fb1d7e268
4 changed files with 111 additions and 14 deletions
+58 -11
View File
@@ -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 (4555 % = 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)