""" 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 MomentumMeanReversion(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