from datetime import datetime from pandas import DataFrame import talib.abstract as ta from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter, informative class TrendRegimeV1(IStrategy): """Simple 15m trend-regime strategy for Hyperliquid futures dry-run testing.""" INTERFACE_VERSION = 3 can_short = True timeframe = "15m" startup_candle_count = 250 process_only_new_candles = True position_adjustment_enable = False max_entry_position_adjustment = 0 minimal_roi = { "0": 0.04, "120": 0.02, "240": 0.0, } stoploss = -0.06 trailing_stop = True trailing_stop_positive = 0.015 trailing_stop_positive_offset = 0.03 trailing_only_offset_is_reached = True use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False rsi_long_min = IntParameter(35, 55, default=45, space="buy", optimize=False) rsi_long_max = IntParameter(60, 78, default=68, space="buy", optimize=False) rsi_short_min = IntParameter(22, 40, default=32, space="buy", optimize=False) rsi_short_max = IntParameter(45, 65, default=55, space="buy", optimize=False) adx_min = IntParameter(18, 35, default=22, space="buy", optimize=False) volume_min_factor = DecimalParameter(0.2, 1.2, default=0.5, decimals=2, space="buy", optimize=False) rsi_long_exit = IntParameter(72, 88, default=78, space="sell", optimize=False) rsi_short_exit = IntParameter(12, 28, default=22, space="sell", optimize=False) @property def protections(self) -> list[dict]: return [ { "method": "CooldownPeriod", "stop_duration_candles": 2, }, { "method": "StoplossGuard", "lookback_period_candles": 48, "trade_limit": 3, "stop_duration_candles": 12, "required_profit": 0.0, "only_per_pair": False, "only_per_side": False, }, { "method": "MaxDrawdown", "calculation_mode": "equity", "lookback_period_candles": 96, "trade_limit": 10, "stop_duration_candles": 24, "max_allowed_drawdown": 0.12, }, ] @informative("1h") def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["volume_mean_20"] = dataframe["volume"].rolling(20, min_periods=20).mean() dataframe["ema_50_slope"] = dataframe["ema_50"] - dataframe["ema_50"].shift(3) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: volume_ok = ( (dataframe["volume"] > 0) & (dataframe["volume_mean_20"] > 0) & (dataframe["volume"] >= dataframe["volume_mean_20"] * self.volume_min_factor.value) ) long_reclaim_ema50 = ( (dataframe["close"] > dataframe["ema_50"]) & ( (dataframe["close"].shift(1) <= dataframe["ema_50"].shift(1)) | ((dataframe["low"] <= dataframe["ema_50"]) & (dataframe["close"] > dataframe["open"])) ) ) short_reject_ema50 = ( (dataframe["close"] < dataframe["ema_50"]) & ( (dataframe["close"].shift(1) >= dataframe["ema_50"].shift(1)) | ((dataframe["high"] >= dataframe["ema_50"]) & (dataframe["close"] < dataframe["open"])) ) ) long_conditions = ( volume_ok & (dataframe["close"] > dataframe["ema_200"]) & (dataframe["ema_50"] > dataframe["ema_200"]) & (dataframe["ema_50_slope"] > 0) & (dataframe["close_1h"] > dataframe["ema_200_1h"]) & (dataframe["ema_50_1h"] > dataframe["ema_200_1h"]) & (dataframe["adx"] > self.adx_min.value) & (dataframe["adx_1h"] > self.adx_min.value) & (dataframe["rsi"] > self.rsi_long_min.value) & (dataframe["rsi"] < self.rsi_long_max.value) & long_reclaim_ema50 ) short_conditions = ( volume_ok & (dataframe["close"] < dataframe["ema_200"]) & (dataframe["ema_50"] < dataframe["ema_200"]) & (dataframe["ema_50_slope"] < 0) & (dataframe["close_1h"] < dataframe["ema_200_1h"]) & (dataframe["ema_50_1h"] < dataframe["ema_200_1h"]) & (dataframe["adx"] > self.adx_min.value) & (dataframe["adx_1h"] > self.adx_min.value) & (dataframe["rsi"] > self.rsi_short_min.value) & (dataframe["rsi"] < self.rsi_short_max.value) & short_reject_ema50 ) dataframe.loc[long_conditions, ["enter_long", "enter_tag"]] = (1, "long_trend_pullback") dataframe.loc[short_conditions, ["enter_short", "enter_tag"]] = (1, "short_trend_pullback") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: exit_long_ema50 = (dataframe["volume"] > 0) & (dataframe["close"] < dataframe["ema_50"]) exit_long_regime = (dataframe["volume"] > 0) & (dataframe["ema_50"] < dataframe["ema_200"]) exit_long_rsi = (dataframe["volume"] > 0) & (dataframe["rsi"] > self.rsi_long_exit.value) exit_short_ema50 = (dataframe["volume"] > 0) & (dataframe["close"] > dataframe["ema_50"]) exit_short_regime = (dataframe["volume"] > 0) & (dataframe["ema_50"] > dataframe["ema_200"]) exit_short_rsi = (dataframe["volume"] > 0) & (dataframe["rsi"] < self.rsi_short_exit.value) dataframe.loc[exit_long_ema50, ["exit_long", "exit_tag"]] = (1, "ema50_break") dataframe.loc[exit_long_regime, ["exit_long", "exit_tag"]] = (1, "regime_lost") dataframe.loc[exit_long_rsi, ["exit_long", "exit_tag"]] = (1, "rsi_extreme") dataframe.loc[exit_short_ema50, ["exit_short", "exit_tag"]] = (1, "ema50_break") dataframe.loc[exit_short_regime, ["exit_short", "exit_tag"]] = (1, "regime_lost") dataframe.loc[exit_short_rsi, ["exit_short", "exit_tag"]] = (1, "rsi_extreme") return dataframe 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: return 1.0