# source: https://raw.githubusercontent.com/nicolasasauer/tradingbot-v8/e23da3fb0dc4b7b247e9ce4b97645b7916e2702f/user_data/strategies/SmartAlphaStrategy.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401

from datetime import datetime
from typing import Optional

import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame

from freqtrade.strategy import (
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IStrategy,
    IntParameter,
    merge_informative_pair,
)


class Github_nicolasasauer_tradingbot_v8__SmartAlphaStrategy__20260311_135647(IStrategy):
    """
    Github_nicolasasauer_tradingbot_v8__SmartAlphaStrategy__20260311_135647 – RSI + EMA Trend + Bollinger Bands

    Entry logic:
      - RSI is oversold (below buy_rsi threshold)
      - Price is above the EMA-200 (long-term uptrend filter)
      - EMA-20 is above EMA-50 (medium-term uptrend confirmation)
      - Price touches or crosses the lower Bollinger Band (mean-reversion entry)

    Exit logic:
      - RSI is overbought (above sell_rsi threshold)
      - Price crosses above the upper Bollinger Band
      - Trailing stop-loss as a safety net

    Protective mechanisms:
      - Hard stop-loss at 5 % below entry
      - Trailing stop-loss that kicks in after 2 % of profit
    """

    # ------------------------------------------------------------------
    # Strategy metadata
    # ------------------------------------------------------------------
    INTERFACE_VERSION = 3
    timeframe = "5m"
    can_short = False
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # ------------------------------------------------------------------
    # ROI – fallback exits (time-based)
    # ------------------------------------------------------------------
    minimal_roi = {
        "0": 0.03,    # 3 % at any time
        "15": 0.015,  # 1.5 % after 15 minutes
        "30": 0.01,   # 1 % after 30 minutes
        "60": 0.005,  # 0.5 % after 60 minutes
    }

    # ------------------------------------------------------------------
    # Stop-loss & trailing
    # ------------------------------------------------------------------
    stoploss = -0.05          # Hard stop: -5 %
    trailing_stop = True
    trailing_stop_positive = 0.02   # Trailing activates after +2 %
    trailing_stop_positive_offset = 0.03  # Trailing trails from +3 %
    trailing_only_offset_is_reached = True

    # ------------------------------------------------------------------
    # Hyperopt-tunable parameters
    # ------------------------------------------------------------------
    # Buy parameters
    buy_rsi = IntParameter(20, 50, default=30, space="buy", optimize=True)
    buy_rsi_enabled = BooleanParameter(default=True, space="buy", optimize=True)
    buy_ema_short = IntParameter(10, 30, default=20, space="buy", optimize=True)
    buy_ema_long = IntParameter(40, 70, default=50, space="buy", optimize=True)
    buy_bb_enabled = BooleanParameter(default=True, space="buy", optimize=True)

    # Sell parameters
    sell_rsi = IntParameter(60, 90, default=70, space="sell", optimize=True)
    sell_rsi_enabled = BooleanParameter(default=True, space="sell", optimize=True)
    sell_bb_enabled = BooleanParameter(default=True, space="sell", optimize=True)

    # ------------------------------------------------------------------
    # Startup candle count (needed so indicators are warmed up)
    # ------------------------------------------------------------------
    startup_candle_count: int = 200

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Calculate all technical indicators."""

        # ---- RSI --------------------------------------------------------
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)

        # ---- EMA family -------------------------------------------------
        for period in (20, 50, 200):
            dataframe[f"ema_{period}"] = ta.EMA(dataframe, timeperiod=period)

        # ---- Dynamic EMA windows (hyperopt) ------------------------------
        for val in self.buy_ema_short.range:
            dataframe[f"ema_short_{val}"] = ta.EMA(dataframe, timeperiod=val)
        for val in self.buy_ema_long.range:
            dataframe[f"ema_long_{val}"] = ta.EMA(dataframe, timeperiod=val)

        # ---- Bollinger Bands (20, 2σ) ------------------------------------
        bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
        dataframe["bb_upperband"] = bollinger["upperband"]
        dataframe["bb_middleband"] = bollinger["middleband"]
        dataframe["bb_lowerband"] = bollinger["lowerband"]
        dataframe["bb_width"] = (
            (dataframe["bb_upperband"] - dataframe["bb_lowerband"])
            / dataframe["bb_middleband"]
        )

        # ---- MACD (auxiliary confirmation) -------------------------------
        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"] = macd["hist"]

        # ---- Volume indicator --------------------------------------------
        dataframe["volume_mean"] = dataframe["volume"].rolling(window=20).mean()

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Define entry (buy) conditions."""

        conditions = []

        # RSI oversold
        if self.buy_rsi_enabled.value:
            conditions.append(dataframe["rsi"] < self.buy_rsi.value)

        # Dynamic EMA: short EMA above long EMA (uptrend)
        conditions.append(
            dataframe[f"ema_short_{self.buy_ema_short.value}"]
            > dataframe[f"ema_long_{self.buy_ema_long.value}"]
        )

        # Price above EMA-200 (macro uptrend filter)
        conditions.append(dataframe["close"] > dataframe["ema_200"])

        # Price touches or is below lower Bollinger Band (mean-reversion)
        if self.buy_bb_enabled.value:
            conditions.append(dataframe["close"] <= dataframe["bb_lowerband"])

        # Minimum volume filter (avoid thin markets)
        conditions.append(dataframe["volume"] > dataframe["volume_mean"] * 0.5)

        # Candle must be valid
        conditions.append(dataframe["volume"] > 0)

        if conditions:
            dataframe.loc[
                np.logical_and.reduce(conditions),
                "enter_long",
            ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Define exit (sell) conditions."""

        conditions = []

        # RSI overbought
        if self.sell_rsi_enabled.value:
            conditions.append(dataframe["rsi"] > self.sell_rsi.value)

        # Price crosses above upper Bollinger Band
        if self.sell_bb_enabled.value:
            conditions.append(dataframe["close"] >= dataframe["bb_upperband"])

        # Candle must be valid
        conditions.append(dataframe["volume"] > 0)

        if conditions:
            # Use OR logic for exits: trigger on any exit condition
            dataframe.loc[
                np.logical_or.reduce(conditions) & (dataframe["volume"] > 0),
                "exit_long",
            ] = 1

        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        """
        Dynamic protective stop-loss:
        - If profit > 5 %, lock in at least 2 % profit.
        - If profit > 10 %, lock in at least 5 % profit.
        - Otherwise fall back to the static stoploss.
        """
        if current_profit > 0.10:
            return max(self.stoploss, -0.05 + current_profit - 0.05)
        if current_profit > 0.05:
            return max(self.stoploss, -0.02)
        return self.stoploss
