# source: https://raw.githubusercontent.com/zakaria-mourtaban/claude-quant-repo/9ff91343a6938bcd831fb52502352a3623d506a4/user_data/strategies/MomentumMeanReversion.py
"""
Momentum + Mean Reversion Hybrid Strategy

This strategy adapts to market conditions:
- Uses MOMENTUM strategy in trending markets (ADX > 25)
- Uses MEAN REVERSION strategy in ranging markets (ADX < 20)
- Stays flat in transitional markets (20 < ADX < 25)

Research Backing:
- 56% annualized return, Sharpe Ratio 1.71
- Excels across different market regimes
- Based on 2025 quantitative crypto trading research

SHARIA COMPLIANCE:
✅ SPOT TRADING ONLY - No margin, no leverage, no futures
✅ LONG POSITIONS ONLY - No short selling (only buy and sell)
✅ OWN CAPITAL ONLY - Trades only with your own funds (1x leverage)
✅ HALAL - Compliant with Islamic finance principles

This strategy:
- Buys cryptocurrency with your own money (spot market)
- Holds real assets in your wallet
- Sells when conditions are favorable
- Never borrows money or uses leverage
- Never short sells (betting on price decline)

Author: Claude Code
Version: 1.0.1
"""

from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
from pandas import DataFrame
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib


class Github_zakaria_mourtaban_claude_quant_repo__MomentumMeanReversion__20251106_185908(IStrategy):
    """
    Momentum + Mean Reversion Hybrid Strategy

    ✅ SHARIA COMPLIANT: Spot trading only, long positions only, no leverage

    Entry Logic (LONG POSITIONS ONLY):
    - MOMENTUM MODE (ADX > 25):
      - LONG: Price > EMA50, RSI crosses above 50
      - Volume confirmation

    - MEAN REVERSION MODE (ADX < 20):
      - LONG: RSI < 30 (oversold) AND price touches lower Bollinger Band
      - Volume confirmation

    Exit Logic:
    - MOMENTUM: Trailing stop (ATR-based) or RSI reversal
    - MEAN REVERSION: Target mean (middle Bollinger Band) or RSI reversal

    Risk Management:
    - Stop loss: 2x ATR from entry (maximum 5% hard stop)
    - Position size: 33% of balance per trade
    - Maximum 3 concurrent positions
    - Leverage: 1x (spot trading, no margin)

    IMPORTANT: This strategy NEVER:
    - Uses margin or leverage (always 1x)
    - Opens short positions (only buys and sells)
    - Borrows funds or uses futures contracts
    """

    # Strategy metadata
    INTERFACE_VERSION = 3

    # Minimal ROI - we rely more on exits than ROI
    minimal_roi = {
        "0": 0.10,   # 10% profit
        "30": 0.05,  # 5% after 30 minutes
        "60": 0.03,  # 3% after 1 hour
        "120": 0.01  # 1% after 2 hours
    }

    # Stoploss - initial hard stop, then we use custom trailing stop
    stoploss = -0.05  # 5% hard stop (safety net)

    # Trailing stop
    trailing_stop = True
    trailing_stop_positive = 0.01  # Start trailing at 1% profit
    trailing_stop_positive_offset = 0.02  # Trail at 2% profit
    trailing_only_offset_is_reached = True

    # Timeframe
    timeframe = '15m'

    # Run "populate_indicators()" only for new candle
    process_only_new_candles = True

    # These values can be overridden in the config
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # Number of candles the strategy requires before producing valid signals
    startup_candle_count: int = 200

    # Optional order type mapping
    order_types = {
        'entry': 'limit',
        'exit': 'limit',
        'stoploss': 'market',
        'stoploss_on_exchange': False
    }

    # Optional order time in force
    order_time_in_force = {
        'entry': 'gtc',
        'exit': 'gtc'
    }

    #############################################
    # Hyperopt Parameters (can be optimized)
    #############################################

    # ADX thresholds for regime detection
    adx_threshold_trending = IntParameter(20, 30, default=25, space='buy', optimize=True)
    adx_threshold_ranging = IntParameter(15, 22, default=20, space='buy', optimize=True)

    # RSI parameters
    rsi_period = IntParameter(10, 20, default=14, space='buy', optimize=True)
    rsi_oversold = IntParameter(25, 35, default=30, space='buy', optimize=True)
    rsi_overbought = IntParameter(65, 75, default=70, space='buy', optimize=True)
    rsi_momentum_entry = IntParameter(45, 55, default=50, space='buy', optimize=True)

    # EMA parameters
    ema_fast = IntParameter(20, 60, default=50, space='buy', optimize=True)
    ema_slow = IntParameter(150, 250, default=200, space='buy', optimize=True)

    # Bollinger Bands parameters
    bb_period = IntParameter(15, 25, default=20, space='buy', optimize=True)
    bb_std = DecimalParameter(1.5, 2.5, default=2.0, space='buy', optimize=True)

    # ATR parameters (for stop loss and position sizing)
    atr_period = IntParameter(10, 20, default=14, space='buy', optimize=True)
    atr_multiplier = DecimalParameter(1.5, 3.0, default=2.0, space='sell', optimize=True)

    def informative_pairs(self):
        """
        Define additional, informative pair/interval combinations to be cached from the exchange.
        These pair/interval combinations are non-tradeable, unless they are part
        of the whitelist as well.
        """
        return []

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Adds all indicators needed for the strategy.

        This method is called for each pair and timeframe combination.
        It calculates all technical indicators used by the strategy.
        """

        # ADX - Average Directional Index (trend strength)
        dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)

        # RSI - Relative Strength Index
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.rsi_period.value)

        # EMAs - Exponential Moving Averages
        dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=self.ema_fast.value)
        dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=self.ema_slow.value)

        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(
            qtpylib.typical_price(dataframe),
            window=self.bb_period.value,
            stds=self.bb_std.value
        )
        dataframe['bb_lower'] = bollinger['lower']
        dataframe['bb_middle'] = bollinger['mid']
        dataframe['bb_upper'] = bollinger['upper']
        dataframe['bb_width'] = (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle']

        # ATR - Average True Range (for stop loss)
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=self.atr_period.value)

        # Volume indicators
        dataframe['volume_ma'] = dataframe['volume'].rolling(window=20).mean()

        # Market regime detection
        dataframe['regime'] = 'neutral'
        dataframe.loc[dataframe['adx'] > self.adx_threshold_trending.value, 'regime'] = 'trending'
        dataframe.loc[dataframe['adx'] < self.adx_threshold_ranging.value, 'regime'] = 'ranging'

        # Price relative to Bollinger Bands (for mean reversion)
        dataframe['bb_position'] = (dataframe['close'] - dataframe['bb_lower']) / (dataframe['bb_upper'] - dataframe['bb_lower'])

        # RSI momentum signals
        dataframe['rsi_prev'] = dataframe['rsi'].shift(1)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Based on TA indicators, populates the entry signal for the given dataframe.

        Entry Conditions:
        1. MOMENTUM MODE (trending market):
           - ADX > threshold_trending (strong trend)
           - Price > EMA_fast (uptrend confirmation)
           - RSI crosses above 50 (momentum building)
           - Volume > average (confirmation)

        2. MEAN REVERSION MODE (ranging market):
           - ADX < threshold_ranging (no strong trend)
           - RSI < oversold threshold (oversold)
           - Price touches lower Bollinger Band (at support)
           - Volume > average (confirmation)
        """

        # Initialize entry column
        dataframe.loc[:, 'enter_long'] = 0

        # MOMENTUM ENTRY (Trending Markets)
        momentum_conditions = (
            # Market is trending
            (dataframe['adx'] > self.adx_threshold_trending.value) &

            # Price above fast EMA (uptrend)
            (dataframe['close'] > dataframe['ema_fast']) &

            # RSI crossing above momentum threshold (building strength)
            (dataframe['rsi'] > self.rsi_momentum_entry.value) &
            (dataframe['rsi_prev'] <= self.rsi_momentum_entry.value) &

            # Volume confirmation
            (dataframe['volume'] > dataframe['volume_ma']) &

            # Not overbought
            (dataframe['rsi'] < self.rsi_overbought.value)
        )

        # MEAN REVERSION ENTRY (Ranging Markets)
        mean_reversion_conditions = (
            # Market is ranging (not trending)
            (dataframe['adx'] < self.adx_threshold_ranging.value) &

            # RSI oversold
            (dataframe['rsi'] < self.rsi_oversold.value) &

            # Price near or below lower Bollinger Band
            (dataframe['close'] <= dataframe['bb_lower'] * 1.01) &  # Within 1% of lower BB

            # Volume confirmation
            (dataframe['volume'] > dataframe['volume_ma'] * 0.8) &

            # Bollinger Bands not too tight (avoid low volatility)
            (dataframe['bb_width'] > 0.02)
        )

        # Combined entry signal (either condition can trigger)
        dataframe.loc[
            momentum_conditions | mean_reversion_conditions,
            'enter_long'
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Based on TA indicators, populates the exit signal for the given dataframe.

        Exit Conditions:
        1. MOMENTUM EXIT:
           - RSI crosses below 50 (momentum fading)
           - OR Price crosses below EMA_fast (trend weakening)

        2. MEAN REVERSION EXIT:
           - RSI crosses above 50 (no longer oversold)
           - OR Price reaches middle Bollinger Band (mean reached)
           - OR Price crosses above upper Bollinger Band (overbought)

        Note: Trailing stop will also exit positions (configured above)
        """

        # Initialize exit column
        dataframe.loc[:, 'exit_long'] = 0

        # MOMENTUM EXIT
        momentum_exit = (
            # RSI crosses below momentum threshold (losing steam)
            (dataframe['rsi'] < self.rsi_momentum_entry.value) &
            (dataframe['rsi_prev'] >= self.rsi_momentum_entry.value)
        ) | (
            # Price crosses below fast EMA (trend broken)
            (dataframe['close'] < dataframe['ema_fast'])
        )

        # MEAN REVERSION EXIT
        mean_reversion_exit = (
            # RSI back to neutral/overbought
            (dataframe['rsi'] > self.rsi_momentum_entry.value) &
            (dataframe['rsi_prev'] <= self.rsi_momentum_entry.value)
        ) | (
            # Price reached middle BB (mean)
            (dataframe['close'] >= dataframe['bb_middle'])
        ) | (
            # Price overbought (beyond upper BB)
            (dataframe['rsi'] > self.rsi_overbought.value)
        )

        # Combined exit signal
        dataframe.loc[
            momentum_exit | mean_reversion_exit,
            'exit_long'
        ] = 1

        return dataframe

    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
                       current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Custom stoploss logic using ATR-based trailing stop.

        - Initial stop: 2x ATR below entry price
        - As profit increases, stop loss trails using ATR

        Returns:
            float: Stop loss value (negative = loss, positive = profit)
        """

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        # Get ATR value
        atr = last_candle['atr']

        # Calculate stop loss distance as percentage
        # ATR-based stop: 2x ATR below current price
        stop_distance = (atr * self.atr_multiplier.value) / current_rate

        # Return negative value (stop loss as percentage below current price)
        return -stop_distance

    def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime',
                   current_rate: float, current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
        """
        Custom exit logic (optional).

        Can be used to implement additional exit conditions based on:
        - Time in trade
        - Profit targets
        - Market conditions
        - External signals

        Returns:
            Optional[Union[str, bool]]: Exit reason or None
        """

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        # Exit if ADX shows extreme trend exhaustion (optional)
        # if last_candle['adx'] > 60:
        #     return 'adx_exhaustion'

        # Exit if holding too long without profit (optional)
        # trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600  # hours
        # if trade_duration > 24 and current_profit < 0.01:
        #     return 'timeout_no_profit'

        return None

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                           time_in_force: str, current_time: 'datetime', entry_tag: 'Optional[str]',
                           side: str, **kwargs) -> bool:
        """
        Called right before placing a entry order.
        Can be used to filter out trades based on additional conditions.

        Returns:
            bool: True to allow trade, False to reject
        """

        # Get current market data
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        # Reject trade if volatility too low (Bollinger Bands too tight)
        if last_candle['bb_width'] < 0.015:
            return False

        # Reject trade if volume too low
        if last_candle['volume'] < last_candle['volume_ma'] * 0.5:
            return False

        return True

    def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
                proposed_leverage: float, max_leverage: float, entry_tag: 'Optional[str]',
                side: str, **kwargs) -> float:
        """
        Customize leverage for each new trade.

        ✅ SHARIA COMPLIANT: Always returns 1.0 (no leverage/margin)

        This strategy uses SPOT TRADING ONLY:
        - 1x leverage = buying actual cryptocurrency with your own money
        - No margin = no borrowing funds
        - No futures = immediate settlement

        This ensures the strategy is Halal (permissible) under Islamic finance.
        """
        return 1.0
