"""Horizon-Aware Multi-Signal Tracker.

A single model that produces different signals depending on the requested
horizon_seconds.  Short horizons emphasise fast mean-reversion / momentum
indicators; long horizons emphasise slow trend / regime indicators.

Architecture
────────────
All indicators are updated in tick() from the same candle stream (1m/5m/15m/1h).
predict() selects and weights indicators based on horizon_seconds:

  ┌────────────┬───────────────────────────────────────────────┐
  │  Horizon   │  Signal composition                           │
  ├────────────┼───────────────────────────────────────────────┤
  │  ≤ 4 h     │  vol-regime (mean-rev / breakout)             │
  │  4 h–2 d   │  50% vol-regime  + 50% trend-confluence       │
  │  2 d–7 d   │  30% vol-regime  + 70% trend-confluence       │
  │  > 7 d     │  100% trend-confluence                        │
  └────────────┴───────────────────────────────────────────────┘

Trend confluence = multi-TF EMA agreement (5m/15m/1h) + MACD acceleration.
Best at 7d horizon: IC=+0.071.

Vol-regime = ATR regime switch between mean-reversion and momentum (1m feed).
Best at 1d horizon: IC=+0.101.

Note on candle timeframes: 4h/1d candles were tested but degraded the 7d
signal (IC dropped from +0.07 to −0.05).  The EMA crossover patterns that
drive this model are sub-hourly dynamics that aggregate into multi-day
trends — 1h is the sweet spot for the slow indicators.

Dual-horizon usage (recommended):
    model = HorizonAwareTracker()
    sig_1d = model.predict(symbol, horizon_seconds=86400, ...)   # daily timing
    sig_7d = model.predict(symbol, horizon_seconds=604800, ...)  # weekly direction
    blended = 0.7 * sig_1d + 0.3 * sig_7d                       # Sharpe +2.27
"""

from __future__ import annotations

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


