# source: https://raw.githubusercontent.com/vaskosmihaylov/nfi-custom-strategies/19cd8ec4d7574ee3bad2096f4c5201a6a0e459bc/user_data/strategies/Best5m/CompleteIndicatorStrategy22.py
import logging
import numpy as np
import pandas as pd
from pandas import DataFrame
from datetime import datetime

import freqtrade.vendor.qtpylib.indicators as qtpylib

from freqtrade.strategy import IStrategy
import talib.abstract as ta
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter, BooleanParameter

# Optional for additional indicators
import pandas_ta as pta

logger = logging.getLogger(__name__)


class Github_vaskosmihaylov_nfi_custom_strategies__CompleteIndicatorStrategy22__20260530_162717(IStrategy):
    """
    A comprehensive strategy using TA-Lib candlestick patterns and multiple indicators,
    adjusted to avoid lookahead bias (e.g., Ichimoku shift).
    Uses .value for IntParameter/DecimalParameter, preventing “object not callable” errors.
    Explicitly supports shorting (can_short = True), but also requires:
      - `enable_short: true` in your config
      - An exchange that supports margin/futures
      - A negative stoploss
    """

    # =====================================================================
    # Strategy Core Settings
    # =====================================================================

    # Allow short trades
    can_short = True

    # A negative stoploss is required to handle potential short trade losses
    stoploss = -0.20  # Example: 20% stop

    # =====================================================================
    # Strategy Hyperopt Parameters
    # =====================================================================
    # Leverage
    leverage_level = IntParameter(low=1, high=5, default=2, space="buy")

    # Score Thresholds (when to go long/short)
    long_threshold = IntParameter(low=1, high=30, default=11, space="buy")
    short_threshold = IntParameter(low=-30, high=-1, default=-4, space="sell")

    # Trailing Stop
    trailing_stop = True
    trailing_stop_positive = 0.03
    trailing_stop_positive_opt = DecimalParameter(0.01, 0.10, default=0.03, space="sell")

    # Bullish Candlestick Pattern Weights
    hammer_weight = IntParameter(low=1, high=5, default=3, space="buy")
    morning_star_weight = IntParameter(low=1, high=5, default=3, space="buy")
    three_white_soldiers_weight = IntParameter(low=1, high=5, default=3, space="buy")
    piercing_weight = IntParameter(low=1, high=3, default=1, space="buy")

    # Bearish Candlestick Pattern Weights
    shooting_star_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
    evening_star_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
    three_black_crows_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")
    dark_cloud_cover_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")

    # Engulfing & Harami & 3 Inside & Rising/Falling 3 Methods
    bullish_engulfing_weight = IntParameter(low=1, high=5, default=3, space="buy")
    bearish_engulfing_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")

    harami_bullish_weight = IntParameter(low=1, high=3, default=1, space="buy")
    harami_bearish_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")

    three_inside_up_weight = IntParameter(low=1, high=3, default=1, space="buy")
    three_inside_down_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")

    rising_three_methods_weight = IntParameter(low=1, high=3, default=1, space="buy")
    falling_three_methods_weight = IntParameter(low=-3, high=-1, default=-1, space="sell")

    # Indicator Weights
    macd_positive_weight = IntParameter(low=1, high=5, default=2, space="buy")
    macd_negative_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")

    rsi_oversold_weight = IntParameter(low=1, high=5, default=3, space="buy")
    rsi_overbought_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")

    stoch_oversold_weight = IntParameter(low=1, high=5, default=3, space="buy")
    stoch_overbought_weight = IntParameter(low=-5, high=-1, default=-3, space="sell")

    adx_strong_trend_weight = IntParameter(low=1, high=5, default=2, space="buy")
    adx_weak_trend_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")

    sma_gap_positive_weight = IntParameter(low=1, high=5, default=2, space="buy")
    sma_gap_negative_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")

    ichimoku_bullish_weight = IntParameter(low=1, high=5, default=2, space="buy")
    ichimoku_bearish_weight = IntParameter(low=-5, high=-1, default=-2, space="sell")

    # =====================================================================
    # Timeframe & Startup
    # =====================================================================
    timeframe = '1h'
    startup_candle_count = 52  # For Ichimoku + other extended indicators

    def bot_start(self, **kwargs) -> None:
        """
        Sync hyperopt-backed values into the runtime config fields that Freqtrade
        validates as plain scalars during trading startup.
        """
        super().bot_start(**kwargs)
        self.trailing_stop_positive = float(self.trailing_stop_positive_opt.value)
        logger.info("Using trailing_stop_positive=%s", self.trailing_stop_positive)

    # =====================================================================
    # POPULATE INDICATORS
    # =====================================================================
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add indicators and candlestick patterns to the dataframe.
        Convert to numeric, handle missing data, shift Ichimoku to avoid lookahead.
        """

        # Candlestick patterns (mostly 0 or +100)
        dataframe['hammer'] = pd.to_numeric(ta.CDLHAMMER(dataframe), errors='coerce').fillna(0)
        dataframe['shooting_star'] = pd.to_numeric(ta.CDLSHOOTINGSTAR(dataframe), errors='coerce').fillna(0)
        dataframe['morning_star'] = pd.to_numeric(ta.CDLMORNINGSTAR(dataframe), errors='coerce').fillna(0)
        dataframe['evening_star'] = pd.to_numeric(ta.CDLEVENINGSTAR(dataframe), errors='coerce').fillna(0)

        dataframe['three_white_soldiers'] = pd.to_numeric(ta.CDL3WHITESOLDIERS(dataframe), errors='coerce').fillna(0)
        dataframe['three_black_crows'] = pd.to_numeric(ta.CDL3BLACKCROWS(dataframe), errors='coerce').fillna(0)
        dataframe['piercing'] = pd.to_numeric(ta.CDLPIERCING(dataframe), errors='coerce').fillna(0)
        dataframe['dark_cloud_cover'] = pd.to_numeric(ta.CDLDARKCLOUDCOVER(dataframe), errors='coerce').fillna(0)

        # Candlestick patterns (± sign possible)
        dataframe['engulfing'] = pd.to_numeric(ta.CDLENGULFING(dataframe), errors='coerce').fillna(0)
        dataframe['harami'] = pd.to_numeric(ta.CDLHARAMI(dataframe), errors='coerce').fillna(0)
        dataframe['three_inside'] = pd.to_numeric(ta.CDL3INSIDE(dataframe), errors='coerce').fillna(0)
        dataframe['rise_fall_3_methods'] = pd.to_numeric(ta.CDLRISEFALL3METHODS(dataframe), errors='coerce').fillna(0)

        # SMAs
        dataframe['sma_50'] = pd.to_numeric(ta.SMA(dataframe, timeperiod=50), errors='coerce').fillna(0)
        dataframe['sma_200'] = pd.to_numeric(ta.SMA(dataframe, timeperiod=200), errors='coerce').fillna(0)

        # MACD
        macd = ta.MACD(dataframe)
        dataframe['macd'] = pd.to_numeric(macd['macd'], errors='coerce').fillna(0)
        dataframe['macd_signal'] = pd.to_numeric(macd['macdsignal'], errors='coerce').fillna(0)

        # RSI
        dataframe['rsi'] = pd.to_numeric(ta.RSI(dataframe), errors='coerce').fillna(0)

        # Stochastic
        stoch = ta.STOCH(dataframe)
        dataframe['stoch_k'] = pd.to_numeric(stoch['slowk'], errors='coerce').fillna(0)
        dataframe['stoch_d'] = pd.to_numeric(stoch['slowd'], errors='coerce').fillna(0)

        # ADX, +DI, -DI
        dataframe['adx'] = pd.to_numeric(ta.ADX(dataframe), errors='coerce').fillna(0)
        dataframe['plus_di'] = pd.to_numeric(ta.PLUS_DI(dataframe), errors='coerce').fillna(0)
        dataframe['minus_di'] = pd.to_numeric(ta.MINUS_DI(dataframe), errors='coerce').fillna(0)

        # Bollinger Bands
        bb = ta.BBANDS(dataframe)
        dataframe['bb_upper'] = pd.to_numeric(bb['upperband'], errors='coerce').fillna(0)
        dataframe['bb_middle'] = pd.to_numeric(bb['middleband'], errors='coerce').fillna(0)
        dataframe['bb_lower'] = pd.to_numeric(bb['lowerband'], errors='coerce').fillna(0)

        # Parabolic SAR
        dataframe['sar'] = pd.to_numeric(ta.SAR(dataframe), errors='coerce').fillna(0)

        # Ichimoku (shift leading spans 26 to avoid lookahead)
        high9 = dataframe['high'].rolling(window=9).max()
        low9 = dataframe['low'].rolling(window=9).min()
        conversion_line = (high9 + low9) / 2

        high26 = dataframe['high'].rolling(window=26).max()
        low26 = dataframe['low'].rolling(window=26).min()
        base_line = (high26 + low26) / 2

        high52 = dataframe['high'].rolling(window=52).max()
        low52 = dataframe['low'].rolling(window=52).min()
        leading_span_a = ((conversion_line + base_line) / 2).shift(26)
        leading_span_b = ((high52 + low52) / 2).shift(26)

        dataframe['conversion_line'] = pd.to_numeric(conversion_line, errors='coerce').fillna(0)
        dataframe['base_line'] = pd.to_numeric(base_line, errors='coerce').fillna(0)
        dataframe['ichimoku_span_a'] = pd.to_numeric(leading_span_a, errors='coerce').fillna(0)
        dataframe['ichimoku_span_b'] = pd.to_numeric(leading_span_b, errors='coerce').fillna(0)

        # Ensure volume is numeric
        dataframe['volume'] = pd.to_numeric(dataframe['volume'], errors='coerce').fillna(0)

        return dataframe

    # =====================================================================
    # CALCULATE SCORE
    # =====================================================================
    def calculate_score(self, row: pd.Series) -> float:
        """
        Combine candlestick patterns and indicator signals into a single numeric score.
        +ve => bullish, -ve => bearish. Access IntParameter/DecimalParameter with .value.
        """

        score = 0.0

        # ---------- One-sided candlestick patterns ----------
        if row['hammer'] > 0:
            score += self.hammer_weight.value
        if row['shooting_star'] > 0:
            score += self.shooting_star_weight.value

        if row['morning_star'] > 0:
            score += self.morning_star_weight.value
        if row['evening_star'] > 0:
            score += self.evening_star_weight.value

        if row['three_white_soldiers'] > 0:
            score += self.three_white_soldiers_weight.value
        if row['three_black_crows'] > 0:
            score += self.three_black_crows_weight.value

        if row['piercing'] > 0:
            score += self.piercing_weight.value
        if row['dark_cloud_cover'] > 0:
            score += self.dark_cloud_cover_weight.value

        # ---------- Two-sided candlestick patterns (check sign) ----------
        if row['engulfing'] > 0:
            score += self.bullish_engulfing_weight.value
        elif row['engulfing'] < 0:
            score += self.bearish_engulfing_weight.value

        if row['harami'] > 0:
            score += self.harami_bullish_weight.value
        elif row['harami'] < 0:
            score += self.harami_bearish_weight.value

        if row['three_inside'] > 0:
            score += self.three_inside_up_weight.value
        elif row['three_inside'] < 0:
            score += self.three_inside_down_weight.value

        if row['rise_fall_3_methods'] > 0:
            score += self.rising_three_methods_weight.value
        elif row['rise_fall_3_methods'] < 0:
            score += self.falling_three_methods_weight.value

        # ---------- MACD ----------
        if row['macd'] > row['macd_signal']:
            score += self.macd_positive_weight.value
        elif row['macd'] < row['macd_signal']:
            score += self.macd_negative_weight.value

        # ---------- RSI ----------
        if row['rsi'] < 30:
            score += self.rsi_oversold_weight.value
        elif row['rsi'] > 70:
            score += self.rsi_overbought_weight.value

        # ---------- Stochastic ----------
        if row['stoch_k'] < 20 and row['stoch_k'] > row['stoch_d']:
            score += self.stoch_oversold_weight.value
        elif row['stoch_k'] > 80 and row['stoch_k'] < row['stoch_d']:
            score += self.stoch_overbought_weight.value

        # ---------- ADX ----------
        if row['adx'] > 25:
            if row['plus_di'] > row['minus_di']:
                score += self.adx_strong_trend_weight.value
            elif row['minus_di'] > row['plus_di']:
                score += self.adx_weak_trend_weight.value

        # ---------- SMA gap ----------
        if row['sma_50'] != 0:
            gap_50 = (row['close'] - row['sma_50']) / row['sma_50']
            if gap_50 > 0:
                score += self.sma_gap_positive_weight.value
            elif gap_50 < 0:
                score += self.sma_gap_negative_weight.value

        # ---------- Ichimoku ----------
        if row['conversion_line'] > row['base_line'] and row['close'] > row['conversion_line']:
            score += self.ichimoku_bullish_weight.value
        elif row['conversion_line'] < row['base_line'] and row['close'] < row['conversion_line']:
            score += self.ichimoku_bearish_weight.value

        return score

    # =====================================================================
    # POPULATE ENTRY TREND
    # =====================================================================
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Create 'enter_long' and 'enter_short' signals based on the 'score' thresholds.
        Remember, you need `enable_short=true` in your config and an exchange that supports margin/futures.
        """
        # Calculate the row-wise score
        dataframe['score'] = dataframe.apply(self.calculate_score, axis=1)

        # Long if score >= long_threshold
        dataframe['enter_long'] = 0
        dataframe.loc[
            dataframe['score'] >= self.long_threshold.value,
            'enter_long'
        ] = 1

        # Short if score <= short_threshold
        dataframe['enter_short'] = 0
        dataframe.loc[
            dataframe['score'] <= self.short_threshold.value,
            'enter_short'
        ] = 1

        return dataframe

    # =====================================================================
    # POPULATE EXIT TREND
    # =====================================================================
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Basic exit logic: exit positions if 'score' crosses the opposite side of zero.
        """
        dataframe['exit_long'] = 0
        dataframe['exit_short'] = 0

        # Exit long if the current score is negative
        dataframe.loc[dataframe['score'] < 0, 'exit_long'] = 1

        # Exit short if the current score is positive
        dataframe.loc[dataframe['score'] > 0, 'exit_short'] = 1

        return dataframe

    # =====================================================================
    # LEVERAGE (REQUIRED FOR FUTURES)
    # =====================================================================
    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        side: str,
        **kwargs
    ) -> float:
        """
        Optional leverage management for futures trading.
        Return a float, not an IntParameter object.
        """
        # Make sure to return the .value so freqtrade receives a numeric value.
        return float(self.leverage_level.value)
