# source: https://raw.githubusercontent.com/witooh/else/cba146d2bad6323490f1cccb0c768ce082ddfe6d/user_data/strategies/SupertrendDaily.py
"""
Github_witooh_else__SupertrendDaily__20260304_101949 — Trend-following strategy

Core thesis: Ride 4h trends, filtered by Daily macro direction.
Exit on signal reversal, NOT on fixed % stops.

Design principles:
  - 2 parameters only (ATR period + multiplier) → minimal overfit risk
  - Daily EMA(50) as macro trend filter → Sharpe improvement 3x (QuantPedia)
  - Signal-based exit (Supertrend flip) → no fixed % stop killing profits
  - Wide safety stoploss (-25%) → emergency only, should never trigger
  - Time-of-day filter → block 12-16 UTC entries (75-83% loss rate from data)
"""

import numpy as np
import pandas as pd
import talib.abstract as ta
from freqtrade.strategy import IStrategy, merge_informative_pair


def calculate_supertrend(
    dataframe: pd.DataFrame, period: int = 10, multiplier: float = 3.0
) -> tuple[pd.Series, pd.Series]:
    """
    Calculate Supertrend indicator.

    Returns:
        supertrend: The Supertrend line values
        direction:  1 = bullish (price above), -1 = bearish (price below)
    """
    atr = ta.ATR(dataframe, timeperiod=period)
    hl2 = (dataframe["high"] + dataframe["low"]) / 2

    upper = (hl2 + multiplier * atr).values.copy()
    lower = (hl2 - multiplier * atr).values.copy()
    close = dataframe["close"].values

    n = len(dataframe)
    direction = np.zeros(n, dtype=int)
    supertrend = np.full(n, np.nan)

    # Find first valid index (ATR needs `period` bars to warm up)
    first_valid = -1
    for i in range(n):
        if not np.isnan(atr.iloc[i]):
            first_valid = i
            break

    if first_valid < 0:
        return (
            pd.Series(supertrend, index=dataframe.index),
            pd.Series(direction, index=dataframe.index),
        )

    # Initialize: assume bearish until proven otherwise
    supertrend[first_valid] = upper[first_valid]
    direction[first_valid] = -1

    for i in range(first_valid + 1, n):
        if np.isnan(upper[i]) or np.isnan(lower[i]):
            direction[i] = direction[i - 1]
            supertrend[i] = supertrend[i - 1]
            continue

        # Final upper band: lock in lower values (tightening resistance)
        if not (upper[i] < upper[i - 1] or close[i - 1] > upper[i - 1]):
            upper[i] = upper[i - 1]

        # Final lower band: lock in higher values (rising support)
        if not (lower[i] > lower[i - 1] or close[i - 1] < lower[i - 1]):
            lower[i] = lower[i - 1]

        # Determine direction based on previous state
        if direction[i - 1] == -1:  # was bearish
            if close[i] > upper[i]:  # price breaks above resistance
                direction[i] = 1
                supertrend[i] = lower[i]
            else:
                direction[i] = -1
                supertrend[i] = upper[i]
        else:  # was bullish
            if close[i] < lower[i]:  # price breaks below support
                direction[i] = -1
                supertrend[i] = upper[i]
            else:
                direction[i] = 1
                supertrend[i] = lower[i]

    return (
        pd.Series(supertrend, index=dataframe.index),
        pd.Series(direction, index=dataframe.index),
    )


