# source: https://raw.githubusercontent.com/PabloM03/freqtrade/a604b29b7c4f06b8d981207d8a452179ca427465/user_data/strategies/EMAPullback_Pro.py
# directory_url: https://github.com/PabloM03/freqtrade/blob/develop/user_data/strategies/
# User: PabloM03
# Repository: freqtrade
# --------------------import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np
import talib.abstract as ta

from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy import stoploss_from_open
from pandas import DataFrame
from datetime import datetime
from typing import Optional
from freqtrade.persistence import Trade


class Github_PabloM03_freqtrade__EMAPullback_Pro__20260827_190157(IStrategy):
    """
    Estrategia simple y robusta basada en medias móviles:
    - Filtro de tendencia: EMA200
    - Estructura: EMA20 / EMA50
    - Entrada: pullback hacia EMA20 dentro de tendencia alcista
    - Anti-cuchillo: límites de caídas + cooldown tras velón rojo
    - Salidas: take profit duro + pérdida de tendencia con beneficio mínimo
    - Trailing: simple, se activa solo cuando ya hay beneficio decente
    """

    # ===== Config base =====
    timeframe = "5m"
    startup_candle_count = 240  # para EMA200 con margen

    # Importante: mantenemos compatibilidad.
    use_sell_signal = False
    sell_profit_only = True
    ignore_roi_if_buy_signal = False
    trailing_stop = False
    minimal_roi = {"0": 0.0}

    # ===== Parámetros (pocos y con sentido) =====
    # Costes / mínimos
    FEE_RATE = 0.001
    SLIPPAGE_BUFFER = 0.0005
    MIN_PROFIT_NET = 2 * FEE_RATE + SLIPPAGE_BUFFER  # ~0.25% aprox

    # Riesgo
    stoploss = -0.06  # -6%

    # Take profit “duro”
    HARD_TP = 0.035   # 3.5%

    # Anti-cuchillo (en %)
    PCT1_MIN = -1.8   # si la última vela cae más de -1.8%, no comprar
    PCT3_MIN = -4.0   # si en 3 velas cae más de -4%, no comprar
    COOLDOWN_BARS = 3 # tras velón rojo, esperar N velas

    # Tendencia / entradas (medias)
    EMA_FAST = 20
    EMA_SLOW = 50
    EMA_TREND = 200

    # Entrada por pullback
    PULLBACK_EMA20_MULT = 1.002   # close <= ema20 * 1.002 (cerca)
    BUY_BELOW_EMA20 = True        # exige estar en pullback real
    AVOID_CHASING_PCT1 = 0.7      # no comprar si última vela sube > 0.7%
    AVOID_NEAR_HH = 0.010         # evitar compra a <1% del máximo 50 velas

    # RSI “ligero” (no protagonista)
    RSI_MIN = 35
    RSI_MAX = 60

    # Mínimo hold para no vender “al instante”
    MIN_HOLD_BARS = 3

    # Trailing simple
    TRAIL_START = 0.020   # cuando profit >= 2%, activar trailing
    TRAIL_DIST  = 0.018   # trailing ~1.8% desde el open (con stoploss_from_open)
    TRAIL_DIST_TIGHT = 0.012  # si pierde fuerza, apretar

    # ===== Indicadores =====
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema20"]  = ta.EMA(dataframe, timeperiod=self.EMA_FAST)
        dataframe["ema50"]  = ta.EMA(dataframe, timeperiod=self.EMA_SLOW)
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=self.EMA_TREND)

        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        # caídas/subidas rápidas (%)
        dataframe["pct_1"] = dataframe["close"].pct_change(1) * 100.0
        dataframe["pct_3"] = dataframe["close"].pct_change(3) * 100.0

        # velón rojo (cuchillo) + cooldown
        body = (dataframe["close"] - dataframe["open"]).abs()
        dataframe["big_red"] = (dataframe["close"] < dataframe["open"]) & (body > 1.2 * dataframe["atr"])
        dataframe["cooldown"] = dataframe["big_red"].rolling(self.COOLDOWN_BARS).max().fillna(0).astype(bool)

        # evitar comprar pegado a máximos recientes
        dataframe["hh_50"] = dataframe["high"].rolling(50).max()
        dataframe["near_hh"] = dataframe["close"] >= (dataframe["hh_50"] * (1.0 - self.AVOID_NEAR_HH))

        # estructura de tendencia
        dataframe["trend_up"] = (
            (dataframe["close"] > dataframe["ema200"]) &
            (dataframe["ema20"] > dataframe["ema50"]) &
            (dataframe["ema50"] > dataframe["ema200"])
        )

        # “pullback” hacia ema20 (no persecución)
        dataframe["pullback_ok"] = dataframe["close"] <= (dataframe["ema20"] * self.PULLBACK_EMA20_MULT)
        dataframe["ema20_slope_up"] = dataframe["ema20"] > dataframe["ema20"].shift(1)

        return dataframe

    # ===== Entradas =====
    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        anti_cuchillo = (
            (dataframe["pct_1"] > self.PCT1_MIN) &
            (dataframe["pct_3"] > self.PCT3_MIN) &
            (~dataframe["cooldown"]) &
            (dataframe["volume"] > 0)
        )

        no_chase = (
            (dataframe["pct_1"] < self.AVOID_CHASING_PCT1) &
            (~dataframe["near_hh"])
        )

        rsi_ok = (dataframe["rsi"] >= self.RSI_MIN) & (dataframe["rsi"] <= self.RSI_MAX)

        # Entrada principal: tendencia + pullback + pequeña confirmación
        # Confirmación mínima: o vela verde, o cierre subiendo vs previo
        confirm = (dataframe["close"] >= dataframe["open"]) | (dataframe["close"] > dataframe["close"].shift(1))

        entry = (
            dataframe["trend_up"] &
            dataframe["ema20_slope_up"] &
            rsi_ok &
            anti_cuchillo &
            no_chase &
            dataframe["pullback_ok"] &
            confirm
        )

        # Si quieres exigir estar por debajo de EMA20 (pullback real)
        if self.BUY_BELOW_EMA20:
            entry = entry & (dataframe["close"] <= dataframe["ema20"])

        dataframe.loc[entry, "buy"] = 1
        return dataframe

    # ===== Señal de salida (solo para compatibilidad; la salida real va en custom_exit) =====
    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["sell"] = 0
        return dataframe

    # ===== Util =====
    def _bars_elapsed(self, trade: Trade, current_time: datetime) -> int:
        tf_minutes = int(self.timeframe.rstrip("m"))
        seconds = (current_time - trade.open_date_utc).total_seconds()
        return int(max(0, seconds) // (tf_minutes * 60))

    # ===== Exits =====
    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs
    ) -> Optional[str]:

        # Evitar “compra y vende al instante”
        bars = self._bars_elapsed(trade, current_time)
        if bars < self.MIN_HOLD_BARS:
            return None

        # TP duro
        if current_profit is not None and current_profit >= self.HARD_TP:
            return "hard_tp"

        # Requiere beneficio neto mínimo para vender por señal (si no, no scalpeamos migajas)
        if current_profit is None or current_profit < self.MIN_PROFIT_NET:
            return None

        try:
            df = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe)
            last = df.iloc[-1]

            # Pérdida de estructura / tendencia:
            # 1) close pierde EMA20
            lose_ema20 = last["close"] < last["ema20"]
            # 2) cruce bajista EMA20 < EMA50 (más serio)
            bear_cross = last["ema20"] < last["ema50"]

            # Si estás en beneficio, y empieza a romper estructura, sal
            if bear_cross:
                return "ema20_below_ema50_exit"
            if lose_ema20 and (last["rsi"] < 55):
                return "lose_ema20_exit"

        except Exception:
            pass

        return None

    # ===== Stoploss dinámico (trailing simple) =====
    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs
    ) -> float:

        # Antes de activar trailing, usa stoploss base
        if current_profit is None or current_profit < self.TRAIL_START:
            return self.stoploss

        # Trailing simple: protege beneficio ya ganado
        # Si pierde EMA20 o RSI cae, aprieta un poco
        dist = self.TRAIL_DIST
        try:
            df = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe)
            last = df.iloc[-1]
            if (last["close"] < last["ema20"]) or (last["rsi"] < 50):
                dist = self.TRAIL_DIST_TIGHT
        except Exception:
            pass

        return stoploss_from_open(current_profit, dist)
