"""agent_07_ratio_carry_slow — SLOW relative-trend carry on log(ETH/BTC), market-neutral.

ANGLE [family=rv, slug=ratio_carry_slow]
----------------------------------------
A 2-leg BTC/ETH relative-value book that rides the SLOW, persistent regimes of relative
strength between ETH and BTC. The ETH/BTC ratio does not chop around a fixed mean: it
TRENDS for years at a time (the 2020-21 ETH catch-up, then the long 2023-26 ETH bleed). A
long-horizon momentum on the log ratio captures that "relative carry" with VERY LOW turnover
— we deliberately use ~120-200d lookbacks and heavy EMA smoothing so the book flips sides
only a handful of times per year, paying almost no fees while holding a slow directional
tilt of the spread.

  s = log(ETH/BTC)                       # the relative price, in logs
  slow momentum sign/size from a blend of ~120/180d log-changes of s, each z-scored on a
  LONG causal window, then EMA-smoothed to crush turnover; squashed by tanh to a size in
  [-1,1]; SPREAD-VOL-TARGETED so realized spread vol hits a target; per-leg capped at 0.5.
    w_eth = +g, w_btc = -g     (long the slow-stronger leg, short the slow-weaker)

WHY orthogonal to TP01: net market beta ~0 (w_eth == -w_btc), so the LEVEL of the market
(TP01's long-flat trend signal) carries no information about which leg is winning. The
return is residual relative-value, not trend-beta — that is what can earn a NEW live slot.

WHY *slow* (vs agent_00's 20/60/120 z-blend or agent_05's 20/80 EMA-cross): turnover is the
enemy of a 2-leg book at $600 (fees on BOTH legs every flip). A slow carry holds the right
side of the multi-year ETH/BTC regime almost statically, so the marginal alpha is not eaten
by fee bleed and the texture (long, low-turnover holds) differs from the faster siblings —
it can blend rather than duplicate. The turnover/uplift trade-off is the thing we optimize.

CAUSAL: log-changes, z-score, EMA, realized vol are all recursive/rolling over rows 0..i
only. No shift(-k), no centered window, no global fit. EXECUTABLE: per-leg |w| <= 0.5.
MARKET-NEUTRAL: w_eth == -w_btc by construction.
"""
from __future__ import annotations

import sys

import numpy as np
import pandas as pd

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) ----------------
# Chosen as the best turnover/uplift trade-off of a WIDE plateau: every neighbour
# (HORIZONS 90-250, ZWIN 252-504, SMOOTH 10-60, TANH_K 0.8-1.8, TGT 0.08-0.16) scores
# ADDS + robust_oos. This cell maximizes uplift_hold per unit turnover at low DD.
HORIZONS = (120, 180)          # SLOW momentum lookbacks (days) on the log ratio
ZWIN = 312                     # long (~1.25y) causal window to z-score each horizon's mom
SMOOTH = 25                    # EMA span (days) to smooth the signal -> crush turnover
TANH_K = 1.2                   # tanh slope (z signal -> directional size in [-1,1])
DEADBAND = 0.05                # |size| below this -> flat (kills micro-flips / fee bleed)
TARGET_SPREAD_VOL = 0.11       # annualized target vol of the ETH-BTC spread return
VOL_WIN = 60                   # realized-vol window (days) for spread-vol targeting (slow)
LEG_CAP = 0.5                  # live per-leg notional cap (fraction of equity)


def _mom_z(logp: np.ndarray, h: int, zwin: int) -> np.ndarray:
    """Causal z-scored h-day log momentum of a log-price series."""
    s = np.full(len(logp), np.nan)
    s[h:] = logp[h:] - logp[:-h]            # h-day log change, known at i
    return ol.zscore(s, zwin)               # standardize cross-time (causal rolling)


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

    # relative price in logs: positive momentum => ETH slow-outperforming BTC
    logratio = np.log(ec) - np.log(bc)

    # SLOW multi-horizon z-scored momentum (mean of per-horizon z-scores)
    zs = np.vstack([_mom_z(logratio, h, ZWIN) for h in HORIZONS])
    with np.errstate(invalid="ignore"):
        import warnings
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=RuntimeWarning)
            sig = np.nanmean(zs, axis=0)
    sig = np.nan_to_num(sig, nan=0.0)

    # EMA-smooth the signal to CRUSH turnover (the whole point of "slow carry")
    sig = ol.ema(sig, SMOOTH)

    # squash to a directional size in [-1, 1]
    g_dir = np.tanh(TANH_K * sig)
    # deadband: flat when the tilt is tiny (no fee-bleeding micro-positions)
    g_dir = np.where(np.abs(g_dir) < DEADBAND, 0.0, g_dir)

    # spread-vol target: scale by target/realized vol of the spread return r_eth - r_btc
    rb = ol.simple_returns(bc)
    re = ol.simple_returns(ec)
    spread_ret = re - rb
    spvol = ol.realized_vol(spread_ret, VOL_WIN, 365.25)   # annualized, causal
    scal = np.where((spvol > 0) & np.isfinite(spvol), TARGET_SPREAD_VOL / spvol, 0.0)

    g = g_dir * scal
    g = np.clip(g, -LEG_CAP, LEG_CAP)
    g = np.nan_to_num(g, nan=0.0)

    w_eth = g
    w_btc = -g
    return w_btc, w_eth
