#!/usr/bin/env python3
"""
Research v3: 1-hour horizon strategies.

Predict every 15 minutes, score against 1-hour forward return.
~96 predictions/day. Much higher signal-to-noise than 5-min.

Usage:
    cd challenge && COORDINATOR_URL=http://localhost:8000 .venv/bin/python research/run_1h.py
"""
import sys, os, time, math
from collections import deque
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from vanta.backtest import BacktestRunner
from vanta.tracker import TrackerBase
from vanta.indicators import SMA, EMA, RSI, MACD, BollingerBands, ATR, VWAP, Returns


# ── Signal Scaler (reused) ────────────────────────────────────────────────────

class SignalScaler:
    def __init__(self, atr_period=14, scale_period=60, target_risk=0.008):
        self.atr = ATR(period=atr_period)
        self.price_sma = SMA(period=atr_period)
        self.atr_sma = SMA(period=scale_period)
        self.target_risk = target_risk

    def update(self, high, low, close):
        self.atr.update(high, low, close)
        self.price_sma.update(close)
        if self.atr.ready:
            self.atr_sma.update(self.atr.value)

    @property
    def ready(self):
        return self.atr.ready and self.price_sma.ready and self.atr_sma.ready

    def scale(self, raw, max_abs=0.05):
        if not self.ready or self.price_sma.value == 0:
            return raw * self.target_risk
        norm_atr = self.atr.value / self.price_sma.value
        avg_norm_atr = self.atr_sma.value / self.price_sma.value
        if avg_norm_atr < 1e-9:
            return raw * self.target_risk
        vol_ratio = max(0.3, min(3.0, avg_norm_atr / max(norm_atr, 1e-9)))
        return max(-max_abs, min(max_abs, raw * self.target_risk * vol_ratio))


# ═══════════════════════════════════════════════════════════════════════════════
# STRATEGY 1: Session Momentum
# Hourly trends persist. If the last 2-4 hours have been trending, the next
# hour is likely to continue — especially when confirmed across timeframes.
# ═══════════════════════════════════════════════════════════════════════════════

class SessionMomentum(TrackerBase):
    """Trade with the prevailing multi-hour session trend."""

    def __init__(self):
        super().__init__()
        self.scaler = SignalScaler(target_risk=0.008)
        # 1h trend (primary signal for 1h horizon)
        self.ema_1h_fast = EMA(period=5)   # 5h
        self.ema_1h_slow = EMA(period=13)  # 13h
        # 15m for finer entry
        self.ema_15m_fast = EMA(period=8)
        self.ema_15m_slow = EMA(period=21)
        # Momentum confirmation
        self.returns_4h = Returns(period=4)  # 4-hour return on 1h candles

    def tick(self, data):
        super().tick(data)
        for c in data.get("candles_1m", []):
            self.scaler.update(float(c["high"]), float(c["low"]), float(c["close"]))
        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)
            self.returns_4h.update(p)

    def predict(self, symbol, horizon_seconds, step_seconds):
        if not (self.ema_1h_fast.ready and self.ema_15m_fast.ready
                and self.returns_4h.ready and self.scaler.ready):
            return {"expected_return": 0.0}

        trend_1h = (self.ema_1h_fast.value - self.ema_1h_slow.value) / self.ema_1h_slow.value
        trend_15m = (self.ema_15m_fast.value - self.ema_15m_slow.value) / self.ema_15m_slow.value
        mom_4h = self.returns_4h.value

        # All must agree on direction
        if not ((trend_1h > 0 and trend_15m > 0 and mom_4h > 0) or
                (trend_1h < 0 and trend_15m < 0 and mom_4h < 0)):
            return {"expected_return": 0.0}

        direction = 1.0 if trend_1h > 0 else -1.0
        strength = min(abs(trend_1h) * 15, 1.0)
        return {"expected_return": self.scaler.scale(direction * strength)}


# ═══════════════════════════════════════════════════════════════════════════════
# STRATEGY 2: Hourly Mean Reversion
# At 1h horizon, large deviations from the daily mean tend to revert.
# Use 24h SMA as the anchor, trade deviations > 1 ATR.
# ═══════════════════════════════════════════════════════════════════════════════

