# source: https://raw.githubusercontent.com/wangc-joker/ft_userdata/4b4fda04062336f5b248659e6caaf763570b083d/user_data/strategies/DoubleShunStrategy.py
from typing import Optional

import pandas as pd
from pandas import DataFrame

import talib.abstract as ta
from freqtrade.persistence import Trade
from freqtrade.strategy import (
    DecimalParameter,
    IStrategy,
    IntParameter,
    informative,
    stoploss_from_absolute,
)


class Github_wangc_joker_ft_userdata__DoubleShunStrategy__20260407_090856(IStrategy):
    INTERFACE_VERSION = 3
    allowed_pairs = {"BTC/USDT:USDT", "BNB/USDT:USDT", "SOL/USDT:USDT"}

    can_short: bool = True
    timeframe = "1h"
    process_only_new_candles = True
    startup_candle_count: int = 240

    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = True
    use_custom_stoploss = True

    minimal_roi = {
        "0": 0.10,
    }

    stoploss = -0.02

    trend_ema_fast = IntParameter(5, 20, default=10, space="buy", optimize=True)
    trend_ema_slow = IntParameter(20, 80, default=30, space="buy", optimize=True)
    center_window = IntParameter(4, 12, default=6, space="buy", optimize=True)
    pullback_window = IntParameter(3, 8, default=4, space="buy", optimize=True)
    restart_window = IntParameter(2, 6, default=3, space="buy", optimize=True)
    triangle_window = IntParameter(4, 12, default=6, space="buy", optimize=True)
    compression_window = IntParameter(4, 12, default=6, space="buy", optimize=True)
    swing_window = IntParameter(2, 8, default=3, space="sell", optimize=True)

    pullback_depth = DecimalParameter(0.002, 0.025, default=0.010, decimals=3, space="buy", optimize=True)
    breakout_buffer = DecimalParameter(0.001, 0.012, default=0.002, decimals=3, space="buy", optimize=True)
    compression_limit = DecimalParameter(0.006, 0.040, default=0.018, decimals=3, space="buy", optimize=True)
    level_tolerance = DecimalParameter(0.002, 0.020, default=0.006, decimals=3, space="buy", optimize=True)
    level_proximity = DecimalParameter(0.002, 0.020, default=0.006, decimals=3, space="buy", optimize=True)
    volume_multiplier = DecimalParameter(1.00, 2.20, default=1.10, decimals=2, space="buy", optimize=True)

    @property
    def protections(self):
        return [
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": 2,
            }
        ]

    @staticmethod
    def _bool_series(dataframe: DataFrame, column: str) -> pd.Series:
        return dataframe[column].eq(True)

    @informative("1d")
    def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return self._populate_structure_indicators(dataframe)

    def _populate_structure_indicators(self, dataframe: DataFrame) -> DataFrame:
        ema_fast = int(self.trend_ema_fast.value)
        ema_slow = int(self.trend_ema_slow.value)
        center_window = int(self.center_window.value)
        pullback_window = int(self.pullback_window.value)
        restart_window = int(self.restart_window.value)
        triangle_window = int(self.triangle_window.value)
        compression_window = int(self.compression_window.value)
        swing_window = int(self.swing_window.value)

        pullback_depth = float(self.pullback_depth.value)
        breakout_buffer = float(self.breakout_buffer.value)
        compression_limit = float(self.compression_limit.value)
        level_tolerance = float(self.level_tolerance.value)
        level_proximity = float(self.level_proximity.value)
        volume_multiplier = float(self.volume_multiplier.value)

        half_window = max(2, triangle_window // 2)
        typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3.0

        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=ema_fast)
        dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=ema_slow)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        dataframe["volume_mean"] = dataframe["volume"].rolling(max(5, triangle_window)).mean()

        dataframe["market_center"] = typical_price.rolling(center_window).mean()
        dataframe["center_up"] = dataframe["market_center"] > dataframe["market_center"].shift(2)
        dataframe["center_down"] = dataframe["market_center"] < dataframe["market_center"].shift(2)

        dataframe["uptrend"] = (
            (dataframe["close"] > dataframe["ema_slow"])
            & (dataframe["ema_fast"] > dataframe["ema_slow"])
            & dataframe["center_up"]
        )
        dataframe["downtrend"] = (
            (dataframe["close"] < dataframe["ema_slow"])
            & (dataframe["ema_fast"] < dataframe["ema_slow"])
            & dataframe["center_down"]
        )

        dataframe["recent_high"] = dataframe["high"].shift(1).rolling(triangle_window).max()
        dataframe["recent_low"] = dataframe["low"].shift(1).rolling(triangle_window).min()
        dataframe["recent_high_short"] = dataframe["high"].shift(1).rolling(half_window).max()
        dataframe["recent_low_short"] = dataframe["low"].shift(1).rolling(half_window).min()
        dataframe["prior_high_short"] = dataframe["high"].shift(half_window + 1).rolling(half_window).max()
        dataframe["prior_low_short"] = dataframe["low"].shift(half_window + 1).rolling(half_window).min()

        dataframe["rising_lows"] = dataframe["recent_low_short"] > dataframe["prior_low_short"]
        dataframe["falling_highs"] = dataframe["recent_high_short"] < dataframe["prior_high_short"]
        dataframe["flat_ceiling"] = (
            (dataframe["recent_high"] - dataframe["recent_high_short"]).abs() / dataframe["close"]
        ) < level_tolerance
        dataframe["flat_floor"] = (
            (dataframe["recent_low_short"] - dataframe["recent_low"]).abs() / dataframe["close"]
        ) < level_tolerance

        dataframe["range_width"] = (
            (dataframe["high"].shift(1).rolling(compression_window).max()
             - dataframe["low"].shift(1).rolling(compression_window).min())
            / dataframe["close"]
        )
        dataframe["range_width_prev"] = dataframe["range_width"].shift(max(2, compression_window // 2))
        dataframe["range_tight"] = dataframe["range_width"] < compression_limit
        dataframe["range_contracting"] = dataframe["range_width"] < dataframe["range_width_prev"]
        dataframe["volume_expansion"] = (
            dataframe["volume"] > dataframe["volume_mean"] * volume_multiplier
        )

        dataframe["pullback_low"] = dataframe["low"].shift(1).rolling(pullback_window).min()
        dataframe["pullback_high"] = dataframe["high"].shift(1).rolling(pullback_window).max()
        dataframe["restart_high"] = dataframe["high"].shift(1).rolling(restart_window).max()
        dataframe["restart_low"] = dataframe["low"].shift(1).rolling(restart_window).min()

        dataframe["pullback_seen_long"] = dataframe["pullback_low"] <= (
            dataframe["ema_fast"] * (1 + pullback_depth)
        )
        dataframe["pullback_seen_short"] = dataframe["pullback_high"] >= (
            dataframe["ema_fast"] * (1 - pullback_depth)
        )
        dataframe["structure_intact_long"] = dataframe["pullback_low"] > (
            dataframe["ema_slow"] * (1 - pullback_depth * 2.0)
        )
        dataframe["structure_intact_short"] = dataframe["pullback_high"] < (
            dataframe["ema_slow"] * (1 + pullback_depth * 2.0)
        )

        dataframe["restart_ready_long"] = (
            dataframe["uptrend"]
            & dataframe["pullback_seen_long"]
            & dataframe["structure_intact_long"]
            & (dataframe["close"] > dataframe["ema_fast"])
            & (dataframe["rsi"] > dataframe["rsi"].shift(1))
        )
        dataframe["restart_ready_short"] = (
            dataframe["downtrend"]
            & dataframe["pullback_seen_short"]
            & dataframe["structure_intact_short"]
            & (dataframe["close"] < dataframe["ema_fast"])
            & (dataframe["rsi"] < dataframe["rsi"].shift(1))
        )

        dataframe["near_high_compression"] = (
            dataframe["close"].shift(1) >= dataframe["recent_high"] * (1 - level_proximity)
        )
        dataframe["near_low_compression"] = (
            dataframe["close"].shift(1) <= dataframe["recent_low"] * (1 + level_proximity)
        )
        dataframe["breakout_above_recent"] = (
            (dataframe["close"] > dataframe["recent_high"] * (1 + breakout_buffer))
            & (
                dataframe["close"].shift(1)
                <= dataframe["recent_high"].shift(1) * (1 + breakout_buffer)
            )
        )
        dataframe["breakout_below_recent"] = (
            (dataframe["close"] < dataframe["recent_low"] * (1 - breakout_buffer))
            & (
                dataframe["close"].shift(1)
                >= dataframe["recent_low"].shift(1) * (1 - breakout_buffer)
            )
        )
        dataframe["ema_slow_slope_up"] = dataframe["ema_slow"] > dataframe["ema_slow"].shift(3)
        dataframe["ema_slow_slope_down"] = dataframe["ema_slow"] < dataframe["ema_slow"].shift(3)
        dataframe["daily_momentum_long"] = (
            dataframe["uptrend"]
            & dataframe["ema_slow_slope_up"]
            & (dataframe["rsi"] > 55)
        )
        dataframe["daily_momentum_short"] = (
            dataframe["downtrend"]
            & dataframe["ema_slow_slope_down"]
            & (dataframe["rsi"] < 45)
        )

        dataframe["triangle_breakout_long"] = (
            dataframe["restart_ready_long"]
            & dataframe["rising_lows"]
            & dataframe["flat_ceiling"]
            & dataframe["range_contracting"]
            & dataframe["breakout_above_recent"]
            & dataframe["volume_expansion"]
        )
        dataframe["triangle_breakout_short"] = (
            dataframe["restart_ready_short"]
            & dataframe["falling_highs"]
            & dataframe["flat_floor"]
            & dataframe["range_contracting"]
            & dataframe["breakout_below_recent"]
            & dataframe["volume_expansion"]
        )

        dataframe["center_breakout_long"] = (
            dataframe["restart_ready_long"]
            & dataframe["center_up"]
            & dataframe["range_contracting"]
            & dataframe["near_high_compression"]
            & (dataframe["market_center"] > dataframe["market_center"].shift(1))
            & dataframe["breakout_above_recent"]
            & (dataframe["close"] > dataframe["market_center"])
            & dataframe["volume_expansion"]
        )
        dataframe["center_breakout_short"] = (
            dataframe["restart_ready_short"]
            & dataframe["center_down"]
            & dataframe["range_contracting"]
            & dataframe["near_low_compression"]
            & (dataframe["market_center"] < dataframe["market_center"].shift(1))
            & dataframe["breakout_below_recent"]
            & (dataframe["close"] < dataframe["market_center"])
            & dataframe["volume_expansion"]
        )

        dataframe["compression_breakout_long"] = (
            dataframe["restart_ready_long"]
            & dataframe["range_tight"]
            & dataframe["range_contracting"]
            & dataframe["near_high_compression"]
            & dataframe["breakout_above_recent"]
            & dataframe["volume_expansion"]
        )
        dataframe["compression_breakout_short"] = (
            dataframe["restart_ready_short"]
            & dataframe["range_tight"]
            & dataframe["range_contracting"]
            & dataframe["near_low_compression"]
            & dataframe["breakout_below_recent"]
            & dataframe["volume_expansion"]
        )

        dataframe["signal_long"] = (
            dataframe["triangle_breakout_long"]
            | dataframe["center_breakout_long"]
            | dataframe["compression_breakout_long"]
        )
        dataframe["signal_short"] = (
            dataframe["triangle_breakout_short"]
            | dataframe["center_breakout_short"]
            | dataframe["compression_breakout_short"]
        )

        dataframe["structure_stop_long"] = dataframe["low"].shift(1).rolling(swing_window).min()
        dataframe["structure_stop_short"] = dataframe["high"].shift(1).rolling(swing_window).max()

        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return self._populate_structure_indicators(dataframe)

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if metadata["pair"] not in self.allowed_pairs:
            return dataframe

        hourly_long_context = (
            self._bool_series(dataframe, "restart_ready_long_1d")
            & self._bool_series(dataframe, "daily_momentum_long_1d")
        )
        hourly_short_context = self._bool_series(dataframe, "restart_ready_short_1d")
        triangle_long = self._bool_series(dataframe, "triangle_breakout_long")
        compression_long = self._bool_series(dataframe, "compression_breakout_long")
        center_short = self._bool_series(dataframe, "center_breakout_short")
        compression_short = self._bool_series(dataframe, "compression_breakout_short")
        triangle_long_1d = self._bool_series(dataframe, "triangle_breakout_long_1d")
        center_long_1d = self._bool_series(dataframe, "center_breakout_long_1d")
        triangle_short_1d = self._bool_series(dataframe, "triangle_breakout_short_1d")
        center_short_1d = self._bool_series(dataframe, "center_breakout_short_1d")
        daily_long_context = self._bool_series(dataframe, "restart_ready_long_1d")
        daily_short_context = self._bool_series(dataframe, "restart_ready_short_1d")
        daily_long_signal = daily_long_context & (triangle_long_1d | center_long_1d)
        daily_short_signal = daily_short_context & (triangle_short_1d | center_short_1d)
        daily_long_trigger = daily_long_signal & ~daily_long_signal.shift(1).eq(True)
        daily_short_trigger = daily_short_signal & ~daily_short_signal.shift(1).eq(True)
        strong_hourly_long_triangle = (
            hourly_long_context
            & triangle_long
            & self._bool_series(dataframe, "range_tight")
            & self._bool_series(dataframe, "ema_slow_slope_up")
            & (dataframe["rsi"] > 52)
            & self._bool_series(dataframe, "breakout_above_recent_1d")
        )
        dataframe.loc[
            strong_hourly_long_triangle,
            ["enter_long", "enter_tag"],
        ] = (1, "long_1h_triangle")

        dataframe.loc[
            daily_long_trigger & triangle_long_1d,
            ["enter_long", "enter_tag"],
        ] = (1, "long_1d_triangle")
        dataframe.loc[
            daily_long_trigger & center_long_1d,
            ["enter_long", "enter_tag"],
        ] = (1, "long_1d_center_compression")

        dataframe.loc[
            hourly_short_context & center_short,
            ["enter_short", "enter_tag"],
        ] = (1, "short_1h_center")
        dataframe.loc[
            hourly_short_context & compression_short,
            ["enter_short", "enter_tag"],
        ] = (1, "short_1h_compression")

        dataframe.loc[
            daily_short_trigger & triangle_short_1d,
            ["enter_short", "enter_tag"],
        ] = (1, "short_1d_triangle")
        dataframe.loc[
            daily_short_trigger & center_short_1d,
            ["enter_short", "enter_tag"],
        ] = (1, "short_1d_center_compression")

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> Optional[float]:
        if not self.dp:
            return self.stoploss

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty:
            return self.stoploss

        candle = dataframe.iloc[-1]
        tag = trade.enter_tag or ""
        scope = "1d" if "_1d_" in tag else "1h"
        suffix = "_1d" if scope == "1d" else ""

        if trade.is_short:
            structure_stop = candle.get(f"structure_stop_short{suffix}")
            capped_stop = trade.open_rate * 1.02
            stop_price = capped_stop
            if pd.notna(structure_stop):
                stop_price = min(float(structure_stop), capped_stop)
        else:
            structure_stop = candle.get(f"structure_stop_long{suffix}")
            capped_stop = trade.open_rate * 0.98
            stop_price = capped_stop
            if pd.notna(structure_stop):
                stop_price = max(float(structure_stop), capped_stop)

        return stoploss_from_absolute(
            stop_price,
            current_rate,
            is_short=trade.is_short,
            leverage=trade.leverage,
        )

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> Optional[str]:
        if not self.dp:
            return None

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty:
            return None

        candle = dataframe.iloc[-1]
        tag = trade.enter_tag or ""
        scope = "1d" if "_1d_" in tag else "1h"
        suffix = "_1d" if scope == "1d" else ""

        stop_long = candle.get(f"structure_stop_long{suffix}")
        stop_short = candle.get(f"structure_stop_short{suffix}")

        if trade.is_short:
            if bool(candle.get("uptrend_1d", False)):
                return "trend_flip_short"
            if scope == "1d":
                if bool(candle.get("center_up_1d", False)) and candle["close"] > candle.get("ema_fast_1d", candle["close"]):
                    return "structure_exit_short_1d"
            else:
                if bool(candle.get("center_up", False)) and candle["close"] > candle.get("ema_fast", candle["close"]):
                    return "structure_exit_short_1h"

            if pd.notna(stop_short) and candle["close"] > stop_short:
                return f"swing_exit_short_{scope}"
            return None

        if bool(candle.get("downtrend_1d", False)):
            return "trend_flip_long"
        if scope == "1d":
            if bool(candle.get("center_down_1d", False)) and candle["close"] < candle.get("ema_fast_1d", candle["close"]):
                return "structure_exit_long_1d"
        else:
            if bool(candle.get("center_down", False)) and candle["close"] < candle.get("ema_fast", candle["close"]):
                return "structure_exit_long_1h"

        if pd.notna(stop_long) and candle["close"] < stop_long:
            return f"swing_exit_long_{scope}"

        return None
