# source: https://raw.githubusercontent.com/BuyMeAnIcecream/freqtrade-strategies/467128fe23ceda7f530e405d0447da5753c2bbbd/user_data/strategies/CryptoKMLMSwitcher_Advanced.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np  # noqa
import pandas as pd  # noqa
from pandas import DataFrame
from typing import Dict, List
from functools import reduce

from freqtrade.strategy import (
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IStrategy,
    IntParameter,
)

# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import merge_informative_pair


class Github_BuyMeAnIcecream_freqtrade_strategies__CryptoKMLMSwitcher_Advanced__20250904_000942(IStrategy):
    """
    Crypto KMLM Switcher Strategy - ADVANCED VERSION
    
    Implements the sophisticated switching logic from defsymphony.clj
    "Simons KMLM switcher (single pops)| BT 4/13/22 = A.R. 466% / D.D. 22%"
    
    Core Logic:
    1. Multi-pair RSI monitoring (like the original's multi-asset approach)
    2. Defensive switching to stable pairs when major pairs are overbought
    3. Aggressive switching to volatile pairs when major pairs are oversold
    4. Dynamic position sizing based on market conditions
    5. Sophisticated nested decision tree for allocation
    
    Key Features:
    - Defensive mode: Switch to stable pairs (like UVXY in original)
    - Aggressive mode: Switch to volatile pairs (like TECL, SOXL, SPXL)
    - Risk management through RSI thresholds
    - Multi-tier decision tree for optimal allocation
    """

    INTERFACE_VERSION = 3
    timeframe = "5m"  # Optimal timeframe for crypto switching
    
    # ROI table - more realistic for crypto volatility
    minimal_roi = {
        "0": 0.18,     # 6% profit target
        "30": 0.12,    # 4% after 30 minutes
        "60": 0.09,    # 3% after 1 hour
        "120": 0.06,   # 2% after 2 hours
        "240": 0.045,  # 1.5% after 4 hours
        "480": 0.03,   # 1% after 8 hours
    }

    # Stoploss - more appropriate for crypto volatility
    stoploss = -0.90  # 90% stop loss - effectively disabled (let ROI and trailing stop handle exits)
    use_custom_stoploss = False  # Disable custom stoploss to avoid regime-based exits

    # Disable shorting for spot trading
    can_short = False

    # Trailing stoploss - less aggressive
    trailing_stop = True
    trailing_stop_positive = 0.015  # 1.5% trailing stop
    trailing_stop_positive_offset = 0.025  # 2.5% offset
    trailing_only_offset_is_reached = True

    # Run "populate_indicators()" only for new candle
    process_only_new_candles = True

    # Number of candles the strategy requires before producing valid signals
    startup_candle_count: int = 30

    # Strategy parameters - optimized for crypto
    rsi_window = IntParameter(10, 15, default=12, space="buy")
    rsi_overbought_high = IntParameter(75, 85, default=78, space="sell")
    rsi_overbought_medium = IntParameter(70, 80, default=72, space="sell")
    rsi_oversold = IntParameter(20, 40, default=35, space="buy")  # Relaxed from 28 to 35
    
    # Volume confirmation
    volume_multiplier = DecimalParameter(1.0, 2.0, default=1.2, space="buy")  # Relaxed from 1.5 to 1.2
    
    # Position sizing parameters
    max_risk_per_trade = DecimalParameter(0.02, 0.08, default=0.05, space="buy")
    defensive_allocation = DecimalParameter(0.3, 0.7, default=0.5, space="buy")
    aggressive_allocation = DecimalParameter(0.6, 0.9, default=0.8, space="buy")

    # Exit signal parameters - easily editable
    enable_exit_signals = False  # Set to False to disable exit signals (main source of losses)
    exit_rsi_threshold = 92  # RSI level for exit (higher = less exits)
    exit_trend_drop = 0.88  # Price drop below SMA20 for trend exit (lower = less exits)
    exit_extreme_drop = 0.80  # Extreme price drop for exit (lower = less exits)
    exit_volume_spike = 2.5  # Volume ratio for panic selling exit (higher = less exits)
    exit_candle_drop = 0.85  # Intra-candle drop for volume spike exit (lower = less exits)
    
    # Regime change exit parameters - reduce these to minimize regime change losses
    enable_regime_exits = False  # Set to False to disable regime change exits (bleeding the most)
    regime_exit_profit_threshold = 0.12  # Exit defensive positions when aggressive (higher = less exits)
    regime_exit_loss_threshold = -0.20  # Exit aggressive positions when defensive (lower = less exits)
    
    # Timeout parameters
    enable_trade_timeout = False  # Set to False to disable timeout exits
    trade_timeout_hours = 72  # Hours before timeout exit (higher = longer trades)

    def informative_pairs(self):
        """
        Define additional, informative pair/interval combinations to be cached from the exchange.
        These represent the "market leaders" that drive switching decisions.
        """
        return [
            ("BTC/USDT", "5m"),  # Market leader (like SPY in original)
            ("ETH/USDT", "5m"),  # Tech leader (like QQQE in original)
            ("BNB/USDT", "5m"),  # Exchange token (like XLK in original)
            ("ADA/USDT", "5m"),  # Volatile alt (like TECL in original)
            ("DOT/USDT", "5m"),  # Volatile alt (like SOXL in original)
            ("SOL/USDT", "5m"),  # Volatile alt (like SPXL in original)
            #("AVAX/USDT", "5m"), # Volatile alt (like LABU in original)
        ]

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Adds several different TA indicators to the given DataFrame
        """
        # RSI
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.rsi_window.value)
        
        # Moving averages for trend detection
        dataframe['sma_20'] = ta.SMA(dataframe, timeperiod=20)
        dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50)
        dataframe['sma_200'] = ta.SMA(dataframe, timeperiod=200)
        
        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
        dataframe['bb_lowerband'] = bollinger['lower']
        dataframe['bb_middleband'] = bollinger['mid']
        dataframe['bb_upperband'] = bollinger['upper']
        dataframe['bb_percent'] = (dataframe['close'] - dataframe['bb_lowerband']) / (dataframe['bb_upperband'] - dataframe['bb_lowerband'])
        
        # Volume indicators
        dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()
        dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_mean']
        
        # Market conditions
        dataframe['bullish_trend'] = dataframe['sma_20'] > dataframe['sma_50']
        dataframe['bearish_trend'] = dataframe['sma_20'] < dataframe['sma_50']
        dataframe['strong_trend'] = dataframe['sma_50'] > dataframe['sma_200']
        
        # RSI conditions
        dataframe['rsi_oversold'] = dataframe['rsi'] < self.rsi_oversold.value
        dataframe['rsi_overbought_medium'] = dataframe['rsi'] > self.rsi_overbought_medium.value
        dataframe['rsi_overbought_high'] = dataframe['rsi'] > self.rsi_overbought_high.value
        
        # Volatility
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
        dataframe['high_volatility'] = dataframe['atr'] > dataframe['atr'].rolling(window=20).mean() * 1.5
        
        # Market regime detection
        dataframe['market_regime'] = self.detect_market_regime(dataframe)
        
        return dataframe

    def detect_market_regime(self, dataframe: DataFrame) -> pd.Series:
        """
        Detect market regime based on RSI and volatility
        Returns: 0 = Defensive, 1 = Neutral, 2 = Aggressive
        """
        regime = pd.Series(1, index=dataframe.index)  # Default to neutral
        
        # Defensive regime: High RSI + High volatility
        defensive_condition = (
            (dataframe['rsi_overbought_high']) &
            (dataframe['high_volatility'])
        )
        regime[defensive_condition] = 0
        
        # Aggressive regime: Low RSI + Strong trend
        aggressive_condition = (
            (dataframe['rsi_oversold']) &
            (dataframe['strong_trend']) &
            (dataframe['volume_ratio'] > self.volume_multiplier.value)
        )
        regime[aggressive_condition] = 2
        
        return regime

    def get_market_rsis(self, metadata: dict) -> Dict[str, float]:
        """
        Get RSI values from major pairs to determine switching conditions
        """
        market_rsis = {}
        
        # Define the pairs we want to monitor (like the original strategy)
        pairs_to_monitor = [
            'BTC/USDT', 'ETH/USDT', 'BNB/USDT', 
            'ADA/USDT', 'DOT/USDT', 'SOL/USDT'#, 'AVAX/USDT' binance us does not trade this
        ]
        
        for pair in pairs_to_monitor:
            if pair != metadata['pair']:
                try:
                    pair_dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
                    if pair_dataframe is not None and len(pair_dataframe) > 0:
                        # Calculate RSI for this pair
                        pair_rsi = ta.RSI(pair_dataframe, timeperiod=self.rsi_window.value)
                        market_rsis[pair] = pair_rsi.iloc[-1] if not pd.isna(pair_rsi.iloc[-1]) else 50
                except:
                    market_rsis[pair] = 50  # Default if data unavailable
        
        return market_rsis

    def determine_position_size(self, dataframe: DataFrame, metadata: dict) -> float:
        """
        Determine position size based on market regime and RSI conditions
        Implements the sophisticated allocation logic from the original strategy
        """
        # Get market RSI values
        market_rsis = self.get_market_rsis(metadata)
        
        # Count how many pairs are overbought/oversold
        overbought_count = sum(1 for rsi in market_rsis.values() if rsi > self.rsi_overbought_high.value)
        oversold_count = sum(1 for rsi in market_rsis.values() if rsi < self.rsi_oversold.value)
        
        # Get current pair's RSI
        current_rsi = dataframe['rsi'].iloc[-1]
        
        # Determine allocation based on market conditions
        if overbought_count >= 3:  # Defensive mode (like UVXY in original)
            return self.defensive_allocation.value
        elif oversold_count >= 3 and current_rsi < self.rsi_oversold.value:  # Aggressive mode
            return self.aggressive_allocation.value
        else:  # Neutral mode
            return 0.6  # Default allocation
        
        return 0.5  # Fallback

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Implements the sophisticated nested decision tree from the original strategy
        """
        
        # Get market RSI values
        market_rsis = self.get_market_rsis(metadata)
        
        # Initialize entry conditions
        long_conditions = []
        
        # Count overbought/oversold pairs for better decision making
        overbought_count = sum(1 for rsi in market_rsis.values() if rsi > self.rsi_overbought_high.value)
        oversold_count = sum(1 for rsi in market_rsis.values() if rsi < self.rsi_oversold.value)
        
        # Base entry condition - always check for oversold RSI
        base_condition = (
            (dataframe['rsi_oversold']) &
            (dataframe['volume'] > 0)
        )
        
        # Defensive mode: When major pairs are overbought, prefer stable pairs
        defensive_mode = overbought_count >= 1  # Relaxed from 2 to 1
        
        # Aggressive mode: When many pairs are oversold, prefer volatile pairs
        aggressive_mode = oversold_count >= 1  # Relaxed from 2 to 1
        
        # Entry logic with pair-specific conditions
        if metadata['pair'] in ['BTC/USDT', 'ETH/USDT', 'BNB/USDT']:
            # Stable pairs - enter in defensive mode or when oversold
            if defensive_mode:
                # Defensive entry for stable pairs
                defensive_entry = (
                    base_condition &
                    (dataframe['volume_ratio'] > 0.8)  # Relaxed from 1.1 to 0.8
                )
                long_conditions.append(defensive_entry)
            else:
                # Normal entry for stable pairs
                stable_entry = (
                    base_condition &
                    (dataframe['volume_ratio'] > 0.9)  # Relaxed from 1.2 to 0.9
                )
                long_conditions.append(stable_entry)
        
        elif metadata['pair'] in ['ADA/USDT', 'DOT/USDT', 'SOL/USDT']:
            # Volatile pairs - enter in aggressive mode or when oversold
            if aggressive_mode:
                # Aggressive entry for volatile pairs
                aggressive_entry = (
                    base_condition &
                    (dataframe['volume_ratio'] > 1.0)  # Relaxed from volume_multiplier to 1.0
                )
                long_conditions.append(aggressive_entry)
            else:
                # Normal entry for volatile pairs
                volatile_entry = (
                    base_condition &
                    (dataframe['volume_ratio'] > 1.0)  # Relaxed from 1.3 to 1.0
                )
                long_conditions.append(volatile_entry)
        
        else:
            # Other pairs - balanced approach
            balanced_entry = (
                base_condition &
                (dataframe['volume_ratio'] > 0.9)  # Relaxed from 1.2 to 0.9
            )
            long_conditions.append(balanced_entry)
        
        # Combine all conditions
        if long_conditions:
            dataframe.loc[reduce(lambda x, y: x | y, long_conditions), 'enter_long'] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Exit logic - conservative exit signals that work with ROI and trailing stop
        """
        
        # Disable exit signals if parameter is False
        if not self.enable_exit_signals:
            dataframe['exit_long'] = 0
            dataframe['exit_short'] = 0
            return dataframe
        
        # Exit long positions - only on very strong reversal signals
        # This should be much less frequent than ROI/trailing stop exits
        dataframe.loc[
            (
                # Exit only on extreme RSI overbought (very high threshold)
                (dataframe['rsi'] > self.exit_rsi_threshold) |
                # Exit only on strong bearish trend AND significant price drop
                (dataframe['bearish_trend'] & (dataframe['close'] < dataframe['sma_20'] * self.exit_trend_drop)) |
                # Exit only on extreme reversal (very significant drop)
                (dataframe['close'] < dataframe['sma_20'] * self.exit_extreme_drop) |
                # Exit on volume spike with price drop (panic selling)
                ((dataframe['volume_ratio'] > self.exit_volume_spike) & (dataframe['close'] < dataframe['open'] * self.exit_candle_drop))
            ) &
            (dataframe['volume'] > 0),
            'exit_long'
        ] = 1

        return dataframe

    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                       current_profit: float, **kwargs) -> float:
        """
        Custom stoploss logic based on market regime
        """
        # Get the dataframe for this pair
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        
        if len(dataframe) > 0:
            # Get current market regime
            current_regime = dataframe['market_regime'].iloc[-1]
            
            # Adjust stoploss based on regime
            if current_regime == 0:  # Defensive
                return -0.05  # Tighter stoploss
            elif current_regime == 2:  # Aggressive
                return -0.15  # Wider stoploss
            else:  # Neutral
                return self.stoploss
        
        return self.stoploss

    def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                   current_profit: float, **kwargs) -> str:
        """
        Custom exit logic based on market regime changes - less aggressive
        """
        # Disable regime exits if parameter is False
        if not self.enable_regime_exits:
            # Only check timeout if regime exits are disabled
            if self.enable_trade_timeout and (current_time - trade.open_date_utc).total_seconds() > (self.trade_timeout_hours * 3600):
                return "timeout_exit"
            return None
        
        # Get the dataframe for this pair
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        
        if len(dataframe) > 0:
            # Exit if market regime changes significantly
            current_regime = dataframe['market_regime'].iloc[-1]
            
            # Exit defensive positions when market becomes aggressive (higher profit threshold)
            if current_regime == 2 and current_profit > self.regime_exit_profit_threshold:
                return "regime_change_exit"
            
            # Exit aggressive positions when market becomes defensive (lower loss threshold)
            if current_regime == 0 and current_profit < self.regime_exit_loss_threshold:
                return "regime_change_exit"
        
        # Exit if we've been in the trade too long (configurable timeout)
        if self.enable_trade_timeout and (current_time - trade.open_date_utc).total_seconds() > (self.trade_timeout_hours * 3600):
            return "timeout_exit"
        
        return None
