"""Structural Trend Tracker — multi-timeframe momentum with structure detection.

Designed to complement EnhancedTracker in regimes where it struggles
(sideways/down markets). Uses slower momentum signals and price structure
analysis rather than volatility-regime switching.

Alpha sources:
1. **Hourly EMA momentum** — EMA crossover on 1h candles. Slower than
   the 1m-based signals, better at capturing persistent multi-day moves.

2. **Price structure** — detects higher-highs/higher-lows (uptrend) or
   lower-highs/lower-lows (downtrend) from swing points on 1h candles.
   Persistent structural signal that doesn't flip on noise.

3. **Breakdown/breakout detection** — when price decisively breaks above/below
   the 1h SMA with volume confirmation, signal persists until structure changes.

4. **Multi-timeframe alignment** — requires 1h and 15m momentum to agree
   for full conviction. Disagreement → reduced signal.

Key property: this model produces signals that are **anticorrelated** with
EnhancedTracker in weak periods (Sep-Nov 2025), making it ideal for
ensemble diversification.
"""

from __future__ import annotations

from collections import deque

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


class StructuralTrendTracker(TrackerBase):
    """Structural momentum model for regime diversification."""

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

        # ── 1h momentum (primary signal) ──────────────────────────
        self.ema_1h_fast = EMA(period=8)
        self.ema_1h_slow = EMA(period=21)
        self.ema_1h_trend = EMA(period=50)  # long-term trend filter
        self.sma_1h = SMA(period=20)

        # ── 15m momentum (confirmation) ───────────────────────────
        self.ema_15m_fast = EMA(period=8)
        self.ema_15m_slow = EMA(period=21)

        # ── Price structure detection ─────────────────────────────
        self._1h_highs: deque[float] = deque(maxlen=10)
        self._1h_lows: deque[float] = deque(maxlen=10)
        self._1h_closes: deque[float] = deque(maxlen=5)
        self._last_1h_close: float = 0.0

        # ── Volume + volatility ───────────────────────────────────
        self.vol_ratio_1h = VolumeRatio(period=20)
        self.atr_1h = ATR(period=14)
        self._last_1h_volume: float = 0.0

        # ── Signal scaling ────────────────────────────────────────
        self.price_sma = SMA(period=20)
        self.atr_sma = SMA(period=60)

        # 1m feed for scaling only
        self.atr_1m = ATR(period=14)
        self.price_sma_1m = SMA(period=14)
        self.atr_sma_1m = SMA(period=60)

    # ── tick ──────────────────────────────────────────────────────

    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_1m.update(h, l, p)
            if self.atr_1m.ready:
                self.atr_sma_1m.update(self.atr_1m.value)
            self.price_sma_1m.update(p)

        for c in data.get("candles_15m", []):
            p = float(c["close"])
            self.ema_15m_fast.update(p)
            self.ema_15m_slow.update(p)

        for c in data.get("candles_1h", []):
            p = float(c["close"])
            h = float(c["high"])
            l = float(c["low"])
            v = float(c.get("volume", 0))

            self.ema_1h_fast.update(p)
            self.ema_1h_slow.update(p)
            self.ema_1h_trend.update(p)
            self.sma_1h.update(p)
            self.price_sma.update(p)
            self.atr_1h.update(h, l, p)
            if self.atr_1h.ready:
                self.atr_sma.update(self.atr_1h.value)
            self.vol_ratio_1h.update(v)

            self._1h_highs.append(h)
            self._1h_lows.append(l)
            self._1h_closes.append(p)
            self._last_1h_close = p
            self._last_1h_volume = v

    # ── predict ───────────────────────────────────────────────────

    def predict(self, symbol: str, horizon_seconds: int, step_seconds: int) -> dict:
        if not self._ready():
            return {"expected_return": 0.0}

        # Sub-signals
        hourly_mom = self._hourly_momentum()
        structure = self._price_structure()
        breakdown = self._breakdown_signal()

        # Confirmation: 15m must agree with 1h for full conviction
        confirm = self._confirmation_multiplier()

        # Blend: structure-weighted momentum
        raw = (0.40 * hourly_mom + 0.35 * structure + 0.25 * breakdown) * confirm

        # Volume-confirm the breakdown signal
        if self.vol_ratio_1h.ready:
            vr = self.vol_ratio_1h.value or 1.0
            if abs(breakdown) > 0.3 and vr > 1.5:
                raw *= 1.2  # boost breakdowns with volume
            elif abs(breakdown) > 0.3 and vr < 0.7:
                raw *= 0.7  # discount low-volume breakdowns

        raw = max(-1.0, min(1.0, raw))
        scaled = self._scale(raw)
        return {"expected_return": scaled}

    # ── Sub-signals ───────────────────────────────────────────────

    def _hourly_momentum(self) -> float:
        """1h EMA crossover momentum. ±1 range.

        Uses the trend EMA as a directional filter — only signal in
        the direction of the long-term trend.
        """
        if not (self.ema_1h_fast.ready and self.ema_1h_slow.ready
                and self.ema_1h_trend.ready):
            return 0.0

        spread = (self.ema_1h_fast.value - self.ema_1h_slow.value)
        norm = spread / max(abs(self.ema_1h_slow.value), 1e-9)

        # Trend filter: only full signal in direction of EMA(50)
        above_trend = self._last_1h_close > self.ema_1h_trend.value
        signal = max(-1.0, min(1.0, norm / 0.003))  # ±0.3% spread → ±1

        if signal > 0 and not above_trend:
            signal *= 0.3  # long against trend → reduce
        elif signal < 0 and above_trend:
            signal *= 0.3  # short against trend → reduce

        return signal

    def _price_structure(self) -> float:
        """Detect higher-highs/lows (uptrend) or lower-highs/lows (downtrend).

        Returns ±1 range. Persistent signal that doesn't flip on noise.
        """
        if len(self._1h_highs) < 5 or len(self._1h_lows) < 5:
            return 0.0

        highs = list(self._1h_highs)
        lows = list(self._1h_lows)

        # Count recent higher-highs and higher-lows
        n = min(len(highs), 5)
        hh_count = sum(1 for i in range(1, n) if highs[-(i)] > highs[-(i+1)])
        hl_count = sum(1 for i in range(1, n) if lows[-(i)] > lows[-(i+1)])
        lh_count = sum(1 for i in range(1, n) if highs[-(i)] < highs[-(i+1)])
        ll_count = sum(1 for i in range(1, n) if lows[-(i)] < lows[-(i+1)])

        bull_score = (hh_count + hl_count) / (2 * (n - 1))  # 0–1
        bear_score = (lh_count + ll_count) / (2 * (n - 1))  # 0–1

        return bull_score - bear_score  # ±1 range

    def _breakdown_signal(self) -> float:
        """Detect price breaking above/below the 1h SMA. ±1 range."""
        if not self.sma_1h.ready or not self.atr_1h.ready or not self._last_1h_close:
            return 0.0

        # How far from SMA, normalized by ATR
        distance = self._last_1h_close - self.sma_1h.value
        atr = max(self.atr_1h.value, 1e-9)
        norm_dist = distance / atr

        # Only signal significant breakdowns (>1 ATR from SMA)
        if abs(norm_dist) < 0.5:
            return 0.0

        return max(-1.0, min(1.0, norm_dist / 2.0))  # 2 ATR → ±1

    def _confirmation_multiplier(self) -> float:
        """15m momentum must agree with 1h for full conviction."""
        if not (self.ema_15m_fast.ready and self.ema_15m_slow.ready
                and self.ema_1h_fast.ready and self.ema_1h_slow.ready):
            return 1.0

        mom_1h = self.ema_1h_fast.value - self.ema_1h_slow.value
        mom_15m = self.ema_15m_fast.value - self.ema_15m_slow.value

        if mom_1h * mom_15m > 0:
            return 1.0  # same direction — full conviction
        else:
            return 0.5  # disagreement — halve signal

    # ── Helpers ───────────────────────────────────────────────────

    def _ready(self) -> bool:
        return (self.ema_1h_fast.ready and self.ema_1h_slow.ready
                and self.ema_1h_trend.ready and self.sma_1h.ready
                and self.ema_15m_fast.ready and self.atr_1h.ready
                and len(self._1h_highs) >= 5
                and self.price_sma_1m.ready)

    def _scale(self, raw: float, max_abs: float = 0.02) -> float:
        """ATR-normalise raw signal to expected return magnitude."""
        target = 0.004
        if not (self.atr_1m.ready and self.price_sma_1m.ready and self.atr_sma_1m.ready):
            return max(-max_abs, min(max_abs, raw * target))
        norm_atr = self.atr_1m.value / max(self.price_sma_1m.value, 1e-9)
        avg_atr = self.atr_sma_1m.value / max(self.price_sma_1m.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))
