"""Agent 48 — Multi-timescale agreement (family=mix, slug=multiscale). The angle (assigned): build a weekly-ish momentum by rolling aggregation up to i and combine it with a daily momentum, going long/short only when the timescales AGREE. Why agreement, not just averaging: a single horizon whipsaws when its window straddles a chop. By measuring momentum at DAILY (1-bar EMA slope), WEEKLY (~5-bar aggregated returns) and MONTHLY (~21-bar) timescales and requiring them to point the same way, we filter the rule down to the bars where the trend is coherent across scales. The position size = the (weighted) fraction of timescales that agree, so a unanimous up-vote is full size and a split vote is light/flat. A vol-target then makes the two curves risk- comparable and shrinks size into every vol spike (i.e. into every crash), turning the ~77-79% buy&hold drawdown into a ~0.23 one at comparable PnL. Multi-timescale construction (all causal, value at i uses rows <= i only): * DAILY momentum: sign of close vs a short EMA (fast trend state). * WEEKLY momentum: rolling aggregation — mean of the last WEEK_WIN daily log-returns (= ~WEEK_WIN/5 weeks of weekly drift) up to i. This is the "weekly-ish momentum by rolling aggregation up to i" the angle asks for. * MONTHLY momentum: sign of the past-MONTH_H-bar return (slow ~6-month macro trend). The three signs are combined with weights into a -1..+1 direction; the short side is zeroed (SHORT_W=0 -> long-flat) because both curves trend structurally up, so any short bleeds by shorting the dips — tuning on train, long-flat dominated every de-weighted short on sharpe_min (1.475 vs 1.45 at SHORT_W=0.3). CAUSAL: EMAs / rolling means / past-return signs all use data <= i; 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 (split='train' only, combined A&B). Coarse->fine sweep on the timescale set, weights, the short weight and the vol-target block; one-axis neighbor check confirms the cell is interior on a wide plateau (ema 6-10, wk 30-35, mo 110-126, tv 0.26-0.30, vw 30-35 all give sharpe_min 1.42-1.50). Chosen cell: DAILY_EMA=8, WEEK_WIN=35 (~7 weeks of daily drift), MONTH_H=126 weights (daily,weekly,monthly) = (0.15, 0.40, 0.45) SHORT_W=0.0 (long-flat), TARGET_VOL=0.28, VOL_WIN=35d, LEV_CAP=1.5 -> train combined: pnl_mean ~3.62, maxdd_worst ~0.23, sharpe_min ~1.48. """ import numpy as np import blindlib as bl # timescale set DAILY_EMA = 8 # daily-ish trend state (fast EMA) WEEK_WIN = 35 # rolling window of daily log-returns (~7 weeks of weekly drift) MONTH_H = 126 # ~6-month macro lookback (monthly-ish slow trend) # combination weights (sum ~1) — weekly + monthly carry the agreement W_DAILY = 0.15 W_WEEK = 0.40 W_MONTH = 0.45 SHORT_W = 0.0 # zero the short side (curves trend up) -> long-flat # sizing TARGET_VOL = 0.28 VOL_WIN_DAYS = 35 LEV_CAP = 1.5 def _daily_mom(c: np.ndarray) -> np.ndarray: """Sign of close vs a short EMA — the fast (daily) trend state, causal.""" e = bl.ema(c, DAILY_EMA) return np.sign(c / e - 1.0) def _weekly_mom(c: np.ndarray) -> np.ndarray: """Weekly-ish momentum by ROLLING AGGREGATION up to i (the assigned angle). Aggregate daily log-returns into the average drift over the last WEEK_WIN bars (~7 weeks), then take its sign. Causal: at bar i it only averages r[i-W+1..i]. Vectorized via a prefix-sum so it is O(n).""" lr = bl.log_returns(c) # lr[i] = log(c[i]/c[i-1]), causal win = WEEK_WIN s = np.concatenate([[0.0], np.cumsum(lr)]) # prefix sums, s[k] = sum(lr[:k]) out = np.zeros(len(c)) idx = np.arange(len(c)) lo = np.maximum(0, idx - win + 1) full = idx >= (win - 1) # only emit once the full window exists means = (s[idx + 1] - s[lo]) / win out[full] = np.sign(means[full]) return out def _monthly_mom(c: np.ndarray) -> np.ndarray: """Sign of the past-MONTH_H-bar return — the slow macro trend, causal.""" out = np.zeros(len(c)) h = MONTH_H if h < len(c): out[h:] = np.sign(c[h:] / c[:-h] - 1.0) return out def signal(df): c = df["close"].values.astype(float) d = _daily_mom(c) w = _weekly_mom(c) m = _monthly_mom(c) # weighted multi-timescale agreement -> direction in [-1, +1] sig = W_DAILY * d + W_WEEK * w + W_MONTH * m # asymmetric long-short: keep longs full size, de-weight shorts raw = np.where(sig >= 0.0, sig, sig * 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)