# source: https://raw.githubusercontent.com/aragier/tradingStrategies/ba3f2fd3be7a23d51a2f518cb043f0db05091e41/user_data/strategies/BtcEma9_21Scalp_v3.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Optional, Union

from freqtrade.strategy import (
    IStrategy,
    Trade,
    Order,
    PairLocks,
    informative,
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IntParameter,
    RealParameter,
    timeframe_to_minutes,
    timeframe_to_next_date,
    timeframe_to_prev_date,
    merge_informative_pair,
    stoploss_from_absolute,
    stoploss_from_open,
)

# --------------------------------
import talib.abstract as ta
from technical import qtpylib


class Github_aragier_tradingStrategies__BtcEma9_21Scalp_v3__20260327_161149(IStrategy):
    """
    EMA 9/21 Crossover Scalping Strategy v3 — ATR Dynamic Exits.

    All v2 improvements (MTF filter, volume, MACD exits, RSI extremes) PLUS:
    - ATR-based dynamic stoploss via custom_stoploss
    - ATR-based take-profit via custom_exit
    - EMA distance filter (reject micro-crosses)
    - Stricter MACD exit (histogram declining 2+ bars)
    - Relaxed entry filters from v2r (ADX 20, volume 1.2x, wider RSI)

    Designed for 5m timeframe, BTC futures (long + short).
    """

    INTERFACE_VERSION = 3
    can_short: bool = True

    # --- Minimal ROI --- (wider since ATR take-profit handles exits)
    minimal_roi = {
        "60": 0.01,
        "30": 0.02,
        "0": 0.04,
    }

    # --- Stoploss (fallback — custom_stoploss overrides) ---
    stoploss = -0.03

    # --- Trailing Stop ---
    trailing_stop = True
    trailing_stop_positive = 0.005
    trailing_stop_positive_offset = 0.01
    trailing_only_offset_is_reached = True

    # --- Custom stoploss enabled ---
    use_custom_stoploss = True

    # --- Timeframe ---
    timeframe = "5m"

    # --- General settings ---
    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # --- Hyperoptable Parameters ---
    ema_fast_period = IntParameter(5, 15, default=9, space="buy", optimize=True, load=True)
    ema_slow_period = IntParameter(15, 30, default=21, space="buy", optimize=True, load=True)

    # Relaxed RSI bands (from v2r learnings)
    rsi_long_min = IntParameter(40, 55, default=45, space="buy", optimize=True, load=True)
    rsi_long_max = IntParameter(65, 80, default=75, space="buy", optimize=True, load=True)
    rsi_short_min = IntParameter(20, 35, default=25, space="sell", optimize=True, load=True)
    rsi_short_max = IntParameter(45, 60, default=55, space="sell", optimize=True, load=True)

    rsi_exit_long = IntParameter(65, 80, default=70, space="sell", optimize=True, load=True)
    rsi_exit_short = IntParameter(20, 35, default=30, space="sell", optimize=True, load=True)

    adx_threshold = IntParameter(15, 30, default=20, space="buy", optimize=True, load=True)
    volume_factor = DecimalParameter(1.0, 2.0, default=1.2, decimals=1, space="buy", optimize=True, load=True)
    ema_1h_lookback = IntParameter(1, 5, default=2, space="buy", optimize=True, load=True)

    # ATR parameters
    atr_stoploss_mult = DecimalParameter(1.0, 2.5, default=1.5, decimals=1, space="sell", optimize=True, load=True)
    atr_takeprofit_mult = DecimalParameter(2.0, 4.0, default=2.5, decimals=1, space="sell", optimize=True, load=True)

    # EMA distance filter (minimum % separation after crossover)
    ema_min_distance = DecimalParameter(0.0001, 0.002, default=0.0003, decimals=4, space="buy", optimize=True, load=True)

    startup_candle_count: int = 650

    order_types = {
        "entry": "limit",
        "exit": "limit",
        "stoploss": "market",
        "stoploss_on_exchange": False,
    }

    order_time_in_force = {"entry": "GTC", "exit": "GTC"}

    plot_config = {
        "main_plot": {
            "ema_fast": {"color": "blue"},
            "ema_slow": {"color": "orange"},
        },
        "subplots": {
            "RSI": {"rsi": {"color": "red"}},
            "ADX": {"adx": {"color": "purple"}},
            "MACD": {"macdhist": {"color": "green", "type": "bar"}},
            "ATR": {"atr": {"color": "cyan"}},
        },
    }

    @informative("1h")
    def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # EMAs
        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=self.ema_fast_period.value)
        dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=self.ema_slow_period.value)

        # EMA distance (percentage)
        dataframe["ema_distance"] = abs(dataframe["ema_fast"] - dataframe["ema_slow"]) / dataframe["ema_slow"]

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

        # ADX
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)

        # MACD
        macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
        dataframe["macdhist"] = macd["macdhist"]

        # MACD histogram declining for 2 bars
        dataframe["macdhist_declining"] = (
            (dataframe["macdhist"] < dataframe["macdhist"].shift(1))
            & (dataframe["macdhist"].shift(1) < dataframe["macdhist"].shift(2))
        )
        dataframe["macdhist_rising"] = (
            (dataframe["macdhist"] > dataframe["macdhist"].shift(1))
            & (dataframe["macdhist"].shift(1) > dataframe["macdhist"].shift(2))
        )

        # ATR for dynamic stoploss/take-profit
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        # Volume SMA
        dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=20)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # --- Long Entry ---
        dataframe.loc[
            (
                (qtpylib.crossed_above(dataframe["ema_fast"], dataframe["ema_slow"]))
                & (dataframe["ema_distance"] > self.ema_min_distance.value)
                & (dataframe["ema_50_1h"] > dataframe["ema_50_1h"].shift(self.ema_1h_lookback.value))
                & (dataframe["rsi"] > self.rsi_long_min.value)
                & (dataframe["rsi"] < self.rsi_long_max.value)
                & (dataframe["adx"] > self.adx_threshold.value)
                & (dataframe["volume"] > (dataframe["volume_sma"] * self.volume_factor.value))
            ),
            "enter_long",
        ] = 1

        # --- Short Entry ---
        dataframe.loc[
            (
                (qtpylib.crossed_below(dataframe["ema_fast"], dataframe["ema_slow"]))
                & (dataframe["ema_distance"] > self.ema_min_distance.value)
                & (dataframe["ema_50_1h"] < dataframe["ema_50_1h"].shift(self.ema_1h_lookback.value))
                & (dataframe["rsi"] > self.rsi_short_min.value)
                & (dataframe["rsi"] < self.rsi_short_max.value)
                & (dataframe["adx"] > self.adx_threshold.value)
                & (dataframe["volume"] > (dataframe["volume_sma"] * self.volume_factor.value))
            ),
            "enter_short",
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # --- Exit Long ---
        # Stricter: EMA cross back + MACD histogram negative AND declining 2 bars
        # OR RSI extreme
        dataframe.loc[
            (
                (
                    (qtpylib.crossed_below(dataframe["ema_fast"], dataframe["ema_slow"]))
                    & (dataframe["macdhist"] < 0)
                    & (dataframe["macdhist_declining"])
                )
                | (dataframe["rsi"] > self.rsi_exit_long.value)
            )
            & (dataframe["volume"] > 0),
            "exit_long",
        ] = 1

        # --- Exit Short ---
        # Stricter: EMA cross back + MACD histogram positive AND rising 2 bars
        # OR RSI extreme
        dataframe.loc[
            (
                (
                    (qtpylib.crossed_above(dataframe["ema_fast"], dataframe["ema_slow"]))
                    & (dataframe["macdhist"] > 0)
                    & (dataframe["macdhist_rising"])
                )
                | (dataframe["rsi"] < self.rsi_exit_short.value)
            )
            & (dataframe["volume"] > 0),
            "exit_short",
        ] = 1

        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float | None:
        """
        ATR-based dynamic stoploss.
        Returns negative value as percentage from current rate.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

        if len(dataframe) < 1:
            return None

        last_candle = dataframe.iloc[-1]
        atr = last_candle["atr"]

        if atr <= 0 or current_rate <= 0:
            return None

        # Dynamic stoploss based on ATR
        stoploss_distance = atr * self.atr_stoploss_mult.value
        stoploss_pct = -(stoploss_distance / current_rate)

        # Clamp between -1% and -3%
        stoploss_pct = max(stoploss_pct, -0.03)  # floor at -3%
        stoploss_pct = min(stoploss_pct, -0.01)  # ceiling at -1%

        return stoploss_pct

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> str | bool | None:
        """
        ATR-based dynamic take-profit.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

        if len(dataframe) < 1:
            return None

        last_candle = dataframe.iloc[-1]
        atr = last_candle["atr"]

        if atr <= 0 or trade.open_rate <= 0:
            return None

        # Calculate take-profit distance
        tp_distance = atr * self.atr_takeprofit_mult.value
        tp_pct = tp_distance / trade.open_rate

        if current_profit >= tp_pct:
            return "atr_take_profit"

        return None
