#!/usr/bin/env python3
"""S/R-verankertes TRAILING statt fixem ATR-Abstand (User-Idee 2026-07-31).

FRAGE: Der Live-Trail zieht den SL stur `mult×ATR` hinter das Hoch. Wäre es besser,
ihn hinter das **nächste S/R-Level** zu legen (LONG: knapp unter die Unterstützung)?
Und/oder einen TP ans Gegenlevel zu setzen?

⚠ ABGRENZUNG — zwei der drei Teil-Ideen sind SCHON gemessen und durchgefallen:
  * **Initial-SL an S/R** → `backtest_sl_method.py`: gedeckelt identisch zu fix
    2,0×ATR (Ø-R 0,196–0,201), der Band-Cap dominiert; ungedeckelt Worst −11,4×ATR.
  * **Fester TP am Gegenlevel** → `backtest_srclose.py` (2×): verdoppelt die WR,
    verschlechtert aber PF/ΣR in BEIDEN Hälften = Gewinner-Kappen.
  * **TRAILING-SL an S/R** = hier NEU gemessen (bisher nur ATR-Abstände getestet,
    `backtest_trailing.py`).

Deshalb wird der TP-Teil hier NUR als Kontrollvariante mitgeführt (Erwartung: fällt
durch — dient als Sanity-Check, dass die Sim mit dem alten Befund übereinstimmt).

AUFBAU (identisch zu `backtest_trailing.py`, damit die Zahlen vergleichbar sind):
echte Phasen-Mechanik (Init→Trail→Lock, HW-Ratsche, Breakeven-Boden 1,3), Signal-Set
Trend+Reversal, 2 Halbjahre. Level-Quelle = **M30-Pivots** (k=3) — dieselbe Quelle,
auf die der Live-Bot am 2026-07-30 umgestellt hat; kausal (ein Pivot ist erst 3 M30-
Bars später bekannt).

Urteil nur „trägt", wenn eine Variante in BEIDEN Hälften ≥ Live-Basis liegt UND der
Worst-Case nicht davonläuft (die Lehre aus `backtest_sl_method.py`).
"""
import sys
from bisect import bisect_left

import MetaTrader5 as mt5

from core.analysis import calc_trend_angle
from core.wave_rec import (_EMA_FAST, _EMA_SLOW, _N_BARS, _ANGLE_LR, _ANGLE_DEAD,
                           _REVERSAL_STRETCH, _STRETCH_MAX)

_MAXH = 200; _ATRMIN = 0.12; _TP_INIT = 3.5
_LOCK_START = 3.5; _LOCK_SCALE = 0.6; _LOCK_MIN = 1.2
_BE = 1.3                 # Breakeven-Boden (live)
_M30 = 6                  # M5-Bars je M30-Bar
_PIV_K = 3                # Pivot-Fenster (wie live `pb_levels30`)
_PIV_LOOKBACK = 50        # letzte N M30-Bars (wie live)
# Sicherheits-Deckel für den S/R-Trail: ohne den droht der −11,4×ATR-Fall aus
# `backtest_sl_method.py` (Level zu weit weg = Kontokiller unter Margin-Sizing).
_SL_MIN_ATR = 0.5
_SL_MAX_ATR = 2.0


def _ema_series(v, p):
    k = 2.0 / (p + 1); o = []; e = v[0]
    for i, x in enumerate(v):
        e = x if i == 0 else x * k + e * (1 - k)
        o.append(e)
    return o


def _atr_series(H, L, C, p=14):
    t = [0.0]
    for i in range(1, len(C)):
        t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
    out = []
    for i in range(len(C)):
        if not i:
            out.append(None); continue
        w = t[max(1, i - p + 1):i + 1]
        out.append(sum(w) / max(1, len(w)))
    return out


def build_levels(H, L):
    """M30-Pivots (k=3) je M5-Bar-Index — KAUSAL.

    Liefert `lv[m]` = sortierte Level-Liste, die zum Beginn von M30-Bar `m` bereits
    bekannt ist (ein Pivot bei M30-Index p ist erst ab p+k bestätigt).
    """
    nm = len(H) // _M30
    mh = [max(H[i * _M30:(i + 1) * _M30]) for i in range(nm)]
    ml = [min(L[i * _M30:(i + 1) * _M30]) for i in range(nm)]
    # Pivot bei p → bekannt ab p + _PIV_K
    born: list[list[float]] = [[] for _ in range(nm)]
    for p in range(_PIV_K, nm - _PIV_K):
        seg_h = mh[p - _PIV_K:p + _PIV_K + 1]
        seg_l = ml[p - _PIV_K:p + _PIV_K + 1]
        if mh[p] == max(seg_h):
            if p + _PIV_K < nm:
                born[p + _PIV_K].append(mh[p])
        if ml[p] == min(seg_l):
            if p + _PIV_K < nm:
                born[p + _PIV_K].append(ml[p])
    # rollierendes Fenster der letzten _PIV_LOOKBACK M30-Bars
    lv: list[list[float]] = []
    from collections import deque
    win: deque = deque()
    cur: list[float] = []
    for m in range(nm):
        win.append(born[m])
        cur.extend(born[m])
        while len(win) > _PIV_LOOKBACK:
            old = win.popleft()
            for x in old:
                try:
                    cur.remove(x)
                except ValueError:
                    pass
        lv.append(sorted(cur))
    return lv


