# source: https://raw.githubusercontent.com/aldrinmagno/FreqtradeStrategies/3d9c1b176faceca3c950f99c3c55c8a7abe77275/user_data/strategies/MeanReversionScalper.py
"""
Github_aldrinmagno_FreqtradeStrategies__MeanReversionScalper__20260317_150802 — Bollinger Band + RSI + Volume Spike Mean-Reversion Scalper

Timeframe: 5m (with 1h informative for trend filter)
Description: Enters when price touches the lower Bollinger Band with oversold RSI
             and a volume spike, targeting a 0.3-0.6% snap-back toward the midline.

Entry conditions:
    - Close <= BB lower band
    - RSI < 30 (momentum exhaustion)
    - Volume > 1.5x 20-period average (capitulation spike)
    - BB width > 0.5% (enough room for profitable reversion)
    - 1h EMA50 slope > -0.3% (avoids buying into waterfall sell-offs)

Risk management:
    - Tiered ROI: 0.6% immediately down to 0.1% at 40 minutes
    - Trailing stop activating at +0.3%
    - Time-based tightening custom stoploss (-0.8% -> -0.3%)
    - Signal exit when price hits BB midline
    - StoplossGuard after 3 consecutive losses + per-pair cooldown

WARNING: This strategy has NOT been backtested. Run through at least 6 months of
data on 8+ liquid pairs, verify win rate > ~55% with profit factor > 1.3, and
paper trade 2-4 weeks before using real capital. Requires fees <= 0.075% per side.
"""

from datetime import datetime

from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter, merge_informative_pair
from pandas import DataFrame
import pandas_ta as pta


class Github_aldrinmagno_FreqtradeStrategies__MeanReversionScalper__20260317_150802(IStrategy):

    INTERFACE_VERSION: int = 3

    timeframe: str = "5m"

    # Tiered ROI — 0.6% at open, tapering to 0.1% at 40 min
    minimal_roi: dict = {
        "0": 0.006,
        "10": 0.005,
        "20": 0.003,
        "30": 0.002,
        "40": 0.001,
    }

    stoploss: float = -0.008  # -0.8% hard stoploss

    # Trailing stop — activates at +0.3%
    trailing_stop: bool = True
    trailing_stop_positive: float = 0.002
    trailing_stop_positive_offset: float = 0.003
    trailing_only_offset_is_reached: bool = True

    # Trade management
    max_open_trades: int = 6
    use_exit_signal: bool = True
    exit_profit_only: bool = False

    # Informative timeframe for trend filter
    informative_timeframe: str = "1h"

    # Process only new candles for performance
    process_only_new_candles: bool = True

    # Enough candles for 1h EMA50 to warm up (50 * 12 five-min candles per hour)
    startup_candle_count: int = 600

    # -----------------------------------------------------------------------
    # Buy hyperopt parameters
    # -----------------------------------------------------------------------
    buy_rsi_threshold = IntParameter(20, 40, default=30, space="buy",
                                     help="RSI must be below this to enter")
    buy_volume_factor = DecimalParameter(1.2, 2.5, default=1.5, space="buy",
                                         help="Volume must exceed SMA(20) * this factor")
    buy_bb_width_min = DecimalParameter(0.003, 0.01, default=0.005, space="buy",
                                         help="Minimum BB width (fraction) for entry")
    buy_ema_slope_min = DecimalParameter(-0.005, 0.0, default=-0.003, space="buy",
                                          help="Min 1h EMA50 slope (fraction) — trend filter")

    # -----------------------------------------------------------------------
    # Sell hyperopt parameters
    # -----------------------------------------------------------------------
    sell_bb_mid_offset = DecimalParameter(0.995, 1.005, default=1.0, space="sell",
                                           help="Exit at BB mid * this offset")

    # -----------------------------------------------------------------------
    # Protection — StoplossGuard + CooldownPeriod
    # -----------------------------------------------------------------------
    @property
    def protections(self):
        return [
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 12,  # 1 hour of 5m candles
                "trade_limit": 3,
                "stop_duration_candles": 12,    # pause for 1 hour
                "only_per_pair": False,
            },
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": 2,     # 10 min cooldown per pair
                "only_per_pair": True,
            },
        ]

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        return [(pair, self.informative_timeframe) for pair in pairs]

    def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Populate indicators for the 1h informative timeframe."""
        dataframe["ema50"] = pta.ema(dataframe["close"], length=50)
        dataframe["ema50_slope"] = dataframe["ema50"].pct_change(periods=1)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # -- 5m indicators --

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

        # Bollinger Bands (20, 2 std)
        bbands = pta.bbands(dataframe["close"], length=20, std=2.0)
        dataframe["bb_lower"] = bbands["BBL_20_2.0"]
        dataframe["bb_mid"] = bbands["BBM_20_2.0"]
        dataframe["bb_upper"] = bbands["BBU_20_2.0"]

        # BB width as fraction of midline
        dataframe["bb_width"] = (
            (dataframe["bb_upper"] - dataframe["bb_lower"]) / dataframe["bb_mid"]
        )

        # Volume SMA (20)
        dataframe["volume_sma_20"] = pta.sma(dataframe["volume"], length=20)

        # -- 1h informative: EMA50 slope as trend filter --
        if self.dp:
            inf_1h = self.dp.get_pair_dataframe(
                pair=metadata["pair"], timeframe=self.informative_timeframe
            )
            inf_1h = self.populate_indicators_1h(inf_1h, metadata)
            dataframe = merge_informative_pair(
                dataframe, inf_1h, self.timeframe, self.informative_timeframe,
                ffill=True,
            )
            dataframe.rename(
                columns={"ema50_slope_1h": "ema50_1h_slope"}, inplace=True
            )
        else:
            dataframe["ema50_1h_slope"] = 0.0

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                # Price at or below lower Bollinger Band
                (dataframe["close"] <= dataframe["bb_lower"])
                # RSI confirms oversold exhaustion
                & (dataframe["rsi"] < self.buy_rsi_threshold.value)
                # Volume spike — capitulation, not low-conviction slide
                & (dataframe["volume"] > dataframe["volume_sma_20"] * self.buy_volume_factor.value)
                # Enough BB width for profitable reversion
                & (dataframe["bb_width"] > self.buy_bb_width_min.value)
                # 1h trend filter — not a waterfall sell-off
                & (dataframe["ema50_1h_slope"] > self.buy_ema_slope_min.value)
                # Sanity
                & (dataframe["volume"] > 0)
            ),
            "enter_long",
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                # Price reverted to BB midline
                (dataframe["close"] >= dataframe["bb_mid"] * self.sell_bb_mid_offset.value)
                & (dataframe["volume"] > 0)
            ),
            "exit_long",
        ] = 1

        return dataframe

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        """
        Time-based tightening stoploss:
          0-10 min : -0.8% (use the static stoploss)
          10-20 min: -0.5%
          20-30 min: -0.4%
          30+ min  : -0.3%
        """
        elapsed = (current_time - trade.open_date_utc).total_seconds() / 60.0

        if elapsed > 30:
            return -0.003
        elif elapsed > 20:
            return -0.004
        elif elapsed > 10:
            return -0.005

        # Return 1 to keep the default stoploss (-0.8%)
        return 1
