"""Agent 40 — Return-skew regime gate on a trend signal (family=stat, slug=skewgate).

THE ANGLE (assigned): avoid fat-tail-DOWN regimes. A trend follower is happy to ride a
persistent up-move; the danger is the crash leg — a cluster of large negative returns that
shows up FIRST as a strongly NEGATIVELY-skewed recent return distribution (a few big down
days dominating). So we run a plain multi-horizon TSMOM trend as the base direction, then
GATE the LONG exposure DOWN — toward flat — whenever a causal rolling window of recent
returns turns negatively skewed.

WHAT THE DATA SAID (train diagnostics, both curves):
  * Conditioning forward 20-bar returns on rolling SKEW: the most negatively-skewed windows
    have materially WORSE forward returns than the most positively-skewed ones (e.g. Series B,
    40-bar skew: bottom-quartile fwd ~0.00 vs top-quartile ~+0.08). So a negative-skew gate
    has real, if modest, predictive value -> it earns its slot as a defensive overlay.
  * KURTOSIS, by contrast, is BULLISH on these curves (high-excess-kurt windows have BETTER
    forward returns — fat tails here come mostly from up-shocks in a structural bull). So a
    kurtosis "fat-tail" gate would throw away upside; it was tested and DROPPED. The gate is
    SKEW-ONLY. (This is the honest version of "avoid fat-tail-down": the down-tail signature
    on these curves is the SKEW, not the raw kurtosis.)

Construction (all causal, value at i uses only rows <= i):
  * BASE = multi-horizon TSMOM: average the SIGN of the past-H return for H in HORIZONS,
    direction in [-1, +1] (slow horizon = macro trend, fast ones cut early into a turn).
    Asymmetric long-short: de-weight the short side (curves trend up structurally).
  * GATE = rolling SKEW_WIN skewness of returns. A smooth multiplier on the LONG side only:
    1.0 when skew >= SKEW_CUT (benign), falling linearly to GATE_FLOOR as skew drops below
    the cut (fat-tail-down). Shorts are left untouched — being short into a negatively-skewed
    decline is exactly where the trend signal should earn, not be muzzled.
  * vol_target sizes the gated direction so the two curves are risk-comparable.

CAUSAL: rolling skew uses a trailing window (pandas .rolling, no shift(-k)); TSMOM uses
close[i]/close[i-H]; vol_target uses trailing realized vol. Verified by causality_ok
(max_diff 0.0).

TUNING (split='train' only, combined A&B). Sweep over (SKEW_WIN, SKEW_CUT, GATE_FLOOR)
found a plateau at SKEW_WIN in {35,40}, SKEW_CUT=-0.3, GATE_FLOOR=0: the gate lifts
sharpe_min from 1.37 (ungated base) to ~1.46 and pnl_mean from 3.22 to ~3.32. The chosen
cell (40, -0.3, 0.0) is interior on every axis. FINAL train combined:
  pnl_mean ~3.32, maxdd_worst ~0.21, sharpe_min ~1.46.

HONEST CAVEAT: the gate improves the RISK-ADJUSTED return (Sharpe) by trimming long size in
locally negative-skew clusters that precede pullbacks; it does NOT shrink the *worst* drawdown.
Inspection showed each curve's worst-DD leg is a slow whipsaw/chop where the position is
already small or short and skew is ~0 — i.e. NOT a fat-tail-down crash. So the angle's
defensive value here is Sharpe, not maxdd. A negative result on the maxdd front, reported
honestly.
"""
import numpy as np
import pandas as pd
import blindlib as bl

# --- trend base (multi-horizon TSMOM) ---
HORIZONS = (45, 130, 240)   # ~1.5 / 4.5 / 8 months of daily bars
SHORT_W = 0.25              # de-weight short side (curves trend up)
TARGET_VOL = 0.30
VOL_WIN_DAYS = 45
LEV_CAP = 1.5

# --- negative-skew (fat-tail-down) gate on the LONG side ---
SKEW_WIN = 40               # window for rolling return skew
SKEW_CUT = -0.3             # skew >= this = benign (gate 1.0); below = bite
GATE_FLOOR = 0.0            # min long multiplier when skew is deeply negative


def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
    """Sign of the past-h-bar return, causal. 0 for i < h."""
    out = np.zeros(len(c))
    if h < len(c):
        out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
    return out


def _neg_skew_gate(r: np.ndarray) -> np.ndarray:
    """Causal multiplier in [GATE_FLOOR, 1] for the LONG side. 1.0 when rolling skew is at
    or above SKEW_CUT; falls linearly to GATE_FLOOR as skew drops below the cut."""
    sk = pd.Series(r).rolling(SKEW_WIN, min_periods=SKEW_WIN).skew().values
    sk = np.nan_to_num(sk, nan=0.0)
    skew_bad = np.clip((SKEW_CUT - sk) / abs(SKEW_CUT), 0.0, 1.0)   # 0 benign -> 1 deeply neg
    gate = 1.0 - (1.0 - GATE_FLOOR) * skew_bad
    return gate


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

    # base trend direction (multi-horizon TSMOM, asymmetric long-short)
    sig = np.zeros(len(c))
    for h in HORIZONS:
        sig += _tsmom_sign(c, h)
    sig /= len(HORIZONS)
    raw = np.where(sig >= 0.0, sig, sig * SHORT_W)

    # negative-skew gate: shrink LONG risk only, leave shorts at full size
    gate = _neg_skew_gate(r)
    gated = np.where(raw > 0.0, raw * gate, raw)

    pos = bl.vol_target(gated, 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)
