"""Agent 04 — MACD (family=trend, slug=macd). The angle: MACD = EMA(fast) - EMA(slow); signal line = EMA(MACD, signal_span); histogram = MACD - signal. Direction comes from the histogram SIGN reinforced by its SLOPE, exactly as the angle prescribes. Concretely: * BASE direction = +1/-1 only when the histogram sign AGREES with the MACD-line sign (MACD above its signal line AND above zero -> uptrend), else flat. Requiring agreement kills the histogram-sign whipsaw that bleeds the naive 12/26/9 to fees (turnover ~24/yr -> ~15/yr) and roughly halves the drawdown. * SLOPE confirmation = sign of the histogram's backward diff (histogram rising = momentum accelerating). Blended in at weight SLOPE_W; it trims the drawdown further (~0.18 -> ~0.12) by stepping aside while momentum is decelerating. Refinements that survived a plateau check on split='train': * Both anonymized curves are persistently up-trending, so a symmetric short bleeds (it shorts the dips of a bull). We keep a genuine long/short MACD but size the SHORT side down (SHORT_W=0.5). * Direction is vol-targeted (causal trailing window) so the two curves are sized comparably and the drawdown stays bounded. Tuning (train only) — broad plateau, chosen cell is the interior, not a grid edge: fast in [24..28], slow in [50..56], signal=9, SHORT_W in [0.5..0.6], SLOPE_W in [0.2..0.35], VOL_WIN in [20..60] all give sharpe_min ~1.35-1.45 at DD ~0.10-0.13. Picked fast=26, slow=52, signal=9, SHORT_W=0.5, SLOPE_W=0.20. Fee-robust: sharpe_min only 1.40 -> 1.29 as round-trip fee goes 0.10% -> 0.30%. Benchmark: long-only buy&hold on train is pnl ~6.7/23.0 but maxDD ~0.77/0.79 (sharpe ~0.89/1.16). This MACD anticipates the trend at a MUCH smaller drawdown (~0.12) with a higher risk-adjusted return (sharpe_min ~1.40). CAUSAL: ema(c, span) is an online recursion (value at i uses rows 0..i only); the histogram slope is a backward diff; vol_target uses a trailing vol window. No look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0). """ import numpy as np import blindlib as bl # --- tuned ONLY on split='train' (plateau interior) --- FAST_SPAN = 26 SLOW_SPAN = 52 SIGNAL_SPAN = 9 SLOPE_W = 0.20 # weight of histogram-slope confirmation in the direction SHORT_W = 0.5 # short side sized down (asymmetric L/S in a bull); 0 -> long-flat TARGET_VOL = 0.20 VOL_WIN = 30 LEV_CAP = 1.0 def _macd(c, fast, slow, sig): macd = bl.ema(c, fast) - bl.ema(c, slow) signal_line = bl.ema(macd, sig) hist = macd - signal_line return macd, signal_line, hist def signal(df): c = df["close"].values.astype(float) macd, signal_line, hist = _macd(c, FAST_SPAN, SLOW_SPAN, SIGNAL_SPAN) # base direction: take a side only when the histogram sign and the MACD-line # sign AGREE (MACD vs signal AND MACD vs zero point the same way), else flat. base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0) # slope confirmation: is the histogram rising or falling (causal backward diff)? slope = np.sign(np.diff(hist, prepend=hist[0])) raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope raw = np.clip(raw, -1.0, 1.0) # de-weight the short side (persistent up-trend -> symmetric short is drag) raw = np.where(raw < 0, raw * SHORT_W, raw) raw = np.nan_to_num(raw, nan=0.0) 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)