from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import IntParameter, DecimalParameter from pandas import DataFrame import talib.abstract as ta class GridLikeStrategy(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