# source: https://raw.githubusercontent.com/djienne/YOUTUBE_STRATEGIES_FREQTRADE/509f23cfc4c389ca168ab0e551247458753a30fd/Experiences/HEAD_SHOULDER/user_data/strategies/HEAD_SHOULDER.py
import numpy as np
import pandas as pd
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
from typing import List, Optional, Tuple
from typing import List, Dict
from typing import List
from collections import deque
from dataclasses import dataclass
import logging
import warnings

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__)

# Checks if there is a local top detected at curr index
def rw_top(data: np.array, curr_index: int, order: int) -> bool:
    if curr_index < order * 2 + 1:
        return False

    top = True
    k = curr_index - order
    v = data[k]
    for i in range(1, order + 1):
        if data[k + i] > v or data[k - i] > v:
            top = False
            break
    
    return top

# Checks if there is a local top detected at curr index
def rw_bottom(data: np.array, curr_index: int, order: int) -> bool:
    if curr_index < order * 2 + 1:
        return False

    bottom = True
    k = curr_index - order
    v = data[k]
    for i in range(1, order + 1):
        if data[k + i] < v or data[k - i] < v:
            bottom = False
            break
    
    return bottom

def rw_extremes(data: np.array, order:int):
    # Rolling window local tops and bottoms
    tops = []
    bottoms = []
    for i in range(len(data)):
        if rw_top(data, i, order):
            # top[0] = confirmation index
            # top[1] = index of top
            # top[2] = price of top
            top = [i, i - order, data[i - order]]
            tops.append(top)
        
        if rw_bottom(data, i, order):
            # bottom[0] = confirmation index
            # bottom[1] = index of bottom
            # bottom[2] = price of bottom
            bottom = [i, i - order, data[i - order]]
            bottoms.append(bottom)
    
    return tops, bottoms

@dataclass
class HSPattern:

    # True if inverted, False if not. Inverted is "bullish" according to technical analysis dogma
    inverted: bool

    # Indices of the parts of the H&S pattern
    l_shoulder: int = -1
    r_shoulder: int = -1
    l_armpit: int = -1
    r_armpit: int = -1
    head: int = -1
   
    # Price of the parts of the H&S pattern. _p stands for price.
    l_shoulder_p: float = -1
    r_shoulder_p: float = -1
    l_armpit_p: float = -1
    r_armpit_p: float = -1
    head_p: float = -1
   
    start_i: int = -1
    break_i: int = -1
    break_p: float = -1

    neck_start: float = -1
    neck_end: float = -1

    # Attributes
    neck_slope: float = -1
    head_width: float = -1
    head_height: float = -1
    pattern_r2: float = -1

def compute_pattern_r2(data: np.array, pat: HSPattern):

    line0_slope = (pat.l_shoulder_p - pat.neck_start) / (pat.l_shoulder - pat.start_i)
    line0 = pat.neck_start + np.arange(pat.l_shoulder - pat.start_i) * line0_slope
    
    line1_slope = (pat.l_armpit_p - pat.l_shoulder_p) / (pat.l_armpit - pat.l_shoulder)
    line1 = pat.l_shoulder_p + np.arange(pat.l_armpit - pat.l_shoulder) * line1_slope
    
    line2_slope = (pat.head_p - pat.l_armpit_p) / (pat.head - pat.l_armpit)
    line2 = pat.l_armpit_p + np.arange(pat.head - pat.l_armpit) * line2_slope
    
    line3_slope = (pat.r_armpit_p - pat.head_p) / (pat.r_armpit - pat.head)
    line3 = pat.head_p + np.arange(pat.r_armpit - pat.head) * line3_slope
    
    line4_slope = (pat.r_shoulder_p - pat.r_armpit_p) / (pat.r_shoulder - pat.r_armpit)
    line4 = pat.r_armpit_p + np.arange(pat.r_shoulder - pat.r_armpit) * line4_slope
    
    line5_slope = (pat.break_p - pat.r_shoulder_p) / (pat.break_i - pat.r_shoulder)
    line5 = pat.r_shoulder_p + np.arange(pat.break_i - pat.r_shoulder) * line5_slope
    
    raw_data = data[pat.start_i:pat.break_i]
    hs_model = np.concatenate([line0, line1, line2, line3, line4, line5])
    mean = np.mean(raw_data)

    ss_res = np.sum( (raw_data - hs_model) ** 2.0 )
    ss_tot = np.sum( (raw_data - mean) ** 2.0 )

    r2 = 1.0 - ss_res / ss_tot
    return r2

