diff --git a/CLAUDE.md b/CLAUDE.md index a1cce65..6639686 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6161,6 +6161,52 @@ Neumessung. Sie bleiben unverändert und reproduzierbar; wo ihre Schlussfolgerun relevant wird, gehört sie live-treu **neu gerechnet** (Muster: `backtest_auto_signal_norev.py`). +## ✅ AUTO-SIGNAL-ENTRY (🎯 SIG) KOMPLETT ENTFERNT (User-Entscheidung 2026-08-12) + +User: „den Signal-Autotrader brauchen wir im Moment nicht mehr — sollten wir den +inkl. Button entfernen?" **Ja.** Er war seit dem 01.08. aus und ist auf **beiden** +Ausführungs-Achsen gemessen negativ: +| Variante | H1 / H2 | +|---|---| +| Markt-Einstieg | negativ in **allen 8** Feldern (`backtest_auto_signal.py` 30.07., `_norev` 04.08.) | +| ruhende Order, Fill bei BERÜHRUNG | **−0,392/−0,338**, negativ in **allen 6** Feldern (`backtest_signal_touchfill.py` 06.08.) | +⚠⚠ **Der zweite Punkt gibt den Ausschlag:** die ruhende Order — genau der Umbau, +der den Squeeze gerettet hat — macht SIG sogar **schlechter als den +Markt-Einstieg**. Die +0,35/+0,32 aus `backtest_auto_signal_v3.py` galten in +Variante B (Fill beim Close-Bruch) und waren **Look-ahead**; der Aufschlag betrug +0,77–0,85 R. Ursache ist strukturell und nicht reparierbar: `_pend` **wandert** +(neu verankert bei jeder k×ATR-Gegenbewegung, nach Timeout, bei Richtungswechsel) +— „ein Level ist nur dann ein Ausführungs-Vorteil, wenn es STILLSTEHT". +✅ Nebenbei entfällt der **offene Churn-Defekt** (4× setzen/stornieren in 76 s bei +identischem Level, gefunden 06.08., nie behoben). + +**Entfernt:** `_check_auto_signal` · `set_auto_signal` · der Signal-Zweig in +`_pending_ziel` · `POST /api/autosignal` · Button 🎯 SIG (HTML + Handler) · die +vier Snapshot-Felder · `signal_pending_entry` und die `auto_signal*`-Leser. +**Geblieben:** die **AUTOSIG_\*-Trades in der DB** (Historie), die Backtests (sie +dokumentieren das Warum), `wave_rec.breakout` (speist `block_reason`) und +`_confirm_breakout` selbst (das ist das Gate, nicht der Trader). +Sicherung: **`.removed_backup/auto_signal_2026-08-12.py`** + Git-Historie +(Vorbild: der Wave-Auto-Trader, 23.07. genauso entfernt). + +⚠⚠ **DAS RISIKO WAR NICHT SIG, SONDERN DER GETEILTE MANAGER.** Squeeze und Signal +teilten sich `_pending_ziel` — bewusst, weil zwei getrennte Manager über +`trader.pending_orders()` die Orders des jeweils anderen sehen und sich +**gegenseitig stornieren** würden. Deshalb wurde das Squeeze-Verhalten **VOR** dem +Eingriff in `tests/test_pending_ziel.py` festgenagelt (7 Tests: beide Box-Grenzen · +nicht-armed · Guard · Feature aus · kein ATR · fehlende Box · ATR-Skalierung) — +sie liefen vorher grün und laufen **unverändert** weiter. +✅ **Zwei Folgefehler fing die Prüfkette sofort:** `ruff F821` meldete ein +verwaistes `_t` (im entfernten Block definiert, zwei Zeilen später noch benutzt), +und `py_compile` ein dangling `if` im runtime_state-Laden. Beide behoben. +✅ **Zwei Tests mitgezogen statt gelöscht:** `test_signal_fill_long` wurde +**umgedreht** — ein Pending-Fill mit unbekannter Quelle darf jetzt NICHT +stillschweigend als Squeeze durchgehen (das wäre Deployment-Drift Fall 8 mit +umgekehrtem Vorzeichen); und die Systemzustand-Zeile prüft nun ausdrücklich, dass +„SIG" **nicht wieder auftaucht**. +✅ **Live verifiziert:** alle vier Snapshot-Felder weg, `/api/autosignal` +antwortet nicht mehr, `auto_squeeze`/`squeeze_pending` unverändert, 67 Tests grün. + ## ⚠⚠ WIEDEREINSTIEG NACH S/R-CLOSE — 2-HALBJAHRES-BACKTEST: FÄLLT DURCH (2026-08-12) `backtest_srclose_reentry.py`, 80k M5-Bars. Nachfolger des Live-Laufs (n=14, von diff --git a/core/engine.py b/core/engine.py index af96d88..fa0a0c2 100644 --- a/core/engine.py +++ b/core/engine.py @@ -354,11 +354,6 @@ class TradingEngine: "squeeze_max_chase_atr", "0.20")) except (TypeError, ValueError): self._squeeze_max_chase = 0.20 - # Signal-Pfad bekommt dieselbe Mechanik (2026-08-05, gemessen - # `backtest_auto_signal_v3.py`: am Bestätigungs-Level ØR +0,35/+0,32 statt - # −0,09/−0,11 am Markt — SIG besteht damit ERSTMALS die 2-Stichproben-Regel). - self._signal_pending = self.cfg["trading"].get( - "signal_pending_entry", "true").lower() == "true" self._pending_levels = None self._pending_quelle = "" # Welche Pending-Order stammt von welchem Pfad? Bei einem FILL ist die @@ -368,22 +363,25 @@ class TradingEngine: self._pending_tagged: set = set() self._squeeze_pending_err = None self._squeeze_chase_logged = None - self._signal_chase_logged = None self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade) self._squeeze_entry_count = 0 - # ── Autonomer Entry auf die EMPFEHLUNG (User-Wunsch 2026-07-30) ─────────── - # ⚠ GEMESSEN NICHT tragfähig (H1 negativ, s. backtest_auto_signal.py) — auf - # ausdrücklichen User-Wunsch gebaut, Default AUS, mit den besten Parametern. - _t = self.cfg["trading"] - self._auto_signal = _t.get("auto_signal", "false").lower() == "true" - try: - self._auto_signal_min_conf = int(float(_t.get("auto_signal_min_conf", "75"))) - except (TypeError, ValueError): - self._auto_signal_min_conf = 75 - self._auto_signal_skip_night = _t.get("auto_signal_skip_night", "true").lower() == "true" - self._signal_last_sig = None # Dedup: (dir, Signal-Episode) 1× handeln - self._signal_entry_count = 0 + # ⚠⚠ DER AUTONOME ENTRY AUF DIE EMPFEHLUNG (Auto-Signal / „SIG") IST AM + # 2026-08-12 KOMPLETT ENTFERNT (User-Entscheidung). Er war seit dem + # 01.08. aus und auf BEIDEN Ausfuehrungs-Achsen gemessen negativ — + # Markt-Einstieg in allen 8 Feldern, ruhende Order mit Touch-Fill in + # allen 6 und dabei sogar SCHLECHTER als Markt. Ursache strukturell: + # das Bestaetigungs-Level `_pend` WANDERT. + # Weg sind: `_check_auto_signal`, `set_auto_signal`, der Signal-Zweig in + # `_pending_ziel`, `/api/autosignal`, der Dashboard-Button und die + # Snapshot-Felder. Die ini-Schluessel (`auto_signal*`, + # `signal_pending_entry`) werden nicht mehr gelesen. + # GEBLIEBEN: die AUTOSIG_*-Trades in der DB (Historie) und die + # Backtests (sie dokumentieren das Warum). + # Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git. # 15-Minuten-Regel für BOT-eröffnete Trades (gemessen kalibriert: 0,5×ATR) + # ⚠ `_t` wurde bis 2026-08-12 im entfernten Auto-Signal-Block gesetzt; der + # Linter (`ruff F821`) hat den Bruch beim Entfernen sofort gemeldet. + _t = self.cfg["trading"] try: self._adverse15_atr = max(0.0, float(_t.get("adverse_15min_atr", "0.5"))) except (TypeError, ValueError): @@ -765,7 +763,6 @@ class TradingEngine: self._check_pending_fill() # gefuellte Pending-Order zuordnen self._manage_squeeze_pending() self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch - self._check_auto_signal() # autonomer Entry auf die Empfehlung except Exception as e: log.error(f"_pos_loop: {e}", exc_info=True) self._sleep(_POS_S) @@ -1092,14 +1089,13 @@ class TradingEngine: Divergenz, die dieses Projekt siebenmal als Deployment-Drift dokumentiert hat — und bei einer RUHENDEN Order wiegt sie schwerer: eine vergessene Pending-Order feuert im gesperrten Fenster, ohne dass jemand hinschaut.""" - if pfad == "squeeze": - if not self._auto_squeeze: - return "Auto-Squeeze aus" - skip = self._squeeze_skip_night - else: - if not self._auto_signal: - return "Auto-Signal aus" - skip = self._auto_signal_skip_night + # ⚠ Seit dem Entfernen des Auto-Signal-Pfads (2026-08-12) gibt es nur noch + # den Squeeze. Der Parameter `pfad` bleibt in der Signatur — die + # Guard-Kette ist bewusst EINE Quelle für alle autonomen Wege, und ein + # künftiger zweiter Weg soll sie wieder benutzen, nicht nachbauen. + if not self._auto_squeeze: + return "Auto-Squeeze aus" + skip = self._squeeze_skip_night if self._circuit_halted(): return "Circuit-Breaker" if skip and datetime.now(_BERLIN).hour in self._squeeze_night_hours: @@ -1143,17 +1139,23 @@ class TradingEngine: if not quelle: return # fremde/manuelle Position d = "LONG" if ps.get("order_type") == 0 else "SHORT" - setup = f"SQUEEZE_{d}" if quelle == "squeeze" else f"AUTOSIG_{d}" + # ⚠ Seit 2026-08-12 legt nur noch der Squeeze Pending-Orders (der + # Auto-Signal-Pfad ist entfernt). Die `quelle` bleibt trotzdem im + # Marker — ein künftiger zweiter Weg würde sie wieder brauchen, und + # ein Fill ohne bekannte Quelle darf NICHT stillschweigend als + # Squeeze gebucht werden (Deployment-Drift Fall 8). + if quelle != "squeeze": + log.warning(f"Pending-Fill mit unbekannter Quelle {quelle!r} " + f"(Ticket {tk}) — NICHT getaggt") + return + setup = f"SQUEEZE_{d}" self._pending_tagged.add(tk) self._pending_tickets.pop(int(tk), None) ok = self.history.tag_bot_trade(int(tk), setup) if self.history else False # Dieselbe Buchfuehrung wie beim Market-Pfad, damit B4/B5 zaehlen self._bot_open_ticket = tk - self._bot_open_source = "auto_squeeze" if quelle == "squeeze" else "auto_signal" - if quelle == "squeeze": - self._squeeze_entry_count += 1 - else: - self._signal_entry_count += 1 + self._bot_open_source = "auto_squeeze" + self._squeeze_entry_count += 1 log.info(f"🎯 PENDING-FILL ({quelle}): {d} @ {ps.get('entry_price')} " f"— Position {tk} als {setup} getaggt" + ("" if ok else " ⚠ DB-Nachtrag fehlgeschlagen")) @@ -1195,17 +1197,25 @@ class TradingEngine: return ({mt5.ORDER_TYPE_BUY_STOP: round(float(hi) + _SQ_K_ENG * atr, 3), mt5.ORDER_TYPE_SELL_STOP: round(float(lo) - _SQ_K_ENG * atr, 3)}, "squeeze") - # ── 2) Signal: EINE Order am Bestätigungs-Level ────────────────── - # Gemessen `backtest_auto_signal_v3.py`: am Level ØR +0,35/+0,32 (PF 2,2) - # gegen −0,09/−0,11 am Markt — dieselbe Mechanik, die den Squeeze rettet. - bo = ws.get("breakout") or {} - if (self._signal_pending and bo.get("pending") and bo.get("level") - and bo.get("dir") in ("LONG", "SHORT") - and (bo.get("conf") or 0) >= self._auto_signal_min_conf - and self._auto_guard("signal") is None): - ot = (mt5.ORDER_TYPE_BUY_STOP if bo["dir"] == "LONG" - else mt5.ORDER_TYPE_SELL_STOP) - return {ot: round(float(bo["level"]), 3)}, "signal" + # ⚠⚠ DER SIGNAL-ZWEIG IST AM 2026-08-12 ENTFERNT (User-Entscheidung). + # Hier lag eine Order am Bestätigungs-Level von `_confirm_breakout`. + # Gemessen negativ auf BEIDEN Ausführungs-Achsen: + # · Markt-Einstieg — negativ in allen 8 Feldern + # (`backtest_auto_signal.py` 30.07., `_norev` 04.08.) + # · ruhende Order, Fill bei BERÜHRUNG — **−0,392/−0,338**, negativ in + # allen 6 Feldern und damit sogar SCHLECHTER als der Markt-Einstieg + # (`backtest_signal_touchfill.py` 06.08.) + # ⚠ Die +0,35/+0,32 aus `backtest_auto_signal_v3.py` galten in Variante + # B (Fill beim Close-Bruch) und waren Look-ahead — der Aufschlag + # betrug 0,77–0,85 R. + # Der Grund ist STRUKTURELL und nicht reparierbar: `_pend` **wandert** + # (neu verankert bei jeder k×ATR-Gegenbewegung, nach Timeout und bei + # Richtungswechsel). Eine Order an einem wandernden Level fängt + # systematisch die Fehlausbrüche ein — „ein Level ist nur dann ein + # Ausführungs-Vorteil, wenn es STILLSTEHT". + # Damit entfällt auch der offene Churn-Defekt (4× setzen/stornieren in + # 76 s bei identischem Level, gefunden 06.08.). + # Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git-Historie. return {}, "" def _manage_squeeze_pending(self): @@ -1418,120 +1428,6 @@ class TradingEngine: self._save_runtime_state() return self._auto_squeeze - def _check_auto_signal(self): - """Autonomer Entry auf die EMPFEHLUNG (Wave-Signal), nur wenn FLAT. - ⚠ GEMESSEN NICHT tragfähig (`backtest_auto_signal.py`: H1 ØR −0,05…−0,18 in - BEIDEN Konfidenz-Stufen, H2 positiv = Regime-Kippen; Live-Dry-Run 2026-07-23 - WR 29 %/PF 0,64). Auf ausdrücklichen User-Wunsch (2026-07-30) gebaut, Default - AUS, mit den gemessen besten Parametern (Konf≥75 + Nacht-Sperre) und dem - 15-Min-Adverse-Schutz. B4/B5: Live-Perf. gegen die Erwartung halten — driftet - es weg, zurückbauen. Dedup: 1× je Signal-Episode (re-armt bei WARTEN/Flip).""" - if not self._auto_signal: - return - if self._circuit_halted(): # Tagesverlust-Stopp → heute keine Auto-Trades - return - try: - sig_snap = self.wave.signal() or {} - d = sig_snap.get("signal") - if d not in ("LONG", "SHORT"): - self._signal_last_sig = None # WARTEN → Episode zu Ende, re-armen - return - conf = sig_snap.get("conf_pct") or 0 - if conf < self._auto_signal_min_conf: - return # zu schwach (Dedup NICHT setzen) - # Nacht-Sperre — dasselbe Fenster wie beim Auto-Squeeze - # (`auto_squeeze_night_hours`, Default 3–6). Kein Dedup-Marker, damit das - # Signal danach noch feuern kann, falls es dann noch steht. - # ⚠ Anders als beim Squeeze ist die Kostenfalle für die WELLEN-Population - # gemessen einschlägig (alle Nacht-Bars 0,333×ATR) — hier ist der Guard also - # auch ertrags-begründet. Der Pfad ist ohnehin aus (`auto_signal=false`). - if (self._auto_signal_skip_night - and datetime.now(_BERLIN).hour in self._squeeze_night_hours): - return - # News-Blackout (s. `_check_auto_squeeze`) — kein Dedup-Marker. - if self.calendar.blackout(): - return - ps = self.trader.snapshot() - if ps.get("ticket"): - # Position offen: läuft sie in Signalrichtung, gilt die Episode als - # „schon dabei" (kein Nachkauf); Gegen-Position wird NICHT gedreht - # (unbelegt — dieselbe Begründung wie beim Squeeze-Reverse). - pos_dir = "LONG" if ps.get("order_type") == 0 else "SHORT" - if pos_dir == d: - self._signal_last_sig = d - return - if d == self._signal_last_sig: - return # diese Episode schon gehandelt - # Wiedereinstiegs-Sperren (wie beim Squeeze): nach S/R-Close bzw. nach - # JEDEM Close nicht sofort gleichgerichtet neu eröffnen. - now = time.time() - if (d == self._sr_close_dir and now - self._sr_close_ts < _SQUEEZE_SR_COOLDOWN_S): - return - if (d == self._pos_close_dir and now - self._pos_close_ts < _SQUEEZE_REENTRY_COOLDOWN_S): - return - # ── NACHJAGD-BREMSE (2026-08-05, gemessen `backtest_auto_signal_v3.py`). - # Der Market-Weg ist nur noch FALLBACK, wenn die ruhende Stop-Order am - # Bestätigungs-Level nicht gegriffen hat. Am Level ØR +0,35/+0,32, am - # Bar-Close −0,09/−0,11 — und der Härtetest zeigt den Kipp-Punkt bei - # ~0,25–0,30×ATR Abstand (bei +0,20 noch +0,148/+0,090). Also nur bis - # `squeeze_max_chase_atr` nachkaufen, sonst diese Episode auslassen. - # ⚠ `_signal_last_sig` wird NICHT gesetzt → kommt der Kurs ans Level - # zurück, darf der Entry noch kommen. - ws = self.wave.snapshot() or {} - bo = ws.get("breakout") or {} - atr_m5 = (ws.get("pb_feats") or {}).get("atr") or 0.0 - lvl = bo.get("level") if bo.get("dir") == d else None - tick = self.data.snapshot() or {} - px = tick.get("ask") if d == "LONG" else tick.get("bid") - if lvl and atr_m5 > 0 and px: - weg = (float(px) - float(lvl)) * (1 if d == "LONG" else -1) / atr_m5 - if weg > self._squeeze_max_chase: - if self._signal_chase_logged != d: - self._signal_chase_logged = d - log.info(f"⏭ Auto-Signal übersprungen: Kurs steht schon " - f"{weg:.2f}×ATR über dem Bestätigungs-Level {lvl} " - f"(Grenze {self._squeeze_max_chase:.2f}) — " - f"gemessen ab hier kein Edge mehr") - return - self._signal_chase_logged = None - self._signal_last_sig = d - if isinstance(self._last_rec, dict): - self._last_rec["setup"] = f"AUTOSIG_{d}" - log.info(f"🎯 AUTO-SIGNAL-ENTRY: {d} (Konfidenz {conf}% ≥ " - f"{self._auto_signal_min_conf}%) — eröffne selbständig") - err = (self.open_long(source="auto_signal") if d == "LONG" - else self.open_short(source="auto_signal")) - sym = self.data.symbol or "" - tg = self.cfg["telegram"] - tg_on = (tg.get("enabled", "false").lower() == "true" and tg.get("bot_token")) - if err: - log.error(f"Auto-Signal-Entry fehlgeschlagen: {err}") - self._signal_last_sig = None # Fehlschlag → nächster Tick erneut - if tg_on: - try: send_telegram(f"⚠ Auto-Signal-Entry FEHLGESCHLAGEN\n" - f"{sym} {d} (Konf {conf}%) — {err}", - tg["bot_token"], tg["chat_id"]) - except Exception as e: log.warning(f"Signal-Telegram (Fehler): {e}") - return - self._signal_entry_count += 1 - if tg_on: - try: send_telegram(f"🎯 Auto-Signal-Entry\n{sym} {d} selbständig " - f"eröffnet (Empfehlung, Konfidenz {conf}%).", - tg["bot_token"], tg["chat_id"]) - except Exception as e: log.warning(f"Signal-Telegram: {e}") - except Exception as e: - log.error(f"_check_auto_signal: {e}", exc_info=True) - - def set_auto_signal(self, on: bool) -> bool: - """Autonomen Signal-Entry an/aus (UI/Config). Rückgabe = neuer Zustand.""" - self._auto_signal = bool(on) - self._signal_last_sig = None - log.info(f"Auto-Signal-Entry {'AN' if self._auto_signal else 'AUS'} " - f"(Konf≥{self._auto_signal_min_conf}%" - f"{', Nacht-Sperre' if self._auto_signal_skip_night else ''})") - self._save_runtime_state() - return self._auto_signal - def _check_adverse15(self): """15-Minuten-Regel (User-Wunsch 2026-07-30): steht ein **vom Bot eröffneter** Trade 15 min nach Entry mehr als `adverse_15min_atr`×ATR im Minus → schließen @@ -2154,8 +2050,9 @@ class TradingEngine: st = json.load(f) if "auto_squeeze" in st: self._auto_squeeze = bool(st["auto_squeeze"]) - if "auto_signal" in st: - self._auto_signal = bool(st["auto_signal"]) + # ⚠ `auto_signal` wird seit 2026-08-12 NICHT mehr gelesen (Pfad + # entfernt). Ein Altbestand in der Datei schadet nicht — er wird + # beim naechsten Schreiben ohnehin nicht mehr mitgeschrieben. if "auto_sr_close" in st: self._auto_sr_close = bool(st["auto_sr_close"]) if "sr_close_min_gain" in st: @@ -2190,7 +2087,6 @@ class TradingEngine: tmp = self._runtime_state_path + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump({"auto_squeeze": self._auto_squeeze, - "auto_signal": self._auto_signal, "auto_sr_close": self._auto_sr_close, "sr_close_min_gain": self._sr_close_min_gain, "manual_margin": getattr(self, "_manual_margin", 0.0), @@ -4170,16 +4066,12 @@ class TradingEngine: # Einstieg über ruhende Stop-Orders (2026-08-05) + die aktuell gesetzten # Trigger — macht im Snapshot prüfbar, ob wirklich Orders liegen. "squeeze_pending": self._squeeze_pending, - "signal_pending": self._signal_pending, "pending_levels": ( sorted(round(v, 3) for v in self._pending_levels.values()) if self._pending_levels else []), "pending_quelle": self._pending_quelle, "squeeze_max_chase_atr": self._squeeze_max_chase, "squeeze_entry_count": self._squeeze_entry_count, - "auto_signal": self._auto_signal, - "auto_signal_min_conf": self._auto_signal_min_conf, - "signal_entry_count": self._signal_entry_count, "adverse15_atr": self._adverse15_atr, "adverse15_count": self._adverse15_count, # Auto-Flip-Close (gemessen negativ, auf User-Wunsch gebaut) — für den @@ -4386,7 +4278,6 @@ class TradingEngine: try: an = lambda b: "AN" if b else "aus" # noqa: E731 z.append(f"Automatik: BRK {an(self._auto_squeeze)} · " - f"SIG {an(self._auto_signal)} · " f"S/R-Close {an(self._auto_sr_close)} · " f"Flip {an(getattr(self, '_flip_close', False))}") except Exception as e: diff --git a/measurement_reminder.py b/measurement_reminder.py index 136b6ce..d05dcac 100644 --- a/measurement_reminder.py +++ b/measurement_reminder.py @@ -396,15 +396,24 @@ CONFIG_DEPS = [ "nur im Live-Pfad über diese Config.", }, { - "key": "auto_signal", "test": "backtest_auto_signal.py", + # ⚠⚠ GEGENSTANDSLOS seit 2026-08-12: der Auto-Signal-Pfad ist KOMPLETT + # entfernt (Code, Endpunkt, Button, Snapshot-Felder). Der Schlüssel + # wird nirgends mehr gelesen — ein Wächter darüber könnte also nur + # noch Fehlalarm oder falsche Beruhigung liefern. + # Der Eintrag bleibt als STUMME Notiz stehen (`validated` = der + # Ist-Zustand), damit die Historie lesbar bleibt und niemand den + # Schlüssel versehentlich reaktiviert, ohne die Messlage zu kennen. + "key": "auto_signal", "test": "backtest_signal_touchfill.py", "validated": "false", - "why": "ABGESCHALTET 2026-08-01. Gemessen in H1 durchgehend negativ (alle 8 " - "Varianten, n=718–1167/Hälfte), H2 positiv = Regime-Kippen. Zusätzlich: " - "beide Auto-Pfade konkurrieren um den EINEN Positions-Slot — läuft er " - "mit, ist die B5-Abbruchprüfung des Auto-Squeeze nicht sauber messbar. " - "⚠ Dieser Schlüssel lebt AUCH in runtime_state.json (UI-Toggle) und wird " - "von dort überschrieben — genau so lief er am 31.07. unbemerkt weiter, " - "obwohl die ini 'false' sagte.", + "why": "ENTFERNT 2026-08-12 (Pfad gelöscht, nicht nur abgeschaltet). " + "Gemessen negativ auf BEIDEN Ausführungs-Achsen: Markt-Einstieg " + "in allen 8 Feldern (backtest_auto_signal.py 30.07.), ruhende " + "Order mit Touch-Fill −0,392/−0,338 in allen 6 Feldern und damit " + "SCHLECHTER als Markt (backtest_signal_touchfill.py 06.08.). " + "Ursache strukturell: das Bestätigungs-Level `_pend` WANDERT. " + "⚠ Die ini-Schlüssel bleiben stehen, werden aber nicht mehr " + "gelesen — Reaktivieren ginge nur über die Git-Historie bzw. " + ".removed_backup/auto_signal_2026-08-12.py.", }, { "key": "auto_squeeze", "test": "backtest_breakout_squeeze.py", diff --git a/server.py b/server.py index 7de8ae1..00a3ea4 100644 --- a/server.py +++ b/server.py @@ -378,16 +378,12 @@ async def set_margin_pct(body: dict, return {"ok": True, "value": val} -@app.post("/api/autosignal") -async def toggle_autosignal(x_auth_token: str | None = Header(default=None)): - """Autonomen Entry auf die EMPFEHLUNG an/aus (User-Wunsch 2026-07-30). - ⚠ Gemessen NICHT tragfähig (backtest_auto_signal.py: H1 negativ) — bewusst - vom User gewollt, Default aus, mit 15-Min-Adverse-Schutz.""" - _require_auth(x_auth_token) - eng = app.state.engine - state = eng.set_auto_signal(not eng._auto_signal) - get_logger("server").info(f"[WEB] Auto-Signal-Entry {'AN' if state else 'AUS'}") - return {"ok": True, "enabled": state} +# ⚠⚠ `/api/autosignal` ENTFERNT am 2026-08-12 (User-Entscheidung). Der autonome +# Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN +# Ausführungs-Achsen gemessen negativ — Markt-Einstieg in allen 8 Feldern, +# ruhende Order mit Touch-Fill in allen 6 und dabei sogar schlechter als +# Markt. Ursache strukturell: das Bestätigungs-Level wandert. +# Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git-Historie. @app.post("/api/srclose_min") diff --git a/tests/test_pending_fill.py b/tests/test_pending_fill.py index 397a2ae..732280d 100644 --- a/tests/test_pending_fill.py +++ b/tests/test_pending_fill.py @@ -24,14 +24,24 @@ def test_squeeze_fill_wird_getaggt(engine): assert 49483981 not in engine._pending_tickets # Merker verbraucht -def test_signal_fill_long(engine): - engine._pending_tickets = {777: "signal"} +def test_unbekannte_quelle_wird_NICHT_getaggt(engine): + """⚠ Ersetzt `test_signal_fill_long` (der Auto-Signal-Pfad ist am + 2026-08-12 entfernt). Der Test wurde NICHT geloescht, sondern umgedreht: + seit es nur noch den Squeeze gibt, darf ein Fill mit einer FREMDEN Quelle + nicht stillschweigend als Squeeze durchgehen — das waere genau + Deployment-Drift Fall 8 (vier reale Trades landeten als `setup=NULL` und + waren von manuellen nicht unterscheidbar), nur mit umgekehrtem Vorzeichen: + ein Fremd-Trade wuerde die B4-/B5-Monitore verfaelschen.""" + engine._pending_tickets = {777: "signal"} # Altbestand / unbekannt engine.trader.snap = {"ticket": 777, "order_type": 0, "entry_price": 75.2} engine._check_pending_fill() - assert engine._signal_entry_count == 1 assert engine._squeeze_entry_count == 0 - assert engine._bot_open_source == "auto_signal" + assert engine._bot_open_source is None + # ⚠ Der Merker bleibt STEHEN — nur ein sauber zugeordneter Fill verbraucht + # ihn. So wird derselbe Fall beim naechsten Tick erneut gemeldet, statt + # still zu verschwinden. + assert 777 in engine._pending_tickets def test_fremde_position_wird_nicht_getaggt(engine): diff --git a/tests/test_pending_ziel.py b/tests/test_pending_ziel.py new file mode 100644 index 0000000..9a5d33d --- /dev/null +++ b/tests/test_pending_ziel.py @@ -0,0 +1,108 @@ +"""`_pending_ziel`: das SQUEEZE-Verhalten darf sich beim Entfernen von SIG +NICHT aendern. + +ANLASS (2026-08-12): der Auto-Signal-Pfad wird entfernt — gemessen negativ auf +BEIDEN Ausfuehrungs-Achsen (Markt: alle 8 Felder; ruhende Order mit Touch-Fill: +alle 6 Felder, sogar schlechter als der Markt-Einstieg). Grund strukturell: +`_pend` WANDERT, und „ein Level ist nur dann ein Ausfuehrungs-Vorteil, wenn es +STILLSTEHT". + +⚠⚠ DAS RISIKO IST NICHT SIG, SONDERN DER GETEILTE MANAGER. Squeeze und Signal +teilen sich `_pending_ziel` — bewusst, weil zwei getrennte Manager ueber +`trader.pending_orders()` die Orders des jeweils anderen sehen und sich +**gegenseitig stornieren** wuerden (der Bug waere sporadisch und im Log kaum +erkennbar). Dieser Test nagelt den Squeeze-Zweig fest, BEVOR der Signal-Zweig +faellt, und laeuft danach unveraendert weiter. +""" +from __future__ import annotations + +import MetaTrader5 as mt5 +import pytest + +from core.engine import TradingEngine + +_SQ_K = 0.1 # `_SQ_K_ENG` — Ausbruchspuffer in ×ATR + + +class _Wave: + def __init__(self, snap): + self._snap = snap + + def snapshot(self): + return self._snap + + +class _Stub: + """Minimal-Engine — nur was `_pending_ziel` liest.""" + + _pending_ziel = TradingEngine._pending_ziel + + def __init__(self, *, sq_state="armed", hi=80.5, lo=80.0, atr=0.2, + sq_pending=True, guard=None): + self._squeeze_pending = sq_pending + self._guard = guard + self.wave = _Wave({ + "pb_feats": {"atr": atr}, + "squeeze": {"state": sq_state, "box_hi": hi, "box_lo": lo}, + }) + # SIG-Felder bleiben gesetzt, damit der Test VOR und NACH dem Entfernen + # laeuft: existiert der Signal-Zweig nicht mehr, werden sie ignoriert. + self._signal_pending = False + self._auto_signal_min_conf = 75 + + def _auto_guard(self, quelle): + return self._guard + + +def test_armed_legt_beide_box_grenzen(): + """Der Kern: solange der Squeeze `armed` ist, liegen BEIDE Stop-Orders — + je einen Ausbruchspuffer ausserhalb der Box.""" + ziel, quelle = _Stub(hi=80.5, lo=80.0, atr=0.2)._pending_ziel() + assert quelle == "squeeze" + assert ziel == {mt5.ORDER_TYPE_BUY_STOP: round(80.5 + _SQ_K * 0.2, 3), + mt5.ORDER_TYPE_SELL_STOP: round(80.0 - _SQ_K * 0.2, 3)} + + +def test_nicht_armed_legt_nichts(): + for st in ("active", None): + ziel, quelle = _Stub(sq_state=st)._pending_ziel() + assert ziel == {} and quelle == "" + + +def test_guard_storniert(): + """Nacht-Fenster / News / Circuit-Breaker: der Guard ist die EINE Quelle fuer + beide Wege — schlaegt er zu, darf nichts liegen.""" + ziel, quelle = _Stub(guard="nacht")._pending_ziel() + assert ziel == {} and quelle == "" + + +def test_feature_aus_legt_nichts(): + ziel, quelle = _Stub(sq_pending=False)._pending_ziel() + assert ziel == {} and quelle == "" + + +def test_ohne_atr_kein_absturz(): + s = _Stub() + s.wave = _Wave({"pb_feats": {"atr": 0.0}, "squeeze": {"state": "armed", + "box_hi": 80.5, "box_lo": 80.0}}) + ziel, quelle = s._pending_ziel() + assert ziel == {} and quelle == "" + + +def test_fehlende_box_legt_nichts(): + for hi, lo in ((None, 80.0), (80.5, None), (None, None)): + ziel, quelle = _Stub(hi=hi, lo=lo)._pending_ziel() + assert ziel == {} and quelle == "" + + +def test_preise_haengen_am_atr(): + """Der Puffer skaliert mit dem ATR — sonst waere er in ruhigen Phasen zu weit + und in schnellen zu eng.""" + a, _ = _Stub(atr=0.1)._pending_ziel() + b, _ = _Stub(atr=0.4)._pending_ziel() + assert a[mt5.ORDER_TYPE_BUY_STOP] < b[mt5.ORDER_TYPE_BUY_STOP] + assert a[mt5.ORDER_TYPE_SELL_STOP] > b[mt5.ORDER_TYPE_SELL_STOP] + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-v"])) diff --git a/tests/test_system_status.py b/tests/test_system_status.py index 6500792..745952f 100644 --- a/tests/test_system_status.py +++ b/tests/test_system_status.py @@ -34,9 +34,8 @@ class _Hist: class _Stub: _system_status = TradingEngine._system_status - def __init__(self, *, brk=False, sig=False, src=True, flip=False, puls=None): + def __init__(self, *, brk=False, src=True, flip=False, puls=None): self._auto_squeeze = brk - self._auto_signal = sig self._auto_sr_close = src self._flip_close = flip self.history = _Hist(puls if puls is not None else []) @@ -46,9 +45,15 @@ def _text(z): return "\n".join(z) -def test_automatik_zeile_nennt_alle_vier_schalter(): - z = _text(_Stub(brk=True, sig=False, src=True, flip=False)._system_status()) - assert "BRK AN" in z and "SIG aus" in z and "S/R-Close AN" in z and "Flip aus" in z +def test_automatik_zeile_nennt_alle_schalter(): + """⚠ Seit dem 2026-08-12 sind es DREI — der Auto-Signal-Pfad (SIG) ist + entfernt. Der Test zaehlt sie bewusst einzeln auf: verschwindet einer + stillschweigend aus der Zeile, faellt es hier auf und nicht erst, wenn + monatelang niemand merkt, dass ein Schalter aus ist (real geschehen: + `auto_sr_close` stand 5 Tage aus, gemessener Wert +350,81 EUR).""" + z = _text(_Stub(brk=True, src=True, flip=False)._system_status()) + assert "BRK AN" in z and "S/R-Close AN" in z and "Flip aus" in z + assert "SIG" not in z, "der entfernte Pfad darf nicht wieder auftauchen" def test_toter_logger_wird_gemeldet(): diff --git a/web/app.js b/web/app.js index d557c0a..38540fd 100644 --- a/web/app.js +++ b/web/app.js @@ -1058,14 +1058,11 @@ function render(d) { shortBtn.disabled = hasPos; closeBtn.disabled = !hasPos; $("btn-squeeze").classList.toggle("on", !!d.auto_squeeze); - // Auto-Signal-Entry: bernstein-„warn" statt grün — er ist gemessen NICHT - // tragfähig (H1 negativ) und läuft nur auf ausdrücklichen Wunsch. - const asBtn = $("btn-autosig"); - if (asBtn) { - asBtn.classList.toggle("warn-on", !!d.auto_signal); - asBtn.textContent = d.auto_signal - ? `🎯 SIG ${d.auto_signal_min_conf || 75}%` : "🎯 SIG"; - } + // ⚠⚠ Der 🎯 SIG-Button ist am 2026-08-12 ENTFERNT (User-Entscheidung). Der + // autonome Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN + // Ausführungs-Achsen gemessen negativ — Markt in allen 8 Feldern, ruhende + // Order mit Touch-Fill in allen 6 und dabei schlechter als Markt. Grund + // strukturell: das Bestätigungs-Level `_pend` WANDERT. $("btn-trail").classList.toggle("on", !!(d.trail || {}).enabled); // Empfehlungs-Blinken: LONG/SHORT-Button blinkt, sobald die Welle eine @@ -1661,7 +1658,7 @@ _updateMuteBtn(); // Toggle-Buttons (AUTO/TRAIL/S-R-Close) — ohne Bestätigung $("btn-squeeze").onclick = () => toggle("api/autosqueeze", "Auto-Squeeze (autonom!)"); -$("btn-autosig").onclick = () => toggle("api/autosignal", "Auto-Signal-Entry (autonom!)"); +// (🎯 SIG entfernt 2026-08-12 — s. renderOrderButtons oben) $("btn-trail").onclick = () => toggle("api/trail", "Trailing"); $("srclose-btn").onclick = () => toggle("api/srclose", "Auto-S/R-Close"); diff --git a/web/index.html b/web/index.html index a391173..9bb881a 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -335,7 +335,9 @@ - + @@ -354,6 +356,6 @@ - +