"""Agent 02 — TSMOM multi-horizon (family=trend, slug=tsmom_multi).

The angle (assigned): time-series momentum over several lookback horizons. For each
horizon H in {~30, ~90, ~180} bars take the SIGN of the past-H-bar return (is the
asset up or down vs H bars ago?), average the three signs into a -1..+1 direction,
then size it with a causal vol-target so the two curves are risk-comparable and the
drawdown stays bounded.

Why multi-horizon: a single lookback is regime-fragile (whipsaws when its window
straddles a chop). Averaging 1/3/6-month TSMOM signs is the classic TP01 trick —
the slow horizon carries the macro trend, the fast ones cut exposure early into a
turn. On these two persistently up-trending curves the net effect is to stay long
through the bull and de-risk (toward flat / light short) into the big declines,
turning a ~77-79% buy&hold drawdown into a much smaller one at comparable PnL.

Long-short vs long-flat: a symmetric short bleeds in a structural bull (it shorts
the dips). Tuned on split='train', a lightly de-weighted short (SHORT_W<1) beats both
pure long-flat (misses the protection of going short the worst legs) and a symmetric
long-short (too much drag). SHORT_W=0.25 sits in the interior of a flat plateau.

CAUSAL: each horizon return uses close[i]/close[i-H] (rows <= i only); vol_target
uses a trailing realized-vol window. No look-ahead, no centered windows, no global
fit. Verified by causality_ok (max_diff 0.0).

Tuning (train only, combined A&B). A coarse->fine sweep found a WIDE plateau around
slow horizons ~ (1.5, 4.5, 8 months): the whole block H1 in [40..55], H2 in [120..130],
H3 = 240 gives sharpe_min 1.25..1.41 at DD 0.16..0.21. The chosen cell is interior on
every axis (all 8 H-neighbors, sw, vw within the plateau) -> robust, not a lucky spike:
  horizons = (45, 130, 240)  # ~1.5 / 4.5 / 8 months of daily bars
  SHORT_W  = 0.25            # asymmetric L/S; plateau sw in [0.0..0.5]
  TARGET_VOL=0.30, VOL_WIN=45d, LEV_CAP=1.5
  -> train combined: pnl_mean ~3.2, maxdd_worst ~0.21, sharpe_min ~1.37.
A single fast lookback (e.g. 30) is regime-fragile here; the slow multi-horizon blend
is what both lifts the Sharpe and roughly halves the buy&hold (~77-79%) drawdown.
"""
import numpy as np
import blindlib as bl

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


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


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

    # average the SIGN of TSMOM over the three horizons -> direction in [-1, +1]
    sig = np.zeros(len(c))
    for h in HORIZONS:
        sig += _tsmom_sign(c, h)
    sig /= len(HORIZONS)

    # asymmetric long-short: keep the long full size, de-weight the short side
    raw = np.where(sig >= 0.0, sig, sig * SHORT_W)

    # causal vol-targeting: shrinks 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)