def check_hs_pattern(extrema_indices: List[int], data: np.array, i:int, early_find: bool = False) -> HSPattern:
    ''' Returns a HSPattern if found, or None if not found ''' 
    # Unpack list
    l_shoulder = extrema_indices[0]
    l_armpit = extrema_indices[1]
    head = extrema_indices[2]
    r_armpit = extrema_indices[3]

    if i - r_armpit < 2:
        return None

    # Find right shoulder as max price since r_armpit
    r_shoulder = r_armpit + data[r_armpit + 1: i].argmax() + 1

    # Head must be higher than shoulders
    if data[head] <= max(data[l_shoulder], data[r_shoulder]):
        return None

    # Balance rule. Shoulders are above the others midpoint.
    # A shoulder's midpoint is the midpoint between the shoulder and armpit
    r_midpoint = 0.5 * (data[r_shoulder] + data[r_armpit])
    l_midpoint = 0.5 * (data[l_shoulder] + data[l_armpit])
    if data[l_shoulder] < r_midpoint  or data[r_shoulder] < l_midpoint:
        return None

    # Symmetry rule. time from shoulder to head are comparable
    r_to_h_time = r_shoulder - head
    l_to_h_time = head - l_shoulder
    if r_to_h_time > 2.5 * l_to_h_time or l_to_h_time > 2.5 * r_to_h_time:
        return None
        
    # Compute neckline
    neck_run = r_armpit - l_armpit
    neck_rise = data[r_armpit] - data[l_armpit]
    neck_slope = neck_rise / neck_run
    
    # neckline value at current index
    neck_val = data[l_armpit] + (i - l_armpit) * neck_slope

    # Confirm pattern when price is halfway from right shoulder
    if early_find: 
        if data[i] > r_midpoint:
            return None
    else:
       
        # Price has yet to break neckline, unconfirmed
        if data[i] > neck_val:
            return None

    # Find beginning of pattern. Neck to left shoulder
    head_width = r_armpit - l_armpit
    pat_start = -1
    neck_start = -1
    for j in range(1, head_width):
        neck = data[l_armpit] + (l_shoulder - l_armpit - j) * neck_slope
        
        if l_shoulder - j < 0:
            return None
        
        if data[l_shoulder - j] < neck:
            pat_start = l_shoulder - j
            neck_start = neck
            break

    if pat_start == -1:
        return None

    # Pattern confirmed if here :)
    pat = HSPattern(inverted=False)  
    
    pat.l_shoulder = l_shoulder
    pat.r_shoulder = r_shoulder
    pat.l_armpit = l_armpit
    pat.r_armpit = r_armpit
    pat.head = head
    
    pat.l_shoulder_p = data[l_shoulder]
    pat.r_shoulder_p = data[r_shoulder]
    pat.l_armpit_p = data[l_armpit]
    pat.r_armpit_p = data[r_armpit]
    pat.head_p = data[head]

    pat.start_i = pat_start
    pat.break_i = i
    pat.break_p = data[i]

    pat.neck_start = neck_start
    pat.neck_end = neck_val

    pat.neck_slope = neck_slope
    pat.head_width = head_width
    pat.head_height = data[head] - (data[l_armpit] + (head - l_armpit) * neck_slope)
    pat.pattern_r2 = compute_pattern_r2(data, pat)

    # I experiemented with r-squared as a filter for H&S, but this can delay recognition.
    # It didn't seem terribly potent, may be useful as a filter in conjunction with other attributes
    # if one wanted to add a machine learning layer before trading these patterns. 

    #if pat.pattern_r2 < 0.0:
    #    return None

    return pat

