#!/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())
