diff --git a/CLAUDE.md b/CLAUDE.md index 59e110c..d58af7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1432,6 +1432,52 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: hinterher nicht verschoben wird; überwacht als Messung `squeeze_b5` im `measurement_reminder.py`. Nacht-Anteil der Altdaten: 12 der 31 Trades / −78,92 € — die stammen aus der Zeit vor dem Nacht-Guard (seit 24.07. wieder an). +- **⚠⚠ DEPLOYMENT-DRIFT FALL 7 — DER SQUEEZE WIRD ZU EINEM PREIS GEHANDELT, DEN DIE + VALIDIERUNG NIE UNTERSTELLT HAT (gemessen `backtest_squeeze_entry.py`, 2026-08-05).** + Anlass: User-Frage zu einem Auto-Trade, der „am höchsten Punkt einer aufsteigenden + Kerze" eröffnet wurde (05.08. 08:33:20 — Level 75,757, Einstieg **75,822**, also + über dem Hoch 75,808 der laufenden M5-Kerze; die schloss dann bei 75,693). + **`backtest_breakout_squeeze.py` steigt AM LEVEL ein** (`sim(lvl, …)`). Live schickt + `_check_auto_squeeze` eine **MARKET**-Order, sobald `state=='active'` — der Kurs ist + dann schon ein Stück weiter. **Aus 34 echten Live-Entries (Logs 16.07.–05.08., + Level aus der Log-Zeile × Einstiegspreis aus der DB): Median +0,306×ATR über dem + Level**, Mittel +0,741 · 75 % +0,863 · Max +4,339; nur **18 %** liegen innerhalb + 0,10×ATR, **53 % über 0,30×ATR**. + **Was dieser Preisunterschied kostet** (80k M5, 2 Halbjahre, sequentiell EIN Slot, + kanonischer Exit, Echtkosten): + | Variante | H1 ØR / ΣR | H2 ØR / ΣR | WR | + |---|---|---|---| + | **A Einstieg AM LEVEL** (Backtest-Annahme) | **+0,456 / +369,8** | **+0,611 / +777,8** | 56/62 % | + | **B Einstieg am Bar-Close** (≈ live) | **−0,158 / −125,9** | **−0,006 / −7,6** | 37/42 % | + **Allein der Einstiegspreis dreht das Setup — −0,61 ØR in BEIDEN Hälften, WR bricht + von 56 auf 37 %.** Modell B ist dabei **nicht pessimistisch**: sein Median-Abstand + (0,38×ATR) liegt nah am LIVE gemessenen (0,31×ATR); der Live-Betrieb liegt also + ~80 % des Weges von A nach B, also grob bei **breakeven bis leicht negativ** — + nicht bei den dokumentierten +0,14…+0,23. + ⚠⚠ **Das ordnet den B4-Befund vom 01.08. NEU ein.** Dort stand: „Mechanik +0,102 vs. + Live −0,242 → **die Lücke entsteht im EXIT**". Die Mechanik-Zahl unterstellt aber + denselben Level-Einstieg. Der Einstiegspreis erklärt den Großteil der 0,34-R-Lücke + — **es war überwiegend der ENTRY, nicht der Exit.** + ⚠ **BEIDE naheliegenden Reparaturen fallen durch** (Regel vorab fixiert: ΣR und ØR + in beiden Hälften > B, Nachbarn ebenso): + · **Abstands-Bremse** („überspringen, wenn der Kurs schon > X×ATR über dem Level + steht", X = 0,10…0,40) — **alle 5 Schwellen durch.** Kontraintuitiv, aber konsistent: + die *nahen* Ausbrüche sind pro Trade die SCHLECHTEREN (0,10×ATR → ØR −0,254), weil ein + gerade eben überschrittenes Level oft ein Fehlausbruch ist (54 % der Box-Ausbrüche + kehren binnen 3 Bars zurück, `backtest_consolidation.py`). Die Bremse senkt den + Gesamtverlust nur, indem sie weniger handelt. + · **LIMIT-Order am Level** (Retest, 3/6/12 Bars Gültigkeit, ±0,1×ATR Puffer) — + **alle 5 Varianten SCHLECHTER als Market** (ØR −0,14…−0,27 gegen −0,006…−0,158); + 151–430 Ausbrüche werden dabei verpasst. Deckt sich mit dem Retest-Befund aus + `backtest_consolidation.py`. + ✅ **Schlussfolgerung: Variante A war nie erreichbar** — sie unterstellt einen Fill zu + einem Preis, den der Markt bereits verlassen hat. Die Frage ist damit nicht „wie + steigt man klüger ein", sondern **ob der Auto-Squeeze überhaupt bleibt**. Das ist die + bereits fixierte **B5-Abbruchregel** (≥20 Squeeze-Trades ab 01.08.; Verhältnis < 1,0 + ODER PF < 1 → `auto_squeeze=false`) — dieser Befund stärkt sie erheblich. + **Nichts abgeschaltet** — Autonomie-Schalter sind User-Entscheidungen. + ⚠ Grenzen: die Live-Stichprobe ist n=34 mit genäherter ATR-Zuordnung; A und B sind + zwei Modell-Enden, der Live-Betrieb liegt dazwischen (näher an B). - **Auto-Squeeze-Entry = LIVE AUTONOM (User-Vorgabe 2026-07-16, `[trading] auto_squeeze=true`, Default FALSE):** der Bot eröffnet **selbständig eine echte Order**, sobald der **gemessen-validierte** Squeeze-Ausbruch feuert (`wave.squeeze. @@ -2888,6 +2934,7 @@ Bedingungen BETRIEBEN als VALIDIERT. An EINEM Tag wurden drei Fälle gefunden: | 2 | `breakout_k=0,3` | **feste** Zeitebene | TF wechselt 33×/Tag, löscht den Anker | 43 % → **93 % WARTEN** | | 3 | Trailing `mult` | **1,5** | **1,5–3,0** je nach TF | H1 ΣR −249 → **−1367**, Worst −1,50 → −2,50 | | 4 | `_broker_offset_s()` | **frischem** Tick | Tick beliebig alt (Wochenende) | Offset **−9,5 h statt +3 h** → `open_time` 12,5 h in der Zukunft, **Time-Stop-Alter negativ** | +| 7 | Auto-Squeeze-**Einstiegspreis** | Fill **AM Ausbruchs-Level** (`backtest_breakout_squeeze.py`) | **MARKET**-Order, live median **+0,306×ATR** über dem Level (34 echte Entries) | ØR **+0,456 → −0,158** (H1) bzw. **+0,611 → −0,006** (H2); WR 56 → 37 % | | 6 | `auto_squeeze_skip_night` | Nacht-Kostenfalle am **Wellensignal** (alle Nacht-Bars: 0,333×ATR) | **Squeeze**-Ausbrüche, die nur bei anziehendem ATR feuern (0,183×ATR = Tag-Niveau) | Gate kostet ΣR **−42 (H1) / −243 (H2)**; geblockte Ausbrüche sind in beiden Hälften positiv | | 5 | `backtest_auto_signal.py` | **ohne Winkel** (`_build` ohne `angle=` → Default 90 → `ad=0`) | Winkel steuert einen **Signalzweig** (Reversal) UND **±15 Konfidenzpunkte** | Urteil vom 30.07. beschrieb weder das alte noch das neue System; der Winkelterm allein hebt H1 bei conf 75 von **−0,123 auf −0,047** | diff --git a/backtest_squeeze_entry.py b/backtest_squeeze_entry.py new file mode 100644 index 0000000..b795d31 --- /dev/null +++ b/backtest_squeeze_entry.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Wie teuer ist der SPÄTE Squeeze-Einstieg — und hilft eine Abstands-Bremse? + +ANLASS (2026-08-05, User): „Der Auto-Trade wurde am höchsten Punkt einer +aufsteigenden Kerze geschlossen und ein neuer Long eröffnet. Aus meiner Sicht ist +das nicht klug." Realfall 08:33:20 — Ausbruchs-Level **75,757**, Einstieg +**75,822** = **0,28×ATR darüber**, oberhalb des Hochs (75,808) der laufenden +M5-Kerze; die Kerze schloss dann bei 75,693. + +⚠ DER VERDACHT IST EIN DEPLOYMENT-DRIFT-KANDIDAT, kein Bauchgefühl: +`backtest_breakout_squeeze.py` hat den Squeeze mit Einstieg **AM LEVEL** validiert +(+0,14…+0,23 ØR). Live läuft `_check_auto_squeeze` alle ~5 s und schickt eine +MARKET-Order — der Kurs ist dann schon ein Stück über dem Level. Gemessen unter X, +betrieben unter Y: dieselbe Klasse wie die fünf dokumentierten Fälle. + +GEMESSEN WIRD DESHALB + A) Einstieg AM LEVEL (die Backtest-Annahme) + B) Einstieg am BAR-CLOSE (realistisch: so weit ist der Kurs gelaufen, wenn + `_squeeze_one` den Ausbruch das erste Mal sieht) + C) B + Abstands-Bremse: Ausbruch überspringen, wenn der Kurs bereits mehr als + X×ATR über/unter dem Level steht („zu spät, der Zug ist weg") + D) B + Bremse, aber die übersprungenen Ausbrüche NACHTRÄGLICH prüfen — steigt + man nur später ein oder gar nicht? (Hier: gar nicht, wie live.) + +VORAB FIXIERTE ENTSCHEIDUNGSREGEL: + Eine Bremse X wird nur empfohlen, wenn sie B in **ΣR und ØR in BEIDEN Hälften** + schlägt UND die beiden Nachbar-Schwellen ebenfalls besser als B sind. + +Sequentiell mit EINEM Positions-Slot (wie live), kanonischer Exit, Echtkosten. + +Aufruf: python backtest_squeeze_entry.py [bars] +""" +import sys + +import MetaTrader5 as mt5 + +from backtest_cost_gate import _atr_series, _berlin, kandidaten, kennzahlen +from core.exit_model import LIVE, simulate + +_SCHWELLEN = (0.10, 0.15, 0.20, 0.30, 0.40) + + +def lauf(K, H, L, C, lo, hi, am_level: bool, max_abstand: float | None): + """Sequentiell, EIN Slot. `am_level`=True → Backtest-Annahme, sonst Bar-Close. + `max_abstand` in ×ATR → Ausbruch überspringen, wenn der Kurs schon weiter ist.""" + rs, abst = [], [] + frei_ab = lo + for k in K: + i = k["i"] + if not (lo <= i < hi) or i < frei_ab: + continue + d, lvl, atr = k["d"], k["lvl"], k["atr"] + weg = (C[i] - lvl) * d / atr # wie weit ist der Kurs schon drüber? + if max_abstand is not None and weg > max_abstand: + continue # zu spät → gar nicht einsteigen + entry = lvl if am_level else C[i] + r, xb = simulate(entry, d, atr, H, L, C, i + 1, LIVE, + timestop=True, use_tp=True, ret_bar=True) + # Kosten immer auf den ATR bezogen; beim Bar-Close kommt der bereits + # gelaufene Weg als echter Nachteil über den schlechteren `entry` rein. + rs.append(r - k["kosten"]) + abst.append(weg) + frei_ab = xb + 1 + return rs, abst + + +def lauf_limit(K, H, L, C, lo, hi, warte: int, puffer: float = 0.0): + """Variante D — LIMIT am Ausbruchs-Level statt Market. + + Die Order liegt `warte` Bars lang am Level (+`puffer`×ATR in Trade-Richtung + zurückversetzt = etwas günstiger). Kommt der Kurs zurück → Fill zum Limit-Preis; + kommt er nicht zurück → **kein Trade** (der Zug ist weg, das ist der Preis). + Damit wird Variante A überhaupt erst erreichbar: dort wird der Level-Preis + einfach unterstellt, obwohl der Kurs ihn schon verlassen hat.""" + rs = [] + frei_ab = lo + verpasst = 0 + for k in K: + i = k["i"] + if not (lo <= i < hi) or i < frei_ab: + continue + d, atr = k["d"], k["atr"] + limit = k["lvl"] - d * puffer * atr + fill = None + for j in range(i + 1, min(i + 1 + warte, len(C) - 1)): + # LONG: Limit unter dem Kurs → Fill, wenn das Bar-Tief es erreicht + if (L[j] <= limit) if d > 0 else (H[j] >= limit): + fill = j + break + if fill is None: + verpasst += 1 + continue + r, xb = simulate(limit, d, atr, H, L, C, fill + 1, LIVE, + timestop=True, use_tp=True, ret_bar=True) + rs.append(r - k["kosten"]) + frei_ab = xb + 1 + return rs, verpasst + + +def zeile(lbl, k1, k2, extra=""): + def f(k): + if not k: + return f"{'—':>34}" + return (f"{k['n']:>5} {k['wr']:>5.1f}% {k['oer']:>+7.3f} " + f"{k['pf']:>5.2f} {k['sr']:>+8.1f}") + print(f" {lbl:<30}{f(k1)} |{f(k2)} {extra}") + + +def main(): + n_bars = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 + mt5.initialize() + sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") + if mt5.symbol_info(c)), None) + bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n_bars) + point = mt5.symbol_info(sym).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]; T = [int(b["time"]) for b in bars] + SP = [float(b["spread"]) * point for b in bars] + A = _atr_series(H, L, C) + N = len(C); mid = N // 2 + + print("=" * 104) + print(f" SQUEEZE-EINSTIEG: am Level vs. hinterhergelaufen — {sym} M5, {N} Bars") + print(f" {_berlin(T[0]):%d.%m.%Y} bis {_berlin(T[-1]):%d.%m.%Y} · 2 Halbjahre · " + f"sequentiell (EIN Slot) · kanonischer Exit · Echtkosten") + print("=" * 104) + K = kandidaten(H, L, C, T, SP, A) + + # Wie weit läuft der Kurs typischerweise, bis der Ausbruch sichtbar ist? + weg = sorted((C[k["i"]] - k["lvl"]) * k["d"] / k["atr"] for k in K) + print(f" Abstand Kurs↔Level beim ERSTEN Sichtbarwerden des Ausbruchs (×ATR):") + print(f" Median {weg[len(weg)//2]:.2f} · 75 % {weg[3*len(weg)//4]:.2f} · " + f"90 % {weg[int(0.9*len(weg))]:.2f} · Max {weg[-1]:.2f} (n={len(weg)})") + print(f" Der Realfall vom 05.08. lag bei 0,28×ATR — " + f"{100.0*sum(1 for w in weg if w <= 0.28)/len(weg):.0f} % der Fälle sind näher dran.") + print() + + print(f" {'Variante':<30}{'H1 n WR OeR PF SigmaR':>34}" + f" |{'H2 n WR OeR PF SigmaR':>34}") + print(" " + "-" * 100) + erg = {} + for lbl, am_level, mx in (("A Einstieg AM LEVEL", True, None), + ("B Einstieg am Bar-Close", False, None)): + r1, _ = lauf(K, H, L, C, 0, mid, am_level, mx) + r2, _ = lauf(K, H, L, C, mid, N, am_level, mx) + erg[lbl] = (kennzahlen(r1), kennzahlen(r2)) + zeile(lbl, *erg[lbl]) + print(" " + "-" * 100) + for x in _SCHWELLEN: + lbl = f"C Bremse: max {x:.2f}xATR" + r1, _ = lauf(K, H, L, C, 0, mid, False, x) + r2, _ = lauf(K, H, L, C, mid, N, False, x) + erg[lbl] = (kennzahlen(r1), kennzahlen(r2)) + zeile(lbl, *erg[lbl]) + + print(" " + "-" * 100) + for warte, puf in ((3, 0.0), (6, 0.0), (12, 0.0), (6, 0.1), (12, 0.1)): + lbl = f"D Limit am Level {warte}B" + (f" +{puf:.1f}" if puf else "") + r1, v1 = lauf_limit(K, H, L, C, 0, mid, warte, puf) + r2, v2 = lauf_limit(K, H, L, C, mid, N, warte, puf) + erg[lbl] = (kennzahlen(r1), kennzahlen(r2)) + zeile(lbl, *erg[lbl], extra=f"verpasst {v1}/{v2}") + + a1, a2 = erg["A Einstieg AM LEVEL"] + b1, b2 = erg["B Einstieg am Bar-Close"] + print() + print("=" * 104) + print(" BEFUND 1 — was kostet das Hinterherlaufen? (A gegen B)") + print("=" * 104) + print(f" OeR H1 {a1['oer']:+.3f} -> {b1['oer']:+.3f} ({b1['oer']-a1['oer']:+.3f})" + f" H2 {a2['oer']:+.3f} -> {b2['oer']:+.3f} ({b2['oer']-a2['oer']:+.3f})") + print(f" SigmaR H1 {a1['sr']:+.1f} -> {b1['sr']:+.1f} ({b1['sr']-a1['sr']:+.1f})" + f" H2 {a2['sr']:+.1f} -> {b2['sr']:+.1f} ({b2['sr']-a2['sr']:+.1f})") + print(" ⚠ B ist der LIVE-Fall. Weicht es stark ab, gilt die validierte") + print(" Squeeze-Kennzahl (+0,14…+0,23 OeR) fuer den Live-Betrieb NICHT.") + + print() + print("=" * 104) + print(" BEFUND 2 — hilft eine Abstands-Bremse? (Regel VOR dem Lauf fixiert:") + print(" SigmaR UND OeR in BEIDEN Haelften > B, plus beide Nachbarn)") + print("=" * 104) + + def schlaegt(x): + k1, k2 = erg[f"C Bremse: max {x:.2f}xATR"] + return (k1 and k2 and k1["sr"] > b1["sr"] and k2["sr"] > b2["sr"] + and k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"]) + + treffer = [] + for idx, x in enumerate(_SCHWELLEN): + nb = [_SCHWELLEN[j] for j in (idx - 1, idx + 1) if 0 <= j < len(_SCHWELLEN)] + selbst, nb_ok = schlaegt(x), all(schlaegt(m) for m in nb) + if selbst and nb_ok: + treffer.append(x) + print(f" X = {x:.2f}xATR " + + ("BESTEHT" if (selbst and nb_ok) else + "schlaegt B, Nachbarn NICHT" if selbst else "faellt durch")) + print() + if treffer: + print(f" => Kandidat(en): {', '.join(f'{x:.2f}' for x in treffer)}") + else: + print(" => KEINE Schwelle besteht. Eine Abstands-Bremse ist nicht belegt —") + print(" uebersprungene Ausbrueche waeren im Schnitt NICHT schlechter.") + + print() + print("=" * 104) + print(" BEFUND 3 — schlaegt eine LIMIT-Order am Level den Market-Einstieg?") + print(" (Regel: SigmaR UND OeR in BEIDEN Haelften > B, Nachbarn ebenso)") + print("=" * 104) + d_treffer = [] + d_keys = [k for k in erg if k.startswith("D ")] + for k in d_keys: + k1, k2 = erg[k] + gut = (k1 and k2 and k1["sr"] > b1["sr"] and k2["sr"] > b2["sr"] + and k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"]) + if gut: + d_treffer.append(k) + print(f" {k:<30} {'BESSER als Market' if gut else 'faellt durch'}") + print() + if d_treffer: + print(f" => {len(d_treffer)} von {len(d_keys)} Limit-Varianten schlagen den") + print(" Market-Einstieg in beiden Haelften.") + else: + print(" => Auch die Limit-Order rettet es nicht.") + print() + + +if __name__ == "__main__": + main()