def check_ihs_pattern(extrema_indices: List[int], data: np.array, i:int, early_find: bool = False) -> HSPattern:
    
    # Unpack list
    l_shoulder = extrema_indices[0]
    l_armpit = extrema_indices[1]
    head = extrema_indices[2]
    r_armpit = extrema_indices[3]
    
    if i - r_armpit < 2:
        return None

    # Find right shoulder as max price since r_armpit
    r_shoulder = r_armpit + data[r_armpit+1: i].argmin() + 1

    # Head must be lower than shoulders
    if data[head] >= min(data[l_shoulder], data[r_shoulder]):
        return None

    # Balance rule. Shoulders are below the others midpoint.
    # A shoulder's midpoint is the midpoint between the shoulder and armpit
    r_midpoint = 0.5 * (data[r_shoulder] + data[r_armpit])
    l_midpoint = 0.5 * (data[l_shoulder] + data[l_armpit])
    if data[l_shoulder] > r_midpoint  or data[r_shoulder] > l_midpoint:
        return None

    # Symmetry rule. time from shoulder to head are comparable
    r_to_h_time = r_shoulder - head
    l_to_h_time = head - l_shoulder
    if r_to_h_time > 2.5 * l_to_h_time or l_to_h_time > 2.5 * r_to_h_time:
        return None

    # Compute neckline
    neck_run = r_armpit - l_armpit
    neck_rise = data[r_armpit] - data[l_armpit]
    neck_slope = neck_rise / neck_run
    
    # neckline value at current index
    neck_val = data[l_armpit] + (i - l_armpit) * neck_slope
    
    # Confirm pattern when price is halfway from right shoulder
    if early_find: 
        if data[i] < r_midpoint:
            return None
    else:
       
        # Price has yet to break neckline, unconfirmed
        if data[i] < neck_val:
            return None
   
    # Find beginning of pattern. Neck to left shoulder
    head_width = r_armpit - l_armpit
    pat_start = -1
    neck_start = -1
    for j in range(1, head_width):
        neck = data[l_armpit] + (l_shoulder - l_armpit - j) * neck_slope
        
        if l_shoulder - j < 0:
            return None
        
        if data[l_shoulder - j] > neck:
            pat_start = l_shoulder - j
            neck_start = neck
            break

    if pat_start == -1:
        return None

    # Pattern confirmed if here :)
    pat = HSPattern(inverted=True)  
    
    pat.l_shoulder = l_shoulder
    pat.r_shoulder = r_shoulder
    pat.l_armpit = l_armpit
    pat.r_armpit = r_armpit
    pat.head = head
    
    pat.l_shoulder_p = data[l_shoulder]
    pat.r_shoulder_p = data[r_shoulder]
    pat.l_armpit_p = data[l_armpit]
    pat.r_armpit_p = data[r_armpit]
    pat.head_p = data[head]

    pat.start_i = pat_start
    pat.break_i = i
    pat.break_p = data[i]

    pat.neck_start = neck_start
    pat.neck_end = neck_val
    pat.pattern_r2 = compute_pattern_r2(data, pat)
    
    pat.neck_slope = neck_slope
    pat.head_width = head_width
    pat.head_height = (data[l_armpit] + (head - l_armpit) * neck_slope) - data[head]
    pat.pattern_r2 = compute_pattern_r2(data, pat)
    
    #if pat.pattern_r2 < 0.0:
    #    return None

    return pat


def find_hs_patterns(data: np.array, order:int, early_find:bool = False):
    assert(order >= 1)
    
    # head and shoulders top checked from/after a confirmed bottom (before right shoulder)
    # head and shoulders bottom checked from/after a confirmed top 
    
    last_is_top = False
    recent_extrema = deque(maxlen=5)
    recent_types = deque(maxlen=5) # -1 for bottoms 1 for tops

    # Lock variables to prevent finding the same pattern multiple times
    hs_lock = False
    ihs_lock = False

    ihs_patterns = [] # Inverted (bullish)
    hs_patterns = []  # Regular (bearish)
    for i in range(len(data)):

        if rw_top(data, i, order):
            recent_extrema.append(i - order)
            recent_types.append(1)
            ihs_lock = False
            last_is_top = True
        
        if rw_bottom(data, i, order):
            recent_extrema.append(i - order)
            recent_types.append(-1)
            hs_lock = False
            last_is_top = False

        if len(recent_extrema) < 5:
            continue
        
        hs_alternating = True
        ihs_alternating = True
        
        if last_is_top:
            for j in range(2, 5):
                if recent_types[j] == recent_types[j - 1]: 
                    ihs_alternating = False
            
            for j in range(1, 4):
                if recent_types[j] == recent_types[j - 1]: 
                    hs_alternating = False
            
            ihs_extrema = list(recent_extrema)[1:5]
            hs_extrema = list(recent_extrema)[0:4]
        else:
            
            for j in range(2, 5):
                if recent_types[j] == recent_types[j - 1]: 
                    hs_alternating = False
            
            for j in range(1, 4):
                if recent_types[j] == recent_types[j - 1]: 
                    ihs_alternating = False
            
            ihs_extrema = list(recent_extrema)[0:4]
            hs_extrema = list(recent_extrema)[1:5]
        
        if ihs_lock or not ihs_alternating:
            ihs_pat = None
        else:
            ihs_pat = check_ihs_pattern(ihs_extrema, data, i, early_find)

        if hs_lock or not hs_alternating:
            hs_pat = None
        else:
            hs_pat = check_hs_pattern(hs_extrema, data, i, early_find)

        if hs_pat is not None:
            hs_lock = True
            hs_patterns.append(hs_pat)
        
        if ihs_pat is not None:
            ihs_lock = True
            ihs_patterns.append(ihs_pat)


    return hs_patterns, ihs_patterns


