# source: https://raw.githubusercontent.com/djienne/YOUTUBE_STRATEGIES_FREQTRADE/5c660dd6211bab4ea894f6078b8ac99ed8014144/Experiences/SUPPORT_RESISTANCE/user_data/strategies/SUPPORT_RESISTANCE.py
import numpy as np
import pandas as pd
import pandas_ta as ta
from datetime import datetime
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
                                IStrategy, IntParameter, stoploss_from_absolute, informative, Order)
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
import scipy
import logging
import warnings

# Suppress common warnings that don't affect strategy performance
warnings.filterwarnings(
    'ignore', message='The objective has been evaluated at this point before.')
warnings.simplefilter(action="ignore", category=pd.errors.PerformanceWarning)
warnings.filterwarnings("ignore", category=RuntimeWarning)

logger = logging.getLogger(__name__)

# based on original idea from Neurotrader: https://www.youtube.com/watch?v=mNWPSFOVoYA

def find_levels(price: np.array, atr: float, 
                first_w: float = 0.1, 
                atr_mult: float = 3.0, 
                prom_thresh: float = 0.25):
    """
    Identify support and resistance levels using weighted kernel density estimation.
    
    This function uses a Gaussian kernel to create a market profile from historical
    price data, with more recent prices weighted higher. Significant peaks in the 
    density represent potential support/resistance levels.
    
    Args:
        price (np.array): Array of log closing prices
        atr (float): Average True Range (log scale) for bandwidth scaling
        first_w (float): Weight for oldest price data (default: 0.1)
        atr_mult (float): Multiplier for ATR bandwidth scaling (default: 3.0)
        prom_thresh (float): Minimum prominence threshold for peak detection (default: 0.25)
    
    Returns:
        tuple: (levels, peaks, props, price_range, pdf, weights)
            - levels: List of identified support/resistance levels
            - peaks: Peak indices in the density function
            - props: Peak properties from scipy.signal.find_peaks
            - price_range: Price range used for density calculation
            - pdf: Probability density function values
            - weights: Weight array used for recent price emphasis
    """
    # Create linearly increasing weights (recent prices weighted more heavily)
    last_w = 1.0
    weight_step = (last_w - first_w) / len(price)
    weights = first_w + np.arange(len(price)) * weight_step
    weights[weights < 0] = 0.0

    # Generate kernel density estimation with ATR-scaled bandwidth
    kernel = scipy.stats.gaussian_kde(price, bw_method=atr * atr_mult, weights=weights)

    # Create market profile over price range
    min_price = np.min(price)
    max_price = np.max(price)
    price_step = (max_price - min_price) / 200
    price_range = np.arange(min_price, max_price, price_step)
    pdf = kernel(price_range)  # Market profile density

    # Identify significant peaks representing support/resistance levels
    pdf_max = np.max(pdf)
    prominence_minimum = pdf_max * prom_thresh

    peaks, peak_properties = scipy.signal.find_peaks(pdf, prominence=prominence_minimum)
    
    # Convert log prices back to actual price levels
    levels = []
    for peak in peaks:
        levels.append(np.exp(price_range[peak]))

    return levels, peaks, peak_properties, price_range, pdf, weights


def support_resistance_levels(data_in: pd.DataFrame, lookback: int, 
                             first_w: float = 0.01, 
                             atr_mult: float = 3.0, 
                             prom_thresh: float = 0.25):
    """
    Calculate support and resistance levels for each candle in the dataset.
    
    For each candle, this function analyzes the previous 'lookback' candles
    to identify current support and resistance levels using kernel density estimation.
    
    Args:
        data_in (pd.DataFrame): OHLC price data
        lookback (int): Number of previous candles to analyze
        first_w (float): Weight for oldest price data
        atr_mult (float): ATR multiplier for bandwidth scaling
        prom_thresh (float): Prominence threshold for peak detection
    
    Returns:
        list: List of support/resistance levels for each candle (None for insufficient data)
    """
    data = data_in.copy()
    
    # Calculate Average True Range on log scale for bandwidth estimation
    log_high = np.log(data['high'])
    log_low = np.log(data['low'])
    log_close = np.log(data['close'])
    atr = ta.atr(log_high, log_low, log_close, lookback)

    # Initialize levels array
    all_levels = [None] * len(data)
    
    # Calculate levels for each candle with sufficient lookback data
    for i in range(lookback, len(data)):
        start_index = i - lookback
        # Extract log closing prices for the lookback period
        log_closes = np.log(data.iloc[start_index + 1: i + 1]['close'].to_numpy())
        
        levels, _, _, _, _, _ = find_levels(
            log_closes, atr.iloc[i], first_w, atr_mult, prom_thresh
        )
        all_levels[i] = levels
        
    return all_levels


