# source: https://raw.githubusercontent.com/NorthStartLabas/crypto-llm-trader/2baa23b931375de2f0caeb5e2557ec8f8cf1c4bb/user_data/strategies/BBBreakoutStrategy.py
"""
Github_NorthStartLabas_crypto_llm_trader__BBBreakoutStrategy__20260520_011426 — Phase 1.7

Bollinger Band breakout with EMA200 trend filter and volume confirmation.
Designed to be backtested per-timeframe (1h, 2h, 4h, 6h, 8h, 12h) to find
which pairs trend cleanly on which timeframe for this setup.

Entry (all three must be true on the same candle):
  1. Close > BB upper band (period=20, StdDev=1) — momentum breakout
  2. Close > EMA200 — bull structure only, skip bear markets
  3. Volume > Volume MA(20) — confirms the breakout has real participation

Exit:
  - Signal: close < BB middle (SMA20) — momentum exhausted, back to mean
  - Stop: ATR(14) * atr_multiplier from entry price (custom_stoploss)
  - ROI table: lock in profit over time if mean-close exit doesn't fire

StdDev=1 (not the usual 2): tighter bands = earlier signals, but requires
the EMA200 and volume filters to reduce false breakouts.

Not financial advice. Do not use with real funds without independent review.
"""

import talib.abstract as ta
from pandas import DataFrame

from freqtrade.strategy import DecimalParameter, IStrategy


class Github_NorthStartLabas_crypto_llm_trader__BBBreakoutStrategy__20260520_011426(IStrategy):
    INTERFACE_VERSION = 3

    can_short = False
    timeframe = "1h"
    startup_candle_count = 200  # EMA200 needs 200 candles to warm up

    # Hyperopt best (epoch 255/500, 6h timeframe, 500 epochs):
    #   atr_multiplier=3.4, buy_volume_factor=1.0
    # ROI times are in minutes (timeframe-independent).
    minimal_roi = {
        "0": 0.222,    # 22.2% anytime
        "2219": 0.167, # 16.7% after ~37 h
        "5267": 0.09,  # 9% after ~87 h (~3.6 days)
        "9945": 0,     # breakeven after ~166 h (~6.9 days)
    }

    # Hard cap — safety net if ATR-based stop fails.
    # In practice the 2xATR custom stop fires first.
    stoploss = -0.15
    use_custom_stoploss = True

    trailing_stop = False
    process_only_new_candles = True

    # ------------------------------------------------------------------ #
    # Hyperopt parameters                                                  #
    # ------------------------------------------------------------------ #
    atr_multiplier = DecimalParameter(1.0, 3.5, default=3.4, decimals=1, space="sell", optimize=True)
    buy_volume_factor = DecimalParameter(1.0, 2.0, default=1.0, decimals=1, space="buy", optimize=True)

    # ------------------------------------------------------------------ #
    # Indicators                                                           #
    # ------------------------------------------------------------------ #
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=1.0, nbdevdn=1.0)
        dataframe["bb_upper"] = bb["upperband"]
        dataframe["bb_middle"] = bb["middleband"]

        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["volume_ma20"] = dataframe["volume"].rolling(window=20).mean()
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        return dataframe

    # ------------------------------------------------------------------ #
    # Entry                                                                #
    # ------------------------------------------------------------------ #
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe["close"] > dataframe["bb_upper"])
                & (dataframe["close"] > dataframe["ema200"])
                & (dataframe["volume"] > dataframe["volume_ma20"] * self.buy_volume_factor.value)
                & (dataframe["volume"] > 0)
            ),
            ["enter_long", "enter_tag"],
        ] = [1, "bb_breakout"]
        return dataframe

    # ------------------------------------------------------------------ #
    # Exit                                                                 #
    # ------------------------------------------------------------------ #
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (dataframe["close"] < dataframe["bb_middle"])
                & (dataframe["volume"] > 0)
            ),
            ["exit_long", "exit_tag"],
        ] = [1, "bb_mean_close"]
        return dataframe

    # ------------------------------------------------------------------ #
    # Dynamic 2x ATR stop                                                  #
    # ------------------------------------------------------------------ #
    def custom_stoploss(self, pair: str, trade, current_time,
                        current_rate: float, current_profit: float, **kwargs) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) == 0:
            return self.stoploss
        atr = dataframe.iloc[-1]["atr"]
        if not (atr > 0):
            return self.stoploss
        stop_price = trade.open_rate - (self.atr_multiplier.value * atr)
        stop_ratio = (stop_price / current_rate) - 1.0
        # Floor at -25% in case ATR is unusually large on illiquid candles
        return max(stop_ratio, -0.25)