def _sr_below(levels, px):
    i = bisect_left(levels, px)
    return levels[i - 1] if i > 0 else None


def _sr_above(levels, px):
    i = bisect_left(levels, px)
    return levels[i] if i < len(levels) else None


def sim(entry, d, atr, H, L, C, j0, LV, mode="atr", mult=1.5,
        buf=0.25, trail_start=0.3, use_sr_tp=False):
    """Phasen-Trailing. `mode`:
       'atr'    — Live: SL = HW − mult×ATR
       'sr'     — SL = nächstes Level in Gegenrichtung ∓ buf×ATR (auf [0,5..2,0]×ATR gedeckelt)
       'hybrid' — der ENGERE von beiden (schützt Gewinne früher)
       'sr_wide'— der WEITERE von beiden (gibt dem Trade mehr Luft)
    """
    sl = entry - d * 2.0 * atr          # Initial-SL wie live (2,0×ATR)
    tp = entry + d * _TP_INIT * atr
    if use_sr_tp:
        lv = LV[min(j0 // _M30, len(LV) - 1)]
        t = _sr_above(lv, entry) if d > 0 else _sr_below(lv, entry)
        if t is not None:
            tp = t
    hw = entry; rank = 0
    end = min(j0 + _MAXH, len(C) - 1)
    for j in range(j0, end + 1):
        hi, lo = H[j], L[j]
        if (lo <= sl) if d > 0 else (hi >= sl):
            return (sl - entry) * d / atr      # pessimistisch: SL vor TP im selben Bar
        if (hi >= tp) if d > 0 else (lo <= tp):
            return (tp - entry) * d / atr
        hw = max(hw, hi) if d > 0 else min(hw, lo)
        profit = (hw - entry) * d
        ph = 0 if profit < trail_start * atr else (1 if profit < _LOCK_START * atr else 2)
        if ph < rank:
            ph = rank
        rank = ph
        if ph == 0:
            continue
        m = mult if ph == 1 else max(_LOCK_MIN, mult * _LOCK_SCALE)
        cand_atr = hw - d * m * atr
        cand = cand_atr
        if mode != "atr":
            lv = LV[min(j // _M30, len(LV) - 1)]
            lev = _sr_below(lv, C[j]) if d > 0 else _sr_above(lv, C[j])
            if lev is not None:
                cand_sr = lev - d * buf * atr
                # Deckel: nie enger als 0,5×ATR (Rauschen) / weiter als 2,0×ATR (Tail)
                dist = (C[j] - cand_sr) * d
                if dist < _SL_MIN_ATR * atr:
                    cand_sr = C[j] - d * _SL_MIN_ATR * atr
                elif dist > _SL_MAX_ATR * atr:
                    cand_sr = C[j] - d * _SL_MAX_ATR * atr
                if mode == "sr":
                    cand = cand_sr
                elif mode == "hybrid":      # der engere = höher (LONG)
                    cand = max(cand_atr, cand_sr) if d > 0 else min(cand_atr, cand_sr)
                elif mode == "sr_wide":
                    cand = min(cand_atr, cand_sr) if d > 0 else max(cand_atr, cand_sr)
        if profit >= _BE * atr or ph == 2:
            cand = max(cand, entry) if d > 0 else min(cand, entry)
        sl = max(sl, cand) if d > 0 else min(sl, cand)
    return (C[end] - entry) * d / atr


def st(Rs):
    if not Rs:
        return "  -"
    n = len(Rs); w = sum(1 for r in Rs if r > 0)
    g = sum(r for r in Rs if r > 0); ls = -sum(r for r in Rs if r < 0)
    return (f"WR={100*w/n:>3.0f}% Ø-R={sum(Rs)/n:+.3f} PF={(g/ls if ls>0 else 99):>4.2f} "
            f"Worst={min(Rs):+.2f} ΣR={sum(Rs):+.0f}")


def squeeze_signals(H, L, C, AT, lo, hi):
    """Nur SQUEEZE-Ausbrüche (das einzige validierte Auto-Setup) — Nachtest
    2026-07-31: der Squeeze ist runner-abhängig (nur 43 % Treffer, der Ertrag
    kommt aus wenigen Läufern), dort könnte ein level-verankerter Stop mehr
    bewegen als über ALLE Wave-Signale gemittelt.

    Regel exakt wie live (`wave._squeeze_one`): Box = Spanne der letzten _SQ_N
    abgeschlossenen M5-Bars ≤ _SQ_MULT×ATR, Ausbruch um _SQ_K×ATR. Einstieg AM
    Ausbruchslevel (nicht am Bar-Close), Cooldown wie in den Squeeze-Backtests.
    """
    from core.wave_rec import _SQ_N, _SQ_MULT, _SQ_K
    _W = 24; _COOL = 12
    out = []
    i = max(lo, _N_BARS, _SQ_N + 15)
    while i < min(hi, len(C) - _MAXH - 1):
        atr = AT[i]
        if not atr or atr < _ATRMIN:
            i += 1; continue
        boxHi = max(H[i - _SQ_N:i]); boxLo = min(L[i - _SQ_N:i])
        if (boxHi - boxLo) > _SQ_MULT * atr:      # keine Kompression
            i += 1; continue
        hit = None
        for j in range(i, min(i + _W, len(C) - _MAXH - 1)):
            up = boxHi + _SQ_K * atr; dn = boxLo - _SQ_K * atr
            if H[j] >= up:
                hit = (j, 1, up); break
            if L[j] <= dn:
                hit = (j, -1, dn); break
        if hit is None:
            i += 1; continue
        j, d, lvl = hit
        out.append((j, d, atr, lvl))
        i = j + _COOL
    return out


def signals(H, L, C, ES, EF, AT, lo, hi, step):
    TH = _REVERSAL_STRETCH; out = []
    for i in range(max(lo, _N_BARS), min(hi, len(C) - _MAXH - 1), step):
        atr = AT[i]
        if not atr or atr <= 0:
            continue
        atr = max(atr, _ATRMIN); es = ES[i]; ef = EF[i]
        stretch = (C[i] - es) / atr
        ang = calc_trend_angle(C[i - _ANGLE_LR - 2:i], _ANGLE_LR); ad = ang - 90.0
        d = 0
        if stretch <= -TH and ad >= _ANGLE_DEAD:
            d = 1
        elif stretch >= TH and ad <= -_ANGLE_DEAD:
            d = -1
        elif abs(stretch) < _STRETCH_MAX:
            d = 1 if ef > es else -1 if ef < es else 0
        if d:
            out.append((i, d, atr))
    return out


def main():
    squeeze = "--squeeze" in sys.argv
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    n = int(args[0]) if args else 80000
    step = int(args[1]) if len(args) > 1 else 2
    mt5.initialize(); sym = None
    for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
        if mt5.symbol_info(c):
            sym = c; break
    bars = None
    for req in (n, 100000, 80000, 60000, 40000):
        bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, req)
        if bars is not None and len(bars) > 2000:
            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]
    EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C)
    LV = build_levels(H, L)
    mid = len(C) // 2
    halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))]
    print("=" * 96)
    kind = "NUR SQUEEZE-Ausbrüche (Echtkosten)" if squeeze else "Signal = Trend+Reversal"
    print(f"  S/R-VERANKERTES TRAILING — {sym} M5 ({len(C)} Bars)   {kind}")
    print(f"  Level = M30-Pivots k={_PIV_K}, Fenster {_PIV_LOOKBACK} M30-Bars (kausal), "
          f"Deckel [{_SL_MIN_ATR}..{_SL_MAX_ATR}]×ATR")
    if squeeze:
        print("  Erwartung Squeeze-Entry-Edge (validiert): ØR +0,14…+0,23 · PF>1")
    print("=" * 96)
    if squeeze:
        SIG = {lbl: squeeze_signals(H, L, C, AT, a, b) for lbl, a, b in halves}
    else:
        SIG = {lbl: [(i, d, atr, C[i]) for (i, d, atr)
                     in signals(H, L, C, ES, EF, AT, a, b, step)] for lbl, a, b in halves}
    for lbl, _, _ in halves:
        print(f"  {lbl}: {len(SIG[lbl])} Signale")

    def cost(i, atr):
        """Echte Kosten = Bar-Spread/ATR (nur im Squeeze-Modus; für den reinen
        VARIANTEN-Vergleich neutral, da alle Varianten dieselben Entries teilen —
        macht das ØR aber mit der +0,14…+0,23-Erwartung vergleichbar."""
        if not squeeze:
            return 0.0
        return (SP[i] if SP[i] > 0 else 0.0225) / atr

    variants = [
        ("LIVE  ATR-Trail 1,5 (Basis)",   dict(mode="atr",     mult=1.5)),
        ("S/R-Trail  Puffer 0,10",        dict(mode="sr",      buf=0.10)),
        ("S/R-Trail  Puffer 0,25",        dict(mode="sr",      buf=0.25)),
        ("S/R-Trail  Puffer 0,50",        dict(mode="sr",      buf=0.50)),
        ("Hybrid (engerer von beiden)",   dict(mode="hybrid",  buf=0.25)),
        ("Hybrid weit (weiterer)",        dict(mode="sr_wide", buf=0.25)),
        ("KONTROLLE: TP am Gegenlevel",   dict(mode="atr",     mult=1.5, use_sr_tp=True)),
    ]
    for name, kw in variants:
        print(f"\n{name}")
        for lbl, _, _ in halves:
            Rs = [sim(entry, d, atr, H, L, C, i + 1, LV, **kw) - cost(i, atr)
                  for (i, d, atr, entry) in SIG[lbl]]
            print(f"   {lbl}  {st(Rs)}")


if __name__ == "__main__":
    main()
