"""Vol-managed momentum — momentum direction scaled inversely by realized volatility.

Rationale (Moreira-Muir): scaling exposure inversely to realized vol raises Sharpe because
volatility is persistent and forecastable while returns are not. Known limitation: exposure is
clipped to [0,1], so this is DE-RISK ONLY (target_vol is a ceiling) — it never levers up toward
target_vol in calm regimes, a deliberate consequence of the [-1,1] position contract.
"""
from __future__ import annotations

import numpy as np

from fxhnt.domain.models import PriceSeries
from fxhnt.domain.strategies.base import register, rolling_mean, rolling_std


class VolManagedMomentum:
    kind = "vol_managed_momentum"
    rationale = "vol-managed momentum (Moreira-Muir): scaling exposure inversely to realized vol raises Sharpe — volatility is persistent and forecastable, returns are not"

    def positions(self, prices: PriceSeries, params: dict[str, float]) -> np.ndarray:
        window = int(params.get("window", 100))
        vol_window = int(params.get("vol_window", 20))
        target_vol = float(params.get("target_vol", 0.15))
        c = prices.close
        ma = rolling_mean(c, window)
        diff = c - ma
        direction = np.zeros_like(c)
        direction[diff > 0] = 1.0
        direction[diff < 0] = -1.0
        r = prices.returns()
        realized = rolling_std(r, vol_window) * np.sqrt(252.0)            # annualized realized vol
        scale = np.divide(target_vol, realized, out=np.zeros_like(c), where=realized > 0)
        scale = np.clip(scale, 0.0, 1.0)                                  # de-risk only: never lever past 1x
        signal = direction * scale
        signal[:max(window, vol_window)] = 0.0                            # warmup
        pos = np.zeros(len(c))
        pos[1:] = signal[:-1]                                             # decision at t-1 drives return at t
        return pos


register(VolManagedMomentum())
