# source: https://raw.githubusercontent.com/wiktorj137/btc-strategy-lab/0a3f134a09c468be0525bfaca57e71c6e683faf5/user_data/strategies/BtcHybrid.py
# directory_url: https://github.com/wiktorj137/btc-strategy-lab/blob/main/user_data/strategies/
# User: wiktorj137
# Repository: btc-strategy-lab
# --------------------"""Github_wiktorj137_btc_strategy_lab__BtcHybrid__20260821_220947 v1 - trend-following na BTC/USDT z filtrem rezimu.

TEZA (wymagana przez skill strategy-framework, zanim cokolwiek policzymy):
    BTC porusza sie w dlugich, wyraznych rezimach. Buy&hold zbiera caly wzrost,
    ale placi za to drawdownem -81%. Teza tej strategii nie brzmi "pobijemy BTC
    w zwrocie" - brzmi "oddamy czesc zwrotu w zamian za wyjscie z najgorszych
    czesci bessy". Mierzalny cel to Calmar i Sortino, nie CAGR.

    Zrodlo przewagi: momentum/trend persistence - aktywa w silnym trendzie maja
    tendencje do jego kontynuacji w horyzoncie tygodni. To jest udokumentowana,
    trwala anomalia na wielu klasach aktywow, a nie wzorzec dopasowany do danych.

    Kiedy ta strategia PRZEGRA: rynek w szerokiej konsolidacji z falszywymi
    wybiciami (whipsaw). Wtedy bedziemy kupowac gore i sprzedawac dol.
    To akceptowany koszt, nie blad do zoptymalizowania.

Struktura:
    - rezim: dzienny EMA200 + ADX -> handlujemy TYLKO w trendzie wzrostowym
    - wejscie: 1h, cofniecie RSI w trwajacym trendzie (nie kupowanie wybicia)
    - wyjscie: ATR-owy stop kroczacy + rozpad trendu
    - sizing: staly ulamek kapitalu (max_open_trades=1 -> pelna pozycja)

Liczba parametrow hyperoptowalnych: 5. Celowo malo - przy ~8 latach danych
kazdy dodatkowy parametr to prosta droga do overfittingu (patrz: PBO w etapie 4).
"""
from __future__ import annotations

from datetime import datetime

import talib.abstract as ta
from pandas import DataFrame

from freqtrade.persistence import Trade
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair


class Github_wiktorj137_btc_strategy_lab__BtcHybrid__20260821_220947(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "1h"
    informative_timeframe = "1d"

    can_short = False
    process_only_new_candles = True
    startup_candle_count = 400

    # Stop-loss obsluguje custom_stoploss (ATR). To jest twardy backstop.
    stoploss = -0.25
    use_custom_stoploss = True

    # ROI wylaczone - wyjscie ma byc sterowane trendem, nie stalym targetem.
    minimal_roi = {"0": 10}

    # --- parametry hyperoptowalne (5) ---
    buy_rsi_max = IntParameter(35, 65, default=50, space="buy", optimize=True)
    buy_adx_min = IntParameter(15, 35, default=20, space="buy", optimize=True)
    atr_stop_mult = DecimalParameter(1.5, 5.0, default=3.0, decimals=1,
                                     space="sell", optimize=True)
    atr_trail_mult = DecimalParameter(1.0, 4.0, default=2.0, decimals=1,
                                      space="sell", optimize=True)
    sell_rsi_min = IntParameter(60, 90, default=75, space="sell", optimize=True)

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        return [(p, self.informative_timeframe) for p in pairs]

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # --- rezim rynkowy z interwalu dziennego ---
        inf = self.dp.get_pair_dataframe(pair=metadata["pair"],
                                         timeframe=self.informative_timeframe)
        inf["ema200"] = ta.EMA(inf, timeperiod=200)
        inf["ema50"] = ta.EMA(inf, timeperiod=50)
        inf["adx"] = ta.ADX(inf, timeperiod=14)
        inf["atr"] = ta.ATR(inf, timeperiod=14)
        # merge_informative_pair przesuwa dane o 1 swiece - bez tego mielibysmy
        # look-ahead bias (dzienne zamkniecie znane w srodku dnia).
        dataframe = merge_informative_pair(dataframe, inf, self.timeframe,
                                           self.informative_timeframe, ffill=True)

        # --- sygnaly na interwale 1h ---
        dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]

        # rezim: 1 = bull (handlujemy), 0 = bear/chop (siedzimy w cashu)
        d = f"_{self.informative_timeframe}"
        dataframe["regime_bull"] = (
            (dataframe[f"close{d}"] > dataframe[f"ema200{d}"])
            & (dataframe[f"ema50{d}"] > dataframe[f"ema200{d}"])
        ).astype(int)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        d = f"_{self.informative_timeframe}"
        dataframe.loc[
            (
                (dataframe["regime_bull"] == 1)
                & (dataframe[f"adx{d}"] > self.buy_adx_min.value)
                & (dataframe["close"] > dataframe["ema200"])
                & (dataframe["ema50"] > dataframe["ema200"])
                # cofniecie, nie pogon za wybiciem
                & (dataframe["rsi"] < self.buy_rsi_max.value)
                & (dataframe["macd"] > dataframe["macdsignal"])
                & (dataframe["volume"] > 0)
            ),
            ["enter_long", "enter_tag"],
        ] = (1, "trend_pullback")
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                (
                    # rozpad trendu na interwale dziennym
                    (dataframe["regime_bull"] == 0)
                    # albo wykupienie po silnym ruchu
                    | ((dataframe["rsi"] > self.sell_rsi_min.value)
                       & (dataframe["macd"] < dataframe["macdsignal"]))
                )
                & (dataframe["volume"] > 0)
            ),
            ["exit_long", "exit_tag"],
        ] = (1, "trend_break")
        return dataframe

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float,
                        after_fill: bool, **kwargs) -> float | None:
        """Stop ATR-owy oparty na ATR DZIENNYM, nie godzinowym.

        v1 uzywala ATR z 1h (~1% ceny). 3x tego = stop 3% na BTC, ktory wylatuje
        przy zwyklym szumie - stad 220 transakcji po srednio 16h i -51%.
        ATR dzienny to ~2.5-3% ceny, wiec stop ma realna przestrzen na oddech.

        Freqtrade zwraca stoploss relatywnie do ceny biezacej, a stop moze sie
        tylko zaciskac. Liczymy wiec docelowa CENE stopu i przeliczamy ja na
        wartosc relatywna.
        """
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df is None or len(df) == 0 or current_rate <= 0:
            return None
        atr = df[f"atr_{self.informative_timeframe}"].iat[-1]
        if not atr:
            return None

        if current_profit > 0.05:
            # trailing od szczytu, ktory pozycja osiagnela
            peak = max(trade.max_rate or current_rate, current_rate)
            stop_price = peak - atr * float(self.atr_trail_mult.value)
        else:
            # stop poczatkowy liczony od ceny wejscia, nie od biezacej
            stop_price = trade.open_rate - atr * float(self.atr_stop_mult.value)

        return min(stop_price / current_rate - 1, -0.005)
