"""GTAA — sleeve EQUITY/ETF: trend difensivo multi-asset (analogo di TP01, su IB).

Validato sul branch research/equities-ib (diari 2026-06-22-equities-*). Trend long-flat TSMOM
multi-orizzonte su un paniere di classi (azioni US/tech/small + bond + oro + credito), equal-weight
sugli asset in trend-up, vol-target 12%. Difensivo: Sharpe ~0.64 / maxDD ~15% standalone, e — il punto
— corr ~0.21 col crypto (TP01) -> diversificatore reale (blend Sharpe ~1.5, DD dimezzato).

Eseguibile su IB a basso capitale (ETF frazionabili, switch mensile/basso turnover). Legge la CACHE
su disco data/raw/eq_*.parquet (ADJUSTED_LAST, scritta da scripts/research/fetch_ib_equities.py);
in produzione va rinfrescata giornalmente (gateway IB). Espone rendimenti + PESI CORRENTI (posizioni).
"""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
import numpy as np, pandas as pd

RAW = Path(__file__).resolve().parents[2] / "data" / "raw"
EQ_UNIVERSE = ("SPY", "QQQ", "IWM", "TLT", "GLD", "HYG")
HORIZONS = (21, 63, 126, 252)
TARGET_VOL = 0.12
FEE_SIDE = 0.0002
ANN = np.sqrt(252.0)


@lru_cache(maxsize=16)
def _close(sym: str) -> pd.Series:
    p = RAW / f"eq_{sym.lower()}_1d.parquet"
    if not p.exists():
        raise FileNotFoundError(f"{p} assente — gira scripts/research/fetch_ib_equities.py")
    d = pd.read_parquet(p)
    return pd.Series(d["close"].astype(float).values,
                     index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()


def _exposure(close: pd.Series) -> pd.Series:
    """Esposizione long-flat [0,1] su un asset: frazione di orizzonti in trend-up, vol-targeted, cap 1.
    Causale (solo dati <= i)."""
    px = close.values; n = len(px); tgt = np.zeros(n); mh = max(HORIZONS)
    for i in range(mh, n):
        tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS])
    s = pd.Series(tgt, index=close.index)
    rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN
    scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0)
    return (s * scale).clip(0, 1.0)


def _gated_returns(sym: str) -> pd.Series:
    close = _close(sym); ex = _exposure(close)
    ret = close.pct_change().fillna(0.0).values
    held = np.zeros(len(ex)); held[1:] = ex.values[:-1]      # causale: esposizione decisa a i-1, tenuta in i
    net = held * ret - FEE_SIDE * np.abs(np.diff(held, prepend=0.0))
    return pd.Series(net, index=close.index)


def gtaa_returns(universe=EQ_UNIVERSE) -> pd.Series:
    """Rendimenti netti daily del GTAA: EW dei rendimenti trend-gated sugli asset disponibili."""
    cols = {a: _gated_returns(a) for a in universe}
    return pd.concat(cols, axis=1).sort_index().mean(axis=1, skipna=True).dropna()


def gtaa_weights(universe=EQ_UNIVERSE) -> dict:
    """Pesi target CORRENTI (ultima barra): quanto allocare a ciascun ETF (e quanto in cash).
    weight_i = esposizione_i / N_disponibili. Azionabile su IB."""
    out = {}; n = len(universe)
    for a in universe:
        try:
            ex = _exposure(_close(a))
            out[a] = round(float(ex.iloc[-1]) / n, 4)
        except FileNotFoundError:
            continue
    out["_cash"] = round(1.0 - sum(out.values()), 4)
    out["_asof"] = str(_close("SPY").index[-1].date())
    return out
