from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import IntParameter, DecimalParameter from pandas import DataFrame import talib.abstract as ta class SwingTradingStrategy(IStrategy): can_short: bool = True timeframe = '4h' # Swing targets: larger moves and longer holds minimal_roi = { "0": 0.15, } stoploss = -0.15 # Hard floor – custom ATR stoploss will trail above this # We'll use a custom ATR-based stop instead of classical trailing stop use_custom_stoploss = True trailing_stop = False # Ensure enough candles for EMA200 and ATR(14) startup_candle_count = 210 process_only_new_candles = True # --- Hyperoptable parameters to enable default buy/sell spaces --- buy_rsi_max = IntParameter(55, 80, default=70, space='buy') buy_macd_hist_min = DecimalParameter(0.0, 0.010, decimals=3, default=0.000, space='buy') sell_rsi_min = IntParameter(60, 90, default=80, space='sell') sell_macd_hist_min = DecimalParameter(0.0, 0.010, decimals=3, default=0.000, space='sell') def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # MACD macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] # Trend filter – avoid counter-trend swings dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200) # Momentum / sanity filter (optional but helpful) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # ATR for volatility-aware stop management dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) # Volume filter helpers 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.15 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 populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Golden cross detection macd_cross_up = ( (dataframe['macd'] > dataframe['macdsignal']) & (dataframe['macd'].shift(1) <= dataframe['macdsignal'].shift(1)) ) # MACD histogram threshold (momentum confirmation) macd_hist_ok = (dataframe['macdhist'] > float(self.buy_macd_hist_min.value)) dataframe.loc[ ( macd_cross_up & (dataframe['close'] > dataframe['ema200']) & # trend filter (dataframe['rsi'] < int(self.buy_rsi_max.value)) & # avoid buying into extreme overbought macd_hist_ok & (dataframe['volume'] > 0) & (dataframe['volume'] > dataframe['volume_mean_slow']) ), 'enter_long' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Dead cross detection – primary exit condition macd_cross_down = ( (dataframe['macd'] < dataframe['macdsignal']) & (dataframe['macd'].shift(1) >= dataframe['macdsignal'].shift(1)) ) # MACD histogram negative enough (loss of momentum) macd_hist_weak = ((dataframe['macdsignal'] - dataframe['macd']) > float(self.sell_macd_hist_min.value)) dataframe.loc[ ( macd_cross_down | # Safety: loss of momentum or trend break (dataframe['rsi'] > int(self.sell_rsi_min.value)) | macd_hist_weak | (dataframe['close'] < dataframe['ema200']) ), 'exit_long' ] = 1 return dataframe # --- Custom ATR-based stoploss --- # Define a modest ATR multiplier for swings – can be hyperoptimized later atr_multiplier: float = 2.0 def custom_stoploss( self, pair: str, trade, # type: ignore[override] current_time, current_rate: float, current_profit: float, after_fill: bool, **kwargs, ) -> float | None: """ ATR-based dynamic stoploss. For long trades: stop_price = close - atr_multiplier * ATR(14). Convert absolute stop to distance ratio relative to current_rate. Returning None leaves current stop unchanged. """ try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last = dataframe.iloc[-1].squeeze() atr = float(last.get('atr', 0.0)) close = float(last.get('close', 0.0)) except Exception: return None if atr <= 0 or close <= 0: return None # Long-only logic – for futures shorting, mirror the formula accordingly stop_price = close - self.atr_multiplier * atr # If stop would be above current price due to extreme spikes, keep unchanged if stop_price >= current_rate: return None # Convert absolute stop to relative distance (sign ignored by framework) distance = abs((current_rate - stop_price) / current_rate) # Keep within hard floor enforced by self.stoploss (framework will cap it) return float(distance) if distance > 0 else None