from __future__ import annotations try: from freqtrade.strategy import IStrategy except ModuleNotFoundError: # Lets this module import (and its pure helpers be unit-tested) without a full # freqtrade install. In the freqtrade Docker image the real IStrategy is used. IStrategy = object # type: ignore def atr_stop_price(rate: float, atr: float, mult: float) -> float: """Price of an ATR-based stop `mult` ATRs below entry `rate`. Never above entry.""" return rate - max(0.0, mult) * max(0.0, atr) class SwingV1(IStrategy): """v1 hypothesis: RSI mean-reversion with a trend filter and an ATR stop, plus a momentum entry (fast EMA crossing the trend EMA) so the bot also trades breakouts rather than only waiting for dips. Every manifest tunable is read from config so M3 can change it via a restart; the EMA lengths and momentum RSI band are fixed.""" timeframe = "1h" can_short = False startup_candle_count = 200 # native config keys (stoploss / minimal_roi / trailing_*) come from config.json; # this attribute is only a safety floor — the real stop is custom_stoploss below. stoploss = -0.99 use_custom_stoploss = True def _p(self, key: str, default): return self.config.get("strategy_params", {}).get(key, default) # Fixed (not in the manifest): shorter EMAs than v1's EMA100 so the trend filter # admits more bars and the momentum entry reacts within a day or two on 1h candles. ema_fast_len = 20 ema_slow_len = 50 momentum_rsi_band = (50, 70) # momentum entries need strength, but not overbought def populate_indicators(self, df, metadata: dict): import pandas_ta as pta df["rsi"] = pta.rsi(df["close"], length=14) df["ema_fast"] = pta.ema(df["close"], length=self.ema_fast_len) df["ema_slow"] = pta.ema(df["close"], length=self.ema_slow_len) df["atr"] = pta.atr(df["high"], df["low"], df["close"], length=14) return df def populate_entry_trend(self, df, metadata: dict): rsi_buy = self._p("rsi_buy_threshold", 40) uptrend = df["close"] > df["ema_slow"] has_volume = df["volume"] > 0 # 1) mean-reversion: RSI crossing up through the threshold, only in an uptrend rsi_rebound = ( (df["rsi"] > rsi_buy) & (df["rsi"].shift(1) <= rsi_buy) & uptrend & has_volume ) # 2) momentum: fast EMA crossing above the trend EMA with healthy, non-overbought RSI lo, hi = self.momentum_rsi_band ema_cross = ( (df["ema_fast"] > df["ema_slow"]) & (df["ema_fast"].shift(1) <= df["ema_slow"].shift(1)) & df["rsi"].between(lo, hi) & has_volume ) df.loc[rsi_rebound, ["enter_long", "enter_tag"]] = (1, "rsi_rebound") df.loc[ema_cross & ~rsi_rebound, ["enter_long", "enter_tag"]] = (1, "ema_cross") return df def populate_exit_trend(self, df, metadata: dict): # exits handled by minimal_roi, trailing stop, and custom_stoploss return df def custom_stoploss(self, pair, trade, current_time, current_rate, current_profit, **kwargs) -> float: mult = self._p("atr_stop_multiplier", 2.0) df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if df is None or len(df) == 0: return self.stoploss atr = float(df["atr"].iloc[-1]) stop_price = atr_stop_price(trade.open_rate, atr, mult) # convert to a ratio relative to current rate for freqtrade return (stop_price / current_rate) - 1.0