"""agent_15_intraday_meanrev_gated — REVERT family, slug=intraday_meanrev_gated (TF 1h).

ASSIGNED ANGLE: short-horizon mean reversion, but ONLY after a causal EXTREME (RSI / z beyond a
high threshold) AND only a few times/week. Keep turnover under ~120/yr. Fade gently.

WHAT THE 1h DATA ACTUALLY SAYS (measured, honest — see the probes in the diary). The naive
reading of this angle dies on the data, and it dies the SAME way the 15m version did (agent_06):
  * A 1h RSI EXTREME (rsi14 <= 15 or >= 85) is a MOMENTUM / breakout event, not a reversion
    event. Fading it has the WRONG SIGN: over the next 4-24 bars the price CONTINUES. The fade
    P&L is strongly NEGATIVE (BTC fade_mean -45..-110 bp, t=-2..-4 at the deep tail). The deeper
    the RSI threshold, the worse the fade. So "fade the RSI extreme" is a clean negative at 1h.
  * The only revert mechanic that exists at this horizon is the LEVEL OVERSHOOT (close far from a
    multi-day EMA, standardized by a causal sigma). But at 1h even THAT is weak and sign-unstable:
    across H in {8,12,24,48} and k in {1.5..2.5} the fade flips sign by horizon (BTC continues at
    H=12/48, mildly reverts at H=8/24; ETH mildly reverts), and the conditional t-stats are mostly
    < 2. The clean, strong tail-fade that earned a 15m slot (agent_06) does NOT reproduce at 1h:
    1h aggregates away the micro-overshoot that mean-reverts and leaves the macro-overshoot that
    trends (which is exactly TP01's domain, with the wrong sign for a fade).

So this agent implements the angle FAITHFULLY (gated, rare, gentle level-overshoot fade) and
reports the honest result. It is the reverting flavor (level overshoot, not the RSI-bar spike),
fired only at the extreme tail (rare => low turnover), held a few hours, then flat.

CAUSAL: the per-bar return sigma is a trailing rolling std SHIFTED by 1 (excludes the current
bar); the EMA uses only past bars (adjust=False). Entry is decided at close[i] and held for a
fixed H bars, then flat. No shift(-k), no full-sample stats. The evaluator holds position[i]
during bar i+1, so there is no leak by construction.

SCORED RESULT (2026-06-21, hardened marginal judge) — EARNS_SLOT = FALSE (a clean NEGATIVE):
  config k=2.0, H=24, ema(1,2,3): abs_grade=FAIL, marginal_verdict=NOISE.
    turnover ~19/yr (well under the 120 cap — the gate is genuinely rare),
    cand_insample_sharpe ~+0.10 (<< the 0.5 bar => NO standalone edge: BTC overshoots CONTINUE,
       ETH barely reverts; the fade is ~flat in-sample at best, negative on the BTC leg),
    abs_full_sharpe ~+0.04 / abs_hold_sharpe -1.19, fee020_full_sharpe ~-0.01 (does NOT survive),
    uplift_hold -0.13, multicut_persistent=False, has_insample_edge=False, is_hedge=False.
  A faster config (k=1.75, H=8) flips the lucky-window uplift positive (+0.18) but is in-sample
  NEGATIVE (-0.40) => the judge correctly calls it NOISE (diversification math on a near-zero
  stream, dressed by the 2025 window). NO cell in the (ema x k x H) grid has a positive in-sample
  Sharpe on BOTH assets; the best single-asset cells reach ~+0.45 (BTC) while ETH is ~0, or vice
  versa, and never together. The 1h fade does NOT reproduce the 15m tail-fade edge (agent_06):
  1h aggregates away the micro-overshoot that mean-reverts and leaves the macro-overshoot that
  TRENDS (TP01's domain, wrong sign for a fade). Honest outcome for a fee-bound intraday revert
  idea on the dominant trend asset: it dies. Kept as a documented negative, not a deploy.
"""
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

_EMA_HORIZONS_D = (1, 2, 3)   # multi-day EMAs whose overshoot we fade (price far from trend)
_K = 2.0                      # k-sigma overshoot gate — extreme tail only (rare => low turnover)
_H_BARS = 24                  # hold the fade ~1 day (the only horizon where the 1h fade is not
                              #   strongly WRONG-signed; H<24 => continuation, H>36 => decays)
_VOL_WIN_BARS = 24            # ~1 day of 1h bars for the causal return-sigma estimate
_BPD = 24                     # 1h bars per day


def _overshoot_dist(c: np.ndarray, r: np.ndarray, ema_d: int) -> np.ndarray:
    """Standardized distance of close from a multi-day EMA, in units of the expected move over
    the EMA window. CAUSAL: per-bar return sigma is a trailing rolling std SHIFTED by 1; the EMA
    uses only past bars. A value >= +k means price has overshot the trend to the upside."""
    sig = pd.Series(r).rolling(_VOL_WIN_BARS, min_periods=_VOL_WIN_BARS // 2).std().shift(1).values
    win = ema_d * _BPD
    ema = al.ema(c, win)
    return (c - ema) / c / (sig * np.sqrt(win))


def target(df: pd.DataFrame) -> np.ndarray:
    """Continuous gentle fade in {-1, 0, +1}. When the AVERAGE level-overshoot across the
    1/2/3-day EMAs exceeds _K sigma (price extended on all timescales at once), take a unit fade
    toward the trend and hold for _H_BARS, then flat. Rare gate => low turnover."""
    c = df["close"].values.astype(float)
    r = al.simple_returns(c)
    n = len(c)
    dists = [_overshoot_dist(c, r, h) for h in _EMA_HORIZONS_D]

    pos = np.zeros(n)
    cur = 0.0
    countdown = 0
    for i in range(n):
        if countdown > 0:                       # still holding a fade -> carry, no new trade
            pos[i] = cur
            countdown -= 1
            continue
        vals = [d[i] for d in dists if np.isfinite(d[i])]
        if vals:
            mean_dist = float(np.mean(vals))
            if abs(mean_dist) >= _K:
                cur = -float(np.sign(mean_dist))   # FADE toward the trend
                pos[i] = cur
                countdown = _H_BARS - 1
    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"])
