Squeeze-Trades bekommen den SL aus M5-ATR (User-Entscheidung), TP unveraendert

trader._calc_sl_tp bekommt die ATR-Zeitebene als Parameter (Default SL_TF =
M15); _send/_send_locked/open_long/open_short und place_stop reichen sie durch.
Gesetzt wird sie an genau zwei Stellen: engine._open bei source="auto_squeeze"
und _manage_squeeze_pending bei quelle="squeeze". Manuelle Trades und
Auto-Signal bleiben bei M15.

Grundlage backtest_sl_basis.py: auf Squeeze-Entries ist der Ertrag zwischen
beiden Basen ein Wash (alle Varianten innerhalb eines Standardfehlers), der
Worst-Case sinkt aber von -8,83 auf -2,44 xATR_M5 = Faktor 3. Auf der
Wellen-Population ist der weite M15-SL in BEIDEN Haelften besser - deshalb
bewusst keine globale Umstellung.

Die Multiplikatoren (1,8-2,2 xATR) sind nicht angefasst, nur die Zeitebene.

TP bewusst NICHT betroffen (User: "nur fuer SL, der TP soll mit dem Runner
mitwachsen"). Geprueft statt angenommen: der von _calc_sl_tp zurueckgegebene TP
wird von KEINEM Aufrufer gesendet, beide setzen nur req["sl"]. Das Ziel
verwaltet trailing.py dynamisch (3,5 x ATR der Wellen-Zeitebene). Warnkommentar
im Code, warum der TP dort nicht mit in die Order darf.

4 Szenarien getestet. Dabei eigene Fehlannahme korrigiert: ohne Pivot greift der
1,2-%-Fallback, der beim M15 vom MINDEST-Deckel aufgeweitet wird - die
2,2x-Obergrenze wird dort gar nicht erreicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-05 17:39:41 +02:00
co-authored by Claude Opus 5
parent a6c43183e2
commit 7009541c42
3 changed files with 73 additions and 15 deletions
+35 -12
View File
@@ -83,7 +83,25 @@ class TradeManager:
self._si_cache: object = None
self._si_cache_ts: float = 0.0
def _calc_sl_tp(self, sym, otype, entry_price):
def _calc_sl_tp(self, sym, otype, entry_price, atr_tf=None):
"""`atr_tf` = Zeitebene fuer den ATR-Deckel. None → `SL_TF` (M15, Default).
⚠ SQUEEZE-Trades bekommen seit 2026-08-05 **M5** (User-Entscheidung nach
`backtest_sl_basis.py`): der Ertrag ist dort ein Wash (alle Varianten
innerhalb eines Standardfehlers), aber der Worst-Case sinkt von 8,83 auf
2,44×ATR_M5 — Faktor 3. Auf der WELLEN-Population ist der weite M15-SL
dagegen in beiden Halbjahren besser, deshalb bleibt er dort Default.
Die Multiplikatoren (1,82,2×ATR) sind unveraendert — nur die Zeitebene,
aus der der ATR kommt, wechselt.
⚠⚠ DER TP IST DAVON NICHT BETROFFEN — und das ist Absicht (User-Vorgabe
2026-08-05: „nur fuer SL, der TP soll mit dem Runner mitwachsen").
Der hier zurueckgegebene `tp` wird von KEINEM Aufrufer gesendet: sowohl
`_send_locked` als auch `place_stop` setzen ausschliesslich `req["sl"]`.
Das Ziel verwaltet `core/trailing.py` dynamisch (`_TP_INIT_ATR`=3,5 x ATR
der Wellen-Zeitebene) und zieht es mit dem Lauf nach. Wer hier je den TP
mit in die Order legt, koppelt ihn an die SL-Breite und kappt damit genau
die Runner, von denen der Squeeze lebt (43 % Treffer, Ertrag im Tail)."""
si = mt5.symbol_info(sym)
if not si:
return 0.0, 0.0, "?"
@@ -91,7 +109,7 @@ class TradeManager:
# Risiko-Deckel: SL-Distanz max. INIT_SL_MAX_ATR × ATR(M15).
# Pivot-SLs lagen teils ~80 Pips weg → Einzelverluste -30..-40 €
# bei Durchschnittsgewinnen von ~+4 €.
atr = atr_value(sym)
atr = atr_value(sym, atr_tf) if atr_tf else atr_value(sym)
max_dist = (INIT_SL_MAX_ATR * atr) if atr else None
min_dist = (INIT_SL_MIN_ATR * atr) if atr else None
capped = floored = False
@@ -115,21 +133,24 @@ class TradeManager:
sl = round(sl, si.digits)
sl_dist = sl - entry_price
tp = round(entry_price - INIT_TP_RR * sl_dist, si.digits) if sl_dist > 0 else 0.0
_tfn = "M5" if atr_tf == mt5.TIMEFRAME_M5 else "M15"
src_str = f"M15-Pivot {piv:.3f}" if piv else "Fallback 1.2%"
if atr_tf:
src_str += f", ATR aus {_tfn}"
if capped:
src_str += f", gekappt auf {INIT_SL_MAX_ATR}xATR={max_dist:.3f}"
if floored:
src_str += f", auf min {INIT_SL_MIN_ATR}xATR={min_dist:.3f} aufgeweitet"
return sl, tp, src_str
def _send(self, sym, otype):
def _send(self, sym, otype, atr_tf=None):
with mt5_lock(timeout=15) as got:
if not got:
self.last_error = "MT5 belegt — bitte gleich nochmal"
return None, 0.0
return self._send_locked(sym, otype)
return self._send_locked(sym, otype, atr_tf)
def _send_locked(self, sym, otype):
def _send_locked(self, sym, otype, atr_tf=None):
tick = get_tick(sym)
if not tick:
self.last_error = "Kein Tick"; return None, 0.0
@@ -139,7 +160,7 @@ class TradeManager:
# Initial-SL ≈ risk_pct der Equity). Margin bleibt Obergrenze. Fallback auf
# margin-basiert, wenn risk_pct=0 oder Daten fehlen. Behebt die großen
# EUR-Verluste aus 90 %-Margin × 2×ATR-SL.
sl, _tp, sl_src = self._calc_sl_tp(sym, otype, price)
sl, _tp, sl_src = self._calc_sl_tp(sym, otype, price, atr_tf)
risk = get_risk_per_trade()
sl_dist = abs(price - sl) if sl else None
# Eine manuell gesetzte Einsatz-Margin hat VORRANG vor dem Risiko-Modus
@@ -216,7 +237,8 @@ class TradeManager:
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]:
comment: str = "Widget-SQZ",
atr_tf=None) -> 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
@@ -244,7 +266,8 @@ class TradeManager:
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)
else mt5.ORDER_TYPE_SELL, trigger,
atr_tf)
lots = calc_lots(sym, trigger,
mt5.ORDER_TYPE_BUY if ist_long else mt5.ORDER_TYPE_SELL)
if lots <= 0:
@@ -282,11 +305,11 @@ class TradeManager:
return ""
return _retcode_msg(res)
def open_long(self, sym):
def open_long(self, sym, atr_tf=None):
with self._lock:
if self.ticket:
return "Position bereits offen!"
t, e = self._send(sym, mt5.ORDER_TYPE_BUY)
t, e = self._send(sym, mt5.ORDER_TYPE_BUY, atr_tf)
if not t:
return self.last_error
with self._lock:
@@ -295,11 +318,11 @@ class TradeManager:
self._log_open(t, sym, "BUY", e)
return ""
def open_short(self, sym):
def open_short(self, sym, atr_tf=None):
with self._lock:
if self.ticket:
return "Position bereits offen!"
t, e = self._send(sym, mt5.ORDER_TYPE_SELL)
t, e = self._send(sym, mt5.ORDER_TYPE_SELL, atr_tf)
if not t:
return self.last_error
with self._lock: