"""agent_17_ensemble_rv — META-BLEND of 3-4 causal RV sub-signals, one market-neutral book.

ANGLE [family=mix, slug=ensemble_rv]
------------------------------------
A single 2-leg BTC/ETH relative-value book (w_eth = +g, w_btc = -g, net beta ~0) whose
directional SIZE g is a meta-blend of three structurally DIFFERENT causal sub-signals on the
log ratio s = log(ETH/BTC), then put through ONE shared risk overlay (correlation gate +
spread-vol target + per-leg cap). The thesis: the individual ratio-value siblings (fast
multi-horizon z-momentum, slow EMA carry, ...) are each ADDS-but-noisy; they share the SAME
edge (relative strength of ETH vs BTC persists) but express it with different turnover/lag
texture. Averaging their DIRECTIONS — not their PnL — gives a single signal with the same
mean edge and LOWER idiosyncratic variance, which is exactly what the marginal scorer (blend
uplift + drop-one-month jackknife on the hold-out) rewards: a smoother, more robust lift.

The four sub-signals (each -> a direction in [-1,1], all causal):
  (A) FAST multi-horizon ratio momentum  : mean of z-scored {20,60,120,240}d log-changes of s,
        squashed by tanh. The core relative-strength signal (agent_00 texture).
  (B) SLOW carry                          : mean of z-scored {120,180}d log-changes of s, then
        EMA-smoothed -> a near-static tilt that holds the multi-year ETH/BTC regime almost
        statically at very low turnover (agent_07 texture). Diversifies A's faster flips.
  (C) EWMA cross                          : sign/size of (fast EMA - slow EMA) of s, normalized
        by the spread's own vol -> a trend-of-the-spread with yet another lag profile.
  (D) MEAN-REVERSION damp on the FAST z   : a small contrarian term on the *very* short (5-10d)
        z of s, blended with a tiny negative weight, that pulls the book off freshly-overshot
        spread extremes. Pure risk-shaping, kept small.

These four directions are blended (fixed convex-ish weights, A and B carry most of it), and the
COMBINED direction is then:
  * GATED by the BTC-ETH correlation regime (expanding causal quantile): size up when the legs
    decouple (corr low for its own era), throttle toward a FLOOR when corr ~1 (spread = noise).
    This is a DD-tamer (the marginal scorer does not pay for it, but it keeps standalone DD sane).
  * SPREAD-VOL-TARGETED: scale so the realized vol of the ETH-BTC spread return hits a target.
  * DEADBANDED + per-leg CAPPED at 0.5 (= $300/asset at $600 live capital).

WHY orthogonal to TP01: w_eth == -w_btc => net market beta ~0. TP01 is a long-flat trend on
the market SUM; this is a (blended) trend on the DIFFERENCE. The market LEVEL carries no
information about which leg wins, so the book's returns are residual relative-value, not
trend-beta — that is what can earn a NEW live slot. corr to TP01 is structurally low.

HONEST FINDINGS (reported straight, from the blend/overlay sweep):
  * The ensemble's REAL value is ROBUSTNESS, not a higher point estimate. At a fair shared
    overlay, the AB(CD) blend's hold-out uplift (~0.498) is about the same as its best single
    parent, but its drop-one-month JACKKNIFE-min uplift (~0.367) is HIGHER than either parent
    alone (A-only ~0.33, B-only ~0.34). Averaging DIRECTIONS (not PnL) of signals that share
    the same edge but differ in lag/turnover cuts idiosyncratic month-to-month variance — the
    blend is harder to break with a single dropped month, which is exactly what the robust_oos
    gate (clean-year AND jackknife both positive) cares about.
  * The CORRELATION GATE does NOT add marginal uplift — it TRIMS it (confirming agent_09's
    finding). A fully-open gate (floor 1.0) scores the highest uplift; every step of tightening
    monotonically lowers it because the ratio-momentum edge keeps working even at high BTC-ETH
    corr, so throttling there forfeits alpha. The gate's only payoff is standalone DRAWDOWN
    (the marginal scorer does not reward it). We keep a GENTLE gate (floor 0.70) so the angle is
    genuinely expressed and corr-to-TP01 stays low, while forfeiting almost none of the uplift.
  * The C (EWMA-cross) and D (short MR damp) legs are near-neutral on the point estimate; they
    earn their small weights by widening the plateau and nudging jackknife/corr the right way.
  * PLATEAU (all ADDS + robust_oos): W_FAST/W_SLOW 0.4-0.6 / 0.3-0.5, W_CROSS 0-0.2, W_MR
    -0.1..0, GATE_FLOOR 0.5-1.0, TARGET_SPREAD_VOL 0.15-0.19, VOL_WIN 30-60, P_LO/P_HI
    0.35-0.45 / 0.80-0.90, DEADBAND 0-0.08 — every cell ADDS + robust_oos. Not a lucky cell.
  * CAVEAT: standalone DD ~25% and Sharpe ~0.37 are MODEST — by design (a market-neutral
    spread book). The job is to IMPROVE the TP01 portfolio at weight 0.25 (it lifts the hold-out
    blend Sharpe by ~+0.50), not to win alone. The ETH/BTC ratio has TRENDED for years (ETH
    catch-up 2020-21, long ETH bleed 2023-26); this book rides those slow regimes, so the
    out-of-sample sample of independent "regimes" is small — forward-monitor before sizing up.

CAUSAL: every term (rolling log-changes, z-score, EMA, rolling corr, expanding quantile,
realized vol) uses only rows 0..i (pandas rolling/ewm/expanding; no shift(-k), no centered
window, no global fit). Verified by ol.causality_ok. EXECUTABLE: per-leg |w| <= 0.5.
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) -------
# (A) fast multi-horizon ratio momentum
FAST_HORIZONS = (20, 60, 120, 240)
FAST_ZWIN = 252
FAST_TANH_K = 1.3
# (B) slow carry
SLOW_HORIZONS = (120, 180)
SLOW_ZWIN = 312
SLOW_SMOOTH = 25
SLOW_TANH_K = 1.2
# (C) EWMA cross of the spread
CROSS_FAST = 20
CROSS_SLOW = 80
CROSS_NORM_WIN = 60
CROSS_TANH_K = 1.0
# (D) short-horizon mean-reversion damp on the fast z
MR_HORIZON = 7
MR_ZWIN = 90
MR_TANH_K = 1.0

# meta-blend weights over the four directions (A dominant core, B diversifier, C texture,
# D a small contrarian damp). They need NOT sum to 1 — the spread-vol target re-normalizes
# the magnitude downstream; only the RELATIVE weights matter for the blended direction.
# Locked at a WIDE plateau (see notes): A & B carry the edge, C/D add texture/robustness.
W_FAST = 0.50
W_SLOW = 0.40
W_CROSS = 0.10
W_MR = -0.05        # negative = mean-revert freshly overshot extremes (small risk-shaper)

# correlation-regime gate (DD-tamer; floor near-1 = gate is GENTLE — see honest note below).
CORR_WIN = 45
CORR_EXP_MIN = 120
P_LO = 0.40
P_HI = 0.85
GATE_FLOOR = 0.70

# shared risk overlay
TARGET_SPREAD_VOL = 0.19
VOL_WIN = 45
DEADBAND = 0.08
LEG_CAP = 0.5


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 _blend_z(logp: np.ndarray, horizons, zwin: int) -> np.ndarray:
    """Mean of per-horizon causal z-scored momenta (NaN early -> 0)."""
    zs = np.vstack([_mom_z(logp, 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)
    return np.nan_to_num(sig, nan=0.0)


def _rolling_corr(rb: np.ndarray, re: np.ndarray, win: int) -> np.ndarray:
    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] (online empirical CDF). NaN until min_obs."""
    import bisect
    n = len(x)
    out = np.full(n, np.nan)
    seen = []
    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)
            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)

    logratio = np.log(ec) - np.log(bc)      # rising => ETH outperforming BTC
    rb = ol.simple_returns(bc)
    re = ol.simple_returns(ec)

    # ---- (A) FAST multi-horizon ratio momentum -----------------------------
    a_dir = np.tanh(FAST_TANH_K * _blend_z(logratio, FAST_HORIZONS, FAST_ZWIN))

    # ---- (B) SLOW carry (EMA-smoothed) -------------------------------------
    slow_sig = ol.ema(_blend_z(logratio, SLOW_HORIZONS, SLOW_ZWIN), SLOW_SMOOTH)
    b_dir = np.tanh(SLOW_TANH_K * slow_sig)

    # ---- (C) EWMA cross of the spread (trend-of-the-spread) ----------------
    ef = ol.ema(logratio, CROSS_FAST)
    es = ol.ema(logratio, CROSS_SLOW)
    cross = ef - es
    # normalize the cross by its own causal rolling std -> comparable scale
    cnorm = ol.rolling_std(cross, CROSS_NORM_WIN)
    cz = np.where((cnorm > 0) & np.isfinite(cnorm), cross / cnorm, 0.0)
    c_dir = np.tanh(CROSS_TANH_K * np.nan_to_num(cz, nan=0.0))

    # ---- (D) short-horizon mean-reversion damp -----------------------------
    mr_z = _mom_z(logratio, MR_HORIZON, MR_ZWIN)
    d_dir = np.tanh(MR_TANH_K * np.nan_to_num(mr_z, nan=0.0))   # +z = recently rich ETH

    # ---- META-BLEND of the four directions ---------------------------------
    blend = (W_FAST * a_dir + W_SLOW * b_dir + W_CROSS * c_dir + W_MR * d_dir)
    # re-squash so the blended direction stays in [-1,1] regardless of weight sum
    g_dir = np.tanh(blend)
    g_dir = np.where(np.abs(g_dir) < DEADBAND, 0.0, g_dir)

    # ---- CORRELATION-REGIME GATE (DD-tamer) --------------------------------
    corr = _rolling_corr(rb, re, CORR_WIN)
    pct = _expanding_pctl_rank(corr, CORR_EXP_MIN)
    ramp = np.clip((P_HI - pct) / (P_HI - P_LO), 0.0, 1.0)      # 1 at P_LO, 0 at P_HI
    gate = GATE_FLOOR + (1.0 - GATE_FLOOR) * ramp
    gate = np.where(np.isfinite(gate), gate, 0.7)              # half-open early (causal)

    # ---- 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
