from pandas import DataFrame import talib.abstract as ta from freqtrade.strategy import IStrategy, merge_informative_pair class HalalSpotStrategy(IStrategy): INTERFACE_VERSION = 3 timeframe = "15m" # Higher timeframe used to confirm the broader trend before entering. informative_timeframe = "4h" can_short = False minimal_roi = { "0": 0.05, # exit at +5% immediately "120": 0.025, # after 2h, exit at +2.5% "480": 0.012, # after 8h, exit at +1.2% "1440": 0.005, # after 24h, exit at +0.5% } stoploss = -0.05 # Trailing stop activates after a meaningful move and then protects profit. trailing_stop = True trailing_stop_positive = 0.015 trailing_stop_positive_offset = 0.03 trailing_only_offset_is_reached = True process_only_new_candles = True startup_candle_count = 200 # Protections guard against whipsaw re-entries and drawdown clusters. @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 4, }, { "method": "StoplossGuard", "lookback_period_candles": 24, "trade_limit": 2, "stop_duration_candles": 12, "only_per_pair": True, }, { "method": "MaxDrawdown", "lookback_period_candles": 48, "trade_limit": 4, "stop_duration_candles": 12, "max_allowed_drawdown": 0.1, }, ] def informative_pairs(self): pairs = self.dp.current_whitelist() return [(pair, self.informative_timeframe) for pair in pairs] def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["sma_fast"] = ta.SMA(dataframe, timeperiod=20) dataframe["sma_slow"] = ta.SMA(dataframe, timeperiod=50) dataframe["ema_trend"] = ta.EMA(dataframe, timeperiod=200) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["atr_ratio"] = dataframe["atr"] / dataframe["close"] dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean() dataframe["volume_ratio"] = dataframe["volume"] / dataframe["volume_mean"] # Higher-timeframe trend filter keeps entries aligned with the broader move. informative = self.dp.get_pair_dataframe( pair=metadata["pair"], timeframe=self.informative_timeframe ) informative["sma_trend"] = ta.SMA(informative, timeperiod=50) informative["sma_trend_slope"] = informative["sma_trend"] - informative[ "sma_trend" ].shift(3) dataframe = merge_informative_pair( dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True ) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: htf_close = f"close_{self.informative_timeframe}" htf_trend = f"sma_trend_{self.informative_timeframe}" htf_trend_slope = f"sma_trend_slope_{self.informative_timeframe}" dataframe.loc[ ( (dataframe["rsi"] > 32) & (dataframe["rsi"].shift(1) <= 32) & (dataframe["sma_fast"] > dataframe["sma_slow"]) & (dataframe["close"] > dataframe["ema_trend"]) # Only buy recovering dips while the 4h trend is up. & (dataframe[htf_close] > dataframe[htf_trend]) & (dataframe[htf_trend_slope] > 0) & (dataframe["close"] > dataframe["open"]) & (dataframe["volume_ratio"] > 1.0) & (dataframe["atr_ratio"] > 0.002) & (dataframe["atr_ratio"] < 0.06) & (dataframe["volume"] > 0) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( (dataframe["rsi"] > 70) | (dataframe["sma_fast"] < dataframe["sma_slow"]) | (dataframe["close"] < dataframe["ema_trend"]) ), "exit_long", ] = 1 return dataframe