""" RSI + MACD Strategy for Freqtrade Combines RSI oversold/overbought with MACD crossovers """ import talib.abstract as ta from pandas import DataFrame from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter from freqtrade.persistence import Trade from datetime import datetime import logging logger = logging.getLogger(__name__) class RSIMACDStrategy(IStrategy): """ RSI + MACD Crossover Strategy Buy when: - RSI is oversold (< 35) - MACD crosses above signal line - Price is above 200 EMA (trend filter) Sell when: - RSI is overbought (> 75) - MACD crosses below signal line """ # Strategy interface version INTERFACE_VERSION = 3 # Optimal timeframe for this strategy timeframe = '5m' # Can this strategy go short? can_short = False # Minimal ROI designed for the strategy minimal_roi = { "0": 0.10, # 10% profit target "30": 0.05, # 5% after 30 minutes "60": 0.025, # 2.5% after 1 hour "120": 0.01 # 1% after 2 hours } # Optimal stoploss stoploss = -0.10 # -10% # Trailing stop trailing_stop = True trailing_stop_positive = 0.01 trailing_stop_positive_offset = 0.02 trailing_only_offset_is_reached = True # Run "populate_indicators()" only for new candle process_only_new_candles = True # Use exit signal use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False # Hyperoptable parameters buy_rsi = IntParameter(20, 40, default=35, space="buy", optimize=True) sell_rsi = IntParameter(60, 85, default=75, space="sell", optimize=True) # Protections startup_candle_count: int = 200 @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 5 }, { "method": "MaxDrawdown", "trade_limit": 20, "stop_duration_candles": 30, "max_allowed_drawdown": 0.20 # 20% max drawdown }, { "method": "StoplossGuard", "trade_limit": 4, "stop_duration_candles": 30, "only_per_pair": False } ] def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame """ # RSI dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # MACD macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] # Moving Averages dataframe['sma_20'] = ta.SMA(dataframe, timeperiod=20) dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50) dataframe['ema_200'] = ta.EMA(dataframe, timeperiod=200) # Bollinger Bands bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) dataframe['bb_upper'] = bollinger['upperband'] dataframe['bb_middle'] = bollinger['middleband'] dataframe['bb_lower'] = bollinger['lowerband'] # ATR for volatility dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) # Volume indicators dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=20) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the entry signal for the given dataframe """ dataframe.loc[ ( # RSI oversold (dataframe['rsi'] < self.buy_rsi.value) & # MACD bullish crossover (dataframe['macd'] > dataframe['macdsignal']) & (dataframe['macd'].shift(1) <= dataframe['macdsignal'].shift(1)) & # Price above 200 EMA (uptrend) (dataframe['close'] > dataframe['ema_200']) & # Volume confirmation (dataframe['volume'] > dataframe['volume_sma'] * 0.5) & # Ensure we have data (dataframe['volume'] > 0) ), '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 """ dataframe.loc[ ( # RSI overbought (dataframe['rsi'] > self.sell_rsi.value) & # MACD bearish crossover (dataframe['macd'] < dataframe['macdsignal']) & (dataframe['macd'].shift(1) >= dataframe['macdsignal'].shift(1)) & # Volume confirmation (dataframe['volume'] > 0) ), 'exit_long'] = 1 return dataframe def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs ) -> float: """ Custom stake amount - uses 10% of available balance per trade """ return proposed_stake * 0.10 def leverage( self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, entry_tag: str | None, side: str, **kwargs ) -> float: """ Customize leverage for each new trade - no leverage for this strategy """ return 1.0