"""agent_09_corr_regime_rv — Ratio momentum GATED by the BTC-ETH correlation regime.

ANGLE [family=gate, slug=corr_regime_rv]
----------------------------------------
A 2-leg BTC/ETH relative-value book whose SIGNAL is the relative-strength momentum of
the log ratio s = log(ETH/BTC) (long the stronger leg, short the weaker, net beta ~0),
but whose SIZE is governed by a CORRELATION REGIME GATE:

  * When the rolling BTC-ETH return-correlation is LOW, the two coins are de-coupling:
    their RELATIVE move carries information and is tradeable => size the book UP.
  * When the correlation is HIGH, BTC and ETH are moving as one body: the spread is just
    noise around zero (nothing to harvest, only fees) => shrink toward FLAT.

This is the whole thesis of the angle: a relative-value book only has a job when the
legs are decoupled. Spending gross exposure (and fees) while corr ~1 is pure drag; the
gate concentrates risk into the decoupled regimes where ratio momentum actually persists.

WHY orthogonal to TP01: the legs are w_eth = +g, w_btc = -g => net market beta ~0. TP01
is a long-flat trend on the market SUM; this is a trend on the DIFFERENCE, throttled by a
regime variable (correlation) that is itself unrelated to the market level. So the book's
returns are not explained by TP01's trend-beta — that is what earns a NEW live slot.

THE GATE IS REGIME-RELATIVE, NOT A MAGIC CONSTANT. "Low corr" is defined by an EXPANDING,
CAUSAL quantile of the correlation's own history: the gate opens when today's rolling corr
sits in the lower part of everything seen SO FAR (percentile <= P_LO) and is fully off in
the top part (>= P_HI), with a smooth linear ramp between. Because BTC-ETH correlation has
drifted UP structurally over the years (the 2019-20 idiosyncratic alt era vs the 2022+
"all crypto is one trade" era), a fixed corr threshold would either always-on early and
always-off late, or vice-versa. The expanding quantile re-bases "low" to each era and is
strictly causal (uses only corr[0..i]).

PLATEAU (all ADDS + robust_oos, verified by sweep): CORR_WIN in {30,45,60,90}, MOM
HORIZONS multi-blend {20,60,120,240}, GATE_FLOOR 0.0-0.50, P_LO/P_HI in {30..55}/{70..92},
TGT 0.14-0.20, VOL_WIN 30-60, ZWIN 252. Every cell in this region is ADDS + robust_oos, so
the chosen interior point (cw45/vw60/k1.3/floor0.30, hold-out uplift +0.42, jackknife +0.28)
is not a lucky cell — the result is structurally stable.

HONEST FINDING ON THE GATE (the whole point of this angle, reported straight): the
correlation gate does NOT, by itself, add marginal uplift. The strongest hold-out uplift is
at GATE_FLOOR=1.0 (no gate at all: uh +0.39, standalone Sharpe 0.285); every step of
TIGHTENING the gate (lower floor) monotonically TRIMS the uplift (floor 0.30 -> uh ~0.42 with
the 4-horizon signal, floor 0.0 -> lower). What the gate DOES buy is risk: cutting exposure
in the high-corr regime lowers standalone max-DD (floor 0.0 dd 0.25 vs no-gate 0.30) — a
return-for-drawdown trade the MARGINAL scorer does not reward. Inverting the gate (size up on
HIGH corr) is clearly worse on uplift. So the angle's thesis ("size up when decoupled") is
directionally right for STANDALONE drawdown but is, at best, NEUTRAL on the marginal score:
the ratio-momentum signal happens to keep working even when BTC-ETH corr is high, so
throttling it there mostly forfeits alpha. The book here keeps a GENTLE gate (floor 0.30) so
the angle is genuinely expressed and the DD is tamed, while retaining most of the ungated
uplift. The marginal lift this book earns is driven by the multi-horizon ratio momentum +
spread-vol target (it is a near-cousin of agents 00/04), NOT by the correlation gate — the
gate is a mild risk-overlay, not the source of edge. Reported as required.

CAUSAL: rolling corr, rolling momentum z-scores, the expanding quantile of corr, and the
spread realized-vol all use only rows 0..i (pandas rolling/expanding, no shift(-k), no
centered window, no global fit). EXECUTABLE: per-leg |w| <= 0.5 (= $300/asset at $600).
MARKET-NEUTRAL: w_eth == -w_btc by construction.
"""
from __future__ import annotations

