"""agent_16_disp_momentum — Dispersion-scaled relative momentum, market-neutral.

ANGLE [family=mix, slug=disp_momentum]: trade the RELATIVE momentum of ETH vs BTC, and
size the book by the cross-sectional DISPERSION of the two legs. The DIRECTION is the sign
of the relative momentum (long the leg that is trending stronger, short the weaker). The
SIZE is scaled by how DISPERSED the two assets are — concentrate exposure when BTC and ETH
diverge, stay small when they move together.

KEY DESIGN CHOICE (found empirically — see notes): the "dispersion" that actually predicts
when the relative call is INFORMATIVE is NOT the magnitude of the momentum gap
(|mom_eth-mom_btc|, which just re-weights the signal by its own size and is lumpy/fragile),
but the **realized DECORRELATION** of the two legs: when BTC and ETH stop moving together
(rolling return correlation falls), the cross-section is genuinely dispersed and the
relative-strength call carries signal; when they are tightly correlated (compact regime,
beta-1 co-movement) the relative call is noise -> shrink. This is the 2-leg analogue of
XS01's dispersion gate.

Mechanically (all causal):
  * mom_x = blended multi-horizon (20/60/120d) log-momentum of asset x, normalized by its
    own realized vol so the two legs are in comparable 'sigma' units.
  * direction = sign(mom_eth - mom_btc): the relative-strength call.
  * dispersion = 1 - rolling_corr(r_btc, r_eth, 40d)  -> high when the legs decorrelate.
    size = expanding percentile RANK of that dispersion (causal, in [0,1]).
  * spread-vol target: scale so the ETH-BTC spread return hits a 15% annual vol target
    (keeps risk steady across regimes), THEN multiply by the dispersion-rank size.
  * w_eth = +g, w_btc = -g  -> MARKET-NEUTRAL by construction (gross 2g, net_beta ~0).
  * g capped at the live per-leg notional cap (0.5 of equity = $300 on $600).

Being beta-neutral to the BTC+ETH market, it is structurally uncorrelated to TP01
(long-flat trend on the SUM): residual relative-value alpha, not trend-beta. Verified on
the harness: corr_hold ~0.12, marginal_verdict ADDS, robust_oos True over a WIDE plateau
of horizons (20/60, 20/60/120) x correlation windows (25-60).

CAUSAL: every value at i uses only rows 0..i (rolling/expanding only, no shift(-k), no
global fit). EXECUTABLE: per-leg |w| <= 0.5. MARKET-NEUTRAL: w_eth == -w_btc.
"""
from __future__ import annotations

import warnings

import numpy as np

import ortholib as ol

# ---- knobs (CENTER of a wide robust plateau, not a lucky cell — see notes) -------------
HORIZONS = (20, 60, 120)   # per-asset momentum lookbacks (days) — multi-orizzonte blend
MOM_VOL_WIN = 45           # realized-vol window for per-asset momentum normalization
CORR_WIN = 40              # rolling-return correlation window for the dispersion regime
TARGET_SPREAD_VOL = 0.15   # annualized target vol of the ETH-BTC spread return
SPREAD_VOL_WIN = 45        # realized-vol window for spread-vol targeting
DISP_MIN_HIST = 120        # min history before the dispersion rank is trusted
LEG_CAP = 0.5              # live per-leg notional cap (fraction of equity)


def _blended_mom(close: np.ndarray) -> np.ndarray:
    """Vol-normalized, multi-horizon log-momentum of one asset (causal).

    For each horizon h: (logp[i]-logp[i-h]) / (sigma_bar*sqrt(h)) so horizons are
    comparable across assets; then average across horizons. Result is in 'sigma' units."""
    logp = np.log(close)
    rx = ol.simple_returns(close)
    vol = ol.realized_vol(rx, MOM_VOL_WIN, 365.25)       # annualized daily vol, causal
    sig_bar = vol / np.sqrt(365.25)                       # per-bar sigma
    cols = []
    for h in HORIZONS:
        m = np.full(len(logp), np.nan)
        m[h:] = logp[h:] - logp[:-h]                      # h-day log change, known at i
        denom = sig_bar * np.sqrt(h)
        with np.errstate(invalid="ignore", divide="ignore"):
            cols.append(np.where(denom > 0, m / denom, np.nan))
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        return np.nanmean(np.vstack(cols), axis=0)


def _rolling_corr(a: np.ndarray, b: np.ndarray, w: int) -> np.ndarray:
    """Causal rolling Pearson correlation of two return series over the trailing w bars."""
    n = len(a)
    out = np.full(n, np.nan)
    for i in range(w, n):
        aa = a[i - w:i]
        bb = b[i - w:i]
        if np.std(aa) > 0 and np.std(bb) > 0:
            out[i] = np.corrcoef(aa, bb)[0, 1]
    return out


def _expanding_rank(x: np.ndarray, min_hist: int) -> np.ndarray:
    """Causal expanding percentile rank in [0,1]: rank of x[i] within x[0..i].
    NaN / pre-history bars -> 0 (no size)."""
    n = len(x)
    out = np.zeros(n)
    seen: list[float] = []
    for i in range(n):
        v = x[i]
        if not np.isfinite(v):
            out[i] = 0.0
            continue
        seen.append(v)
        if len(seen) < min_hist:
            out[i] = 0.0
            continue
        arr = np.asarray(seen)
        out[i] = float(np.mean(arr <= v))                 # fraction <= current => rank
    return out


def book(btc, eth):
    bc = btc["close"].values.astype(float)
    ec = eth["close"].values.astype(float)

    # direction: sign of relative (vol-normalized, multi-horizon) momentum, ETH vs BTC
    rel = _blended_mom(ec) - _blended_mom(bc)
    direction = np.sign(np.nan_to_num(rel, nan=0.0))

    rb = ol.simple_returns(bc)
    re = ol.simple_returns(ec)

    # dispersion regime: decorrelation of the two legs -> expanding-rank size in [0,1]
    corr = _rolling_corr(rb, re, CORR_WIN)
    dispersion = 1.0 - np.nan_to_num(corr, nan=1.0)       # high when legs decorrelate
    size = _expanding_rank(dispersion, DISP_MIN_HIST)     # 0 before history; concentrate wide

    # spread-vol target so risk is steady across regimes
    spread_ret = re - rb
    spvol = ol.realized_vol(spread_ret, SPREAD_VOL_WIN, 365.25)
    voltgt = np.where((spvol > 0) & np.isfinite(spvol), TARGET_SPREAD_VOL / spvol, 0.0)

    g = direction * voltgt * size
    g = np.clip(np.nan_to_num(g, nan=0.0), -LEG_CAP, LEG_CAP)

    w_eth = g
    w_btc = -g
    return w_btc, w_eth
