# source: https://raw.githubusercontent.com/ubaidzafar05/nexus-alpha/6b5ecf9914e70e119538adbb5110a684589649a7/freqtrade/strategies/NexusAlphaStrategy.py
"""
NexusAlpha Freqtrade Strategy — Free Edition.

Combines:
  - Multi-timeframe trend following (EMA crossover + ADX)
  - Mean reversion (Bollinger Band + RSI)
  - Regime detection (Hurst Exponent + ADX)
  - Order book imbalance gate (Binance REST — free)
  - Kelly criterion position sizing
  - Dynamic ATR trailing stoploss
  - Sentiment gate from NEXUS-ALPHA free pipeline (Redis cache)

FreqAI variant (NexusAlphaMLStrategy) adds:
  - LightGBM directional classifier trained on rolling 90-day window
  - Walk-forward cross-validation via Freqtrade Hyperopt

Usage:
  freqtrade trade --strategy Github_ubaidzafar05_nexus_alpha__NexusAlphaStrategy__20260427_172833 --config config/config.json
  freqtrade backtesting --strategy Github_ubaidzafar05_nexus_alpha__NexusAlphaStrategy__20260427_172833 --timerange 20230101-20251231
  freqtrade hyperopt --strategy NexusAlphaMLStrategy --hyperopt-loss SharpeHyperOptLoss --epochs 500
"""

from __future__ import annotations

import os
from datetime import datetime

import numpy as np
import pandas as pd
import requests
import talib.abstract as ta

from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter


class Github_ubaidzafar05_nexus_alpha__NexusAlphaStrategy__20260427_172833(IStrategy):
    """
    NEXUS-ALPHA Free Edition — production Freqtrade strategy.
    All signal logic is self-contained; no paid data sources required.
    """

    INTERFACE_VERSION = 3
    timeframe = "1h"

    # Multi-timeframe analysis
    informative_timeframes: list[str] = ["4h", "1d"]

    # Risk management
    minimal_roi = {
        "0": 0.08,
        "30": 0.05,
        "60": 0.03,
        "120": 0.01,
    }

    stoploss = -0.05
    use_custom_stoploss = True
    trailing_stop = False

    stake_currency = "USDT"
    stake_amount = "unlimited"
    max_open_trades = 5

    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # ── Hyperopt search space ─────────────────────────────────────────────────
    ema_fast = IntParameter(8, 30, default=21, space="buy")
    ema_slow = IntParameter(40, 80, default=55, space="buy")
    rsi_oversold = IntParameter(20, 40, default=30, space="buy")
    rsi_overbought = IntParameter(60, 80, default=70, space="sell")
    atr_multiplier = DecimalParameter(1.5, 3.5, default=2.5, space="sell")
    sentiment_threshold = DecimalParameter(0.2, 0.7, default=0.4, space="buy")

    # ── Sentiment cache (written by nexus-alpha intelligence pipeline) ────────
    _REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
    _sentiment_cache: dict[str, float] = {}

    @staticmethod
    def _env_flag(name: str, default: bool = True) -> bool:
        value = os.getenv(name)
        if value is None:
            return default
        return value.strip().lower() not in {"0", "false", "no", "off", ""}

    def _trend_entries_enabled(self) -> bool:
        return self._env_flag("NEXUS_ENABLE_TREND", True)

    def _mean_reversion_entries_enabled(self) -> bool:
        return self._env_flag("NEXUS_ENABLE_MEAN_REVERSION", True)

    def _regime_filter_enabled(self) -> bool:
        return self._env_flag("NEXUS_ENABLE_REGIME_FILTER", True)

    def _get_sentiment(self, base_asset: str) -> float:
        """Pull pre-computed sentiment from Redis (set by HybridSentimentPipeline)."""
        try:
            import redis

            r = redis.from_url(self._REDIS_URL, decode_responses=True)
            value = r.get(f"sentiment:{base_asset.upper()}")
            return float(value) if value else 0.0
        except Exception:
            return self._sentiment_cache.get(base_asset, 0.0)

    @staticmethod
    def _hurst_exponent(price_series: np.ndarray) -> float:
        if len(price_series) < 20:
            return 0.5
        try:
            lags = range(2, min(20, len(price_series) // 2))
            tau = [
                np.std(np.subtract(price_series[lag:], price_series[:-lag]))
                for lag in lags
            ]
            poly = np.polyfit(np.log(lags), np.log(tau), 1)
            return float(poly[0])
        except Exception:
            return 0.5

    def _get_ob_imbalance(self, pair: str) -> float:
        """Order book imbalance from Binance REST — free, no API key required."""
        try:
            symbol = pair.replace("/", "")
            resp = requests.get(
                "https://api.binance.com/api/v3/depth",
                params={"symbol": symbol, "limit": 20},
                timeout=2,
            )
            ob = resp.json()
            bid_vol = sum(float(b[1]) for b in ob["bids"][:10])
            ask_vol = sum(float(a[1]) for a in ob["asks"][:10])
            total = bid_vol + ask_vol
            return (bid_vol - ask_vol) / total if total > 0 else 0.0
        except Exception:
            return 0.0

    # ── Indicator computation ─────────────────────────────────────────────────

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        pair = metadata["pair"]
        base_asset = pair.split("/")[0]

        # Trend
        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=self.ema_fast.value)
        dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=self.ema_slow.value)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)

        # Momentum
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"] = macd["macdhist"]

        # Volatility
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
        dataframe["bb_upper"] = bb["upperband"]
        dataframe["bb_lower"] = bb["lowerband"]
        dataframe["bb_mid"] = bb["middleband"]
        dataframe["bb_pct"] = (dataframe["close"] - bb["lowerband"]) / (
            bb["upperband"] - bb["lowerband"] + 1e-8
        )

        # Volume
        dataframe["obv"] = ta.OBV(dataframe)
        dataframe["volume_zscore"] = (
            dataframe["volume"] - dataframe["volume"].rolling(20).mean()
        ) / (dataframe["volume"].rolling(20).std() + 1e-8)

        # Regime detection
        dataframe["hurst"] = dataframe["close"].rolling(100).apply(
            self._hurst_exponent, raw=True
        )
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)

        dataframe["regime"] = "unknown"
        dataframe.loc[
            (dataframe["adx"] > 25) & (dataframe["hurst"] > 0.55), "regime"
        ] = "trending"
        dataframe.loc[
            (dataframe["adx"] < 20) & (dataframe["hurst"] < 0.45), "regime"
        ] = "mean_reverting"
        dataframe.loc[
            dataframe["atr_pct"] > dataframe["atr_pct"].rolling(30).mean() * 1.5,
            "regime",
        ] = "high_vol"

        # External signals (live mode only — skip in backtesting)
        if self.dp.runmode.value not in ("backtest", "hyperopt"):
            dataframe["sentiment"] = self._get_sentiment(base_asset)
            dataframe["ob_imbalance"] = self._get_ob_imbalance(pair)
        else:
            dataframe["sentiment"] = 0.0
            dataframe["ob_imbalance"] = 0.0

        return dataframe

    # ── Entry signal ──────────────────────────────────────────────────────────

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        all_rows = pd.Series(True, index=dataframe.index, dtype=bool)
        trend_regime_ok = (dataframe["regime"] == "trending") if self._regime_filter_enabled() else all_rows
        mean_rev_regime_ok = (dataframe["regime"] == "mean_reverting") if self._regime_filter_enabled() else all_rows

        trend_long = (
            trend_regime_ok
            & (dataframe["ema_fast"] > dataframe["ema_slow"])
            & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
            & (dataframe["adx"] > 25)
            & (dataframe["volume_zscore"] > 0.5)
            & (dataframe["close"] > dataframe["ema_200"])
        )

        mean_rev_long = (
            mean_rev_regime_ok
            & (dataframe["bb_pct"] < 0.1)
            & (dataframe["rsi"] < self.rsi_oversold.value)
            & (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
        )

        if not self._trend_entries_enabled():
            trend_long = pd.Series(False, index=dataframe.index, dtype=bool)
        if not self._mean_reversion_entries_enabled():
            mean_rev_long = pd.Series(False, index=dataframe.index, dtype=bool)

        sentiment_ok = dataframe["sentiment"] > -self.sentiment_threshold.value
        ob_ok = dataframe["ob_imbalance"] > -0.3

        dataframe.loc[
            (trend_long | mean_rev_long) & sentiment_ok & ob_ok,
            "enter_long",
        ] = 1

        return dataframe

    # ── Exit signal ───────────────────────────────────────────────────────────

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe.loc[
            (
                (dataframe["ema_fast"] < dataframe["ema_slow"])
                | (dataframe["rsi"] > self.rsi_overbought.value)
                | (dataframe["regime"] == "high_vol")
            ),
            "exit_long",
        ] = 1
        return dataframe

    # ── Dynamic ATR stoploss ──────────────────────────────────────────────────

    def custom_stoploss(
        self,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(kwargs["pair"], self.timeframe)
        if dataframe.empty:
            return self.stoploss
        last = dataframe.iloc[-1]
        atr_stop = -(last["atr"] * self.atr_multiplier.value) / current_rate
        return max(atr_stop, self.stoploss)

    # ── Kelly position sizing ─────────────────────────────────────────────────

    def custom_stake_amount(
        self,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        entry_tag: str,
        **kwargs,
    ) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(kwargs["pair"], self.timeframe)
        if dataframe.empty:
            return proposed_stake * 0.1

        win_rate = 0.55
        win_loss_ratio = 1.5
        kelly = win_rate - (1 - win_rate) / win_loss_ratio
        fractional_kelly = kelly * 0.25  # Quarter Kelly — conservative

        regime = dataframe.iloc[-1]["regime"]
        regime_mult = {
            "trending": 1.0,
            "mean_reverting": 0.7,
            "high_vol": 0.3,
            "unknown": 0.5,
        }.get(regime, 0.5)

        atr_pct = dataframe.iloc[-1]["atr_pct"]
        target_vol = 0.02
        vol_scale = min(target_vol / max(atr_pct, 0.001), 1.0)

        nav = self.wallets.get_total_stake_amount()
        final = nav * fractional_kelly * regime_mult * vol_scale

        max_size = nav * 0.20
        min_size = 10.0
        return max(min_size, min(final, max_size))


# ── FreqAI variant ────────────────────────────────────────────────────────────

class NexusAlphaMLStrategy(Github_ubaidzafar05_nexus_alpha__NexusAlphaStrategy__20260427_172833):
    """
    NEXUS-ALPHA ML Edition — adds FreqAI LightGBM classifier.

    FreqAI is built into Freqtrade (no extra cost).
    Trains a directional classifier on rolling 90-day windows with
    walk-forward re-training every 4 hours.
    """

    def feature_engineering_expand_all(
        self,
        dataframe: pd.DataFrame,
        period: int,
        **kwargs,
    ) -> pd.DataFrame:
        dataframe[f"%-hurst-period_{period}"] = dataframe["close"].rolling(100).apply(
            self._hurst_exponent, raw=True
        )
        dataframe[f"%-volume_zscore-period_{period}"] = (
            dataframe["volume"] - dataframe["volume"].rolling(period).mean()
        ) / (dataframe["volume"].rolling(period).std() + 1e-8)
        return dataframe

    def feature_engineering_standard(
        self,
        dataframe: pd.DataFrame,
        **kwargs,
    ) -> pd.DataFrame:
        pair = kwargs.get("metadata", {}).get("pair", "")
        base = pair.split("/")[0] if pair else ""
        dataframe["%-sentiment"] = self._get_sentiment(base) if base else 0.0
        dataframe["%-ob_imbalance"] = 0.0  # Live-only; filled by populate_indicators
        return dataframe

    def set_freqai_targets(self, dataframe: pd.DataFrame, **kwargs) -> pd.DataFrame:
        dataframe["&-direction"] = np.where(
            dataframe["close"].shift(-24) > dataframe["close"] * 1.02,
            "long",
            np.where(
                dataframe["close"].shift(-24) < dataframe["close"] * 0.98,
                "short",
                "neutral",
            ),
        )
        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # Guard: do_predict column is only present when FreqAI is active
        freqai_active = "do_predict" in dataframe.columns

        ml_signal = (
            (dataframe["&-direction"] == "long")
            & (dataframe["do_predict"] == 1)
        ) if freqai_active else (
            # Fallback to base strategy logic when FreqAI not yet trained
            (dataframe["ema_fast"] > dataframe["ema_slow"])
            & (dataframe["rsi"] < 50)
        )

        dataframe.loc[
            ml_signal
            & (dataframe["sentiment"] > -self.sentiment_threshold.value),
            "enter_long",
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe.loc[
            (dataframe["&-direction"] == "short")
            | (dataframe["regime"] == "high_vol"),
            "exit_long",
        ] = 1
        return dataframe
