"""Agent 28 — Williams %R momentum/reversion HYBRID, trend-gated (family=osc, slug=willr).

The angle (assigned): Williams %R momentum/reversion hybrid with a trend gate. Williams %R
is the inverse of the Stochastic %K: %R = -100 * (HH - close) / (HH - LL) over a trailing
window, ranging -100 (close at the window LOW = oversold) .. 0 (close at the window HIGH =
overbought). It measures where the close sits in its own rolling high/low channel, so it is
self-normalizing and sweeps the FULL -100..0 range even inside a bull (measured on train:
%R<-80 ~14% of bars, %R>-20 ~26% of bars). That dual occupancy is what makes a HYBRID
(reversion on one leg + momentum on the other) genuinely playable here.

Reading the train curves first (both A and B, split='train'): they trend UP very hard
(A 100->792, B 100->2400). A pure symmetric reversion ("short every %R>-20") would just
short the bull and bleed; a pure momentum rule rides crashes. The HYBRID + trend gate
resolves this by using %R DIFFERENTLY on each side of a long trend filter:

  REVERSION LEG (in an UPTREND, close above a long SMA):
    %R dipping into oversold (< OS, e.g. -80) is a BUY-THE-DIP setup. To ANTICIPATE the
    bounce instead of knife-catching a still-falling close, we require %R to TURN BACK UP
    (cross up through a short signal line = SMA of %R, the standard stochastic-style
    trigger). We then HOLD the long (hysteresis) until %R recovers past EXIT, then flat.
    This is the reversion half of the hybrid.

  MOMENTUM LEG (in an UPTREND): once %R pushes into and STAYS overbought (> OB, e.g. -20),
    in a hard bull that is NOT a fade signal — overbought persists and the trend runs. So
    instead of shorting it (textbook reversion) we take a SMALLER continuation LONG
    (MOM_W). This is the momentum half of the hybrid: %R>-20 in an uptrend = "trend is
    strong, stay with it", the opposite trade to what reversion alone would do. This is
    the key difference from the pure-reversion stochastic/RSI agents.

  DOWNTREND (close below the long SMA): the symmetry returns and %R is read as reversion
    again — %R overbought (> OB) with a cross DOWN through its signal line is a reversion
    SHORT (rips fade). %R oversold we stand flat (don't knife-catch long under a
    downtrend). The short side is down-weighted (SHORT_W) because the drift is up; on
    train it is marginal (see HONEST NOTE).

So the gate does three jobs: (1) picks the reversion side (dip-long in up, rip-short in
down), (2) flips the overbought reading from "fade" to "ride" inside the bull (the hybrid),
(3) suppresses the side that fights the drift. Sizing is smooth (deeper extreme -> bigger
appetite, floored at BASE while holding) then VOL-TARGETED so the two curves are
risk-comparable and exposure shrinks into vol spikes (crashes are vol spikes) — that is
what bounds the drawdown. The leverage cap rarely binds, so the edge is NOT leverage.

HONEST NOTE (negative findings kept): (1) The downtrend reversion-short is nearly free but
adds little on train; kept small to honor the bidirectional angle. (2) The momentum
continuation leg (MOM_W) is what distinguishes this from a pure-reversion oscillator — in a
market that trends this hard it earns by riding the overbought regime instead of fading it,
but it ALSO partly degenerates toward trend participation (the honest ceiling for any
direction-on-a-bull rule). The genuine oscillator content is the cross-timed dip entry +
overbought exit cycle plus the DD control from the trend gate + vol-target. (3) A pure
always-on %R weighting (no flat state) degenerated into buy-and-hold (DD blew out); the
hysteresis flat state is what keeps DD modest. Result: an honest, modest combined train
Sharpe at a small DD — a fraction of buy&hold PnL but several-x less drawdown (it
anticipates the dip / rides the strong trend rather than holding through every crash).

CAUSAL: %R uses trailing rolling max(high)/min(low) (<= i); its signal line is a trailing
SMA of %R; the cross compares (%R - sig) at i vs i-1 (past only); the hold-state is a
forward cumulative pass over PAST bars only; the SMA trend filter and vol_target use
trailing data. No shift(-k), no centered windows, no global fit. Verified by causality_ok.

Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell sits
on a broad plateau (OB in [-35..-25], MOM_W in [0.3..0.5], SIG_WIN=5, R_WIN in [20..28],
EXIT in [-50..-40], OS=-80, BASE/TVOL/VWD all hold sharpe_min ~1.1..1.29 at DD ~3.3..5.6% —
a plateau, not a spike; SHORT_W is nearly free / marginal):
  R_WIN=20, SIG_WIN=5, OS=-80, OB=-35, EXIT=-45, TREND_WIN=150
  MOM_W=0.4, SHORT_W=0.4, BASE=0.6, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5
  -> train combined: pnl_mean ~0.46, maxdd_worst ~0.045, sharpe_min ~1.22
"""
import numpy as np
import pandas as pd
import blindlib as bl