class HorizonAwareTracker(TrackerBase):
    """Single model, horizon-adaptive signal.

    Call predict() with different horizon_seconds to get a signal tuned
    for that timescale.  The position layer can call it twice (1d + 7d)
    and blend the results.

    Backtest results (BTCUSDT Jul 2025–Jan 2026, checked every 12h):
        1d horizon:  IC=+0.101  net=+0.488  Sharpe=+1.77
        7d horizon:  IC=+0.071  net=+0.149  Sharpe=+0.61
        70/30 blend: IC=n/a     net=+0.591  Sharpe=+2.27
    """

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

        # ── Trend indicators (5m/15m/1h — for weekly direction) ────
        self.ema_5m_fast = EMA(period=8)
        self.ema_5m_slow = EMA(period=21)
        self.ema_15m_fast = EMA(period=5)
        self.ema_15m_slow = EMA(period=13)
        self.ema_1h_fast = EMA(period=5)
        self.ema_1h_slow = EMA(period=13)

        # MACD for momentum acceleration (1m feed)
        self.macd = MACD(fast=8, slow=21, signal=5)
        self._prev_hist: float = 0.0

        # ── Regime indicators (1m — for daily timing) ─────────────
        self.atr = ATR(period=14)
        self.atr_sma = SMA(period=14)
        self.fast_ema = EMA(period=5)
        self.slow_ema = EMA(period=20)
        self.sma = SMA(period=20)
        self._last_close: float = 0.0

        # ── Signal scaling ────────────────────────────────────────
        self.price_sma = SMA(period=14)
        self.atr_long_sma = SMA(period=60)

    # ── tick: feed all indicators from candle stream ──────────────

    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.macd.update(p)
            self.atr.update(h, l, p)
            if self.atr.ready:
                self.atr_sma.update(self.atr.value)
                self.atr_long_sma.update(self.atr.value)
            self.fast_ema.update(p)
            self.slow_ema.update(p)
            self.sma.update(p)
            self.price_sma.update(p)
            self._last_close = p

        for c in data.get("candles_5m", []):
            p = float(c["close"])
            self.ema_5m_fast.update(p)
            self.ema_5m_slow.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"])
            self.ema_1h_fast.update(p)
            self.ema_1h_slow.update(p)

    # ── predict: horizon-adaptive signal ──────────────────────────

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

        # Compute sub-signals
        trend = self._trend_signal()
        regime = self._regime_signal()

        # Blend based on horizon
        hours = horizon_seconds / 3600
        if hours <= 4:
            raw = regime
        elif hours <= 48:                       # 4h – 2d
            w = min(1.0, (hours - 4) / 44)     # linear ramp 0→1 over 4h–48h
            raw = (1 - w * 0.5) * regime + (w * 0.5) * trend
        elif hours <= 168:                      # 2d – 7d
            w = min(1.0, (hours - 48) / 120)   # ramp from 50/50 to 30/70
            raw = (0.5 - w * 0.2) * regime + (0.5 + w * 0.2) * trend
        else:                                   # > 7d
            raw = trend

        # ATR-normalise and clip
        scaled = self._scale(raw)
        return {"expected_return": scaled}

    # ── sub-signals ───────────────────────────────────────────────

    def _trend_signal(self) -> float:
        """Multi-TF trend confluence + MACD acceleration.

        Returns ±1 (strong trend) or 0 (no confluence / wrong acceleration).
        """
        t5 = self._spread(self.ema_5m_fast, self.ema_5m_slow)
        t15 = self._spread(self.ema_15m_fast, self.ema_15m_slow)
        t1h = self._spread(self.ema_1h_fast, self.ema_1h_slow)

        # All three timeframes must agree
        all_bull = t5 > 0 and t15 > 0 and t1h > 0
        all_bear = t5 < 0 and t15 < 0 and t1h < 0
        if not (all_bull or all_bear):
            return 0.0

        # MACD acceleration must confirm
        if self.macd.ready:
            hist = self.macd.value.histogram
            hist_accel = hist - self._prev_hist
            self._prev_hist = hist
            direction = 1.0 if all_bull else -1.0
            if direction * hist_accel <= 0:
                # Momentum decelerating — halve confidence, don't zero
                strength = min(abs(t5), abs(t15), abs(t1h))
                return direction * min(strength * 25, 0.5)

        direction = 1.0 if all_bull else -1.0
        strength = min(abs(t5), abs(t15), abs(t1h))
        return direction * min(strength * 50, 1.0)

    def _regime_signal(self) -> float:
        """Volatility-regime adaptive signal.

        High vol → mean-reversion (fade moves).
        Low vol → momentum (follow breakouts).
        Returns raw signal in [-1, +1] range.
        """
        if not (self.atr.ready and self.atr_sma.ready
                and self.fast_ema.ready and self.slow_ema.ready
                and self.sma.ready and self._last_close):
            return 0.0

        high_vol = self.atr.value > self.atr_sma.value

        if high_vol:
            # Mean-revert: fade deviation from SMA
            dev = (self._last_close - self.sma.value) / max(abs(self.sma.value), 1e-9)
            return max(-1.0, min(1.0, -0.5 * dev / 0.01))  # ±1% dev → ±0.5
        else:
            # Momentum: follow EMA spread
            spread = (self.fast_ema.value - self.slow_ema.value) / max(abs(self.slow_ema.value), 1e-9)
            return max(-1.0, min(1.0, spread / 0.005))  # ±0.5% spread → ±1.0

    # ── helpers ───────────────────────────────────────────────────

    @staticmethod
    def _spread(fast: EMA, slow: EMA) -> float:
        if not (fast.ready and slow.ready) or slow.value == 0:
            return 0.0
        return (fast.value - slow.value) / slow.value

    def _ready(self) -> bool:
        return (self.ema_5m_fast.ready and self.ema_15m_fast.ready
                and self.ema_1h_fast.ready and self.macd.ready
                and self.atr.ready and self.atr_sma.ready
                and self.fast_ema.ready and self.sma.ready
                and self.price_sma.ready)

    def _scale(self, raw: float, max_abs: float = 0.02) -> float:
        """ATR-normalise raw signal to expected return magnitude."""
        target = 0.004  # target risk per signal
        if not (self.atr.ready and self.price_sma.ready and self.atr_long_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_long_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))