class HourlyMeanReversion(TrackerBase):
    """Revert to 24h mean when price deviates significantly."""

    def __init__(self):
        super().__init__()
        self.scaler = SignalScaler(target_risk=0.008)
        self.sma_24h = SMA(period=24)  # 24-hour SMA on 1h candles
        self.atr_1h = ATR(period=14)   # hourly ATR for threshold
        self.price = 0.0

    def tick(self, data):
        super().tick(data)
        for c in data.get("candles_1m", []):
            self.scaler.update(float(c["high"]), float(c["low"]), float(c["close"]))
        for c in data.get("candles_1h", []):
            p = float(c["close"])
            self.price = p
            self.sma_24h.update(p)
            self.atr_1h.update(float(c["high"]), float(c["low"]), p)

    def predict(self, symbol, horizon_seconds, step_seconds):
        if not (self.sma_24h.ready and self.atr_1h.ready
                and self.scaler.ready and self.atr_1h.value > 0):
            return {"expected_return": 0.0}

        deviation = self.price - self.sma_24h.value
        atr_units = deviation / self.atr_1h.value

        # Only trade when deviation > 1 ATR
        if abs(atr_units) < 1.0:
            return {"expected_return": 0.0}

        # Fade the move — strength increases with deviation
        raw = -atr_units / 4.0  # 2 ATR → raw=0.5
        raw = max(-1.0, min(1.0, raw))

        return {"expected_return": self.scaler.scale(raw)}


# ═══════════════════════════════════════════════════════════════════════════════
# STRATEGY 3: Funding Rate Contrarian
# Funding rates are 8h cycles. Extreme funding → crowded trade → reversion.
# At 1h horizon we can capture the beginning of the unwind.
# (Simulated from price momentum as proxy — real funding in production)
# ═══════════════════════════════════════════════════════════════════════════════

class FundingProxy(TrackerBase):
    """Contrarian signal based on simulated funding pressure.

    In backtest, we proxy funding rate with:
    - Short-term momentum (last 8h) as "funding pressure"
    - Mean of 1h returns as "crowdedness"
    Extreme positive momentum → longs crowded → fade.
    """

    def __init__(self):
        super().__init__()
        self.scaler = SignalScaler(target_risk=0.006)
        self.returns_8h = Returns(period=8)  # 8-hour return on 1h candles
        self.returns_1h_hist = deque(maxlen=24)  # last 24 hours of 1h returns
        self.prev_1h_price = None

    def tick(self, data):
        super().tick(data)
        for c in data.get("candles_1m", []):
            self.scaler.update(float(c["high"]), float(c["low"]), float(c["close"]))
        for c in data.get("candles_1h", []):
            p = float(c["close"])
            self.returns_8h.update(p)
            if self.prev_1h_price is not None and self.prev_1h_price > 0:
                self.returns_1h_hist.append((p - self.prev_1h_price) / self.prev_1h_price)
            self.prev_1h_price = p

        # Use actual funding if available
        fund = data.get("funding")
        if fund and fund.get("funding_rate") is not None:
            self._real_funding = fund["funding_rate"]
        else:
            self._real_funding = None

    def predict(self, symbol, horizon_seconds, step_seconds):
        if not self.scaler.ready:
            return {"expected_return": 0.0}

        # Use real funding rate if available
        if self._real_funding is not None:
            rate = self._real_funding
            # Extreme funding → fade
            if abs(rate) < 0.0001:  # normal range
                return {"expected_return": 0.0}
            raw = -rate * 3000  # 0.001 funding → raw=-3 → clamp to -1
            raw = max(-1.0, min(1.0, raw))
            return {"expected_return": self.scaler.scale(raw)}

        # Proxy: use 8h momentum as funding pressure indicator
        if not self.returns_8h.ready or len(self.returns_1h_hist) < 12:
            return {"expected_return": 0.0}

        mom_8h = self.returns_8h.value
        # Count how many of last 12 hours were in the same direction
        recent = list(self.returns_1h_hist)[-12:]
        if mom_8h > 0:
            streak = sum(1 for r in recent if r > 0) / len(recent)
        else:
            streak = sum(1 for r in recent if r < 0) / len(recent)

        # Only fade when move is extended AND one-sided
        if abs(mom_8h) < 0.005 or streak < 0.7:
            return {"expected_return": 0.0}

        raw = -mom_8h * 20  # 1% 8h move → raw=-0.2
        raw = max(-1.0, min(1.0, raw))

        return {"expected_return": self.scaler.scale(raw)}


# ═══════════════════════════════════════════════════════════════════════════════
# STRATEGY 4: Volatility Regime Adaptive
# Detect volatility regime from ATR, then apply the right strategy:
# High vol → mean reversion dominates
# Low vol → momentum dominates
# ═══════════════════════════════════════════════════════════════════════════════

