"""agent_47_trail_mom — momentum entry with ACTIVE TRAILING-STOP position management.

Angle [family=mix, slug=trail_mom]:
  * Enter LONG/SHORT on multi-horizon momentum (the "trend is your friend" entry).
  * Then actively MANAGE the position with a trailing stop measured in ATR units from
    the best favourable price seen since the trade opened:
      - adverse excursion (price pulls back toward the trail) -> REDUCE exposure,
      - follow-through (new favourable extreme) -> ADD exposure back, up to full size.
  * Vol-target the whole thing so DD stays bounded.

CAUSAL: every value at bar i uses only rows 0..i. The trailing state machine is a pure
forward loop (no future peek). The evaluator shifts the position, so position[i] is the
weight held during bar i+1 — decided from data up to close[i].
"""
import numpy as np
import blindlib as bl


def _mom_dir(c):
    """Multi-horizon momentum direction in [-1,1] (causal). Equal-weight 20/50/100."""
    d = np.zeros(len(c))
    for w, wt in ((20, 0.34), (50, 0.33), (100, 0.33)):
        m = c / bl.sma(c, w) - 1.0
        d += wt * np.tanh(8.0 * m)
    return np.clip(d, -1.0, 1.0)


def signal(df):
    c = df["close"].values.astype(float)
    h = df["high"].values.astype(float)
    l = df["low"].values.astype(float)
    n = len(c)

    direction = _mom_dir(c)                      # desired sign + conviction
    a = bl.atr(df, 14)                           # causal ATR (vol unit for trail)
    a = np.where(np.isfinite(a) & (a > 0), a, np.nan)

    # ---- trailing-stop state machine (pure causal forward loop) -------------
    TRAIL_K = 4.0        # trail distance in ATR from the favourable extreme
    REDUCE_K = 0.8       # adverse excursion (ATR) at which we start shrinking
    sized = np.zeros(n)  # managed exposure scalar in [0,1]
    cur_sign = 0.0
    best = np.nan        # best favourable price since entry (max if long, min if short)
    expo = 0.0           # current exposure fraction in [0,1]

    for i in range(n):
        d = direction[i]
        sgn = np.sign(d) if abs(d) > 0.20 else 0.0   # dead-zone: avoid chop flip
        ai = a[i]
        if not np.isfinite(ai):
            sized[i] = 0.0
            continue

        # entry / flip: reset trailing state, start at conviction-scaled exposure
        if sgn != 0.0 and sgn != cur_sign:
            cur_sign = sgn
            best = c[i]
            expo = min(1.0, abs(d))
        elif sgn == 0.0:
            cur_sign = 0.0
            expo = 0.0
            best = np.nan

        if cur_sign != 0.0 and np.isfinite(best):
            # update favourable extreme
            if cur_sign > 0:
                best = max(best, h[i])
                adverse = (best - c[i]) / ai      # how far pulled back (ATR units)
            else:
                best = min(best, l[i])
                adverse = (c[i] - best) / ai
            # trailing management:
            if adverse >= TRAIL_K:
                expo = 0.0                        # stopped out
            elif adverse >= REDUCE_K:
                # linearly reduce between REDUCE_K and TRAIL_K
                frac = 1.0 - (adverse - REDUCE_K) / (TRAIL_K - REDUCE_K)
                target = min(1.0, abs(d)) * max(0.0, frac)
                expo = min(expo, target)          # reduce only on adverse
            else:
                # follow-through region -> add back toward full conviction
                target = min(1.0, abs(d))
                expo = expo + 0.34 * (target - expo)   # ease back up
            sized[i] = cur_sign * expo
        else:
            sized[i] = 0.0

    # ---- vol-target the managed directional series --------------------------
    pos = bl.vol_target(sized, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
    return np.clip(pos, -1.0, 1.0)