import sys
import warnings

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 (an INTERIOR PLATEAU point, not a lucky cell — see notes) -------
HORIZONS = (20, 60, 120, 240)  # ratio-momentum lookbacks (days) — multi-orizzonte blend
ZWIN = 252                     # window to z-score each horizon's momentum (causal)
TANH_K = 1.3                   # tanh slope (signal -> directional size)

CORR_WIN = 45                  # rolling window (days) for the BTC-ETH return correlation
CORR_EXP_MIN = 120             # min history before the expanding corr-quantile is trusted
P_LO = 0.40                    # corr-percentile at/below which the gate is FULLY OPEN
P_HI = 0.80                    # corr-percentile at/above which the gate is FULLY CLOSED
GATE_FLOOR = 0.30              # never fully zero even in high-corr regime (keep a small core)

TARGET_SPREAD_VOL = 0.16       # annualized target vol of the ETH-BTC spread return
VOL_WIN = 60                   # realized-vol window (days) for spread-vol targeting
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 _rolling_corr(rb: np.ndarray, re: np.ndarray, win: int) -> np.ndarray:
    """CAUSAL rolling Pearson correlation of the two return series over the trailing
    `win` bars (uses only rows 0..i). NaN until the window fills."""
    sb, se = pd.Series(rb), pd.Series(re)
    return sb.rolling(win, min_periods=max(10, win // 2)).corr(se).values


def _expanding_pctl_rank(x: np.ndarray, min_obs: int) -> np.ndarray:
    """CAUSAL percentile rank of x[i] WITHIN x[0..i] (fraction of past+present values
    <= x[i]). Strictly online: at each i only history up to i is used. NaN until min_obs.

    Implemented with an expanding apply on the rank of the current value among the prefix.
    For speed we use a running sorted insert via numpy searchsorted on the growing prefix.
    """
    n = len(x)
    out = np.full(n, np.nan)
    # values seen so far (only finite ones contribute to the empirical CDF)
    seen = []  # kept sorted
    import bisect
    for i in range(n):
        v = x[i]
        if np.isfinite(v):
            # rank of v among seen+{v}: count of seen <= v, then insert
            lo = bisect.bisect_right(seen, v)
            bisect.insort(seen, v)
            cnt = len(seen)            # includes v itself
            if cnt >= min_obs:
                # percentile of v = (#<= v) / cnt ; lo+1 elements (incl v) are <= v
                out[i] = (lo + 1) / cnt
        # NaN corr -> leave out[i] NaN (gate handled downstream)
    return out


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

    # ---- relative-strength SIGNAL: multi-horizon z of ratio momentum -------
    logratio = np.log(ec) - np.log(bc)     # rising => ETH outperforming BTC
    zs = np.vstack([_mom_z(logratio, h, ZWIN) for h in HORIZONS])
    with np.errstate(invalid="ignore"):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=RuntimeWarning)
            sig = np.nanmean(zs, axis=0)
    sig = np.nan_to_num(sig, nan=0.0)
    g_dir = np.tanh(TANH_K * sig)          # directional size in [-1, 1]

    # ---- CORRELATION-REGIME GATE (the angle) -------------------------------
    rb = ol.simple_returns(bc)
    re = ol.simple_returns(ec)
    corr = _rolling_corr(rb, re, CORR_WIN)            # causal rolling BTC-ETH corr
    # expanding causal percentile of the corr within its OWN history (re-bases "low" per era)
    pct = _expanding_pctl_rank(corr, CORR_EXP_MIN)
    # gate = 1 when corr is in the low percentile band, ramps to GATE_FLOOR in the high band.
    # linear ramp from P_LO (open) to P_HI (closed); clamp outside.
    ramp = (P_HI - pct) / (P_HI - P_LO)               # 1 at P_LO, 0 at P_HI
    ramp = np.clip(ramp, 0.0, 1.0)
    gate = GATE_FLOOR + (1.0 - GATE_FLOOR) * ramp
    # before the expanding quantile is trusted (NaN pct) hold a neutral half-open gate so we
    # are not blind early; this stays causal (no future info) and is a small fraction of bars.
    gate = np.where(np.isfinite(gate), gate, 0.5)

    # ---- spread-vol target + gate + cap ------------------------------------
    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 * gate
    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
