S/R-Level von M5 auf M30 umgestellt + P(break)-Modell nachtrainiert

User: "DIE S/R Linien liegen oft sehr nahe beieinander" -> Option 2 (beides
umstellen, Modell nachtrainieren) gewaehlt. Erst gemessen: backtest_level_tf.py,
80k M5-Bars, 2 Halbjahre, Trades bleiben M5, nur die Level-Quelle variiert.

Ergebnis (M30 gewinnt in BEIDEN Haelften):
  TF    AUC(H2)  Abstand   <1xATR   dR bei P<0.55 (H1/H2)
  M5     0.710   0.61xATR    71%     +6698  +7749   (bisher live)
  M15    0.725   1.16xATR    46%    +10138 +12071
  M30    0.721   1.95xATR    28%    +12424 +13047   <- gewaehlt

Die Beschwerde ist damit quantifiziert: 71% der M5-Level lagen naeher als 1xATR.

Umsetzung:
- wave_rec: _m30_hl gespeichert, neues _pb_levels30 (k=3, letzte 50 M30-Bars =
  trainingsgleich zum 300-M5-Lookback), im Snapshot.
- engine._draw_levels nutzt pb_levels30 (Fallback pb_levels beim Kaltstart);
  speist Chart, MQL5-CSV, Dashboard UND den P(break)-Auto-Close.
- _PB_MU/_PB_SD/_PB_W auf M30 nachtrainiert: out-of-sample validiert (H1->H2),
  dann auf allen 80k gefittet (34771 Touches, Kalibrierung 23/25, 38/36, 58/59,
  86/87). Merkmale bleiben M5. Alte M5-Werte als Kommentar (Rueckweg).
- NICHT mitveraendert: Entry-Raum-Gate nutzt weiter M5-pb_levels
  (entry_room_atr=0.6 ist darauf kalibriert).

