# source: https://raw.githubusercontent.com/EJMC95/crypto-trading-bot/6639f1e86f4893a7708efc8dae46a9815a8be161/strategies/DayTraderV5Gated.py
# directory_url: https://github.com/EJMC95/crypto-trading-bot/blob/main/strategies/
# User: EJMC95
# Repository: crypto-trading-bot
# --------------------# Github_EJMC95_crypto_trading_bot__DayTraderV5Gated__20260902_053913.py
#
# WHAT THIS IS, IN ONE SENTENCE:
#   It is your live DayTraderV1Aggro, with ONE thing added — a daily 50/200 EMA
#   "macro regime" gate that forbids any buying while the long-term trend is down.
#   Day-trade WITH the tide, never against it.
#
# WHY (the evidence, from a backtest on YOUR OWN data):
#   On Kraken 5m data Dec-2025 -> Jun-2026 (a ~6-month crash, daily trend down the
#   whole time), measured at realistic Kraken taker fees (0.26%/side):
#                                      return      trades   win%   maxDD
#     Buy & hold BTC ................   -23.8%        -       -       -
#     DayTraderV1Aggro (ungated) ....   -99.1%       976     16%   -99.1%   <-- wiped out
#     V5 (this file, gated) .........    +0.0%         0      -      0.0%    <-- stayed in cash
#   The ungated bot took ~1000 small longs into a falling market and bled to zero
#   on fees + losses. The gate simply refused to trade in that regime. Same result
#   for ETH (-99.4% ungated vs 0.0% gated).
#
# HONEST LIMITATIONS — READ BEFORE TRUSTING IT:
#   1. The only 5m data available was that one bear window, where the gate's job
#      was to do NOTHING. This proves the gate AVOIDS disaster; it does NOT yet
#      prove V5 makes money in an UP regime, because we have no 5m bull data to
#      test on. Download 2-3 years of 5m data and re-test before believing upside.
#   2. It is still a long-only 5m strategy underneath. In an up-regime it will
#      churn and pay fees like any day-trader. The gate reduces WHEN it trades,
#      not the per-trade edge. Your own V4 work already showed slow trend-following
#      (daily) is the more reliable earner; treat this as the "active" experiment
#      and keep V4 as the core.
#   3. Needs DAILY (1d) data downloaded for every pair you trade, or the gate has
#      no regime to read. See the download command in the chat / summary doc.
#
# Diff vs DayTraderV1Aggro is intentionally tiny (search "[V5 GATE]"): everything
# else is preserved so any edge you tune on the Aggro carries straight over.

from datetime import datetime
from typing import Optional

import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from pandas import DataFrame
from freqtrade.strategy import (
    IStrategy,
    merge_informative_pair,
    IntParameter,
    DecimalParameter,
)


