# source: https://raw.githubusercontent.com/Ilovecodinghhh/Astock/5f12aa0e26f7a2eb60e44059fed6de29e2681dae/crypto_freqtrade/strategies/NewStrategyExperiments.py
from functools import reduce

import pandas as pd
import talib.abstract as ta
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
from pandas import DataFrame


class Github_Ilovecodinghhh_Astock__NewStrategyExperiments__20260613_081719(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "4h"
    informative_timeframe = "1d"
    startup_candle_count = 500
    can_short = False

    stoploss = -0.10
    trailing_stop = True
    trailing_stop_positive = 0.03
    trailing_stop_positive_offset = 0.10
    trailing_only_offset_is_reached = True

    btc_pair = "BTC/USDT"
    target_trade_volatility = 0.055
    min_stake_fraction = 0.35
    max_stake_fraction = 1.0

    @property
    def protections(self):
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 2},
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 24,
                "trade_limit": 3,
                "stop_duration_candles": 12,
                "required_profit": 0.0,
                "only_per_pair": False,
            },
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 90,
                "trade_limit": 18,
                "stop_duration_candles": 18,
                "max_allowed_drawdown": 0.18,
                "calculation_mode": "equity",
            },
        ]

    def informative_pairs(self):
        pairs = {(self.btc_pair, self.informative_timeframe)}
        if self.dp:
            for pair in self.dp.current_whitelist():
                pairs.add((pair, self.informative_timeframe))
        return sorted(pairs)

    @staticmethod
    def _daily_indicators(dataframe: DataFrame, prefix: str) -> DataFrame:
        dataframe[f"{prefix}_ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe[f"{prefix}_ema_100"] = ta.EMA(dataframe, timeperiod=100)
        dataframe[f"{prefix}_ema_200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe[f"{prefix}_rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe[f"{prefix}_momentum_30"] = dataframe["close"] / dataframe["close"].shift(30) - 1.0
        dataframe[f"{prefix}_momentum_90"] = dataframe["close"] / dataframe["close"].shift(90) - 1.0
        dataframe[f"{prefix}_volatility_30"] = dataframe["close"].pct_change().rolling(30, min_periods=30).std()
        return dataframe

    @staticmethod
    def _trend_ok(dataframe: DataFrame, prefix: str) -> pd.Series:
        return (
            (dataframe["close"] > dataframe[f"{prefix}_ema_100"])
            & (dataframe[f"{prefix}_ema_50"] > dataframe[f"{prefix}_ema_200"])
            & (dataframe[f"{prefix}_momentum_30"] > -0.03)
            & (dataframe[f"{prefix}_volatility_30"] < 0.08)
        )

    def _merge_daily_context(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if self.dp:
            btc_daily = self.dp.get_pair_dataframe(pair=self.btc_pair, timeframe=self.informative_timeframe)
            btc_daily = self._daily_indicators(btc_daily, "btc")
            btc_daily["btc_trend_ok"] = self._trend_ok(btc_daily, "btc").astype(int)
            dataframe = merge_informative_pair(
                dataframe,
                btc_daily[["date", "btc_trend_ok"]],
                self.timeframe,
                self.informative_timeframe,
                ffill=True,
            )

            pair_daily = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=self.informative_timeframe)
            pair_daily = self._daily_indicators(pair_daily, "pair")
            pair_daily["pair_trend_ok"] = self._trend_ok(pair_daily, "pair").astype(int)
            pair_daily["pair_daily_rsi"] = pair_daily["pair_rsi"]
            dataframe = merge_informative_pair(
                dataframe,
                pair_daily[["date", "pair_trend_ok", "pair_daily_rsi"]],
                self.timeframe,
                self.informative_timeframe,
                ffill=True,
            )
        else:
            dataframe["btc_trend_ok_1d"] = 1
            dataframe["pair_trend_ok_1d"] = 1
            dataframe["pair_daily_rsi_1d"] = 50
        return dataframe

    def custom_stake_amount(
        self,
        pair: str,
        current_time,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> float:
        if not self.dp:
            return proposed_stake

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty or "atr_pct" not in dataframe:
            return proposed_stake

        atr_pct = dataframe.iloc[-1]["atr_pct"]
        if pd.isna(atr_pct) or atr_pct <= 0:
            return proposed_stake

        fraction = self.target_trade_volatility / float(atr_pct)
        fraction = min(self.max_stake_fraction, max(self.min_stake_fraction, fraction))
        stake = proposed_stake * fraction
        if min_stake:
            stake = max(min_stake, stake)
        return min(max_stake, stake)


class CrossSectionalMomentumStrategy(Github_Ilovecodinghhh_Astock__NewStrategyExperiments__20260613_081719):
    minimal_roi = {
        "0": 0.55,
        "120": 0.24,
        "360": 0.10,
        "900": 0,
    }

    rank_top_n = 3
    buy_momentum_min = DecimalParameter(0.02, 0.20, default=0.06, decimals=3, space="buy", optimize=True)
    buy_rsi_max = IntParameter(68, 86, default=80, space="buy", optimize=True)
    exit_rank_threshold = 6

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_100"] = ta.EMA(dataframe, timeperiod=100)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        dataframe["volume_mean_12"] = dataframe["volume"].rolling(12, min_periods=12).mean()
        dataframe["momentum_30"] = dataframe["close"] / dataframe["close"].shift(180) - 1.0
        dataframe["momentum_14"] = dataframe["close"] / dataframe["close"].shift(42) - 1.0
        dataframe["volatility_14"] = dataframe["close"].pct_change().rolling(42, min_periods=42).std()
        dataframe["risk_adj_momentum"] = dataframe["momentum_14"] / dataframe["volatility_14"].replace(0, pd.NA)
        dataframe["ema_100_slope"] = dataframe["ema_100"] / dataframe["ema_100"].shift(12) - 1.0
        dataframe["momentum_rank"] = self._momentum_rank(dataframe, metadata["pair"])
        return self._merge_daily_context(dataframe, metadata)

    def _momentum_rank(self, dataframe: DataFrame, pair: str) -> pd.Series:
        if not self.dp:
            return pd.Series(1.0, index=dataframe.index)

        strength = pd.DataFrame(index=dataframe["date"])
        strength[pair] = dataframe.set_index("date")["risk_adj_momentum"]
        for candidate in self.dp.current_whitelist():
            if candidate == pair:
                continue
            candidate_df = self.dp.get_pair_dataframe(candidate, self.timeframe)
            if candidate_df.empty:
                continue
            candidate_strength = (
                candidate_df["close"] / candidate_df["close"].shift(42) - 1.0
            ) / candidate_df["close"].pct_change().rolling(42, min_periods=42).std().replace(0, pd.NA)
            strength[candidate] = candidate_strength.set_axis(candidate_df["date"]).reindex(strength.index)
        return strength.rank(axis=1, ascending=False, method="first")[pair].to_numpy()

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = [
            dataframe["volume"] > dataframe["volume_mean_12"],
            dataframe["close"] > dataframe["ema_100"],
            dataframe["ema_50"] > dataframe["ema_200"],
            dataframe["ema_100_slope"] > 0,
            dataframe["momentum_30"] > self.buy_momentum_min.value,
            dataframe["risk_adj_momentum"] > 0,
            dataframe["momentum_rank"] <= self.rank_top_n,
            dataframe["rsi"] < self.buy_rsi_max.value,
            dataframe["atr_pct"].between(0.012, 0.12),
            dataframe["btc_trend_ok_1d"] == 1,
            dataframe["pair_trend_ok_1d"] == 1,
        ]
        dataframe.loc[reduce(lambda left, right: left & right, conditions), ["enter_long", "enter_tag"]] = (
            1,
            "cross_sectional_momentum",
        )
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = [
            dataframe["volume"] > 0,
            (
                (dataframe["momentum_rank"] > self.exit_rank_threshold)
                | (dataframe["close"] < dataframe["ema_100"])
                | (dataframe["rsi"] < 42)
                | (dataframe["pair_trend_ok_1d"] == 0)
            ),
        ]
        dataframe.loc[reduce(lambda left, right: left & right, conditions), ["exit_long", "exit_tag"]] = (
            1,
            "momentum_rotation_exit",
        )
        return dataframe


class CrossSectionalMomentumTop2Strategy(CrossSectionalMomentumStrategy):
    rank_top_n = 2
    buy_momentum_min = DecimalParameter(0.04, 0.24, default=0.08, decimals=3, space="buy", optimize=True)


class CrossSectionalMomentumAggressiveStrategy(CrossSectionalMomentumStrategy):
    rank_top_n = 4
    target_trade_volatility = 0.075
    stoploss = -0.14
    trailing_stop_positive = 0.04
    trailing_stop_positive_offset = 0.14

    minimal_roi = {
        "0": 0.80,
        "180": 0.34,
        "540": 0.12,
        "1080": 0,
    }


class PullbackTrendStrategy(Github_Ilovecodinghhh_Astock__NewStrategyExperiments__20260613_081719):
    minimal_roi = {
        "0": 0.30,
        "96": 0.14,
        "240": 0.05,
        "720": 0,
    }

    pullback_rsi_min = IntParameter(34, 48, default=40, space="buy", optimize=True)
    pullback_rsi_max = IntParameter(48, 62, default=55, space="buy", optimize=True)
    trend_adx_min = IntParameter(14, 30, default=18, space="buy", optimize=True)
    exit_rsi = 44

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20)
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_100"] = ta.EMA(dataframe, timeperiod=100)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        dataframe["volume_mean_12"] = dataframe["volume"].rolling(12, min_periods=12).mean()
        dataframe["bb_upper"], dataframe["bb_mid"], dataframe["bb_lower"] = ta.BBANDS(
            dataframe["close"],
            timeperiod=20,
            nbdevup=2.0,
            nbdevdn=2.0,
        )
        dataframe["ema_100_slope"] = dataframe["ema_100"] / dataframe["ema_100"].shift(12) - 1.0
        dataframe["recent_high"] = dataframe["high"].rolling(18, min_periods=18).max().shift(1)
        dataframe["exit_low"] = dataframe["low"].rolling(8, min_periods=8).min().shift(1)
        return self._merge_daily_context(dataframe, metadata)

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        trend = (
            (dataframe["close"] > dataframe["ema_200"])
            & (dataframe["ema_20"] > dataframe["ema_50"])
            & (dataframe["ema_50"] > dataframe["ema_200"])
            & (dataframe["ema_100_slope"] > 0)
            & (dataframe["adx"] > self.trend_adx_min.value)
        )
        pullback = (
            dataframe["rsi"].between(self.pullback_rsi_min.value, self.pullback_rsi_max.value)
            & (dataframe["low"] <= dataframe["ema_50"] * 1.015)
            & (dataframe["close"] > dataframe["bb_mid"])
        )
        conditions = [
            dataframe["volume"] > dataframe["volume_mean_12"],
            trend,
            pullback,
            dataframe["atr_pct"].between(0.010, 0.10),
            dataframe["btc_trend_ok_1d"] == 1,
            dataframe["pair_trend_ok_1d"] == 1,
            dataframe["pair_daily_rsi_1d"] < 78,
        ]
        dataframe.loc[reduce(lambda left, right: left & right, conditions), ["enter_long", "enter_tag"]] = (
            1,
            "trend_pullback",
        )
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = [
            dataframe["volume"] > 0,
            (
                (dataframe["close"] < dataframe["exit_low"])
                | (dataframe["ema_20"] < dataframe["ema_50"])
                | (dataframe["rsi"] < self.exit_rsi)
                | (dataframe["pair_trend_ok_1d"] == 0)
            ),
        ]
        dataframe.loc[reduce(lambda left, right: left & right, conditions), ["exit_long", "exit_tag"]] = (
            1,
            "pullback_trend_exit",
        )
        return dataframe


class PullbackTrendFastStrategy(PullbackTrendStrategy):
    pullback_rsi_min = IntParameter(38, 52, default=44, space="buy", optimize=True)
    pullback_rsi_max = IntParameter(54, 66, default=60, space="buy", optimize=True)
    exit_rsi = 48


class PullbackTrendDeepStrategy(PullbackTrendStrategy):
    pullback_rsi_min = IntParameter(28, 42, default=34, space="buy", optimize=True)
    pullback_rsi_max = IntParameter(42, 56, default=50, space="buy", optimize=True)
    stoploss = -0.08
    trailing_stop_positive = 0.025
    trailing_stop_positive_offset = 0.08


class VolatilityExpansionBreakoutStrategy(Github_Ilovecodinghhh_Astock__NewStrategyExperiments__20260613_081719):
    timeframe = "1h"
    startup_candle_count = 720

    minimal_roi = {
        "0": 0.18,
        "24": 0.08,
        "72": 0.03,
        "168": 0,
    }

    stoploss = -0.07
    trailing_stop_positive = 0.018
    trailing_stop_positive_offset = 0.055
    target_trade_volatility = 0.035

    breakout_window = IntParameter(24, 96, default=48, space="buy", optimize=True)
    compression_window = IntParameter(72, 240, default=120, space="buy", optimize=True)
    volume_mult = DecimalParameter(1.05, 2.50, default=1.30, decimals=2, space="buy", optimize=True)

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_100"] = ta.EMA(dataframe, timeperiod=100)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        dataframe["volume_mean_24"] = dataframe["volume"].rolling(24, min_periods=24).mean()
        dataframe["volume_mean_72"] = dataframe["volume"].rolling(72, min_periods=72).mean()
        dataframe["bb_upper"], dataframe["bb_mid"], dataframe["bb_lower"] = ta.BBANDS(
            dataframe["close"],
            timeperiod=40,
            nbdevup=2.0,
            nbdevdn=2.0,
        )
        dataframe["bb_width"] = (dataframe["bb_upper"] - dataframe["bb_lower"]) / dataframe["bb_mid"]

        for window in self.breakout_window.range:
            dataframe[f"breakout_high_{window}"] = dataframe["high"].rolling(window, min_periods=window).max().shift(1)

        for window in self.compression_window.range:
            dataframe[f"bb_width_floor_{window}"] = (
                dataframe["bb_width"].rolling(window, min_periods=window).quantile(0.35).shift(1)
            )

        dataframe["ema_100_slope"] = dataframe["ema_100"] / dataframe["ema_100"].shift(24) - 1.0
        dataframe["exit_low"] = dataframe["low"].rolling(18, min_periods=18).min().shift(1)
        return self._merge_daily_context(dataframe, metadata)

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        breakout_window = self.breakout_window.value
        compression_window = self.compression_window.value
        compression = dataframe["bb_width"].shift(1) <= dataframe[f"bb_width_floor_{compression_window}"]
        expansion = dataframe["close"] > dataframe[f"breakout_high_{breakout_window}"]
        conditions = [
            dataframe["volume"] > dataframe["volume_mean_72"] * self.volume_mult.value,
            dataframe["close"] > dataframe["ema_200"],
            dataframe["ema_50"] > dataframe["ema_100"],
            dataframe["ema_100"] > dataframe["ema_200"],
            dataframe["ema_100_slope"] > 0,
            dataframe["adx"] > 16,
            dataframe["rsi"].between(54, 82),
            dataframe["atr_pct"].between(0.004, 0.07),
            compression,
            expansion,
            dataframe["btc_trend_ok_1d"] == 1,
            dataframe["pair_trend_ok_1d"] == 1,
        ]
        dataframe.loc[reduce(lambda left, right: left & right, conditions), ["enter_long", "enter_tag"]] = (
            1,
            "volatility_expansion_breakout",
        )
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = [
            dataframe["volume"] > 0,
            (
                (dataframe["close"] < dataframe["exit_low"])
                | (dataframe["ema_50"] < dataframe["ema_100"])
                | (dataframe["rsi"] < 45)
                | (dataframe["pair_trend_ok_1d"] == 0)
            ),
        ]
        dataframe.loc[reduce(lambda left, right: left & right, conditions), ["exit_long", "exit_tag"]] = (
            1,
            "volatility_breakout_exit",
        )
        return dataframe


class VolatilityExpansionBreakoutLooseStrategy(VolatilityExpansionBreakoutStrategy):
    volume_mult = DecimalParameter(1.00, 2.00, default=1.10, decimals=2, space="buy", optimize=True)
    trailing_stop_positive = 0.014
    trailing_stop_positive_offset = 0.045


class VolatilityExpansionBreakoutAggressiveStrategy(VolatilityExpansionBreakoutStrategy):
    stoploss = -0.10
    target_trade_volatility = 0.05
    trailing_stop_positive = 0.025
    trailing_stop_positive_offset = 0.075

    minimal_roi = {
        "0": 0.26,
        "36": 0.12,
        "120": 0.04,
        "240": 0,
    }
