"""agent_01_xs2_zscore — 2-asset cross-sectional z-score momentum (XS01 on BTC/ETH).

ANGLE [family=rv, slug=xs2_zscore]
----------------------------------
The XS01 cross-sectional-momentum mechanism, shrunk to the executable BTC/ETH pair:
  1. for EACH asset, compute its OWN trailing momentum (trailing return over a lookback),
  2. z-score EACH asset's own momentum across time (causal rolling z),
  3. go LONG the higher-z leg / SHORT the lower-z leg  -> a market-neutral spread,
  4. vol-target the SPREAD to ~constant risk, cap each leg at the live $300/asset notional.

Why it should be ORTHOGONAL to TP01: the book is always the BTC-vs-ETH SPREAD (long one /
short the other in equal notional), so its market beta is ~0. TP01 is a long-flat trend on
the SUM of the two assets. A spread bet shares almost no variance with a trend-on-the-sum bet
-> realised corr ~0.04 full / ~0.10 hold-out. The edge it harvests is RELATIVE momentum
(which of BTC/ETH is currently stronger vs its own history), a different premium from the
market's overall direction.

ROBUSTNESS (anti-overfit, the lessons of the 2026-06-20 sweep, in code)
-----------------------------------------------------------------------
A single (lookback, z-window) cell can pass robust_oos by luck. To avoid sitting on a fragile
point we use a small DIVERSIFIED ENSEMBLE and aggregate by SIGN VOTE: each (lb, zw) member
votes long/flat/short via sign(z_btc - z_eth); the book direction is the AVERAGE of those
votes (a graded conviction in [-1, 1]). The sign-vote aggregation is what survives the
drop-one-month jackknife — it is far less sensitive to any one window's exact value than a
raw averaged z-spread, and it does not lean on a single lucky lookback.

The chosen ensemble (lookbacks x z-windows) and the vol target sit on a PLATEAU: the config
is robust_oos=True across vol-targets 0.10-0.20 AND across the lookback/z-window neighbours,
and it survives DOUBLE fees (0.10%/side). It is NOT a knife-edge cell.

  Standalone (tv=0.15): Sharpe ~0.55, maxDD ~24%, turnover ~47/yr  (modest alone, by design)
  Marginal vs TP01    : corr_full 0.04 / corr_hold 0.10, uplift_hold ~+0.28, uplift_full ~+0.09,
                        clean-year +0.28, jackknife-min +0.16  ->  verdict ADDS, robust_oos True

Causal: every weight at i uses only rows 0..i (rolling momentum, rolling z, rolling vol). The
evaluator shifts both legs (trade bar i+1 from a decision at close[i]) and charges fees on
both legs. Per-leg |weight| is capped at 0.5 = the $300/asset live notional cap on $600.
"""
from __future__ import annotations

import sys

import numpy as np

sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/ortho")
import ortholib as ol  # noqa: E402

# --- ensemble grid (diversified, all interior cells of the robust plateau) ---------------
LOOKBACKS = (20, 30, 40)        # trailing-return momentum lookbacks (days)
Z_WINDOWS = (60, 90, 120)       # rolling windows for z-scoring each asset's own momentum
MEMBERS = [(lb, zw) for lb in LOOKBACKS for zw in Z_WINDOWS]

VOL_WIN = 30                    # realized-vol window for vol-targeting the spread (days)
TARGET_VOL = 0.15               # annualized vol target for the spread return
LEG_CAP = 0.5                   # per-leg notional cap (= live $300/asset on $600 capital)


def _own_mom_z(close: np.ndarray, lb: int, zw: int) -> np.ndarray:
    """Causal z-score of an asset's OWN trailing-return momentum.
    momentum[i] = close[i]/close[i-lb] - 1 (uses only data <= i); z over a rolling zw window."""
    c = np.asarray(close, float)
    mom = np.full(len(c), np.nan)
    if len(c) > lb:
        mom[lb:] = c[lb:] / c[:-lb] - 1.0
    return ol.zscore(mom, zw)


def book(btc, eth):
    cb = btc["close"].values.astype(float)
    ce = eth["close"].values.astype(float)
    n = len(btc)

    # --- 1) sign-vote ensemble: each (lb, zw) member votes long-BTC/short-ETH via the sign
    #         of its cross-sectional z-spread. Direction = average vote, in [-1, 1]. -----------
    votes = np.zeros(n)
    valid = np.ones(n, dtype=bool)
    for lb, zw in MEMBERS:
        zb = _own_mom_z(cb, lb, zw)
        ze = _own_mom_z(ce, lb, zw)
        votes += np.nan_to_num(np.sign(zb - ze), nan=0.0)
        valid &= np.isfinite(zb) & np.isfinite(ze)
    dir_b = votes / len(MEMBERS)          # graded conviction long(+)/short(-) BTC vs ETH
    dir_e = -dir_b                        # dollar-neutral by construction

    # --- 2) vol-target the SPREAD. Risk unit = realized vol of a static long-BTC/short-ETH
    #         unit spread. Scale to TARGET_VOL, never grossing a single leg above unit. --------
    rb = ol.simple_returns(cb)
    re = ol.simple_returns(ce)
    spread_ret = rb - re
    rv = ol.realized_vol(spread_ret, VOL_WIN, 365.25)
    scale = np.where((rv > 0) & np.isfinite(rv), TARGET_VOL / rv, 0.0)
    scale = np.clip(scale, 0.0, 1.0)

    wb = np.clip(dir_b * scale, -LEG_CAP, LEG_CAP)
    we = np.clip(dir_e * scale, -LEG_CAP, LEG_CAP)

    # warmup: flat until every ensemble member's z-score is defined
    wb[~valid] = 0.0
    we[~valid] = 0.0
    return wb.astype(float), we.astype(float)
