"""GATE PORT06: i top candidati sostituiscono MR02/ETH. Misura FULL+OOS Sharpe/DD.

Ogni candidato genera i trade ETH 1h con l'ENGINE INTRABAR identico al sleeve canonico
(explore_lab.simulate: SL/TP intrabar al livello, fee 0.10% RT, lev 3x), equity giornaliera
normalizzata su IDX (2021-01-01 -> 2026-05-26), swap su all_sleeve_equities()['MR02_ETH'],
e ri-misura PORT06 (cap weighting PAIRS 0.33 / SHAPE 0.0588).

uv run python scripts/analysis/mr02eth_port06_gate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

from scripts.analysis.explore_lab import get_df, atr, ema
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns, _norm
from src.portfolio.sleeves import all_sleeve_equities
from src.portfolio import weighting as W

FEE_RT, LEV, POS = 0.001, 3.0, 0.15
CAPS = {"PAIRS": 0.33, "SHAPE": 0.0588}


# ----------------------- engine intrabar (== explore_lab.simulate) -----------------------
def build_trades(entries, df, fee_rt=FEE_RT, lev=LEV):
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    n = len(c); out = []; last = -1
    for e in entries:
        i, d = e["i"], e["d"]
        if i <= last or i + 1 >= n:
            continue
        entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
        exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
        for k in range(1, mb + 1):
            j = i + k
            if j >= n:
                exit_p = c[n - 1]; break
            hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
            hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
            if hit_sl:
                exit_p = sl; break
            if hit_tp:
                exit_p = tp; break
            if k == mb:
                exit_p = c[j]
        out.append((i, j, (exit_p - entry) / entry * d * lev - fee_rt * lev)); last = j
    return out


def build_trades_exit16(entries, df, sl_confirm=0.5, fee_rt=FEE_RT, lev=LEV,
                        dvol=None, otm=None, skew=1.10, tenor_mult=1.0):
    """Engine EXIT-16 close-confirm (== config LIVE): SL intrabar OFF, lo stop scatta solo se il
    CLOSE sfonda sl ∓ sl_confirm*ATR14; TP intrabar ha priorita'.
    Se dvol+otm sono dati, AGGIUNGE un overlay opzione (put se long / call se short) a otm OTM."""
    from scripts.analysis.option_overlay_lab import bs_put, bs_call
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    a = atr(df, 14); n = len(c); out = []; last = -1
    for e in entries:
        i, d = e["i"], e["d"]
        if i <= last or i + 1 >= n:
            continue
        entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
        exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
        for k in range(1, mb + 1):
            j = i + k
            if j >= n:
                exit_p = c[n - 1]; break
            hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
            if hit_tp:
                exit_p = tp; break
            if sl is not None and not np.isnan(a[j]):
                buf = sl_confirm * a[j]
                if (d == 1 and c[j] < sl - buf) or (d == -1 and c[j] > sl + buf):
                    exit_p = c[j]; break
            if k == mb:
                exit_p = c[j]
        ret = (exit_p - entry) / entry * d * lev - fee_rt * lev
        if dvol is not None and otm is not None:
            T = max(mb * tenor_mult, 1.0) / _HOURS_YEAR; sig = dvol[i] * skew
            if d == 1:
                K = entry * (1 - otm); prem = bs_put(entry, K, T, sig) / entry; pay = max(K - exit_p, 0.0) / entry
            else:
                K = entry * (1 + otm); prem = bs_call(entry, K, T, sig) / entry; pay = max(exit_p - K, 0.0) / entry
            ret += lev * (pay - prem)
        out.append((i, j, ret)); last = j
    return out


def blend_equity(eqs, weights=None) -> pd.Series:
    """Combina N equity giornaliere mediando i rendimenti giornalieri (ribilancio daily)."""
    drs = [e.pct_change().fillna(0.0) for e in eqs]
    w = weights or [1.0 / len(drs)] * len(drs)
    dr = sum(wi * di for wi, di in zip(w, drs))
    return _norm((1 + dr).cumprod())


def daily_equity(trades, df) -> pd.Series:
    ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    n = len(df); eq = np.full(n, 1000.0); cap = 1000.0
    for i, j, ret in sorted(trades, key=lambda t: t[1]):
        cap = max(cap + cap * POS * ret, 10.0); eq[j:] = cap
    s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
    return _norm(s)


# ----------------------- indicatori -----------------------
def choppiness(df, n=14):
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    pc = np.roll(c, 1); pc[0] = c[0]
    tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
    str_ = pd.Series(tr).rolling(n).sum().values
    hh = pd.Series(h).rolling(n).max().values
    ll = pd.Series(l).rolling(n).min().values
    rng = hh - ll
    with np.errstate(divide="ignore", invalid="ignore"):
        ci = 100.0 * np.log10(str_ / rng) / np.log10(n)
    return ci


def var_ratio(close, k=30, win=100):
    r1 = pd.Series(close).pct_change()
    rk = pd.Series(close).pct_change(k)
    v1 = r1.rolling(win).var().values
    vk = rk.rolling(win).var().values
    with np.errstate(divide="ignore", invalid="ignore"):
        vr = vk / (k * v1)
    return vr


def donchian_levels(df, n):
    hh = pd.Series(df["high"].values).rolling(n).max().shift(1).values
    ll = pd.Series(df["low"].values).rolling(n).min().shift(1).values
    return hh, ll


# ----------------------- generatori di segnale (candidati) -----------------------
def gen_donchian_base(df, n=20, sl_atr=2.0, max_bars=24, trend_max=None, ema_long=200, gate=None, use_sl=True):
    """gate(i)->bool: True = consenti il segnale alla barra i. None = sempre. use_sl=False -> sl=None (no-SL)."""
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    a = atr(df, 14); hh, ll = donchian_levels(df, n); em = ema(c, ema_long)
    ents = []
    for i in range(max(n, ema_long, 14) + 1, len(c)):
        if np.isnan(hh[i]) or np.isnan(ll[i]) or np.isnan(a[i]) or a[i] <= 0:
            continue
        if trend_max is not None and not np.isnan(em[i]) and abs(c[i] - em[i]) / a[i] > trend_max:
            continue
        if gate is not None and not gate(i):
            continue
        tp = (hh[i] + ll[i]) / 2.0
        if c[i] < ll[i] and c[i - 1] >= ll[i - 1]:
            ents.append({"i": i, "d": 1, "tp": tp, "sl": (c[i] - sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
        elif c[i] > hh[i] and c[i - 1] <= hh[i - 1]:
            ents.append({"i": i, "d": -1, "tp": tp, "sl": (c[i] + sl_atr * a[i]) if use_sl else None, "max_bars": max_bars})
    return ents


# ----------------------- engine intrabar + overlay opzione (per i candidati no-SL) -----------------------
_HOURS_YEAR = 24 * 365.0


def build_trades_hedged(entries, df, dvol, otm=0.10, skew=1.10, tenor_mult=1.0, fee_rt=FEE_RT, lev=LEV):
    from scripts.analysis.option_overlay_lab import bs_put, bs_call
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    n = len(c); out = []; last = -1
    for e in entries:
        i, d = e["i"], e["d"]
        if i <= last or i + 1 >= n:
            continue
        entry = c[i]; tp, sl, mb = e.get("tp"), e.get("sl"), e["max_bars"]
        exit_p = c[min(i + mb, n - 1)]; j = min(i + mb, n - 1)
        for k in range(1, mb + 1):
            j = i + k
            if j >= n:
                exit_p = c[n - 1]; break
            hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
            hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
            if hit_sl:
                exit_p = sl; break
            if hit_tp:
                exit_p = tp; break
            if k == mb:
                exit_p = c[j]
        base = (exit_p - entry) / entry * d * lev - fee_rt * lev
        T = max(mb * tenor_mult, 1.0) / _HOURS_YEAR; sig = dvol[i]; sigb = sig * skew
        if d == 1:
            K = entry * (1.0 - otm); prem = bs_put(entry, K, T, sigb) / entry; pay = max(K - exit_p, 0.0) / entry
        else:
            K = entry * (1.0 + otm); prem = bs_call(entry, K, T, sigb) / entry; pay = max(exit_p - K, 0.0) / entry
        out.append((i, j, base + lev * (pay - prem))); last = j
    return out


def cand_choppiness_gate_fade(df):
    ci = choppiness(df, 14)
    return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=None,
                             gate=lambda i: not np.isnan(ci[i]) and ci[i] >= 50.0)


def cand_choppiness_donchian(df):
    ci = choppiness(df, 14)
    return gen_donchian_base(df, n=14, sl_atr=2.0, trend_max=3.0,
                             gate=lambda i: not np.isnan(ci[i]) and ci[i] > 61.8)


def cand_varratio_gate_fade(df):
    vr = var_ratio(df["close"].values, k=30, win=100)
    return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0,
                             gate=lambda i: not np.isnan(vr[i]) and vr[i] < 0.95)


def cand_baseline_recon(df):
    """MR02/ETH canonico ricostruito col MIO engine (sanity check vs all_sleeve_equities)."""
    return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)


def cand_vrp_neg_dvol_low(df):
    from scripts.analysis.regime_lab import load, regime_features
    rdf = load("ETH", "1h")
    R = regime_features(rdf)
    # allinea per indice posizionale (regime_lab.load parte da get_df, stesso ordinamento)
    vrp = R["vrp"]; dvp = R["dvol_pct"]
    m = min(len(df), len(vrp))
    def gate(i):
        if i >= m:
            return False
        return (not np.isnan(vrp[i]) and vrp[i] < 0) and (not np.isnan(dvp[i]) and dvp[i] < 0.60)
    return gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=None, gate=gate)


# ----------------------- gate PORT06 -----------------------
def port_metrics(members, ids):
    w = W.weight_vector("cap", ids, None, caps=CAPS)
    drp = port_returns({i: members[i] for i in ids}, w)
    return metrics(drp), metrics(drp, lo=SPLIT)


def main():
    print("=" * 104)
    print(f"  GATE PORT06 — sostituire MR02/ETH | finestra {IDX[0].date()}..{IDX[-1].date()} | OOS da {OOS_DATE}")
    print("=" * 104)
    eq = dict(all_sleeve_equities())
    ids = [k for k in eq if k in {
        "MR01_BTC","MR02_BTC","MR07_BTC","MR01_ETH","MR02_ETH","MR07_ETH",
        "DIP01_BTC","TR01_basket","ROT02_rot",
        "PR_ETHBTC","PR_LTCETH","PR_ADAETH","PR_BTCLTC","PR_ETHSOL","TSM01","SH_BTC","SH_ETH"}]
    print(f"  sleeve PORT06: {len(ids)}  | MR02_ETH presente: {'MR02_ETH' in ids}")

    f_b, o_b = port_metrics(eq, ids)
    print(f"\n  {'variante':<22s}{'FULL Sh':>8s}{'FULL DD':>8s}{'FULL CAGR':>10s}   |{'OOS Sh':>8s}{'OOS DD':>8s}{'OOS CAGR':>9s}")
    print("  " + "-" * 100)
    print(f"  {'BASELINE (canonico)':<22s}{f_b['sharpe']:>8.2f}{f_b['dd']:>8.2f}{f_b['cagr']:>9.0f}%   |{o_b['sharpe']:>8.2f}{o_b['dd']:>8.2f}{o_b['cagr']:>8.0f}%")

    df = get_df("ETH", "1h")
    from scripts.analysis.option_overlay_lab import dvol_for
    dvol = dvol_for(df, "ETH")
    # candidati: (nome, builder) dove builder(df)->trades
    def b_signal(fn):
        return lambda df: build_trades(fn(df), df)
    base_ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0)
    def b_exit16(df):
        return build_trades_exit16(base_ents, df, sl_confirm=0.5)
    def b_exit16_put10(df):
        return build_trades_exit16(base_ents, df, sl_confirm=0.5, dvol=dvol, otm=0.10)
    def b_noSL(df):
        return build_trades(gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False), df)
    def b_noSL_put10(df):
        ents = gen_donchian_base(df, n=20, sl_atr=2.0, trend_max=3.0, use_sl=False)
        return build_trades_hedged(ents, df, dvol, otm=0.10)
    cands = {
        "MR02recon(sanity)": b_signal(cand_baseline_recon),
        "varratio_gate": b_signal(cand_varratio_gate_fade),
        "choppiness_donch": b_signal(cand_choppiness_donchian),
        "vrp_neg_dvol_low": b_signal(cand_vrp_neg_dvol_low),
        "EXIT16(live)": b_exit16,
        "EXIT16+put10%OTM": b_exit16_put10,
        "noSL_raw": b_noSL,
        "noSL_put10%OTM": b_noSL_put10,
    }
    rows = []
    equ = {}     # salva le equity per i blend
    for name, fn in cands.items():
        try:
            tr = fn(df)
            ce = daily_equity(tr, df)
            equ[name] = ce
            m2 = dict(eq); m2["MR02_ETH"] = ce
            f_c, o_c = port_metrics(m2, ids)
            rows.append((name, len(tr), f_c, o_c))
            print(f"  {name:<22s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f}{f_c['cagr']:>9.0f}%   |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}{o_c['cagr']:>8.0f}%"
                  f"   ({len(tr)} trade)")
        except Exception as ex:
            print(f"  {name:<22s} ERRORE: {ex}")

    # ---- BLEND within-sleeve: riempi lo sleeve ETH con EXIT-16 + un candidato decorrelato ----
    print("  " + "-" * 100 + "\n  BLEND within-sleeve (lo sleeve ETH = mix di 2 strategie, peso PORT06 invariato):")
    blends = {
        "50/50 EXIT16+varratio": (["EXIT16(live)", "varratio_gate"], [0.5, 0.5]),
        "50/50 EXIT16+chopDonch": (["EXIT16(live)", "choppiness_donch"], [0.5, 0.5]),
        "50/50 EXIT16+vrp": (["EXIT16(live)", "vrp_neg_dvol_low"], [0.5, 0.5]),
        "70/30 EXIT16+vrp": (["EXIT16(live)", "vrp_neg_dvol_low"], [0.7, 0.3]),
        "50/50 EXIT16put+vrp": (["EXIT16+put10%OTM", "vrp_neg_dvol_low"], [0.5, 0.5]),
        "tri EXIT16put+vrp+chop": (["EXIT16+put10%OTM", "vrp_neg_dvol_low", "choppiness_donch"], [0.5, 0.25, 0.25]),
    }
    for name, (keys, wts) in blends.items():
        try:
            be = blend_equity([equ[k] for k in keys], wts)
            m2 = dict(eq); m2["MR02_ETH"] = be
            f_c, o_c = port_metrics(m2, ids)
            rows.append((name, -1, f_c, o_c))
            print(f"  {name:<22s}{f_c['sharpe']:>8.2f}{f_c['dd']:>8.2f}{f_c['cagr']:>9.0f}%   |{o_c['sharpe']:>8.2f}{o_c['dd']:>8.2f}{o_c['cagr']:>8.0f}%")
        except Exception as ex:
            print(f"  {name:<22s} ERRORE: {ex}")

    # riferimento ONESTO = EXIT-16 (config LIVE), non il canonico intrabar-SL
    ex = next((r for r in rows if r[0] == "EXIT16(live)"), None)
    f_l, o_l = (ex[2], ex[3]) if ex else (f_b, o_b)
    print("\n  " + "=" * 100)
    print(f"  GATE vs LIVE EXIT-16 (FULL {f_l['sharpe']:.2f}/{f_l['dd']:.2f}  OOS {o_l['sharpe']:.2f}/{o_l['dd']:.2f}):")
    print("  MIGLIORIA = nessuna metrica peggiora oltre il rumore E almeno una migliora (Sharpe +>=0.03 o DD -)")
    print("  " + "-" * 100)
    for name, ntr, f_c, o_c in rows:
        if name.startswith("MR02recon") or name == "EXIT16(live)":
            continue
        dfs, dfd = f_c['sharpe'] - f_l['sharpe'], f_c['dd'] - f_l['dd']
        dos, dod = o_c['sharpe'] - o_l['sharpe'], o_c['dd'] - o_l['dd']
        no_worse = dfs >= -0.03 and dos >= -0.03 and dfd <= 0.05 and dod <= 0.03
        better = dfs >= 0.03 or dos >= 0.03 or dfd <= -0.03 or dod <= -0.03
        ok = no_worse and better
        print(f"  {name:<22s} ΔFULL Sh {dfs:+.2f} DD {dfd:+.2f} | ΔOOS Sh {dos:+.2f} DD {dod:+.2f}  -> "
              f"{'>>> MIGLIORIA' if ok else ('= pari' if no_worse else 'peggiora')}")


if __name__ == "__main__":
    main()
