"""ortholib — lab for strategies ORTHOGONAL to TP01 (the only thing worth a NEW live slot).

The blind fleet proved (again) that directional BTC/ETH is trend-beta of TP01 — a ~1.3
ceiling, nothing new to deploy. The ONLY way to earn a live slot is a mechanism whose
returns are NOT explained by TP01's trend. The most promising one that is ALSO executable
at our real ~$600 (unlike XS01's 19-leg book, which needs ~$20k) is a **2-leg BTC/ETH
relative-value book**: long one leg / short (or under-weight) the other. Being roughly
market-neutral, its correlation to TP01 (a long-flat trend on the SUM) is naturally low.

A candidate here is a BOOK function:
    def book(btc, eth) -> (w_btc, w_eth)
where w_*[i] in [-1,1] is the per-leg weight (fraction of equity, sign = long/short),
decided causally with data <= close[i]. The evaluator SHIFTS both legs (held during bar
i+1), charges fees on BOTH legs' turnover, and:
  * builds the book's net DAILY return series,
  * scores it MARGINALLY vs TP01 via altlib.marginal_vs_tp01 (corr, OOS blend uplift,
    residual alpha, robust_oos jackknife) — the verdict that matters is ADDS, not Sharpe,
  * checks EXECUTABILITY at $600 (per-leg notional within the live cap, turnover sane),
  * runs the same online causality guard as the blind lab.

Judge a candidate on: marginal_verdict == 'ADDS' AND robust_oos AND executable. Absolute
PnL/DD are reported but are NOT the gate (a market-neutral sleeve has modest absolute
numbers by design; its job is to improve the PORTFOLIO, not to win alone).
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al  # noqa: E402

FEE_SIDE = al.FEE_SIDE
CAPITAL = 600.0          # real mainnet capital (NOT the 2000 paper nominal)
CAP_NOTIONAL = 300.0     # live guardrail: $300 notional / asset
MIN_ORDER = 5.0
CAP_FRAC = CAP_NOTIONAL / CAPITAL    # 0.5 of capital per leg


def aligned():
    """BTC & ETH 1d on COMMON dates (inner join), as two parallel DataFrames with a
    shared 'datetime'. Relative-value needs both legs at the same bar."""
    b = al.get("BTC", "1d").copy()
    e = al.get("ETH", "1d").copy()
    b["dt"] = pd.to_datetime(b["datetime"], utc=True)
    e["dt"] = pd.to_datetime(e["datetime"], utc=True)
    common = pd.Index(b["dt"]).intersection(pd.Index(e["dt"]))
    b = b[b["dt"].isin(common)].sort_values("dt").reset_index(drop=True)
    e = e[e["dt"].isin(common)].sort_values("dt").reset_index(drop=True)
    return b, e


def eval_book(book_fn, fee_side: float = FEE_SIDE) -> dict:
    """Honest backtest of a 2-leg BTC/ETH book. Weights decided at close[i] are HELD
    during bar i+1 (shift here -> no leak). Fee on both legs' turnover. Returns standalone
    metrics, the net DAILY series (for marginal scoring), and executability stats."""
    btc, eth = aligned()
    wb, we = book_fn(btc, eth)
    wb = np.clip(np.nan_to_num(np.asarray(wb, float), nan=0.0), -1, 1)
    we = np.clip(np.nan_to_num(np.asarray(we, float), nan=0.0), -1, 1)
    n = len(btc)
    rb = al.simple_returns(btc["close"].values.astype(float))
    re = al.simple_returns(eth["close"].values.astype(float))
    pb = np.zeros(n); pb[1:] = wb[:-1]
    pe = np.zeros(n); pe[1:] = we[:-1]
    gross = pb * rb + pe * re
    turn = np.abs(np.diff(pb, prepend=0.0)) + np.abs(np.diff(pe, prepend=0.0))
    net = gross - fee_side * turn
    net[0] = 0.0
    idx = pd.DatetimeIndex(btc["dt"])
    m = al._metrics_from_net(net, idx)
    daily = al._to_daily(pd.Series(net, index=idx))
    # executability: worst per-leg notional fraction, and net market exposure
    max_leg = float(np.max(np.maximum(np.abs(pb), np.abs(pe)))) if n else 0.0
    gross_lev = float(np.max(np.abs(pb) + np.abs(pe))) if n else 0.0
    net_beta = float(np.mean(pb + pe))     # ~0 => market-neutral
    turnover_yr = float(turn.sum() / (len(turn) / 365.25))
    return dict(
        pnl=m["ret"], maxdd=m["maxdd"], sharpe=m["sharpe"], cagr=m["cagr"],
        net=net, idx=idx, daily=daily,
        max_leg_frac=round(max_leg, 3), gross_lev=round(gross_lev, 3),
        net_beta=round(net_beta, 3), turnover_per_year=round(turnover_yr, 1),
        executable=bool(max_leg <= CAP_FRAC + 1e-9 and max_leg * CAPITAL >= MIN_ORDER),
    )


def marginal(book_fn, fee_side: float = FEE_SIDE) -> dict:
    """eval_book -> altlib.marginal_vs_tp01 on the book's daily returns. The real verdict."""
    ev = eval_book(book_fn, fee_side)
    marg = al.marginal_vs_tp01(ev["daily"])
    return dict(book=ev, marginal=marg)


