"""Agent 01 — Dual EMA crossover (family=trend, slug=ema_cross). The angle: long/short on the sign of (fast EMA - slow EMA). The two spans are the core tuned knobs. One refinement that survived a plateau check on split='train': the two anonymized curves are strongly up-trending, so a SYMMETRIC short is pure drag (it shorts the dips of a bull market). We keep the long/short crossover but size the SHORT side down by `SHORT_W` — still a genuine long/short EMA cross, just risk-asymmetric. Direction is then vol-targeted (causal trailing window) so the two curves are sized comparably and the drawdown stays bounded. Tuning (train only): a broad plateau f in [18..30], s in [40..50], SHORT_W in [0.1..0.3] all give sharpe_min ~1.3 / DD ~0.23. f=25, s=40, SHORT_W=0.25 sits in the plateau interior (not on a grid edge) -> robust, not a lucky cell. CAUSAL: ema(c, span) is an online recursion (value at i uses rows 0..i only); vol_target uses a trailing vol window. No look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0.0). """ import numpy as np import blindlib as bl # --- tuned ONLY on split='train' (plateau interior) --- FAST_SPAN = 25 SLOW_SPAN = 40 SHORT_W = 0.25 # short side sized down (asymmetric L/S); 0 -> long-flat TARGET_VOL = 0.20 VOL_WIN = 30 LEV_CAP = 1.0 def signal(df): c = df["close"].values.astype(float) fast = bl.ema(c, FAST_SPAN) slow = bl.ema(c, SLOW_SPAN) # +1 when fast above slow, -SHORT_W when below: genuine EMA-cross direction, # short side de-weighted because the curves are persistently up-trending. raw = np.where(fast >= slow, 1.0, -SHORT_W) pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN, leverage_cap=LEV_CAP) return np.clip(pos, -1.0, 1.0)