"""Agent 06 — Acceleration / momentum-of-momentum (family=trend, slug=accel).

The angle (assigned): 2nd difference / momentum-of-momentum. Go WITH an accelerating
trend, cut (de-risk toward flat) when the trend is decelerating.

Construction (all causal):
  1. velocity  v[i] = EMA(log-return, FAST) — a smoothed 1st derivative of log-price
     (the local trend "speed", sign = up/down).
  2. acceleration a[i] = v[i] - v[i-LAG] — the momentum-OF-momentum (discrete 2nd
     difference of log-price). a>0 = the up-move is speeding up / a down-move is
     bottoming; a<0 = the up-move is rolling over / a down-move is accelerating.
  3. Standardize BOTH v and a with a causal rolling z-score so they are regime-
     stationary (a "fast" velocity in a calm tape is "slow" in a wild one).
  4. Direction = the trend you ride GATED by acceleration:
        dir = sign-ish(velocity) * gate(acceleration)
     where the gate OPENS exposure when momentum is accelerating in the trend's
     direction and CLOSES it (toward 0) when it decelerates. Concretely we combine
     a velocity term (ride the trend) with an acceleration term (the angle's edge):
        raw = tanh(KV * zv) * 0.5  +  tanh(KA * za) * 0.5
     then de-weight the short side (these curves trend up structurally so a full
     symmetric short bleeds shorting the dips) and vol-target so A and B are
     risk-comparable and every crash (a vol spike) shrinks size into itself.

Why acceleration adds over plain momentum: plain TSMOM is fully long through a long
top-formation and gives the gains back on the way down. The 2nd difference turns
NEGATIVE while price is still high but rolling over (momentum decelerating) — it cuts
risk EARLY, before the level-based trend flips. Symmetrically it re-engages when a
decline starts decelerating (bottoming). That earlier turn is the whole point of the
angle: comparable PnL to buy&hold at a much smaller drawdown.

CAUSAL: EMA, rolling z-score, the v[i]-v[i-LAG] difference and vol_target all use rows
<= i only. No shift(-k), no centered windows, no global fit. Verified by causality_ok.

Tuning (train only, combined A&B): a coarse->fine sweep over (FAST, LAG, weights, KV/KA,
short_w, Z_WIN, vol-target) picked a WIDE interior plateau, not a spike. The chosen cell
(FAST=28, LAG=30, Z_WIN=200, KV=KA=1.5, W_VEL=0.4/W_ACC=0.6, SHORT_W=0, vol25) is interior
on EVERY axis: FAST in [22..36] -> sh_min 1.50..1.52; LAG in [26..40] -> 1.41..1.52
(peak 30); Z_WIN in [160..220] -> 1.52..1.56; W_ACC/KA/KV/vol all smooth & monotone.
  -> train combined: pnl_mean ~2.3, maxdd_worst ~0.20, sharpe_min ~1.52.
SHORT_W=0 (long-flat) beat every short weight on train (sh_min collapses 1.31->0.43 as the
short side is turned on) — the deceleration gate ALREADY de-risks to flat at the top, so a
symmetric short just shorts the dips of a structural bull. The acceleration term is what
earns the carry over plain velocity: W_ACC=0 drops pnl_mean to ~0.6 (it ducks risk too
early); W_ACC~0.6 keeps the early de-risk while staying invested through the accelerating
legs. DD ~0.20 vs a ~77-79% buy&hold drawdown.
"""
import numpy as np
import blindlib as bl

FAST = 28          # EMA span for the velocity (smoothed log-return / local slope)
LAG = 30           # horizon of the 2nd difference: accel = v[i] - v[i-LAG]
Z_WIN = 200        # causal window to standardize velocity & acceleration
KV = 1.5           # tanh gain on the velocity z (ride the trend)
KA = 1.5           # tanh gain on the acceleration z (the angle's edge)
W_VEL = 0.4        # weight on the velocity (trend) term
W_ACC = 0.6        # weight on the acceleration (momentum-of-momentum) term
SHORT_W = 0.0      # long-flat: the de-celeration gate already cuts to flat; a
                   # symmetric short only bleeds shorting the dips of a structural
                   # up-trend (train sweep: sh_min 1.31@0.0 -> 0.43@1.0). 0 = flat.
TARGET_VOL = 0.27
VOL_WIN_DAYS = 25
LEV_CAP = 1.5


def _lagged_diff(x: np.ndarray, lag: int) -> np.ndarray:
    """Causal discrete derivative: out[i] = x[i] - x[i-lag], 0 for i < lag."""
    out = np.zeros(len(x))
    if lag < len(x):
        out[lag:] = x[lag:] - x[:-lag]
    return out


def signal(df):
    c = df["close"].values.astype(float)
    lr = np.zeros(len(c))
    lr[1:] = np.log(c[1:] / c[:-1])               # causal log returns

    # 1) velocity: smoothed 1st derivative of log-price (local trend speed)
    vel = bl.ema(lr, FAST)
    # 2) acceleration: momentum-of-momentum = 2nd difference of the trend
    acc = _lagged_diff(vel, LAG)

    # 3) standardize both vs their own trailing distribution (regime-stationary)
    zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
    za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)

    # 4) ride the trend, GATED/boosted by acceleration (the angle's edge)
    raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
    raw = np.clip(raw, -1.0, 1.0)

    # asymmetric long-short: full long, de-weighted short (structural up-trend)
    raw = np.where(raw >= 0.0, raw, raw * SHORT_W)

    # causal vol-targeting: shrink size into vol spikes (every crash is a vol spike)
    pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
                        vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
    return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
