"""Agent 07 — KAMA / Kaufman efficiency ratio (family=trend, slug=kama_eff). The angle (assigned): an ADAPTIVE moving average driven by Kaufman's Efficiency Ratio (ER). ER over a window of n bars is ER[i] = |close[i] - close[i-n]| / sum_{k=i-n+1..i} |close[k] - close[k-1]| i.e. net displacement / total path length, in [0, 1]. ER -> 1 when the move is a clean straight trend (worth following); ER -> 0 in chop (the path wanders, net displacement is small -> stay out). KAMA turns ER into an adaptive smoothing constant SC = (ER*(fast-slow)+slow)^2 so the average snaps to price in a trend and freezes in chop: KAMA[i] = KAMA[i-1] + SC[i] * (close[i] - KAMA[i-1]) DIRECTION: sign of the KAMA slope (KAMA[i] vs KAMA[i-k]) — KAMA is up-sloping in an up-trend, flat/down in a decline. GATE: the efficiency ratio itself. We only take a position when ER exceeds a causal, expanding-quantile threshold (trend is efficient ENOUGH right now relative to this curve's own history); otherwise flat. This is the literal statement of the angle: "trend-follow when efficiency high, flat when choppy". LONG-SHORT: the curves trend up structurally, so a full symmetric short bleeds (it shorts the dips). We keep the long full size and de-weight the short side (SHORT_W < 1) — the short is there to protect the big efficient DECLINES (which is where flat-only leaves the worst drawdown on the table), not to fade every wiggle. SIZING: causal vol-target so A and B are risk-comparable and the drawdown stays bounded (every crash is a vol spike -> exposure auto-shrinks). CAUSAL: ER, KAMA (a recursive EWMA-like filter), the slope, the expanding ER threshold, and vol_target all use rows <= i only. No shift(-k), no centered window, no global fit. Verified by causality_ok (max_diff ~0). Tuning (train only, combined A&B, coarse->fine). ER window ~ a month, KAMA fast/slow the canonical (2,30), slope over a few bars, ER gate at an expanding quantile. A WIDE interior plateau (every 1-axis neighbor holds sharpe_min 1.25-1.54 at dd 0.18-0.33, no spike) sits around: ER_WIN=30, FAST=2, SLOW=30, SLOPE=5, ER_Q=0.30 (expanding causal quantile), SHORT_W=0.20, TARGET_VOL=0.30, VOL_WIN=35d, LEV_CAP=1.5 -> train combined: pnl_mean ~4.75, maxdd_worst ~0.19, sharpe_min ~1.43 (causality.ok). Notes: LEV_CAP is non-binding here (vol_target keeps |pos|<1 on these vol levels); the ER gate is what de-risks chop, the de-weighted short protects the efficient declines, and vol_target turns the ~77-79% buy&hold drawdown into ~19%. """ import numpy as np import pandas as pd import blindlib as bl ER_WIN = 30 # efficiency-ratio lookback (~1 month of daily bars) FAST = 2 # KAMA fast EMA constant SLOW = 30 # KAMA slow EMA constant SLOPE = 5 # bars to measure KAMA slope (direction) ER_Q = 0.30 # expanding-quantile gate: trade only when ER above its own history WARMUP = 60 # min bars before the expanding gate is trusted SHORT_W = 0.20 # de-weight the short side (curves trend up); 0 -> long-flat TARGET_VOL = 0.30 VOL_WIN_DAYS = 35 LEV_CAP = 1.5 def _efficiency_ratio(c: np.ndarray, n: int) -> np.ndarray: """Kaufman efficiency ratio over n bars, causal. ER[i] uses close[i-n..i].""" change = np.zeros(len(c)) change[n:] = np.abs(c[n:] - c[:-n]) d = np.abs(np.diff(c, prepend=c[0])) # |close[k]-close[k-1]| volatility = pd.Series(d).rolling(n, min_periods=n).sum().values er = np.where(volatility > 0, change / volatility, 0.0) er[:n] = 0.0 return np.nan_to_num(er, nan=0.0) def _kama(c: np.ndarray, er: np.ndarray, fast: int, slow: int) -> np.ndarray: """Kaufman Adaptive Moving Average. SC = (ER*(fast_sc-slow_sc)+slow_sc)^2. Recursive (only uses past) -> fully causal.""" fast_sc = 2.0 / (fast + 1.0) slow_sc = 2.0 / (slow + 1.0) sc = (er * (fast_sc - slow_sc) + slow_sc) ** 2 kama = np.empty(len(c)) kama[0] = c[0] for i in range(1, len(c)): kama[i] = kama[i - 1] + sc[i] * (c[i] - kama[i - 1]) return kama def _expanding_quantile(x: np.ndarray, q: float, warmup: int) -> np.ndarray: """Causal expanding quantile: thr[i] = q-quantile of x[0..i]. For i= thr), 1.0, 0.0) raw = direction * active # asymmetric long-short: keep long full size, de-weight the short side raw = np.where(raw >= 0.0, raw, raw * SHORT_W) 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)