# source: https://raw.githubusercontent.com/wangc-joker/ft_userdata/4b4fda04062336f5b248659e6caaf763570b083d/user_data/strategies/SpotMTFMomentumStrategy.py
from datetime import timedelta
from typing import Optional

import pandas as pd
from pandas import DataFrame

import talib.abstract as ta
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative


class Github_wangc_joker_ft_userdata__SpotMTFMomentumStrategy__20260407_090856(IStrategy):
    INTERFACE_VERSION = 3

    can_short = False
    timeframe = "1h"
    process_only_new_candles = True
    startup_candle_count = 250

    minimal_roi = {"0": 10.0}
    stoploss = -0.025

    trailing_stop = True
    trailing_stop_positive = 0.02
    trailing_stop_positive_offset = 0.03
    trailing_only_offset_is_reached = True

    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    @property
    def protections(self):
        return [
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 200,
                "trade_limit": 20,
                "stop_duration_candles": 48,
                "max_allowed_drawdown": 0.10,
            },
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": 2,
            },
        ]

    @staticmethod
    def _crossed_above(series_a: pd.Series, series_b: pd.Series) -> pd.Series:
        return (series_a > series_b) & (series_a.shift(1) <= series_b.shift(1))

    @staticmethod
    def _crossed_below(series_a: pd.Series, series_b: pd.Series) -> pd.Series:
        return (series_a < series_b) & (series_a.shift(1) >= series_b.shift(1))

    @informative("4h")
    def populate_indicators_4h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20)
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_200"] = 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)
        dataframe["atr_ratio"] = dataframe["atr"] / dataframe["close"]
        dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean()
        dataframe["recent_high"] = dataframe["high"].shift(1).rolling(12).max()
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        long_signal = (
            self._crossed_above(dataframe["ema_20"], dataframe["ema_50"])
            & (dataframe["close_4h"] > dataframe["ema_50_4h"])
            & (dataframe["ema_50_4h"] > dataframe["ema_50_4h"].shift(3))
            & (dataframe["close"] > dataframe["ema_200"])
            & (dataframe["ema_50"] > dataframe["ema_200"])
            & dataframe["rsi"].between(52, 65)
            & (dataframe["adx"] > 20)
            & dataframe["atr_ratio"].between(0.005, 0.04)
            & (dataframe["volume"] > dataframe["volume_mean"])
            & (dataframe["close"] > dataframe["recent_high"])
            & (dataframe["volume"] > 0)
        )

        dataframe.loc[long_signal, ["enter_long", "enter_tag"]] = (1, "mtf_momentum_long")
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        exit_signal = (
            (
                self._crossed_below(dataframe["ema_20"], dataframe["ema_50"])
                | (dataframe["close_4h"] < dataframe["ema_50_4h"])
            )
            & (dataframe["volume"] > 0)
        )
        dataframe.loc[exit_signal, "exit_long"] = 1
        return dataframe

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> Optional[str]:
        if trade.open_date_utc and current_time - trade.open_date_utc >= timedelta(hours=48):
            return "time_stop_48h"
        return None

    def custom_stake_amount(
        self,
        pair: str,
        current_time,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> float:
        closed_trades = [
            trade
            for trade in Trade.get_trades_proxy(is_open=False)
            if trade.close_date_utc and trade.close_date_utc <= current_time
        ]
        closed_trades.sort(key=lambda trade: trade.close_date_utc, reverse=True)

        kelly_fraction = 0.50
        sample = closed_trades[:20]
        if len(sample) >= 10:
            wins = [trade.close_profit for trade in sample if trade.close_profit and trade.close_profit > 0]
            losses = [abs(trade.close_profit) for trade in sample if trade.close_profit and trade.close_profit < 0]
            if wins and losses:
                win_rate = len(wins) / len(sample)
                avg_win = sum(wins) / len(wins)
                avg_loss = sum(losses) / len(losses)
                payoff = avg_win / avg_loss if avg_loss else 0.0
                edge = win_rate - ((1 - win_rate) / payoff) if payoff > 0 else 0.0
                kelly_fraction = min(0.75, max(0.25, edge * 0.5))

        last_three = sample[:3]
        loss_streak = len(last_three) == 3 and all((trade.close_profit or 0) < 0 for trade in last_three)

        risk_cap_stake = min(max_stake, proposed_stake * 4.0)
        stake = min(proposed_stake * kelly_fraction, risk_cap_stake, max_stake)

        if loss_streak:
            stake *= 0.5

        if min_stake is not None:
            stake = max(stake, min_stake)

        return min(stake, max_stake)
