#!/usr/bin/env python3
"""Die NEUEN Handbuch-Muster messen (User-Vorgabe 2026-07-31 „messe die neuen Muster").

Ergänzt wurden am selben Tag: **Dreifach-Top/-Boden · Flagge · Wimpel · Rechteck ·
Steigender/Fallender Keil**. Sie stehen in `core/patterns.py` auf `measured=False`
und tragen die Verdict-Stimme NICHT — dieses Skript prüft, ob sich das ändern darf.

⚠ ZWEI UNTERSCHIEDE ZU `backtest_patterns.py` (bewusst):
 1. **Der ECHTE Detektor wird benutzt** (`core.patterns._detect`), nicht eine
    Nachbildung. Das alte Skript hat die Bedingungen in `scan()` NACHGEBAUT — misst
    also möglicherweise etwas anderes als das, was live läuft. Nach den fünf
    Deployment-Drift-Fällen dieses Tages ist das keine akzeptable Grundlage mehr.
 2. **Exit aus `core/exit_model.py`** statt einer eigenen Kopie (das alte Skript war
    ein FÜNFTES Exit-Modell: Trail fest 1,5 · `_MAXH` 288 · kein Time-Stop).

KONTROLLGRUPPE (entscheidend): derselbe Einstieg/Exit auf einem **generischen
Swing-Bruch** — Bruch des letzten Swing-Hochs/-Tiefs ohne jedes Muster. Nur wenn ein
Muster die Kontrolle in BEIDEN Hälften schlägt, trägt das MUSTER etwas bei und nicht
bloss „Breakout + Trailing" (die Lehre aus dem 30.07.-Kontrolltest).

Zusätzlich: **Ziel-Trefferquote** — wie oft wird der Measured Move vor dem SL
erreicht? Das ist der ehrliche Muster-Check (alt: nur 13–38 %).
"""
import sys
from collections import defaultdict

import MetaTrader5 as mt5

from core.exit_model import LIVE, simulate
from core.patterns import _atr, _pivots, _detect, _PIVOT_K, _MEASURED

_BREAK_WIN = 48      # so viele Bars auf den Trigger-Bruch warten
_ATRMIN = 0.06
_NEW = ("triple_top", "triple_bottom", "flag", "pennant", "wedge_rise", "wedge_fall")


def _atr_series(H, L, C, p=14):
    out = [None]
    trs = []
    for i in range(1, len(C)):
        trs.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
        out.append(sum(trs[-p:]) / min(len(trs), p))
    return out


def scan(H, L, C, A, SP, lo, hi, step=3):
    """Läuft die History kausal ab und sammelt (typ, entry_bar, dir, trigger, target).

    Nutzt den ECHTEN Detektor auf dem jeweils bis `i` bekannten Fenster.
    """
    found = []
    seen = set()
    for i in range(max(lo, 120), min(hi, len(C) - LIVE.max_hold - 1), step):
        atr = A[i]
        if not atr or atr < _ATRMIN:
            continue
        piv = _pivots(H[:i + 1], L[:i + 1], _PIVOT_K)
        if len(piv) < 3:
            continue
        try:
            pats = _detect(piv, C[i], atr, i)
        except Exception:
            continue
        for m in pats:
            if m.get("status") != "forming" or m.get("dir") not in ("bull", "bear"):
                continue
            key = (m["type"], round(m["trigger"], 2), m["dir"])
            if key in seen:
                continue
            seen.add(key)
            d = 1 if m["dir"] == "bull" else -1
            trig = m["trigger"]
            # auf den Trigger-Bruch warten
            for j in range(i + 1, min(i + 1 + _BREAK_WIN, len(C) - LIVE.max_hold - 1)):
                broke = (H[j] >= trig) if d > 0 else (L[j] <= trig)
                if broke:
                    found.append((m["type"], j, d, trig, m.get("target"), max(A[j] or 0, _ATRMIN)))
                    break
    return found


