"""agent_06_ratio_accel — ACCELERATION of log(ETH/BTC), market-neutral 2-leg book.

ANGLE [family=rv, slug=ratio_accel]
-----------------------------------
A 2-leg BTC/ETH relative-value book driven by the ACCELERATION (2nd difference /
momentum-of-momentum) of the relative price s = log(ETH/BTC). Where agent_00/05 ride the
LEVEL/VELOCITY of the ratio trend, this book reads its *curvature*: it leans INTO an
accelerating relative move and CUTS size when the relative move decelerates. Market-neutral.

Construction (all causal, online):
  s   = log(ETH) - log(BTC)              relative price in logs
  v   = EMA( diff(s) )                   velocity  = smoothed 1st difference (relative slope)
  a   = EMA( diff(v) )                   acceleration = smoothed 2nd difference (curvature)
Each is normalized by its OWN causal rolling std (vn, an) so the inputs are stationary across
regimes. The DIRECTION is a velocity trend TILTED by acceleration — `tanh(k*(vn + WA*an))`:
the relative trend sets the base direction, and the acceleration term pulls the position
EARLIER into moves that are curving up and OUT of moves that are curving over. On top of that
a DECELERATION-CUT gate throttles size toward DECEL_FLOOR whenever acceleration opposes the
current direction (the move is losing steam). Finally the size is lightly EMA-smoothed (fewer
fee-bleeding flips) and SPREAD-VOL-TARGETED so the realized vol of the ETH-BTC spread return
hits a target, capped at the live per-leg notional (0.5 of equity = $300/asset at $600 real).
    a-tilt > 0  (ratio curving up)   => LONG the ratio (LONG ETH / SHORT BTC)
    a-tilt < 0  (ratio curving down) => SHORT the ratio (SHORT ETH / LONG BTC)

HONEST NOTE on the angle (the research underneath these knobs):
  * Pure short-horizon acceleration as a STANDALONE direction is whipsaw noise (Sharpe < 0,
    DILUTES). Long-horizon "acceleration" is just velocity in disguise. The genuine residual
    that acceleration contributes is NOT extra return on top of the ratio trend — it is LOWER
    CORRELATION + an earlier turn. So the design keeps the ratio velocity as the spine and
    uses acceleration as a MATERIAL tilt (WA=0.6, a real contributor, not decoration): that
    drops corr_full to ~0.02 / corr_hold ~0.06 and differentiates it from agent_05 (the pure
    EMA-cross velocity book, corr ~0.6) while still ADDING to the TP01 portfolio out-of-sample.

WHY orthogonal to TP01: legs are w_eth = +g, w_btc = -g => net market beta ~0. TP01 is a
long-flat trend on the market SUM; this is curvature of the DIFFERENCE — residual relative
value, not trend-beta. That low correlation is what earns the marginal uplift.

CAUSAL: EMAs/diffs are recursive over rows 0..i only; normalization uses a causal rolling std;
tanh is pointwise; the size EMA and the spread-vol target use only data up to i. No shift(-k),
no centered window, no global fit. EXECUTABLE: per-leg |w| <= 0.5. MARKET-NEUTRAL: w_eth == -w_btc.

Plateau (all ADDS + robust_oos True; not a lucky cell):
  VEL_SPAN 35-45, ACC_SPAN 25-30, NORM_WIN 150-180, WA 0.5-0.7, DECEL_FLOOR 0.4-0.6,
  SMOOTH 3-5, TARGET_SPREAD_VOL 0.13-0.15  -> up_h ~0.20-0.29, corr_hold ~0.05-0.07.
"""
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 (center of the plateau above) ----------------------------------
VEL_SPAN = 40                  # EMA span to smooth the velocity (1st diff) of the log ratio
ACC_SPAN = 30                  # EMA span to smooth the acceleration (2nd diff)
NORM_WIN = 180                 # causal window to normalize velocity & acceleration (own scale)
WA = 0.6                       # weight of the acceleration TILT on the velocity direction
TANH_K = 1.3                   # tanh slope: normalized (v + WA*a) -> directional size in [-1,1]
DECEL_FLOOR = 0.5              # min retained size when acceleration fully opposes the move
SMOOTH = 4                     # EMA span on the final size (cuts turnover, fewer fee-flips)
TARGET_SPREAD_VOL = 0.13       # 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 _ema_diff(x: np.ndarray, span: int) -> np.ndarray:
    """Causal smoothed first difference: EMA( x[i] - x[i-1] )."""
    d = np.zeros(len(x))
    d[1:] = np.diff(x)
    return ol.ema(d, span)


def _causal_norm(x: np.ndarray, win: int) -> np.ndarray:
    """x divided by its OWN causal rolling std (stationary input for tanh)."""
    sd = pd.Series(x).rolling(win, min_periods=max(2, win // 2)).std().values
    return np.nan_to_num(np.where((sd > 0) & np.isfinite(sd), x / sd, 0.0), nan=0.0)


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

    # relative price in logs: rising => ETH outperforming BTC
    s = np.log(ec) - np.log(bc)

    # velocity (1st diff, EMA) and acceleration (2nd diff, EMA) of the ratio — both causal
    v = _ema_diff(s, VEL_SPAN)
    a = _ema_diff(v, ACC_SPAN)
    vn = _causal_norm(v, NORM_WIN)
    an = _causal_norm(a, NORM_WIN)

    # DIRECTION: ratio velocity tilted by acceleration (lean EARLY into curving-up moves).
    g_dir = np.tanh(TANH_K * (vn + WA * an))

    # DECELERATION CUT: throttle size toward DECEL_FLOOR when acceleration opposes the move
    # (curvature against the current direction => losing steam). agree>0 => accelerating.
    agree = np.tanh(an * np.sign(g_dir + 1e-12))
    gate = DECEL_FLOOR + (1.0 - DECEL_FLOOR) * np.clip(0.5 + 0.5 * agree, 0.0, 1.0)

    g_sig = g_dir * gate
    if SMOOTH and SMOOTH > 1:
        g_sig = ol.ema(g_sig, SMOOTH)              # fewer fee-bleeding flips (causal EMA)

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