"""agent_04_ratio_donchian — Donchian/channel BREAKOUT on log(ETH/BTC), market-neutral.

ANGLE [family=rv, slug=ratio_donchian]
--------------------------------------
A 2-leg BTC/ETH relative-value book that trades a CHANNEL BREAKOUT of the relative price,
not its momentum z-score (that is agents 00/03). Build the log ratio s = log(ETH/BTC) and
run a Donchian channel on it: when s breaks ABOVE its prior N-bar high, the ETH/BTC ratio
is trending up => go LONG the ratio (LONG ETH / SHORT BTC). When it breaks BELOW its prior
N-bar low, go SHORT the ratio (SHORT ETH / LONG BTC). In between, HOLD the last breakout
state (classic Donchian/turtle: a position is only reversed by an opposite breakout). The
state is then SPREAD-VOL-TARGETED so the ETH-BTC spread return hits a target, capped at the
live per-leg notional (0.5 of equity = $300/asset at $600).

WHY this is 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. A breakout
of the ratio carries no information about the market level, so the book's returns are not
explained by TP01's trend-beta — that is the whole point of earning a NEW live slot.

WHY a channel breakout (not the momentum z of agents 00/03): a Donchian on the ratio fires
on PERSISTENT regime shifts of relative strength (the ETH/BTC ratio has long, slow trends
punctuated by sharp regime breaks — the 2020-21 ETH catch-up, the 2022 unwind, the 2025
rotation). The channel HOLDS through the trend and only flips on a confirmed opposite break,
which is a different return texture than the mean-reverting-when-extended z-score book, so
it can blend with rather than duplicate the momentum sleeve.

A multi-horizon channel BLEND (fast + slow, like TP01's multi-orizzonte) replaces a single
length: averaging the {45d, 90d} breakout states smooths the entry/exit and, crucially,
SPREADS the alpha across years instead of concentrating it in one episode. The single 45d
channel posts a larger hold-out uplift but earns most of it in the 2025 ETH rotation alone
(2022-24 are weak/negative); the {45,90} blend is positive in 2019/20/21/25/26, halves the
worst year, and lifts standalone Sharpe ~0.40->0.56 / cuts DD ~34%->28% — the more HONEST,
less single-episode-dependent point on the plateau. The hold-through-state design plus the
slow leg keeps turnover ~7/yr, so fee survival is first-order even paying on BOTH legs.

PLATEAU (all ADDS + robust_oos, verified): N legs in {[40,80]..[50,100]}, TGT 0.15-0.20,
VOL_WIN 30-90. The interior point [45,90]/0.18/45 maximizes balanced uplift (hold +0.53,
full +0.10) at the best standalone Sharpe/DD and a flat jackknife (+0.36) — not a lucky cell.

CAUSAL: the Donchian high/low use only bars STRICTLY before i (prior N-bar extreme), the
breakout state at i depends only on s[0..i], and the spread-vol target uses realized vol up
to i. 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 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) -------
N_CHANNELS = (45, 90)          # Donchian lookbacks (days) on the log ratio (fast+slow blend)
TARGET_SPREAD_VOL = 0.18       # 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 _donchian_state(s: np.ndarray, n: int) -> np.ndarray:
    """CAUSAL Donchian breakout state on series s, in {-1,0,+1}.

    At each i: upper = max(s[i-n .. i-1]), lower = min(s[i-n .. i-1]) (STRICTLY prior n
    bars). If s[i] > upper => state +1 (broke up). If s[i] < lower => state -1 (broke down).
    Otherwise HOLD the previous state (turtle: only an opposite break reverses). Before the
    first full channel the state is 0 (flat). Uses only rows 0..i => no look-ahead."""
    m = len(s)
    state = np.zeros(m)
    cur = 0.0
    # prior-n rolling extremes, shifted by 1 (strictly before i)
    ss = pd.Series(s)
    upper = ss.rolling(n, min_periods=n).max().shift(1).values
    lower = ss.rolling(n, min_periods=n).min().shift(1).values
    for i in range(m):
        hi, lo = upper[i], lower[i]
        if np.isfinite(hi) and np.isfinite(lo):
            if s[i] > hi:
                cur = 1.0
            elif s[i] < lo:
                cur = -1.0
            # else: HOLD cur
        state[i] = cur
    return state


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

    # log price ratio: rising => ETH outperforming BTC
    logratio = np.log(ec) - np.log(bc)

    # multi-horizon Donchian breakout state on the ratio: average the fast+slow channel
    # states (+1 = long ratio, -1 = short ratio, 0 = flat). The blend smooths flips and
    # spreads the alpha across regimes (see notes), all still causal.
    g_dir = np.mean([_donchian_state(logratio, n) for n in N_CHANNELS], axis=0)

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