"""agent_01_session_overlay — SESSION OVERLAY on the daily TSMOM trend (TF=1h).

ANGLE [family=session, slug=session_overlay]: be in the daily trend position only during
the strongest session (Asia/EU/US blocks); reduce/flat in the weak session. Causal
session-return estimates. MINIMIZE flips.

STRUCTURAL BASIS (measured, both BTC & ETH): crypto drift is concentrated in EU+US hours;
Asia hours (UTC 0-7) carry ~0 mean return but full variance -> bad reward/risk. So holding
the trend through dead Asia hours adds vol without return. The overlay down-weights the
session that is causally the weakest.

FEE DISCIPLINE: a naive in/out-per-session flip churns ~700x/yr = fee-death. We keep
turnover bounded by (a) a SLOW trend (TP01 horizons 30/90/180d -> monthly flips), and (b)
modulating exposure across only 2 levels with a session weight that itself changes slowly
(a causal EXPANDING ranking of sessions, re-evaluated, not per-bar noise).

CAUSAL: the session strength is an expanding mean of past per-session hourly returns
(data strictly < current bar). No full-sample calendar fit.
"""
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

# Session blocks in UTC (8h each). Asia tends to be the dead block for crypto.
# 0=Asia(0-7), 1=EU(8-15), 2=US(16-23)
def _session_id(hours: np.ndarray) -> np.ndarray:
    return np.where(hours < 8, 0, np.where(hours < 16, 1, 2)).astype(int)


def _causal_session_weak(r: np.ndarray, sess: np.ndarray, bpd: int,
                         warmup_days: int = 180) -> np.ndarray:
    """For each bar i, return the id of the session that is CAUSALLY weakest by expanding
    mean hourly return using data strictly before i. Before warmup -> -1 (no opinion).
    Computed once per day (at the first bar of each session-0 day) so it changes slowly."""
    n = len(r)
    weak = np.full(n, -1, dtype=int)
    # running sums per session
    ssum = np.zeros(3)
    scnt = np.zeros(3)
    warm = warmup_days * bpd
    # We update the running stats with bar i-1 before deciding for bar i (strictly causal).
    cur_weak = -1
    for i in range(1, n):
        s_prev = sess[i - 1]
        ssum[s_prev] += r[i - 1]
        scnt[s_prev] += 1
        if i >= warm and scnt.min() > 0:
            means = ssum / scnt
            cur_weak = int(np.argmin(means))
        weak[i] = cur_weak
    return weak


def target(df: pd.DataFrame) -> np.ndarray:
    c = df["close"].values.astype(float)
    dt = pd.to_datetime(df["datetime"], utc=True)
    hours = dt.dt.hour.values
    bpd = al.bars_per_day(df)            # 24 at 1h

    # --- TP01-style slow trend direction (long-flat) -------------------------
    horizons = tuple(d * bpd for d in (30, 90, 180))
    nbar = len(c)
    acc = np.zeros(nbar); cnt = np.zeros(nbar)
    for h in horizons:
        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]
    direction = np.clip(direction, 0, None)            # long-flat like TP01

    # vol-target (TP01 canonical)
    base = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)

    # --- session overlay -----------------------------------------------------
    r = al.simple_returns(c)
    sess = _session_id(hours)
    weak = _causal_session_weak(r, sess, bpd, warmup_days=180)

    # weight: full exposure outside the causally-weak session, reduced during it.
    # NOTE (honest, after a full sweep): every step away from 1.0 (i.e. MORE overlay)
    # strictly degrades both Sharpe and turnover vs plain TP01 — the dead-Asia effect is
    # already captured by TP01's vol-targeting, and gating removes good trend days too.
    # 0.9 is the least-harmful overlay. The angle does NOT earn a slot (see report notes).
    w_weak = 0.9
    sess_w = np.where(sess == weak, w_weak, 1.0)
    sess_w[weak < 0] = 1.0                              # no opinion -> full (TP01 behavior)

    return base * sess_w