def get_pattern_signals_ihs(data: pd.DataFrame, pat: HSPattern) -> pd.DataFrame:
    """
    Generate trading signals for head and shoulders pattern.
    
    Args:
        data: DataFrame containing price data
        pat: Head and shoulders pattern object
        
    Returns:
        DataFrame with added 'signal' column containing 1 (entry) and -1 (exit)
    """
    # Create copy of dataframe to avoid modifying original
    result = data.copy()
    
    entry_i = pat.break_i
    stop_price = pat.r_shoulder_p
    tp_price = pat.neck_end + pat.head_height
    
    # Set entry signal
    result.loc[entry_i, 'signal'] = 1
    
    # Find exit point
    exit_found = False
    reached_time_limit = False

    for i in range(pat.head_width):
        current_i = entry_i + i

        result.loc[current_i, 'signal'] = 1
        
        # Check if we've reached the end of the data
        if current_i >= len(result):
            reached_time_limit = True
            break
            
        current_price = result.iloc[current_i]['close']  # Assuming 'close' is the price column
        
        # Check if stop loss or take profit is hit
        if (current_price > tp_price or current_price < stop_price):
            result.loc[current_i, 'signal'] = 0
            exit_found = True
            break
    
    # If no exit signal was generated due to stop/tp, exit at pattern width
    if not exit_found or reached_time_limit:
        result.loc[entry_i + pat.head_width, 'signal'] = 1
        
    return result

def generate_signals(df, order=74, early_find=False, pattern_type="both", hold_or_tpsl="tpsl"):
    """
    Generate a signal-only DataFrame from input OHLCV data.

    The input df is assumed to have a datetime index and a 'close' column.
    This function:
      - Converts the close prices to log-space.
      - Uses the existing head–shoulders (HS/IHS) pattern detector (find_hs_patterns)
        to identify potential trade setups.
      - For each pattern, marks an entry (1 for long on inverted HS, -1 for short on HS)
        at the pattern’s break index.
      - Simulates an exit by scanning forward (up to pat.head_width bars) until either
        the take-profit or stop condition is met. (At the exit bar the position returns to 0.)
      - Does not allow overlapping trades (later patterns that occur before a previous trade’s exit are skipped).
      - Allows the user to choose to use only inverted HS ("ihs"), regular HS ("hs"), or both ("both").

    Parameters:
        df (pd.DataFrame): Input DataFrame with a datetime index and a 'close' column.
        order (int): Rolling window order for pattern detection.
        early_find (bool): Whether to use early detection.
        pattern_type (str): One of "hs", "ihs", or "both" to select which pattern(s) to use.

    Returns:
        signal_df (pd.DataFrame): A DataFrame with the same index as df and a single column
                                  "signal" containing:
                                    1 for long positions,
                                   -1 for short positions,
                                    0 when not in a trade.
    """
    import numpy as np

    # Validate pattern_type input.
    pattern_type = pattern_type.lower()
    if pattern_type not in ("both", "hs", "ihs"):
        raise ValueError("pattern_type must be one of 'both', 'hs', or 'ihs'.")

    # Convert close prices to log prices (as used in the original script)
    prices = np.log(df["close"].to_numpy())
    
    # Run pattern detection (assumes find_hs_patterns and required helpers are defined)
    hs_patterns, ihs_patterns = find_hs_patterns(prices, order, early_find)
    
    # Build a combined list of detected trades based on the chosen pattern_type.
    trades = []
    if pattern_type in ("both", "hs"):
        # For regular HS patterns, the expected move is downward (signal = -1)
        for pat in hs_patterns:
            trades.append((pat.break_i, pat, -1))
    if pattern_type in ("both", "ihs"):
        # For inverted HS patterns, the expected move is upward (signal = 1)
        for pat in ihs_patterns:
            trades.append((pat.break_i, pat, 1))
    
    # Sort trades by their break index (chronological order)
    trades.sort(key=lambda x: x[0])
    
    # Initialize a signal array with zeros (no position)
    signals = np.zeros(len(prices))
    last_trade_exit = -1  # to avoid overlapping trades

    # For each detected trade, determine the exit index and fill in the position
    for entry, pat, pos in trades:
        # Skip if this trade starts before the previous one has exited
        if entry <= last_trade_exit:
            continue

        # Define exit conditions:
        # For an inverted pattern (long trade):
        #   take-profit (tp) is defined as neck_end + head_height, and stop is the right shoulder price.
        # For a regular HS (short trade):
        #   take-profit is neck_end - head_height, and stop is the right shoulder price.

        if "tpsl" in hold_or_tpsl:
            entry_idx = pat.break_i
            stop_price = pat.r_shoulder_p
            if pat.inverted:
                tp_price = pat.neck_end + pat.head_height
            else:
                tp_price = pat.neck_end - pat.head_height
            
            # Scan forward from the entry to determine the exit index.
            exit_idx = None
            for offset in range(int(pat.head_width)):
                idx = entry_idx + offset
                if idx >= len(prices):
                    break  # reached end of available data
                price = prices[idx]
                if pat.inverted:
                    # For a long trade: exit if price exceeds tp or falls below stop.
                    if price > tp_price or price < stop_price:
                        exit_idx = idx
                        break
                else:
                    # For a short trade: exit if price falls below tp or rises above stop.
                    if price < tp_price or price > stop_price:
                        exit_idx = idx
                        break
            # If no exit index was found within the allowed window, exit at end of pat.head_width.
            if exit_idx is None:
                exit_idx = entry_idx + offset
        elif "hold" in hold_or_tpsl: # if hold, exit at end of pat.head_width.
            entry_idx = entry_idx = pat.break_i
            exit_idx = pat.break_i + int(pat.head_width)

        # Mark the trade: from the entry bar until (but not including) the exit bar,
        # assign the trade's position.
        signals[entry_idx:exit_idx] = pos
        # The exit bar remains 0 (no position)
        last_trade_exit = exit_idx

    df["signal"] = signals
    return df