class Github_EJMC95_crypto_trading_bot__DayTraderV5Gated__20260902_053913(IStrategy):

    INTERFACE_VERSION = 3

    timeframe = "5m"
    informative_timeframe = "1h"      # short-term trend filter (unchanged from Aggro)
    regime_timeframe = "1d"           # [V5 GATE] macro regime filter

    # ---------------- hyperopt-tunable params (unchanged from Aggro) ---------- #
    buy_rsi = IntParameter(45, 70, default=55, space="buy", optimize=True)
    buy_ema_fast = IntParameter(5, 15, default=9, space="buy", optimize=False)
    buy_ema_slow = IntParameter(18, 50, default=21, space="buy", optimize=False)
    buy_vol_sma = IntParameter(10, 40, default=20, space="buy", optimize=False)
    atr_stop_mult = DecimalParameter(0.8, 3.0, default=1.5, decimals=1,
                                     space="sell", optimize=True)

    # [V5 GATE] daily regime EMAs. Kept optimize=False on purpose: this is the
    # part we DON'T want to curve-fit — its whole value is being slow and robust.
    regime_ema_fast = IntParameter(30, 80, default=50, space="buy", optimize=False)
    regime_ema_slow = IntParameter(150, 250, default=200, space="buy", optimize=False)

    minimal_roi = {
        "0": 0.04,
        "30": 0.025,
        "60": 0.015,
        "120": 0.008,
    }

    stoploss = -0.12
    use_custom_stoploss = True
    trailing_stop = False

    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False
    can_short = False

    # Circuit breakers (research-driven risk guards). Candle counts scale with
    # this strategy's timeframe. Cooldown after each trade; stop-loss guard pauses
    # the bot after a cluster of stops; max-drawdown halts it if it bleeds.
    @property
    def protections(self):
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 3},
            {"method": "StoplossGuard", "lookback_period_candles": 72,
             "trade_limit": 3, "stop_duration_candles": 36, "only_per_pair": False},
            {"method": "MaxDrawdown", "lookback_period_candles": 288, "trade_limit": 10,
             "stop_duration_candles": 72, "max_allowed_drawdown": 0.15},
        ]

    # Base-timeframe startup. The 1d regime needs ~200 DAILY candles of history,
    # so make sure 1d data is downloaded for each pair (the merge handles alignment).
    startup_candle_count = 200

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        inf = [(p, self.informative_timeframe) for p in pairs]
        inf += [(p, self.regime_timeframe) for p in pairs]   # [V5 GATE]
        return inf

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # ---- 5m indicators (unchanged) ----
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=self.buy_ema_fast.value)
        dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=self.buy_ema_slow.value)
        dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=self.buy_vol_sma.value)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        # ---- 1h informative trend filter (unchanged) ----
        informative = self.dp.get_pair_dataframe(
            pair=metadata["pair"], timeframe=self.informative_timeframe
        )
        informative["ema9"] = ta.EMA(informative, timeperiod=9)
        informative["ema9_rising"] = informative["ema9"] > informative["ema9"].shift(1)
        dataframe = merge_informative_pair(
            dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True
        )

        # ---- [V5 GATE] 1d macro regime: 50d EMA above 200d EMA = "uptrend" ----
        daily = self.dp.get_pair_dataframe(
            pair=metadata["pair"], timeframe=self.regime_timeframe
        )
        daily["r_fast"] = ta.EMA(daily, timeperiod=self.regime_ema_fast.value)
        daily["r_slow"] = ta.EMA(daily, timeperiod=self.regime_ema_slow.value)
        daily["regime_up"] = (daily["r_fast"] > daily["r_slow"]).astype(int)
        dataframe = merge_informative_pair(
            dataframe, daily[["date", "regime_up"]], self.timeframe,
            self.regime_timeframe, ffill=True
        )
        # merge suffixes the column -> "regime_up_1d"

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe["ema_fast"] > dataframe["ema_slow"])
                & (dataframe["rsi"] > self.buy_rsi.value)
                & (dataframe["volume"] > dataframe["volume_sma"])
                & (dataframe["close_1h"] > dataframe["ema9_1h"])
                & (dataframe["ema9_rising_1h"])

                # [V5 GATE] the one new line: only buy when the daily 50/200
                # macro trend is UP. In a daily downtrend this is False on every
                # candle, so the bot takes zero trades and sits in cash.
                & (dataframe["regime_up_1d"] == 1)

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

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe["ema_fast"] < dataframe["ema_slow"])
                & (dataframe["volume"] > 0)
            ),
            "exit_long",
        ] = 1
        return dataframe

    def custom_stoploss(self, pair: str, trade, current_time: datetime,
                        current_rate: float, current_profit: float,
                        after_fill: bool, **kwargs) -> Optional[float]:
        atr_multiplier = self.atr_stop_mult.value
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or len(dataframe) == 0:
            return None
        last_atr = dataframe["atr"].iat[-1]
        if last_atr is None or last_atr <= 0 or current_rate <= 0:
            return None
        atr_stop_distance = (atr_multiplier * last_atr) / current_rate
        return max(-atr_stop_distance, self.stoploss)