def control(H, L, C, A, lo, hi, step=3):
    """KONTROLLE: Bruch des letzten Swing-Hochs/-Tiefs, ohne jedes Muster."""
    out = []
    seen = set()
    for i in range(max(lo, 120), min(hi, len(C) - LIVE.max_hold - 1), step):
        atr = A[i]
        if not atr or atr < _ATRMIN:
            continue
        piv = _pivots(H[:i + 1], L[:i + 1], _PIVOT_K)
        if len(piv) < 2:
            continue
        for d, kind in ((1, "H"), (-1, "L")):
            last = next((p for p in reversed(piv) if p[2] == kind), None)
            if not last:
                continue
            trig = last[1]
            key = (kind, round(trig, 2))
            if key in seen:
                continue
            seen.add(key)
            for j in range(i + 1, min(i + 1 + _BREAK_WIN, len(C) - LIVE.max_hold - 1)):
                broke = (H[j] >= trig) if d > 0 else (L[j] <= trig)
                if broke:
                    out.append(("control", j, d, trig, None, max(A[j] or 0, _ATRMIN)))
                    break
    return out


def evaluate(rows, H, L, C, SP):
    """→ {typ: [(R_netto, ziel_getroffen)]}"""
    res = defaultdict(list)
    for typ, j, d, trig, target, atr in rows:
        cost = (SP[j] if SP[j] > 0 else 0.0225) / atr
        R = simulate(trig, d, atr, H, L, C, j + 1, LIVE) - cost
        hit = None
        if target is not None:
            hit = False
            sl = trig - d * LIVE.sl_atr * atr
            for k in range(j + 1, min(j + 1 + LIVE.max_hold, len(C))):
                if (L[k] <= sl) if d > 0 else (H[k] >= sl):
                    break
                if (H[k] >= target) if d > 0 else (L[k] <= target):
                    hit = True; break
        res[typ].append((R, hit))
    return res


def rep(name, rows):
    if not rows or len(rows) < 8:
        print(f"    {name:<24} n={len(rows):<4} — zu wenige")
        return
    Rs = [r for r, _ in rows]
    n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
    g = sum(x for x in Rs if x > 0); l = -sum(x for x in Rs if x < 0)
    hits = [h for _, h in rows if h is not None]
    zt = f"{100*sum(hits)/len(hits):>3.0f}%" if hits else "  —"
    print(f"    {name:<24} n={n:<4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} "
          f"PF={(g/l if l > 0 else 9.99):>4.2f} ΣR={s:>+6.0f} Ziel={zt}")


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 120000
    mt5.initialize()
    sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") if mt5.symbol_info(c)), None)
    bars = None
    for req in (n, 100000, 80000, 60000, 40000):
        bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M30, 0, req)
        if bars is not None and len(bars) > 3000:
            break
    si = mt5.symbol_info(sym); point = si.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); N = len(C); mid = N // 2

    print("=" * 96)
    print(f"  NEUE HANDBUCH-MUSTER — {sym} M30 ({N} Bars), Echtkosten")
    print(f"  Detektor: core/patterns._detect (der ECHTE) · Exit: core/exit_model.py "
          f"(Trail {LIVE.mult})")
    print("  Urteil: nur wenn ein Typ die KONTROLLE (generischer Swing-Bruch) in BEIDEN")
    print("  Hälften schlägt, trägt das MUSTER etwas bei — sonst ist es Breakout+Trailing.")
    print("=" * 96)

    label = {"triple_top": "Dreifach-Top", "triple_bottom": "Dreifach-Boden",
             "flag": "Flagge", "pennant": "Wimpel",
             "wedge_rise": "Steigender Keil", "wedge_fall": "Fallender Keil"}
    for lbl, a, b in (("H1 (alt)", _PIVOT_K, mid), ("H2 (neu)", mid, N - LIVE.max_hold - 1)):
        print(f"\n{lbl}:")
        res = evaluate(scan(H, L, C, A, SP, a, b), H, L, C, SP)
        ctl = evaluate(control(H, L, C, A, a, b), H, L, C, SP)
        rep("KONTROLLE Swing-Bruch", ctl["control"])
        print("    " + "-" * 74)
        for t in _NEW:
            rep(label[t], res.get(t, []))
        print("    " + "-" * 74)
        for t in sorted(_MEASURED):
            if res.get(t):
                rep(f"(alt) {t}", res[t])


if __name__ == "__main__":
    main()