def causality_ok(book_fn, tail: int = 60, tol: float = 1e-4) -> dict:
    """Online-consistency guard: book(btc[:cut], eth[:cut]) must match book(full)[:cut]
    on the tail before each cut. Catches any look-ahead / global fit in either leg."""
    btc, eth = aligned()
    wbf, wef = book_fn(btc, eth)
    wbf = np.nan_to_num(np.asarray(wbf, float)); wef = np.nan_to_num(np.asarray(wef, float))
    n = len(btc)
    max_diff = 0.0; frac_bad = 0.0; checked = []
    for cut in (int(n * 0.80), int(n * 0.92)):
        if cut <= tail + 5 or cut >= n:
            continue
        wbs, wes = book_fn(btc.iloc[:cut].reset_index(drop=True),
                           eth.iloc[:cut].reset_index(drop=True))
        wbs = np.nan_to_num(np.asarray(wbs, float)); wes = np.nan_to_num(np.asarray(wes, float))
        if len(wbs) != cut or len(wes) != cut:
            return dict(ok=False, reason=f"book returned wrong length on prefix {cut}",
                        max_diff=9.99, frac_bad=1.0)
        d = np.maximum(np.abs(wbs[cut - tail:cut] - wbf[cut - tail:cut]),
                       np.abs(wes[cut - tail:cut] - wef[cut - tail:cut]))
        max_diff = max(max_diff, float(np.max(d)) if len(d) else 0.0)
        frac_bad = max(frac_bad, float(np.mean(d > tol)) if len(d) else 0.0)
        checked.append(cut)
    ok = (max_diff <= max(tol * 10, 1e-3)) and (frac_bad <= 0.02)
    return dict(ok=bool(ok), max_diff=round(max_diff, 6), frac_bad=round(frac_bad, 4),
                checked_at=checked)


# convenient causal helpers re-exported (same as blind/altlib)
simple_returns = al.simple_returns
log_returns = al.log_returns
ema = al.ema; sma = al.sma; rolling_std = al.rolling_std; zscore = al.zscore
rsi = al.rsi; realized_vol = al.realized_vol; donchian = al.donchian; bbands = al.bbands
vol_target = al.vol_target

__all__ = ["aligned", "eval_book", "marginal", "causality_ok", "CAPITAL", "CAP_FRAC",
           "FEE_SIDE", "simple_returns", "log_returns", "ema", "sma", "rolling_std",
           "zscore", "rsi", "realized_vol", "donchian", "bbands", "vol_target"]