R_WIN = 20          # %R lookback (rolling high/low window). 20 > textbook 14 for these trends.
SIG_WIN = 5         # signal line = SMA(%R, SIG_WIN): the line %R crosses (stochastic-style trigger).
OS = -80.0          # oversold: %R below this in an uptrend + cross-up = dip-long entry.
OB = -35.0          # overbought: momentum-ride (uptrend) / reversion-short (downtrend) threshold.
EXIT = -45.0        # dip-long HELD until %R recovers past EXIT (hysteresis entry/exit pair).
TREND_WIN = 150     # long SMA: above = uptrend (dips=long, OB=ride), below = downtrend (OB=short).
MOM_W = 0.4         # weight on the uptrend overbought MOMENTUM-continuation long (the hybrid half).
SHORT_W = 0.4       # weight on the downtrend reversion-short; marginal (see HONEST NOTE).
BASE = 0.6          # base long size while holding a dip (scaled up if %R still oversold).
TARGET_VOL = 0.25
VOL_WIN_DAYS = 35
LEV_CAP = 1.5


def _willr(df, r_win, sig_win):
    """Causal Williams %R + its signal line. %R[i] = -100*(HH-close)/(HH-LL) over the
    trailing r_win bars (<= i); sig[i] = SMA(%R, sig_win) (trailing). No look-ahead."""
    h = df["high"].values.astype(float)
    l = df["low"].values.astype(float)
    c = df["close"].values.astype(float)
    hh = pd.Series(h).rolling(r_win, min_periods=1).max().values
    ll = pd.Series(l).rolling(r_win, min_periods=1).min().values
    rng = hh - ll
    wr = np.where(rng > 1e-12, -100.0 * (hh - c) / rng, -50.0)
    sig = bl.sma(wr, sig_win)
    return wr, sig


def signal(df):
    c = df["close"].values.astype(float)
    n = len(c)
    wr, sig = _willr(df, R_WIN, SIG_WIN)
    trend_up = c > bl.sma(c, TREND_WIN)               # causal trailing SMA trend gate

    # --- %R / signal-line crosses (past-only: compares i vs i-1) ---
    ds = wr - sig
    ds_prev = np.concatenate(([0.0], ds[:-1]))
    cross_up = (ds > 0) & (ds_prev <= 0)              # %R turns up through its signal line
    cross_dn = (ds < 0) & (ds_prev >= 0)              # %R turns down through its signal line

    # --- smooth appetites (further past the extreme -> bigger) ---
    # oversold depth: %R from OS down to -100 -> long appetite 0..1
    long_app = np.clip((OS - wr) / (100.0 + OS), 0.0, 1.0)
    # overbought depth: %R from OB up to 0 -> 0..1 (used by both momentum-long & rev-short)
    ob_app = np.clip((wr - OB) / (0.0 - OB), 0.0, 1.0)

    # --- trend-gated Williams %R momentum/reversion hybrid with hysteresis ---
    # Forward pass is PURE PAST-ONLY: state at bar i depends only on bars <= i.
    held = np.zeros(n)
    in_long = False
    for i in range(n):
        if in_long:
            # exit the held dip-long when trend breaks down OR %R has recovered past EXIT
            if (not trend_up[i]) or (wr[i] >= EXIT):
                in_long = False
        else:
            # enter a dip-long in an uptrend when %R is oversold AND turns up through its line
            if trend_up[i] and (wr[i] < OS) and cross_up[i]:
                in_long = True
        if in_long:
            held[i] = max(BASE, long_app[i])          # ride the recovery, bigger if still oversold
        elif trend_up[i]:
            # MOMENTUM half of the hybrid: overbought in an uptrend = ride the strong trend
            held[i] = MOM_W * ob_app[i]
        else:
            # downtrend reversion-short: overbought AND %R turning down through its line
            if (wr[i] > OB) and cross_dn[i]:
                held[i] = -SHORT_W * ob_app[i]
            else:
                held[i] = 0.0

    pos = bl.vol_target(held, 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)
