"""agent_04_intraday_range_size — VOL family, slug=intraday_range_size (TF 1h).

ANGLE (assigned): use the recent INTRADAY realized range/vol to SIZE a slow daily
directional position (risk-responsive sizing). ~daily turnover. Is it orthogonal to a
constant-vol-target trend?

THE ORTHOGONALITY PROBLEM. TP01 already vol-targets with close-to-close (c2c) 30d vol, so
"size a trend by c2c vol" is just TP01 -> REDUNDANT. The only way intraday range adds NEW
information is the part of the range that c2c vol does NOT see: the gap between how far price
TRAVELS inside the day (high-low / Parkinson range) and how far it NETS (close-to-close).

  variance ratio  VR = parkinson_vol / c2c_vol   (always > 1)
  - VR ~ low  : intraday path is efficient -> directional days -> TREND PAYS.
  - VR ~ high : price thrashes inside the bar and retraces -> choppy/reverting -> TREND BLEEDS
                (and the climax/whipsaw spikes that precede trend drawdowns light up here
                BEFORE c2c vol does, because the range expands intra-bar first).

So we keep TP01's c2c vol-target as the carrier and add ONE intraday-only knob: a causal,
expanding-standardized VR that DE-RISKS the trend in high-range/choppy regimes and lets it run
in efficient ones. This is risk-responsive sizing whose information is genuinely intraday
(Parkinson high-low vs c2c), i.e. orthogonal to a constant c2c vol-target.

TURNOVER. The VR multiplier is heavily smoothed (multi-day EMA) and the slow trend changes
~monthly, so the position drifts rather than flips: turnover stays ~50/yr (well under the
~120/yr cap, miles under the ~2000/yr fee-death of an hourly flip).

CAUSAL: VR at bar i uses Parkinson/c2c vol over a trailing window ending at i, standardized by
an EXPANDING mean/std (data <= i). No full-sample stats, no shift(-k). The evaluator holds
position[i] during bar i+1.

HONEST VERDICT (scored 2026-06-21): REDUNDANT. abs_grade PASS (standalone in-sample Sharpe
1.48, full ~1.07, hold ~0.35 both assets, fee-survivable to 0.20% RT, turnover ~41/yr -- all
green), but corr to TP01 = 0.965 -> earns_slot=false. The VR overlay genuinely improves the
STANDALONE hold-out (BTC 0.11->0.35) as risk management, yet the daily stream IS TP01 + a
small vol knob, so it adds ~0 at the margin (uplift_hold +0.03). Confirmed structurally by the
exploration: NO intraday-range sizing config breaks below corr ~0.90 while a c2c-trend carrier
is in the direction -- and every intraday-NATIVE direction tried (close-location-value
pressure, range-compression-long) decorrelates to corr ~0.5-0.76 but turns the hold-out
NEGATIVE (-0.5..-0.8) and DILUTES. So: intraday range carries usable RISK information (it
lifts the standalone hold-out and survives fees at ~daily turnover), but NOT marginal alpha vs
a TP01-led book -- a clean negative, consistent with the project's ~1.3 trend ceiling.
"""
from __future__ import annotations

import sys

import numpy as np
import pandas as pd

sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al  # noqa: E402

# --- carrier (TP01-style slow long-flat trend) ---
_HORIZONS_D = (30, 90, 180)
_VOL_TARGET = 0.20
_VOL_WIN_D = 30
_LEV_CAP = 2.0

# --- intraday range sizer ---
_PARK_WIN_D = 3        # trailing window (days) for Parkinson & c2c vol estimates
_VR_EXP_MIN_D = 60     # min days before the expanding standardization is trusted
_VR_SMOOTH_D = 5       # EMA smoothing of the VR multiplier (kills turnover)
_VR_GAIN = 0.50        # how hard the choppy regime de-risks
_SIZE_LO, _SIZE_HI = 0.4, 1.3


def _tsmom_long_flat(c: np.ndarray, bpd: int) -> np.ndarray:
    nbar = len(c)
    acc = np.zeros(nbar)
    cnt = np.zeros(nbar)
    for d in _HORIZONS_D:
        h = d * bpd
        if h >= nbar:
            continue
        s = np.full(nbar, np.nan)
        s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
        v = np.isfinite(s)
        acc[v] += s[v]
        cnt[v] += 1
    direction = np.zeros(nbar)
    nz = cnt > 0
    direction[nz] = acc[nz] / cnt[nz]
    return np.clip(direction, 0, None)              # long-flat


def _expanding_z(x: np.ndarray, min_obs: int) -> np.ndarray:
    """Strictly causal expanding-standardized z-score (mean/std over rows 0..i-1).

    Uses pandas expanding().shift(1) so bar i is standardized by stats that EXCLUDE i
    -> no peeking at the current bar. NaN until min_obs samples are available."""
    s = pd.Series(x)
    m = s.expanding(min_periods=min_obs).mean().shift(1)
    sd = s.expanding(min_periods=min_obs).std().shift(1)
    z = (s - m) / sd.replace(0, np.nan)
    return z.values


def target(df: pd.DataFrame) -> np.ndarray:
    c = df["close"].values.astype(float)
    hi = df["high"].values.astype(float)
    lo = df["low"].values.astype(float)
    bpd = al.bars_per_day(df)
    r = al.simple_returns(c)

    # --- carrier: slow long-flat TSMOM, c2c vol-targeted (this IS the TP01 leg) ----
    direction = _tsmom_long_flat(c, bpd)
    base = al.vol_target(direction, df, _VOL_TARGET, _VOL_WIN_D, _LEV_CAP)

    # --- intraday-only signal: Parkinson range vol vs close-to-close vol -----------
    w = _PARK_WIN_D * bpd
    park = (np.log(np.where((hi > 0) & (lo > 0), hi / lo, 1.0))) ** 2 / (4.0 * np.log(2.0))
    park_vol = np.sqrt(pd.Series(park).rolling(w, min_periods=w).mean().values)
    c2c_vol = pd.Series(r).rolling(w, min_periods=w).std().values
    vr = park_vol / np.where(c2c_vol > 0, c2c_vol, np.nan)   # always > 1; high = choppy

    # causal expanding standardization of the regime, smoothed to keep turnover low
    vrz = _expanding_z(vr, _VR_EXP_MIN_D * bpd)
    vrz = np.nan_to_num(vrz, nan=0.0)
    size = np.clip(1.0 - _VR_GAIN * np.tanh(vrz), _SIZE_LO, _SIZE_HI)
    size = al.ema(size, _VR_SMOOTH_D * bpd)

    pos = base * size
    return np.nan_to_num(pos, nan=0.0)


if __name__ == "__main__":
    for a in ("BTC", "ETH"):
        d = al.get(a, "1h")
        ev = al.eval_weights(d, target(d))
        print(a, "full", ev["full"]["sharpe"], "hold", ev["holdout"]["sharpe"],
              "turn/yr", ev["turnover_per_year"], "TiM", ev["time_in_market"])
