# source: https://raw.githubusercontent.com/zevrichards/freqtrade-strategies/9dc9ef238325c4d29fa0b8e64346db96fe96343c/BinHV45_Regime_5m.py
# directory_url: https://github.com/zevrichards/freqtrade-strategies/blob/main//
# User: zevrichards
# Repository: freqtrade-strategies
# --------------------# Github_zevrichards_freqtrade_strategies__BinHV45_Regime_5m__20260820_234001.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 Github_zevrichards_freqtrade_strategies__BinHV45_Regime_5m__20260820_234001(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
