"""TIMING SWEEP — famiglie PAIRS & HONEST su 5/10/15/30m (vs live), goal 2026-06-14.

Domanda utente: i pairs e le honest beneficiano di timeframe piu' veloci, come hanno
fatto le fade (swap 1h->15m, v1.1.30)?

VINCOLO DATI (hard): solo BTC/ETH hanno 5m/15m/30m in locale (10m = resample da 5m, qui
in data/raw/{a}_10m.parquet temporanei). TUTTI gli alt (ADA/BNB/DOGE/LTC/SOL/XRP) sono
SOLO 1h. Conseguenze:
  - PAIRS: solo ETH/BTC e' sweepabile sub-orario. Gli altri 4 pair (gambe alt:
    LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL) restano 1h per mancanza di dati alt sub-orari.
  - HONEST: solo DIP01 (BTC, mean-reversion) ha senso + dati. TR01 (trend EMA 4h su
    basket alt) e ROT02 (rotazione dual-momentum 1d su universo alt) sono lente
    (orizzonte multi-giorno/mese) E multi-asset-su-alt -> sub-orario infattibile (dati)
    e insensato (momentum a 60g su barre 5min).

Tutto NETTO (fee 0.10% RT single / 0.20% RT per coppia a 2 gambe), leva 3x, OOS = held-out.
Engine CANONICI riusati (pairs_sim_flat, replica dip intrabar == dip_market_gated, gate
PORT06 == pairs30m_gate/dip01). Niente re-tuning dei parametri al cambio TF (anti-overfit,
come lo swap fade).

    uv run python scripts/analysis/timing_sweep_pairs_honest.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 src.data.downloader import load_data
from scripts.analysis.pairs_research import pairs_sim_flat
from scripts.analysis.report_families import daily_from
from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE, IDX, _norm
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio.sleeves import all_sleeve_equities
from src.portfolio import weighting as W

TFS = ["5m", "10m", "15m", "30m", "1h"]
BARS_PER_DAY = {"5m": 288, "10m": 144, "15m": 96, "30m": 48, "1h": 24}
LEV, POS = 3.0, 0.15
EPOCH = pd.Timestamp(0, tz="UTC")


def _ensure_10m():
    """10m non e' nativo (download_all fa 5m/15m/30m/1h): lo resampla da 5m se manca.
    Aggregazione OHLCV causale (first/max/min/last/sum). File gitignored, rigenerabile."""
    for a in ("btc", "eth"):
        dst = PROJECT_ROOT / f"data/raw/{a}_10m.parquet"
        if dst.exists():
            continue
        df = load_data(a.upper(), "5m").sort_values("timestamp").reset_index(drop=True)
        ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
        g = df.set_index(ts).resample("10min")
        out = pd.DataFrame({"open": g["open"].first(), "high": g["high"].max(),
                            "low": g["low"].min(), "close": g["close"].last(),
                            "volume": g["volume"].sum()}).dropna()
        out["timestamp"] = (out.index - EPOCH) // pd.Timedelta(milliseconds=1)
        out.reset_index(drop=True).to_parquet(dst, index=False)
        print(f"  [bootstrap] generato {dst.name} ({len(out)} righe da 5m)")

# config UNIVERSALE pairs (1h, CLAUDE.md) — NON ri-tunata al cambio TF (anti-overfit)
PAIR_CFG = dict(n=50, z_in=2.0, z_exit=0.75, max_bars=72)
# config DIP01 canonica (dip_market_gated default, market_n=0 = live)
DIP_CFG = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)


# ============================ helper dati ============================
def flat_share(asset, tf):
    df = load_data(asset, tf)
    o, h, l, c = df["open"], df["high"], df["low"], df["close"]
    return ((o == h) & (h == l) & (l == c)).mean() * 100


# ============================ DIP01 engine TF-aware ============================
def _atr(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)))
    return pd.Series(tr).rolling(n).mean().values


def dip_sim(asset, tf, n=50, z_in=2.5, sl_atr=2.5, max_bars=24,
            fee_rt=0.001, oos_frac=0.0):
    """Replica TF-aware di honest_improve2.dip_market_gated(market_n=0): dip-buy z-score,
    TP=SMA intrabar, SL=close-sl_atr*ATR intrabar (SL prioritario), max_bars. Engine canonico."""
    df = load_data(asset, tf)
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    N = len(c); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    ma = pd.Series(c).rolling(n).mean().values
    sd = pd.Series(c).rolling(n).std().values
    a = _atr(df, 14)
    z = (c - ma) / np.where(sd == 0, np.nan, sd)
    fee = fee_rt * LEV
    cap = peak = 1000.0; dd = 0.0; last_exit = -1
    eq_ts, eq_v = [], []; rets = []; trades = wins = 0; yearly = {}
    split = int(N * (1 - oos_frac)) if oos_frac else 0
    for i in range(n + 14, N):
        if i < split or np.isnan(z[i]) or np.isnan(a[i]):
            continue
        if not (z[i] <= -z_in and z[i - 1] > -z_in):
            continue
        if i <= last_exit or i + 1 >= N:
            continue
        entry = c[i]; tp, sl, mb = ma[i], c[i] - sl_atr * a[i], 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:
                j = N - 1; exit_p = c[j]; break
            if l[j] <= sl:
                exit_p = sl; break
            if h[j] >= tp:
                exit_p = tp; break
            if k == mb:
                exit_p = c[j]
        ret = (exit_p - entry) / entry * LEV - fee
        cap = max(cap + cap * POS * ret, 10.0)
        peak = max(peak, cap); dd = max(dd, (peak - cap) / peak)
        last_exit = j; trades += 1; wins += ret > 0; rets.append(ret * POS)
        yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
        eq_ts.append(ts.iloc[j]); eq_v.append(cap)
    yrs = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1
    sh = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs)) if len(rets) > 1 and np.std(rets) > 0 else 0.0
    return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=(cap / 1000 - 1) * 100,
                dd=dd * 100, sharpe=sh, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v)


# ============================ gate PORT06 ============================
def port_metrics(members, ids, clusters, caps):
    dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
    w = W.weight_vector("cap", ids, dr, caps=caps, clusters=clusters)
    drp = port_returns({i: members[i] for i in ids}, w)
    return metrics(drp), metrics(drp, lo=SPLIT), w


def daily_norm(eq_ts, eq_v):
    return _norm(daily_from(eq_ts, eq_v))


# ============================ PART 1: dati ============================
def part1_data():
    print("=" * 96)
    print("  PART 1 — REALTA' DATI: flat-share (O=H=L=C, = print stale, rischio fill) per TF")
    print("=" * 96)
    print(f"  {'asset':<6}" + "".join(f"{tf:>8}" for tf in TFS))
    for a in ("BTC", "ETH"):
        print(f"  {a:<6}" + "".join(f"{flat_share(a, tf):>7.1f}%" for tf in TFS))
    print("\n  ALT (ADA/BNB/DOGE/LTC/SOL/XRP): SOLO 1h disponibile -> NON sweepabili sub-orario.")
    print("  => pairs con gamba alt (4/5) e honest multi-asset (TR01/ROT02) bloccati a 1h dai dati.")


# ============================ PART 2: pairs ETH/BTC standalone ============================
def part2_pairs_standalone():
    print("\n" + "=" * 96)
    print("  PART 2 — PAIRS ETH/BTC standalone, config UNIVERSALE n=50 z_in=2.0 z_exit=0.75 mb=72")
    print(f"  (flat_skip=True, live-realizable; OOS held-out; fee 0.20% RT/coppia; f2x = OOS Sh a fee 2x)")
    print("=" * 96)
    print(f"  {'tf':<5}{'trd':>7}{'FULL%':>9}{'DD%':>7}{'Sh':>7} | {'OOS%':>9}{'oDD%':>7}{'oSh':>7} | {'f2x_oSh':>8}{'mb_h':>6}")
    eqs = {}
    for tf in TFS:
        f = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True)
        o = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, split_frac=1 - 0.30)
        o2 = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, split_frac=1 - 0.30, fee_rt=0.002)
        eqs[tf] = daily_norm(f["eq_ts"], f["eq_v"])
        mb_h = PAIR_CFG["max_bars"] / BARS_PER_DAY[tf] * 24
        print(f"  {tf:<5}{f['trades']:>7}{f['ret']:>9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}"
              f" | {o['ret']:>9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f} | {o2['sharpe']:>8.2f}{mb_h:>6.1f}")
    # correlazioni daily fra TF
    print("\n  CORR rendimenti daily fra TF (alta = ridondante):")
    print(f"  {'':<6}" + "".join(f"{tf:>7}" for tf in TFS))
    for t1 in TFS:
        row = []
        for t2 in TFS:
            c = eqs[t1].pct_change().fillna(0).corr(eqs[t2].pct_change().fillna(0))
            row.append(f"{c:>7.2f}")
        print(f"  {t1:<6}" + "".join(row))
    return eqs


# ============================ PART 3: pairs gate PORT06 ============================
def part3_pairs_gate(eqs):
    print("\n" + "=" * 96)
    print("  PART 3 — GATE PORT06: aggiungere ETH/BTC 5m e/o 10m al BLEND live (1h+15m), mezza size")
    print(f"  (baseline = sleeve canonici live; OOS da {OOS_DATE})")
    print("=" * 96)
    p = PORTFOLIOS["PORT06"]
    base = dict(all_sleeve_equities())   # include PR_ETHBTC (1h) + PR_ETHBTC_15M
    ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps
    f0, o0, _ = port_metrics(base, ids0, cl0, caps)
    print(f"  {'config':<26}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}")
    print(f"  {'ATTUALE (1h+15m)':<26}{f0['sharpe']:>9.2f}{f0['dd']:>10.2f}{o0['sharpe']:>9.2f}{o0['dd']:>9.2f}")
    # half-size pairs equity (pos 0.075 come 15m live): ricalcolo eq a pos dimezzato
    for tf in ("10m", "5m"):
        fr = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, pos=0.075)
        cand = daily_norm(fr["eq_ts"], fr["eq_v"])
        mem = dict(base); sid = f"PR_ETHBTC_{tf.upper()}"
        mem[sid] = cand
        ids = ids0 + [sid]; cl = dict(cl0); cl[sid] = "ETH-rev"
        f1, o1, w1 = port_metrics(mem, ids, cl, caps)
        ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9
              and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9)
        verdict = "MIGLIORA (promosso)" if ok else "non domina (vedi numeri)"
        print(f"  {'+' + tf + ' (half size)':<26}{f1['sharpe']:>9.2f}{f1['dd']:>10.2f}{o1['sharpe']:>9.2f}{o1['dd']:>9.2f}   {verdict}")


# ============================ PART 4: DIP01 ============================
def part4_dip():
    print("\n" + "=" * 96)
    print("  PART 4 — HONEST/DIP01 (BTC) standalone, config canonica n=50 z_in=2.5 sl_atr=2.5 mb=24")
    print("  (engine == dip_market_gated market_n=0; OOS held-out; fee 0.10% RT; f2x = OOS Sh a fee 2x)")
    print("=" * 96)
    print(f"  {'tf':<5}{'asset':<5}{'trd':>7}{'WR%':>6}{'FULL%':>9}{'DD%':>7}{'Sh':>7} | {'OOS%':>9}{'oDD%':>7}{'oSh':>7} | {'f2x_oSh':>8}{'mb_h':>6}")
    eqs = {}
    for asset in ("BTC", "ETH"):
        for tf in TFS:
            f = dip_sim(asset, tf, **DIP_CFG)
            o = dip_sim(asset, tf, **DIP_CFG, oos_frac=0.30)
            o2 = dip_sim(asset, tf, **DIP_CFG, oos_frac=0.30, fee_rt=0.002)
            if asset == "BTC":
                eqs[tf] = daily_norm(f["eq_ts"], f["eq_v"]) if f["eq_v"] else None
            mb_h = DIP_CFG["max_bars"] / BARS_PER_DAY[tf] * 24
            print(f"  {tf:<5}{asset:<5}{f['trades']:>7}{f['win']:>6.1f}{f['ret']:>9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}"
                  f" | {o['ret']:>9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f} | {o2['sharpe']:>8.2f}{mb_h:>6.1f}")
        print()
    # corr daily BTC fra TF
    print("  CORR rendimenti daily DIP01 BTC fra TF:")
    print(f"  {'':<6}" + "".join(f"{tf:>7}" for tf in TFS))
    for t1 in TFS:
        row = []
        for t2 in TFS:
            if eqs.get(t1) is None or eqs.get(t2) is None:
                row.append(f"{'-':>7}"); continue
            c = eqs[t1].pct_change().fillna(0).corr(eqs[t2].pct_change().fillna(0))
            row.append(f"{c:>7.2f}")
        print(f"  {t1:<6}" + "".join(row))
    return eqs


# ============================ PART 5: DIP01 gate PORT06 ============================
def part5_dip_gate(eqs):
    print("\n" + "=" * 96)
    print("  PART 5 — GATE PORT06: SWAP DIP01_BTC 1h -> TF piu' veloce (sostituisce lo sleeve)")
    print("=" * 96)
    p = PORTFOLIOS["PORT06"]
    base = dict(all_sleeve_equities())
    ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps
    f0, o0, _ = port_metrics(base, ids0, cl0, caps)
    print(f"  {'config':<26}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}")
    print(f"  {'ATTUALE (DIP01 1h)':<26}{f0['sharpe']:>9.2f}{f0['dd']:>10.2f}{o0['sharpe']:>9.2f}{o0['dd']:>9.2f}")
    for tf in ("30m", "15m", "10m", "5m"):
        if eqs.get(tf) is None:
            continue
        mem = dict(base); mem["DIP01_BTC"] = eqs[tf]
        f1, o1, _ = port_metrics(mem, ids0, cl0, caps)
        ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9
              and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9)
        verdict = "MIGLIORA" if ok else "non domina"
        print(f"  {'DIP01 ' + tf:<26}{f1['sharpe']:>9.2f}{f1['dd']:>10.2f}{o1['sharpe']:>9.2f}{o1['dd']:>9.2f}   {verdict}")


if __name__ == "__main__":
    _ensure_10m()
    part1_data()
    pe = part2_pairs_standalone()
    part3_pairs_gate(pe)
    de = part4_dip()
    part5_dip_gate(de)
    print("\n  NB TR01/ROT02: nessuno sweep — dati alt solo 1h + orizzonte multi-giorno/mese")
    print("     (trend EMA20/100 4h, rotazione momentum 60g 1d) rendono il sub-orario infattibile e insensato.")
