Auto-Signal-Entry (SIG) komplett entfernt inkl. Button (v=166)

User-Entscheidung. Der Pfad war seit 01.08. aus und auf BEIDEN
Ausfuehrungs-Achsen gemessen negativ: Markt-Einstieg in allen 8
Feldern, ruhende Order mit Touch-Fill -0,392/-0,338 in allen 6 und
damit sogar schlechter als Markt. Die +0,35/+0,32 aus
backtest_auto_signal_v3.py galten in Variante B (Fill beim
Close-Bruch) und waren Look-ahead - Aufschlag 0,77-0,85 R. Ursache
strukturell: das Bestaetigungs-Level _pend WANDERT.

Nebenbei entfaellt der offene Churn-Defekt (4x 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, /api/autosignal, Button + Handler, vier Snapshot-Felder,
die auto_signal*/signal_pending_entry-Leser.
Geblieben: AUTOSIG_*-Trades in der DB, die Backtests, wave_rec.breakout
(speist block_reason) und _confirm_breakout selbst.
Sicherung: .removed_backup/auto_signal_2026-08-12.py + Git.

Das Risiko war nicht SIG, sondern der GETEILTE Pending-Manager (zwei
getrennte Manager wuerden sich gegenseitig stornieren). Deshalb wurde
das Squeeze-Verhalten VOR dem Eingriff in tests/test_pending_ziel.py
festgenagelt - 7 Tests, vorher gruen, laufen unveraendert weiter.

Zwei Folgefehler fing die Pruefkette sofort: ruff F821 ein verwaistes
_t, py_compile ein dangling if. Zwei Tests wurden mitgezogen statt
geloescht - test_signal_fill_long ist jetzt umgedreht (unbekannte
Quelle darf NICHT als Squeeze durchgehen).

Live verifiziert: alle vier Snapshot-Felder weg, Endpunkt tot,
auto_squeeze/squeeze_pending unveraendert, 67 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-12 18:47:28 +02:00
co-authored by Claude Opus 5
parent b85dd010ab
commit bf7e382818
9 changed files with 269 additions and 205 deletions
+57 -166
View File
@@ -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,770,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,050,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 (Konf75 + 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 36). 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,250,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"⚠ <b>Auto-Signal-Entry FEHLGESCHLAGEN</b>\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"🎯 <b>Auto-Signal-Entry</b>\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: