# source: https://raw.githubusercontent.com/wynnforthework/quant-strategies-knowledge/523670ea407d3e361fdf8adcd2c74819d2275e83/freqtrade-strategies/GridLikeStrategy.py
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy import IntParameter, DecimalParameter
from pandas import DataFrame
import talib.abstract as ta


class Github_wynnforthework_quant_strategies_knowledge__GridLikeStrategy__20251007_112722(IStrategy):
    can_short: bool = True
    timeframe = '15m'

    # Mean-reversion ROI with small targets per layer
    minimal_roi = {
        "0": 0.01,
        "60": 0.02,
        "180": 0.03,
    }
    stoploss = -0.12

    # Use custom ATR-based stoploss to limit one-way trend risk
    use_custom_stoploss = True
    trailing_stop = False

    # Allow limited position adjustments (DCA) to simulate grid layering
    position_adjustment_enable = True
    # Max additional entries after the initial one (e.g., 2 extra layers)
    max_entry_position_adjustment = 2

    startup_candle_count = 200

    # --- Hyperoptable parameters to enable default buy/sell spaces ---
    buy_adx_max = IntParameter(20, 35, default=25, space='buy')
    buy_ema200_ratio = DecimalParameter(0.90, 1.00, decimals=3, default=0.92, space='buy')

    sell_bb_mid_offset = DecimalParameter(0.0, 0.02, decimals=3, default=0.0, space='sell')
    sell_adx_min = IntParameter(25, 40, default=30, space='sell')

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Bollinger bands as core grid bands
        bb_upper, bb_mid, bb_lower = ta.BBANDS(
            dataframe['close'], timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0
        )
        dataframe['bb_upper'] = bb_upper
        dataframe['bb_mid'] = bb_mid
        dataframe['bb_lower'] = bb_lower

        # Volatility for dynamic grid steps and stoploss
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)

        # Trend guards to avoid heavy DCA against strong trends
        dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
        dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)

        # Volume sanity
        dataframe['volume_mean_slow'] = dataframe['volume'].rolling(30).mean()

        return dataframe

    def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs) -> float:
        stop = abs(float(self.stoploss)) if getattr(self, "stoploss", None) is not None else 0.12
        base = 0.05 / stop if stop > 0 else (proposed_leverage or 1.0)
        base = max(1.0, min(float(base), float(max_leverage)))
        return base

    def _grid_long_conditions(self, df: DataFrame) -> DataFrame:
        # Grid anchoring: below mid and below lower band are potential buy zones
        # Add ATR-stepped layers below lower band
        layer1 = (df['close'] <= df['bb_lower'])
        layer2 = (df['close'] <= (df['bb_lower'] - 0.5 * df['atr']))
        layer3 = (df['close'] <= (df['bb_lower'] - 1.0 * df['atr']))

        # Trend/vol filters: avoid strong downtrends (adx high and price << ema200)
        guard = (
            (df['close'] >= float(self.buy_ema200_ratio.value) * df['ema200']) |
            (df['adx'] < int(self.buy_adx_max.value))
        )

        vol_ok = (df['volume'] > 0) & (df['volume'] > df['volume_mean_slow'])

        cond = (df['close'] < df['bb_mid']) & (layer1 | layer2 | layer3) & guard & vol_ok
        return cond

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Base long entry on grid condition
        dataframe.loc[
            self._grid_long_conditions(dataframe),
            'enter_long'
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Mean reversion exits near/above midline or upper band
        exit_mean = (dataframe['close'] >= dataframe['bb_mid'] * (1 + float(self.sell_bb_mid_offset.value)))
        exit_strong = (dataframe['close'] >= dataframe['bb_upper'])

        # Emergency: trend flips down hard
        emergency = (
            (dataframe['adx'] > int(self.sell_adx_min.value)) & (dataframe['close'] < 0.98 * dataframe['ema200'])
        )

        dataframe.loc[(exit_mean | exit_strong | emergency), 'exit_long'] = 1
        return dataframe

    # --- DCA settings ---
    # Freqtrade uses `position_adjustment_enable`/`max_entry_position_adjustment`.
    # Optional: You can define `custom_entry_position_adjustment` for variable DCA sizing.

    # --- ATR-based custom stoploss to limit one-way trend blowups ---
    atr_sl_multiplier: float = 2.2

    def custom_stoploss(
        self,
        pair: str,
        trade,  # type: ignore[override]
        current_time,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float | None:
        try:
            df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            last = df.iloc[-1].squeeze()
            atr = float(last.get('atr', 0.0))
            close = float(last.get('close', 0.0))
            ema200 = float(last.get('ema200', 0.0))
            adx = float(last.get('adx', 0.0))
        except Exception:
            return None

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

        # Base ATR stop
        stop_price = close - self.atr_sl_multiplier * atr

        # If market turns into strong downtrend, tighten further toward EMA200 threshold
        if adx >= 30 and close < ema200:
            # raise stop closer (reduce distance)
            stop_price = max(stop_price, 0.98 * ema200)

        if stop_price >= current_rate:
            return None

        distance = abs((current_rate - stop_price) / current_rate)
        return float(distance) if distance > 0 else None
