"""Agent 19 — Vol-targeted long-only / risk-parity single asset
(family=vol, slug=voltarget_lo).

The angle (assigned): NO direction call. Hold the asset LONG at all times, but size
the position by INVERSE realized volatility so the book runs at a roughly constant
target volatility:  exposure[i] = clip( target_vol / realized_vol[i] , 0, cap ).

Why this anticipates anything at all, despite never predicting direction: realized
vol is PERSISTENT (today's vol forecasts tomorrow's vol far better than today's return
forecasts tomorrow's return). The big declines on these two curves are also the high-
vol regimes — a crash is a vol spike. So scaling exposure DOWN when trailing vol is
high mechanically pulls the book light right when the worst legs happen, and levers UP
in the calm grind higher. The result on a structurally up-trending curve is a long-only
book with most of buy&hold's upside but a much smaller drawdown (the risk-parity / "vol
control" effect), at modest turnover (the weight only drifts with the vol forecast).

CAUSAL: realized_vol[i] uses returns over a trailing window ending at i (rows <= i);
the position is then shifted by the evaluator (held during bar i+1). No direction is
derived from any future bar; no global fit. Verified by causality_ok (max_diff 0.0).

Tuning (split='train' only, combined A&B). The free knobs are the trailing vol window,
the target vol, and the leverage cap.
  * CAP is the single most important choice. Because both curves trend up hard, a high
    cap just re-levers into buy&hold and brings the drawdown right back. cap=1.0 (never
    more than fully invested) is what preserves the risk-parity de-risking benefit; with
    a vol-driven weight that almost always sits below 1.0 this is the whole point.
  * VOL_WIN is the vol-forecast horizon. A SLOW window (~120d) gives a stabler vol
    estimate, less whipsaw, lower turnover and the BEST risk-adjusted result here:
    sharpe_min climbs from ~0.85 (30d) to ~0.97 (120d) and the plateau (110..200d) is
    flat at sharpe 0.91..0.99 / DD ~0.42-0.44 -> 120 is a robust interior pick.
  * TARGET_VOL is a pure DD/PnL dial: it scales exposure up and down but (for a long-
    only inverse-vol book) leaves the Sharpe essentially flat (0.971 across 0.24..0.32).
    So it is chosen for the DD/PnL trade-off, not the Sharpe.
Chosen cell, interior on every axis:
  TARGET_VOL = 0.28   # DD/PnL dial; Sharpe flat across 0.24..0.32 -> balanced cell
  VOL_WIN_D  = 120    # slow, stable vol forecast; plateau 110..200d
  LEV_CAP    = 1.0    # never lever past fully-invested -> keeps the DD-cut benefit
  -> train combined: pnl_mean ~2.93, maxdd_worst ~0.43, sharpe_min ~0.97.
This is a DEFENSIVE long-only book, NOT alpha. Its honest value is the drawdown: ~0.43
vs ~0.77-0.79 buy&hold at comparable PnL. Because it never shorts, its Sharpe ceiling
(~1.0) is set by the absence of any direction call -> it can avoid sizing into the big
declines but cannot profit from them. That is the inherent limit of this angle.
"""
import numpy as np
import blindlib as bl

TARGET_VOL = 0.28
VOL_WIN_D = 120
LEV_CAP = 1.0


def signal(df):
    # direction = always long (+1), NO direction call. Sizing is pure inverse-vol.
    direction = np.ones(len(df))
    pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
                        vol_win_days=VOL_WIN_D, leverage_cap=LEV_CAP)
    # long-only risk-parity: clip to [0, cap] (no shorts by construction)
    return np.clip(np.nan_to_num(pos, nan=0.0), 0.0, LEV_CAP)
