"""agent_03_funding_clock_15m — FUNDING family, slug=funding_clock_15m (TF=15m).

ANGLE [family=funding]: perp funding settles at 00/08/16 UTC. The folklore is a
pre-funding DRIFT (positioning into the stamp) and a post-funding REVERSION (the crowd
that paid funding gets flushed). We mine that TIME structure on the certified 15m feed and
turn it into a LOW-TURNOVER tilt on the daily-ish TSMOM trend.

CONSTRUCTION (strictly causal, low-turnover):
  * BASE = TP01-style long-flat TSMOM (30/90/180d horizons) vol-targeted. This carries the
    real, slow trend (monthly flips) and is what gives a positive standalone Sharpe.
  * FUNDING TILT = a CAUSAL EXPANDING mean of past 15m returns bucketed by the funding-phase
    (hours since the last 00/08/16 stamp, h%8 in 0..7). We compute, per phase, the expanding
    average return using ONLY bars strictly before i. The tilt scales the base UP in phases
    that have been historically strong and DOWN (toward flat, never short) in weak phases.
  * The phase changes only on the hour boundary and the expanding bias evolves slowly, so the
    tilt is a smooth multiplier on an already-slow trend -> turnover stays bounded (~tens/yr).

FEE DISCIPLINE: a naive "flip in the pre-funding window, flip out after" churns ~2000x/yr =
fee-death (-8 Sharpe NET). We NEVER trade the window directly; the funding clock only
re-weights a slow trend by a slowly-moving causal bias, and we smooth the multiplier with an
EMA so it cannot oscillate bar-to-bar.

HONEST PRIOR (measured before coding): on the certified Deribit *index* price the pre/post-
funding 15m windows carry ~the same drift as every other bar (PRE 0.16 vs other 0.17 bps on
BTC) and the small pre-2025 edge FLIPS sign in the hold-out (BTC PRE 0.22 -> -0.12 bps; ETH
0.32 -> -0.27). Funding is a perp-vs-spot cashflow; the spot/index price has no robust
tradeable drift around the stamp net of the trend.

FINAL VERDICT (hardened judge, 15m): abs_grade=FAIL, marginal=NEUTRAL, earns_slot=FALSE.
  * turnover 56/yr (LOW, well under the 120 cap), fee survives (fee020 full Sharpe +0.74).
  * BUT corr to TP01 0.91 (full) / 0.94 (hold): the position IS the trend. The funding tilt
    is net-HARMFUL out-of-sample (uplift_hold -0.10, uplift_full -0.03) -> DILUTES, not ADDS.
  * abs FAIL is driven by ETH hold-out Sharpe -0.32 (the 15m TSMOM base itself is weak in the
    bull-quiet 2025-26 hold-out), NOT by the funding overlay.
  * Isolated tests confirm NO funding alpha: the pure market-neutral funding-phase signal is
    -7.5/-3.7 Sharpe NET (turnover ~3000/yr = fee-death) and even GROSS (fee=0) is incoherent
    (BTC -0.13, ETH +0.21 full); a slow daily funding long-flat overlay is buy&hold-in-disguise
    (TiM 91-93%, hold-out -0.54 both assets). A tilt_lo/EMA sweep never leaves NEUTRAL.
  CONCLUSION: the funding clock carries no orthogonal, fee-survivable, robust edge on the
  certified index feed. This angle does NOT earn a slot. Recorded as a clean negative.
"""
from __future__ import annotations

import sys

import numpy as np
import pandas as pd

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

# --- tunables (conservative for LOW turnover) -------------------------------
_HORIZONS_D = (30, 90, 180)     # TP01 trend horizons (days)
_VOL_TARGET = 0.20
_VOL_WIN_D = 30
_LEV_CAP = 2.0
_WARMUP_D = 180                 # need 180d of phase history before trusting the funding bias
_MIN_OBS = 400                  # need >=400 past obs of a phase before using it
_TILT_EMA_BARS = 96 * 7        # smooth the funding multiplier over ~7 days -> kills churn
_TILT_LO, _TILT_HI = 0.85, 1.0  # weak phases trimmed to 0.85, never boosted >1 (no leverage add)


