diff --git a/CLAUDE.md b/CLAUDE.md index c6112cc..a077c0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2539,6 +2539,63 @@ 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). +## KI-Copilot: warum er neutral ist, und was 2026-08-02 repariert wurde + +**User-Frage:** „Der Bias ist fast immer neutral — können wir konkretere Empfehlungen +bekommen? Weitere Quellen (Kerzen, Muster)?" + +**Diagnose — es ist NICHT der Prompt, es ist der Provider.** Der Bruch ist exakt +datierbar (`recommendations.ai_sentiment` je Tag): bis **19.07.** war der Copilot zu +**42–90 %** gerichtet, ab **21.07.** nur noch zu **1,5–9,3 %**. Dazwischen liegt der +Wechsel auf **DeepSeek (2026-07-20)**. Der Prompt war die ganze Zeit unverändert. +⚠ Und er hedged **aus korrekter Schlussfolgerung**, nicht aus Schwäche: die +Begründungen sind konkret und zutreffend, und er bekommt zu ~90 % ein WARTEN-Signal, +einen negativen Track-Record UND die ausdrückliche Prompt-Anweisung, genau dann +NEUTRAL zu empfehlen. Er tut, was dort steht. + +**Sind seine gerichteten Rufe etwas wert? Unentschieden — die Stichprobe trägt nicht.** +Forward-Return über `verdict_votes` × `candles_m1`: KI-LONG n=286 → **+0,29 $** in +Richtung / 76 % Treffer (60 min), KI-SHORT n=93 → −0,02 $ / 55 %. ⚠ **Das ist KEIN +Beleg:** die 286 Stimmen sind **20 Episoden an 5 Tagen** (Minutentakt-Logger), die 93 +sind 16 Episoden — effektiv n≈20 gegen n≈16, alles in einem Regime. Dasselbe +Cluster-Muster, an dem ORB gescheitert ist. **Deshalb bewusst NICHT am Prompt gedreht, +um mehr Meinung zu erzwingen** — das würde eine korrekte Schlussfolgerung überschreiben +und einer unbelegten Stimme mehr Gewicht geben. Verdict-Gewicht bleibt 1,0 bei +gerichteter Aussage, 0 bei NEUTRAL. + +**Zwei echte Prompt-FEHLER behoben (Korrektheit, keine Meinungsmache):** +1. `_SYS` beschrieb das Wellen-Signal als **„ATR-ZigZag"** — es ist seit Langem + **EMA12 vs. EMA50**; ZigZag wurde verworfen (~0 Edge). Das Modell missverstand also + die Natur des Hauptsignals. +2. `_SYS` nannte **„TradersUnion-Tachos"** als Input — TU ist seit 2026-07-19 gar nicht + mehr im Copilot-Kontext (`_tool_market`). Das Modell wurde auf Daten hingewiesen, + die es nie sieht. + +**Die zwei kalibrierten Größen ergänzt.** Befund beim Nachsehen: `_tool_market` gab dem +Copiloten **nur Wellen-Signal + Session** — er war das schlechtest informierte Modul im +System und bekam ausgerechnet die einzigen beiden Komponenten nicht, deren Kalibrierung +out-of-sample nachgewiesen ist. Neu im Kontext: **`p_break_target` / `p_break_stop`** +(AUC 0,65 bzw. 0,68–0,72) und **`cone`** (80 %-Bänder, mit der REAL gemessenen Abdeckung +77 %, nicht dem Nennwert). Dazu eine Lese-Anleitung im Prompt — ohne sie liest ein LLM +„P(break) 30 %" als schwaches Signal statt als klare Aussage „Level hält mit 70 %"; +45–55 % ist ausdrücklich als Münzwurf markiert. +⚠ **Zwei Fallen beim Bau, beide an Live-Daten korrigiert:** (a) `p_break` steht im +Snapshot **bereits in Prozent** (3 = 3 %, deckungsgleich mit der Chart-Zeile „97 % +Abprall") — die zuerst gebaute ×100-Umrechnung hätte 300 erzeugt; (b) die Engine-Felder +`_last_sr_close_hint`/`_last_stop_approach`/`_last_cone` und die `engine`-Referenz im +Agent **existierten gar nicht** — der Fail-safe hätte still nichts geliefert. Beides +verdrahtet, der Snapshot rechnet die Werte ohnehin und weist sie jetzt nur zu (kein +zweites Rechnen, anders als beim Konsens-Pfeil). Mit Stub-Test verifiziert (Werte +unverändert durchgereicht, nur 80 %-Bänder, ohne Engine kein Absturz). + +⚠ **Kerzen/Muster bewusst NICHT eingespeist.** Ein LLM wendet darauf die Lehrbuch-Lesart +an — und die ist hier gemessen **invertiert**: „langer Körper im Trend = Fortsetzung" +→ −0,132/−0,042 (Klimax, läuft dagegen); „2× Volumen = echt" → −0,117/−0,204 (monoton +falsch herum); Muster-Kursziel trifft nur zu 13–38 %. Roh eingespeist würden sie den +Copiloten aktiv verschlechtern. Offener Kandidat: sie MIT der gemessenen Lesart +mitgeben („langer Docht oben; gemessen läuft der Kurs danach eher GEGEN die Kerze") — +noch nicht gebaut. + ## Gesamtprüfung 2026-08-02 (nach `docs/review-prompt.md`) Erster vollständiger Durchlauf des Review-Prompts. **Keine Strategie-Änderung** — alle diff --git a/core/agent.py b/core/agent.py index 68020d4..a88162d 100644 --- a/core/agent.py +++ b/core/agent.py @@ -50,8 +50,16 @@ _OPENAI_URL = "https://api.openai.com/v1/chat/completions" _SYS = ( "Du bist ein nüchterner Trading-Copilot für WTI-Rohöl (Intraday-Scalping). " "Du bekommst den aktuellen Systemzustand eines automatischen Handels-Bots " - "(Wellen-Signal als ATR-ZigZag, TradersUnion-Tachos, offene Position, " - "Konto, Track-Record, News). Beurteile die Lage knapp und ehrlich — keine " + # ⚠ KORRIGIERT 2026-08-02: hier stand „Wellen-Signal als ATR-ZigZag, + # TradersUnion-Tachos". BEIDES war falsch und hat das Modell in die Irre + # geführt: (a) die Richtung kommt seit Langem aus EMA12 vs. EMA50 — ZigZag + # wurde verworfen, weil es ~0 Edge hatte; (b) TradersUnion ist seit + # 2026-07-19 gar nicht mehr im Copilot-Kontext. Das Modell wurde also auf + # Daten hingewiesen, die es nie sieht, und hat die Natur des Hauptsignals + # missverstanden. + "(Wellen-Signal aus EMA12 gegen EMA50 mit Totband, Durchbruch-" + "Wahrscheinlichkeit am nächsten S/R-Level, erwartete Kursspanne, offene " + "Position, Konto, Track-Record, News). Beurteile die Lage knapp und ehrlich — keine " "Garantien, keine Hype-Sprache. Wenn die Signale widersprüchlich oder dünn " "sind, sag das klar und empfiehl NEUTRAL/abwarten.\n\n" "WICHTIG — nutze den Track-Record zur Kalibrierung: 'current_setup_history' " @@ -62,6 +70,28 @@ _SYS = ( "Live-Signal stark wirkt. Hat es sich bewährt, darf das deine Konfidenz " "stützen. Folge dem Signal nicht blind gegen eine klar negative Historie; " "nenne den Bezug in 'reasoning' kurz (z.B. Setup-Trefferquote).\n\n" + # ── Die zwei EINZIGEN kalibriert geprüften Größen des Projekts ──────────── + # ⚠ Ergänzt 2026-08-02. Der Copilot sah bis dahin NUR das Wellen-Signal und + # die Session — er war damit das schlechtest informierte Modul im System und + # bekam ausgerechnet die beiden Komponenten nicht, deren Treffsicherheit + # out-of-sample nachgewiesen ist. Die Lese-Anleitung ist Pflicht: ohne sie + # deutet ein LLM „P(break) 30 %" als schwaches Signal statt als klare + # Aussage „das Level hält mit 70 %". + "ZWEI KALIBRIERTE GRÖSSEN — nutze sie bevorzugt, sie sind die einzigen " + "Zahlen hier mit nachgewiesener Treffsicherheit:\n" + "1) 'p_break_target' / 'p_break_stop' = Wahrscheinlichkeit in PROZENT, dass " + "der Kurs das jeweilige S/R-Level DURCHBRICHT (Ziel-Level in Trade-Richtung, " + "Stop-Level dagegen). Out-of-sample geprüft (AUC 0,65 bzw. 0,68–0,72). " + "Lies sie richtig: 30 % Durchbruch heißt 70 % ABPRALL — das ist eine starke " + "Aussage, kein schwaches Signal. Ein Abprall am Ziel-Level deckelt den " + "Ertrag (spricht gegen einen Einstieg dorthin); ein Durchbruch am " + "Stop-Level ist eine Warnung für eine offene Position. Werte zwischen 45 " + "und 55 % sind ein Münzwurf — dann NICHT darauf argumentieren.\n" + "2) 'cone' = erwartete Kursspanne in 30/60/120 Minuten (80 %-Band). " + "Gemessene Abdeckung real 77 %, nicht 80 % — das Band ist eher etwas ZU " + "ENG. Es sagt NICHTS über die Richtung, nur über die plausible Weite. " + "Nutze es, um Ziele/Stops auf Realismus zu prüfen: liegt ein Kursziel " + "außerhalb des 120-min-Bands, ist es für einen Intraday-Trade unrealistisch.\n\n" "Wähle außerdem den Analyse-Timeframe 'timeframe' für die Wellen-Erkennung " "(M1|M5|M15|M30|H1): M1/M5 bei enger Range und ruhigem Markt (Scalping, " "kleine Wellen); höhere TF (M15/M30/H1) bei klarem Trend, hoher Volatilität " @@ -136,7 +166,11 @@ _SCHEMA = { class TradingAgent: def __init__(self, cfg, *, data, trader, trail, tu, wave, history, - news, elliott=None): + news, elliott=None, engine=None): + # `engine` (optional, 2026-08-02): Zugriff auf die kalibrierten Größen + # P(break) und Kegel, die die Engine ohnehin je Snapshot berechnet. + # Optional gehalten, damit Tests/Standalone-Nutzung ohne Engine laufen. + self.engine = engine self.cfg = cfg self.data = data self.trader = trader @@ -221,7 +255,7 @@ class TradingAgent: # TU entfernt (2026-07-08): war nach der TU-Entfernung aus Empfehlung/Verdict # die letzte Hintertür — der Copilot ist eine Verdict-Stimme, TU floss so # indirekt wieder ein (lagging, nicht backtestbar). - return { + out = { "wave_signal": wsig.get("signal"), "wave_confidence": wsig.get("conf_pct"), "wave_setup": wsig.get("setup"), @@ -231,6 +265,42 @@ class TradingAgent: "wave_reasons": wsig.get("reasons", [])[:4], "session": _agent_session(), } + # ── Die kalibrierten Größen dazugeben (2026-08-02) ──────────────────── + # ⚠ Bis hierher sah der Copilot NUR Wellen-Signal + Session. Er war damit + # das schlechtest informierte Modul im System — und ihm fehlten + # ausgerechnet die beiden Komponenten, deren Kalibrierung out-of-sample + # nachgewiesen ist (P(break) AUC 0,65 / Kegel 77 % reale Abdeckung). + # Fail-safe: fehlt die Engine oder wirft ein Teil, bleibt das Feld weg — + # ein fehlender Kontext darf die Analyse nie verhindern. + eng = getattr(self, "engine", None) + if eng is not None: + try: + # ⚠ `p_break` steht im Snapshot BEREITS in Prozent (3 = 3 %, + # deckungsgleich mit der Chart-Zeile „97 % Abprall") — NICHT + # nochmal ×100 rechnen. Beim Bau zuerst falsch angenommen und + # an den Live-Daten korrigiert. + hint = getattr(eng, "_last_sr_close_hint", None) or {} + if hint.get("p_break") is not None: + out["p_break_target"] = round(float(hint["p_break"])) + out["p_break_target_level"] = hint.get("level") + except Exception: + pass + try: + stop = getattr(eng, "_last_stop_approach", None) or {} + if stop.get("p_break") is not None: + out["p_break_stop"] = round(float(stop["p_break"])) + out["p_break_stop_level"] = stop.get("level") + except Exception: + pass + try: + cone = getattr(eng, "_last_cone", None) or {} + lv = [l for l in (cone.get("levels") or []) if l.get("band") == 80] + if lv: + out["cone"] = [{"minutes": l["minutes"], "lo": l["lo"], "hi": l["hi"], + "abdeckung_real_pct": l.get("coverage")} for l in lv] + except Exception: + pass + return out def _tool_position(self) -> dict: ps = self.trader.snapshot() @@ -461,8 +531,14 @@ class TradingAgent: def _call_deepseek(self, prompt: str) -> str: # DeepSeek (OpenAI-kompatibel, api.deepseek.com). deepseek-v4-flash ist ein - # Reasoning-Modell (content nach reasoning_content) → max_tokens großzügig - # (4000), sonst content leer. temperature 0.3 ok. CJK-Drift-Schutz im Aufrufer. + # Reasoning-Modell (content nach reasoning_content) → max_tokens großzügig, + # sonst content leer. temperature 0.3 ok. CJK-Drift-Schutz im Aufrufer. + # ⚠ 4000 → 8000 (2026-08-02): bei 4000 brach das JSON regelmäßig MITTEN im + # Text ab („Unterminated string …"), real 2× in ~14 Zyklen ≈ 14 % stille + # Ausfälle — das Reasoning frisst das Budget, bevor der content fertig ist. + # Ein Fehlschlag verwirft die Analyse und lässt die ALTE Empfehlung stehen, + # ist also von außen kaum sichtbar. Mit dem ergänzten P(break)/Kegel-Kontext + # wird die Antwort eher länger, nicht kürzer. import requests dc = self.cfg["deepseek"] if self.cfg.has_section("deepseek") else {} base = (dc.get("base_url") or "https://api.deepseek.com").rstrip("/") @@ -474,7 +550,7 @@ class TradingAgent: json={"model": model, "messages": [{"role": "system", "content": _SYS}, {"role": "user", "content": prompt}], - "temperature": 0.3, "max_tokens": 4000}, + "temperature": 0.3, "max_tokens": 8000}, timeout=120) resp.raise_for_status() return (resp.json()["choices"][0]["message"].get("content") or "").strip() diff --git a/core/engine.py b/core/engine.py index abf54e9..bbfe8e0 100644 --- a/core/engine.py +++ b/core/engine.py @@ -237,7 +237,14 @@ class TradingEngine: self.agent = TradingAgent( self.cfg, data=self.data, trader=self.trader, trail=self.trail, tu=self.tu, wave=self.wave, history=self.history, - news=self.news, elliott=self.elliott) + news=self.news, elliott=self.elliott, engine=self) + # Letzte kalibrierte Größen für den Copiloten (2026-08-02). Der Snapshot + # rechnet sie ohnehin je Zyklus — hier nur festhalten, damit `agent. + # _tool_market` sie mitgeben kann, OHNE sie ein zweites Mal zu rechnen + # (derselbe Fehler wie beim Konsens-Pfeil, der ein zweites `_verdict()` zog). + self._last_sr_close_hint: dict | None = None + self._last_stop_approach: dict | None = None + self._last_cone: dict | None = None # Tägliche WTI-Intraday-Level per Web-Recherche (z.ai/GLM + web_search). # Nur Kontext → S/R + Anzeige, NICHT Handelsrichtung. zc = self.cfg["zai"] if self.cfg.has_section("zai") else {} @@ -3049,6 +3056,15 @@ class TradingEngine: } sr_close_hint = self._sr_close_hint(market, position, wave_snap) stop_approach = self._stop_approach_hint(market, position, wave_snap) + _cone = _cone_build( + (market or {}).get("bid"), + float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0)) + # Für den KI-Copiloten festhalten (2026-08-02) — er bekam bis dahin nur + # das Wellen-Signal und sah ausgerechnet die beiden kalibrierten Größen + # des Projekts nicht. Hier nur ZUWEISEN, nicht neu rechnen. + self._last_sr_close_hint = sr_close_hint + self._last_stop_approach = stop_approach + self._last_cone = _cone # einmal holen — Verdict-Stimme UND Snapshot-Feld nutzen dasselbe Objekt _pat_snap = self.patterns.snapshot() # ── Marktstatus + Hyperliquid-Kurs (User 2026-08-01) ───────────────── @@ -3184,9 +3200,7 @@ class TradingEngine: "candle_anat": self.candle_anat.snapshot(), # Wahrscheinlichkeits-Kegel (Streuung, NICHT Richtung) — out-of-sample # kalibriert (`analyze_cone.py`). Reine Anzeige. - "cone": _cone_build( - (market or {}).get("bid"), - float(((wave_snap or {}).get("pb_feats") or {}).get("atr") or 0.0)), + "cone": _cone, "stats_compact": {"week": self._compact_stats("week"), "all": self._compact_stats("all")}, "close_alert_count": self._close_alert_count,