def sr_penetration_signal(data_in: pd.DataFrame, levels: list):
    """
    Generate trading signals based on support/resistance level penetrations.
    
    Creates buy signals when price closes above a resistance level and
    sell signals when price closes below a support level.
    
    Args:
        data_in (pd.DataFrame): OHLC price data
        levels (list): Support/resistance levels for each candle
    
    Returns:
        pd.DataFrame: Original data with added 'signal' column
            - 1.0: Buy signal (resistance breakout)
            - -1.0: Sell signal (support breakdown)
            - 0.0: No signal
    """
    data = data_in.copy()
    signal = np.zeros(len(data))
    current_signal = 0.0
    close_prices = data['close'].to_numpy()
    
    # Analyze each candle for level penetrations
    for i in range(1, len(data)):
        if levels[i] is None:
            continue

        previous_close = close_prices[i - 1]
        current_close = close_prices[i]
        
        # Check each support/resistance level for penetrations
        for level in levels[i]:
            # Resistance breakout (bullish signal)
            if current_close > level and previous_close <= level:
                current_signal = 1.0
            # Support breakdown (bearish signal)
            elif current_close < level and previous_close >= level:
                current_signal = -1.0

        signal[i] = current_signal

    # Add signal to output dataframe
    data_out = data_in.copy()
    data_out['signal'] = signal
    return data_out


class Github_djienne_YOUTUBE_STRATEGIES_FREQTRADE__SUPPORT_RESISTANCE__20250608_082133(IStrategy):
    """
    Support and Resistance Trading Strategy
    
    This strategy identifies dynamic support and resistance levels using kernel
    density estimation and generates trading signals when price penetrates these levels.
    
    Key Features:
    - Uses weighted kernel density to identify key price levels
    - ATR-based bandwidth scaling for adaptive level detection
    - Long-only strategy focusing on resistance breakouts
    - Exit signals on support breakdowns
    """
    
    # Strategy configuration
    minimal_roi = {"0": 5000}  # Hold positions indefinitely (5000% ROI target)
    stoploss = -0.85  # very large, never hit
    timeframe = '12h'  # 12-hour candles
    startup_candle_count: int = 370  # Minimum candles needed before trading
    can_short: bool = False  # Long-only strategy
    use_custom_stoploss: bool = False
    process_only_new_candles: bool = True

    # Optimization parameters
    first_w = DecimalParameter(
        1.0, 2.0, decimals=1, default=1.6, space="buy", optimize=True,
        load=True
    )
    atr_mult = DecimalParameter(
        2.0, 4.0, decimals=1, default=3.2, space="buy", optimize=True,
        load=True
    )

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Calculate all technical indicators and support/resistance levels.
        
        Args:
            dataframe (pd.DataFrame): Raw OHLC data
            metadata (dict): Pair metadata
            
        Returns:
            pd.DataFrame: Dataframe with indicators and trading signals
        """
        # Calculate support/resistance levels using 365-candle lookback
        levels = support_resistance_levels(
            data_in=dataframe, 
            lookback=365, 
            first_w=self.first_w.value, 
            atr_mult=self.atr_mult.value
        )

        # Generate penetration signals based on calculated levels
        dataframe = sr_penetration_signal(data_in=dataframe, levels=levels)

        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Define entry conditions for long positions.
        
        Entry signal: Resistance level breakout (signal == 1)
        
        Args:
            dataframe (pd.DataFrame): Dataframe with indicators
            metadata (dict): Pair metadata
            
        Returns:
            pd.DataFrame: Dataframe with entry signals
        """
        dataframe.loc[
            (dataframe['signal'] == 1),
            'enter_long'
        ] = 1
        
        # Short entries disabled for this long-only strategy
        # dataframe.loc[
        #     (dataframe['signal'] == -1),
        #     'enter_short'
        # ] = 1
        
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Define exit conditions for positions.
        
        Exit signal: Support level breakdown (signal == -1)
        
        Args:
            dataframe (pd.DataFrame): Dataframe with indicators
            metadata (dict): Pair metadata
            
        Returns:
            pd.DataFrame: Dataframe with exit signals
        """
        dataframe.loc[
            (dataframe['signal'] == -1),
            'exit_long'
        ] = 1
        
        # Short exits disabled for this long-only strategy
        # dataframe.loc[
        #     (dataframe['signal'] == 0),
        #     'exit_short'
        # ] = 1
        
        return dataframe