From 35e91474e80cd5a3585ee51cc9526fb37985b5c4 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Tue, 11 Aug 2026 09:01:34 +0200 Subject: [PATCH] Auto-Squeeze Zuverlaessigkeitspruefung + Wochen-Simulation Befund 0 beantwortet die Frage vorweg: der Trader ist AUS. runtime_state.json enthaelt "auto_squeeze": false und ueberschreibt die ini (true) beim Start - dieselbe Falle wie am 31.07. bei auto_signal, nur mit umgekehrtem Vorzeichen. Letzter Squeeze-Trade 06.08. 13:52, entry_count 0. Der Config-Waechter meldet es korrekt. Befund 1: die Mechanik arbeitet. Die Simulation nutzt die echte _squeeze_one und reproduziert 10 von 11 Live-Trades binnen 10 min auf wenige Cent genau. Befund 2: Woche n=47, Treffer 57 %, OeR +0,196, PF 1,37, SumR +9,20 (~+610 EUR bei 2,5 Lots) - im validierten Band. ABER: die Sim ueberhandelt um das 2,4-fache (kein Live-Dedup/Cooldown), der Edge stirbt bei 0,20xATR Fill-Slippage, und eine Woche ist Rauschen - die Phase mit AUSgeschaltetem Trader war sogar die bessere. Befund 3: auf denselben 10 Einstiegen war der LIVE-Exit 70 EUR BESSER als der kanonische (-109,52 gegen -179,27). Das widerspricht dem B4-Befund vom 01.08. ("die Luecke entsteht im Exit"). Der S/R-Close rettete drei Trades. n=10, gemischt - kein Urteil, aber ein Warnschild gegen den umgekehrten Schluss. Befund 4: B5 bei 11/20 Trades, beide Bedingungen weit verletzt (Verhaeltnis 0,17, PF 0,45 bei 73 % Trefferquote). Formal nicht faellig. Offen: zwei Live-BUYs am 06.08. 13:51/13:52 zum identischen Preis auf einen Ausbruch - moeglicher Doppel-Einstieg, ungeprueft. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 47 +++++++++++ backtest_squeeze_woche.py | 169 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 backtest_squeeze_woche.py diff --git a/CLAUDE.md b/CLAUDE.md index 5cb9f47..41c03a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2523,6 +2523,53 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Konf-Gate, Dedup, WARTEN-Re-Arm, Nachkauf, Gegen-Position, Cooldown, Toggle, Nacht-Sperre · 9 Adverse: über/unter Schwelle, zu früh, im Plus, SHORT-Richtung, manueller Trade, aus, 1×-je-Ticket, Startup-Grace) — alle korrekt. +- **⚠⚠ ZUVERLÄSSIGKEITS-PRÜFUNG 2026-08-11 (`backtest_squeeze_woche.py`, User: + „funktioniert der Breakout-Auto-Trader zuverlässig? simuliere die letzte Woche").** + ⚠⚠ **BEFUND 0 beantwortet die Frage vorweg: DER TRADER IST AUS.** + `runtime_state.json` enthält `"auto_squeeze": false` und **überschreibt die ini** + (`= true`) beim Start — dieselbe Falle wie am 31.07. bei `auto_signal`, nur mit + umgekehrtem Vorzeichen. Letzter Squeeze-Trade **06.08. 13:52**, + `squeeze_entry_count = 0`. ✅ Der Config-Wächter meldet es korrekt („ist 'false' + — validiert gegen 'true'"); es wurde nur nicht gehandelt. Zurück: 🚀 BRK. + ✅ **BEFUND 1 — die MECHANIK arbeitet korrekt.** Die Simulation nutzt die echte + `_squeeze_one` und reproduziert **10 von 11 Live-Trades binnen 10 min und auf + wenige Cent genau** (live 06.08 02:11 SELL 75,127 ↔ Sim 02:10 SHORT 75,132). + Die Erkennung ist nicht das Problem. + **BEFUND 2 — die Woche** (ruhende Stop-Order, Fill bei BERÜHRUNG, Nacht 3–6 + gesperrt, EIN Slot, kanonischer Exit, Echtkosten): + | Fill-Slippage | n | Treffer | ØR | PF | ΣR | + |---|---|---|---|---|---| + | **0,00×ATR** | 47 | 57 % | **+0,196** | 1,37 | +9,20 | + | 0,10×ATR | 47 | 57 % | +0,100 | 1,17 | +4,69 | + | **0,20×ATR** | 46 | 50 % | **−0,013** | 0,98 | −0,58 | + ≈ **+610 €** bei 2,5 Lots, ØR im validierten Band (+0,14…+0,23). ⚠⚠ **Drei + Vorbehalte:** **(1)** die Sim **überhandelt um das 2,4-fache** (26 statt 11 + Einstiege in der An-Phase) — ihr fehlen Live-Dedup (dir, level), 3-min- + Wiedereinstiegssperre und S/R-Cooldown; ΣR ist nach oben verzerrt. **(2)** der + Edge **stirbt bei 0,20×ATR Fill-Slippage**, und live lag der Abstand + Einstieg↔Level vor dem Pending-Umbau bei Median **+0,275×ATR** — alles hängt + daran, ob die ruhenden Orders am Level füllen (`squeeze_entry_gap`: **8/12**). + **(3)** eine Woche ist Rauschen — die Phase, in der der Trader **AUS** war, ist + sogar die bessere (ØR +0,287 gegen +0,122). + ⚠⚠ **BEFUND 3 — der GEPAARTE Vergleich widerspricht dem B4-Befund vom 01.08.** + Auf **denselben 10 Einstiegen**: **LIVE −109,52 €** gegen **kanonischer Exit + −179,27 €** — der Live-Exit war **70 € BESSER**. Am 01.08. hiess es „die Lücke + entsteht im EXIT"; hier ist es umgekehrt. Im Detail rettete der **S/R-Close** + drei Trades (05.08 08:33 **+12,56 gegen −113,64** · 06.08 13:51/52 je + **+0,26/+2,88 gegen −86,38**), während der kanonische Exit bei 06.08 09:04 + (**−46,82 gegen −175,21**) und 02:11 (**+23,83 gegen −43,29**) klar besser war. + **Gemischt, n=10, von wenigen Trades dominiert — kein Urteil, aber ein + Warnschild gegen den umgekehrten Schluss.** + **BEFUND 4 — B5: 11 von 20 Trades (55 %).** Beide Bedingungen sind bereits weit + verletzt (je Lot ØGewinn 5,20 · ØVerlust 31,12 → **Verhältnis 0,17**, **PF + 0,45**) bei **73 % Trefferquote** — die dokumentierte Squeeze-Signatur. ⚠ Formal + **nicht fällig**; die Latte nachträglich zu senken wäre genau der Fehler, gegen + den sie vorab fixiert wurde. + ⚠ **OFFEN, nicht verifiziert:** am 06.08. **13:51 und 13:52** stehen ZWEI + Live-BUYs zum identischen Preis 76,315 auf EINEN Ausbruch (beide Pending-Fills, + beide binnen einer Minute per `sr_close` geschlossen) — sieht nach + **Doppel-Einstieg auf ein Signal** aus. Ungeprüft, ob Defekt im OCO-Ersatz oder + Teilfüllung. - **⚠⚠ AUTO-SQUEEZE B4-BEFUND 2026-08-01: der EINSTIEG ist intakt, der EXIT hat den Edge gefressen — deshalb NICHT zurückgebaut, aber mit vorab fixierter Abbruchregel.** Anlass war die User-Frage zu den negativen Auto-Trades vom 31.07. Der B4-Monitor diff --git a/backtest_squeeze_woche.py b/backtest_squeeze_woche.py new file mode 100644 index 0000000..22bf882 --- /dev/null +++ b/backtest_squeeze_woche.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Haette der Auto-Squeeze in der letzten Woche Gewinn gemacht? + +ANLASS (2026-08-11, User: „analysiere den Breakout-Auto-Trader, ob er zuverlaessig +funktioniert; simuliere ueber die letzte Woche, ob er Gewinn erzielt haette"). + +⚠⚠ WAS DIESE SIMULATION IST UND WAS NICHT. **Eine Woche ist KEIN Edge-Nachweis.** +Der Squeeze feuert 2–3×/Woche; das sind Stichproben von n≈2–6, und im Projekt sind +Einzelzeitraeume schon dreimal faelschlich zum Beleg erhoben worden (15-Minuten- +Regel 31.07., Nacht-Guard 05.08., das Veto im Wochentest 08.08.). Gemessen wird +hier die **MECHANIK**: feuert die Regel ueberhaupt, zu welchen Preisen, und was +haette der kanonische Exit daraus gemacht. + +MODELLIERT WIRD DER LIVE-PFAD (Stand seit 05.08.): + · echte Live-Funktion `WaveRecommender._squeeze_one` — kein Nachbau + · **ruhende Stop-Order** an der Box-Grenze, solange `armed`; Fuellung bei + BERUEHRUNG (nicht bei Bar-Schluss). Das ist der teurere und richtige Fall: + (a) „Close bricht Level" ist Look-ahead und rund +0,32 R zu optimistisch + (`backtest_squeeze_touchfill.py`). + · Nacht-Sperre 3–6 Uhr Berlin (`auto_squeeze_night_hours`) + · ATR-Floor, EIN Positions-Slot (sequentiell), Echtkosten aus dem Bar-Spread + · **kanonischer Exit** `core.exit_model.LIVE` — ausdruecklich NICHT + `squeeze_scan.exit_legacy` (Trail 1,5 · kein Lock · kein Time-Stop · kein TP), + gegen den der B4-Monitor bis heute misst. + +Aufruf: python backtest_squeeze_woche.py [tage] (Default 7) +""" +from __future__ import annotations + +import sys +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate +from core.wave_rec import WaveRecommender + +_BERLIN = ZoneInfo("Europe/Berlin") +_BROKER = timezone(timedelta(hours=3)) +_NACHT = {3, 4, 5, 6} +_ATR_MIN = 0.06 +_SLIPPAGE = (0.0, 0.05, 0.10, 0.20) # ×ATR Aufschlag auf den Fill + + +def _berlin(ts): + return datetime.fromtimestamp(ts, timezone.utc).replace( + tzinfo=_BROKER).astimezone(_BERLIN) + + +def atr_series(H, L, C, p=14): + tr = [0.0] + for i in range(1, len(C)): + tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) + out = [None] + for i in range(1, len(C)): + seg = tr[max(1, i - p + 1):i + 1] + out.append(sum(seg) / len(seg)) + return out + + +def lauf(T, H, L, C, SP, A, lo_i, slip=0.0, nacht=True): + """Sequentiell EIN Slot, ruhende Stop-Order, Fuellung bei Beruehrung.""" + trades = [] + i = lo_i + n = len(C) + while i < n - LIVE.max_hold - 2: + atr = A[i] + if not atr or atr < _ATR_MIN: + i += 1; continue + if nacht and _berlin(T[i]).hour in _NACHT: + i += 1; continue + sq = WaveRecommender._squeeze_one(H[:i + 1], L[:i + 1], C[:i + 1], atr) + if not sq or sq.get("box_hi") is None or sq.get("state") is None: + i += 1; continue + # ⚠ Die Box stammt aus den N bereits GESCHLOSSENEN Bars vor i — eine + # ruhende Order darf waehrend Bar i also legitim gefuellt werden. + up = sq["box_hi"] + 0.1 * atr + dn = sq["box_lo"] - 0.1 * atr + traf_up = H[i] >= up + traf_dn = L[i] <= dn + if not (traf_up or traf_dn): + i += 1; continue + if traf_up and traf_dn: + # Beide Seiten in EINEM Bar — aus OHLC nicht aufloesbar. Die naeher + # am Open liegende Seite wird zuerst beruehrt worden sein. + traf_up = abs(up - C[i - 1]) <= abs(C[i - 1] - dn) + traf_dn = not traf_up + d = 1 if traf_up else -1 + fill = (up + slip * atr) if traf_up else (dn - slip * atr) + kost = (SP[i] if SP[i] > 0 else 0.0225) / atr + R, xb = simulate(fill, d, atr, H, L, C, i + 1, LIVE, ret_bar=True) + trades.append({"i": i, "t": T[i], "d": d, "fill": fill, "atr": atr, + "R": R - kost, "bars": xb - i, + "box_atr": sq.get("box_atr")}) + i = xb + 1 # Slot war belegt + return trades + + +def kz(tr, lbl): + if not tr: + return f" {lbl:<34} keine Ausbrueche" + n = len(tr); Rs = [x["R"] for x in tr] + w = sum(1 for x in Rs if x > 0) + g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0) + ow = g / w if w else 0.0 + ol = v / (n - w) if n - w else 0.0 + return (f" {lbl:<34} n={n:>3} Treffer {100*w/n:>3.0f} % OeR {sum(Rs)/n:>+6.3f} " + f"PF {(g/v if v else 9.99):>4.2f} Verh {(ow/ol if ol else 9.99):>4.2f} " + f"SumR {sum(Rs):>+6.2f}") + + +def main(): + tage = int(sys.argv[1]) if len(sys.argv) > 1 else 7 + 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, 20000) + point = mt5.symbol_info(sym).point + mt5.shutdown() + T = [int(b["time"]) for b in bars] + 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] + A = atr_series(H, L, C) + + grenze = T[-1] - tage * 86400 + lo_i = next(i for i, t in enumerate(T) if t >= grenze) + lo_i = max(lo_i, 60) + + print("=" * 100) + print(f" AUTO-SQUEEZE — Simulation der letzten {tage} Tage ({sym})") + print(f" {_berlin(T[lo_i]):%d.%m %H:%M} bis {_berlin(T[-1]):%d.%m %H:%M} " + f"{len(C)-lo_i} M5-Bars") + print(" Live-Pfad: ruhende Stop-Order an der Box, Fuellung bei BERUEHRUNG,") + print(" Nacht 3-6 gesperrt, EIN Slot, kanonischer Exit, Echtkosten.") + print("=" * 100) + + basis = lauf(T, H, L, C, SP, A, lo_i) + print("\n ── Fill-Slippage (eine Stop-Order fuellt zum Markt, nicht zum Ideal) ──") + for s in _SLIPPAGE: + print(kz(lauf(T, H, L, C, SP, A, lo_i, slip=s), f"Slippage +{s:.2f}xATR")) + + print("\n ── Gegenprobe: ohne Nacht-Sperre ──") + print(kz(lauf(T, H, L, C, SP, A, lo_i, nacht=False), "alle Stunden")) + + print(f"\n ── die einzelnen Ausbrueche (Slippage 0) ──") + if not basis: + print(" KEINER — die Regel hat in diesem Fenster nicht gefeuert.") + for x in basis: + print(f' {_berlin(x["t"]):%d.%m %H:%M} {"LONG " if x["d"]>0 else "SHORT"} ' + f'@ {x["fill"]:.3f} Box {x["box_atr"]:.2f}xATR ' + f'{x["bars"]*5:>4} min R {x["R"]:>+6.2f}') + + if basis: + # EUR-Naeherung: R x ATR x Lots x ~87,6 EUR/$/Lot (robust geschaetzt, s. CLAUDE.md) + lots = 2.5 + eur = sum(x["R"] * x["atr"] for x in basis) * lots * 87.6 + print(f"\n grob in EUR bei {lots} Lots: {eur:>+8.0f} EUR") + print(" ⚠ Naeherung — die echte Lot-Groesse schwankt mit dem Konto.") + + print("\n " + "-" * 96) + print(" ⚠⚠ EINE WOCHE IST KEIN EDGE-NACHWEIS. Der Squeeze feuert 2-3x/Woche;") + print(" n dieser Groesse ist Rauschen. Die 2-Stichproben-Regel gilt weiter.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())