"""agent_35_rls — Online recursive (EWMA-weighted) linear model of return on lagged returns.

ANGLE [family=ml, slug=rls]:
  Recursive Least Squares with exponential forgetting. At each bar we maintain a linear
  predictor  r_hat[t+1] = w . x[t]  where x[t] = [1, lagged log-returns ...].  After we
  observe the realized return we update (w, P) via the standard RLS recursion with a
  forgetting factor lambda (EWMA weighting of past samples). NO batch refit, NO peeking:
  the prediction for bar t+1 uses only weights estimated from data up to and including
  bar t.  Position = sign/strength of the predicted next return, vol-targeted.

Fully causal: the weight vector used to predict bar i+1 is updated only with the target
observed AT bar i (return from i-1 -> i), so no future leakage.
"""
import numpy as np
import blindlib as bl


def _rls_predict(r, n_lags=3, lam=0.985, delta=100.0, warmup=60):
    """Online RLS. Returns pred[t] = predicted return for the NEXT bar, decided at close t.

    r        : array of (log) returns, r[t] = return realized over bar t.
    n_lags   : number of lagged returns used as features.
    lam      : forgetting factor (EWMA). Closer to 1 = longer memory.
    delta    : ridge init for P = (delta) * I.
    warmup   : bars to accumulate before emitting a non-zero prediction.
    """
    T = len(r)
    p = n_lags + 1  # +1 for intercept
    w = np.zeros(p)
    P = np.eye(p) * delta
    pred = np.zeros(T)

    for t in range(T):
        # feature vector available AT close[t]: intercept + last n_lags returns ending at r[t]
        if t >= n_lags:
            x = np.empty(p)
            x[0] = 1.0
            # x[1] = r[t], x[2] = r[t-1], ... most recent first
            for k in range(n_lags):
                x[1 + k] = r[t - k]
            # PREDICT next-bar return from CURRENT weights (estimated from data <= t-1's target)
            pred[t] = float(w @ x) if t >= warmup else 0.0

            # --- RLS update using the target observed AT bar t (r[t]) with the feature
            #     vector that was available at close[t-1] (lags ending at r[t-1]) ---
            if t >= n_lags + 1:
                x_prev = np.empty(p)
                x_prev[0] = 1.0
                for k in range(n_lags):
                    x_prev[1 + k] = r[t - 1 - k]
                Px = P @ x_prev
                denom = lam + float(x_prev @ Px)
                g = Px / denom                       # Kalman gain
                err = r[t] - float(w @ x_prev)        # prediction error on realized target
                w = w + g * err
                P = (P - np.outer(g, Px)) / lam
    return pred


def signal(df):
    c = df["close"].values.astype(float)
    r = bl.log_returns(c)  # r[t] = log(c[t]/c[t-1]); r[0]=0, causal

    # Tuned on split='train' (both series). Fast forgetting (lam=0.97) makes the
    # predictor ADAPTIVE: it tracks a *local* return-on-lagged-returns relationship
    # rather than a stale long-run fit. lags=2 is the robust plateau (lags=2,
    # lam 0.95-0.97, smooth 3-8 all give shmin 0.35-0.44 at DD ~0.20-0.26).
    pred = _rls_predict(r, n_lags=2, lam=0.97, delta=100.0, warmup=120)

    # Smooth the raw prediction (short causal EWMA) to cut whipsaw turnover, then
    # normalize by a causal std of the prediction so the strength is regime-stable.
    ps = bl.ema(pred, 3)
    sd = bl.rolling_std(ps, 60)
    sd = np.where(sd > 1e-9, sd, 1e-9)
    raw = np.tanh(ps / sd)
    raw = np.clip(raw, -1.0, 1.0)

    # Vol-target the directional view -> comparable PnL to buy&hold at ~4x smaller DD.
    pos = bl.vol_target(raw, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
    return np.clip(pos, -1.0, 1.0)
