"""agent_38_autocorr — Autocorrelation-sign ADAPTIVE momentum/reversion.

ANGLE [family=stat, slug=autocorr]:
  Measure the CAUSAL rolling lag-1 autocorrelation of recent returns. If returns are
  positively autocorrelated -> the move PERSISTS -> trade MOMENTUM (trend-follow). If
  negatively autocorrelated -> the move MEAN-REVERTS -> trade REVERSION (fade overshoot).
  The two legs are blended smoothly by w = tanh(k * autocorr): w>0 weights the trend
  leg, w<0 weights the reversion leg.

Why the legs are shaped the way they are (honest finding on TRAIN):
  Both series have strong positive drift and are negatively autocorrelated MOST of the
  time, so a naive symmetric reversion leg fights the trend and bleeds. So the reversion
  leg keeps a long/short BASE from the medium trend and only FADES short-term overshoot
  (z-score of recent returns) on top of that base — it de-risks, it doesn't fight drift.
  Final exposure is vol-targeted (20% annual, 30d window, no leverage) which is what
  actually crushes the drawdown (~30-40% raw -> ~6-8%).

CAUSAL: autocorr, MAs, z-scores and vol-target all use rows 0..i only. The rolling
lag-1 autocorr is a closed-form (rolling-sum) Pearson over the in-window (r[t], r[t-1])
pairs, so it is exact and online. Verified by bl.causality_ok.

Tuned ONLY on split='train'. Config aw=65, tw=50, k=4.0, rz=8 chosen for best COMBINED
min-Sharpe across A and B (shmin ~0.71, pnl ~0.23, maxdd ~0.08) — a robust plateau, not
a corner of the grid.
"""
import numpy as np
import pandas as pd
import blindlib as bl

# --- tuned on TRAIN only ---
AC_WIN = 65      # window for the rolling lag-1 autocorrelation (the regime detector)
TREND_WIN = 50   # MA window for the trend / base direction
REV_Z = 8        # window for the short-term overshoot z-score (reversion leg)
K = 4.0          # sharpness of the autocorr->blend map  w = tanh(K * ac)


def _roll_lag1_autocorr(r: np.ndarray, win: int) -> np.ndarray:
    """Causal rolling lag-1 autocorrelation of returns.

    At bar i, over the window covering r[i-win+1 .. i], correlate the in-window pairs
    (r[t], r[t-1]). Closed-form Pearson via rolling sums -> exact, online, O(n).
    Returns array len(r); value at i uses only r[0..i].
    """
    n = len(r)
    out = np.zeros(n)
    if n < 3:
        return out
    x = r[1:]            # r[t]
    y = r[:-1]           # r[t-1]
    m = win - 1          # number of pairs inside a full window
    if m < 2:
        return out

    def rsum(a):
        return pd.Series(a).rolling(m).sum().values

    sx = rsum(x); sy = rsum(y)
    sxy = rsum(x * y); sxx = rsum(x * x); syy = rsum(y * y)
    cov = sxy - sx * sy / m
    vx = sxx - sx * sx / m
    vy = syy - sy * sy / m
    den = np.sqrt(np.clip(vx * vy, 0.0, None))
    ac_pairs = np.where(den > 1e-12, cov / den, 0.0)
    out[1:] = np.nan_to_num(ac_pairs, nan=0.0)
    return np.nan_to_num(out, nan=0.0)


def signal(df):
    c = df["close"].values.astype(float)
    r = bl.simple_returns(c)

    # 1) regime detector: causal rolling lag-1 autocorrelation of returns
    ac = _roll_lag1_autocorr(r, AC_WIN)
    w = np.tanh(K * ac)                       # +1 = persist (momentum), -1 = revert

    # 2) MOMENTUM leg: follow the trend (long above the MA, short below)
    ma = bl.sma(c, TREND_WIN)
    rel = np.nan_to_num(c / ma - 1.0, nan=0.0)
    trend = np.tanh(3.0 * rel)

    # 3) REVERSION leg: keep the medium-trend BASE, fade only short-term overshoot
    #    (so it de-risks in a chop without shorting a persistent uptrend)
    zsh = np.nan_to_num(bl.zscore(r, REV_Z), nan=0.0)
    base = np.sign(rel)
    rev = np.clip(0.5 * base - 0.6 * np.tanh(0.8 * zsh), -1.0, 1.0)

    # 4) blend by autocorr sign, then vol-target to control drawdown
    wp = np.clip(w, 0.0, 1.0)
    wn = np.clip(-w, 0.0, 1.0)
    raw = wp * trend + wn * rev

    pos = bl.vol_target(raw, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
    return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
