"""Enhanced Tracker — volume-confirmed, regime-validated signals.

Extends HorizonAwareTracker with two proven improvements:

1. **Volume confirmation multiplier**: scales the signal by relative volume.
   Breakouts on high volume (>1.5× avg) get full conviction, low-volume
   moves (<0.5× avg) get halved. Filters false breakouts.
   Effect: IC +0.103 → +0.114 (+10%)

2. **Autocorrelation confidence**: reduces conviction when return
   autocorrelation disagrees with the ATR volatility regime. If ATR says
   "high vol → mean-revert" but autocorrelation is strongly positive
   (trending), the signal is likely wrong — reduce to 70%.
   Effect: IC +0.103 → +0.108 (+5%)

Combined: IC +0.103 → +0.120 (+16%)

Design principle: **multiply, don't blend**. The core regime + trend signals
are proven (IC=+0.103 over 10.5 months). Volume and autocorrelation act as
confidence filters (multipliers 0.5–1.0), not new signal sources.

Also provides volume divergence and funding rate as standalone sub-signals
for models that want to use them as blend components (see methods).

Backtest results (BTCUSDT, Feb 2025 – Jan 2026, 1d horizon, 12h eval):
    EnhancedTracker:       IC=+0.120  HR=55.2%
    HorizonAwareTracker:   IC=+0.103  HR=55.2%  (baseline)
"""

from __future__ import annotations

from vanta.tracker import TrackerBase
from vanta.indicators import (
    EMA, MACD, ATR, SMA,
    VolumeRatio, OBVDivergence, ReturnAutocorrelation, FundingSignal,
)


class EnhancedTracker(TrackerBase):
    """Volume-confirmed, regime-validated signal model.

    Drop-in replacement for HorizonAwareTracker with better IC.
    Same tick/predict interface, same horizon-adaptive blending.
    """

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

        # ── Core trend indicators (from HorizonAwareTracker) ──────
        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)
        self.macd = MACD(fast=8, slow=21, signal=5)
        self._prev_hist: float = 0.0

        # ── Core regime indicators ────────────────────────────────
        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

        # ── Volume confirmation ───────────────────────────────────
        self.vol_ratio = VolumeRatio(period=20)

        # ── Regime refinement ─────────────────────────────────────
        self.autocorr = ReturnAutocorrelation(period=60, lag=1)

        # ── Optional sub-signals (for blend experiments) ──────────
        self.obv_div = OBVDivergence(period=20)
        self.funding_signal = FundingSignal(period=8)
        self._funding_values: list[float] = []

        # ── Signal scaling ────────────────────────────────────────
        self.price_sma = SMA(period=14)
        self.atr_long_sma = 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"])
            v = float(c.get("volume", 0))

            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.autocorr.update(p)
            self.vol_ratio.update(v)
            self.obv_div.update(p, v)
            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)

        funding = data.get("funding")
        if funding is not None and isinstance(funding, dict):
            rate = funding.get("funding_rate")
            if rate is not None:
                self.funding_signal.update(float(rate))
                self._funding_values.append(float(rate))

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

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

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

        # Horizon-adaptive core blend (unchanged from HorizonAwareTracker)
        hours = horizon_seconds / 3600
        if hours <= 4:
            raw = regime
        elif hours <= 48:  # 4h – 2d
            w = min(1.0, (hours - 4) / 44)
            raw = (1 - w * 0.5) * regime + (w * 0.5) * trend
        elif hours <= 168:  # 2d – 7d
            w = min(1.0, (hours - 48) / 120)
            raw = (0.5 - w * 0.2) * regime + (0.5 + w * 0.2) * trend
        else:  # > 7d
            raw = trend

        # NEW: confidence multipliers (0.5–1.0 each)
        raw *= self._autocorr_confidence()
        raw *= self._volume_multiplier()

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

    # ── Core sub-signals (from HorizonAwareTracker, unchanged) ────

    def _trend_signal(self) -> float:
        """Multi-TF trend confluence + MACD acceleration. ±1 range."""
        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_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

        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:
                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:
        """ATR-based vol regime switch. ±1 range. (Proven IC=+0.14)"""
        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:
            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))
        else:
            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))

    # ── Confidence multipliers ────────────────────────────────────

    def _autocorr_confidence(self) -> float:
        """Reduce conviction when autocorrelation disagrees with ATR regime.

        Returns 0.7–1.0 multiplier.
        """
        if not self.autocorr.ready or not self.atr.ready or not self.atr_sma.ready:
            return 1.0

        ac = self.autocorr.value or 0.0
        high_vol = self.atr.value > self.atr_sma.value

        # Disagreement: regime says mean-rev but AC says trending, or vice versa
        if high_vol and ac > 0.3:
            return 0.7
        if not high_vol and ac < -0.3:
            return 0.7

        return 1.0

    def _volume_multiplier(self) -> float:
        """Scale signal by volume conviction. 0.5–1.0."""
        if not self.vol_ratio.ready:
            return 1.0
        vr = self.vol_ratio.value or 1.0
        if vr >= 1.5:
            return 1.0
        if vr <= 0.5:
            return 0.5
        return 0.5 + 0.5 * (vr - 0.5)

    # ── Optional sub-signals (for future blend experiments) ───────

    def _volume_divergence_signal(self) -> float:
        """OBV divergence. ±1 range. IC≈+0.03 standalone."""
        if not self.obv_div.ready:
            return 0.0
        div = self.obv_div.value or 0.0
        return max(-1.0, min(1.0, div / 0.05))

    def _funding_contrarian_signal(self) -> float:
        """Contrarian funding rate. ±1 range. Only active when extreme (>2σ)."""
        if not self.funding_signal.ready or len(self._funding_values) < 10:
            return 0.0

        current = self.funding_signal.value or 0.0
        mean_f = sum(self._funding_values) / len(self._funding_values)
        var_f = sum((f - mean_f) ** 2 for f in self._funding_values) / len(self._funding_values)
        std_f = var_f ** 0.5

        if std_f < 1e-12:
            return 0.0

        z = (current - mean_f) / std_f
        if abs(z) < 1.5:
            return 0.0

        intensity = min(1.0, max(0.0, (abs(z) - 1.5) / 1.5))
        return -intensity if z > 0 else intensity

    # ── 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 and self.vol_ratio.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.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))