class Github_witooh_else__SupertrendDaily__20260304_101949(IStrategy):
    """
    Supertrend 4h + Daily EMA(50) trend filter.

    Entry: Supertrend flips bullish on 4h + Daily close > EMA(50)
    Exit:  Supertrend flips bearish on 4h
    Stop:  -25% safety net (emergency only — should never trigger)

    Parameters: ATR(10), Multiplier(3.0), Daily EMA(50)
    """

    INTERFACE_VERSION = 3

    timeframe = "4h"
    startup_candle_count = 60  # ATR(10) + EMA(50) warmup

    # Safety net — NOT the primary exit mechanism
    # Supertrend signal-based exit handles normal exits
    stoploss = -0.25
    trailing_stop = False
    use_custom_stoploss = False

    # Disable ROI exits — Supertrend handles everything
    minimal_roi = {"0": 100}

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

    # --- Supertrend parameters (proven defaults from 60-year backtest data) ---
    st_period = 10
    st_multiplier = 3.0

    # --- Daily trend filter ---
    daily_ema_period = 50

    def informative_pairs(self):
        """Request 1d data for all pairs (macro trend filter)."""
        pairs = self.dp.current_whitelist()
        return [(pair, "1d") for pair in pairs]

    def populate_indicators(
        self, dataframe: pd.DataFrame, metadata: dict
    ) -> pd.DataFrame:
        # --- 4h Supertrend (primary signal) ---
        st_line, st_dir = calculate_supertrend(
            dataframe, self.st_period, self.st_multiplier
        )
        dataframe["supertrend"] = st_line
        dataframe["st_direction"] = st_dir

        # --- Daily EMA trend filter (macro direction) ---
        informative = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="1d")
        informative["ema50"] = ta.EMA(informative, timeperiod=self.daily_ema_period)
        informative["daily_uptrend"] = (
            informative["close"] > informative["ema50"]
        ).astype(int)

        dataframe = merge_informative_pair(
            dataframe, informative, self.timeframe, "1d", ffill=True
        )
        # After merge: columns become ema50_1d, daily_uptrend_1d

        return dataframe

    def populate_entry_trend(
        self, dataframe: pd.DataFrame, metadata: dict
    ) -> pd.DataFrame:
        # --- Entry 1: Supertrend FLIP (original) ---
        # Supertrend flips from bearish to bullish
        flip_bullish = (
            (dataframe["st_direction"] == 1)
            & (dataframe["st_direction"].shift(1) == -1)
        )

        # --- Entry 2: PULLBACK during established uptrend ---
        # Conditions:
        #   - Supertrend bullish for at least 5 candles (strong established trend)
        #   - Price low within 0.3% of Supertrend line (tight pullback touch)
        #   - Price closes clearly above Supertrend (bounce confirmed, not marginal)
        #   - Previous candle was also above Supertrend (not a breakdown recovery)
        established_uptrend = (
            (dataframe["st_direction"] == 1)
            & (dataframe["st_direction"].shift(1) == 1)
            & (dataframe["st_direction"].shift(2) == 1)
            & (dataframe["st_direction"].shift(3) == 1)
            & (dataframe["st_direction"].shift(4) == 1)
        )
        # Price low within 0.3% of Supertrend line = tight pullback touch
        pullback_touch = (
            dataframe["low"] <= dataframe["supertrend"] * 1.003
        )
        # Price must close clearly above Supertrend (>0.2% above = real bounce)
        bounce_confirmed = dataframe["close"] > dataframe["supertrend"] * 1.002
        # Previous candle also closed above Supertrend (avoids breakdown recovery)
        prev_healthy = dataframe["close"].shift(1) > dataframe["supertrend"].shift(1)

        pullback_entry = established_uptrend & pullback_touch & bounce_confirmed & prev_healthy

        # --- Common filters (apply to both entry types) ---
        common_filter = (
            (dataframe["daily_uptrend_1d"] == 1)  # Daily macro trend up
            & (dataframe["date"].dt.hour != 12)   # Block 12-16 UTC window
            & (dataframe["volume"] > 0)            # Volume sanity
        )

        # Combine: enter on flip OR pullback, both filtered
        dataframe.loc[
            (flip_bullish | pullback_entry) & common_filter,
            "enter_long",
        ] = 1

        return dataframe

    def populate_exit_trend(
        self, dataframe: pd.DataFrame, metadata: dict
    ) -> pd.DataFrame:
        dataframe.loc[
            # Supertrend flips bearish (was bullish, now bearish)
            (dataframe["st_direction"] == -1)
            & (dataframe["st_direction"].shift(1) == 1),
            "exit_long",
        ] = 1

        return dataframe
