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:
Axel Hocks
2026-08-05 09:03:14 +02:00
co-authored by Claude Opus 5
parent 3b00820c15
commit 5e0e8b00ca
4 changed files with 627 additions and 36 deletions
+88
View File
@@ -194,6 +194,94 @@ class TradeManager:
self.last_error = _retcode_msg(res)
return None, 0.0
# ══════════════════════════════════════════════════════════════════════
# PENDING STOP-ORDERS (Squeeze-Einstieg am Level, 2026-08-05)
# ══════════════════════════════════════════════════════════════════════
# WARUM: `backtest_squeeze_entry.py` hat gemessen, dass der Squeeze seinen
# gesamten Edge im EINSTIEGSPREIS hat. 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) — damit
# dreht ØR von +0,456 auf 0,158. Eine ruhende Stop-Order deckelt den Preis
# konstruktiv: selbst mit 0,20×ATR unterstellter Fill-Slippage bleibt ØR
# +0,244/+0,426 in beiden Halbjahren.
def pending_orders(self, sym: str) -> list:
"""Eigene (magic-gleiche) Pending-Orders des Symbols."""
with mt5_lock(timeout=5) as got:
if not got:
return []
try:
orders = mt5.orders_get(symbol=sym) or ()
except Exception:
return []
return [o for o in orders if getattr(o, "magic", 0) == MAGIC]
def place_stop(self, sym: str, otype: int, trigger: float,
comment: str = "Widget-SQZ") -> tuple[int | None, str]:
"""Pending BUY_STOP/SELL_STOP am Ausbruchs-Level. → (ticket, Fehlertext).
Größe und SL werden mit DERSELBEN Logik bestimmt wie bei der Market-Order
(`calc_lots` / `_calc_sl_tp`), nur bezogen auf den Trigger-Preis statt auf
den aktuellen Tick — sonst wäre der SL-Abstand ein anderer als validiert.
⚠ Broker-Mindestabstand (`trade_stops_level`) wird geprüft: liegt der Kurs
schon zu nah am Trigger, lehnt MT5 die Order mit einem kryptischen retcode
ab. Dann meldet die Funktion das sauber, und der Aufrufer entscheidet."""
with mt5_lock(timeout=15) as got:
if not got:
return None, "MT5 belegt"
si = mt5.symbol_info(sym)
tick = get_tick(sym)
if not si or not tick:
return None, "Kein Symbol/Tick"
ist_long = otype == mt5.ORDER_TYPE_BUY_STOP
trigger = round(float(trigger), si.digits)
# Stop-Order muss auf der richtigen Seite des Marktes liegen …
bezug = tick.ask if ist_long else tick.bid
if (trigger <= bezug) if ist_long else (trigger >= bezug):
return None, f"Kurs hat das Level bereits passiert ({bezug:.3f})"
# … und den Broker-Mindestabstand einhalten
mind = (getattr(si, "trade_stops_level", 0) or 0) * (si.point or 0.001)
if mind and abs(trigger - bezug) < mind:
return None, (f"unter Broker-Mindestabstand "
f"({abs(trigger-bezug):.3f} < {mind:.3f})")
sl, _tp, sl_src = self._calc_sl_tp(sym, mt5.ORDER_TYPE_BUY if ist_long
else mt5.ORDER_TYPE_SELL, trigger)
lots = calc_lots(sym, trigger,
mt5.ORDER_TYPE_BUY if ist_long else mt5.ORDER_TYPE_SELL)
if lots <= 0:
return None, "Lot-Fehler"
req = {"action": mt5.TRADE_ACTION_PENDING, "symbol": sym,
"volume": float(lots), "type": otype, "price": float(trigger),
"magic": MAGIC, "comment": comment,
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": get_filling(sym)}
if sl:
req["sl"] = float(sl)
res = mt5.order_send(req)
for mode in (mt5.ORDER_FILLING_RETURN, mt5.ORDER_FILLING_IOC,
mt5.ORDER_FILLING_FOK):
if res and res.retcode != 10030:
break
req["type_filling"] = mode
res = mt5.order_send(req)
if res and res.retcode == mt5.TRADE_RETCODE_DONE:
log_trade.info(f"PENDING {'BUY_STOP' if ist_long else 'SELL_STOP'} "
f"{lots:.2f}L @ {trigger:.3f} T={res.order} "
f"SL={sl:.3f} ({sl_src})")
return res.order, ""
return None, _retcode_msg(res)
def cancel_pending(self, ticket: int) -> str:
"""Eine Pending-Order löschen. → "" bei Erfolg, sonst Fehlertext."""
with mt5_lock(timeout=15) as got:
if not got:
return "MT5 belegt"
res = mt5.order_send({"action": mt5.TRADE_ACTION_REMOVE,
"order": int(ticket)})
if res and res.retcode == mt5.TRADE_RETCODE_DONE:
log_trade.info(f"PENDING storniert T={ticket}")
return ""
return _retcode_msg(res)
def open_long(self, sym):
with self._lock:
if self.ticket: