#!/usr/bin/env python3
"""Plausibilitaets-Test der M15-Karte an EINEM Handelstag.

⚠⚠ WAS DAS IST UND WAS NICHT. Ein Tag ist KEIN Edge-Nachweis — die
2-Stichproben-Regel gilt unveraendert, und im Projekt sind Einzeltag-Anekdoten
schon zweimal als Beleg missbraucht worden (15-Minuten-Regel 31.07.,
Nacht-Guard 05.08.). Geprueft wird deshalb nur die MECHANIK:
  · Wie oft steht die Karte auf gruen / gelb / rot?
  · Sind die Gates zu streng (nie gruen) oder zu lasch (dauernd gruen)?
  · WELCHES Gate blockt wie oft — dieselbe Telemetrie-Frage wie bei
    `block_reason`, die den 93-%-WARTEN-Befund erst erklaerbar gemacht hat.
  · Und als Kontext, nicht als Beleg: was waere nach einem gruenen Status
    passiert?

⚠ ZWEI SCHWELLEN SIND GESETZT, NICHT GEMESSEN — und der Test soll das sichtbar
machen, nicht verstecken:
  (a) „Level in Reichweite" = 0,5×ATR_M15 — von mir gewaehlt.
  (b) „Bias einig" verlangt M30 UND H1 in derselben Richtung. Gemessen ist nur
      der M30-GEGEN-Trend-Filter (Edge ×2); H1 ist im Projekt Konfluenz-BONUS,
      kein Pflicht-Gate. Die Und-Verknuepfung ist strenger als alles Belegte.
Beide Varianten werden deshalb mitgerechnet, damit der Preis der Strenge
sichtbar wird.

Aufruf:  python test_m15_karte_tag.py [YYYY-MM-DD]      (Default 2026-08-07)
"""
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.sr_levels import halten, linien, naechste

_BERLIN = ZoneInfo("Europe/Berlin")
_BROKER = timezone(timedelta(hours=3))
_HTF_DEAD = 0.15        # Totband wie _HTF_DEADBAND
_PIV_K = 3
_LOOK = 50              # M30-Pivot-Fenster wie live


def _berlin(ts):
    return datetime.fromtimestamp(ts, timezone.utc).replace(
        tzinfo=_BROKER).astimezone(_BERLIN)


def ema(v, p):
    k = 2 / (p + 1); out = [v[0]]
    for x in v[1:]:
        out.append(out[-1] + k * (x - out[-1]))
    return out


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 htf_sign(H, L, C, step):
    """Trend-Vorzeichen der hoeheren TF je M15-Index — LOOK-AHEAD-frei."""
    hh, hl, hc = [], [], []
    for s in range(0, len(C) - step + 1, step):
        hh.append(max(H[s:s + step])); hl.append(min(L[s:s + step]))
        hc.append(C[s + step - 1])
    if len(hc) < 55:
        return [0] * len(C)
    ef, es = ema(hc, 12), ema(hc, 50)
    ha = atr_series(hh, hl, hc)
    sg = []
    for j in range(len(hc)):
        a = ha[j]
        if j < 52 or not a:
            sg.append(0); continue
        d = ef[j] - es[j]
        sg.append(0 if abs(d) < _HTF_DEAD * a else (1 if d > 0 else -1))
    out = [0] * len(C)
    for i in range(len(C)):
        j = i // step - 1
        out[i] = sg[j] if 0 <= j < len(sg) else 0
    return out


def m30_levels(H, L, C):
    """Rohe M30-Pivots je M15-Index (2 M15-Bars = 1 M30-Bar), kausal."""
    st = 2
    mh = [max(H[s:s + st]) for s in range(0, len(C) - st + 1, st)]
    ml = [min(L[s:s + st]) for s in range(0, len(C) - st + 1, st)]
    out = [None] * len(C)
    for i in range(len(C)):
        j = i // st - 1
        if j < _PIV_K + 5:
            continue
        a, b = max(0, j - _LOOK), j + 1
        hs, ls = mh[a:b], ml[a:b]
        ph = [round(hs[x], 3) for x in range(_PIV_K, len(hs) - _PIV_K)
              if hs[x] == max(hs[x - _PIV_K:x + _PIV_K + 1])]
        pl = [round(ls[x], 3) for x in range(_PIV_K, len(ls) - _PIV_K)
              if ls[x] == min(ls[x - _PIV_K:x + _PIV_K + 1])]
        out[i] = (ph, pl)
    return out


