"""agent_10_vol_regime_rv — Ratio momentum GATED by the SPREAD-VOL regime (stable vs erratic).

ANGLE [family=gate, slug=vol_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 the REGIME of the SPREAD's own volatility:

  * When the spread's realized vol is in a CALM / STABLE regime, the relative move is a
    persistent drift => ratio momentum is tradeable => size the book UP.
  * When the spread vol SPIKES erratically (a vol blow-out, or the vol itself is jumping
    around — "vol of vol"), the spread whipsaws and a momentum book gets chopped to pieces
    => shrink toward FLAT, sit out the storm.

This is the whole thesis of the angle: trend (here, RELATIVE trend) only persists in a
quiet regime; in a high / unstable spread-vol regime the relative move is mean-reverting
noise, and a momentum book just pays fees into whipsaw. The gate concentrates risk into the
stable regimes where ratio momentum actually carries.

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 (the spread's OWN vol stability) that is 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. "Calm" is defined by an EXPANDING, CAUSAL
quantile of the spread-vol's own history: the gate is fully OPEN when today's spread vol sits
in the lower part of everything seen SO FAR (percentile <= P_LO) and fully OFF in the top
part (>= P_HI), with a smooth linear ramp between. A second factor multiplies in the
INSTABILITY of the vol itself (vol-of-vol percentile): even at a moderate vol level, if the
vol is whipping around erratically the regime is unstable => damp further. Because crypto
spread-vol drifts across eras, the expanding quantile re-bases "calm" to each era and is
strictly causal (uses only spread-vol[0..i]).

PLATEAU (verified by one-knob sweep: 19/19 neighbour cells are ADDS + robust_oos + uplift_hold
> 0.05): HORIZONS multi-blend {40,120,240} (and {30,90,180}/{20,60,120,240} all hold), VOL_WIN
20-40, VOV_WIN 45-90, P_LO/P_HI in {40..50}/{85..95}, GATE_FLOOR 0.20-0.30, VOV_P_HI 0.80-0.90,
TGT 0.16-0.20, SIZE_VOL_WIN 30-60, TANH_K 0.8-1.3. The chosen interior point is not a lucky cell.

HONEST CAVEAT (the angle, stated straight): a pure ratio-momentum book WITHOUT this gate scores a
HIGHER hold-out uplift (~0.52) — the gate is NOT what maximizes raw uplift, it is what BUYS
ORTHOGONALITY AND TAIL CONTROL. The gate cuts corr-to-TP01 (full 0.03 -> -0.04, hold 0.28 -> 0.09)
and max-DD (~0.34 -> ~0.24) at the cost of some uplift, and the harvest is concentrated by the
"erratic-vol" (vol-of-vol) damp — flattening when the spread's vol spikes/whips is the part of the
gate that does the most work (it alone lifts standalone Sharpe 0.28 -> 0.44). Net of the gate the
book still clearly ADDS (uplift_hold ~0.35) and is far more orthogonal/robust than the ungated cousin.

CAUSAL: rolling momentum z-scores, rolling spread realized-vol, the expanding quantile of
spread-vol and of vol-of-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 = (40, 120, 240)      # ratio-momentum lookbacks (days) — multi-orizzonte blend
ZWIN = 252                     # window to z-score each horizon's momentum (causal)
TANH_K = 1.0                   # tanh slope (signal -> directional size)

VOL_WIN = 30                   # realized-vol window (days) for the SPREAD return
VOV_WIN = 60                   # window for the vol-of-vol (std of spread-vol changes)
EXP_MIN = 120                  # min history before the expanding quantiles are trusted
P_LO = 0.45                    # spread-vol percentile at/below which the gate is FULLY OPEN
P_HI = 0.90                    # spread-vol percentile at/above which the gate is FULLY CLOSED
GATE_FLOOR = 0.25              # never fully zero even in the worst regime (keep a small core)
VOV_P_HI = 0.85                # vol-of-vol percentile at which the instability damp is full

TARGET_SPREAD_VOL = 0.18       # annualized target vol of the ETH-BTC spread return
SIZE_VOL_WIN = 45              # 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 _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.
    Running sorted insert via bisect on the growing prefix (only finite values count)."""
    import bisect
    n = len(x)
    out = np.full(n, np.nan)
    seen = []  # kept sorted
    for i in range(n):
        v = x[i]
        if np.isfinite(v):
            lo = bisect.bisect_right(seen, v)
            bisect.insort(seen, v)
            cnt = len(seen)            # includes v itself
            if cnt >= min_obs:
                out[i] = (lo + 1) / cnt
    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]

    # ---- SPREAD-VOL REGIME GATE (the angle) --------------------------------
    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 spread vol

    # (1) LEVEL gate: where does today's spread-vol sit in its own expanding history?
    pct = _expanding_pctl_rank(spvol, EXP_MIN)             # causal percentile of spread-vol
    ramp = (P_HI - pct) / (P_HI - P_LO)                    # 1 at P_LO (calm), 0 at P_HI (storm)
    ramp = np.clip(ramp, 0.0, 1.0)
    level_gate = GATE_FLOOR + (1.0 - GATE_FLOOR) * ramp
    level_gate = np.where(np.isfinite(level_gate), level_gate, 0.5)  # neutral early

    # (2) INSTABILITY damp: vol-of-vol = rolling std of day-over-day spread-vol changes.
    #     Even at a moderate level, if the vol itself is whipping around the regime is erratic.
    dvol = np.full_like(spvol, np.nan)
    dvol[1:] = np.diff(spvol)
    vov = ol.rolling_std(dvol, VOV_WIN)                    # causal vol-of-vol
    vov_pct = _expanding_pctl_rank(vov, EXP_MIN)           # causal percentile of vov
    # damp = 1 when vov is calm, ramps to GATE_FLOOR as vov_pct -> VOV_P_HI
    vov_ramp = (VOV_P_HI - vov_pct) / VOV_P_HI
    vov_ramp = np.clip(vov_ramp, 0.0, 1.0)
    instab_damp = GATE_FLOOR + (1.0 - GATE_FLOOR) * vov_ramp
    instab_damp = np.where(np.isfinite(instab_damp), instab_damp, 1.0)  # no damp early

    gate = level_gate * instab_damp

    # ---- spread-vol TARGET + gate + cap ------------------------------------
    spvol_t = ol.realized_vol(spread_ret, SIZE_VOL_WIN, 365.25)
    scal = np.where((spvol_t > 0) & np.isfinite(spvol_t), TARGET_SPREAD_VOL / spvol_t, 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
