diff --git a/backtest_srclose_reentry.py b/backtest_srclose_reentry.py new file mode 100644 index 0000000..a89dd6a --- /dev/null +++ b/backtest_srclose_reentry.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Wiedereinstieg nach einem „falschen" S/R-Close — ueber 2 Halbjahre. + +ANLASS (2026-08-12, User). Der Live-Lauf (`analyze_srclose_reentry.py`, 43 echte +Closes) sah gut aus — 71 % Treffer, ØR +0,859, ~+501 € — war aber **nicht +entscheidbar**: das 95-%-KI enthielt die Null, und **drei Trades eines einzigen +Crash-Tages (04.08.) trugen +530 €, die uebrigen elf −29 €**. Hier dieselbe Frage +auf 80k M5-Bars in zwei Halbjahren. + +DIE FRAGE: Das P(break)-Gate schliesst, wenn das Level wahrscheinlich HAELT. +In ~38 % der Faelle bricht es doch. Lohnt es, dann wieder einzusteigen? + +METHODE — bewusst LIVE-TREU, nicht nachgebaut: + · Level via `build_levels_live` + `_cluster` + `pick_level` aus + `backtest_pbreak_retrain` (dieselbe Hysterese wie `_draw_levels`). + · P(break) mit den **echten Live-Gewichten** `_PB_MU/_SD/_W` aus `core.engine` + — kein Nachbau, damit das Gate hier genau so entscheidet wie im Betrieb. + · Schliess-Moment = „am Level" (≤0,15×ATR, wie live `at_level`) UND + `p_break < sr_close_pbreak` (live 0,35). + · Bruch = **Barrieren-Rennen** `outcome()` (Bestaetigung gegen Ablehnung, wer + zuerst; Timeout = Abprall) — dieselbe Definition wie im Modell-TRAINING. + ⚠ Genau hier lag der Fehler im ersten Live-Lauf: nur EINE Barriere gefragt + → 93 % „Brueche" bei einer Basisrate von 38 %. + · Wiedereinstieg = ruhende Stop-Order an der Bestaetigungsmarke, Fuellung bei + BERUEHRUNG, **kanonischer Exit** (`core.exit_model.LIVE`), Echtkosten. + +⚠⚠ ENTSCHEIDUNGSREGEL, VORAB FIXIERT: + (1) ØR in BEIDEN Haelften > 0, (2) 95-%-Bootstrap-KI schliesst in beiden die + Null aus, (3) die Nachbar-Schwellen (0,3 / 0,7×ATR Bestaetigung) kippen nicht. + Alle drei muessen erfuellt sein. + +Aufruf: python backtest_srclose_reentry.py [n_bars] (Default 80000) +""" +from __future__ import annotations + +import random +import sys + +import MetaTrader5 as mt5 + +from backtest_pbreak_retrain import (_ATRMIN, _BRK_W, _M30, _TWIN, _ZONE, + _atr_series, _ema_series, + build_levels_live) +from core.engine import _p_break +from core.exit_model import LIVE, simulate +# ⚠ Clusterung/Auswahl/Hysterese kommen aus dem LIVE-Modul, nicht aus dem +# Backtest-Nachbau: `_cluster` dort laeuft ueber den GEMISCHTEN ph+pl-Pool und +# erzeugt Ketten-Mega-Cluster — im Live-Code ist genau das als behobener Fehler +# dokumentiert. `linien()` clustert ph und pl GETRENNT. +from core.sr_levels import halten, linien, naechste + +_SCHWELLE = 0.35 # live `sr_close_pbreak` +_BRK_STUFEN = (0.3, 0.5, 0.7) + + +def ci(v, n=4000, seed=42): + if len(v) < 20: + 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 lauf(lo, hi, H, L, C, SP, A, EF, ES, LV, brk): + """Alle Schliess-Momente durchgehen; bei bestaetigtem Bruch wieder einsteigen.""" + zu, gebrochen, Rs = 0, 0, [] + prev_r = prev_s = None + letzt = {} # Dedup: (gerundetes Level, Seite) -> Bar + for i in range(lo, min(hi, len(C) - LIVE.max_hold - _BRK_W - 2)): + atr = A[i] + if not atr or atr < _ATRMIN: + continue + # ⚠ `build_levels_live` ist nach M30-Bars indiziert, nicht nach M5. + m = i // _M30 + if m >= len(LV): + continue + lvd = LV[m] + px = C[i] + lv = linien(lvd["ph"], lvd["pl"], atr) # ph/pl GETRENNT geclustert + r_, s_ = naechste(lv, px) + r_ = halten(r_, prev_r, px, atr, True) + s_ = halten(s_, prev_s, px, atr, False) + prev_r, prev_s = r_, s_ + mom6 = (C[i] - C[i - 6]) / atr + mom3 = (C[i] - C[i - 3]) / atr + ediff = EF[i] - ES[i] + for d, lvl in ((1, r_), (-1, s_)): + if lvl is None: + continue + gap = abs(lvl - px) + if gap > _ZONE * atr: + continue # nicht am Level + k = (round(lvl, 2), d) + if i - letzt.get(k, -999) < _TWIN: + continue # dieselbe Beruehrung + letzt[k] = i + wt = 1.0 if ediff * d > 0 else 0.0 + p = _p_break(mom6 * d, mom3 * d, wt, gap / atr) + if p >= _SCHWELLE: + continue # Gate haette NICHT geschlossen + zu += 1 + # Barrieren-Rennen mit der jeweils geprueften Bestaetigungsbreite + up = lvl + d * brk * atr + dn = lvl - d * brk * atr + fill = None + for j in range(i + 1, min(i + 1 + _BRK_W, len(C))): + if (L[j] <= dn) if d > 0 else (H[j] >= dn): + break # Level hielt + if (H[j] >= up) if d > 0 else (L[j] <= up): + fill = j; break + if fill is None: + continue + gebrochen += 1 + kost = (SP[fill] if SP[fill] > 0 else 0.0225) / atr + Rs.append(simulate(up, d, atr, H, L, C, fill + 1, LIVE) - kost) + return zu, gebrochen, Rs + + +def zeig(lbl, zu, gebr, Rs): + if not Rs: + return f" {lbl:<22}{zu:>7}{gebr:>9} keine Wiedereinstiege" + n = len(Rs); 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) + lo, hi = ci(Rs) + cis = f"[{lo:+.3f} … {hi:+.3f}]" if lo is not None else "—" + flag = "✅" if (lo is not None and lo > 0) else ("❌" if (hi is not None and hi < 0) else "⚠") + return (f" {lbl:<22}{zu:>7}{gebr:>9}{100*gebr/max(1,zu):>7.0f} %" + f"{n:>7}{100*w/n:>7.0f} %{sum(Rs)/n:>+9.3f}" + f"{(g/v if v else 9.99):>7.2f}{sum(Rs):>+9.1f} {cis:>20} {flag}") + + +def main(): + n = 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) + 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] + SP = [float(b["spread"]) * point for b in bars] + A = _atr_series(H, L, C) + EF, ES = _ema_series(C, 12), _ema_series(C, 50) + LV = build_levels_live(H, L) + mid = len(C) // 2 + + print("=" * 118) + print(f" WIEDEREINSTIEG NACH S/R-CLOSE — {sym}, {len(C)} M5-Bars, 2 Halbjahre") + print(f" Schliess-Moment: am Level (≤{_ZONE}×ATR) UND p_break < {_SCHWELLE}") + print(" Bruch = Barrieren-Rennen (Bestaetigung gegen Ablehnung, Timeout = Abprall)") + print(" Wiedereinstieg: ruhende Stop-Order, Fill bei Beruehrung, kanonischer Exit") + print("=" * 118) + print(f"\n {'Bestaetigung':<22}{'Closes':>7}{'Brueche':>9}{'Quote':>8}" + f"{'n':>7}{'Treffer':>8}{'ØR':>9}{'PF':>7}{'ΣR':>9} {'95-%-KI':>20}") + for brk in _BRK_STUFEN: + for lbl, (a, b) in (("H1", (60, mid)), ("H2", (mid, len(C)))): + zu, gebr, Rs = lauf(a, b, H, L, C, SP, A, EF, ES, LV, brk) + print(zeig(f"{brk:.1f}×ATR {lbl}", zu, gebr, Rs)) + print() + + print(" " + "-" * 114) + print(" ⚠ REGEL (vorab): ØR beidhaelftig > 0 UND beide KI ohne Null UND die") + print(" Nachbar-Schwellen kippen nicht. Alle drei muessen erfuellt sein.") + print(" ⚠ Nicht modelliert: der Positions-Slot ist waehrend des Wiedereinstiegs") + print(" belegt — andere Setups koennen in der Zeit nicht feuern.") + print(" ⚠ Die Bruchquote MUSS nahe der Basisrate (~38 %) liegen. Tut sie das") + print(" nicht, ist die Bruch-Definition kaputt (so geschehen im Live-Lauf: 93 %).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())