class VolRegimeAdaptive(TrackerBase):
    """Switch between momentum and mean-reversion based on volatility regime."""

    def __init__(self):
        super().__init__()
        self.scaler = SignalScaler(target_risk=0.007)
        # Regime detection
        self.atr_1h = ATR(period=14)
        self.atr_sma = SMA(period=48)  # 48h average ATR
        # Momentum components
        self.ema_1h_fast = EMA(period=5)
        self.ema_1h_slow = EMA(period=13)
        # Mean reversion components
        self.sma_24h = SMA(period=24)
        self.price = 0.0

    def tick(self, data):
        super().tick(data)
        for c in data.get("candles_1m", []):
            self.scaler.update(float(c["high"]), float(c["low"]), float(c["close"]))
        for c in data.get("candles_1h", []):
            p = float(c["close"])
            self.price = p
            self.atr_1h.update(float(c["high"]), float(c["low"]), p)
            self.ema_1h_fast.update(p)
            self.ema_1h_slow.update(p)
            self.sma_24h.update(p)
            if self.atr_1h.ready:
                self.atr_sma.update(self.atr_1h.value)

    def predict(self, symbol, horizon_seconds, step_seconds):
        if not (self.atr_1h.ready and self.atr_sma.ready
                and self.ema_1h_fast.ready and self.sma_24h.ready
                and self.scaler.ready and self.atr_sma.value > 0):
            return {"expected_return": 0.0}

        vol_ratio = self.atr_1h.value / self.atr_sma.value

        if vol_ratio > 1.3:
            # HIGH VOL: mean reversion
            if self.atr_1h.value > 0:
                deviation = (self.price - self.sma_24h.value) / self.atr_1h.value
                if abs(deviation) < 0.5:
                    return {"expected_return": 0.0}
                raw = -deviation / 4.0
            else:
                return {"expected_return": 0.0}
        else:
            # LOW VOL: momentum
            trend = (self.ema_1h_fast.value - self.ema_1h_slow.value) / self.ema_1h_slow.value
            if abs(trend) < 0.001:
                return {"expected_return": 0.0}
            direction = 1.0 if trend > 0 else -1.0
            raw = direction * min(abs(trend) * 15, 1.0)

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


# ═══════════════════════════════════════════════════════════════════════════════
# STRATEGY 5: MACD Hourly
# Classic MACD on 1h candles — histogram crossover with signal confirmation.
# At 1h horizon, MACD works much better than at 5min.
# ═══════════════════════════════════════════════════════════════════════════════

class MACDHourly(TrackerBase):
    """MACD histogram momentum on hourly candles."""

    def __init__(self):
        super().__init__()
        self.scaler = SignalScaler(target_risk=0.007)
        self.macd = MACD(fast=12, slow=26, signal=9)  # classic params on 1h
        self.prev_hist = 0.0

    def tick(self, data):
        super().tick(data)
        for c in data.get("candles_1m", []):
            self.scaler.update(float(c["high"]), float(c["low"]), float(c["close"]))
        for c in data.get("candles_1h", []):
            self.macd.update(float(c["close"]))

    def predict(self, symbol, horizon_seconds, step_seconds):
        if not (self.macd.ready and self.scaler.ready):
            return {"expected_return": 0.0}

        hist = self.macd.value.histogram
        macd_line = self.macd.value.macd

        # Signal: histogram direction + acceleration
        hist_accel = hist - self.prev_hist
        self.prev_hist = hist

        # Only trade on fresh crossovers or strong momentum
        if abs(hist) < 1e-6:
            return {"expected_return": 0.0}

        direction = 1.0 if hist > 0 else -1.0

        # Stronger signal when histogram is accelerating
        if direction * hist_accel > 0:
            strength = 0.8
        else:
            strength = 0.3

        return {"expected_return": self.scaler.scale(direction * strength)}


# ═══════════════════════════════════════════════════════════════════════════════
# STRATEGY 6: Bollinger + RSI Combo (1h)
# ═══════════════════════════════════════════════════════════════════════════════

class BollingerRSI1h(TrackerBase):
    """Bollinger Band extremes confirmed by RSI on 1h candles."""

    def __init__(self):
        super().__init__()
        self.scaler = SignalScaler(target_risk=0.008)
        self.bb = BollingerBands(period=20, num_std=2.0)
        self.rsi = RSI(period=14)

    def tick(self, data):
        super().tick(data)
        for c in data.get("candles_1m", []):
            self.scaler.update(float(c["high"]), float(c["low"]), float(c["close"]))
        for c in data.get("candles_1h", []):
            p = float(c["close"])
            self.bb.update(p)
            self.rsi.update(p)

    def predict(self, symbol, horizon_seconds, step_seconds):
        if not (self.bb.ready and self.rsi.ready and self.scaler.ready):
            return {"expected_return": 0.0}

        pct_b = self.bb.value.percent_b
        rsi = self.rsi.value

        # Oversold: price near lower band + RSI < 30
        if pct_b < 0.1 and rsi < 35:
            strength = (35 - rsi) / 35 * (0.1 - pct_b) / 0.1
            return {"expected_return": self.scaler.scale(min(strength * 4, 1.0))}

        # Overbought: price near upper band + RSI > 70
        if pct_b > 0.9 and rsi > 65:
            strength = (rsi - 65) / 35 * (pct_b - 0.9) / 0.1
            return {"expected_return": self.scaler.scale(-min(strength * 4, 1.0))}

        return {"expected_return": 0.0}


