# source: https://raw.githubusercontent.com/aldrinmagno/FreqtradeStrategies/3d9c1b176faceca3c950f99c3c55c8a7abe77275/user_data/strategies/NostalgiaForInfinityX6.py
"""
Github_aldrinmagno_FreqtradeStrategies__NostalgiaForInfinityX6__20260317_150802 — Institutional-Grade Multi-Signal System (Simplified)

Source: https://github.com/iterativv/NostalgiaForInfinity (simplified adaptation)
Timeframe: 5m (informative: 15m, 1h, 1d)
Description: Faithful simplified version of the NostalgiaForInfinityX architecture.
             Features 8 distinct buy conditions, 5 sell conditions, derisking system,
             and multi-timeframe indicator analysis.

Blacklisted leveraged tokens:
    *UP/USDT, *DOWN/USDT, *BULL/USDT, *BEAR/USDT, *2L/USDT, *2S/USDT,
    *3L/USDT, *3S/USDT, *4L/USDT, *4S/USDT, *5L/USDT, *5S/USDT
    These should be excluded via pair_blacklist in config.
"""

from functools import reduce
from freqtrade.strategy import (
    IStrategy,
    BooleanParameter,
    DecimalParameter,
    IntParameter,
    merge_informative_pair,
)
from pandas import DataFrame
import numpy as np
import pandas_ta as pta


class Github_aldrinmagno_FreqtradeStrategies__NostalgiaForInfinityX6__20260317_150802(IStrategy):

    INTERFACE_VERSION: int = 3

    timeframe: str = "5m"

    # Fully signal-driven
    minimal_roi: dict = {"0": 100}

    stoploss: float = -0.99

    use_exit_signal: bool = True
    exit_profit_only: bool = False
    ignore_roi_if_entry_signal: bool = True

    # Informative timeframes
    inf_timeframes: list = ["15m", "1h", "1d"]

    # -----------------------------------------------------------------------
    # Buy condition toggles
    # -----------------------------------------------------------------------
    buy_cond_1_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_2_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_3_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_4_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_5_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_6_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_7_enabled = BooleanParameter(default=True, space="buy")
    buy_cond_8_enabled = BooleanParameter(default=True, space="buy")

    # -----------------------------------------------------------------------
    # Buy hyperopt parameters — per condition
    # -----------------------------------------------------------------------
    # Condition 1 — EMA crossover + RSI
    buy_1_rsi = IntParameter(25, 50, default=40, space="buy")

    # Condition 2 — BB lower band bounce + volume
    buy_2_rsi = IntParameter(20, 40, default=30, space="buy")
    buy_2_mfi = IntParameter(15, 40, default=30, space="buy")

    # Condition 3 — Williams %R oversold
    buy_3_willr = IntParameter(-99, -70, default=-85, space="buy")
    buy_3_rsi = IntParameter(20, 45, default=35, space="buy")

    # Condition 4 — VWAP dip
    buy_4_vwap_factor = DecimalParameter(0.96, 1.0, default=0.99, space="buy")
    buy_4_rsi = IntParameter(20, 45, default=35, space="buy")
    buy_4_rsi_1h_min = IntParameter(30, 55, default=40, space="buy")

    # Condition 5 — 1h StochRSI oversold
    buy_5_stochrsi_k = IntParameter(10, 35, default=20, space="buy")
    buy_5_stochrsi_d = IntParameter(10, 35, default=20, space="buy")
    buy_5_rsi = IntParameter(20, 45, default=35, space="buy")

    # Condition 6 — EMA 50/200 golden zone
    buy_6_rsi = IntParameter(15, 40, default=30, space="buy")
    buy_6_willr_1h = IntParameter(-99, -60, default=-75, space="buy")

    # Condition 7 — BB squeeze
    buy_7_bb_width = DecimalParameter(0.02, 0.08, default=0.04, space="buy")
    buy_7_rsi = IntParameter(20, 42, default=32, space="buy")
    buy_7_rsi_1h_min = IntParameter(35, 60, default=45, space="buy")

    # Condition 8 — Multi-TF RSI alignment
    buy_8_rsi = IntParameter(15, 38, default=28, space="buy")
    buy_8_rsi_15m = IntParameter(20, 45, default=35, space="buy")
    buy_8_rsi_1h = IntParameter(30, 55, default=45, space="buy")

    # -----------------------------------------------------------------------
    # Sell hyperopt parameters
    # -----------------------------------------------------------------------
    # Condition 1 — RSI overbought
    sell_1_rsi = IntParameter(60, 85, default=70, space="sell")

    # Condition 2 — Williams %R overbought
    sell_2_willr = IntParameter(-20, -3, default=-10, space="sell")
    sell_2_rsi = IntParameter(55, 80, default=65, space="sell")

    # Condition 3 — EMA bearish crossover
    sell_3_rsi = IntParameter(50, 75, default=60, space="sell")

    # Condition 4 — MFI overbought
    sell_4_mfi = IntParameter(70, 95, default=80, space="sell")
    sell_4_bb_factor = DecimalParameter(0.97, 1.01, default=0.99, space="sell")

    # Condition 5 — 1h StochRSI overbought
    sell_5_stochrsi = IntParameter(65, 95, default=80, space="sell")
    sell_5_rsi = IntParameter(55, 80, default=65, space="sell")

    # Derisking threshold
    derisk_profit_threshold = DecimalParameter(-0.08, -0.01, default=-0.03, space="sell")

    def informative_pairs(self) -> list:
        pairs = self.dp.current_whitelist()
        informative_pairs = []
        for tf in self.inf_timeframes:
            informative_pairs.extend([(pair, tf) for pair in pairs])
        return informative_pairs

    def _stochastic_rsi(
        self,
        dataframe: DataFrame,
        rsi_length: int = 14,
        stoch_length: int = 14,
        smooth_k: int = 3,
        smooth_d: int = 3,
    ) -> tuple:
        """Stochastic RSI indicator."""
        rsi = pta.rsi(dataframe["close"], length=rsi_length)
        stoch_rsi_k = (
            (rsi - rsi.rolling(window=stoch_length).min())
            / (rsi.rolling(window=stoch_length).max() - rsi.rolling(window=stoch_length).min())
        ) * 100
        stoch_rsi_k = stoch_rsi_k.rolling(window=smooth_k).mean()
        stoch_rsi_d = stoch_rsi_k.rolling(window=smooth_d).mean()
        return stoch_rsi_k, stoch_rsi_d

    def _williams_r(self, dataframe: DataFrame, length: int = 14) -> "Series":
        """Williams %R indicator."""
        highest_high = dataframe["high"].rolling(window=length).max()
        lowest_low = dataframe["low"].rolling(window=length).min()
        wr = ((highest_high - dataframe["close"]) / (highest_high - lowest_low)) * -100
        return wr

    def _compute_vwap(self, dataframe: DataFrame) -> "Series":
        """Compute VWAP (Volume Weighted Average Price)."""
        typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
        cumulative_tp_vol = (typical_price * dataframe["volume"]).cumsum()
        cumulative_vol = dataframe["volume"].cumsum()
        return cumulative_tp_vol / cumulative_vol

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # =====================================================================
        # 5m indicators
        # =====================================================================
        # EMAs
        for length in [5, 8, 13, 21, 50, 200]:
            dataframe[f"ema_{length}"] = pta.ema(dataframe["close"], length=length)

        # RSI
        dataframe["rsi_14"] = pta.rsi(dataframe["close"], length=14)

        # Bollinger Bands
        bb = pta.bbands(dataframe["close"], length=20, std=2.0)
        dataframe["bb_lowerband"] = bb["BBL_20_2.0"]
        dataframe["bb_middleband"] = bb["BBM_20_2.0"]
        dataframe["bb_upperband"] = bb["BBU_20_2.0"]
        dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]

        # Williams %R
        dataframe["willr_14"] = self._williams_r(dataframe, length=14)

        # VWAP
        dataframe["vwap"] = self._compute_vwap(dataframe)

        # MFI
        dataframe["mfi_14"] = pta.mfi(
            dataframe["high"], dataframe["low"], dataframe["close"], dataframe["volume"], length=14
        )

        # SMA 200 for trend
        dataframe["sma_200"] = pta.sma(dataframe["close"], length=200)

        # =====================================================================
        # 15m informative
        # =====================================================================
        inf_15m = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="15m")
        inf_15m["rsi_14_15m"] = pta.rsi(inf_15m["close"], length=14)
        dataframe = merge_informative_pair(dataframe, inf_15m, self.timeframe, "15m", ffill=True)

        # =====================================================================
        # 1h informative
        # =====================================================================
        inf_1h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="1h")
        inf_1h["rsi_14_1h"] = pta.rsi(inf_1h["close"], length=14)
        for length in [5, 8, 13, 21, 50, 200]:
            inf_1h[f"ema_{length}_1h"] = pta.ema(inf_1h["close"], length=length)
        inf_1h["willr_14_1h"] = self._williams_r(inf_1h, length=14)
        stoch_k, stoch_d = self._stochastic_rsi(inf_1h)
        inf_1h["stochrsi_k_1h"] = stoch_k
        inf_1h["stochrsi_d_1h"] = stoch_d
        dataframe = merge_informative_pair(dataframe, inf_1h, self.timeframe, "1h", ffill=True)

        # =====================================================================
        # 1d informative
        # =====================================================================
        inf_1d = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="1d")
        inf_1d["ema_200_1d"] = pta.ema(inf_1d["close"], length=200)
        inf_1d["rsi_14_1d"] = pta.rsi(inf_1d["close"], length=14)
        dataframe = merge_informative_pair(dataframe, inf_1d, self.timeframe, "1d", ffill=True)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions: list = []

        # -----------------------------------------------------------------
        # Buy condition 1 — EMA crossover with RSI confirmation
        # -----------------------------------------------------------------
        if self.buy_cond_1_enabled.value:
            buy_cond_1 = (
                (dataframe["ema_5"] > dataframe["ema_13"])
                & (dataframe["ema_13"] > dataframe["ema_21"])
                & (dataframe["rsi_14"] < self.buy_1_rsi.value)
                & (dataframe["close"] < dataframe["bb_lowerband"])
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_1)

        # -----------------------------------------------------------------
        # Buy condition 2 — BB lower band bounce with volume
        # -----------------------------------------------------------------
        if self.buy_cond_2_enabled.value:
            buy_cond_2 = (
                (dataframe["close"] < dataframe["bb_lowerband"])
                & (dataframe["rsi_14"] < self.buy_2_rsi.value)
                & (dataframe["mfi_14"] < self.buy_2_mfi.value)
                & (dataframe["volume"] > dataframe["volume"].shift(1))
            )
            conditions.append(buy_cond_2)

        # -----------------------------------------------------------------
        # Buy condition 3 — Williams %R oversold with EMA support
        # -----------------------------------------------------------------
        if self.buy_cond_3_enabled.value:
            buy_cond_3 = (
                (dataframe["willr_14"] < self.buy_3_willr.value)
                & (dataframe["close"] > dataframe["ema_200"])
                & (dataframe["rsi_14"] < self.buy_3_rsi.value)
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_3)

        # -----------------------------------------------------------------
        # Buy condition 4 — VWAP dip buy
        # -----------------------------------------------------------------
        if self.buy_cond_4_enabled.value:
            buy_cond_4 = (
                (dataframe["close"] < dataframe["vwap"] * self.buy_4_vwap_factor.value)
                & (dataframe["rsi_14"] < self.buy_4_rsi.value)
                & (dataframe[f"rsi_14_1h_1h"] > self.buy_4_rsi_1h_min.value)
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_4)

        # -----------------------------------------------------------------
        # Buy condition 5 — 1h Stochastic RSI oversold
        # -----------------------------------------------------------------
        if self.buy_cond_5_enabled.value:
            buy_cond_5 = (
                (dataframe[f"stochrsi_k_1h_1h"] < self.buy_5_stochrsi_k.value)
                & (dataframe[f"stochrsi_d_1h_1h"] < self.buy_5_stochrsi_d.value)
                & (dataframe["rsi_14"] < self.buy_5_rsi.value)
                & (dataframe["close"] < dataframe["ema_50"])
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_5)

        # -----------------------------------------------------------------
        # Buy condition 6 — EMA 50/200 golden zone with RSI
        # -----------------------------------------------------------------
        if self.buy_cond_6_enabled.value:
            buy_cond_6 = (
                (dataframe["close"] < dataframe["ema_50"])
                & (dataframe["ema_50"] > dataframe["ema_200"])
                & (dataframe["rsi_14"] < self.buy_6_rsi.value)
                & (dataframe[f"willr_14_1h_1h"] < self.buy_6_willr_1h.value)
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_6)

        # -----------------------------------------------------------------
        # Buy condition 7 — BB squeeze with 1h RSI divergence
        # -----------------------------------------------------------------
        if self.buy_cond_7_enabled.value:
            buy_cond_7 = (
                (dataframe["bb_width"] < self.buy_7_bb_width.value)
                & (dataframe["close"] < dataframe["bb_lowerband"])
                & (dataframe[f"rsi_14_1h_1h"] > self.buy_7_rsi_1h_min.value)
                & (dataframe["rsi_14"] < self.buy_7_rsi.value)
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_7)

        # -----------------------------------------------------------------
        # Buy condition 8 — Multi-timeframe RSI alignment
        # -----------------------------------------------------------------
        if self.buy_cond_8_enabled.value:
            buy_cond_8 = (
                (dataframe["rsi_14"] < self.buy_8_rsi.value)
                & (dataframe[f"rsi_14_15m_15m"] < self.buy_8_rsi_15m.value)
                & (dataframe[f"rsi_14_1h_1h"] < self.buy_8_rsi_1h.value)
                & (dataframe["close"] < dataframe["sma_200"])
                & (dataframe["close"] < dataframe["bb_middleband"])
                & (dataframe["volume"] > 0)
            )
            conditions.append(buy_cond_8)

        # OR all conditions together
        if conditions:
            dataframe.loc[reduce(lambda x, y: x | y, conditions), "enter_long"] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions: list = []

        # -----------------------------------------------------------------
        # Sell condition 1 — RSI overbought
        # -----------------------------------------------------------------
        sell_cond_1 = (
            (dataframe["rsi_14"] > self.sell_1_rsi.value)
            & (dataframe["close"] > dataframe["bb_upperband"])
            & (dataframe["volume"] > 0)
        )
        conditions.append(sell_cond_1)

        # -----------------------------------------------------------------
        # Sell condition 2 — Williams %R overbought
        # -----------------------------------------------------------------
        sell_cond_2 = (
            (dataframe["willr_14"] > self.sell_2_willr.value)
            & (dataframe["rsi_14"] > self.sell_2_rsi.value)
            & (dataframe["volume"] > 0)
        )
        conditions.append(sell_cond_2)

        # -----------------------------------------------------------------
        # Sell condition 3 — EMA bearish crossover
        # -----------------------------------------------------------------
        sell_cond_3 = (
            (dataframe["ema_5"] < dataframe["ema_13"])
            & (dataframe["ema_13"] < dataframe["ema_21"])
            & (dataframe["rsi_14"] > self.sell_3_rsi.value)
            & (dataframe["volume"] > 0)
        )
        conditions.append(sell_cond_3)

        # -----------------------------------------------------------------
        # Sell condition 4 — MFI overbought with BB upper band
        # -----------------------------------------------------------------
        sell_cond_4 = (
            (dataframe["mfi_14"] > self.sell_4_mfi.value)
            & (dataframe["close"] > dataframe["bb_upperband"] * self.sell_4_bb_factor.value)
            & (dataframe["volume"] > 0)
        )
        conditions.append(sell_cond_4)

        # -----------------------------------------------------------------
        # Sell condition 5 — 1h Stochastic RSI overbought
        # -----------------------------------------------------------------
        sell_cond_5 = (
            (dataframe[f"stochrsi_k_1h_1h"] > self.sell_5_stochrsi.value)
            & (dataframe[f"stochrsi_d_1h_1h"] > self.sell_5_stochrsi.value)
            & (dataframe["rsi_14"] > self.sell_5_rsi.value)
            & (dataframe["volume"] > 0)
        )
        conditions.append(sell_cond_5)

        if conditions:
            dataframe.loc[reduce(lambda x, y: x | y, conditions), "exit_long"] = 1

        return dataframe

    def custom_exit(
        self,
        pair: str,
        trade: "Trade",
        current_time: "datetime",
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> str | bool:
        """
        Derisking system: if open profit drops below -3% AND the 1h EMA-21 < EMA-50
        (downtrend), trigger a sell.
        """
        if current_profit > self.derisk_profit_threshold.value:
            return False

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) < 1:
            return False

        last_candle = dataframe.iloc[-1]

        ema_21_1h = last_candle.get(f"ema_21_1h_1h", 0)
        ema_50_1h = last_candle.get(f"ema_50_1h_1h", 0)

        if ema_21_1h > 0 and ema_50_1h > 0 and ema_21_1h < ema_50_1h:
            return "derisk_1h_downtrend"

        return False

    def custom_stoploss(
        self,
        pair: str,
        trade: "Trade",
        current_time: "datetime",
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        """Exchange stoploss safety net at -0.12."""
        return -0.12
