"""Price Momentum Tracker — pure return-based momentum at multiple scales.

No indicators, no regime switching — just measures the direction and
strength of price momentum at different lookback windows and averages
them.

The insight: in the Sep-Nov 2025 correction, indicator-based models
struggled because ATR regime detection conflated "high vol" with
"should mean-revert" when the correct action was to follow the trend
down. Pure price momentum doesn't have this confusion — it just
follows the move.

Sub-signals:
  Short-term (6h returns):    captures intraday momentum continuation
  Medium-term (24h returns):  captures daily trend direction
  Long-term (7d returns):     captures weekly trend direction

When all three agree → strong signal.
When they disagree → reduced signal.

The horizon-adaptive blend emphasizes longer lookbacks for longer
prediction horizons.
"""

from __future__ import annotations

from collections import deque

from vanta.tracker import TrackerBase
from vanta.indicators import ATR, SMA


class PriceMomentumTracker(TrackerBase):
    """Pure price momentum model — no indicator regime switching."""

    def __init__(self):
        super().__init__()

        # Price history at different resolutions
        self._prices_1h: deque[float] = deque(maxlen=200)  # 200 hours
        self._last_close: float = 0.0

        # ATR for signal scaling only (not regime detection)
        self.atr = ATR(period=14)
        self.price_sma = SMA(period=14)
        self.atr_sma = SMA(period=60)

    def tick(self, data: dict) -> None:
        super().tick(data)

        for c in data.get("candles_1m", []):
            p = float(c["close"])
            h, l = float(c["high"]), float(c["low"])
            self.atr.update(h, l, p)
            if self.atr.ready:
                self.atr_sma.update(self.atr.value)
            self.price_sma.update(p)
            self._last_close = p

        for c in data.get("candles_1h", []):
            p = float(c["close"])
            self._prices_1h.append(p)

    def predict(self, symbol: str, horizon_seconds: int, step_seconds: int) -> dict:
        if len(self._prices_1h) < 168 or not self.atr.ready:  # need 7 days
            return {"expected_return": 0.0}

        prices = list(self._prices_1h)

        # Returns at different lookbacks
        ret_6h = self._returns(prices, 6)
        ret_24h = self._returns(prices, 24)
        ret_7d = self._returns(prices, 168)

        # Normalise by dividing by typical magnitude
        # Use recent volatility (stdev of 24h returns) as normalization
        recent_rets = [self._returns(prices[:-(i)], 24) for i in range(1, min(8, len(prices) - 24))]
        if recent_rets:
            std = max(1e-9, (sum(r**2 for r in recent_rets) / len(recent_rets)) ** 0.5)
        else:
            std = abs(ret_24h) if abs(ret_24h) > 1e-9 else 0.01

        norm_6h = ret_6h / std
        norm_24h = ret_24h / std
        norm_7d = ret_7d / std

        # Clip to ±2
        norm_6h = max(-2, min(2, norm_6h))
        norm_24h = max(-2, min(2, norm_24h))
        norm_7d = max(-2, min(2, norm_7d))

        # Horizon-adaptive blend
        hours = horizon_seconds / 3600
        if hours <= 12:
            raw = 0.50 * norm_6h + 0.35 * norm_24h + 0.15 * norm_7d
        elif hours <= 48:
            raw = 0.20 * norm_6h + 0.50 * norm_24h + 0.30 * norm_7d
        else:
            raw = 0.10 * norm_6h + 0.30 * norm_24h + 0.60 * norm_7d

        # Agreement bonus: when all three agree, boost signal
        signs = [
            1 if norm_6h > 0 else (-1 if norm_6h < 0 else 0),
            1 if norm_24h > 0 else (-1 if norm_24h < 0 else 0),
            1 if norm_7d > 0 else (-1 if norm_7d < 0 else 0),
        ]
        all_agree = signs[0] == signs[1] == signs[2] and signs[0] != 0
        if all_agree:
            raw *= 1.3
        elif signs[0] * signs[2] < 0:  # short and long disagree
            raw *= 0.5

        raw = max(-1.0, min(1.0, raw / 2.0))  # normalise to ±1

        scaled = self._scale(raw)
        return {"expected_return": scaled}

    @staticmethod
    def _returns(prices: list[float], lookback: int) -> float:
        if len(prices) < lookback + 1:
            return 0.0
        old = prices[-(lookback + 1)]
        new = prices[-1]
        if abs(old) < 1e-9:
            return 0.0
        return (new - old) / old

    def _scale(self, raw: float, max_abs: float = 0.02) -> float:
        target = 0.004
        if not (self.atr.ready and self.price_sma.ready and self.atr_sma.ready):
            return max(-max_abs, min(max_abs, raw * target))
        norm_atr = self.atr.value / max(self.price_sma.value, 1e-9)
        avg_atr = self.atr_sma.value / max(self.price_sma.value, 1e-9)
        if avg_atr < 1e-9:
            return max(-max_abs, min(max_abs, raw * target))
        vol_ratio = max(0.3, min(3.0, avg_atr / max(norm_atr, 1e-9)))
        return max(-max_abs, min(max_abs, raw * target * vol_ratio))