# ═══════════════════════════════════════════════════════════════════════════════
# ENSEMBLE: Top strategies weighted by rolling IC
# ═══════════════════════════════════════════════════════════════════════════════

class Ensemble1h(TrackerBase):
    """Performance-weighted ensemble for 1h horizon."""

    def __init__(self):
        super().__init__()
        self.strategies = {
            "session_mom": SessionMomentum(),
            "hourly_mr": HourlyMeanReversion(),
            "funding": FundingProxy(),
            "vol_regime": VolRegimeAdaptive(),
            "macd_1h": MACDHourly(),
            "bb_rsi": BollingerRSI1h(),
        }
        self.signal_history = {n: deque(maxlen=200) for n in self.strategies}
        self.actual_returns = deque(maxlen=200)
        self.prices_15m = deque(maxlen=8)  # last 2h of 15m prices

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

    def predict(self, symbol, horizon_seconds, step_seconds):
        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 approximate actuals (4 bars of 15m ≈ 1h)
        if len(self.prices_15m) >= 5:
            actual = (self.prices_15m[-1] - self.prices_15m[-5]) / self.prices_15m[-5]
            self.actual_returns.append(actual)

        weights = self._compute_weights()
        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
        return {"expected_return": weighted}

    def _compute_weights(self):
        if len(self.actual_returns) < 30:
            return {n: 1.0 for n 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 = _corr(hist[-n:], actuals[-n:])
            weights[name] = max(ic, 0.0)
        return weights


def _corr(x, y):
    n = len(x)
    if n < 2: return 0.0
    mx, my = sum(x)/n, 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)
    return cov/(sx*sy) if sx > 1e-12 and sy > 1e-12 else 0.0


# ═══════════════════════════════════════════════════════════════════════════════
# v1 strategies adapted to 1h horizon for comparison
# ═══════════════════════════════════════════════════════════════════════════════

from vanta.examples.mtf_trend_tracker import MTFTrendTracker
from vanta.examples.extreme_mean_reversion_tracker import ExtremeMeanReversionTracker


# ═══════════════════════════════════════════════════════════════════════════════
# RUNNER
# ═══════════════════════════════════════════════════════════════════════════════

def run():
    strategies = [
        # New 1h-native strategies
        ("Session_Momentum", SessionMomentum()),
        ("Hourly_MeanRev", HourlyMeanReversion()),
        ("Funding_Proxy", FundingProxy()),
        ("Vol_Regime", VolRegimeAdaptive()),
        ("MACD_Hourly", MACDHourly()),
        ("BB_RSI_1h", BollingerRSI1h()),
        ("Ensemble_1h", Ensemble1h()),
        # v1 strategies (designed for 5min, now on 1h — do they transfer?)
        ("v1_MTF_Trend", MTFTrendTracker()),
        ("v1_Extreme_MR", ExtremeMeanReversionTracker()),
    ]

    configs = [
        ("BTCUSDT", "IN-SAMPLE  Jul-Oct", "2025-07-01", "2025-11-01"),
        ("BTCUSDT", "OUT-SAMPLE Nov-Dec", "2025-11-01", "2026-01-01"),
        ("ETHUSDT", "IN-SAMPLE  Jul-Oct", "2025-07-01", "2025-11-01"),
        ("ETHUSDT", "OUT-SAMPLE Nov-Dec", "2025-11-01", "2026-01-01"),
    ]

    for subject, label, start, end in configs:
        print(f"\n{'='*100}")
        print(f"  {subject} | {label} | 1h horizon, predict every 15min")
        print(f"{'='*100}")

        results = {}
        for name, model_template in strategies:
            # Fresh instance
            model = type(model_template)()
            t0 = time.time()
            try:
                result = BacktestRunner(model=model).run(
                    subject=subject, start=start, end=end,
                    window_size=120, prediction_interval_seconds=900,
                    horizon_seconds=3600,
                )
                elapsed = time.time() - t0
                results[name] = result
                m = result.metrics
                print(f"  ✓ {name:22s} {elapsed:5.1f}s  IC={m.get('ic',0):+.4f}  "
                      f"hit={m.get('hit_rate',0):.3f}  sortino={m.get('sortino_ratio',0):+.4f}  "
                      f"dd={m.get('max_drawdown',0):+.4f}")
            except Exception as e:
                print(f"  ✗ {name:22s}  ERROR: {e}")

        if results:
            print(f"\n  Leaderboard (by IC):")
            for rank, name in enumerate(sorted(results, key=lambda n: results[n].metrics.get("ic", 0), reverse=True), 1):
                m = results[name].metrics
                print(f"    {rank:2d}. {name:22s}  IC={m.get('ic',0):+.4f}  Sortino={m.get('sortino_ratio',0):+.4f}")


if __name__ == "__main__":
    run()
