# source: https://raw.githubusercontent.com/camster91/crypto-bots/5fd45118d5be1eaadd338e71ca26072dd5437831/strategies/ImprovedStrategyV2.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple

from freqtrade.strategy import (
    IStrategy,
    Trade,
    Order,
    PairLocks,
    informative,  # @informative decorator
    # Hyperopt Parameters
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IntParameter,
    RealParameter,
    # timeframe helpers
    timeframe_to_minutes,
    timeframe_to_next_date,
    timeframe_to_prev_date,
    # Strategy helper functions
    merge_informative_pair,
    stoploss_from_absolute,
    stoploss_from_open,
    AnnotationType,
)

# --------------------------------
# Add your lib to import here
import talib.abstract as ta
from technical import qtpylib


class Github_camster91_crypto_bots__ImprovedStrategyV2__20260511_032334(IStrategy):
    """
    Improved RSI-based strategy with multiple enhancements:
    
    1. Trend Filter: Only trade in direction of 200-period EMA
    2. Trailing Stop Loss: Protect profits with dynamic trailing stop
    3. MACD Confirmation: Require MACD bullish/bearish alignment
    4. Bollinger Bands: Use BB for volatility-based entries/exits
    5. Multi-Indicator Confirmation: Combine RSI, MACD, and BB signals
    
    This strategy aims to improve upon the basic RSI strategy by:
    - Reducing false signals with trend filter
    - Protecting profits with trailing stop
    - Adding confirmation from multiple indicators
    - Being hyperoptable for parameter optimization
    """
    
    # Strategy interface version
    INTERFACE_VERSION = 3

    # Can this strategy go short?
    can_short: bool = False

    # Minimal ROI designed for the strategy.
    minimal_roi = {
        "0": 0.225,   # 22.5% immediate
        "39": 0.037,  # 3.7% after 39 minutes
        "73": 0.022,  # 2.2% after 73 minutes
        "110": 0,     # 0% after 110 minutes
    }

    # Optimal stoploss designed for the strategy.
    stoploss = -0.243  # Hyperopt optimized stop loss (-24.3%) - overridden by custom_stoploss

    # Trailing stoploss - ENABLED
    trailing_stop = True
    trailing_only_offset_is_reached = False
    trailing_stop_positive = 0.128  # 12.8% trailing stop distance (hyperopt)
    trailing_stop_positive_offset = 0.185  # Activate trailing after 18.5% profit (hyperopt)

    # Optimal timeframe for the strategy.
    timeframe = "5m"

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

    # These values can be overridden in the config.
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False
    use_custom_stoploss = True

    # Number of candles the strategy requires before producing valid signals
    startup_candle_count: int = 300  # For EMA 251 + buffer calculation

    # Hyperoptable parameters
    buy_rsi = IntParameter(15, 40, default=30, space="buy", optimize=True)
    sell_rsi = IntParameter(60, 90, default=78, space="sell", optimize=True)
    
    # EMA period for trend filter (long-term trend)
    ema_period = IntParameter(100, 300, default=168, space="buy", optimize=True)
    
    # Bollinger Bands parameters
    bb_period = IntParameter(10, 30, default=21, space="buy", optimize=True)
    bb_std = DecimalParameter(1.5, 3.0, default=2.118, space="buy", optimize=True)
    
    # MACD parameters
    macd_fast = IntParameter(8, 15, default=12, space="buy", optimize=True)
    macd_slow = IntParameter(20, 30, default=20, space="buy", optimize=True)
    macd_signal = IntParameter(5, 12, default=11, space="buy", optimize=True)

    def informative_pairs(self):
        """
        Define additional, informative pair/interval combinations to be cached.
        """
        return []

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Adds several different TA indicators to the given DataFrame.
        """
        # Trend Indicator: EMA (for trend filter)
        dataframe['ema'] = ta.EMA(dataframe, timeperiod=self.ema_period.value)
        
        # Momentum Indicators
        dataframe['rsi'] = ta.RSI(dataframe)
        
        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(
            qtpylib.typical_price(dataframe), 
            window=self.bb_period.value, 
            stds=self.bb_std.value
        )
        dataframe['bb_lowerband'] = bollinger['lower']
        dataframe['bb_middleband'] = bollinger['mid']
        dataframe['bb_upperband'] = bollinger['upper']
        dataframe['bb_width'] = (dataframe['bb_upperband'] - dataframe['bb_lowerband']) / dataframe['bb_middleband']
        
        # MACD
        macd = ta.MACD(
            dataframe, 
            fastperiod=self.macd_fast.value,
            slowperiod=self.macd_slow.value, 
            signalperiod=self.macd_signal.value
        )
        dataframe['macd'] = macd['macd']
        dataframe['macdsignal'] = macd['macdsignal']
        dataframe['macdhist'] = macd['macdhist']
        
        # Volume indicators
        dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=20)
        
        # ATR for volatility measurement
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
        
        # ADX for trend strength
        dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
        
        # Short EMA for sideways markets
        dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=50)
        
        # Dynamic EMA: use long EMA when strong trend (ADX > 25), otherwise short EMA
        dataframe['ema_dynamic'] = np.where(
            dataframe['adx'] > 25,
            dataframe['ema'],
            dataframe['ema_short']
        )
        
        # Calculate percent above/below EMA
        dataframe['ema_distance'] = (dataframe['close'] - dataframe['ema']) / dataframe['ema'] * 100
        
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Based on TA indicators, populates the entry signal for the given dataframe.
        Buy when RSI crosses above oversold level (momentum shift) and price is above EMA (uptrend).
        Minimal test version.
        """
        conditions = []
        
        # Condition 1: Price above dynamic EMA (adaptive trend filter)
        conditions.append(dataframe['close'] > dataframe['ema_dynamic'])
        
        # Condition 2: RSI crosses above oversold level (momentum shift)
        conditions.append(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value))
        
        # Condition 3: Basic volume check
        conditions.append(dataframe['volume'] > 0)
        
        # Apply all conditions
        if conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, conditions),
                'enter_long'] = 1
        
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Based on TA indicators, populates the exit signal for the given dataframe.
        Sell when RSI crosses above overbought level (entering overbought territory).
        """
        conditions = []
        
        # Condition 1: RSI crosses above overbought level
        conditions.append(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value))
        
        # Apply all conditions
        if conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, conditions),
                'exit_long'] = 1
        
        return dataframe
    
    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                           proposed_stake: float, min_stake: float, max_stake: float,
                           entry_tag: Optional[str], side: str, **kwargs) -> float:
        """
        Custom stake amount based on volatility.
        Lower position size during high volatility.
        """
        # Safety check: data provider may not be available in all modes
        if self.dp is None:
            return proposed_stake
            
        # Get the dataframe for this pair
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        
        if len(dataframe) > 0:
            # Use ATR-based position sizing
            current_atr = dataframe['atr'].iloc[-1]
            current_price = dataframe['close'].iloc[-1]
            atr_percent = current_atr / current_price
            
            # Higher volatility = smaller position (risk management)
            # Base risk: 1% of portfolio per trade
            base_risk = 0.01
            volatility_adjustment = base_risk / max(atr_percent, 0.01)  # Cap at 1% risk
            
            # Adjust stake (but respect min/max limits)
            adjusted_stake = proposed_stake * volatility_adjustment
            return min(max(adjusted_stake, min_stake), max_stake)
        
        return proposed_stake

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                       current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Dynamic stop-loss based on ATR volatility.
        Stop loss = 1.5 × ATR below entry price, capped between -2% and -8%.
        """
        # Safety check: data provider may not be available in all modes
        if self.dp is None:
            return self.stoploss  # Fallback to default
        
        # Get the dataframe for this pair
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        
        if len(dataframe) > 0:
            current_atr = dataframe['atr'].iloc[-1]
            current_price = dataframe['close'].iloc[-1]
            atr_percent = current_atr / current_price
            
            # Dynamic stop loss: 1.5 × ATR
            atr_stoploss = 1.5 * atr_percent
            
            # Cap between 2% and 8% (allow tighter stops in low volatility)
            capped_stoploss = max(min(atr_stoploss, 0.08), 0.02)
            
            # Return negative value (stop loss is negative)
            return -capped_stoploss
        
        # Fallback to default stop loss
        return self.stoploss

# Required import for reduce function
from functools import reduce