"""agent_44_obv — On-Balance-Volume trend confirmation [family=vol2, slug=obv].

Angle: cumulative signed volume (OBV) slope CONFIRMS price direction. OBV is the running
sum of sign(Δclose)*volume; when it trends up the buying volume is backing the advance
(accumulation) and the move is more likely to continue; when OBV rolls over relative to
its own EMA the advance is on thinning volume (distribution) and we de-risk / can flip.

Construction (all causal — value at i uses only rows 0..i):
  obv        = cumsum(sign(Δclose) * volume)
  obv_trend  = (obv - EMA(obv, 25)) / rolling_std(...)      # volume-flow z-score
  price_trend= (close/SMA(close,40) - 1) / rolling_std(...) # price z-score
  raw        = 0.35*tanh(k*obv_trend) + 0.65*tanh(k*price_trend)   # volume confirms price
  position   = vol_target(raw, target 20%)                  # bound drawdown, long/short

Why this weighting: on the train view the OBV flow z-score carries genuine, independently
positive next-bar correlation on BOTH overlaid curves, but the price trend is the stronger
single driver; OBV's role is to CONFIRM/temper it. A grid over (obv_win, price_win, blend,
gain, target_vol) shows a broad plateau around these values (Sharpe stable +/- one cell),
so the config is not a knife-edge fit. An explicit OBV-divergence damping gate was tested
and added nothing (the blend already absorbs divergences), so it was left out — simpler.
"""
import numpy as np
import blindlib as bl

# Tuned on split='train' only; chosen from the centre of a robustness plateau.
W_OBV = 25      # OBV-vs-EMA trend window
W_PRICE = 40    # price trend (close vs SMA) window
A_OBV = 0.35    # weight on the volume-flow leg (1 - A on the price leg)
GAIN = 0.9      # tanh gain on the z-scores
TARGET_VOL = 0.20
VOL_WIN = 40


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

    # --- On-Balance-Volume: causal cumulative signed volume ---
    dc = np.diff(c, prepend=c[0])
    obv = np.cumsum(np.sign(dc) * v)

    # OBV trend = OBV relative to its own EMA, z-scored by recent OBV-deviation std.
    obv_dev = obv - bl.ema(obv, W_OBV)
    obv_sc = bl.rolling_std(obv_dev, W_OBV)
    obv_sc = np.where(obv_sc > 1e-9, obv_sc, 1e-9)
    obv_sig = np.tanh(GAIN * (obv_dev / obv_sc))      # >0 accumulation, <0 distribution

    # Price trend = close vs SMA, z-scored.
    ptr = c / bl.sma(c, W_PRICE) - 1.0
    ptr_sc = bl.rolling_std(ptr, W_PRICE)
    ptr_sc = np.where(ptr_sc > 1e-9, ptr_sc, 1e-9)
    price_sig = np.tanh(GAIN * (ptr / ptr_sc))

    # Volume CONFIRMS price: blend the two legs into a -1..1 direction.
    raw = A_OBV * obv_sig + (1.0 - A_OBV) * price_sig
    raw = np.nan_to_num(raw, nan=0.0)

    # Vol-target to bound drawdown; long/short allowed.
    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, nan=0.0), -1.0, 1.0)
