# BinHV45_Regime_5m.py - v3 # # CHANGES FROM v2: # - REMOVED RSI divergence check — too rare on 5m, blocked 99% of valid entries # Backtest showed identical results (3 trades) with or without it. # RSI divergence is valid on 4h+ but not useful as a 5m gate. # - KEPT 30m downtrend filter — confirmed working, 314 trades vs 113 on raw BinHV45 # - TIGHTENED stoploss: -5% → -3% (each loss costs less, break-even drops to ~60%) # - RAISED ROI target: +1.25% → +2% (each win earns more) # - Net result: backtest +22.53% in 6 months pure bear market, 314 trades, 74.5% win rate # # FILTER SUMMARY: # 1h — market regime: blocks entries in confirmed BEAR (death cross + RSI<50 + ADX>20) # 30m — pair downtrend: blocks if pair making lower lows on 30m (EMA20 < EMA50 + RSI<45) # 5m — BinHV45 capitulation: bb40 delta + closedelta + tail + volume checks import sys, os sys.path.insert(0, os.path.dirname(__file__)) from freqtrade.strategy import IStrategy, informative from pandas import DataFrame import pandas_ta as pta import pandas as pd class BinHV45_Regime_5m(IStrategy): minimal_roi = {"0": 0.02} # +2% (was 1.25%) stoploss = -0.03 # -3% (was -5%) timeframe = "5m" startup_candle_count: int = 250 @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 12 }, { "method": "StoplossGuard", "lookback_period_candles": 24, "trade_limit": 2, "stop_duration_candles": 48, "only_per_pair": True } ] # ------------------------------------------------------------------ # # 1h INFORMER — market-level bear detection # # ------------------------------------------------------------------ # @informative('1h') def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["ema_50"] = pta.ema(dataframe["close"], length=50) dataframe["ema_200"] = pta.ema(dataframe["close"], length=200) dataframe["rsi"] = pta.rsi(dataframe["close"], length=14) adx = pta.adx(dataframe["high"], dataframe["low"], dataframe["close"], length=14) if adx is not None and "ADX_14" in adx.columns: dataframe["adx"] = adx["ADX_14"] else: dataframe["adx"] = 0 dataframe["is_bear"] = ( (dataframe["close"] < dataframe["ema_200"]) & (dataframe["ema_50"] < dataframe["ema_200"]) & (dataframe["rsi"] < 50) & (dataframe["adx"] > 20) ).astype(int) return dataframe # ------------------------------------------------------------------ # # 30m INFORMER — pair-level downtrend detection # # ------------------------------------------------------------------ # @informative('30m') def populate_indicators_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["ema_20"] = pta.ema(dataframe["close"], length=20) dataframe["ema_50"] = pta.ema(dataframe["close"], length=50) dataframe["rsi"] = pta.rsi(dataframe["close"], length=14) dataframe["in_downtrend"] = ( (dataframe["close"] < dataframe["ema_20"]) & (dataframe["ema_20"] < dataframe["ema_50"]) & (dataframe["rsi"] < 45) ).astype(int) return dataframe # ------------------------------------------------------------------ # # 5m INDICATORS — BinHV45 capitulation signals # # ------------------------------------------------------------------ # def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: bb40 = pta.bbands(dataframe["close"], length=40, std=2.0) if bb40 is not None: dataframe["bb40_lower"] = bb40["BBL_40_2.0"] dataframe["bb40_mid"] = bb40["BBM_40_2.0"] dataframe["bb40_delta"] = (bb40["BBM_40_2.0"] - bb40["BBL_40_2.0"]).abs() dataframe["closedelta"] = (dataframe["close"] - dataframe["close"].shift(1)).abs() dataframe["tail"] = (dataframe["close"] - dataframe["low"]).abs() dataframe["volume_mean_slow"] = dataframe["volume"].rolling(window=30).mean() return dataframe # ------------------------------------------------------------------ # # ENTRY # # ------------------------------------------------------------------ # def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: not_bear = dataframe["1h_is_bear"] == 0 if "1h_is_bear" in dataframe.columns \ else pd.Series(True, index=dataframe.index) not_downtrend = dataframe["30m_in_downtrend"] == 0 if "30m_in_downtrend" in dataframe.columns \ else pd.Series(True, index=dataframe.index) dataframe.loc[ ( not_bear & not_downtrend & (dataframe["bb40_delta"] > 0) & (dataframe["bb40_lower"].shift(1) > 0) & (dataframe["bb40_delta"] > dataframe["close"] * 0.007) & (dataframe["closedelta"] > dataframe["close"] * 0.0175) & (dataframe["tail"] < dataframe["bb40_delta"] * 0.25) & (dataframe["close"] < dataframe["bb40_lower"].shift(1)) & (dataframe["close"] <= dataframe["close"].shift(1)) & (dataframe["volume"] < dataframe["volume_mean_slow"].shift(1) * 20) & (dataframe["volume"] > 0) ), "enter_long", ] = 1 return dataframe # ------------------------------------------------------------------ # # EXIT — ROI and stoploss handle everything # # ------------------------------------------------------------------ # def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "exit_long"] = 0 return dataframe