From 0208101707d6878f7d5d2633af765c2b2d4643a4 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Fri, 31 Jul 2026 12:37:54 +0200 Subject: [PATCH] TF-Churn-Fix: Breakout-Bestaetigung ueberlebt TF-Wechsel + Verweildauer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auslöser: User-Frage warum der Bot die Rally am 31.07. (82,5 -> 84,4 in 2 h) nicht gehandelt hat. Befund: in dem Fenster 99x WARTEN und 1x SHORT - und dieser SHORT wurde am Tief autonom eroeffnet (AUTOSIG, -26,31 EUR, per adverse15 geschlossen). Den LONG machte der User um 11:56 von Hand. Der WARTEN-Anteil ist strukturell zu hoch: Backtest-Erwartung (backtest_dist.py) ~43 % Live letzte 24 h 93,2 % Live letzte 7 Tage 88,0 % Live gesamt (93.599 Zeilen) 78,6 % MECHANISMUS (im Log belegt): _choose_tf setzt den Score einer TF hart auf 0,0, sobald sie ueberdehnt ist - genau das passiert M5 IM Trend. Real am 31.07.: M5 sprang zwischen 0,00 und 1,93, die TF wechselte 33x am Tag (Median-Abstand 5 min). Die 1,2x-Hysterese ist dagegen wirkungslos. Und set_timeframe() verwarf bei JEDEM Wechsel die laufende Breakout- Bestaetigung und verankerte sie beim aktuellen Kurs neu. Bei k=0,3 und ATR_M30~0,53 braucht sie ~0,16 $, der Kurs lief ~0,08 $ je 5 min -> die Bestaetigung konnte rechnerisch nie fertig werden. Gleicher Fehlertyp wie beim P(break)-Modell am selben Tag: backtest_breakout.py hat k=0,3 auf einer FESTEN Zeitebene validiert; live wandert sie - die Bedingung, unter der die Messung gilt, existiert im Betrieb nicht. FIX 1 (wave_rec.set_timeframe): _pend wird nicht mehr zurueckgesetzt. Der Anker gehoert zum Signal, nicht zur Zeitebene; bei Richtungswechsel verankert _confirm_breakout ohnehin neu. FIX 2 (engine._tf_loop): Mindest-Verweildauer [trading] tf_min_dwell_s=900. Verifiziert mit synthetischen Szenarien: Fix 1 - Bestaetigung ueberlebt M5->M30 und feuert LONG, Richtungswechsel verankert korrekt neu; Fix 2 - an der ECHTEN Score-Folge vom 31.07. nachgespielt: 4 Wechsel -> 2. NICHT backtestbar (Backtests laufen auf fester TF, das Churning existiert dort nicht). Begruendung ist "stellt die Bedingung her, unter der die Messung gilt", nicht "gemessen besser". Erfolgskontrolle = WARTEN-Anteil muss sich Richtung ~43 % bewegen. Bewusst nicht behoben: der 0,0-Einbruch des Scores bei Ueberdehnung selbst - das waere eine Aenderung der TF-Bewertung und damit messpflichtig. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 42 ++++++++++++++++++++++++++++++++++++++++++ core/engine.py | 24 +++++++++++++++++++++++- core/wave_rec.py | 15 ++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86b409d..dc85903 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -423,6 +423,48 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Richtungs-Trefferquote der „aktiv"-Anzeige ~50 % (Münzwurf), Ø-Vorlauf ~0×ATR über alle TF & H1-Regime → reiner Überdehnungs-Kontext, KEIN Signal.** (Die früheren „70 %" aus `backtest_bounce.py` waren exit-getrieben, nicht Richtungs-Vorhersage.) +- **⚠⚠ TF-CHURN-FIX (2026-07-31, zwei Eingriffe) — Ursache für „93 % WARTEN statt 43 %":** + Auslöser war die User-Frage „warum hat der Bot die Treppe nicht erkannt und long + eröffnet?" zur Rally am 31.07. (82,5 → 84,4 in 2 h). Befund: in dem Fenster gab es + **99× WARTEN und 1× SHORT** — und dieser SHORT wurde am Tief autonom eröffnet + (AUTOSIG, −26,31 €, per `adverse15` geschlossen); den LONG machte der User um 11:56 + von Hand gegen die Empfehlung. **Der WARTEN-Anteil live ist strukturell zu hoch:** + | | WARTEN | + |---|---| + | Backtest-Erwartung (`backtest_dist.py`) | ~43 % | + | Live letzte 24 h | **93,2 %** | + | Live letzte 7 Tage | 88,0 % | + | Live gesamt (93.599 Zeilen) | 78,6 % | + **MECHANISMUS (im Log belegt):** `_choose_tf` setzt den Score einer TF **hart auf + 0,0**, sobald sie überdehnt ist (`raw = sep if (sep>=deadband and stretch<=_STRETCH_MAX) + else 0.0`) — genau das passiert M5 IM Trend. Real am 31.07.: M5 sprang zwischen + **0,00 und 1,93**, die TF wechselte **33× am Tag, Median-Abstand 5 min**. Die + vorhandene 1,2×-Hysterese ist dagegen wirkungslos (gegen 0,0 kommt jeder Wert durch). + Und `set_timeframe()` verwarf bei JEDEM Wechsel die laufende Breakout-Bestätigung + (`_pend = None`) und verankerte sie beim AKTUELLEN Kurs neu. Bei `breakout_k=0,3` und + ATR_M30≈0,53 braucht sie ~0,16 $, der Kurs lief ~0,08 $ je 5 min → **die Bestätigung + konnte rechnerisch nie fertig werden.** Dazu: landet die Heuristik im Trend auf M30, + braucht das EMA-Kreuz dort Stunden — eine 2-h-Bewegung wird nie zum Signal. + ⚠ **Gleicher Fehlertyp wie beim P(break)-Modell am selben Tag:** `backtest_breakout.py` + hat k=0,3 auf einer **FESTEN** Zeitebene validiert; live wandert sie — die Bedingung, + unter der die Messung gilt, existiert im Betrieb nicht. + **FIX 1 (`wave_rec.set_timeframe`): `_pend` wird NICHT mehr zurückgesetzt.** Der Anker + gehört zum Signal, nicht zur Zeitebene; dreht die Richtung, verankert + `_confirm_breakout` ohnehin neu (`p.get("dir") != d`). Das Level behält den ATR der + alten TF — bewusst, eine Neuberechnung wäre wieder ein Reset. + **FIX 2 (`engine._tf_loop`): Mindest-Verweildauer `[trading] tf_min_dwell_s=900`** + (0 = aus). Gibt einer TF Zeit, ihr Signal überhaupt fertig zu bestätigen. + **Verifiziert mit synthetischen Szenarien:** Fix 1 — Bestätigung überlebt den Wechsel + M5→M30 und feuert LONG, Richtungswechsel verankert korrekt neu; Fix 2 — an der ECHTEN + Score-Folge vom 31.07. nachgespielt: 4 Wechsel → 2. + ⚠ **Ehrlich zur Beweislage:** das ist **NICHT backtestbar** — Backtests laufen + grundsätzlich auf einer festen TF, das Churning existiert dort per Konstruktion nicht. + Begründung ist also nicht „gemessen besser", sondern „stellt die Bedingung her, unter + der die vorhandene Messung gilt". **Erfolgskontrolle = der WARTEN-Anteil**: er muss + sich Richtung der gemessenen ~43 % bewegen (Abfrage s. Tabelle oben). + ⚠ **Nicht behoben (bewusst):** der 0,0-Einbruch des Scores bei Überdehnung selbst — + das wäre eine Änderung der TF-Bewertung und damit messpflichtig. Fix 2 halbiert das + Churning, verhindert aber nicht den ersten Absturz auf M30. - **Timeframe:** `[trading] tf_select=heuristic` → `engine._choose_tf` wählt jede Minute die TF mit klarstem Trend aus dem **Band `[tf_min … tf_max]`** (leichter Höher-TF-Bias). **Einstellung: `tf_min=M5`, `tf_max=M30`** (M1 raus: gemessen diff --git a/core/engine.py b/core/engine.py index 066e640..a3bfd7e 100644 --- a/core/engine.py +++ b/core/engine.py @@ -258,6 +258,14 @@ class TradingEngine: if self._tf_max not in _ATR_TF_MAP: self._tf_max = "H1" self._wave_tf_label = "M5" + # Mindest-Verweildauer der TF-Heuristik (Fix 2026-07-31, s. `_tf_loop`): + # verhindert das Hin-und-Her, wenn der Score einer TF im Trend auf 0,0 fällt + # (Überdehnung) und nach dem Rücksetzer zurückspringt. 0 = aus. + try: + self._tf_min_dwell_s = float(self.cfg["trading"].get("tf_min_dwell_s", "900")) + except (TypeError, ValueError): + self._tf_min_dwell_s = 900.0 + self._tf_last_switch = 0.0 self._last_rec: dict = {} self._agent_fail_cnt = 0 self._close_alert_ts = 0.0 # letzter CLOSE-Telegram-Push (5-min-Cooldown) @@ -1523,9 +1531,23 @@ class TradingEngine: if self.data.symbol: best, scores = self._choose_tf(self.data.symbol) cur = self._wave_tf_label + # ── Mindest-Verweildauer (Fix 2026-07-31) ──────────────────── + # Die 1,2×-Hysterese unten ist WIRKUNGSLOS, sobald der Score der + # aktuellen TF auf exakt 0,0 fällt — und genau das tut er, sobald + # sie überdehnt ist (`_choose_tf`). Im Trend kippt M5 dadurch + # regelmäßig raus (Score 0,00) und nach dem nächsten Rücksetzer + # wieder rein (1,93): real 33 Wechsel/Tag, Median-Abstand 5 min. + # Zusammen mit dem `_pend`-Reset (dort gefixt) hielt das die + # Breakout-Bestätigung dauerhaft offen → 93 % WARTEN live gegen + # ~43 % Backtest-Erwartung. Die Verweildauer gibt einer TF Zeit, + # ihr Signal überhaupt fertig zu bestätigen. 0 = aus. + now = time.time() + dwell_ok = (self._tf_min_dwell_s <= 0 or + (now - self._tf_last_switch) >= self._tf_min_dwell_s) # Hysterese: nur wechseln, wenn klar besser (kein Flattern) - if best and best != cur and \ + if dwell_ok and best and best != cur and \ scores.get(best, 0) > scores.get(cur, 0) * 1.2: + self._tf_last_switch = now self._apply_wave_tf(best, source=f"Heuristik {scores}") except Exception as e: log.warning(f"_tf_loop: {e}") diff --git a/core/wave_rec.py b/core/wave_rec.py index fb57d11..e82c44f 100644 --- a/core/wave_rec.py +++ b/core/wave_rec.py @@ -237,7 +237,20 @@ class WaveRecommender: self._tf = tf self._rec = None # alte Welle verwerfen — TF gewechselt self._ts = 0.0 - self._pend = None # Breakout-Bestätigung zurücksetzen + # ⚠ `_pend` wird BEWUSST NICHT MEHR zurückgesetzt (Fix 2026-07-31). + # Vorher: `self._pend = None`. Real beobachtet an der Rally vom 31.07. + # (82,5 → 84,4 in 2 h): die TF-Heuristik wechselte 33× am Tag (Median- + # Abstand 5 min), weil der Score einer TF hart auf 0,0 fällt, sobald sie + # überdehnt ist (`_choose_tf`: `raw = sep if … stretch <= _STRETCH_MAX + # else 0.0`) — genau das passiert M5 IM Trend. Jeder Wechsel verwarf die + # laufende Breakout-Bestätigung und verankerte sie beim AKTUELLEN Kurs neu. + # Bei k=0,3 und ATR_M30≈0,53 braucht sie ~0,16 $; der Kurs lief ~0,08 $ + # je 5 min → die Bestätigung konnte RECHNERISCH nie fertig werden. Folge: + # live 93 % WARTEN gegen ~43 % Backtest-Erwartung (`backtest_dist.py`). + # Der Anker gehört zum SIGNAL, nicht zur Zeitebene; dreht die Richtung, + # verankert `_confirm_breakout` ohnehin neu (`p.get("dir") != d`). + # ⚠ Das Level wurde mit dem ATR der alten TF gesetzt — bewusst behalten: + # eine Neuberechnung wäre wieder ein Reset und damit derselbe Fehler. log.info(f"Wellen-TF: {_TF_LABELS.get(tf, str(tf))}") # ── TU als Konfidenz-Faktor (kein harter Blocker mehr) ───────────────────