import logging from datetime import datetime from functools import reduce import talib.abstract as ta from pandas import DataFrame from technical import qtpylib from freqtrade.strategy import IStrategy, Trade, timeframe_to_minutes logger = logging.getLogger(__name__) class FreqaiExampleStrategy(IStrategy): """ FreqAI futures strategy (10x): high-conviction entries, prediction-scaled stake, profit-oriented exits. Tuned for Bitget USDT perpetuals. """ timeframe = "5m" TARGET_LEVERAGE = 10.0 PREDICTION_THRESHOLD = 0.012 PREDICTION_STRONG_MULT = 1.25 PREDICTION_WEAK_MULT = 0.25 MAX_STAKE_PRED_MULT = 2.0 DI_MAX_DEFAULT = 0.95 stoploss = -0.15 use_custom_stoploss = True trailing_stop = False _tf_min = timeframe_to_minutes(timeframe) minimal_roi = { "0": 0.10, str(_tf_min * 12): 0.06, str(_tf_min * 18): 0.04, str(_tf_min * 24): 0.02, str(_tf_min * 30): 0, } MIN_ADX = 22 MAX_ATR_PCT = 0.012 plot_config = { "main_plot": {}, "subplots": { "&-s_close": {"&-s_close": {"color": "blue"}}, "do_predict": {"do_predict": {"color": "brown"}}, }, } process_only_new_candles = True use_exit_signal = True startup_candle_count: int = 310 can_short = True @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 2, }, { "method": "StoplossGuard", "lookback_period_candles": 36, "trade_limit": 4, "stop_duration_candles": 12, "only_per_pair": False, }, { "method": "MaxDrawdown", "calculation_mode": "equity", "lookback_period_candles": 288, "trade_limit": 3, "stop_duration_candles": 72, "max_allowed_drawdown": 0.08, }, ] def _strategy_params(self) -> dict: defaults = { "prediction_threshold": self.PREDICTION_THRESHOLD, "prediction_strong_mult": self.PREDICTION_STRONG_MULT, "prediction_weak_mult": self.PREDICTION_WEAK_MULT, "min_adx": self.MIN_ADX, "max_atr_pct": self.MAX_ATR_PCT, "max_stake_pred_mult": self.MAX_STAKE_PRED_MULT, "target_leverage": self.TARGET_LEVERAGE, "exit_requires_profit": False, } overrides = self.config.get("strategy_params") or {} merged = {**defaults, **overrides} return merged def _prediction_threshold(self) -> float: return float(self._strategy_params()["prediction_threshold"]) def _strong_prediction_threshold(self) -> float: p = self._strategy_params() return self._prediction_threshold() * float(p["prediction_strong_mult"]) def _weak_prediction_threshold(self) -> float: p = self._strategy_params() return self._prediction_threshold() * float(p["prediction_weak_mult"]) def _min_adx(self) -> float: return float(self._strategy_params()["min_adx"]) def _max_atr_pct(self) -> float: return float(self._strategy_params()["max_atr_pct"]) def _target_leverage(self) -> float: return float(self._strategy_params()["target_leverage"]) def _exit_requires_profit(self) -> bool: return bool(self._strategy_params().get("exit_requires_profit", False)) def _di_threshold(self) -> float: return float( self.config.get("freqai", {}) .get("feature_parameters", {}) .get("DI_threshold", self.DI_MAX_DEFAULT) ) def feature_engineering_expand_all( self, dataframe: DataFrame, period: int, metadata: dict, **kwargs ) -> DataFrame: dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period) dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period) dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) bollinger = qtpylib.bollinger_bands( qtpylib.typical_price(dataframe), window=period, stds=2.2 ) dataframe["bb_lowerband-period"] = bollinger["lower"] dataframe["bb_middleband-period"] = bollinger["mid"] dataframe["bb_upperband-period"] = bollinger["upper"] dataframe["%-bb_width-period"] = ( dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"] ) / dataframe["bb_middleband-period"] dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"] dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) dataframe["%-relative_volume-period"] = ( dataframe["volume"] / dataframe["volume"].rolling(period).mean() ) return dataframe def feature_engineering_expand_basic( self, dataframe: DataFrame, metadata: dict, **kwargs ) -> DataFrame: dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-raw_volume"] = dataframe["volume"] dataframe["%-raw_price"] = dataframe["close"] return dataframe def feature_engineering_standard( self, dataframe: DataFrame, metadata: dict, **kwargs ) -> DataFrame: dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: dataframe["&-s_close"] = ( dataframe["close"] .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) .mean() / dataframe["close"] - 1 ) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe = self.freqai.start(dataframe, metadata, self) dataframe["atr_pct"] = ta.ATR(dataframe, timeperiod=14) / dataframe["close"] dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: strong_thr = self._strong_prediction_threshold() vol_ok = df["atr_pct"] < self._max_atr_pct() trend_ok = df["adx"] > self._min_adx() di_ok = df["do_predict"] == 1 if "DI_values" in df.columns: di_ok = di_ok & (df["DI_values"] < self._di_threshold()) enter_long_conditions = [ di_ok, df["&-s_close"] > strong_thr, vol_ok, trend_ok, ] if enter_long_conditions: df.loc[ reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] ] = (1, "long") enter_short_conditions = [ di_ok, df["&-s_close"] < -strong_thr, vol_ok, trend_ok, ] if enter_short_conditions: df.loc[ reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"] ] = (1, "short") return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: if self._exit_requires_profit(): return df weak = self._weak_prediction_threshold() exit_long_conditions = [ df["do_predict"] == 1, df["&-s_close"] < -weak, ] if exit_long_conditions: df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 exit_short_conditions = [ df["do_predict"] == 1, df["&-s_close"] > weak, ] if exit_short_conditions: df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 return df def custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs, ) -> str | bool | None: df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if df.empty or current_profit <= 0: return None pred = float(df.iloc[-1].get("&-s_close", 0) or 0) weak = self._weak_prediction_threshold() if trade.is_short: if pred > -weak: return "pred_weak_profit" if current_profit > 0.04 and pred > 0: return "profit_lock" else: if pred < weak: return "pred_weak_profit" if current_profit > 0.04 and pred < 0: return "profit_lock" return None def leverage( self, pair: str, current_time, current_rate: float, proposed_leverage: float, max_leverage: float, entry_tag: str | None, side: str, **kwargs, ) -> float: return min(self._target_leverage(), max_leverage) 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: df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if df.empty: return proposed_stake pred = abs(float(df.iloc[-1].get("&-s_close", 0) or 0)) thr = self._prediction_threshold() if pred <= thr: return proposed_stake max_mult = float(self._strategy_params()["max_stake_pred_mult"]) multiplier = min(pred / thr, max_mult) stake = proposed_stake * multiplier floor = min_stake if min_stake is not None else 0.0 return max(floor, min(stake, max_stake)) def custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs, ) -> float | None: df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if df.empty: return None last = df.iloc[-1].squeeze() atr_pct = float(last.get("atr_pct", 0.006) or 0.006) atr_stop = -min(0.018, max(0.008, atr_pct * 2.0)) if current_profit > 0.12: return -0.02 if current_profit > 0.08: return -0.025 return max(atr_stop, self.stoploss) def confirm_trade_entry( self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time, entry_tag, side: str, **kwargs, ) -> bool: df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if df.empty: return False last_candle = df.iloc[-1].squeeze() atr_pct = float(last_candle.get("atr_pct", 0) or 0) max_atr = self._max_atr_pct() if atr_pct > max_atr: logger.info( "Entry rejected %s: ATR%% %.4f above max %.4f", pair, atr_pct, max_atr ) return False if "DI_values" in df.columns: di_val = float(last_candle.get("DI_values", 0) or 0) if di_val >= self._di_threshold(): logger.info( "Entry rejected %s: DI_values %.4f >= %.4f", pair, di_val, self._di_threshold() ) return False pred = float(last_candle.get("&-s_close", 0) or 0) strong_thr = self._strong_prediction_threshold() if side == "long" and pred <= strong_thr: return False if side == "short" and pred >= -strong_thr: return False if side == "long": if rate > (last_candle["close"] * (1 + 0.0025)): return False else: if rate < (last_candle["close"] * (1 - 0.0025)): return False return True