from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, CategoricalParameter import pandas as pd import numpy as np class CayiBot_BBPercent_Short(IStrategy): """ Short-only de grindeo bajista con Bollinger %B. Entra cuando hay sobreextensión arriba y giro bajista, filtra por EMA200 bajista. TP en banda inferior. Muestra BB% y ancho en el plot. """ timeframe = '4h' can_short = True process_only_new_candles = True startup_candle_count = 300 # 300*4h ~ 50 días para medias/ATR cómodos # ===== Parámetros tunables ===== bb_window = IntParameter(10, 80, default=20, space='sell') bb_dev = DecimalParameter(1.0, 3.5, decimals=1, default=2.0, space='sell') ma_type = CategoricalParameter(['SMA', 'EMA'], default='SMA', space='sell') price_src = CategoricalParameter(['close', 'hlc3'], default='close', space='sell') # Entrada short: sobreextensión previa y giro pb_enter_short = DecimalParameter(0.95, 1.30, decimals=2, default=1.10, space='sell') # Filtros de tendencia bajista ema_long_period = 200 ema_slope_lookback = 3 # en 4h, 3 velas ~ 12h de pendiente # TP/SL tp_to_lower = True # take-profit en banda inferior sl_atr_mult = 1.5 # SL defensivo por ATR # ===== Plot: BBs y BB% visibles ===== plot_config = { "main_plot": { "bb_upper": {}, "bb_mid": {}, "bb_lower": {}, "ema200": {}, }, "subplots": { "BB%": { "pb": {}, }, "BB Width": { "bb_width": {}, } } } def version(self) -> str: return "1.1-short-bbpercent-4h" # ---------- helpers ---------- @staticmethod def _hlc3(df: pd.DataFrame) -> pd.Series: return (df['high'] + df['low'] + df['close']) / 3.0 @staticmethod def _ema(s: pd.Series, n: int) -> pd.Series: return s.ewm(span=n, adjust=False).mean() def _bb(self, df: pd.DataFrame, window: int, dev: float, ma_type: str, price_src: str): src = df['close'] if price_src == 'close' else self._hlc3(df) basis = self._ema(src, window) if ma_type == 'EMA' else src.rolling(window).mean() stdev = src.rolling(window).std(ddof=0) upper = basis + dev * stdev lower = basis - dev * stdev pb = (src - lower) / (upper - lower) # BB% width = (upper - lower) / basis # recorta %B para evitar outliers absurdos en el plot return basis, upper, lower, pb.clip(-0.5, 1.5), width def populate_indicators(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: if df.empty: return df # BB% w = int(self.bb_window.value) dev = float(self.bb_dev.value) ma = self.ma_type.value src = self.price_src.value bb_mid, bb_up, bb_lo, pb, width = self._bb(df, w, dev, ma, src) df['bb_mid'] = bb_mid df['bb_upper'] = bb_up df['bb_lower'] = bb_lo df['pb'] = pb df['bb_width'] = width # EMA 200 + pendiente negativa df['ema200'] = self._ema(df['close'], self.ema_long_period) df['ema_slope_neg'] = df['ema200'].diff(self.ema_slope_lookback) < 0 df['below_ema'] = df['close'] < df['ema200'] # “Giro bajista” de %B tras sobreextensión thr = float(self.pb_enter_short.value) df['pb_rebound_short'] = (df['pb'].shift(1) > thr) & (df['pb'] < df['pb'].shift(1)) # ATR simple para SL tr1 = (df['high'] - df['low']).abs() tr2 = (df['high'] - df['close'].shift()).abs() tr3 = (df['low'] - df['close'].shift()).abs() df['atr'] = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1).rolling(14).mean() return df def populate_entry_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: df['enter_long'] = 0 df['enter_short'] = 0 # SHORT-ONLY: vende alto (rechazo desde banda superior) + marea bajista cond_short = ( df['pb_rebound_short'] & (df['close'] < df['bb_upper']) & (df['bb_width'] > 0) & df['below_ema'] & df['ema_slope_neg'] ) df.loc[cond_short, 'enter_short'] = 1 return df def populate_exit_trend(self, df: pd.DataFrame, metadata: dict) -> pd.DataFrame: df['exit_long'] = 0 df['exit_short'] = 0 # TP “abajo”: banda inferior o %B<=0 if self.tp_to_lower: hit_lower = (df['low'] <= df['bb_lower']) | (df['pb'] <= 0.0) df.loc[(df['enter_short'].shift().fillna(0) == 1) & hit_lower, 'exit_short'] = 1 # Falla: cierra si fuerza por encima de la banda superior fail = (df['close'] > df['bb_upper']) df.loc[(df['enter_short'].shift().fillna(0) == 1) & fail, 'exit_short'] = 1 return df # Stoploss porcentual dinámico relativo al open_rate (usa ATR) def custom_stoploss(self, pair: str, trade, current_time, current_rate, current_profit, **kwargs): atr = None try: atr = trade.strategy.safe_dataframe['atr'].iloc[-1] except Exception: pass if not atr or atr <= 0: return 0.05 # fallback 5% # convierte ATR a porcentaje relativo al precio de entrada return max(0.003, (float(self.sl_atr_mult) * atr) / trade.open_rate)