#!/usr/bin/env python3 """Die zwei GERATENEN Schwellen der M15-Karte messen. ANLASS (2026-08-08). Der Wochentest hat zwei Schwächen offengelegt: (1) **Reichweite 0,5×ATR** — von mir GESETZT, nie gemessen. Sie ist zugleich der EINZIGE Gate, der überhaupt diskriminiert (das Kosten-Gate blockte 0 von 644 Bars, das Veto trägt nur +0,04 R). (2) **Kosten-Schwelle 0,32** — stammt von M5 (`backtest_realcosts.py`: Ø 0,265×ATR, Nacht 0,32–0,50 = Kostenfalle). Auf M15 liegt Spread/ATR bei ~0,10, die Schwelle bindet dort NIE. Beides wird hier über 2 Halbjahre auf der ECHTEN Population gemessen: Level-Berührungen auf M15, Einstieg AM Level in der vom HTF erlaubten Richtung, kanonischer Exit, Echtkosten, Episoden-entkoppelt (nur die erste Bar je zusammenhängendem Block — sonst zählt man dieselbe Marktlage vielfach, der Fehler aus dem ersten Tagestest). ⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: · Eine Schwelle wird nur übernommen, wenn der Vorteil in BEIDEN Hälften besteht UND die Nachbar-Schwellen nicht kippen. · Zusätzlich das 95-%-Bootstrap-Intervall — bei den kleinen Effekten dieser Population (ØR-Bereich ±0,1) ist ein Punktschätzer keine Aussage. Genau daran ist heute schon die Slippage-Regel gescheitert. Aufruf: python backtest_m15_gates.py [n_M15_bars] (Default 26000) """ from __future__ import annotations import random import sys import MetaTrader5 as mt5 from core.exit_model import LIVE, simulate from core.sr_levels import halten, linien, naechste from test_m15_karte_tag import atr_series, htf_sign, m30_levels _REICH_STUFEN = (0.1, 0.2, 0.3, 0.5, 0.75, 1.0) _KOST_STUFEN = (0.06, 0.08, 0.10, 0.13, 0.16, 0.20) def sammle(H, L, C, SP, A, S30, LV, lo, hi): """Alle Level-Berührungen als Episoden — mit gap und Kostenquote je Fall.""" out = [] prev_r = prev_s = None vor = False for i in range(lo, min(hi, len(C) - LIVE.max_hold - 2)): a = A[i] if not a or not LV[i]: vor = False; continue ph, pl = LV[i] lv = linien(ph, pl, a) r, s = naechste(lv, C[i]) r = halten(r, prev_r, C[i], a, True) s = halten(s, prev_s, C[i], a, False) prev_r, prev_s = r, s gaps = [(abs(x - C[i]) / a, x) for x in (r, s) if x is not None] if not gaps: vor = False; continue g, lvl = min(gaps) # „am Level" im weitesten geprueften Sinn — die Schwelle wird UNTEN variiert nah = g <= max(_REICH_STUFEN) if nah and not vor and S30[i]: d = 1 if S30[i] == 1 else -1 out.append({"gap": g, "kost": SP[i] / a, "i": i, "d": d, "lvl": lvl, "atr": a}) vor = nah return out def rechne(eintraege, H, L, C, warte=8): """⚠⚠ FILL BEI BERUEHRUNG — nicht sofort zum Level-Preis. Der erste Entwurf stieg direkt bei `lvl` ein, auch wenn der Kurs 0,9×ATR entfernt stand. Das schenkt der Sim einen um genau diesen Abstand besseren Einstieg und erzeugte das Scheinergebnis „weiter weg ist besser" (ØR +0,29 im obersten Abstands-Bucket). Derselbe Fehler wie beim Momentum-Test, wo ein ØR von +2,2 den Code entlarvte: **eine Zahl, die zu gut ist, ist ein Befund ueber den Code, nicht ueber den Markt.** Jetzt liegt die Order `warte` Bars am Level; wird sie nicht beruehrt, faellt der Fall raus (`R = None`).""" for e in eintraege: i, lvl, d = e["i"], e["lvl"], e["d"] fill = None for j in range(i + 1, min(i + 1 + warte, len(C) - LIVE.max_hold - 2)): if L[j] <= lvl <= H[j]: fill = j; break e["R"] = (None if fill is None else simulate(lvl, d, e["atr"], H, L, C, fill + 1, LIVE) - e["kost"]) return [e for e in eintraege if e["R"] is not None] def ci(Rs, n=3000, seed=42): if len(Rs) < 20: return None, None r = random.Random(seed); m = len(Rs) b = [sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n)] b.sort(); return b[int(.025 * n)], b[int(.975 * n)] def tabelle(titel, h1, h2, schluessel, stufen, kanten_text): print(f"\n {titel}") print(f" {kanten_text:<16}{'H1 n':>7}{'H1 ØR':>9}{'H1 95-%-KI':>22}" f"{'H2 n':>7}{'H2 ØR':>9}{'H2 95-%-KI':>22}") for lo_s, hi_s in zip((0,) + stufen[:-1], stufen): z = [] for teil in (h1, h2): grp = [e["R"] for e in teil if lo_s <= e[schluessel] < hi_s] if len(grp) < 20: z += [str(len(grp)), "—", "—"]; continue m = sum(grp) / len(grp); a, b = ci(grp) z += [str(len(grp)), f"{m:+.3f}", f"[{a:+.3f} … {b:+.3f}]" if a is not None else "—"] print(f" {f'{lo_s:.2f}–{hi_s:.2f}':<16}{z[0]:>7}{z[1]:>9}{z[2]:>22}" f"{z[3]:>7}{z[4]:>9}{z[5]:>22}") def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 26000 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_M15, 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) S30 = htf_sign(H, L, C, 2) LV = m30_levels(H, L, C) mid = len(C) // 2 r1 = sammle(H, L, C, SP, A, S30, LV, 60, mid) r2 = sammle(H, L, C, SP, A, S30, LV, mid, len(C)) h1 = rechne(r1, H, L, C); h2 = rechne(r2, H, L, C) print(f" Fill-Quote: H1 {len(h1)}/{len(r1)} = {100*len(h1)/max(1,len(r1)):.0f} %" f" H2 {len(h2)}/{len(r2)} = {100*len(h2)/max(1,len(r2)):.0f} %") print("=" * 100) print(f" M15-GATES MESSEN — {sym}, {len(C)} Bars H1 n={len(h1)} H2 n={len(h2)}") print(" Population: Level-Berührungen, Einstieg AM Level in HTF-Richtung,") print(" kanonischer Exit, Echtkosten, Episoden-entkoppelt.") print("=" * 100) tabelle("① REICHWEITE — ab welchem Abstand trägt der Einstieg noch?", h1, h2, "gap", _REICH_STUFEN, "Abstand ×ATR") tabelle("② KOSTEN — ab welcher Quote wird es teuer? (M5-Schwelle 0,32 bindet nie)", h1, h2, "kost", _KOST_STUFEN, "Spread/ATR") ks = sorted(e["kost"] for e in h1 + h2) gs = sorted(e["gap"] for e in h1 + h2) print(f"\n Verteilungen: Spread/ATR Median {ks[len(ks)//2]:.3f} " f"(90-Perz. {ks[int(0.9*len(ks))]:.3f}) · " f"Abstand Median {gs[len(gs)//2]:.2f}×ATR") print("\n ⚠ Übernommen wird eine Schwelle nur, wenn der Vorteil in BEIDEN") print(" Hälften besteht, die Nachbarn nicht kippen UND das KI die Null") print(" ausschliesst. Punktschätzer allein sind auf dieser Population keine Aussage.") return 0 if __name__ == "__main__": raise SystemExit(main())