"""agent_46_vol_div — Volume/price divergence (family=vol2, slug=vol_div).

ANGLE: fade moves where volume does NOT confirm; ride where it does.

How the angle is expressed (all causal, decided at close[i], held over bar i+1):

  * CONFIRMATION = is volume EXPANDING as the trend develops?  We compare a short
    volume mean (5) to a longer one (20): `confirm = v5/v20 - 1`.  When volume is
    rising while price trends, the move is volume-CONFIRMED.
      -> RIDE leg: take the multi-bar (15-bar) price momentum, but only with weight
         proportional to the confirmation (clip(confirm * gain, 0, 1)).  No
         confirmation -> no momentum bet.  This is "ride where volume confirms".

  * DIVERGENCE / EXHAUSTION = a single-bar thrust on a VOLUME SPIKE that is NOT part
    of a broader volume up-trend (volume not confirming the direction).  Such thrusts
    tend to mean-revert.
      -> FADE leg: -sign(last bar) gated by (a vol z-score spike) AND (volume NOT
         broadly expanding).  This is "fade where volume does not confirm".

  * The two legs are blended (0.7 ride / 0.3 fade) and vol-targeted so the drawdown
    stays bounded.  On the train view this is comparable PnL to buy&hold at a fraction
    of the drawdown, and it can go short / flat the unconfirmed declines.

Decomposition note (train): the RIDE leg is the real edge on both overlaid curves
(volume-confirmed momentum persists); the FADE leg is a small DD-reducing overlay.
Parameters chosen on a smooth plateau (rw 12-15, cl 15-20), not a knife-edge.
"""
import numpy as np
import pandas as pd
import blindlib as bl

RIDE_W = 15        # momentum horizon (bars)
CONF_S = 5         # short volume mean
CONF_L = 20        # long volume mean
GAIN = 6.5         # confirmation -> ride-weight gain
W_FADE = 0.30      # weight of the divergence/fade overlay
TARGET_VOL = 0.18  # annualized vol target for sizing
VOL_WIN = 30       # vol-target lookback (days)


def _zscore(x, win):
    s = pd.Series(x)
    m = s.rolling(win, min_periods=win // 2).mean()
    sd = s.rolling(win, min_periods=win // 2).std()
    z = (s - m) / sd.replace(0.0, np.nan)
    return np.nan_to_num(z.values)


def signal(df):
    c = df["close"].values.astype(float)
    v = df["volume"].values.astype(float)
    logc = np.log(c)
    r = np.concatenate([[0.0], np.diff(logc)])             # causal bar return

    # ---- Volume confirmation: short vol mean vs long vol mean (>0 = expanding) ----
    vshort = pd.Series(v).rolling(CONF_S, min_periods=2).mean().values
    vlong = pd.Series(v).rolling(CONF_L, min_periods=10).mean().values
    confirm = np.nan_to_num(vshort / np.where(vlong > 0, vlong, np.nan), nan=1.0) - 1.0

    # ---- RIDE leg: multi-bar momentum, weighted by how strongly volume confirms ----
    pm = np.concatenate([np.zeros(RIDE_W), logc[RIDE_W:] - logc[:-RIDE_W]])
    ride = np.sign(pm) * np.clip(confirm * GAIN, 0.0, 1.0)

    # ---- FADE leg: fade a single-bar thrust on a volume spike w/o broad expansion ----
    vol_spike = _zscore(v, 20)
    fade_gate = np.clip(vol_spike - 1.0, 0.0, 2.0) * np.clip(-confirm * 4.0 + 0.5, 0.0, 1.0)
    fade = -np.sign(r) * np.clip(fade_gate, 0.0, 1.0)

    raw = np.clip((1.0 - W_FADE) * ride + W_FADE * fade, -1.0, 1.0)

    pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN, leverage_cap=1.0)
    return np.clip(np.nan_to_num(pos), -1.0, 1.0)
