"""Performance-Weighted Ensemble — dynamic strategy allocation.

Runs multiple sub-strategies and dynamically allocates weight based on
each strategy's recent IC (information coefficient). Strategies with
higher recent predictive accuracy get more weight.

Uses only the top 4 validated strategies:
- MTF_Trend: Multi-timeframe trend confluence
- Extreme_MR: Mean reversion at z-score extremes
- Ret_Autocorr: Return autocorrelation (1m mean-revert + 5m momentum)
- Hourly_Momentum: 1h trend with 5m pullback entry
"""
import math
from collections import deque
from vanta.tracker import TrackerBase
from vanta.examples.mtf_trend_tracker import MTFTrendTracker
from vanta.examples.extreme_mean_reversion_tracker import ExtremeMeanReversionTracker
from vanta.examples.return_autocorr_tracker import ReturnAutocorrTracker
from vanta.examples.hourly_momentum_tracker import HourlyMomentumTracker


class EnsembleTracker(TrackerBase):
    """Performance-weighted ensemble of top strategies.

    Walk-forward results:
        BTC IS: IC=+0.015  |  BTC OOS: IC=+0.013
        ETH IS: IC=+0.018  |  ETH OOS: IC=-0.005
    """

    def __init__(self, lookback=200):
        super().__init__()
        self.strategies = {
            "mtf_trend": MTFTrendTracker(),
            "extreme_mr": ExtremeMeanReversionTracker(),
            "ret_autocorr": ReturnAutocorrTracker(),
            "hourly_mom": HourlyMomentumTracker(),
        }
        self.lookback = lookback
        self.signal_history: dict[str, deque] = {
            name: deque(maxlen=lookback) for name in self.strategies
        }
        self.actual_returns = deque(maxlen=lookback)
        self.prices = deque(maxlen=10)

        # Circuit breaker: cap cumulative loss
        self._cumulative = 0.0
        self._peak = 0.0
        self._halted = False

    def tick(self, data):
        super().tick(data)
        for strat in self.strategies.values():
            strat.tick(data)
        for c in data.get("candles_1m", []):
            self.prices.append(float(c["close"]))

    def predict(self, symbol, horizon_seconds, step_seconds):
        # Circuit breaker: halt if drawdown > 5%
        if self._halted:
            return {"expected_return": 0.0}

        # Collect signals
        signals = {}
        for name, strat in self.strategies.items():
            out = strat.predict(symbol, horizon_seconds, step_seconds)
            sig = out.get("expected_return", 0.0)
            signals[name] = sig
            self.signal_history[name].append(sig)

        # Track actuals (approximate: 5 bars back)
        if len(self.prices) >= 6:
            actual = (self.prices[-1] - self.prices[-6]) / self.prices[-6]
            self.actual_returns.append(actual)

        weights = self._compute_weights()

        # Weighted signal
        total_w = sum(max(w, 0) for w in weights.values())
        if total_w < 1e-9:
            active = {n: s for n, s in signals.items() if abs(s) > 1e-9}
            if not active:
                return {"expected_return": 0.0}
            return {"expected_return": sum(active.values()) / len(active)}

        weighted = sum(signals[n] * max(weights.get(n, 0), 0) for n in signals) / total_w

        # Update drawdown tracker
        if abs(weighted) > 1e-9 and len(self.actual_returns) > 0:
            self._cumulative += weighted * self.actual_returns[-1]
            self._peak = max(self._peak, self._cumulative)
            dd = self._peak - self._cumulative
            if dd > 0.05:  # 5% drawdown circuit breaker
                self._halted = True
                return {"expected_return": 0.0}

        return {"expected_return": weighted}

    def _compute_weights(self):
        if len(self.actual_returns) < 30:
            return {name: 1.0 for name in self.strategies}

        actuals = list(self.actual_returns)
        weights = {}
        for name in self.strategies:
            hist = list(self.signal_history[name])
            n = min(len(hist), len(actuals))
            if n < 20:
                weights[name] = 0.5
                continue
            ic = _correlation(hist[-n:], actuals[-n:])
            weights[name] = max(ic, 0.0)
        return weights


def _correlation(x, y):
    n = len(x)
    if n < 2:
        return 0.0
    mx = sum(x) / n
    my = sum(y) / n
    cov = sum((a - mx) * (b - my) for a, b in zip(x, y)) / n
    sx = math.sqrt(sum((a - mx) ** 2 for a in x) / n)
    sy = math.sqrt(sum((b - my) ** 2 for b in y) / n)
    if sx < 1e-12 or sy < 1e-12:
        return 0.0
    return cov / (sx * sy)