def _tsmom_dir(c: np.ndarray, bpd: int) -> np.ndarray:
    """Long-flat TSMOM direction in {0,1} from multi-horizon sign agreement (causal)."""
    n = len(c)
    acc = np.zeros(n)
    cnt = np.zeros(n)
    for dd in _HORIZONS_D:
        h = dd * bpd
        if h >= n:
            continue
        s = np.full(n, np.nan)
        s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
        v = np.isfinite(s)
        acc[v] += s[v]
        cnt[v] += 1
    direction = np.zeros(n)
    nz = cnt > 0
    direction[nz] = acc[nz] / cnt[nz]
    return np.clip(np.sign(direction), 0.0, None)


def _causal_funding_bias(r: np.ndarray, phase: np.ndarray, bpd: int) -> np.ndarray:
    """Expanding mean 15m return per funding-phase bucket (h%8 = hours-since-last-stamp),
    strictly causal: bias[i] uses only returns at bars 0..i-1. Before warmup or before a
    phase has _MIN_OBS samples -> 0.0 (no opinion). The accumulator is updated with bar i-1
    BEFORE bias[i] is read, so there is no peeking."""
    n = len(r)
    bias = np.zeros(n)
    nbuck = 8
    psum = np.zeros(nbuck)
    pcnt = np.zeros(nbuck)
    warm = _WARMUP_D * bpd
    for i in range(1, n):
        b = phase[i - 1]
        psum[b] += r[i - 1]
        pcnt[b] += 1
        if i >= warm:
            cur = phase[i]
            if pcnt[cur] > _MIN_OBS:
                bias[i] = psum[cur] / pcnt[cur]
    return bias


def target(df: pd.DataFrame) -> np.ndarray:
    c = df["close"].values.astype(float)
    dt = pd.to_datetime(df["datetime"], utc=True)
    hour = dt.dt.hour.values
    bpd = al.bars_per_day(df)            # 96 at 15m

    # --- BASE: slow long-flat TSMOM trend, vol-targeted (TP01 canonical) -----
    base_dir = _tsmom_dir(c, bpd)
    base = al.vol_target(base_dir, df, _VOL_TARGET, _VOL_WIN_D, _LEV_CAP)

    # --- FUNDING CLOCK TILT (causal expanding phase bias) --------------------
    r = al.simple_returns(c)
    phase = (hour % 8).astype(int)       # 0 = funding bar, 1..7 = hours since the 00/08/16 stamp
    bias = _causal_funding_bias(r, phase, bpd)

    # Rank the current phase's bias against the causal cross-phase spread: a phase with a
    # below-typical expanding mean gets trimmed toward _TILT_LO; an average/strong phase
    # keeps full exposure. We map the bias to a [_TILT_LO, _TILT_HI] multiplier via a slow
    # sign rule on the demeaned bias, then EMA-smooth it so it moves over days, not bars.
    # Causal cross-phase mean = expanding mean of all returns (overall drift baseline).
    overall = np.zeros(len(c))
    csum = np.cumsum(r)
    idx = np.arange(len(c))
    overall[1:] = csum[:-1] / np.maximum(idx[1:], 1)   # expanding mean of r[0..i-1]
    weak = (bias < overall) & (bias != 0.0)            # phase historically below baseline drift
    tilt_raw = np.where(weak, _TILT_LO, _TILT_HI)
    tilt = al.ema(tilt_raw, _TILT_EMA_BARS)            # smooth -> low turnover

    pos = base * tilt
    return np.nan_to_num(pos, nan=0.0)


if __name__ == "__main__":
    for a in ("BTC", "ETH"):
        d = al.get(a, "15m")
        ev = al.eval_weights(d, target(d))
        print(a, "full", ev["full"]["sharpe"], "hold", ev["holdout"]["sharpe"],
              "turn/yr", ev["turnover_per_year"], "TiM", ev["time_in_market"])