class Github_djienne_YOUTUBE_STRATEGIES_FREQTRADE__HEAD_SHOULDER__20250323_093111(IStrategy):
    
    # Paramètres de la stratégie
    minimal_roi = {"0": 5000}
    stoploss = -0.85
    timeframe = '15m'
    startup_candle_count: int = 10
    can_short: bool = True
    use_custom_stoploss: bool = False
    process_only_new_candles: bool = True

    order = IntParameter(3, 200, default=52, space='buy', optimize=True)
    early_find = BooleanParameter(default=False, space="buy", optimize=True)
    pattern_type = CategoricalParameter(['hs', 'ihs', 'both'], default='both', space='buy', optimize=True)
    hold_or_tpsl = CategoricalParameter(['hold', 'tpsl'], default='hold', space='buy', optimize=True)

    LEV = IntParameter(1, 5, default=5, space='buy', optimize=True)

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:

        dataframe = generate_signals(dataframe, order = self.order.value, early_find=self.early_find.value, pattern_type=self.pattern_type.value, hold_or_tpsl=self.hold_or_tpsl.value)

        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        dataframe.loc[
            (dataframe[f'signal'] == 1),
            'enter_long'
        ] = 1
        dataframe.loc[
            (dataframe[f'signal'] == -1),
            'enter_short'
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # exit done with custom_stoploss
        dataframe.loc[
            (dataframe[f'signal'] == 0),
            'exit_long'
        ] = 1
        dataframe.loc[
            (dataframe[f'signal'] == 0),
            'exit_short'
        ] = 1
        return dataframe

    def leverage(self, pair: str, current_time: datetime, current_rate: float,
                 proposed_leverage: float, max_leverage: float, entry_tag: str | None, side: str,
                 **kwargs) -> float:
        """
        Customize leverage for each new trade. This method is only called in futures mode.

        :param pair: Pair that's currently analyzed
        :param current_time: datetime object, containing the current datetime
        :param current_rate: Rate, calculated based on pricing settings in exit_pricing.
        :param proposed_leverage: A leverage proposed by the bot.
        :param max_leverage: Max leverage allowed on this pair
        :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
        :param side: "long" or "short" - indicating the direction of the proposed trade
        :return: A leverage amount, which is between 1.0 and max_leverage.
        """
        return self.LEV.value
