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
+46
View File
@@ -6161,6 +6161,52 @@ Neumessung. Sie bleiben unverändert und reproduzierbar; wo ihre Schlussfolgerun
relevant wird, gehört sie live-treu **neu gerechnet** (Muster: relevant wird, gehört sie live-treu **neu gerechnet** (Muster:
`backtest_auto_signal_norev.py`). `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,770,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) ## ⚠⚠ 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 `backtest_srclose_reentry.py`, 80k M5-Bars. Nachfolger des Live-Laufs (n=14, von
+53 -162
View File
@@ -354,11 +354,6 @@ class TradingEngine:
"squeeze_max_chase_atr", "0.20")) "squeeze_max_chase_atr", "0.20"))
except (TypeError, ValueError): except (TypeError, ValueError):
self._squeeze_max_chase = 0.20 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_levels = None
self._pending_quelle = "" self._pending_quelle = ""
# Welche Pending-Order stammt von welchem Pfad? Bei einem FILL ist die # Welche Pending-Order stammt von welchem Pfad? Bei einem FILL ist die
@@ -368,22 +363,25 @@ class TradingEngine:
self._pending_tagged: set = set() self._pending_tagged: set = set()
self._squeeze_pending_err = None self._squeeze_pending_err = None
self._squeeze_chase_logged = None self._squeeze_chase_logged = None
self._signal_chase_logged = None
self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade) self._squeeze_reverse_sig = None # Dedup Reverse-Close (Gegen-Trade)
self._squeeze_entry_count = 0 self._squeeze_entry_count = 0
# ── Autonomer Entry auf die EMPFEHLUNG (User-Wunsch 2026-07-30) ─────────── # ⚠⚠ DER AUTONOME ENTRY AUF DIE EMPFEHLUNG (Auto-Signal / „SIG") IST AM
# ⚠ GEMESSEN NICHT tragfähig (H1 negativ, s. backtest_auto_signal.py) — auf # 2026-08-12 KOMPLETT ENTFERNT (User-Entscheidung). Er war seit dem
# ausdrücklichen User-Wunsch gebaut, Default AUS, mit den besten Parametern. # 01.08. aus und auf BEIDEN Ausfuehrungs-Achsen gemessen negativ —
_t = self.cfg["trading"] # Markt-Einstieg in allen 8 Feldern, ruhende Order mit Touch-Fill in
self._auto_signal = _t.get("auto_signal", "false").lower() == "true" # allen 6 und dabei sogar SCHLECHTER als Markt. Ursache strukturell:
try: # das Bestaetigungs-Level `_pend` WANDERT.
self._auto_signal_min_conf = int(float(_t.get("auto_signal_min_conf", "75"))) # Weg sind: `_check_auto_signal`, `set_auto_signal`, der Signal-Zweig in
except (TypeError, ValueError): # `_pending_ziel`, `/api/autosignal`, der Dashboard-Button und die
self._auto_signal_min_conf = 75 # Snapshot-Felder. Die ini-Schluessel (`auto_signal*`,
self._auto_signal_skip_night = _t.get("auto_signal_skip_night", "true").lower() == "true" # `signal_pending_entry`) werden nicht mehr gelesen.
self._signal_last_sig = None # Dedup: (dir, Signal-Episode) 1× handeln # GEBLIEBEN: die AUTOSIG_*-Trades in der DB (Historie) und die
self._signal_entry_count = 0 # 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) # 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: try:
self._adverse15_atr = max(0.0, float(_t.get("adverse_15min_atr", "0.5"))) self._adverse15_atr = max(0.0, float(_t.get("adverse_15min_atr", "0.5")))
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -765,7 +763,6 @@ class TradingEngine:
self._check_pending_fill() # gefuellte Pending-Order zuordnen self._check_pending_fill() # gefuellte Pending-Order zuordnen
self._manage_squeeze_pending() self._manage_squeeze_pending()
self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch self._check_auto_squeeze() # autonomer Entry auf Squeeze-Ausbruch
self._check_auto_signal() # autonomer Entry auf die Empfehlung
except Exception as e: except Exception as e:
log.error(f"_pos_loop: {e}", exc_info=True) log.error(f"_pos_loop: {e}", exc_info=True)
self._sleep(_POS_S) self._sleep(_POS_S)
@@ -1092,14 +1089,13 @@ class TradingEngine:
Divergenz, die dieses Projekt siebenmal als Deployment-Drift dokumentiert hat Divergenz, die dieses Projekt siebenmal als Deployment-Drift dokumentiert hat
und bei einer RUHENDEN Order wiegt sie schwerer: eine vergessene und bei einer RUHENDEN Order wiegt sie schwerer: eine vergessene
Pending-Order feuert im gesperrten Fenster, ohne dass jemand hinschaut.""" Pending-Order feuert im gesperrten Fenster, ohne dass jemand hinschaut."""
if pfad == "squeeze": # ⚠ 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: if not self._auto_squeeze:
return "Auto-Squeeze aus" return "Auto-Squeeze aus"
skip = self._squeeze_skip_night skip = self._squeeze_skip_night
else:
if not self._auto_signal:
return "Auto-Signal aus"
skip = self._auto_signal_skip_night
if self._circuit_halted(): if self._circuit_halted():
return "Circuit-Breaker" return "Circuit-Breaker"
if skip and datetime.now(_BERLIN).hour in self._squeeze_night_hours: if skip and datetime.now(_BERLIN).hour in self._squeeze_night_hours:
@@ -1143,17 +1139,23 @@ class TradingEngine:
if not quelle: if not quelle:
return # fremde/manuelle Position return # fremde/manuelle Position
d = "LONG" if ps.get("order_type") == 0 else "SHORT" 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_tagged.add(tk)
self._pending_tickets.pop(int(tk), None) self._pending_tickets.pop(int(tk), None)
ok = self.history.tag_bot_trade(int(tk), setup) if self.history else False ok = self.history.tag_bot_trade(int(tk), setup) if self.history else False
# Dieselbe Buchfuehrung wie beim Market-Pfad, damit B4/B5 zaehlen # Dieselbe Buchfuehrung wie beim Market-Pfad, damit B4/B5 zaehlen
self._bot_open_ticket = tk self._bot_open_ticket = tk
self._bot_open_source = "auto_squeeze" if quelle == "squeeze" else "auto_signal" self._bot_open_source = "auto_squeeze"
if quelle == "squeeze":
self._squeeze_entry_count += 1 self._squeeze_entry_count += 1
else:
self._signal_entry_count += 1
log.info(f"🎯 PENDING-FILL ({quelle}): {d} @ {ps.get('entry_price')} " log.info(f"🎯 PENDING-FILL ({quelle}): {d} @ {ps.get('entry_price')} "
f"— Position {tk} als {setup} getaggt" f"— Position {tk} als {setup} getaggt"
+ ("" if ok else " ⚠ DB-Nachtrag fehlgeschlagen")) + ("" 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), 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)}, mt5.ORDER_TYPE_SELL_STOP: round(float(lo) - _SQ_K_ENG * atr, 3)},
"squeeze") "squeeze")
# ── 2) Signal: EINE Order am Bestätigungs-Level ────────────────── # ⚠⚠ DER SIGNAL-ZWEIG IST AM 2026-08-12 ENTFERNT (User-Entscheidung).
# Gemessen `backtest_auto_signal_v3.py`: am Level ØR +0,35/+0,32 (PF 2,2) # Hier lag eine Order am Bestätigungs-Level von `_confirm_breakout`.
# gegen 0,09/0,11 am Markt — dieselbe Mechanik, die den Squeeze rettet. # Gemessen negativ auf BEIDEN Ausführungs-Achsen:
bo = ws.get("breakout") or {} # · Markt-Einstieg — negativ in allen 8 Feldern
if (self._signal_pending and bo.get("pending") and bo.get("level") # (`backtest_auto_signal.py` 30.07., `_norev` 04.08.)
and bo.get("dir") in ("LONG", "SHORT") # · ruhende Order, Fill bei BERÜHRUNG — **0,392/0,338**, negativ in
and (bo.get("conf") or 0) >= self._auto_signal_min_conf # allen 6 Feldern und damit sogar SCHLECHTER als der Markt-Einstieg
and self._auto_guard("signal") is None): # (`backtest_signal_touchfill.py` 06.08.)
ot = (mt5.ORDER_TYPE_BUY_STOP if bo["dir"] == "LONG" # ⚠ Die +0,35/+0,32 aus `backtest_auto_signal_v3.py` galten in Variante
else mt5.ORDER_TYPE_SELL_STOP) # B (Fill beim Close-Bruch) und waren Look-ahead — der Aufschlag
return {ot: round(float(bo["level"]), 3)}, "signal" # 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 {}, "" return {}, ""
def _manage_squeeze_pending(self): def _manage_squeeze_pending(self):
@@ -1418,120 +1428,6 @@ class TradingEngine:
self._save_runtime_state() self._save_runtime_state()
return self._auto_squeeze 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): def _check_adverse15(self):
"""15-Minuten-Regel (User-Wunsch 2026-07-30): steht ein **vom Bot eröffneter** """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 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) st = json.load(f)
if "auto_squeeze" in st: if "auto_squeeze" in st:
self._auto_squeeze = bool(st["auto_squeeze"]) self._auto_squeeze = bool(st["auto_squeeze"])
if "auto_signal" in st: # ⚠ `auto_signal` wird seit 2026-08-12 NICHT mehr gelesen (Pfad
self._auto_signal = bool(st["auto_signal"]) # entfernt). Ein Altbestand in der Datei schadet nicht — er wird
# beim naechsten Schreiben ohnehin nicht mehr mitgeschrieben.
if "auto_sr_close" in st: if "auto_sr_close" in st:
self._auto_sr_close = bool(st["auto_sr_close"]) self._auto_sr_close = bool(st["auto_sr_close"])
if "sr_close_min_gain" in st: if "sr_close_min_gain" in st:
@@ -2190,7 +2087,6 @@ class TradingEngine:
tmp = self._runtime_state_path + ".tmp" tmp = self._runtime_state_path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f: with open(tmp, "w", encoding="utf-8") as f:
json.dump({"auto_squeeze": self._auto_squeeze, json.dump({"auto_squeeze": self._auto_squeeze,
"auto_signal": self._auto_signal,
"auto_sr_close": self._auto_sr_close, "auto_sr_close": self._auto_sr_close,
"sr_close_min_gain": self._sr_close_min_gain, "sr_close_min_gain": self._sr_close_min_gain,
"manual_margin": getattr(self, "_manual_margin", 0.0), "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 # Einstieg über ruhende Stop-Orders (2026-08-05) + die aktuell gesetzten
# Trigger — macht im Snapshot prüfbar, ob wirklich Orders liegen. # Trigger — macht im Snapshot prüfbar, ob wirklich Orders liegen.
"squeeze_pending": self._squeeze_pending, "squeeze_pending": self._squeeze_pending,
"signal_pending": self._signal_pending,
"pending_levels": ( "pending_levels": (
sorted(round(v, 3) for v in self._pending_levels.values()) sorted(round(v, 3) for v in self._pending_levels.values())
if self._pending_levels else []), if self._pending_levels else []),
"pending_quelle": self._pending_quelle, "pending_quelle": self._pending_quelle,
"squeeze_max_chase_atr": self._squeeze_max_chase, "squeeze_max_chase_atr": self._squeeze_max_chase,
"squeeze_entry_count": self._squeeze_entry_count, "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_atr": self._adverse15_atr,
"adverse15_count": self._adverse15_count, "adverse15_count": self._adverse15_count,
# Auto-Flip-Close (gemessen negativ, auf User-Wunsch gebaut) — für den # Auto-Flip-Close (gemessen negativ, auf User-Wunsch gebaut) — für den
@@ -4386,7 +4278,6 @@ class TradingEngine:
try: try:
an = lambda b: "AN" if b else "aus" # noqa: E731 an = lambda b: "AN" if b else "aus" # noqa: E731
z.append(f"Automatik: BRK {an(self._auto_squeeze)} · " 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"S/R-Close {an(self._auto_sr_close)} · "
f"Flip {an(getattr(self, '_flip_close', False))}") f"Flip {an(getattr(self, '_flip_close', False))}")
except Exception as e: except Exception as e:
+17 -8
View File
@@ -396,15 +396,24 @@ CONFIG_DEPS = [
"nur im Live-Pfad über diese Config.", "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", "validated": "false",
"why": "ABGESCHALTET 2026-08-01. Gemessen in H1 durchgehend negativ (alle 8 " "why": "ENTFERNT 2026-08-12 (Pfad gelöscht, nicht nur abgeschaltet). "
"Varianten, n=7181167/Hälfte), H2 positiv = Regime-Kippen. Zusätzlich: " "Gemessen negativ auf BEIDEN Ausführungs-Achsen: Markt-Einstieg "
"beide Auto-Pfade konkurrieren um den EINEN Positions-Slot — läuft er " "in allen 8 Feldern (backtest_auto_signal.py 30.07.), ruhende "
"mit, ist die B5-Abbruchprüfung des Auto-Squeeze nicht sauber messbar. " "Order mit Touch-Fill 0,392/0,338 in allen 6 Feldern und damit "
"⚠ Dieser Schlüssel lebt AUCH in runtime_state.json (UI-Toggle) und wird " "SCHLECHTER als Markt (backtest_signal_touchfill.py 06.08.). "
"von dort überschrieben — genau so lief er am 31.07. unbemerkt weiter, " "Ursache strukturell: das Bestätigungs-Level `_pend` WANDERT. "
"obwohl die ini 'false' sagte.", "⚠ 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", "key": "auto_squeeze", "test": "backtest_breakout_squeeze.py",
+6 -10
View File
@@ -378,16 +378,12 @@ async def set_margin_pct(body: dict,
return {"ok": True, "value": val} return {"ok": True, "value": val}
@app.post("/api/autosignal") # ⚠⚠ `/api/autosignal` ENTFERNT am 2026-08-12 (User-Entscheidung). Der autonome
async def toggle_autosignal(x_auth_token: str | None = Header(default=None)): # Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN
"""Autonomen Entry auf die EMPFEHLUNG an/aus (User-Wunsch 2026-07-30). # Ausführungs-Achsen gemessen negativ — Markt-Einstieg in allen 8 Feldern,
⚠ Gemessen NICHT tragfähig (backtest_auto_signal.py: H1 negativ) — bewusst # ruhende Order mit Touch-Fill in allen 6 und dabei sogar schlechter als
vom User gewollt, Default aus, mit 15-Min-Adverse-Schutz.""" # Markt. Ursache strukturell: das Bestätigungs-Level wandert.
_require_auth(x_auth_token) # Sicherung: `.removed_backup/auto_signal_2026-08-12.py` + Git-Historie.
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}
@app.post("/api/srclose_min") @app.post("/api/srclose_min")
+14 -4
View File
@@ -24,14 +24,24 @@ def test_squeeze_fill_wird_getaggt(engine):
assert 49483981 not in engine._pending_tickets # Merker verbraucht assert 49483981 not in engine._pending_tickets # Merker verbraucht
def test_signal_fill_long(engine): def test_unbekannte_quelle_wird_NICHT_getaggt(engine):
engine._pending_tickets = {777: "signal"} """⚠ 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.trader.snap = {"ticket": 777, "order_type": 0, "entry_price": 75.2}
engine._check_pending_fill() engine._check_pending_fill()
assert engine._signal_entry_count == 1
assert engine._squeeze_entry_count == 0 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): def test_fremde_position_wird_nicht_getaggt(engine):
+108
View File
@@ -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"]))
+10 -5
View File
@@ -34,9 +34,8 @@ class _Hist:
class _Stub: class _Stub:
_system_status = TradingEngine._system_status _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_squeeze = brk
self._auto_signal = sig
self._auto_sr_close = src self._auto_sr_close = src
self._flip_close = flip self._flip_close = flip
self.history = _Hist(puls if puls is not None else []) self.history = _Hist(puls if puls is not None else [])
@@ -46,9 +45,15 @@ def _text(z):
return "\n".join(z) return "\n".join(z)
def test_automatik_zeile_nennt_alle_vier_schalter(): def test_automatik_zeile_nennt_alle_schalter():
z = _text(_Stub(brk=True, sig=False, src=True, flip=False)._system_status()) """⚠ Seit dem 2026-08-12 sind es DREI — der Auto-Signal-Pfad (SIG) ist
assert "BRK AN" in z and "SIG aus" in z and "S/R-Close AN" in z and "Flip aus" in z 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(): def test_toter_logger_wird_gemeldet():
+6 -9
View File
@@ -1058,14 +1058,11 @@ function render(d) {
shortBtn.disabled = hasPos; shortBtn.disabled = hasPos;
closeBtn.disabled = !hasPos; closeBtn.disabled = !hasPos;
$("btn-squeeze").classList.toggle("on", !!d.auto_squeeze); $("btn-squeeze").classList.toggle("on", !!d.auto_squeeze);
// Auto-Signal-Entry: bernstein-„warn" statt grün — er ist gemessen NICHT // ⚠⚠ Der 🎯 SIG-Button ist am 2026-08-12 ENTFERNT (User-Entscheidung). Der
// tragfähig (H1 negativ) und läuft nur auf ausdrücklichen Wunsch. // autonome Entry auf die Empfehlung war seit dem 01.08. aus und auf BEIDEN
const asBtn = $("btn-autosig"); // Ausführungs-Achsen gemessen negativ — Markt in allen 8 Feldern, ruhende
if (asBtn) { // Order mit Touch-Fill in allen 6 und dabei schlechter als Markt. Grund
asBtn.classList.toggle("warn-on", !!d.auto_signal); // strukturell: das Bestätigungs-Level `_pend` WANDERT.
asBtn.textContent = d.auto_signal
? `🎯 SIG ${d.auto_signal_min_conf || 75}%` : "🎯 SIG";
}
$("btn-trail").classList.toggle("on", !!(d.trail || {}).enabled); $("btn-trail").classList.toggle("on", !!(d.trail || {}).enabled);
// Empfehlungs-Blinken: LONG/SHORT-Button blinkt, sobald die Welle eine // 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 // Toggle-Buttons (AUTO/TRAIL/S-R-Close) — ohne Bestätigung
$("btn-squeeze").onclick = () => toggle("api/autosqueeze", "Auto-Squeeze (autonom!)"); $("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"); $("btn-trail").onclick = () => toggle("api/trail", "Trailing");
$("srclose-btn").onclick = () => toggle("api/srclose", "Auto-S/R-Close"); $("srclose-btn").onclick = () => toggle("api/srclose", "Auto-S/R-Close");
+5 -3
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117"> <meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title> <title>Oil · MT5</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=165"> <link rel="stylesheet" href="style.css?v=166">
</head> </head>
<body> <body>
<!-- Modul-Menü (ganz oben) --> <!-- Modul-Menü (ganz oben) -->
@@ -335,7 +335,9 @@
<button class="btn short" data-side="short">▼ SHORT</button> <button class="btn short" data-side="short">▼ SHORT</button>
<button class="btn close" data-side="close">✕ CLOSE</button> <button class="btn close" data-side="close">✕ CLOSE</button>
<button class="btn tog" id="btn-squeeze" title="Autonomer Entry auf Squeeze-Ausbruch">🚀 BRK</button> <button class="btn tog" id="btn-squeeze" title="Autonomer Entry auf Squeeze-Ausbruch">🚀 BRK</button>
<button class="btn tog" id="btn-autosig" title="Autonomer Entry auf die Empfehlung — GEMESSEN NICHT tragfähig (H1 negativ), auf eigenen Wunsch aktiv">🎯 SIG</button> <!-- 🎯 SIG (Auto-Signal-Entry) ENTFERNT 2026-08-12: seit 01.08. aus und auf
BEIDEN Ausfuehrungs-Achsen gemessen negativ (Markt alle 8 Felder,
ruhende Order mit Touch-Fill alle 6 und schlechter als Markt). -->
<button class="btn tog" id="btn-trail">🎯 TRAIL</button> <button class="btn tog" id="btn-trail">🎯 TRAIL</button>
</nav> </nav>
@@ -354,6 +356,6 @@
</div> </div>
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<script src="app.js?v=165"></script> <script src="app.js?v=166"></script>
</body> </body>
</html> </html>