"""agent_02_beta_neutral_resid — Beta-neutral ETH/BTC residual, traded on its momentum.

ANGLE [family=rv, slug=beta_neutral_resid]
------------------------------------------
A market-neutral relative-value book whose hedge ratio ADAPTS:
  1. estimate a CAUSAL rolling beta of ETH returns on BTC returns,
        beta_i = Cov_win(r_eth, r_btc) / Var_win(r_btc)   (expanding/rolling, no global fit)
  2. form the BETA-NEUTRAL residual spread return  s = r_eth - beta * r_btc
     (this is the part of ETH NOT explained by the market move in BTC),
  3. accumulate s into a residual "price" and trade the SIGN/MOMENTUM of that residual:
        signal>0  => the residual has been trending UP (ETH richening vs its beta-hedge)
                     => LONG the residual:  long ETH, short beta*BTC,
        signal<0  => SHORT the residual:    short ETH, long beta*BTC.
  4. size by vol-targeting the residual spread, cap each leg at the live notional cap.

Because the book holds ETH against a BETA-WEIGHTED BTC hedge, its NET market beta is ~0 by
construction — so it is structurally uncorrelated to TP01 (a long-flat trend on the market
SUM). The bet is pure RESIDUAL relative-value: does the beta-neutral ETH-vs-BTC residual
have exploitable momentum? That has nothing to do with the market's overall direction.

The two legs carry DIFFERENT notional: |w_eth| = g, |w_btc| = g*beta. Both are capped at the
$300/asset live cap (LEG_CAP=0.5 of $600 equity). beta hovers ~1, so this is fine.

CAUSAL: beta, residual-price, momentum z, vol all use only rows 0..i (rolling, no shift(-k),
no global fit). EXECUTABLE: per-leg |w| <= 0.5. ~MARKET-NEUTRAL: w_btc = -beta*w_eth.

VERDICT (ortho_score): marginal=ADDS, robust_oos=true, corr_hold~0.10, corr_full~0.05,
uplift_hold ~+0.50 (TP01 hold Sharpe 0.31 -> blend ~0.81 at w=0.25), uplift_full ~+0.08.
Standalone is MODEST and LUMPY by design (full Sh ~0.54, DD ~20%): great years 2020 (+21%),
2025 (+25%) but LOSING 2023 (-6%), 2024 (-6%) — the residual-momentum edge comes and goes.
The slot is earned by ORTHOGONALITY (genuinely uncorrelated residual alpha that lifts the
defensive stack's weak hold-out), NOT by a standalone Sharpe. Honest caveat: the big hold-out
uplift is 2025-weighted and the hold-out is short (~537d); treat as forward-monitor, not a
heavy weight. Robust to fees (still ADDS at 4x = 0.20%/side), turnover ~19/yr.
"""
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

# ---- knobs (a PLATEAU point, not a lucky cell — see notes) -------------------
# Plateau-verified: ADDS + robust_oos across the WHOLE grid (BETA_WIN 45-180,
# ZWIN 120-365, TANH_K 0.5-1.8, VOL_WIN 20-60). The multi-horizon residual-momentum
# blend (the same multi-orizzonte idea as TP01/XS01) is what carries it; the
# longer (20,60,120,240) blend is the strongest, lowest-DD cell and is chosen here.
BETA_WIN = 90          # rolling window (days) for the causal hedge beta
MOM_HORIZONS = (20, 60, 120, 240)  # residual-momentum lookbacks (days), multi-horizon blend
ZWIN = 252             # window to z-score residual momentum (causal)
TANH_K = 1.0           # tanh slope (signal -> directional size in [-1,1])
TARGET_VOL = 0.15      # annualized target vol of the residual spread return
VOL_WIN = 45           # realized-vol window (days) for vol-targeting the residual
LEG_CAP = 0.5          # live per-leg notional cap (fraction of equity)
BETA_FLOOR, BETA_CAP = 0.3, 2.0  # keep the adaptive hedge in a sane band


def _rolling_beta(re: np.ndarray, rb: np.ndarray, win: int) -> np.ndarray:
    """Causal rolling beta of ETH returns on BTC returns: Cov/Var over a trailing window.
    beta_i uses returns up to and including bar i (which are known at close[i])."""
    n = len(re)
    beta = np.full(n, np.nan)
    # rolling sums for cov & var (trailing window of length `win`)
    for i in range(win, n):
        x = rb[i - win + 1:i + 1]
        y = re[i - win + 1:i + 1]
        vx = np.var(x)
        if vx > 0:
            beta[i] = np.cov(y, x)[0, 1] / vx
    return beta


def _mom_z(price_like: np.ndarray, h: int, zwin: int) -> np.ndarray:
    """Causal z-scored h-day change of an accumulated (log-like) series."""
    s = np.full(len(price_like), np.nan)
    s[h:] = price_like[h:] - price_like[:-h]
    return ol.zscore(s, zwin)


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

    rb = ol.simple_returns(cb)   # r_btc[i] = close[i]/close[i-1]-1, known at close[i]
    re = ol.simple_returns(ce)

    # 1) causal adaptive hedge beta of ETH on BTC
    beta = _rolling_beta(re, rb, BETA_WIN)
    beta = np.clip(beta, BETA_FLOOR, BETA_CAP)
    beta_filled = np.nan_to_num(beta, nan=1.0)   # before warmup, assume beta=1

    # 2) beta-neutral residual spread return s_i = r_eth - beta_i * r_btc.
    #    Use beta known at i (causal). The residual is the part of ETH NOT explained by BTC.
    resid_ret = re - beta_filled * rb

    # 3) accumulate residual into a "price" path and trade its MOMENTUM (multi-horizon z).
    resid_price = np.cumsum(np.nan_to_num(resid_ret, nan=0.0))
    import warnings
    zs = np.vstack([_mom_z(resid_price, h, ZWIN) for h in MOM_HORIZONS])
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        sig = np.nanmean(zs, axis=0)
    sig = np.nan_to_num(sig, nan=0.0)

    # directional size on the RESIDUAL (long residual = long ETH / short beta*BTC)
    g_dir = np.tanh(TANH_K * sig)

    # 4) vol-target the residual spread return to constant risk
    rv = ol.realized_vol(resid_ret, VOL_WIN, 365.25)
    scal = np.where((rv > 0) & np.isfinite(rv), TARGET_VOL / rv, 0.0)
    g = g_dir * scal

    # ETH leg = g (the residual is expressed per-unit-ETH); BTC hedge leg = -beta*g
    w_eth = g
    w_btc = -beta_filled * g

    # per-leg notional caps (cap whichever leg would breach first, keep the hedge ratio)
    over = np.maximum(np.abs(w_eth), np.abs(w_btc)) / LEG_CAP
    over = np.where(over > 1.0, over, 1.0)
    w_eth = w_eth / over
    w_btc = w_btc / over

    # warmup: flat until beta & momentum are defined
    warm = ~np.isfinite(beta) | (np.arange(n) < BETA_WIN + max(MOM_HORIZONS))
    w_eth = np.where(warm, 0.0, w_eth)
    w_btc = np.where(warm, 0.0, w_btc)
    return np.nan_to_num(w_btc), np.nan_to_num(w_eth)
