diff --git a/CLAUDE.md b/CLAUDE.md index 9d230d5..b51974b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -990,6 +990,27 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: weil gemessen nicht tragfähig; zeigt die Schwelle an), neustart-fest via `runtime_state.json`. **B5-Pflicht:** Live-ØR/PF gegen die Backtest-Erwartung halten (H2-Regime +0,14 vs H1-Regime −0,05) — driftet es weg, **zurückbauen** (`=false`). +- **⚠ 15-MINUTEN-REGEL: SQUEEZE-TRADES AUSGENOMMEN (gemessen + `backtest_adverse15_squeeze.py`, 2026-07-31).** Die Schwelle 0,5×ATR wurde in + `backtest_auto_signal.py` auf **WAVE-SIGNAL-Entries** kalibriert und danach auf ALLE + Bot-Trades angewandt — wieder ein Kalibrierungs-Bruch (gemessen unter X, betrieben + unter Y). Auf SQUEEZE-Entries schadet die Regel bei **JEDER** Schwelle in **BEIDEN** + Hälften: Δ gegen „ohne Regel" 0,3 → −25/−38 · **0,5 → −18/−34** · 0,8 → −12/−39 · + 1,2 → −5/−18. **Monoton** — also ist die Regel selbst das Problem, nicht ihre + Einstellung. Grund: der Squeeze ist runner-abhängig (43 % Treffer, Ertrag im Tail), + ein Schnitt nach 15 min trifft genau den Tail. Ohne Regel H1 −54 / H2 **+220**, mit + 0,5 H1 −72 / H2 +186. + **Anlass (real, 2026-07-31):** drei Bot-Trades wurden per `adverse15` geschlossen — + der **Auto-Signal**-Trade um 11:44 (−26,31 €) wurde damit **gerettet** (danach lief + der Kurs 32 € weiter dagegen), die beiden **Squeeze**-Trades um 20:15/20:36 + (−13,77 / −28,09 €) wurden **zu früh gekappt** (danach +19,28 bzw. +17,60 € möglich, + aus `candles_m1` nachgerechnet). Genau das Muster, das die Messung erklärt. + **Umsetzung:** `_bot_open_source` merkt sich beim Öffnen, WOMIT der Bot eröffnet hat + (`auto_squeeze` | `auto_signal`); `_check_adverse15` steigt bei `auto_squeeze` aus. + Mit 7 Szenarien getestet (Signal/Squeeze/manuell/altes Ticket · zu früh · unter + Schwelle · im Plus). **Für den Auto-Signal-Pfad bleibt sie aktiv** — dort ist sie + gemessen ~neutral (ØR +0,001 mit vs +0,026 ohne), also eine **Versicherung mit + kleiner Prämie**: hilft im schlechten Regime, kostet im guten. `=0` schaltet sie ganz aus. - **15-Minuten-Regel (`[trading] adverse_15min_atr=0.5`, AKTIV, User-Wunsch 2026-07-30 „wenn der Trade nach 15 Minuten gegen die Richtung läuft schließe ihn"):** steht ein **vom BOT eröffneter** Trade 15 min nach Entry ≥ X×ATR_M5 gegen die Richtung → diff --git a/backtest_adverse15_squeeze.py b/backtest_adverse15_squeeze.py new file mode 100644 index 0000000..352b76c --- /dev/null +++ b/backtest_adverse15_squeeze.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Schadet die 15-Minuten-Regel den SQUEEZE-Trades? (Anlass 2026-07-31) + +BEOBACHTUNG: Von den drei heute per `adverse15` geschlossenen Bot-Trades hat die +Regel bei EINEM gerettet (Auto-Signal, danach −32 € weiter dagegen) und bei den +beiden SQUEEZE-Trades zu früh gekappt (danach +19,28 bzw. +17,60 € möglich). + +VERDACHT — wieder ein Kalibrierungs-Bruch: die Schwelle `adverse_15min_atr=0.5` +wurde in `backtest_auto_signal.py` auf **Wave-Signal-Entries** gemessen und wird +live auf **ALLE** Bot-Trades angewandt, also auch auf den Squeeze. Der Squeeze ist +aber runner-abhängig (nur 43 % Treffer, der Ertrag steckt im Tail) — eine Regel, +die nach 15 Minuten kappt, trifft genau den Tail. + +GEMESSEN: Squeeze-Entries (Regel exakt wie live), Exit aus `core/exit_model.py`, +Echtkosten, 2 Halbjahre; ohne Regel vs. mit Regel bei mehreren Schwellen. +Der Adverse-Check läuft über den `stop_when`-Hook — die Phasen-Mechanik wird nicht +kopiert. +""" +import sys + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.wave_rec import _N_BARS + +_ATRMIN = 0.06 +_BARS_15MIN = 3 # 15 min = 3 M5-Bars + + +def _atr_series(H, L, C, p=14): + t = [0.0] + for i in range(1, len(C)): + t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [] + for i in range(len(C)): + if not i: + out.append(None); continue + w = t[max(1, i - p + 1):i + 1] + out.append(sum(w) / max(1, len(w))) + return out + + +def squeeze_signals(H, L, C, AT, lo, hi): + """Regel exakt wie live (`wave._squeeze_one`), Einstieg AM Ausbruchslevel.""" + from core.wave_rec import _SQ_N, _SQ_MULT, _SQ_K + _W = 24; _COOL = 12 + out = [] + i = max(lo, _N_BARS, _SQ_N + 15) + while i < min(hi, len(C) - LIVE.max_hold - 1): + atr = AT[i] + if not atr or atr < _ATRMIN: + i += 1; continue + bh = max(H[i - _SQ_N:i]); bl = min(L[i - _SQ_N:i]) + if (bh - bl) > _SQ_MULT * atr: + i += 1; continue + hit = None + for j in range(i, min(i + _W, len(C) - LIVE.max_hold - 1)): + up = bh + _SQ_K * atr; dn = bl - _SQ_K * atr + if H[j] >= up: hit = (j, 1, up); break + if L[j] <= dn: hit = (j, -1, dn); break + if hit is None: + i += 1; continue + j, d, lvl = hit + out.append((j, d, atr, lvl)) + i = j + _COOL + return out + + +def run(sig, H, L, C, SP, adverse=None): + """`adverse` = Schwelle in ×ATR (None = Regel aus).""" + Rs = [] + for (j, d, atr, lvl) in sig: + cost = (SP[j] if SP[j] > 0 else 0.0225) / atr + hook = None + if adverse is not None: + j0 = j + 1 + + def hook(k, prof_atr, _j0=j0, _x=adverse): + # 1× genau beim 15-Min-Zeitpunkt prüfen (wie live) + return (k - _j0) == _BARS_15MIN - 1 and prof_atr <= -_x + Rs.append(simulate(lvl, d, atr, H, L, C, j + 1, LIVE, stop_when=hook) - cost) + return Rs + + +def st(Rs): + if not Rs: + return " —" + n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) + g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0) + return (f"n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={(g/l if l > 0 else 9.99):>4.2f} " + f"ΣR={s:>+6.0f}") + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + mt5.initialize(); sym = None + for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): + if mt5.symbol_info(c): + sym = c; break + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) + si = mt5.symbol_info(sym); point = si.point + mt5.shutdown() + H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] + C = [float(b["close"]) for b in bars]; SP = [float(b["spread"]) * point for b in bars] + AT = _atr_series(H, L, C) + mid = len(C) // 2 + halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))] + + print("=" * 92) + print(f" 15-MIN-REGEL auf SQUEEZE-Entries — {sym} M5 ({len(C)} Bars), Echtkosten") + print(f" Exit core/exit_model.py (Trail {LIVE.mult}) · live: adverse_15min_atr=0.5") + print(" ⚠ Die 0,5 wurden auf WAVE-Signalen gemessen, nicht auf dem Squeeze.") + print("=" * 92) + SIG = {lbl: squeeze_signals(H, L, C, AT, a, b) for lbl, a, b in halves} + for lbl, _, _ in halves: + print(f" {lbl}: {len(SIG[lbl])} Squeeze-Entries") + + base = {} + for name, adv in (("OHNE Regel", None), ("0,3×ATR", 0.3), + ("0,5×ATR ← live", 0.5), ("0,8×ATR", 0.8), ("1,2×ATR", 1.2)): + print(f"\n{name}") + for lbl, _, _ in halves: + Rs = run(SIG[lbl], H, L, C, SP, adverse=adv) + s = sum(Rs) + if adv is None: + base[lbl] = s + d = f" Δ ohne Regel {s - base.get(lbl, 0):+.0f}" if adv is not None else "" + print(f" {lbl} {st(Rs)}{d}") + + +if __name__ == "__main__": + main() diff --git a/core/engine.py b/core/engine.py index b12eec3..9cda53a 100644 --- a/core/engine.py +++ b/core/engine.py @@ -311,6 +311,9 @@ class TradingEngine: except (TypeError, ValueError): self._adverse15_atr = 0.5 self._bot_open_ticket = None # Ticket, das der Bot selbst eröffnet hat + self._bot_open_source = None # ... und WOMIT (auto_squeeze | auto_signal). + # Nötig, weil die 15-Min-Regel nur für den + # Auto-Signal-Pfad gemessen ist (s. unten). 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 @@ -1227,6 +1230,20 @@ class TradingEngine: return if ticket != self._bot_open_ticket: # nur Bot-eröffnete Trades return + # ⚠ SQUEEZE-TRADES AUSGENOMMEN (gemessen `backtest_adverse15_squeeze.py`, + # 2026-07-31). Die Schwelle 0,5×ATR wurde in `backtest_auto_signal.py` auf + # WAVE-SIGNAL-Entries kalibriert und dann auf ALLE Bot-Trades angewandt — + # wieder ein Kalibrierungs-Bruch. Auf SQUEEZE-Entries schadet die Regel bei + # JEDER Schwelle in BEIDEN Hälften (Δ gegen „ohne Regel": 0,3 → −25/−38 · + # **0,5 → −18/−34** · 0,8 → −12/−39 · 1,2 → −5/−18) — monoton, d. h. die + # Regel selbst ist das Problem, nicht ihre Einstellung. Grund: der Squeeze ist + # runner-abhängig (43 % Treffer, Ertrag im Tail), und ein Schnitt nach 15 min + # trifft genau den Tail. Ohne Regel H1 −54/H2 **+220**, mit 0,5 H1 −72/H2 +186. + # Anlass war der 31.07.: zwei Squeeze-Trades wurden per adverse15 gekappt + # (−13,77 / −28,09 €), danach wären +19,28 bzw. +17,60 € möglich gewesen — + # während dieselbe Regel beim Auto-Signal-Trade desselben Tages 32 € rettete. + if self._bot_open_source == "auto_squeeze": + return if ticket == self._adverse15_done_ticket: # je Ticket nur 1× prüfen return ot = ps.get("open_time") or 0 @@ -1981,6 +1998,7 @@ class TradingEngine: if source in ("auto_squeeze", "auto_signal"): try: self._bot_open_ticket = self.trader.snapshot().get("ticket") + self._bot_open_source = source self._adverse15_done_ticket = None except Exception: pass