# source: https://raw.githubusercontent.com/zevrichards/freqtrade-strategies/9dc9ef238325c4d29fa0b8e64346db96fe96343c/MomentumBreakout_4h.py
# directory_url: https://github.com/zevrichards/freqtrade-strategies/blob/main//
# User: zevrichards
# Repository: freqtrade-strategies
# --------------------# Github_zevrichards_freqtrade_strategies__MomentumBreakout_4h__20260820_234001.py - v2
#
# PURPOSE: Catches sustained rallies like the Aug 19 BTC +6% move.
# Enters EARLY in a momentum move, not after full bull confirmation.
#
# KEY DESIGN DECISIONS:
#   - 90-candle (3-week) consolidation window — only genuine multi-week breakouts
#   - 2x volume requirement — real institutional buying, not noise
#   - ADX > 25 and rising with +DI > -DI — confirmed trend direction
#   - Daily regime: blocks on death cross + RSI<45 only — allows early-stage rallies
#   - Trailing stop: lets winners run, 3% trail activates at +5%
#
# COMPLEMENTS:
#   BinHV45: buys drops. MomentumBreakout: buys breakouts. Together they cover
#   both directions of meaningful price moves.

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__MomentumBreakout_4h__20260820_234001(IStrategy):

    minimal_roi = {
        "0":    0.15,   # let trailing stop do the work
        "2880": 0.08,   # 8% after 30 days
        "5760": 0.04,   # 4% after 60 days
    }

    stoploss = -0.05
    trailing_stop = True
    trailing_stop_positive = 0.03
    trailing_stop_positive_offset = 0.05
    trailing_only_offset_is_reached = True

    timeframe = "4h"
    startup_candle_count: int = 250

    # ------------------------------------------------------------------ #
    # 1d INFORMER — daily bear detection                                   #
    # ------------------------------------------------------------------ #
    @informative('1d')
    def populate_indicators_1d(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)
        dataframe["daily_bear"] = (
            (dataframe["close"] < dataframe["ema_200"].fillna(0)) &
            (dataframe["ema_50"].fillna(0) < dataframe["ema_200"].fillna(float("inf"))) &
            (dataframe["rsi"].fillna(50) < 45)
        ).astype(int)
        return dataframe

    # ------------------------------------------------------------------ #
    # 4h INDICATORS                                                        #
    # ------------------------------------------------------------------ #
    def populate_indicators(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)

        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"]
            dataframe["adx_pos"] = adx["DMP_14"]
            dataframe["adx_neg"] = adx["DMN_14"]
        else:
            dataframe["adx"]     = 0
            dataframe["adx_pos"] = 0
            dataframe["adx_neg"] = 0

        dataframe["vol_ma_20"] = pta.sma(dataframe["volume"], length=20)

        # 3-week consolidation window (90 x 4h candles)
        dataframe["consolidation_high"]  = dataframe["high"].shift(1).rolling(90).max()
        dataframe["consolidation_low"]   = dataframe["low"].shift(1).rolling(90).min()
        dataframe["consolidation_range"] = (
            (dataframe["consolidation_high"] - dataframe["consolidation_low"])
            / dataframe["consolidation_low"].replace(0, float("inf"))
        )

        dataframe["adx_rising"] = (dataframe["adx"] > dataframe["adx"].shift(2)).astype(int)
        return dataframe

    # ------------------------------------------------------------------ #
    # ENTRY                                                                #
    # ------------------------------------------------------------------ #
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        not_daily_bear = (
            dataframe["1d_daily_bear"] == 0
            if "1d_daily_bear" in dataframe.columns
            else pd.Series(True, index=dataframe.index)
        )

        dataframe.loc[
            (
                not_daily_bear &

                # Genuine 3-week consolidation breakout
                (dataframe["close"] > dataframe["consolidation_high"]) &
                (dataframe["consolidation_range"] > 0.08) &
                (dataframe["consolidation_range"] < 0.40) &

                # Strong bullish candle
                (dataframe["close"] > dataframe["open"]) &

                # 2x volume surge
                (dataframe["volume"] > dataframe["vol_ma_20"] * 2.0) &

                # RSI in momentum zone
                (dataframe["rsi"] > 52) &
                (dataframe["rsi"] < 75) &

                # Strong trend forming
                (dataframe["adx"] > 25) &
                (dataframe["adx_rising"] == 1) &
                (dataframe["adx_pos"] > dataframe["adx_neg"]) &

                # Price above short-term structure
                (dataframe["close"] > dataframe["ema_20"]) &
                (dataframe["close"] > dataframe["ema_50"]) &

                (dataframe["volume"] > 0)
            ),
            "enter_long",
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[:, "exit_long"] = 0
        return dataframe
