"""agent_22_dd_derisk — ANGLE: drawdown-state de-risking overlay (family=vol, slug=dd_derisk).

Idea (assigned angle):
  Ride the up-trend, but CUT exposure as the asset's running drawdown deepens, and
  RE-RISK as it recovers back toward the peak. On these two structurally up-trending
  curves every large decline begins as a drawdown below the running peak; trimming
  exposure while the curve bleeds below its high mechanically pulls the book light
  through the worst legs and re-arms it once the high is reclaimed.

  Construction (all causal / online):
    * dd[i] = close[i] / running_peak(close[0..i]) - 1   in (-1, 0]  -> the LIVE drawdown.
    * |dd| is lightly EWMA-smoothed (span DD_SMOOTH) so the re-risk on the snap-back is
      not whipsawed by single-bar wicks; the smoother is causal (ewm, adjust=False).
    * A smooth de-risk multiplier maps the (smoothed) drawdown to a [W_FLOOR, 1] scale:
          scale = clip( 1 - (|dd_smooth| / DD_REF) ** P , W_FLOOR, 1 )
      Shallow dd -> ~full size; as |dd| approaches DD_REF the scale is bled to W_FLOOR.
      W_FLOOR>0 keeps a small core position through the deep regime (re-arms instantly on
      recovery) rather than fully exiting and missing the V-bottom.
    * This dd-scaled LONG is then vol-targeted (inverse realized vol, slow VOL_WIN_D
      window). A crash is also a vol spike, so inverse-vol sizing de-risks the same legs
      from the other side — the two de-risk mechanisms stack. Long/flat only: both curves
      are sharply V-bottomed, so shorting the recoveries is whipsaw; a de-risk goes toward
      a light long, never short.

  Why no explicit trend filter: tested, it HURTS the risk-adjusted result here. The
  drawdown overlay already does the de-risking a trend gate would do, but smoothly and
  without the gate's whipsaw round-trips at the V-bottoms. Pure dd-derisk + slow
  inverse-vol gives the better Sharpe.

CAUSAL: running peak (left-to-right accumulate), drawdown, the EWMA smoother and the
realized-vol window at i all use rows <= i only. The evaluator shifts the position (held
during bar i+1). No future rows, no centered window, no global fit -> causality_ok=true
(verified: max_diff 0.0).

Tuning (split='train' only, A & B equal weight; buy&hold ref: A Sh0.89/DD0.77,
B Sh1.16/DD0.79). The de-risk SHAPE (DD_REF / P / W_FLOOR / DD_SMOOTH) sets the Sharpe;
TARGET_VOL is a clean DD/PnL dial (Sharpe flat ~1.10-1.14 across 0.25..0.50). Chosen cell
is interior on every axis with a flat plateau (Sharpe 1.08..1.15, DD 0.19..0.24):
    DD_REF=0.20  P=1.0  W_FLOOR=0.20  DD_SMOOTH=4  VOL_WIN_D=120  TARGET_VOL=0.40
  -> train combined: pnl_mean ~1.63, maxdd_worst ~0.22, sharpe_min ~1.14.
Honest read: this is a DEFENSIVE long-only book, not alpha. Its value is the DRAWDOWN —
~0.22 vs ~0.77-0.79 buy&hold (a ~3.5x cut) at comparable risk-adjusted PnL. Because it
never shorts, its Sharpe ceiling (~1.1-1.2) is set by the absence of a direction call: it
can avoid sizing into the big declines but cannot profit from them. That is the inherent
limit of the de-risk-overlay angle on these curves.
"""
import numpy as np
import pandas as pd
import blindlib as bl

DD_REF = 0.20       # drawdown (fraction) at which the de-risk multiplier hits the floor
P = 1.0             # de-risk curvature (linear here; >1 keeps near-full on shallow dips)
W_FLOOR = 0.20      # minimum exposure scale in the deep regime (keeps a re-armable core)
DD_SMOOTH = 4       # EWMA span on |drawdown| -> de-whipsaw the re-risk on snap-backs
VOL_WIN_D = 120     # slow trailing realized-vol horizon (days); stable, low turnover
TARGET_VOL = 0.40   # DD/PnL dial; Sharpe flat across 0.25..0.50 -> picked for PnL/DD balance
LEV_CAP = 1.0       # long-only, never lever past fully invested -> preserves the DD cut


def _drawdown_scale(c: np.ndarray) -> np.ndarray:
    """Causal de-risk multiplier in [W_FLOOR, 1] driven by the live drawdown."""
    peak = np.maximum.accumulate(c)             # running peak over rows <= i (causal)
    dd = c / peak - 1.0                         # (-1, 0]
    ad = np.abs(dd)
    ad = pd.Series(ad).ewm(span=DD_SMOOTH, adjust=False).mean().values  # causal smoother
    depth = ad / DD_REF
    return np.clip(1.0 - depth ** P, W_FLOOR, 1.0)


def signal(df):
    c = df["close"].values.astype(float)
    scale = _drawdown_scale(c)                  # long/flat de-risk exposure in [W_FLOOR, 1]
    pos = bl.vol_target(scale, df, target_vol=TARGET_VOL,
                        vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
    return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
