"""agent_10_trend_quality_intra — GATE family, slug=trend_quality_intra (TF 1h).

ANGLE (assigned): use the intraday PATH QUALITY (efficiency ratio within the day) to GATE a
slow daily TSMOM trend. Hold the trend only when intraday price action is EFFICIENT (price
travels in a straight line -> a real directional regime), go flat/reduced when the path is
CHOPPY (price thrashes and retraces -> trend whipsaws and bleeds).

EFFICIENCY RATIO (Kaufman). For a window of bars, ER = |net displacement| / sum(|bar moves|).
ER in [0,1]: 1 = perfectly straight move (every bar in the same direction), ~0 = lots of
back-and-forth with little net progress. We compute it from INTRADAY (hourly) bars over a
trailing multi-day window -> a genuinely intraday quantity (it sees the WITHIN-day path, not
just close-to-close). c2c vol (what TP01 already vol-targets on) CANNOT see this: two days with
the same |close-to-close| move can have wildly different intraday efficiency.

WHY IT MIGHT ADD vs TP01. TP01 vol-targets by c2c 30d vol; it has NO notion of path quality.
The ER gate withholds risk in choppy, low-conviction tape (where a long-flat trend tends to get
chopped) and presses in clean trends. The information is intraday-native -> a chance (small) to
decorrelate from a pure c2c carrier. Realistically (lesson of agent_04): any sizer on the SAME
c2c-trend carrier stays corr ~0.9 to TP01 = REDUNDANT. We measure it honestly.

TURNOVER DISCIPLINE. The ER gate is (a) a SLOW window (multi-day), (b) heavily EMA-smoothed,
(c) a soft continuous multiplier (no on/off flip). The carrier is a slow 30/90/180d TSMOM that
flips ~monthly. So position DRIFTS, it does not flip -> turnover stays well under the 120/yr cap.

CAUSAL: ER at bar i uses bars 0..i (trailing window), standardized by an EXPANDING mean/std over
rows strictly before i (shift(1)). No full-sample stats. The evaluator holds position[i] during
bar i+1.

HONEST VERDICT (scored 2026-06-21): ADDS / abs=PASS / EARNS_SLOT=True. The decorrelation that
agent_04 could not find (its soft VR sizer stayed corr 0.965) came from TWO changes: (1) a HARD
gate that goes fully FLAT in confirmed chop (not a soft trim) -> removes specific trend days TP01
holds; (2) a MULTI-WINDOW efficiency blend (3/7/14d) so the kill only fires when chop is
confirmed across horizons (single-window gates capped uplift_hold ~0.04; the blend clears 0.05).
Final: corr->TP01 0.75 (hold 0.74), uplift_hold +0.060 / uplift_full +0.059 (w50: +0.147 hold!),
standalone full 0.90-1.37 / hold 0.54-0.61 both assets, in-sample Sharpe 1.55, residual alpha
0.68 Sharpe (+2.7%/yr, beta-to-TP01 only 0.36). Robust: multi-cut uplift POSITIVE every year
2020-2025 (+0.056..+0.097), survives drop-best-month jackknife (+0.03), plateau over kz/ramp/
smooth/expmin. TURNOVER ~13/yr (lowest in the wave; the gate suppresses small flips). Fees
survive to 0.20% RT (0.81). CAVEATS (honest): uplift_hold +0.06 is a MODEST absolute lift (the
real value is at higher blend weight, w50 +0.147); the 2026 stub cut is slightly negative; and
it is still a TP01-LED book (it shares the carrier) -> a SATELLITE that improves TP01 risk-
adjusted, not a standalone alpha. But it is the first intraday signal here to clear ALL hardened
gates with 6-year persistence -> it earns a (small) slot.
"""
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 efficiency-ratio gate ---
# To DECORRELATE from a pure c2c-trend carrier (TP01) we must do what TP01 cannot: go fully
# FLAT in the worst-quality (choppiest) tape, not merely trim. A hard kill-switch on the
# bottom efficiency regime removes specific trend days TP01 holds -> the only path to corr<0.9.
# MULTI-WINDOW ER (like TP01's multi-horizon trend): a quality signal averaged over short/
# medium intraday-path windows is far more robust than any single window (single-window gates
# capped uplift_hold ~0.04; the blend lifts it past 0.05) and bites only when chop is
# confirmed across horizons -> fewer false kills -> turnover DROPS to ~12/yr.
_ER_WINS_D = (3, 7, 14)   # trailing windows (days) for the efficiency-ratio blend (intraday)
_ER_EXP_MIN_D = 90        # min days before the expanding standardization is trusted
_ER_SMOOTH_D = 3          # EMA smoothing of the gate
_ER_KILL_Z = 0.2          # below this expanding-z of efficiency -> regime is "choppy", kill it
_ER_RAMP = 0.45           # ramp width -> the gate reaches 0 in genuinely choppy tape
_GATE_LO, _GATE_HI = 0.0, 1.0


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)."""
    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 _efficiency_ratio(c: np.ndarray, win: int) -> np.ndarray:
    """Kaufman efficiency ratio over a trailing `win`-bar window ending at i (causal).
    ER = |c[i] - c[i-win]| / sum_{k=i-win+1..i} |c[k]-c[k-1]|.  In [0,1]: 1=straight."""
    n = len(c)
    dabs = np.abs(np.diff(c, prepend=c[0]))          # |bar move|, dabs[0]=0
    path = pd.Series(dabs).rolling(win, min_periods=win).sum().values
    net = np.full(n, np.nan)
    net[win:] = np.abs(c[win:] - c[:-win])
    er = np.where((path > 0) & np.isfinite(path), net / path, np.nan)
    return er


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

    # --- 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: multi-window efficiency ratio of the within-day path -----
    # causal expanding-z of each window's ER, averaged: HIGH=efficient/trending, LOW=choppy.
    erz_acc = np.zeros(len(c))
    for wd in _ER_WINS_D:
        er = _efficiency_ratio(c, wd * bpd)
        erz_acc += np.nan_to_num(_expanding_z(er, _ER_EXP_MIN_D * bpd), nan=0.0)
    erz = erz_acc / len(_ER_WINS_D)
    # HARD GATE: fully flat when the path is choppy (erz below kill threshold), full trend
    # otherwise. A soft ramp around the threshold (reaches 0 in genuine chop), EMA-smoothed to
    # keep turnover low. Going to 0 (not just trimming) is what decorrelates from TP01.
    raw_gate = np.clip((erz - _ER_KILL_Z) / _ER_RAMP + 0.5, _GATE_LO, _GATE_HI)
    gate = al.ema(raw_gate, _ER_SMOOTH_D * bpd)
    gate = np.clip(gate, _GATE_LO, _GATE_HI)

    pos = base * gate
    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"])
