# source: https://raw.githubusercontent.com/IanNauw/asdf/9fcdacfde45f29fc406a66a32fcca412d7eb746f/close_price.py
import logging
from functools import reduce
from typing import Dict, List
import pandas as pd
from pandas import DataFrame
from datetime import datetime
import numpy as np
import talib.abstract as ta
from technical import qtpylib

from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
from freqtrade.strategy import IStrategy, DecimalParameter, CategoricalParameter

logger = logging.getLogger(__name__)


class Github_IanNauw_asdf__close_price__20250313_124722(IStrategy):
    """
    An enhanced LSTM model that predicts the closing price of the next candle.
    The difference between the current price and the predicted closing price is calculated in a percentage.
    If the difference is positive and larger than 3%, the model predicts that the price will increase, and buys long.
    If the difference is negative and larger than -3%, the model predicts that the price will decrease, and buys short.
    Enhanced with additional features for better risk management.
    """
    # General settings
    timeframe = "5m"  # Updated from "1h" to "5min"
    can_short = True
    use_exit_signal = True
    process_only_new_candles = True
    startup_candle_count = 120

    # Stoploss:
    stoploss = -0.09

    # Trailing stop:
    trailing_stop = True
    trailing_stop_positive = 0.001
    trailing_stop_positive_offset = 0.014
    trailing_only_offset_is_reached = True

    # Settings for how orders are placed
    order_types = {
        "entry": "market",
        "exit": "market",
        "stoploss": "market",
        "stoploss_on_exchange": True
    }

    # Custom hyperopt class
    class HyperOpt:
        @staticmethod
        def stoploss_space() -> List[Dimension]:
            return [
                SKDecimal(-0.10, -0.01, decimals=3, name='stoploss'),
            ]

        @staticmethod
        def trailing_space() -> List[Dimension]:
            # All parameters here are mandatory, you can only modify their type or the range.
            return [
                Categorical([True], name='trailing_stop'),
                SKDecimal(0.001, 0.35, decimals=3, name='trailing_stop_positive'),
                SKDecimal(0.001, 0.10, decimals=3, name='trailing_stop_positive_offset_p1'),
                Categorical([True], name='trailing_only_offset_is_reached'),
            ]

        @staticmethod
        def max_open_trades_space() -> List[Dimension]:
            return [
                Integer(1, 10, name='max_open_trades'),
            ]

    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:
        return 2

    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs):
        """
        Enhanced engineering of features that will be expanded across all timeframes, periods, etc.
        """
        # Original indicators
        dataframe["%-cci-period"] = ta.CCI(dataframe, timeperiod=20)
        dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=10)
        dataframe["%-momentum-period"] = ta.MOM(dataframe, timeperiod=4)
        dataframe['%-ma-period'] = ta.SMA(dataframe, timeperiod=10)
        dataframe['%-macd-period'], dataframe['%-macdsignal-period'], dataframe['%-macdhist-period'] = ta.MACD(
            dataframe['close'], slowperiod=12,
            fastperiod=26)
        dataframe['%-roc-period'] = ta.ROC(dataframe, timeperiod=2)

        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(
            qtpylib.typical_price(dataframe), window=period, stds=2.2
        )
        dataframe["bb_lowerband-period"] = bollinger["lower"]
        dataframe["bb_middleband-period"] = bollinger["mid"]
        dataframe["bb_upperband-period"] = bollinger["upper"]
        dataframe["%-bb_width-period"] = (
            dataframe["bb_upperband-period"]
            - dataframe["bb_lowerband-period"]
        ) / dataframe["bb_middleband-period"]
        dataframe["%-close-bb_lower-period"] = (
            dataframe["close"] / dataframe["bb_lowerband-period"]
        )

        # NEW: Directional Movement Index Components
        dataframe['%-plus_di-period'] = ta.PLUS_DI(dataframe, timeperiod=period)
        dataframe['%-minus_di-period'] = ta.MINUS_DI(dataframe, timeperiod=period)
        dataframe['%-dx-period'] = ta.DX(dataframe, timeperiod=period)

        # NEW: Hull Moving Average (more responsive than traditional MAs)
        dataframe['%-hull_ma-period'] = qtpylib.hull_moving_average(
            dataframe['close'], window=period
        )

        # NEW: Keltner Channels (uses ATR for dynamic width)
        dataframe['%-kc_middle-period'] = ta.SMA(dataframe['close'], timeperiod=period)
        dataframe['%-kc_width-period'] = ta.ATR(dataframe, timeperiod=period) * 2
        dataframe['%-kc_upper-period'] = dataframe['%-kc_middle-period'] + dataframe['%-kc_width-period']
        dataframe['%-kc_lower-period'] = dataframe['%-kc_middle-period'] - dataframe['%-kc_width-period']

        # NEW: Distance from Keltner Channels
        dataframe['%-close-kc_upper-period'] = (dataframe['close'] / dataframe['%-kc_upper-period'] - 1) * 100
        dataframe['%-close-kc_lower-period'] = (dataframe['close'] / dataframe['%-kc_lower-period'] - 1) * 100

        # NEW: Parabolic SAR for potential stop placement
        dataframe['%-sar-period'] = ta.SAR(dataframe)
        dataframe['%-sar-distance-period'] = (abs(dataframe['close'] - dataframe['%-sar-period']) / dataframe[
            'close']) * 100

        # NEW: Exponential Moving Averages (EMA)
        dataframe["%-ema10-period"] = ta.EMA(dataframe, timeperiod=10)
        dataframe["%-ema20-period"] = ta.EMA(dataframe, timeperiod=20)
        dataframe["%-ema50-period"] = ta.EMA(dataframe, timeperiod=50)

        # NEW: Double Exponential Moving Average (DEMA) for period=20
        ema20 = ta.EMA(dataframe['close'], timeperiod=20)
        ema20_ema = ta.EMA(ema20, timeperiod=20)
        dataframe["%-dema20-period"] = 2 * ema20 - ema20_ema

        # NEW: Ichimoku Cloud Components (standard parameters: 9, 26, 52)
        high9 = dataframe['high'].rolling(window=9).max()
        low9 = dataframe['low'].rolling(window=9).min()
        dataframe["%-ichimoku_conversion"] = (high9 + low9) / 2

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

        dataframe["%-ichimoku_spanA"] = ((dataframe["%-ichimoku_conversion"] + dataframe["%-ichimoku_base"]) / 2).shift(26)
        high52 = dataframe['high'].rolling(window=52).max()
        low52 = dataframe['low'].rolling(window=52).min()
        dataframe["%-ichimoku_spanB"] = ((high52 + low52) / 2).shift(26)
        dataframe["%-ichimoku_lagging"] = dataframe['close'].shift(-26)

        # NEW: Pivot Points (Classic)
        dataframe["%-pivot"] = (dataframe['high'] + dataframe['low'] + dataframe['close']) / 3
        dataframe["%-support1"] = 2 * dataframe["%-pivot"] - dataframe['high']
        dataframe["%-resistance1"] = 2 * dataframe["%-pivot"] - dataframe['low']

        # NEW: Stochastic Oscillator
        low14 = dataframe['low'].rolling(window=14).min()
        high14 = dataframe['high'].rolling(window=14).max()
        dataframe["%-stoch_k"] = 100 * (dataframe['close'] - low14) / (high14 - low14)
        dataframe["%-stoch_d"] = dataframe["%-stoch_k"].rolling(window=3).mean()

        # NEW: Chaikin Oscillator (using AD line)
        adl = ta.AD(dataframe)
        dataframe["%-chaikin"] = ta.EMA(adl, timeperiod=3) - ta.EMA(adl, timeperiod=10)

        # NEW: Ease of Movement (EoM)
        distance_moved = ((dataframe['high'] + dataframe['low']) / 2) - ((dataframe['high'].shift(1) + dataframe['low'].shift(1)) / 2)
        box_ratio = dataframe['volume'] / (dataframe['high'] - dataframe['low']).replace(0, np.nan)
        dataframe["%-eom"] = distance_moved / box_ratio

        return dataframe

    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
        """
        Enhanced engineering of features that will be expanded across timeframes but not periods
        """
        # Original features
        dataframe["%-pct-change"] = dataframe["close"].pct_change()
        dataframe["%-raw_volume"] = dataframe["volume"]
        dataframe["%-raw_price"] = dataframe["close"]

        # Original market condition indicators
        dataframe['%-atr'] = ta.ATR(dataframe, timeperiod=14)
        dataframe['%-volatility'] = dataframe['%-atr'] / dataframe['close'] * 100
        dataframe['%-adx'] = ta.ADX(dataframe)

        # NEW: Volatility ratios for context
        dataframe['%-volatility_7d_ratio'] = dataframe['%-volatility'] / dataframe['%-volatility'].rolling(7).mean()
        dataframe['%-volatility_30d_ratio'] = dataframe['%-volatility'] / dataframe['%-volatility'].rolling(30).mean()

        # NEW: Volume-based indicators
        dataframe['%-volume_mean_30'] = dataframe['volume'].rolling(30).mean()
        dataframe['%-volume_relative'] = dataframe['volume'] / dataframe['%-volume_mean_30']

        # NEW: Accumulation/Distribution Line
        dataframe['%-adl'] = ta.AD(dataframe)
        dataframe['%-adl_change'] = dataframe['%-adl'].pct_change()

        # NEW: Money Flow Index for volume + price trend
        dataframe['%-mfi'] = ta.MFI(dataframe)

        # NEW: Market regime detection (trending vs ranging)
        dataframe['%-adx_threshold'] = 25  # ADX above 25 often indicates trend
        dataframe['%-is_trending'] = (dataframe['%-adx'] > dataframe['%-adx_threshold']).astype(int)

        # NEW: Swing high/low detection for risk levels
        dataframe['%-high_5'] = dataframe['high'].rolling(5).max()
        dataframe['%-low_5'] = dataframe['low'].rolling(5).min()
        dataframe['%-high_5_pct'] = (dataframe['high'] / dataframe['%-high_5'] - 1) * 100
        dataframe['%-low_5_pct'] = (dataframe['low'] / dataframe['%-low_5'] - 1) * 100

        # NEW: On-Balance Volume (OBV)
        dataframe["%-obv"] = ta.OBV(dataframe['close'], dataframe['volume'])

        # NEW: Volume Price Trend (VPT)
        dataframe["%-vpt"] = (dataframe['close'].pct_change() * dataframe['volume']).cumsum()

        # NEW: Volume Weighted Average Price (VWAP) and its difference from current price
        vwap = (dataframe['close'] * dataframe['volume']).cumsum() / dataframe['volume'].cumsum()
        dataframe["%-vwap"] = vwap
        dataframe["%-vwap_diff"] = (dataframe["%-vwap"] - dataframe['close']) / dataframe['close'] * 100

        # NEW: Rolling Volatility (standard deviation of returns)
        dataframe["%-volatility_10"] = dataframe["%-pct-change"].rolling(window=10).std()
        dataframe["%-volatility_20"] = dataframe["%-pct-change"].rolling(window=20).std()
        dataframe["%-volatility_50"] = dataframe["%-pct-change"].rolling(window=50).std()

        # NEW: Range Ratio (High - Low relative to Close)
        dataframe["%-range_ratio"] = (dataframe['high'] - dataframe['low']) / dataframe['close']

        # NEW: Volatility Acceleration (difference between short and longer window volatilities)
        dataframe["%-vol_accel"] = dataframe["%-volatility_10"] - dataframe["%-volatility_20"]

        # NEW: Relative Volatility Index (RVI)-like measure based on close volatility
        tp_std = dataframe['close'].rolling(window=14).std()
        tp_std_min = tp_std.rolling(window=14).min()
        tp_std_max = tp_std.rolling(window=14).max()
        dataframe["%-rvi"] = 100 * (tp_std - tp_std_min) / (tp_std_max - tp_std_min)

        return dataframe

    def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
        """
        Final feature engineering that isn't expanded
        """
        # Original time-based features
        dataframe['date'] = pd.to_datetime(dataframe['date'])
        dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
        dataframe["%-hour_of_day"] = dataframe["date"].dt.hour

        # NEW: Extended time features
        dataframe["%-month"] = dataframe["date"].dt.month
        dataframe["%-day_of_month"] = dataframe["date"].dt.day
        dataframe["%-quarter"] = dataframe["date"].dt.quarter

        # NEW: Market session information (UTC time)
        dataframe['%-is_asia_session'] = ((dataframe['%-hour_of_day'] >= 0) &
                                          (dataframe['%-hour_of_day'] < 8)).astype(int)
        dataframe['%-is_london_session'] = ((dataframe['%-hour_of_day'] >= 8) &
                                            (dataframe['%-hour_of_day'] < 16)).astype(int)
        dataframe['%-is_ny_session'] = ((dataframe['%-hour_of_day'] >= 13) &
                                        (dataframe['%-hour_of_day'] < 21)).astype(int)

        # NEW: Candle pattern detection
        # Hammer pattern: potential bullish reversal
        dataframe['%-hammer'] = (
                (abs(dataframe['open'] - dataframe['close']) <= (dataframe['high'] - dataframe['low']) * 0.1) &
                ((dataframe['high'] - np.maximum(dataframe['open'], dataframe['close'])) <=
                 (np.minimum(dataframe['open'], dataframe['close']) - dataframe['low']) * 0.5) &
                ((dataframe['high'] - np.maximum(dataframe['open'], dataframe['close'])) <=
                 (np.minimum(dataframe['open'], dataframe['close']) - dataframe['low']) * 0.1)
        ).astype(int)

        # Shooting Star: potential bearish reversal
        dataframe['%-shooting_star'] = (
                (abs(dataframe['open'] - dataframe['close']) <= (dataframe['high'] - dataframe['low']) * 0.1) &
                ((np.minimum(dataframe['open'], dataframe['close']) - dataframe['low']) <=
                 (dataframe['high'] - np.maximum(dataframe['open'], dataframe['close'])) * 0.5) &
                ((np.minimum(dataframe['open'], dataframe['close']) - dataframe['low']) <=
                 (dataframe['high'] - np.maximum(dataframe['open'], dataframe['close'])) * 0.1)
        ).astype(int)

        # NEW: Consecutive candle patterns
        dataframe['%-3_up_candles'] = (
                (dataframe['close'] > dataframe['open']) &
                (dataframe['close'].shift(1) > dataframe['open'].shift(1)) &
                (dataframe['close'].shift(2) > dataframe['open'].shift(2))
        ).astype(int)

        dataframe['%-3_down_candles'] = (
                (dataframe['close'] < dataframe['open']) &
                (dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
                (dataframe['close'].shift(2) < dataframe['open'].shift(2))
        ).astype(int)

        # NEW: Additional Candle Patterns
        # Doji Pattern detection
        dataframe["%-doji"] = (abs(dataframe["open"] - dataframe["close"]) <= (dataframe["high"] - dataframe["low"]) * 0.05).astype(int)
        # Bullish Engulfing Pattern
        dataframe["%-bull_engulfing"] = ((dataframe["open"].shift(1) > dataframe["close"].shift(1)) &
                                          (dataframe["open"] < dataframe["close"]) &
                                          (dataframe["open"] < dataframe["close"].shift(1)) &
                                          (dataframe["close"] > dataframe["open"].shift(1))).astype(int)
        # Bearish Engulfing Pattern
        dataframe["%-bear_engulfing"] = ((dataframe["open"].shift(1) < dataframe["close"].shift(1)) &
                                          (dataframe["open"] > dataframe["close"]) &
                                          (dataframe["open"] > dataframe["close"].shift(1)) &
                                          (dataframe["close"] < dataframe["open"].shift(1))).astype(int)
        # Spinning Top Pattern
        dataframe["%-spinning_top"] = (abs(dataframe["open"] - dataframe["close"]) <= (dataframe["high"] - dataframe["low"]) * 0.3).astype(int)

        # NEW: Gap Detection (Up/Down)
        dataframe["%-gap_up"] = ((dataframe["open"] - dataframe["close"].shift(1)) / dataframe["close"].shift(1) > 0.03).astype(int)
        dataframe["%-gap_down"] = ((dataframe["close"].shift(1) - dataframe["open"]) / dataframe["close"].shift(1) > 0.03).astype(int)

        # Divergence Signal - difference between RSI and its 14-candle rolling mean
        rsi_period = ta.RSI(dataframe, timeperiod=10 * 12)
        dataframe["%-rsi_div"] = rsi_period - rsi_period.rolling(window=14 * 12).mean()

        # Market Sentiment Proxy - volume spike indicator
        dataframe["%-volume_spike"] = (dataframe["volume"] > dataframe["volume"].rolling(window=20 * 12).mean() * 1.5).astype(int)

        # NEW: Risk factor - relation to recent swing points
        dataframe['%-risk_factor'] = 0

        # Higher risk when price is near recent highs (for shorts)
        high_risk_short = (
            (dataframe['close'] >= dataframe['high'].rolling(20 * 12).max() * 0.98)
        )

        # Higher risk when price is near recent lows (for longs)
        high_risk_long = (
            (dataframe['close'] <= dataframe['low'].rolling(20 * 12).min() * 1.02)
        )

        dataframe.loc[high_risk_short | high_risk_long, '%-risk_factor'] = 1

        return dataframe

    def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
        """
        Set prediction targets - keeping this simple as it's working well
        """
        dataframe['&-s_close'] = dataframe['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])

        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Run FreqAI and get predictions
        """
        dataframe = self.freqai.start(dataframe, metadata, self)

        return dataframe

    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        # Calculate the difference between the current price and the closing price of the next candle that is predicted by the LSTM model in a percentage.
        difference_in_percentage = (df["&-s_close"] - df["close"]) / df["close"] * 100

        # If the difference is positive and larger than 3%, the model predicts that the price will increase.
        enter_long_conditions = [
            difference_in_percentage > 3,
            df['volume'] > 0,
            df["do_predict"] == 1
        ]

        # If the difference is negative and larger than -3%, the model predicts that the price will decrease.
        enter_short_conditions = [
            difference_in_percentage < -3,
            df["volume"] > 0,
            df["do_predict"] == 1
        ]

        df.loc[
            reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
        ] = (1, "long")
        df.loc[
            reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
        ] = (1, "short")

        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:

        return df
