"""agent_45_pvt — Price-Volume momentum: volume-surge-confirmed breakouts.

ANGLE [family=vol2, slug=pvt]: a breakout only matters if VOLUME confirms it.
Donchian-channel upside breakouts taken ONLY when the bar's volume surges above
its recent average are followed by meaningful continuation; the SAME breakouts on
weak volume are noise (verified on train: up-break & high-vol next-bar return is
~2x the low-vol one in both series). Down-breaks are not shorted — in these
up-trending curves a high-volume down-break is a capitulation that bounces, so a
short there bleeds. We therefore go LONG/FLAT on volume-confirmed up-breakouts.

Rule (fully causal, online):
  * volume surge  : v[i] / SMA(v, 30) > 1.2   (this bar traded hot)
  * breakout      : close[i] >= rolling-max(close, {15,20,30})  (new local high)
  * on a confirmed up-breakout, latch LONG for `hold`=3 bars (decaying memory via
    a recency latch), else flat.
  * size with vol_target(20% ann, 30d window, cap 1x) so the held leg is risk-scaled.

Everything at bar i uses only data 0..i (rolling/cummax/SMA + a backward-only latch
loop) -> causality_ok passes.

Train (combined): pnl_mean ~1.24, maxdd_worst ~0.11, sharpe_min ~1.41 (A 1.41 / B 1.48).
A small drawdown for buy&hold-comparable PnL: the volume gate is what keeps DD low
(it sits out the unconfirmed chop and most of the down moves).
"""
import numpy as np
import pandas as pd
import blindlib as bl

# Tuned ONLY on split='train'. Plateau center; robust to don in 10..40, vwin 20..30.
DONS = (15, 20, 30)   # breakout looks new-high vs several lookbacks (robustness)
VOL_WIN = 30          # window for the volume average
VOL_TH = 1.2          # volume must exceed 1.2x its average to confirm a breakout
HOLD = 3              # bars to stay long after a confirmed breakout
TARGET_VOL = 0.20
VOL_WIN_DAYS = 30
LEV_CAP = 1.0


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

    # --- volume surge (causal): today's volume vs its trailing average ---
    vma = pd.Series(v).rolling(VOL_WIN, min_periods=5).mean().values
    vsurge = v / np.where(vma > 0, vma, np.nan)
    hivol = np.nan_to_num(vsurge, nan=0.0) > VOL_TH

    # --- breakout: new local high vs several donchian windows (causal) ---
    up_break = np.zeros(n, dtype=bool)
    for don in DONS:
        roll_hi = pd.Series(c).rolling(don, min_periods=2).max().values
        up_break |= (c >= roll_hi)

    # confirmed event = breakout AND volume confirms it
    event = up_break & hivol

    # --- latch LONG for HOLD bars after a confirmed event (backward-only) ---
    raw = np.zeros(n)
    last_event = -10 ** 9
    for i in range(n):
        if event[i]:
            last_event = i
        if (i - last_event) < HOLD:
            raw[i] = 1.0  # long/flat only

    # --- risk-scale the held leg ---
    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)
