"""agent_27_dpo — Detrended Price Oscillator (cycle phase around a LAGGED MA).

ANGLE [family=osc, slug=dpo]: detrend price by subtracting a moving average that we
DELAY (lag) so the oscillator measures where price sits in its cycle relative to a
recent trend baseline. Trade the cycle phase — causal only.

Classic DPO is price[i] - SMA(n)[i - (n/2 + 1)]. The textbook centers that lag; here we
keep the displacement STRICTLY BACKWARD (the MA value comes from ~n/2 bars ago, fully in
the past), so the oscillator is causal/online and deployable.

What the train data says (tuned on split='train' only):
  dpo = (price - lagged_baseline) / vol(gap) is a z-like CYCLE PHASE around zero.
  Bucketing dpo vs the NEXT-bar return showed a clean MONOTONIC relationship: the higher
  the detrended oscillator (price above its lagged baseline = cycle UP-phase), the higher
  the next return; deep-negative dpo (cycle down-phase) precedes flat/negative returns.
  So on these series the cycle is CONTINUATION, not reversion -> we FOLLOW the phase
  (long the up-phase, flat/short the down-phase), confirmed by a slow trend gate, and
  size with vol-targeting. Result on train: positive PnL at ~19% worst DD vs buy&hold's
  ~78% DD — anticipating the move means staying out of (or short) the down-phase.

Config tuned on train (period=30 / trendwin=200 / scale=1.5 / wc=0.6 / ema=2 / tv=0.18):
plateau-robust across period 30, trend 150-200, scale 1.5-2.0, cycle weight 0.5-0.8.
"""
import numpy as np
import blindlib as bl

# --- tuned on split='train' only ------------------------------------------
PERIOD = 30        # DPO moving-average period
LAG = PERIOD // 2 + 1   # textbook DPO displacement, kept strictly backward (causal)
TREND_WIN = 200    # slow-trend confirmation window
SCALE = 1.5        # tanh softness of the cycle phase
W_CYCLE = 0.6      # blend weight: cycle phase vs slow-trend confirmation
EMA_SMOOTH = 2     # position smoothing (cuts turnover/fees)
TARGET_VOL = 0.18  # annualized vol target
VOL_WIN = 30
LEV_CAP = 1.0


def _dpo_phase(c: np.ndarray) -> np.ndarray:
    """Detrended price oscillator z-phase: (price - LAGGED SMA) / rolling std of gap.
    The baseline SMA is delayed by LAG bars, so every value uses only past data."""
    n = len(c)
    base = bl.sma(c, PERIOD)                  # causal SMA
    base_lag = np.full(n, np.nan)
    base_lag[LAG:] = base[:-LAG]              # baseline from LAG bars ago (past only)
    gap = c - base_lag
    gap_vol = bl.rolling_std(gap, PERIOD)
    gap_vol = np.where((gap_vol > 0) & np.isfinite(gap_vol), gap_vol, np.nan)
    return gap / gap_vol                      # z-like cycle phase (NaN during warmup)


def signal(df):
    c = df["close"].values.astype(float)

    # detrended cycle phase (DPO core) — empirically CONTINUATION on these series
    z = np.nan_to_num(_dpo_phase(c), nan=0.0)
    cycle = np.tanh(z / SCALE)                # +1 up-phase, -1 down-phase

    # slow-trend confirmation (don't ride the cycle against a strong regime)
    trend = c / bl.sma(c, TREND_WIN) - 1.0
    follow = np.tanh(np.nan_to_num(trend, nan=0.0) * 6.0)

    raw = np.clip(W_CYCLE * cycle + (1.0 - W_CYCLE) * follow, -1.0, 1.0)
    raw = bl.ema(raw, EMA_SMOOTH)             # smooth -> fewer fee-bleeding flips

    pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
                        vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
    return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
