Squeeze-Einstieg ueber ruhende Stop-Orders + Meta-Labeling gemessen (verworfen)
1) STOP-ORDER STATT MARKET (gebaut, live). engine._manage_squeeze_pending haelt beide Box-Grenzen mit BUY_STOP/SELL_STOP bestueckt, solange der Squeeze armed ist; trader.place_stop/cancel_pending/pending_orders neu. Groesse und SL kommen aus denselben Funktionen wie die Market-Order, nur bezogen auf den Trigger. OCO-Ersatz: faellt eine Seite, wird die andere im selben Tick storniert. Der kritische Punkt bei einer ruhenden Order sind die Gates - sie waren bisher Laufzeit-Checks im Market-Pfad. Neu: engine._squeeze_guard() als EINE Quelle fuer beide Wege, jeden Tick neu bewertet; schlaegt ein Gate zu, werden die Orders storniert. Market bleibt Fallback mit Nachjagd-Bremse squeeze_max_chase_atr=0.20 (gemessen der noch positive Bereich). Behandelt: Broker-Mindestabstand, falsche Marktseite, Toleranz 0,02 xATR gegen Order-Churn, Log nur bei Fehleraenderung, Startup-Schonfrist. 10 Szenarien getestet, live am Snapshot verifiziert. 2) META-LABELING (backtest_metalabel.py) - VERWORFEN. Zweitmodell auf 12 kausalen Merkmalen, Fit/Test in BEIDEN Richtungen: AUC 0,508 und 0,479 = Muenzwurf, Kalibrierung im Top-Bucket 74 % vorhergesagt gegen 39 % real, und 8 von 12 Gewichten kippen das Vorzeichen. Die scheinbar besseren Schwellen sind reine Handelsvermeidung und reproduzieren gegenlaeufig nicht. 23. verworfener Signal-Eingriff - deckt sich mit dem Caveat der Quelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b00820c15
commit
5e0e8b00ca
+157
-28
@@ -50,7 +50,12 @@ from core.candles import CandleAnatomy
|
||||
from core.cone import build as _cone_build
|
||||
from core.candle_logger import CandleLogger
|
||||
from core.wave_rec import (_atr as _wr_atr, _ema_last,
|
||||
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX)
|
||||
_EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _STRETCH_MAX,
|
||||
# Ausbruchs-Aufschlag der Box — DIESELBE Konstante, die
|
||||
# `_squeeze_one` nutzt. Nachbauen wäre die Sorte Kopie,
|
||||
# die im Projekt schon 4 divergierende Exit-Modelle
|
||||
# erzeugt hat.
|
||||
_SQ_K as _SQ_K_ENG)
|
||||
|
||||
log = get_logger("engine")
|
||||
|
||||
@@ -331,6 +336,21 @@ class TradingEngine:
|
||||
# der flat-Entry bleibt davon unberührt (der validierte Teil).
|
||||
self._squeeze_reverse = self.cfg["trading"].get("auto_squeeze_reverse", "false").lower() == "true"
|
||||
self._squeeze_last_sig = None # Dedup Entry (flat)
|
||||
# ── Einstieg über RUHENDE Stop-Orders statt Market (2026-08-05, gemessen
|
||||
# `backtest_squeeze_entry.py`: der Einstiegspreis IST der Edge). Die Orders
|
||||
# liegen, solange der Squeeze `armed` ist, an beiden Box-Grenzen.
|
||||
self._squeeze_pending = self.cfg["trading"].get(
|
||||
"squeeze_pending_entry", "true").lower() == "true"
|
||||
# Market bleibt der Fallback — aber nur, solange der Kurs noch im gemessen
|
||||
# positiven Bereich über dem Level steht (0,20×ATR → ØR +0,244/+0,426).
|
||||
try:
|
||||
self._squeeze_max_chase = float(self.cfg["trading"].get(
|
||||
"squeeze_max_chase_atr", "0.20"))
|
||||
except (TypeError, ValueError):
|
||||
self._squeeze_max_chase = 0.20
|
||||
self._squeeze_pending_levels = None
|
||||
self._squeeze_pending_err = None
|
||||
self._squeeze_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) ───────────
|
||||
@@ -677,6 +697,10 @@ class TradingEngine:
|
||||
self._check_adverse15() # 15-Min-Regel (nur Bot-Trades)
|
||||
self._check_flip_close() # Empfehlung dreht → schließen
|
||||
self._check_sr_close()
|
||||
# ZUERST die ruhenden Stop-Orders pflegen (setzen/stornieren),
|
||||
# DANN der Market-Fallback: so kann eine bereits liegende Order
|
||||
# den Ausbruch am Level abfangen, bevor nachgejagt würde.
|
||||
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:
|
||||
@@ -1049,16 +1073,112 @@ class TradingEngine:
|
||||
tg["bot_token"], tg["chat_id"])
|
||||
except Exception as e: log.warning(f"S/R-Close-Telegram: {e}")
|
||||
|
||||
def _squeeze_guard(self) -> str | None:
|
||||
"""Die NICHT richtungsabhängigen Gates für autonome Squeeze-Entries.
|
||||
→ Grundtext, wenn geblockt, sonst None.
|
||||
|
||||
⚠ Bewusst EINE Funktion für beide Einstiegswege (ruhende Stop-Order UND
|
||||
Market-Fallback). Zwei Kopien derselben Gates wären genau die 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 not self._auto_squeeze:
|
||||
return "Auto-Squeeze aus"
|
||||
if self._circuit_halted():
|
||||
return "Circuit-Breaker"
|
||||
if (self._squeeze_skip_night
|
||||
and datetime.now(_BERLIN).hour in self._squeeze_night_hours):
|
||||
return "Nacht-Fenster"
|
||||
_bl = self.calendar.blackout()
|
||||
if _bl:
|
||||
if self._news_block_logged != _bl["title"]:
|
||||
self._news_block_logged = _bl["title"]
|
||||
log.info(f"📅 News-Blackout — kein Auto-Entry: {_bl['title']} "
|
||||
f"({_bl['impact']}, in {_bl['minutes']:.0f} min)")
|
||||
return "News-Blackout"
|
||||
self._news_block_logged = None
|
||||
return None
|
||||
|
||||
def _manage_squeeze_pending(self):
|
||||
"""RUHENDE Stop-Orders an beiden Box-Grenzen, solange der Squeeze `armed` ist.
|
||||
|
||||
WARUM (gemessen `backtest_squeeze_entry.py`, 2026-08-05): der Squeeze hat
|
||||
seinen gesamten Edge im EINSTIEGSPREIS. Die Market-Order auf dem 5-s-Poll
|
||||
füllt live im Median 0,306×ATR hinter dem Level (Mittel 0,741, Max 4,3) →
|
||||
ØR kippt von +0,456 auf −0,158. Eine ruhende Stop-Order deckelt den Preis
|
||||
konstruktiv: selbst mit 0,20×ATR Fill-Slippage bleibt ØR +0,244/+0,426.
|
||||
|
||||
Beide Seiten werden bestückt (die Box kann nach oben ODER unten brechen);
|
||||
füllt eine, existiert eine Position → `soll` wird False und die Gegenseite
|
||||
wird im selben Tick storniert. Das ersetzt ein echtes OCO, das MT5 nicht hat.
|
||||
"""
|
||||
if not self._squeeze_pending:
|
||||
return
|
||||
sym = self.data.symbol or ""
|
||||
if not sym:
|
||||
return
|
||||
try:
|
||||
ps = self.trader.snapshot()
|
||||
sq = (self.wave.snapshot() or {}).get("squeeze") or {}
|
||||
atr = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0
|
||||
hi, lo = sq.get("box_hi"), sq.get("box_lo")
|
||||
grund = self._squeeze_guard()
|
||||
# Orders sollen NUR liegen, wenn: flat · kein Gate · Kompression aktiv ·
|
||||
# Box bekannt · Schonfrist vorbei. Jede dieser Bedingungen kann sich
|
||||
# jederzeit ändern — deshalb wird das JEDEN Tick neu bewertet und nicht
|
||||
# beim Platzieren einmal geprüft.
|
||||
soll = (not ps.get("ticket") and grund is None
|
||||
and sq.get("state") == "armed"
|
||||
and hi and lo and atr > 0
|
||||
and not self._in_startup_grace())
|
||||
offen = self.trader.pending_orders(sym)
|
||||
if not soll:
|
||||
for o in offen:
|
||||
err = self.trader.cancel_pending(o.ticket)
|
||||
if not err:
|
||||
log.info(f"⏸ Squeeze-Pending storniert (T={o.ticket}) — "
|
||||
f"{grund or ('Position offen' if ps.get('ticket') else 'Squeeze geklärt')}")
|
||||
if offen:
|
||||
self._squeeze_pending_levels = None
|
||||
return
|
||||
ziel = {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)}
|
||||
# Bestehende Orders behalten, wenn ihr Trigger noch passt (Toleranz
|
||||
# 0,02×ATR) — sonst löschen und neu setzen. Ohne die Toleranz würde
|
||||
# jede ATR-Zuckung die Orders löschen/neu setzen (Broker-Last, Log-Flut).
|
||||
tol = max(0.02 * atr, 0.001)
|
||||
vorhanden = {}
|
||||
for o in offen:
|
||||
z = ziel.get(o.type)
|
||||
if z is not None and abs(o.price_open - z) <= tol and o.type not in vorhanden:
|
||||
vorhanden[o.type] = o.ticket
|
||||
else:
|
||||
self.trader.cancel_pending(o.ticket)
|
||||
for otype, preis in ziel.items():
|
||||
if otype in vorhanden:
|
||||
continue
|
||||
t, err = self.trader.place_stop(sym, otype, preis, comment="SQZ-STOP")
|
||||
if t:
|
||||
log.info(f"🎯 Squeeze-Pending gesetzt: "
|
||||
f"{'BUY_STOP' if otype == mt5.ORDER_TYPE_BUY_STOP else 'SELL_STOP'}"
|
||||
f" @ {preis:.3f} (T={t})")
|
||||
elif err and self._squeeze_pending_err != err:
|
||||
# ⚠ Nur bei ÄNDERUNG loggen: „Kurs hat das Level bereits passiert"
|
||||
# tritt im 1-s-Takt auf, sobald der Kurs an einer Box-Grenze klebt.
|
||||
self._squeeze_pending_err = err
|
||||
log.info(f"Squeeze-Pending nicht platzierbar ({preis:.3f}): {err}")
|
||||
self._squeeze_pending_levels = ziel
|
||||
except Exception as e:
|
||||
log.error(f"_manage_squeeze_pending: {e}", exc_info=True)
|
||||
|
||||
def _check_auto_squeeze(self):
|
||||
"""Autonomer Entry auf den gemessen-validierten Volatilitäts-Squeeze-Ausbruch
|
||||
(`wave.squeeze.state == 'active'`), NUR wenn flat. Feuert **1× je Ausbruch**
|
||||
(Dedup über dir+level; re-armt, sobald der Squeeze klärt). Emergency-Stop
|
||||
armiert beim Öffnen automatisch (2 % Konto, via `_check_auto_close`). Fail-safe."""
|
||||
if not self._auto_squeeze:
|
||||
return
|
||||
if self._circuit_halted(): # Tagesverlust-Stopp aktiv → heute keine Auto-Trades
|
||||
return
|
||||
try:
|
||||
if self._squeeze_guard() is not None: # aus · Circuit · Nacht · News
|
||||
return
|
||||
sq = (self.wave.snapshot() or {}).get("squeeze") or {}
|
||||
if sq.get("state") != "active":
|
||||
self._squeeze_last_sig = None # Ausbruch geklärt → re-armen
|
||||
@@ -1067,29 +1187,9 @@ class TradingEngine:
|
||||
if d not in ("LONG", "SHORT") or lvl is None:
|
||||
return
|
||||
sig = (d, round(float(lvl), 2))
|
||||
# ── Nacht-Guard (Fenster aus `auto_squeeze_night_hours`, Default 3–6 Uhr
|
||||
# Berlin; war bis 2026-08-05 fest 0–7): kein Entry UND kein Reverse. Setzt
|
||||
# keine Dedup-Marker → feuert nach dem Fenster, falls noch aktiv.
|
||||
# ⚠ Der Guard ist NICHT ertrags-begründet: gemessen ist keine Nachtstunde in
|
||||
# beiden Halbjahren negativ, jede geblockte Stunde KOSTET (`backtest_cost_
|
||||
# gate.py`). Er kauft Beaufsichtigungs-Ruhe — 3–6 ist die günstigste Variante
|
||||
# davon. `auto_squeeze_skip_night=false` (oder leere Liste) schaltet ihn aus.
|
||||
if (self._squeeze_skip_night
|
||||
and datetime.now(_BERLIN).hour in self._squeeze_night_hours):
|
||||
return
|
||||
# ── News-Blackout (Handbuch-Regel, NICHT gemessen): 15 min um ein
|
||||
# High-Impact-Event (FOMC/NFP/CPI/EIA/OPEC) kein autonomer Entry. Setzt
|
||||
# KEINEN Dedup-Marker → nach dem Fenster feuert der Ausbruch, falls noch
|
||||
# aktiv. Gilt nur für den Bot; manuelle Orders bleiben frei.
|
||||
_bl = self.calendar.blackout()
|
||||
if _bl:
|
||||
if self._news_block_logged != _bl["title"]:
|
||||
self._news_block_logged = _bl["title"]
|
||||
log.info(f"📅 News-Blackout — kein Auto-Entry: {_bl['title']} "
|
||||
f"({_bl['impact']}, in {_bl['minutes']:.0f} min)")
|
||||
return
|
||||
self._news_block_logged = None
|
||||
|
||||
# Nacht-Fenster, News-Blackout, Circuit-Breaker und der An/Aus-Schalter
|
||||
# sitzen jetzt in `_squeeze_guard()` (oben geprüft) — EINE Quelle für
|
||||
# Pending- und Market-Weg, s. dort.
|
||||
ps = self.trader.snapshot()
|
||||
ticket = ps.get("ticket")
|
||||
if ticket:
|
||||
@@ -1148,6 +1248,28 @@ class TradingEngine:
|
||||
if (d == self._pos_close_dir
|
||||
and time.time() - self._pos_close_ts < _SQUEEZE_REENTRY_COOLDOWN_S):
|
||||
return
|
||||
# ── NACHJAGD-BREMSE (2026-08-05, gemessen `backtest_squeeze_entry.py`).
|
||||
# Der Market-Weg ist nur noch der FALLBACK, wenn keine Pending-Order
|
||||
# gegriffen hat (Broker-Mindestabstand, Squeeze sprang direkt auf
|
||||
# `active`, Order-Fehler). Gemessen ist ein Einstieg bis rund
|
||||
# **0,20×ATR** über dem Level in BEIDEN Hälften positiv (ØR +0,244/
|
||||
# +0,426); darüber kippt es Richtung −0,158/−0,006. Also: nur bis dahin
|
||||
# nachkaufen, sonst diesen Ausbruch auslassen.
|
||||
# ⚠ `_squeeze_last_sig` wird dabei NICHT gesetzt — läuft der Kurs zum
|
||||
# Level zurück (Ausbruch bleibt `active`), darf der Entry noch kommen.
|
||||
atr_m5 = ((self.wave.snapshot() or {}).get("pb_feats") or {}).get("atr") or 0.0
|
||||
tick = self.data.snapshot() or {}
|
||||
px = tick.get("ask") if d == "LONG" else tick.get("bid")
|
||||
if 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._squeeze_chase_logged != sig:
|
||||
self._squeeze_chase_logged = sig
|
||||
log.info(f"⏭ Auto-Squeeze übersprungen: Kurs steht schon "
|
||||
f"{weg:.2f}×ATR über dem Level {lvl} "
|
||||
f"(Grenze {self._squeeze_max_chase:.2f}) — "
|
||||
f"gemessen ab hier kein Edge mehr")
|
||||
return
|
||||
self._squeeze_last_sig = sig
|
||||
if isinstance(self._last_rec, dict):
|
||||
self._last_rec["setup"] = f"SQUEEZE_{d}"
|
||||
@@ -3391,6 +3513,13 @@ class TradingEngine:
|
||||
# Python-Code läuft (der Batch hat schon 2× still nicht neu gestartet).
|
||||
"squeeze_night_hours": list(self._squeeze_night_hours)
|
||||
if self._squeeze_skip_night else [],
|
||||
# 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,
|
||||
"squeeze_pending_levels": (
|
||||
sorted(round(v, 3) for v in self._squeeze_pending_levels.values())
|
||||
if self._squeeze_pending_levels else []),
|
||||
"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,
|
||||
|
||||
Reference in New Issue
Block a user