Verifiziert live: R/S 84.617/83.722 = 2.9xATR Abstand (vorher 0.28xATR),
M30-Pivots 4/7 statt M5 27/31, keine Fehler im Log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-30 17:56:20 +02:00
co-authored by Claude Opus 4.8
parent 06b37a1474
commit b77b3d60f5
4 changed files with 356 additions and 8 deletions
+30 -7
View File
@@ -86,9 +86,18 @@ _STARTUP_GRACE_S = 60.0 # nach Bot-Start schließt der Bot 60 s lang KE
# auf H2 KALIBRIERT geprüft: AUC 0,71, predicted≈echt; `analyze_srwaves.py`).
# Merkmale (in Positionsrichtung): [mom6, mom3, mit-Trend(0/1), Level-Distanz/ATR].
# Standardisierung (mu/sd) + Gewichte (intercept + 4) aus `backtest_srclose_prob.py`.
_PB_MU = (0.7482, 0.7036, 0.9112, 0.7072)
_PB_SD = (1.1597, 0.9687, 0.2844, 0.6403)
_PB_W = (-0.5853, 0.1192, 0.7820, 0.1204, -0.2994) # [b0, mom6, mom3, wt, dist]
# P(break)-Modell — auf **M30-Level** trainiert (Umstellung 2026-07-30, gemessen
# `backtest_level_tf.py`): M30-Pivots schlagen die alten M5-Pivots in BEIDEN Hälften
# (AUC out-of-sample 0,721 vs 0,710 · R-Ertrag der Close-Regel ~2× · Level 1,95 statt
# 0,61×ATR entfernt = die „S/R-Linien kleben aneinander"-Beobachtung des Users).
# Ablauf: out-of-sample validiert (H1→H2), danach auf ALLEN 80k Bars nachtrainiert
# (34.771 Touches, Basisrate 39,4 %, Kalibrierung trifft: 23/25 · 38/36 · 58/59 · 86/87).
# ⚠ Die Merkmale bleiben M5-basiert (mom6/mom3 auf M5-Bars) — nur die LEVEL sind M30.
# Alte M5-Werte (falls je zurück): MU (0.7482,0.7036,0.9112,0.7072) ·
# SD (1.1597,0.9687,0.2844,0.6403) · W (-0.5853,0.1192,0.7820,0.1204,-0.2994).
_PB_MU = (1.6565, 1.4523, 0.9465, 1.9133)
_PB_SD = (2.5291, 2.4177, 0.2250, 2.1257)
_PB_W = (-0.3264, 0.4719, 1.4330, 0.0943, -0.1885) # [b0, mom6, mom3, wt, dist]
def _p_break(mom6: float, mom3: float, wt: float, dist: float) -> float:
"""P(Level-Durchbruch) ∈ (0,1) aus den 4 standardisierten Merkmalen."""
@@ -1815,9 +1824,21 @@ class TradingEngine:
}
def _draw_levels(self) -> dict:
"""S/R-Linien fürs ZEICHNEN (PWA-Charts + MQL5-CSV): echte Kursstruktur =
rohe M5-Pivots (`wave.pb_levels`, identisch zum P(break)-/Auto-Close-System),
geclustert (~0,5×ATR). Zeichnet **nur die NÄCHSTE je Seite** (2 Linien total,
"""S/R-Linien fürs ZEICHNEN (PWA-Charts + MQL5-CSV) UND für den
P(break)-Auto-Close: echte Kursstruktur = rohe **M30**-Pivots
(`wave.pb_levels30`), geclustert (~0,5×ATR).
⚠ **TF-Umstellung M5→M30 am 2026-07-30** (User: „die S/R-Linien liegen oft
sehr nahe beieinander"), gemessen `backtest_level_tf.py` über 80k M5-Bars in
2 Halbjahren — M30 gewinnt in BEIDEN: Level-Abstand **1,95 statt 0,61×ATR**
(nur 28 % statt 71 % näher als 1×ATR), AUC out-of-sample 0,721 vs 0,710,
R-Ertrag der Close-Regel **rund doppelt** (+12.424/+13.047 vs +6.698/+7.749).
Das P(break)-Modell (`_PB_MU/_SD/_W`) wurde dafür auf M30-Leveln NACHTRAINIERT.
Fallback auf die alten M5-Level (`pb_levels`), falls M30 noch nicht da ist.
⚠ NICHT betroffen: das **Entry-Raum-Gate** nutzt weiter `pb_levels` (M5) —
`entry_room_atr=0.6` ist darauf kalibriert.
Zeichnet **nur die NÄCHSTE je Seite** (2 Linien total,
deckungsgleich mit den Dashboard-Werten `sr-res`/`sr-sup`) — sonst tauchten
beim Kurslauf die weiter entfernten Linien laufend auf/ab (User-Feedback
2026-07-14). Bugfix davor: Linien kamen aus `_sr_levels` (stale [zones]-Config
@@ -1829,7 +1850,9 @@ class TradingEngine:
return dc[1]
try:
ws = self.wave.snapshot() or {}
lv = ws.get("pb_levels") or {}
# M30-Level (gemessen besser); M5 nur als Fallback beim Kaltstart, bis
# der erste M30-Turn-Fetch durch ist.
lv = ws.get("pb_levels30") or ws.get("pb_levels") or {}
atr = (ws.get("pb_feats") or {}).get("atr") or 0.2
bid = (self.data.snapshot() or {}).get("bid")
if not bid:
+24
View File
@@ -295,6 +295,14 @@ class WaveRecommender:
turn_data[_lbl] = (_c, _atr(_h, _l, _c))
if _lbl == "M5":
self._m5_hl = (_h, _l)
elif _lbl == "M30":
# M30-H/L für die S/R-LEVEL (Chart + P(break)-Auto-Close).
# Gemessen 2026-07-30 (`backtest_level_tf.py`): M30-Level
# schlagen M5 in BEIDEN Hälften (R-Ertrag ~2×, Level 1,95
# statt 0,61×ATR entfernt). Das M5-`_pb_levels` bleibt
# daneben bestehen — es speist das Entry-Raum-Gate, das
# auf M5 kalibriert ist (nicht ungemessen mitverändern!).
self._m30_hl = (_h, _l)
self._turn_due = time.time() + 15
# ── Schneller M5-Squeeze-Fetch (Variante A): entkoppelt von der 15-s-
# Ampel, kleiner 60-Bar-M5-Fetch ~alle 5 s → Ausbrüche werden ~3× schneller
@@ -377,6 +385,19 @@ class WaveRecommender:
self._pb_levels = {"ph": ph, "pl": pl}
# (Squeeze läuft jetzt auf dem schnellen ~5-s-M5-Fetch oben,
# nicht mehr hier auf dem 15-s-Ampel-Pfad — Variante A 2026-07-21.)
# ── S/R-LEVEL auf M30 (gemessen 2026-07-30, `backtest_level_tf.py`) ──
# Trainingsgleiches Fenster: der M5-Lookback von 300 Bars entspricht
# 50 M30-Bars → nur die letzten 50+K betrachten, sonst kämen Level aus
# einem viel längeren Zeitraum als im Training.
_h30, _l30 = getattr(self, "_m30_hl", (None, None))
if _h30 and len(_h30) >= 12:
_K = 3; _WIN = 50 + _K
_hs = _h30[-_WIN:]; _ls = _l30[-_WIN:]
ph30 = [round(_hs[j], 3) for j in range(_K, len(_hs) - _K)
if _hs[j] == max(_hs[j - _K:j + _K + 1])]
pl30 = [round(_ls[j], 3) for j in range(_K, len(_ls) - _K)
if _ls[j] == min(_ls[j - _K:j + _K + 1])]
self._pb_levels30 = {"ph": ph30, "pl": pl30}
# Multi-TF-Bounce: stärksten Status über M1/M5/M15/M30 wählen (H1 ist NUR
# für die Ampel im turn_data, NICHT im gemessen-neutralen Bounce → skip).
cands = []
@@ -396,6 +417,9 @@ class WaveRecommender:
snap["bounce"] = dict(self._last_bounce) # Multi-TF überschreibt Basis-TF
snap["pb_feats"] = dict(self._pb_feats) if getattr(self, "_pb_feats", None) else None
snap["pb_levels"] = dict(self._pb_levels) if getattr(self, "_pb_levels", None) else None
# M30-Level = Quelle für Chart-Linien UND P(break)-Auto-Close (gemessen besser);
# `pb_levels` (M5) bleibt für das Entry-Raum-Gate.
snap["pb_levels30"] = dict(self._pb_levels30) if getattr(self, "_pb_levels30", None) else None
snap["squeeze"] = dict(self._squeeze) if getattr(self, "_squeeze", None) else \
{"state": None, "dir": None, "level": None, "box_atr": None}
# ── Entry-Raum-Gate (gemessen `backtest_entryroom.py`, beide Hälften