From 9e5ea22e08cc4eb2fa6561beee2b037600a5483d Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Wed, 12 Aug 2026 09:40:21 +0200 Subject: [PATCH] Wiedereinstieg nach falschem S/R-Close gemessen - nicht gebaut User-Frage: wenn die Linie trotz Abprall-Berechnung bricht, waere ein Neustart des Trades logisch. Der Fall ist kein Randfall - P(break) liegt live bei 38,7 %, das Gate schliesst 94-97 % aller Beruehrungen. Erst ein eigener Messfehler: der erste Lauf meldete 93 % Brueche bei einer Basisrate von 38,7 %. Ursache - ich prueft nur EINE Barriere. Das Training entscheidet per Rennen (Bestaetigung gegen Ablehnung, wer zuerst kommt); eine 0,5xATR-Bewegung binnen 60 min passiert fast immer. Mit dem Rennen: 33 % Brueche, praktisch die Basisrate. Die Plausibilitaetspruefung hat den Fehler gefangen. Ergebnis: n=14, Treffer 71 %, OeR +0,859, ~+501 EUR - aber das 95-%-KI ist [-0,084 ... +1,788] und enthaelt die Null. Und drei Trades tragen alles: die drei groessten Gewinne stammen alle vom 04.08. (TP-Deckel, zusammen +529,92 EUR), die uebrigen 11 ergeben -28,63 EUR. Der 04.08. ist der dokumentierte Ausreisser-Tag. Urteil: nicht bauen. Die vorab fixierte Regel ist verfehlt. Fairerweise widerlegt der bestehende Wiedereinstiegs-Cooldown die Idee nicht - er betraf den ungegateten Sofort-Wiedereinstieg, nicht den bestaetigten Bruch. Sauberer naechster Schritt waere ein Backtest ueber 2 Halbjahre statt 43 Live-Closes. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 45 ++++++++++ analyze_srclose_reentry.py | 166 +++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 analyze_srclose_reentry.py diff --git a/CLAUDE.md b/CLAUDE.md index 49ef42d..7ae297d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6161,6 +6161,51 @@ Neumessung. Sie bleiben unverändert und reproduzierbar; wo ihre Schlussfolgerun relevant wird, gehört sie live-treu **neu gerechnet** (Muster: `backtest_auto_signal_norev.py`). +## ⚠⚠ WIEDEREINSTIEG NACH EINEM „FALSCHEN" S/R-CLOSE — gemessen, NICHT gebaut (2026-08-12) + +User: „falls die Linie trotz Abprall-Berechnung durchbrochen wird — logisch wäre, +den Trade wieder neu zu starten, oder?" +⚠ Der Fall ist **kein Randfall**: P(break) liegt live bei 38,7 %, das Gate +schliesst 94–97 % aller Berührungen — in rund vier von zehn Closes bricht die +Linie doch. `analyze_srclose_reentry.py`, 43 echte Closes ab dem Nachtrainieren. + +⚠⚠ **ERST EIN EIGENER MESSFEHLER, und er ist die halbe Lehre.** Der erste Lauf +meldete **93 % Brüche** bei einer Basisrate von 38,7 %. Ursache: ich prüfte nur +EINE Barriere („erreicht der Kurs Level +0,5×ATR?"). Das Training entscheidet per +**Rennen** — Bestätigung *gegen* Ablehnung, wer zuerst kommt. Eine 0,5×ATR- +Bewegung binnen 60 min passiert fast immer. Mit dem Rennen: **33 % Brüche**, +praktisch die Basisrate. ✅ Die Plausibilitätsprüfung („die Bruchquote MUSS nahe +38 % liegen") hat den Fehler gefangen. + +**Ergebnis (ruhende Stop-Order an der Bestätigungsmarke, kanonischer Exit):** +| | | +|---|---| +| n | 14 | +| Trefferquote | 71 % | +| ØR | **+0,859** | +| **95-%-KI** | **[−0,084 … +1,788]** ⚠ enthält die Null | +| grob EUR | +501 | +⚠⚠ **ABER: drei Trades tragen alles.** Die drei grössten Gewinne stammen ALLE vom +**04.08. 13:06/13:10/13:44** und liegen exakt am TP-Deckel (+3,44/+3,44/+3,45 R): +| | | +|---|---| +| diese drei | **+529,92 €** | +| **die übrigen 11** | **−28,63 €** | +Der 04.08. ist der dokumentierte Ausreisser-Tag (WTI −4,12 $, davon −4,16 $ in +EINER Stunde). **Ohne ihn ist die Regel negativ.** + +✅ **URTEIL: nicht bauen — aber die beste Idee seit Längerem.** Die vorab fixierte +Regel (ΣR positiv UND KI ohne Null) ist verfehlt. ⚠ Dazu die dokumentierte +Gegenevidenz: der **Wiedereinstiegs-Cooldown** (`_SQUEEZE_SR_COOLDOWN_S`=180 s, +seit 17.07.) wurde gebaut, WEIL ein sofortiger Gleichrichtungs-Wiedereinstieg +nach einem S/R-Close real schädlich war (1-Sekunden-Roundtrip, doppelter Spread). +⚠ Fairerweise: das ist nicht dasselbe — gemessen wurde hier der **bestätigte** +Bruch, nicht der ungegatete Sofort-Wiedereinstieg. Der Cooldown widerlegt die +Idee also nicht, er warnt nur vor ihrer schlampigen Variante. +✅ **Der saubere nächste Schritt** ist ein Backtest über 2 Halbjahre statt 43 +Live-Closes — die Maschinerie existiert (`backtest_pbreak_rvalue.py` simuliert +den P(break)-Close bereits). Erst dann ist die Frage entscheidbar. + ## ⚠⚠ DAS FLACKERN DER M15-KARTE — Ursache gemessen, Hysterese eingebaut (2026-08-12) Die Karte wechselte den Zustand **alle 12 Sekunden** (~1.030/Tag). Das ist der diff --git a/analyze_srclose_reentry.py b/analyze_srclose_reentry.py new file mode 100644 index 0000000..8880483 --- /dev/null +++ b/analyze_srclose_reentry.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Nach einem S/R-Close bricht die Linie doch — lohnt der WIEDEREINSTIEG? + +ANLASS (2026-08-12, User: „falls die Linie trotz Abprall-Berechnung durchbrochen +wird — logisch waere den Trade wieder neu zu starten, oder?"). + +⚠ Der Fall ist kein Randfall: P(break) liegt live bei **38,7 %**, und das Gate +schliesst 94–97 % aller Beruehrungen. In rund **vier von zehn** Closes bricht die +Linie also doch. Die Frage ist damit handelsrelevant, nicht theoretisch. + +⚠⚠ WAS DAGEGEN SPRICHT, BEVOR MAN MISST — beides ist dokumentiert: + (1) Es gibt bereits einen **Wiedereinstiegs-Cooldown** (`_SQUEEZE_SR_COOLDOWN_S` + = 180 s, seit 17.07.): nach einem S/R-Close darf der Auto-Squeeze **nicht in + GLEICHER Richtung** sofort wieder oeffnen. Gebaut wegen eines real + beobachteten 1-Sekunden-Roundtrips (LONG-Close +15 €, 1 s spaeter neuer + LONG, doppelter Spread). Der Wiedereinstieg ist also schon einmal als + schaedlich eingestuft worden — allerdings als SOFORTIGER, ungegateter. + (2) Jeder Wiedereinstieg kostet sofort einen vollen Spread: auf M5 im Median + **0,193×ATR**. Der Durchbruch muss das erst verdienen. + +METHODE — kontrafaktisch auf den ECHTEN Closes: + · alle `closed_by='sr_close'`-Trades ab dem Nachtrainieren (31.07.) + · Level ≈ Ausstiegspreis (dort wurde geschlossen) + · **Bruch = Bestaetigung**, nicht die nackte Levelzahl: Kurs erreicht + `level + 0,5×ATR` in Trade-Richtung — dieselbe Definition wie im + P(break)-TRAINING (`_BRK_ATR=0,5`), sonst zaehlt jeder Docht als Bruch. + · Wiedereinstieg als **ruhende Stop-Order** an der Bestaetigungsmarke, + Fuellung bei BERUEHRUNG, kanonischer Exit, Echtkosten. + · Fenster: 12 M5-Bars = 60 min (wie das Trainings-Fenster `_BRK_W`). + +⚠ ENTSCHEIDUNGSREGEL, VORAB: gebaut wird nur, wenn der Wiedereinstieg in Summe +positiv ist UND das 95-%-Bootstrap-Intervall die Null ausschliesst. n ist klein +(~40 Closes) — ein ✅ waere hier ein Hinweis, kein Beleg; ein ❌ ist belastbar. + +Aufruf: python analyze_srclose_reentry.py +""" +from __future__ import annotations + +import bisect +import datetime as dt +import random +import sqlite3 + +import MetaTrader5 as mt5 + +from core.exit_model import LIVE, simulate + +DB = "oil_widget_history.db" +_SEIT = dt.datetime(2026, 7, 31, 12).timestamp() # P(break) nachtrainiert +_BRK_ATR = 0.5 # Bestaetigung wie im Training +_BRK_W = 12 # 12 M5-Bars = 60 min +_EURF = 87.6 # EUR je $ je Lot (robust aus 251 Trades geschaetzt) + + +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 ci(v, n=4000, seed=42): + if len(v) < 8: + return None, None + r = random.Random(seed); m = len(v) + b = sorted(sum(v[r.randrange(m)] for _ in range(m)) / m for _ in range(n)) + return b[int(.025 * n)], b[int(.975 * n)] + + +def main(): + db = sqlite3.connect(DB); db.row_factory = sqlite3.Row + tr = [dict(r) for r in db.execute( + "SELECT ticket,exit_time,exit_price,direction,lots,pnl FROM trades " + "WHERE closed_by='sr_close' AND exit_time>=? AND exit_price IS NOT NULL " + "ORDER BY exit_time", (_SEIT,))] + 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"]) - 10800 for b in bars] # -> lokale Epoch + 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) + + print("=" * 94) + print(f" WIEDEREINSTIEG NACH S/R-CLOSE — {len(tr)} Closes ab 31.07. ({sym})") + print(" Bruch = Kurs erreicht Level ± 0,5×ATR in Trade-Richtung binnen 60 min") + print(" (dieselbe Definition wie im P(break)-Training — ein Docht zaehlt NICHT)") + print("=" * 94) + + gebrochen, Rs, eur = 0, [], [] + zeilen = [] + for t in tr: + i = bisect.bisect_right(T, t["exit_time"]) - 1 + if i < 20 or i >= len(C) - LIVE.max_hold - 2 or not A[i]: + continue + d = 1 if (t["direction"] or "").upper() in ("BUY", "LONG") else -1 + atr = A[i] + lvl = float(t["exit_price"]) + marke = lvl + d * _BRK_ATR * atr # Bestaetigung (Bruch) + gegen = lvl - d * _BRK_ATR * atr # Ablehnung (Abprall) + # ⚠⚠ BARRIEREN-RENNEN, nicht eine Schwelle (Korrektur 2026-08-12). + # Der erste Entwurf fragte nur „erreicht der Kurs die Bestaetigung?" + # — und meldete **93 % Brueche** bei einer Basisrate von 38,7 %. + # Kein Marktbefund, sondern ein Code-Befund: eine 0,5×ATR-Bewegung + # binnen 60 min passiert fast immer. Das Training entscheidet per + # RENNEN (confirm gegen reject, wer zuerst kommt) — genauso hier. + # ⚠ Innerhalb EINES Bars nicht aufloesbar → pessimistisch die + # Ablehnung zaehlen (wie `simulate` bei SL/TP im selben Bar). + fill = None + for j in range(i + 1, min(i + 1 + _BRK_W, len(C) - LIVE.max_hold - 2)): + traf_gegen = (L[j] <= gegen) if d > 0 else (H[j] >= gegen) + traf_marke = (H[j] >= marke) if d > 0 else (L[j] <= marke) + if traf_gegen: + break # Level hat gehalten → kein Wiedereinstieg + if traf_marke: + fill = j; break + if fill is None: + continue # Level hielt oder unentschieden + gebrochen += 1 + kost = (SP[fill] if SP[fill] > 0 else 0.0225) / atr + R = simulate(marke, d, atr, H, L, C, fill + 1, LIVE) - kost + Rs.append(R) + e = R * atr * (t["lots"] or 1.0) * _EURF + eur.append(e) + zeilen.append((dt.datetime.fromtimestamp(t["exit_time"]), d, lvl, marke, R, e)) + + n_aus = len([t for t in tr if bisect.bisect_right(T, t["exit_time"]) - 1 >= 20]) + print(f"\n auswertbar: {n_aus} Closes") + print(f" davon Linie GEBROCHEN (bestaetigt): {gebrochen} " + f"= {100*gebrochen/max(1,n_aus):.0f} %") + if not Rs: + print(" → kein einziger bestaetigter Bruch. Der Wiedereinstieg haette nie gefeuert.") + return 0 + + lo, hi = ci(Rs) + w = sum(1 for x in Rs if x > 0) + print(f"\n ── der Wiedereinstieg (ruhende Stop-Order an der Bestaetigung) ──") + print(f" n={len(Rs)} Treffer {100*w/len(Rs):.0f} % ØR {sum(Rs)/len(Rs):+.3f} " + f"ΣR {sum(Rs):+.2f}") + print(f" 95-%-KI des ØR: " + + (f"[{lo:+.3f} … {hi:+.3f}]" if lo is not None else "— zu wenig")) + print(f" grob in EUR: {sum(eur):+.2f} (Ø {sum(eur)/len(eur):+.2f} je Wiedereinstieg)") + + print(f"\n {'Zeit':<14}{'Ri':<4}{'Level':>9}{'Einstieg':>10}{'R':>8}{'EUR':>10}") + for z in zeilen[:20]: + print(f" {z[0]:%d.%m %H:%M} {'▲' if z[1]>0 else '▼':<3}{z[2]:>9.3f}" + f"{z[3]:>10.3f}{z[4]:>+8.2f}{z[5]:>+10.2f}") + + print("\n " + "-" * 90) + print(" ⚠ REGEL (vorab): bauen nur, wenn ΣR positiv UND das KI die Null") + print(" ausschliesst. n ist klein — ein ✅ ist ein Hinweis, kein Beleg.") + print(" ⚠ Nicht modelliert: der Slot ist waehrend des Wiedereinstiegs belegt,") + print(" andere Setups koennen in der Zeit nicht feuern.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())