# source: https://raw.githubusercontent.com/loveolu/crypto-strategy-research/e8a9466dc24b5fcdb50cd4d6f6f2442b0486f2fc/user_data/strategies/BTCOvernightVolTarget.py
# directory_url: https://github.com/loveolu/crypto-strategy-research/blob/main/user_data/strategies/
# User: loveolu
# Repository: crypto-strategy-research
# --------------------# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
import numpy as np
import pandas as pd
import talib.abstract as ta
from freqtrade.strategy import IStrategy, informative


class Github_loveolu_crypto_strategy_research__BTCOvernightVolTarget__20260930_054624(IStrategy):
    """
    BTC Overnight Vol Target.
    - Entry: 10-day rolling-max breakout on Mon/Tue/Fri at 20:00 UTC (16:00 EST)
    - Daily regime filter: only enter when daily EMA50 > EMA200 AND close > EMA50_1d
    - Exit: Mon/Tue/Wed at 14:00 UTC (10:00 EST)
    - Position sizing: inverse 90-day realized vol, V_target=25%, cap 1x for spot
    - Stoploss: ATR-based trailing (3x ATR, floored at -15%)
    """

    INTERFACE_VERSION = 3
    timeframe = '1h'
    can_short = False
    use_custom_stoploss = True

    # OKX hard-caps single-call candle history at 300, so freqtrade limits
    # startup to 5x-1 = 1499. The rolling(2160) vol calc returns NaN for the
    # first ~27 days of IS; custom_stake_amount falls back to proposed_stake
    # during that period.
    startup_candle_count: int = 1499

    stoploss = -0.99  # placeholder; real stop is custom_stoploss
    minimal_roi = {"0": 100}  # trend-following: let winners run

    @property
    def protections(self):
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 5},
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 24 * 7,
                "trade_limit": 3,
                "stop_duration_candles": 24,
                "only_per_pair": True,
            },
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 240,
                "trade_limit": 5,
                "stop_duration_candles": 24,
                "max_allowed_drawdown": 0.15,
            },
            {
                "method": "LowProfitPairs",
                "lookback_period_candles": 24 * 30,
                "trade_limit": 5,
                "stop_duration_candles": 24,
                "required_profit": 0.0,
            },
        ]

    @informative('1d')
    def populate_indicators_1d(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
        dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
        return dataframe

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe['rolling_max_10d'] = dataframe['high'].rolling(window=240).max().shift(1)

        dataframe['log_returns'] = np.log(dataframe['close'] / dataframe['close'].shift(1))
        dataframe['realized_vol_90d'] = (
            dataframe['log_returns'].rolling(window=2160).std() * np.sqrt(365 * 24)
        )

        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe.loc[
            (
                (dataframe['close'] >= dataframe['rolling_max_10d'])
                & (dataframe['date'].dt.hour == 20)
                & (dataframe['date'].dt.dayofweek.isin([0, 1, 4]))
                # Daily regime filter (from informative_1d via @informative decorator)
                & (dataframe['ema50_1d'] > dataframe['ema200_1d'])
                & (dataframe['close'] > dataframe['ema50_1d'])
                & (dataframe['volume'] > 0)
            ),
            'enter_long',
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe.loc[
            (
                (dataframe['date'].dt.hour == 14)
                & (dataframe['date'].dt.dayofweek.isin([0, 1, 2]))
                & (dataframe['volume'] > 0)
            ),
            'exit_long',
        ] = 1
        return dataframe

    def custom_stake_amount(self, pair: str, current_time, current_rate: float,
                            proposed_stake: float, min_stake: float, max_stake: float,
                            leverage: float, entry_tag: str, side: str, **kwargs) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        current_candle = dataframe.iloc[-1].squeeze()

        rv = current_candle['realized_vol_90d']
        if pd.isna(rv) or rv <= 0:
            return proposed_stake

        v_target = 0.25
        weight = min(1.0, v_target / rv)
        total_capital = self.wallets.get_total_stake_amount()
        return total_capital * weight

    def custom_stoploss(self, pair: str, trade, current_time, current_rate: float,
                        current_profit: float, after_fill: bool, **kwargs) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        current_candle = dataframe.iloc[-1].squeeze()
        if pd.isna(current_candle['atr']) or current_candle['atr'] <= 0:
            return -0.15
        atr_pct = current_candle['atr'] / current_rate
        return max(-0.15, -(atr_pct * 3.0))
