Auto-Signal-Entry + 15-Min-Regel (User-Wunsch, gegen die Messung, mit besten Params)
User will Auto-Eroeffnung bei Empfehlung trotz negativem Backtest -> gebaut, Default AUS, mit den GEMESSEN besten Parametern statt geratenen: Entry (_check_auto_signal, [trading] auto_signal=false): - min_conf=75 + Nacht-Sperre = beste Kombi (H1 -0.053 mildester Verlust, H2 +0.141 bestes Netto). NICHT weil "mehr Konfidenz besser" -- conf_pct ist unkalibriert, 75 ist empirisch. - Nur FLAT, 1x je Signal-Episode (Dedup, re-armt bei WARTEN/Flip), kein Nachkauf, kein Drehen einer Gegen-Position; erbt Circuit-Breaker + beide Cooldowns. - Setup-Tag AUTOSIG_<dir> -> in der DB separierbar fuer B4-Monitor. - Toggle POST /api/autosignal, Button "SIG" (bernstein = gemessen nicht tragfaehig), neustart-fest via runtime_state.json. 15-Min-Regel (_check_adverse15, adverse_15min_atr=0.5): - Schwelle GEMESSEN kalibriert: "sobald im Minus" ist die SCHLECHTESTE Variante (WR 39->31%, Netto -0.067 vs +0.026 ohne); 0.5xATR ist die beste (H1 -0.111 -> -0.079). Netto bringt die Regel ~nichts = Regime-Schutz. - Nur BOT-eroeffnete Trades (_bot_open_ticket) -- manuelle Diskretion bleibt. - 1x je Ticket geprueft, Startup-Grace respektiert, closed_by=adverse15. Verifiziert: 18 synthetische Szenarien (9 Entry + 9 Adverse) alle korrekt; Live-Neustart ohne Loop-Fehler, Toggle + Persistenz geprueft. v=119. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d40616fbeb
commit
2996eddd7e
@@ -357,6 +357,33 @@ DEFAULT_CONFIG = {
|
||||
# B4-Wochenmonitor prüfen, ob Nacht-Squeezes tragen. Code (`_SQUEEZE_NIGHT`,
|
||||
# `_squeeze_skip_night`) bleibt dormant/reaktivierbar (true = wieder an).
|
||||
"auto_squeeze_skip_night": "true",
|
||||
# ── Autonomer Entry auf die EMPFEHLUNG (Wave-Signal) — User-Wunsch 2026-07-30
|
||||
# („probieren wir es trotzdem mal"). ⚠ GEMESSEN NICHT TRAGFÄHIG
|
||||
# (`backtest_auto_signal.py`, 80k M5, 2 Halbjahre): H1 durchgehend NEGATIV
|
||||
# (ØR −0,05…−0,18, PF 0,78–0,93), H2 positiv (+0,06…+0,15) = Regime-Kippen;
|
||||
# deckt sich mit dem Live-Dry-Run von 2026-07-23 (WR 29 %/PF 0,64). Default AUS.
|
||||
# Der User hat das nach vollständiger Information bewusst gewollt → Schalter
|
||||
# existiert, aber mit den gemessen BESTEN Parametern und Live-Monitor (B4/B5:
|
||||
# driftet es weg → zurückbauen).
|
||||
"auto_signal": "false",
|
||||
# Mindest-Konfidenz für den Auto-Signal-Entry. 75 = gemessen beste Kombi
|
||||
# (mit Nacht-Sperre: H1 −0,053 = mildester Verlust, H2 +0,141). ⚠ conf_pct ist
|
||||
# NICHT kalibriert (`analyze_verdict_calibration.py`) — höher ist nicht monoton
|
||||
# besser, 75 ist empirisch, nicht „mehr ist mehr".
|
||||
"auto_signal_min_conf": "75",
|
||||
# Nacht-Sperre 0–7 Uhr Berlin für den Auto-Signal-Entry (gemessene Kostenfalle,
|
||||
# verbessert JEDE Variante: H1 −0,157→−0,111 bzw. −0,127→−0,053). true = an.
|
||||
"auto_signal_skip_night": "true",
|
||||
# 15-Minuten-Regel (User-Wunsch 2026-07-30 „wenn der Trade nach 15 Minuten
|
||||
# gegen die Richtung läuft schließe ihn"): steht ein BOT-eröffneter Trade
|
||||
# 15 min nach Entry mehr als X×ATR im Minus → schließen (`closed_by=adverse15`).
|
||||
# ⚠ Wert GEMESSEN kalibriert (`backtest_auto_signal.py`): „sobald im Minus"
|
||||
# (=0,0) ist die SCHLECHTESTE Variante (WR bricht 39→31 %, ØR-Netto −0,067);
|
||||
# **0,5 ist die beste Regel-Variante** (H1 −0,111→−0,079). Netto über beide
|
||||
# Hälften bringt die Regel ~nichts (+0,001 vs +0,026 ohne) — sie ist ein
|
||||
# REGIME-Schutz (hilft im schlechten, kostet im guten). 2. Bestätigung von
|
||||
# `backtest_adverse_nosignal.py`. 0 = aus.
|
||||
"adverse_15min_atr": "0.5",
|
||||
},
|
||||
# (Der frühere `[setups]`-Block — Per-Setup-Toggles für den Dry-Run-Auto-Trader —
|
||||
# ist entfernt, 2026-07-23: verwaist seit dem Entfernen des Dry-Run-Traders, wurde
|
||||
|
||||
+185
@@ -222,6 +222,26 @@ class TradingEngine:
|
||||
self._squeeze_last_sig = None # Dedup Entry (flat)
|
||||
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
|
||||
# 15-Minuten-Regel für BOT-eröffnete Trades (gemessen kalibriert: 0,5×ATR)
|
||||
try:
|
||||
self._adverse15_atr = max(0.0, float(_t.get("adverse_15min_atr", "0.5")))
|
||||
except (TypeError, ValueError):
|
||||
self._adverse15_atr = 0.5
|
||||
self._bot_open_ticket = None # Ticket, das der Bot selbst eröffnet hat
|
||||
self._adverse15_done_ticket = None # 15-Min-Prüfung je Ticket nur 1×
|
||||
self._adverse15_count = 0
|
||||
self._sr_close_ts = 0.0 # Zeit + Richtung des letzten S/R-Closes
|
||||
self._sr_close_dir = None # → Cooldown gegen Gleichrichtungs-Roundtrip
|
||||
self._open_pos_dir = None # Richtung der aktuell offenen Position (LONG/SHORT)
|
||||
@@ -505,8 +525,10 @@ class TradingEngine:
|
||||
self.trader.refresh(self.data.symbol)
|
||||
self._check_circuit_breaker() # Tagesverlust-Stopp (Überleben zuerst)
|
||||
self._check_auto_close()
|
||||
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
||||
self._check_sr_close()
|
||||
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)
|
||||
@@ -994,6 +1016,150 @@ 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 (gemessene Kostenfalle 0–7 Uhr Berlin) — kein Dedup-Marker,
|
||||
# damit das Signal nach 08:00 noch feuern kann, falls es dann noch steht.
|
||||
if self._auto_signal_skip_night and datetime.now(_BERLIN).hour in _SQUEEZE_NIGHT:
|
||||
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
|
||||
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
|
||||
(der Einstieg war falsch; gemessen: Gewinner nehmen Ø 0,19 $ Hitze, Verlierer
|
||||
0,41 $). ⚠ Schwelle GEMESSEN kalibriert = **0,5×ATR**; „sobald im Minus" wäre
|
||||
die schlechteste Variante (WR 39→31 %). Netto bringt die Regel über beide
|
||||
Halbjahre ~nichts (Regime-Schutz: hilft im schlechten, kostet im guten) —
|
||||
bewusst als Sicherheit eingebaut. Greift NICHT bei manuellen Trades (dort
|
||||
bleibt die Diskretion beim User) und nicht in der Startup-Schonfrist."""
|
||||
if self._adverse15_atr <= 0:
|
||||
return
|
||||
if self._in_startup_grace():
|
||||
return
|
||||
ps = self.trader.snapshot()
|
||||
ticket = ps.get("ticket")
|
||||
if not ticket:
|
||||
return
|
||||
if ticket != self._bot_open_ticket: # nur Bot-eröffnete Trades
|
||||
return
|
||||
if ticket == self._adverse15_done_ticket: # je Ticket nur 1× prüfen
|
||||
return
|
||||
ot = ps.get("open_time") or 0
|
||||
if not ot:
|
||||
return
|
||||
age = time.time() - float(ot)
|
||||
if age < 15 * 60:
|
||||
return # noch keine 15 min alt
|
||||
self._adverse15_done_ticket = ticket # Prüfzeitpunkt erreicht → 1× bewerten
|
||||
# Verlust in ATR messen (M5-ATR wie im Backtest, Floor wie live)
|
||||
try:
|
||||
atr = float(((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0)
|
||||
except Exception:
|
||||
atr = 0.0
|
||||
atr = max(atr, 0.06)
|
||||
entry = float(ps.get("entry_price") or 0.0)
|
||||
cur = float(ps.get("cur_price") or 0.0)
|
||||
if not entry or not cur:
|
||||
return
|
||||
d = 1 if ps.get("order_type") == 0 else -1
|
||||
adverse_atr = (entry - cur) * d / atr # >0 = läuft gegen die Position
|
||||
if adverse_atr < self._adverse15_atr:
|
||||
log.info(f"15-Min-Check (Pos {ticket}): {adverse_atr:+.2f}×ATR gegen die "
|
||||
f"Position — unter Schwelle {self._adverse15_atr:.2f}, läuft weiter")
|
||||
return
|
||||
pnl = ps.get("pnl")
|
||||
log.warning(f"⏱ 15-MIN-ADVERSE-CLOSE: Pos {ticket} läuft {adverse_atr:.2f}×ATR "
|
||||
f"gegen die Richtung (≥ {self._adverse15_atr:.2f}) — P&L "
|
||||
f"{(pnl if pnl is not None else 0):+.2f} → Einstieg war falsch, schließen")
|
||||
err = self.trader.close(reason="adverse15")
|
||||
tg = self.cfg["telegram"]
|
||||
tg_on = (tg.get("enabled", "false").lower() == "true" and tg.get("bot_token"))
|
||||
if err:
|
||||
log.error(f"15-Min-Adverse-Close fehlgeschlagen: {err}")
|
||||
self._adverse15_done_ticket = None # Fehlschlag → nächster Tick erneut
|
||||
return
|
||||
self._adverse15_count += 1
|
||||
if tg_on:
|
||||
try: send_telegram(f"⏱ <b>15-Min-Regel</b>\n{self.data.symbol or ''} nach 15 min "
|
||||
f"{adverse_atr:.2f}×ATR gegen die Richtung (P&L "
|
||||
f"{(pnl if pnl is not None else 0):+.2f}) → geschlossen.",
|
||||
tg["bot_token"], tg["chat_id"])
|
||||
except Exception as e: log.warning(f"Adverse15-Telegram: {e}")
|
||||
|
||||
def set_sr_autoclose(self, on: bool) -> bool:
|
||||
"""Automatischen S/R-Close an/aus (UI/Config). Rückgabe = neuer Zustand."""
|
||||
self._auto_sr_close = bool(on)
|
||||
@@ -1306,6 +1472,8 @@ 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"])
|
||||
if "auto_sr_close" in st:
|
||||
self._auto_sr_close = bool(st["auto_sr_close"])
|
||||
if "sr_close_min_gain" in st:
|
||||
@@ -1324,6 +1492,7 @@ 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}, f)
|
||||
os.replace(tmp, self._runtime_state_path)
|
||||
@@ -1562,6 +1731,9 @@ class TradingEngine:
|
||||
if source == "auto_squeeze":
|
||||
log.info(f"[ORDER] {want} — autonom vom Auto-Squeeze eröffnet "
|
||||
f"(Signal={sig})")
|
||||
elif source == "auto_signal":
|
||||
log.info(f"[ORDER] {want} — autonom auf die Empfehlung eröffnet "
|
||||
f"(Signal={sig})")
|
||||
elif sig != want:
|
||||
log.info(f"[ORDER] {want} ohne passende Empfehlung (Signal={sig}) "
|
||||
f"— vom User bestätigt, wird eröffnet")
|
||||
@@ -1569,6 +1741,14 @@ class TradingEngine:
|
||||
err = fn(sym)
|
||||
if err:
|
||||
return err
|
||||
# Vom BOT eröffnete Trades markieren → nur für die greift die 15-Min-Regel
|
||||
# (manuelle Trades bleiben in der Diskretion des Users, User-Vorgabe).
|
||||
if source in ("auto_squeeze", "auto_signal"):
|
||||
try:
|
||||
self._bot_open_ticket = self.trader.snapshot().get("ticket")
|
||||
self._adverse15_done_ticket = None
|
||||
except Exception:
|
||||
pass
|
||||
# Trailing automatisch aktivieren (wie _after_trade im Widget)
|
||||
if not self.trail.enabled:
|
||||
self.trail.set_setup(self._last_rec.get("setup"))
|
||||
@@ -2147,6 +2327,11 @@ class TradingEngine:
|
||||
"sr_close_count": self._sr_close_count,
|
||||
"auto_squeeze": self._auto_squeeze,
|
||||
"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,
|
||||
"last_trades": last_trades,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user