def main():
    tag = sys.argv[1] if len(sys.argv) > 1 else "2026-08-07"
    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, 4000)
    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)
    S30, S1H = htf_sign(H, L, C, 2), htf_sign(H, L, C, 4)
    LV = m30_levels(H, L, C)

    idx = [i for i, t in enumerate(T) if _berlin(t).strftime("%Y-%m-%d") == tag]
    if not idx:
        print(f"keine M15-Bars fuer {tag}"); return 1

    print("=" * 92)
    print(f"  M15-KARTE — Mechanik-Test an {tag} ({sym}, {len(idx)} M15-Bars)")
    print("  ⚠ EIN Tag = kein Edge-Nachweis. Geprueft wird nur, ob die Gates")
    print("    plausibel greifen (zu streng / zu lasch) und welches blockt.")
    print("=" * 92)

    zaehler = {"gruen": 0, "gelb": 0, "rot_bias": 0, "rot_kosten": 0, "kein_level": 0}
    zaehler_lax = dict(zaehler)      # Variante: nur „nicht gegen M30"
    gruene = []
    prev_r = prev_s = None
    for i in idx:
        a = A[i]
        if not a or not LV[i]:
            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
        kr = SP[i] / a
        kOk = kr < 0.32
        gaps = [abs(x - C[i]) / a for x in (r, s) if x is not None]
        nah = [g for g in gaps if g <= 0.5]

        for name, bOk, d in (("streng", S30[i] != 0 and S30[i] == S1H[i], S30[i]),
                             ("lax", S30[i] != 0, S30[i])):
            z = zaehler if name == "streng" else zaehler_lax
            if not bOk:            z["rot_bias"] += 1
            elif not kOk:          z["rot_kosten"] += 1
            elif not gaps:         z["kein_level"] += 1
            elif not nah:          z["gelb"] += 1
            else:
                z["gruen"] += 1
                if name == "streng":
                    lvl = min((x for x in (r, s) if x is not None),
                              key=lambda x: abs(x - C[i]))
                    gruene.append((i, d, lvl, a, kr))

    n = len(idx)
    print(f"\n  {'Status':<16}{'STRENG (M30 UND H1)':>22}{'LAX (nur nicht gegen M30)':>28}")
    for k, lbl in (("gruen", "🟢 SETUP BEREIT"), ("gelb", "🟡 warten auf Level"),
                   ("rot_bias", "🔴 Bias"), ("rot_kosten", "🔴 Kosten"),
                   ("kein_level", "kein Level")):
        print(f"  {lbl:<16}{zaehler[k]:>10} ({100*zaehler[k]/n:>4.0f} %)"
              f"{zaehler_lax[k]:>16} ({100*zaehler_lax[k]/n:>4.0f} %)")

    print(f"\n  ── Was folgte auf 🟢? (KONTEXT, kein Beleg — n={len(gruene)}) ──")
    if not gruene:
        print("    kein einziges gruenes Signal an diesem Tag.")
    else:
        Rs = []
        for i, d, lvl, a, kr in gruene:
            R = simulate(lvl, d, a, H, L, C, i + 1, LIVE)
            Rs.append(R - kr)
        w = sum(1 for x in Rs if x > 0)
        print(f"    Trefferquote {100*w/len(Rs):.0f} %   ØR {sum(Rs)/len(Rs):+.3f}   "
              f"ΣR {sum(Rs):+.2f}")
        print(f"    Zeiten: " + ", ".join(f"{_berlin(T[i]):%H:%M}" for i, *_ in gruene[:12]))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
