diff --git a/CLAUDE.md b/CLAUDE.md index 6147723..c6112cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -439,10 +439,18 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: **Winkel-Richtung, gegen die EMA** (überverkauft + Winkel auf → LONG; überkauft + Winkel ab → SHORT). Setup `WAVE_REV_*`, Grund „🔄 Reversal"; hebt Anti-Überdehnung **und** M30-Filter bewusst auf. **Schwelle 3,5→3,0 gesenkt** (User wollte mehr - Bounces; `backtest_bounce.py`): Ø-R **+0,185**, PF 1,35, Treffer 70 %, Worst −2×ATR - (SL-gedeckelt), ~1,5× mehr Bounces als 3,5 — profitabel, aber **schwächer als - Trend** & **regime-anfällig** (trägt in Seitwärts/Mean-Reversion, blutet in starken - Trends). Nachrangiges, markiertes Setup. **Bounce-Status — Multi-TF** im Wave- + Bounces; `backtest_bounce.py`): Ø-R +0,185, PF 1,35, Treffer 70 %, Worst −2×ATR + (SL-gedeckelt), ~1,5× mehr Bounces als 3,5. + ⚠⚠ **DIESE ZAHLEN GELTEN NICHT MEHR** (Legacy-Recheck 2026-07-31, hier eingetragen + am 2026-08-02): `backtest_bounce.py` nutzt den **vereinfachten Exit OHNE Kosten** + (`LEGACY_SIMPLE`: kein Breakeven, kein Lock, kein Time-Stop). Mit dem **echten** + Exit + Echtkosten liegt das Setup bei ~**Breakeven** (PF 0,93/1,06) — es ist + **kein +0,185-Ertragsbringer**. Was HÄLT, ist die *relative* Aussage: Reversal + schlägt Trend in beiden Hälften (+0,140/+0,102). Deshalb bleibt es ein sinnvolles + **nachrangiges, markiertes Zweitsignal** (so ist es live auch eingebaut), aber + nicht mehr als das. Details im Abschnitt „LEGACY-RECHECK". + Weiterhin gültig: **schwächer als Trend** & **regime-anfällig** (trägt in + Seitwärts/Mean-Reversion, blutet in starken Trends). Nachrangiges, markiertes Setup. **Bounce-Status — Multi-TF** im Wave- Snapshot (`wave.bounce = {state, dir, tf}`): scannt **M1/M5/M15/M30** (`_bounce_one`, Aggregat in `refresh_market`, ~15 s-Throttle via TF-Ampel-Fetch), nimmt den **stärksten** (aktiv>erwartet, dann größte Überdehnung). **`expected`** = überdehnt @@ -2531,6 +2539,54 @@ konstant; (d) es gab **keine Telemetrie über die Entscheidung selbst**. in ~20 min gefunden; die Trefferquote spricht für weitere. `analyze_divergence.py` regelmäßig laufen lassen (Kandidat für den Wochenreport). +## Gesamtprüfung 2026-08-02 (nach `docs/review-prompt.md`) + +Erster vollständiger Durchlauf des Review-Prompts. **Keine Strategie-Änderung** — alle +Fixes betreffen Telemetrie, Doku und toten Code. + +**Behoben:** +1. **`analyze_divergence.py` kannte keine Epochen** — und war damit selbst + driftanfällig. D0 mischte Vorhersagen des ALTEN und des am 31.07. nachtrainierten + P(break)-Modells und meldete dessen Fehlkalibrierung als aktuellen Alarm (das neue + Modell hatte **n=0**, weil der Markt seit Fr 22:55 zu ist); B las die + Prä-Migrations-`NULL`s von `block_reason` als blinden Fleck („None 66,8 %"); C + druckte bei 0 Zeilen ein „OK", obwohl es fehlende Daten waren. Neu: `_EPOCHS` + + `_clamp()` schneiden jede Auswertung auf den Beginn ihrer eigenen Datenlage. + ⚠ **Beim Nachtrainieren eines Modells das Datum in `_EPOCHS` mitziehen** — sonst + schlägt der Wächter wochenlang Fehlalarm, und ein Wächter, der immer warnt, wird + ignoriert. +2. **Konsens-Pfeil: doppelte Arbeit für eine unsichtbare Linie.** Der MQL5-Export + rechnete im ~5-s-Takt ein **komplettes zweites `_verdict()`** für `AR;K` — der + Indikator verwirft die Zeile aber seit v1.33 per Default (`InpShowConsensus=false`). + Neu `[trading] export_consensus_arrow` (Default **false**, spiegelt den Indikator). + Nebeneffekt beseitigt: das zweite Verdict konnte vom Verdict im Snapshot abweichen. +3. **`/api/autosqueeze` loggt jetzt die Herkunft** (`[WEB] …`) wie `/api/autosignal`. + Vorher war bei einem Zustandswechsel nicht belegbar, ob Dashboard oder Code — + bei einem Schalter für autonome Echtgeld-Einstiege zu wenig. +4. **Reversal-Kennzahl an der Fundstelle korrigiert.** „ØR +0,185 · PF 1,35 · 70 % + Treffer" stand unverändert in der Reversal-Sektion, die Widerlegung 2 000 Zeilen + später im Legacy-Recheck. Wer die Sektion liest, sah die Korrektur nie. +5. Zwei tote `import datetime` in `core/notify.py` entfernt (beide Funktionen nutzen + `_time`), funktional nachgetestet. + +**Geprüft und sauber:** 0 fehlende Frontend-IDs (von 86) · nur 2 Config-Schlüssel ohne +Leser (beide dokumentiert dormant) · Snapshot-Median **13 ms**, alle DB-Abfragen <13 ms, +Indizes vorhanden → **keine Performance-Maßnahme nötig** · 124 Datei- und 83 +Funktionsreferenzen in CLAUDE.md stimmen · Asset- und Indikator-Version konsistent. + +**Zwischenverdacht zurückgezogen:** „`block_reason` erklärt nur 33 % der WARTEN" war +ein reines Migrations-Artefakt (Spalte kam am 31.07.); seit 01.08. **100 % Abdeckung**. + +**Offen (nicht behoben, bewusst):** +- **`auto_flip_close=true` läuft ohne jede UI-Anzeige.** Ein Exit, der gemessen in + BEIDEN Hälften Geld kostet (−92/−160 R), schließt echte Trades; `flip_close`, + `flip_close_min_r` und `flip_close_count` haben keinen Frontend-Konsumenten. Nur + Log + Telegram. Entweder anzeigen oder abschalten — Entscheidung des Users. +- **`breakout_k=0,3` ruht weiter auf dem `LEGACY_BE10`-Exit** (Breakeven 1,0 statt + live 1,3). Der Legacy-Recheck hat EIA und Dead-Hours nachgerechnet, `breakout_k` + nicht. Kandidat für den nächsten Recheck. +- Die 5 Zähler-Snapshot-Felder (`close_alert_count` usw.) werden von nichts abgefragt. + ## Ausstehende Messungen — Erinnerung per Timer `measurement_reminder.py` + Windows-Task **`OilMeasurementReminder`** (täglich 18:00) diff --git a/analyze_divergence.py b/analyze_divergence.py index 3ec28a3..218a3b1 100644 --- a/analyze_divergence.py +++ b/analyze_divergence.py @@ -38,6 +38,36 @@ LOG = "oil_widget.log" _EXP_WAIT = 43.0 # backtest_dist.py: ~57 % LONG/SHORT, ~43 % WARTEN _EXP_WAIT_TOL = 15.0 # ab dieser Abweichung wird geflaggt (Pp) +# ── EPOCHEN ─────────────────────────────────────────────────────────────────── +# ⚠ Ergänzt 2026-08-02 (Review-Durchgang 2). Der Wächter rechnete über ein starres +# Zeitfenster und wusste NICHT, ab wann ein Modell bzw. eine Telemetrie gilt. +# Dadurch meldete er zwei FEHLALARME, die beide nur Artefakte des Fensters waren: +# · D0 mischte Vorhersagen des ALTEN und des am 31.07. nachtrainierten +# P(break)-Modells (Alarm „Δ +15 Pp" stammte zu 100 % aus dem alten; das neue +# hatte n=0, weil der Markt seit Fr 22:55 zu ist). +# · B zeigte „block_reason: None 66,8 %" — die Spalte existiert aber erst seit +# dem 31.07., davor ist NULL schlicht „nicht erhoben", kein blinder Fleck. +# Ein Wächter, der nach jedem Nachtraining wochenlang Alarm schlägt, wird +# ignoriert — und dann fängt er den echten Fall auch nicht mehr. +# Beim Ändern eines Modells/einer Telemetrie hier das Datum mitziehen. +_EPOCHS = { + # P(break) auf live-spiegelnder Stichprobe neu gefittet (neue _PB_MU/_SD/_W) + "pbreak_model": "2026-07-31", + # `recommendations.block_reason` eingeführt (DB-Migration) + "block_reason": "2026-07-31", +} + + +def _epoch_ts(name: str) -> float: + return datetime.fromisoformat(_EPOCHS[name]).timestamp() + + +def _clamp(t0: float, epoch: str) -> tuple[float, bool]: + """→ (effektiver Startzeitpunkt, wurde beschnitten?). Sorgt dafür, dass eine + Auswertung nie über den Beginn ihrer eigenen Epoche hinausreicht.""" + e = _epoch_ts(epoch) + return (max(t0, e), e > t0) + def _flag(ok: bool) -> str: return "OK " if ok else "⚠ " @@ -65,8 +95,16 @@ def sec_a(c, t0): def sec_b(c, t0): print("\n" + "=" * 84) - print(" B) WELCHES GATE BLOCKT? (block_reason, seit 2026-07-31)") + print(f" B) WELCHES GATE BLOCKT? (block_reason, seit {_EPOCHS['block_reason']})") print("=" * 84) + # ⚠ Fenster auf die Epoche beschneiden: vor der Migration ist block_reason + # NULL, weil die Spalte nicht existierte — nicht, weil kein Gate gegriffen + # hätte. Ungeschnitten meldete der Report „None 66,8 %" und legte damit einen + # blinden Fleck nahe, den es nicht gibt (nachgeprüft: seit 01.08. 100 % Abdeckung). + t0, cut = _clamp(t0, "block_reason") + if cut: + print(f" (Fenster auf den Beginn der Telemetrie gekürzt — davor wurde der " + f"Grund nicht erhoben)") try: c.execute("SELECT block_reason, COUNT(*) FROM recommendations " "WHERE timestamp>=? AND signal='WARTEN' GROUP BY block_reason " @@ -111,7 +149,13 @@ def sec_c(t0): except FileNotFoundError: print(" kein Log gefunden"); return if not sw: - print(" keine TF-Wechsel im Zeitraum OK"); return + # ⚠ Kein „OK" mehr (2026-08-02): 0 Wechsel heißt bei geschlossenem Markt + # schlicht „keine Daten". Das als Entwarnung zu drucken, wäre genau die + # Sorte stiller Fehlschluss, die dieser Report aufdecken soll. + print(" 0 TF-Wechsel gefunden — das ist KEINE Entwarnung, sondern") + print(" möglicherweise fehlende Evidenz (Markt zu / Log rotiert / Bot aus).") + print(" Aussagekräftig ist dieser Abschnitt nur über Handelszeiten.") + return days = max(1.0, (datetime.now().timestamp() - t0) / 86400) gaps = sorted((sw[i][0] - sw[i - 1][0]).total_seconds() for i in range(1, len(sw))) med = gaps[len(gaps) // 2] / 60 if gaps else 0 @@ -137,12 +181,24 @@ def sec_d0(c, t0): print("\n" + "=" * 84) print(" D0) MODELL-KALIBRIERUNG — sagt es voraus, was eintritt?") print("=" * 84) + # ⚠ NUR Vorhersagen des AKTUELLEN Modells auswerten (Epochen-Schnitt, 2026-08-02). + # Ungeschnitten mischte diese Sektion die Vorhersagen des alten und des am + # 31.07. nachtrainierten Modells und meldete dessen Fehlkalibrierung als + # aktuellen Alarm — obwohl das neue Modell noch gar keine ausgewertete + # Vorhersage hatte. Nach einem Nachtraining hätte der Wächter so wochenlang + # falsch Alarm geschlagen, bis die Altdaten aus dem Fenster gerollt wären. + t0, cut = _clamp(t0, "pbreak_model") + if cut: + print(f" (nur Vorhersagen seit dem Nachtraining am {_EPOCHS['pbreak_model']})") c.execute("""SELECT AVG(p_break), AVG(CASE WHEN outcome='break' THEN 100.0 ELSE 0 END), COUNT(*) FROM pbreak_predictions WHERE ts>=? AND outcome IS NOT NULL""", (t0,)) pm, rm, n = c.fetchone() if not n or n < 30: - print(f" nur {n or 0} ausgewertete Vorhersagen — zu wenig"); return + print(f" nur {n or 0} ausgewertete Vorhersagen seit dem Nachtraining — zu wenig.") + print(f" (Das ist KEIN Entwarnungssignal, sondern fehlende Evidenz. " + f"Fälligkeit: Messung `pbreak_accuracy_v2`, ≥800 Vorhersagen.)") + return gap = (rm or 0) - (pm or 0) print(f" n={n} Ø-Vorhersage {pm:.1f} % echte Rate {rm:.1f} % " f"Δ {gap:+.1f} Pp {_flag(abs(gap) <= 8)}") diff --git a/core/engine.py b/core/engine.py index ecd89cb..abf54e9 100644 --- a/core/engine.py +++ b/core/engine.py @@ -385,6 +385,10 @@ class TradingEngine: self._export_mql5 = self.cfg["trading"].get("export_mql5_levels", "true").lower() == "true" # Kegel im MT5-Chart: per Default AUS, seit die Pfeile ihn ersetzen (2026-07-31). self._export_cone = self.cfg["trading"].get("export_cone", "false").lower() == "true" + # Konsens-Pfeil (AR;K) — Default AUS, spiegelt `InpShowConsensus=false` im + # Indikator. Bei `true` wird je Export ein zweites `_verdict()` gerechnet. + self._export_consensus_arrow = self.cfg["trading"].get( + "export_consensus_arrow", "false").lower() == "true" self._draw_last = {"r": None, "s": None} # letzte gezeichnete S/R (Hysterese) try: # WHT-Quellensteuer (Broker behält % je GEWINN ein) → Netto-Anzeige self._wht_pct = max(0.0, float(self.cfg["trading"].get("wht_pct", "0") or 0)) @@ -2368,15 +2372,25 @@ class TradingEngine: f"{round(float(_sq['level']) + _sd * 2.0 * _atr5, 3)};" f"Squeeze {_sq['dir']}") # (K) Konsens — Länge proportional zum Bias-Betrag (max 2×ATR) - _vd = self._verdict(self.data.snapshot(), self.wave.signal(), - self.wave.snapshot(), None, - self.agent.snapshot(), self.elliott.snapshot(), - self.patterns.snapshot()) - _b = float(_vd.get("bias") or 0.0) - if abs(_b) >= 0.2: - lines.append(f"AR;K;{round(_bid, 3)};" - f"{round(_bid + (1 if _b > 0 else -1) * min(abs(_b), 1.0) * 2.0 * _atr5, 3)};" - f"Konsens {_b:+.2f}") + # ⚠ NUR berechnen, wenn der Pfeil auch gezeichnet wird + # (Review 2026-08-02): Der Indikator verwirft `AR;K` seit v1.33 + # per Default (`InpShowConsensus=false` → `continue`), der Bot + # rechnete dafür aber im ~5-s-Export-Takt ein KOMPLETTES zweites + # `_verdict()` — Arbeit für eine Linie, die niemand zeichnet, plus + # das Risiko, dass dieses zweite Verdict vom Verdict im Snapshot + # abweicht. Der Schalter spiegelt jetzt den Indikator-Default; + # wer `InpShowConsensus=true` setzt, setzt hier `export_consensus + # _arrow=true` dazu. + if self._export_consensus_arrow: + _vd = self._verdict(self.data.snapshot(), self.wave.signal(), + self.wave.snapshot(), None, + self.agent.snapshot(), self.elliott.snapshot(), + self.patterns.snapshot()) + _b = float(_vd.get("bias") or 0.0) + if abs(_b) >= 0.2: + lines.append(f"AR;K;{round(_bid, 3)};" + f"{round(_bid + (1 if _b > 0 else -1) * min(abs(_b), 1.0) * 2.0 * _atr5, 3)};" + f"Konsens {_b:+.2f}") except Exception: pass # ── HYPERLIQUID-ERSATZKURS bei geschlossenem Markt (User 2026-08-01): diff --git a/core/notify.py b/core/notify.py index 1c11a90..2c1e1da 100644 --- a/core/notify.py +++ b/core/notify.py @@ -72,7 +72,6 @@ def build_trade_open_message( entry_ts: int | None = None, ) -> str: """Formatiert die Telegram-Nachricht für einen Trade-Einstieg.""" - import datetime dir_str = "LONG" if (direction or "").upper() in ("BUY", "LONG") else "SHORT" emoji = "\U0001f7e2" if dir_str == "LONG" else "\U0001f534" setup_str = _SETUP_LABELS.get(setup or "", setup or "—") @@ -101,7 +100,6 @@ def build_trade_close_message( symbol: str | None = None, ) -> str: """Formatiert die Telegram-Nachricht für einen Trade-Abschluss.""" - import datetime pnl_net = pnl + (commission or 0.0) win = pnl_net >= 0 sign = "+" if win else "" diff --git a/server.py b/server.py index ff53caa..ebbd5ba 100644 --- a/server.py +++ b/server.py @@ -334,6 +334,13 @@ async def toggle_autosqueeze(x_auth_token: str | None = Header(default=None)): _require_auth(x_auth_token) eng = app.state.engine state = eng.set_auto_squeeze(not eng._auto_squeeze) + # ⚠ Herkunft mitloggen (Fix 2026-08-02, Review-Durchgang 1): `set_auto_squeeze` + # loggt nur „Auto-Squeeze-Entry AN/AUS" ohne Quelle — beim Nachvollziehen eines + # Zustandswechsels war dadurch nicht feststellbar, ob der Dashboard-Button oder + # Code dahinterstand (real: BRK stand auf AUS und es kostete mehrere Schritte, + # das als Nutzeraktion zu belegen). `/api/autosignal` macht das längst richtig — + # bei einem Schalter für autonome ECHTGELD-Einstiege gehört die Herkunft ins Log. + get_logger("server").info(f"[WEB] Auto-Squeeze-Entry {'AN' if state else 'AUS'}") return {"ok": True, "enabled": state}