"""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)