""" BOS + FRVP + LVN + VWAP — SHORT Only, STRICT 4-of-4 Confluence ================================================================ Refined version based on backtest analysis: - SHORT only (LONG side was -4%, removed) - STRICT 4-of-4 pillar requirement (3-of-4 produced too many losers) - Tighter 4% stop loss (8% was too wide — 60% of trades hit it) - Keep the winning trailing stop (2.5% trail after 12%) - Only trade top pairs (remove AVAX, DOT, XLM, XRP, DOGE) """ import numpy as np import pandas as pd from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter class BOSV4ShortStrict(IStrategy): INTERFACE_VERSION = 3 can_short = True timeframe = "1h" startup_candle_count = 100 stoploss = -0.04 # 4% tight stop (was 8%, 60% of trades hit it) minimal_roi = {"0": 100} use_exit_signal = True exit_profit_only = False # Winning trailing: 2.5% trail after 12% — this was the best exit trailing_stop = True trailing_stop_positive = 0.025 trailing_stop_positive_offset = 0.12 trailing_only_offset_is_reached = True stake_amount = "unlimited" max_open_trades = 3 # Fewer concurrent = less DD swing_lookback = IntParameter(10, 40, default=20, space="buy", optimize=False) volume_bins = IntParameter(15, 50, default=25, space="buy", optimize=False) lvn_threshold = DecimalParameter(0.2, 0.7, default=0.5, decimals=2, space="buy", optimize=False) vwap_proximity_pct = DecimalParameter(0.3, 1.5, default=0.5, decimals=2, space="buy", optimize=False) min_body_ratio = DecimalParameter(0.3, 0.7, default=0.5, decimals=2, space="buy", optimize=False) # Stricter leverage_num = DecimalParameter(1, 20, default=10.0, decimals=1, space="buy", optimize=False) def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs): return float(self.leverage_num.value) def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame: df = dataframe.copy() # RSI delta = df["close"].diff() gain = delta.clip(lower=0) loss = -delta.clip(upper=0) avg_gain = gain.ewm(alpha=1/14, min_periods=14).mean() avg_loss = loss.ewm(alpha=1/14, min_periods=14).mean() rs = avg_gain / avg_loss.replace(0, np.nan) df["rsi"] = 100 - (100 / (1 + rs)) # ATR high_low = df["high"] - df["low"] high_close = np.abs(df["high"] - df["close"].shift()) low_close = np.abs(df["low"] - df["close"].shift()) tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) df["atr"] = tr.rolling(14).mean() # EMAs df["ema_50"] = df["close"].ewm(span=50, adjust=False).mean() df["ema_200"] = df["close"].ewm(span=200, adjust=False).mean() # VWAP df = self._compute_vwap(df) # BOS df = self._compute_bos(df) # Volume profile + LVN df = self._compute_volume_profile(df) # Confluence df = self._compute_confluence(df) return df def _compute_vwap(self, df): typical_price = (df["high"] + df["low"] + df["close"]) / 3 df["vwap_tp_vol"] = typical_price * df["volume"] df["date_only"] = df["date"].dt.date cum_tp_vol = df.groupby("date_only")["vwap_tp_vol"].cumsum() cum_vol = df.groupby("date_only")["volume"].cumsum() df["vwap"] = cum_tp_vol / cum_vol.replace(0, np.nan) df["vwap"] = df["vwap"].ffill() df["vwap_dist_pct"] = ((df["close"] - df["vwap"]) / df["vwap"]) * 100 df["vwap_slope"] = df["vwap"].diff(3) / df["vwap"].shift(3) * 100 df.drop(columns=["date_only", "vwap_tp_vol"], inplace=True) return df def _compute_bos(self, df): lookback = int(self.swing_lookback.value) half = lookback // 2 highs = df["high"].values lows = df["low"].values n = len(df) is_sh = np.zeros(n, dtype=bool) is_sl = np.zeros(n, dtype=bool) for i in range(half, n - half): if highs[i] == np.max(highs[i - half:i + half + 1]): is_sh[i] = True if lows[i] == np.min(lows[i - half:i + half + 1]): is_sl[i] = True last_sh = np.full(n, np.nan) last_sl = np.full(n, np.nan) sh_val = sl_val = np.nan for i in range(n): if is_sh[i]: sh_val = highs[i] if is_sl[i]: sl_val = lows[i] last_sh[i] = sh_val last_sl[i] = sl_val df["last_swing_high"] = last_sh df["last_swing_low"] = last_sl bullish_bos = ((df["close"] > pd.Series(last_sh).shift(1)) & pd.Series(last_sh).shift(1).notna()).astype(int) bearish_bos = ((df["close"] < pd.Series(last_sl).shift(1)) & pd.Series(last_sl).shift(1).notna()).astype(int) bos_trend = np.zeros(n, dtype=int) state = 0 for i in range(n): if bullish_bos.iloc[i] == 1: state = 1 elif bearish_bos.iloc[i] == 1: state = -1 bos_trend[i] = state df["bos_trend"] = bos_trend return df def _compute_volume_profile(self, df): lookback = int(self.swing_lookback.value) n_bins = int(self.volume_bins.value) lvn_thresh = float(self.lvn_threshold.value) vwap_prox = float(self.vwap_proximity_pct.value) n = len(df) lvn_top = np.full(n, np.nan) lvn_bottom = np.full(n, np.nan) highs = df["high"].values lows = df["low"].values volumes = df["volume"].values closes = df["close"].values for i in range(lookback, n): win_low = lows[i - lookback:i + 1].min() win_high = highs[i - lookback:i + 1].max() if win_high == win_low: continue bin_edges = np.linspace(win_low, win_high, n_bins + 1) vol_per_bin = np.zeros(n_bins) for j in range(i - lookback, i + 1): if volumes[j] <= 0: continue candle_range = highs[j] - lows[j] if candle_range <= 0: continue overlaps = np.maximum(0, np.minimum(highs[j], bin_edges[1:]) - np.maximum(lows[j], bin_edges[:-1])) vol_per_bin += volumes[j] * (overlaps / candle_range) total_vol = vol_per_bin.sum() if total_vol == 0: continue poc_bin = np.argmax(vol_per_bin) poc_volume = vol_per_bin[poc_bin] lvn_bins = np.where((vol_per_bin > 0) & (vol_per_bin < lvn_thresh * poc_volume))[0] if len(lvn_bins) > 0: zones = [] start = lvn_bins[0] for k in range(1, len(lvn_bins)): if lvn_bins[k] != lvn_bins[k - 1] + 1: zones.append((start, lvn_bins[k - 1])) start = lvn_bins[k] zones.append((start, lvn_bins[-1])) current_close = closes[i] best_zone = None best_dist = float("inf") for zs, ze in zones: zone_mid = (bin_edges[zs] + bin_edges[ze + 1]) / 2 dist = abs(zone_mid - current_close) if dist < best_dist: best_dist = dist best_zone = (zs, ze) if best_zone: lvn_bottom[i] = bin_edges[best_zone[0]] lvn_top[i] = bin_edges[best_zone[1] + 1] df["lvn_top"] = lvn_top df["lvn_bottom"] = lvn_bottom df["price_in_lvn"] = ( df["lvn_bottom"].notna() & (df["close"] >= df["lvn_bottom"]) & (df["close"] <= df["lvn_top"]) ).astype(int) df["near_vwap"] = (df["vwap_dist_pct"].abs() <= vwap_prox).astype(int) return df def _compute_confluence(self, df): min_br = float(self.min_body_ratio.value) df["body"] = (df["close"] - df["open"]).abs() df["candle_range"] = df["high"] - df["low"] df["body_ratio"] = df["body"] / df["candle_range"].replace(0, np.nan) # REQUIRE strong bearish candle (body >= 50% of range) df["bearish_candle"] = ((df["close"] < df["open"]) & (df["body_ratio"] >= min_br)).astype(int) df["vwap_declining"] = (df["vwap_slope"] < 0).astype(int) # 4 pillars for SHORT pillar_bos = (df["bos_trend"] == -1).astype(int) pillar_lvn = df["price_in_lvn"] pillar_vwap = (df["near_vwap"] & df["vwap_declining"]).astype(int) pillar_candle = df["bearish_candle"] df["short_pillar_count"] = pillar_bos + pillar_lvn + pillar_vwap + pillar_candle # STRICT: ALL 4 pillars required df["short_strict"] = (df["short_pillar_count"] >= 4).astype(int) # RELAXED: 3-of-4 pillars (but always need BOS) df["short_relaxed"] = ((df["short_pillar_count"] >= 3) & (pillar_bos == 1)).astype(int) # Additional filter: RSI not extremely oversold df["rsi_not_extreme"] = (df["rsi"] > 30).astype(int) return df def populate_entry_trend(self, dataframe, metadata): # SHORT: STRICT 4-of-4 + RSI > 30 dataframe.loc[ (dataframe["short_strict"] == 1) & (dataframe["rsi_not_extreme"] == 1) & (dataframe["volume"] > 0), ["enter_short", "enter_tag"] ] = (1, "bos_frvp_lvn_vwap_strict_short") # NO long entries return dataframe def populate_exit_trend(self, dataframe, metadata): # Exit short: BOS reversal or RSI deep oversold dataframe.loc[ (dataframe["bos_trend"] == 1) | (dataframe["rsi"] < 25), ["exit_short", "exit_tag"] ] = (1, "bos_reversal_or_deep_oversold") # Force exit longs dataframe.loc[ (dataframe["volume"] > 0), ["exit_long", "exit_tag"] ] = (1, "no_long_allowed") return dataframe def custom_stoploss(self, pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs): # Progressive tightening (keep winning trades) if current_profit > 0.08: return -0.01 # Breakeven+ after 8% if current_profit > 0.05: return -0.02 # Tight after 5% if current_profit > 0.03: return -0.03 # Moderate after 3% return None # Use default trailing otherwise def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs): dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if len(dataframe) < 1: return None last = dataframe.iloc[-1] # Exit short if bullish BOS reversal (and profitable) if trade.is_short and last.get("bos_trend", 0) == 1 and current_profit > 0.02: return "bullish_bos_reversal" return None