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