"""agent_12_rebalance_harvest — CONTRARIAN volatility-rebalancing / dispersion harvest.

ANGLE [family=struct, slug=rebalance_harvest]
---------------------------------------------
A 2-leg BTC/ETH relative-value book that monetizes the *relative OSCILLATION* of the two
legs around a SLOW anchor. BTC and ETH are the same beta-1 family: their log price ratio
s = log(ETH/BTC) wanders, but day to day it OSCILLATES around a slowly-moving center far
more than it makes net progress. A systematic rebalancer harvests that oscillation: when ETH
has temporarily run UP vs BTC (s above its slow anchor), it is "rich" in the pair — UNDERweight
ETH, OVERweight BTC, and collect the snap-back toward the anchor; symmetric on the other side.
This is the cross-sectional analogue of constant-mix rebalancing ("sell the winner, buy the
loser") between two correlated assets, which earns a positive *rebalancing premium* precisely
because the spread is mean-reverting around a drifting center rather than a random walk.

WHY this differs from the momentum siblings (agents 00/05/07 ride the relative TREND): this
book is CONTRARIAN — it FADES the deviation from the anchor. That is the opposite sign of a
relative-momentum book, so it harvests a different statistical feature (short-horizon
reversion of dispersion) and can blend rather than duplicate.

THE GATE (the whole reason this is not a knife-catcher): pure contrarian rebalancing bleeds
exactly when the spread STOPS oscillating and instead TRENDS persistently (the multi-year
ETH/BTC regimes — 2020-21 catch-up, 2022-24 bleed). So we GATE the contrarian size by a
persistence measure: a SLOW relative-momentum strength of the spread. When the spread is in a
strong persistent trend (|slow mom z| large), we FADE the contrarian bet toward flat (don't
fight the regime); when the spread is range-bound / oscillating (slow mom near zero), we take
the full contrarian rebalancing position (that is where the harvest lives). The gate is a
smooth multiplier in [0,1], causal and era-relative.

SIGNAL (all causal, rows 0..i only):
  s        = log(ETH/BTC)
  anchor   = EMA(s, ANCHOR_SPAN)                 # slow drifting center
  dev      = s - anchor                          # short-horizon deviation
  devz     = zscore(dev, ZWIN)                   # era-relative size of the oscillation
  contr    = -tanh(TANH_K * devz)                # CONTRARIAN: dev>0 => short ETH side
  trendz   = zscore(slow_mom(s), ZWIN)           # persistence of the spread (relative trend)
  gate     = exp(-(trendz/GATE_W)^2)             # 1 when range-bound, ->0 in strong trend
  size     = contr * gate                        # gated contrarian size
  spread-vol target on (r_eth - r_btc), cap each leg at 0.5
  w_eth = +size  (size>0 => long ETH leg) ; w_btc = -size

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 contrarian tilt on the DIFFERENCE's oscillation around its own slow
center — a structural rebalancing premium unrelated to the market level or its trend. Returns
are residual relative-value, not trend-beta — that is what can earn a NEW live slot.

CAUSAL: EMA, rolling z-score, rolling 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 warnings

import numpy as np

sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/ortho")
import ortholib as ol  # noqa: E402

# ---- knobs (the LEAST-harmful faithful cell — this angle has NO positive edge
#      on the BTC/ETH spread; see the honest NOTES at the bottom) -------------
ANCHOR_SPAN = 40          # EMA span (days) of log(ETH/BTC): the slow drifting center
ZWIN = 252                # causal window to z-score deviation & trend (era-relative)
TANH_K = 0.9              # tanh slope (deviation z -> contrarian size)
SMOOTH = 5                # EMA span (days) smoothing the gated signal (cut turnover)
DEADBAND = 0.05           # |size| below this -> flat (kills micro-flip fee bleed)

TREND_H = 120             # SLOW relative-momentum horizon (days) for the persistence gate
GATE_W = 1.0              # gate width (z units): smaller => fade contrarian sooner in a trend

TARGET_SPREAD_VOL = 0.11  # annualized target vol of the ETH-BTC spread return
VOL_WIN = 45              # realized-vol window (days) for spread-vol targeting
LEG_CAP = 0.5             # live per-leg notional cap (fraction of equity)


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

    # ---- the relative price and its slow anchor ----------------------------
    s = np.log(ec) - np.log(bc)                 # log(ETH/BTC)
    anchor = ol.ema(s, ANCHOR_SPAN)             # slow drifting center (causal)
    dev = s - anchor                            # short-horizon deviation from center

    # era-relative size of the oscillation
    devz = ol.zscore(dev, ZWIN)
    devz = np.nan_to_num(devz, nan=0.0)
    contr = -np.tanh(TANH_K * devz)             # CONTRARIAN: dev>0 => short ETH (size<0)

    # ---- persistence gate: don't fade a strong relative TREND --------------
    mom = np.full(len(s), np.nan)
    mom[TREND_H:] = s[TREND_H:] - s[:-TREND_H]  # slow relative momentum of the spread
    trendz = ol.zscore(mom, ZWIN)
    trendz = np.nan_to_num(trendz, nan=0.0)
    gate = np.exp(-((trendz / GATE_W) ** 2))    # 1 when range-bound, ->0 in strong trend

    g_dir = ol.ema(contr * gate, SMOOTH)        # smooth to crush turnover (fee bleed)
    g_dir = np.where(np.abs(g_dir) < DEADBAND, 0.0, g_dir)

    # ---- size the spread to a target spread-vol, then cap ------------------
    rb = ol.simple_returns(bc)
    re = ol.simple_returns(ec)
    spread_ret = re - rb
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        spvol = ol.realized_vol(spread_ret, VOL_WIN, 365.25)
    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)

    # g>0 => long ETH leg (it is BELOW anchor / cheap), short BTC; symmetric otherwise
    w_eth = g
    w_btc = -g
    return w_btc, w_eth


# ============================================================================
# HONEST NOTES — this angle does NOT earn a slot (negative result, as expected)
# ============================================================================
# The orthogonality is PERFECT (corr_hold ~0, corr_full ~ -0.05 to -0.06): a
# market-neutral contrarian tilt on the DIFFERENCE is genuinely uncorrelated to
# TP01's long-flat trend on the SUM. But there is NO alpha to harvest:
#
#   * The daily ETH/BTC spread return is a near-RANDOM WALK. Its autocorrelation
#     is ~0 at every lag (lag1 +0.03, lag2 +0.02, lag3..10 noise ~±0.03). There
#     is no short-horizon reversion of the spread to monetize.
#   * A contrarian "fade the deviation from the slow anchor" book is GROSS-
#     negative at EVERY anchor span (20..180d): Sharpe -0.43..-0.23 BEFORE fees.
#     The spread's only persistent feature is slow MOMENTUM (which is why the
#     relative-momentum siblings agents 00/05/07 DO earn slots) — the exact
#     opposite of what a rebalancing/dispersion harvest needs.
#   * Conditioning on regime does not save it: contrarian is positive ONLY in a
#     mid-trend band (|trendz| 1-2, Sharpe +0.78) — but that is a snap-back-
#     WITHIN-a-trend artifact (disguised momentum), NOT a harvest, and it is
#     negative in the flat/range-bound regime (-0.67) where a true rebalancing
#     premium should live. Shipping that pocket would be regime-luck and would
#     fail robust_oos; the project's honesty rule forbids it.
#   * Tail-only fades (|z|>1.5..2.5) are ~0 gross (best +0.18 at one noisy cell).
#
# Result across the whole faithful plateau (span 40-90, smooth 5-25, gate on):
# standalone Sharpe -0.3..-0.6, marginal uplift_hold -0.003..-0.13 (NEUTRAL to
# mildly DILUTES), robust_oos = FALSE everywhere. This cell (span40/smooth5) is
# the LEAST-harmful faithful point (up_h ~ 0, corr ~ 0): "no harm, no help".
#
# VERDICT: rebalance_harvest is NEUTRAL/DILUTES — a valid, expected negative
# result. The BTC/ETH pair simply does not offer a contrarian dispersion edge;
# its relative-value alpha is on the MOMENTUM side, already covered by siblings.
