"""Agent 39 — Efficiency-ratio / fractal GATE on a momentum signal (family=stat, slug=effratio).

THE ANGLE (assigned): take a plain momentum bet, but TRADE ONLY WHEN THE MOVE IS
"EFFICIENT". Efficiency = how straight the path is. We measure it with two
interchangeable causal fractal gauges and use them as an ON/OFF gate, NOT as an
adaptive average (that is the sibling KAMA angle). Here momentum decides DIRECTION
and the efficiency ratio decides WHETHER WE ARE ALLOWED TO TAKE THE TRADE.

EFFICIENCY GAUGES (both causal, both in [0,1], higher = straighter / more trending):
  * Kaufman Efficiency Ratio (ER): net displacement / total path length over n bars.
        ER[i] = |c[i]-c[i-n]| / sum_{k} |c[k]-c[k-1]|
    ER -> 1 a clean directional move, ER -> 0 a random-walk chop.
  * Fractal-dimension proxy (1 - normalized roughness): in chop the path's total
    length is many times its displacement (high fractal dimension ~2 = plane-filling);
    in a trend length ~ displacement (dimension ~1 = a line). We map this to an
    efficiency score E_fd in [0,1] = ER itself is the cleanest such proxy, so the
    primary gauge IS ER; we blend a SLOWER ER to require efficiency on two horizons.

DIRECTION (momentum): sign of a fast/slow EMA spread of price (a standard momentum
signal). This is the "plain momentum" the angle gates — not KAMA.

GATE: trade only when the (blended) efficiency ratio is above a CAUSAL expanding
quantile of its own history (the move is efficient ENOUGH for THIS curve right now).
In chop the gate is shut -> flat -> we skip the whipsaw that kills naked momentum.

LONG-SHORT: curves trend up structurally so a symmetric short bleeds (shorts the
dips). Keep the long full size, de-weight the short (SHORT_W) so the short only
protects the big EFFICIENT declines (a crash is a very efficient down-move -> the
gate is OPEN and momentum is down -> we are short exactly when it pays).

SIZING: causal vol_target so A and B are risk-comparable and every vol spike (= every
crash) auto-shrinks exposure -> the ~77-79% buy&hold drawdown collapses.

CAUSAL: EMA spread, ER (both horizons), the expanding-quantile gate, and vol_target
all use rows <= i only. No shift(-k), no centered window, no global fit. Verified by
causality_ok (max_diff ~0).
"""
import numpy as np
import pandas as pd
import blindlib as bl

# --- momentum (direction) ---  [tuned on train, wide plateau]
EMA_FAST = 10
EMA_SLOW = 50

# --- efficiency gate (the angle) ---
ER_WIN = 25          # fast efficiency-ratio lookback (~1 month daily)
ER_WIN2 = 60         # slow efficiency-ratio lookback (require efficiency on 2 horizons)
ER_BLEND = 0.5       # weight of the slow ER in the blended gauge
ER_Q = 0.33          # expanding-quantile gate: trade only when eff above its own history
WARMUP = 60          # min bars before the expanding gate is trusted

# --- exposure ---
SHORT_W = 0.25       # de-weight the short side (curves trend up); 0 -> long-flat
TARGET_VOL = 0.30
VOL_WIN_DAYS = 25
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]))
    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 _expanding_quantile(x: np.ndarray, q: float, warmup: int) -> np.ndarray:
    """Causal expanding quantile: thr[i] = q-quantile of x[0..i]. Impassable before warmup."""
    return pd.Series(x).expanding(min_periods=warmup).quantile(q).values


def signal(df):
    c = df["close"].values.astype(float)
    n = len(c)

    # DIRECTION: plain momentum = sign of fast-slow EMA spread
    ef = bl.ema(c, EMA_FAST)
    es = bl.ema(c, EMA_SLOW)
    direction = np.sign(ef - es)

    # EFFICIENCY GAUGE: blend a fast and a slow Kaufman efficiency ratio
    er_fast = _efficiency_ratio(c, ER_WIN)
    er_slow = _efficiency_ratio(c, ER_WIN2)
    eff = (1.0 - ER_BLEND) * er_fast + ER_BLEND * er_slow

    # GATE: only trade when efficiency is high relative to this curve's own past
    thr = _expanding_quantile(eff, ER_Q, WARMUP)
    active = np.where(np.isfinite(thr) & (eff >= thr), 1.0, 0.0)

    raw = direction * active
    raw = np.where(raw >= 0.0, raw, raw * SHORT_W)   # de-weight the short side

    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)
