# source: https://raw.githubusercontent.com/jacobsterling/quantative-analysis/bc9e5405bdb43a6f3a8f090c0e78b8d66313854c/strategies/CVDDivergence.py
from freqtrade.strategy import (
    IStrategy,
    CategoricalParameter,
    IntParameter,
    DecimalParameter,
)
from pandas import DataFrame, Series
import pandas as pd
import numpy as np
import talib.abstract as ta
from datetime import datetime
from freqtrade.persistence import Trade


class Github_jacobsterling_quantative_analysis__CVDDivergence__20241001_152557(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = "1m"
    trailing_stop = False
    process_only_new_candles = False
    use_exit_signal = True
    exit_profit_only = False
    startup_candle_count: int = 288
    can_short = True

    # Add hyperparameters
    pivot_window = IntParameter(5, 15, default=5, space="buy", optimize=True)
    divergence_threshold = DecimalParameter(
        0.01, 2.0, default=1.266, space="buy", optimize=True
    )
    cvd_ma_window = IntParameter(20, 40, default=22, space="buy", optimize=True)

    # Add ATR period and multiplier parameters
    atr_period = IntParameter(5, 25, default=14, space="sell", optimize=True)
    atr_multiplier = DecimalParameter(
        1.0, 4.0, default=2.0, space="sell", optimize=True
    )

    # Change risk_amount to a percentage
    risk_percent = 0.05  # Risk 5% of the trading balance per trade

    max_trades = IntParameter(1, 9, default=1, space="buy", optimize=True)

    poc_histogram_bins = IntParameter(10, 100, default=50, space="buy", optimize=True)

    @property
    def _allowance_per_trade(self) -> float:

        if self.wallets is None:
            trading_balance = 1000  # Default dry_run_wallet amount
        else:
            trading_balance = self.wallets.get_total_stake_amount()

        allowance_per_trade = trading_balance / self.max_trades.value

        return allowance_per_trade

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["date"] = pd.to_datetime(dataframe["date"])
        dataframe["day"] = dataframe["date"].dt.date

        dataframe.set_index("date", inplace=True)

        dataframe = (
            dataframe.groupby("day")
            .apply(self._daily_calculations)
            .reset_index(level=0, drop=True)
        )

        dataframe.reset_index(inplace=True)

        # Calculate ATR
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=self.atr_period.value)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = (
            dataframe.groupby("day")
            .apply(self._process_day_entry)
            .reset_index(level=0, drop=True)
        )

        # Ensure consistent data types for enter_long and enter_short columns
        dataframe["enter_long"] = dataframe["enter_long"].astype(float)
        dataframe["enter_short"] = dataframe["enter_short"].astype(float)

        # Calculate dynamic stoploss based on ATR
        dataframe["atr_stoploss_long"] = dataframe["close"] - (
            dataframe["atr"] * self.atr_multiplier.value
        )
        dataframe["atr_stoploss_short"] = dataframe["close"] + (
            dataframe["atr"] * self.atr_multiplier.value
        )

        return dataframe

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

        # dataframe.loc[dataframe["divergence"] < 0, ["exit_long", "exit_tag"]] = (
        #     1,
        #     "Bear_Div",
        # )

        dataframe.loc[
            dataframe["high"] >= dataframe["vwap_upper_2"],
            ["exit_long", "exit_short", "exit_tag"],
        ] = (1, 1, "VWAP_Upper")

        long_poc_exit = dataframe["bear_poc_lower"] >= dataframe["vwap_upper_1"]

        dataframe.loc[
            (dataframe["high"] >= dataframe["bear_poc_lower"]) & long_poc_exit,
            ["exit_long", "exit_tag"],
        ] = (1, "POC_Upper")

        # dataframe.loc[dataframe["divergence"] > 0, ["exit_short", "exit_tag"]] = (
        #     1,
        #     "Bull_Div",
        # )

        dataframe.loc[
            dataframe["low"] <= dataframe["vwap_lower_2"],
            ["exit_short", "exit_long", "exit_tag"],
        ] = (1, 1, "VWAP_Lower")

        short_poc_exit = dataframe["bull_poc_upper"] <= dataframe["vwap_lower_1"]

        dataframe.loc[
            (dataframe["low"] <= dataframe["bull_poc_upper"]) & short_poc_exit,
            ["exit_short", "exit_tag"],
        ] = (1, "POC_Lower")

        return dataframe

    def _process_day_entry(self, group: DataFrame) -> DataFrame:
        # Ensure the function returns a DataFrame with consistent data types
        group = group.copy()

        # Initialize enter_long and enter_short columns
        group["enter_long"] = np.nan
        group["enter_short"] = np.nan

        # Find the last bullish and bearish divergences
        last_bull_div = group[group["divergence"] > 0].index.max()
        last_bear_div = group[group["divergence"] < 0].index.max()

        # Process entries after the last divergence
        if last_bull_div is not pd.NaT:
            bull_slice = group.loc[last_bull_div:]
            bull_invalidated = (
                (bull_slice["low"] <= bull_slice["vwap_lower_2"]).cummax().astype(bool)
            )
            valid_poc = bull_slice["bull_poc_upper"] <= bull_slice["vwap"]
            long_entry = (
                (bull_slice["low"] <= bull_slice["bull_poc_upper"])
                & ~bull_invalidated
                & valid_poc
            )
            group.loc[bull_slice.index[long_entry], ["enter_long", "enter_tag"]] = (
                1,
                "Bull_POC",
            )

        if last_bear_div is not pd.NaT:
            bear_slice = group.loc[last_bear_div:]
            bear_invalidated = (
                (bear_slice["high"] >= bear_slice["vwap_upper_2"]).cummax().astype(bool)
            )
            valid_poc = bear_slice["bear_poc_lower"] <= bear_slice["vwap"]
            short_entry = (
                (bear_slice["high"] >= bear_slice["bear_poc_lower"])
                & ~bear_invalidated
                & valid_poc
            )
            group.loc[bear_slice.index[short_entry], ["enter_short", "enter_tag"]] = (
                1,
                "Bear_POC",
            )

        return group

    @staticmethod
    def _find_pivot(series: Series, left: int, right: int, compare_func) -> Series:
        return series.rolling(window=left + right + 1, center=True).apply(
            lambda x: compare_func(x) == left
        )

    @staticmethod
    def _detect_divergence(
        dataframe: DataFrame, pivot_col: str, price_col: str, cvd_condition: str
    ) -> Series:
        return (dataframe[pivot_col] == 1) & (
            dataframe[price_col].diff().fillna(0) * cvd_condition
        )

    def _calculate_poc(self, group: DataFrame) -> tuple:

        close = group["close"]
        delta = group["delta"]

        # Separate bullish and bearish deltas
        bull_delta = np.where(
            delta > 0, delta, 0
        )  # Positive delta indicates buying pressure
        bear_delta = -np.where(
            delta < 0, delta, 0
        )  # Negative delta indicates selling pressure

        # Create price-delta histograms for bull and bear activity
        bull_hist, bull_bin_edges = np.histogram(
            close, bins=self.poc_histogram_bins.value, weights=bull_delta
        )
        bear_hist, bear_bin_edges = np.histogram(
            close, bins=self.poc_histogram_bins.value, weights=bear_delta
        )

        # Find the price bins with the highest total delta for bull and bear activity
        bull_max_bin_index = np.argmax(bull_hist)
        bear_max_bin_index = np.argmax(bear_hist)

        # Calculate the bin bounds for bull and bear POCs
        bull_poc_lower = bull_bin_edges[bull_max_bin_index]
        bull_poc_upper = bull_bin_edges[bull_max_bin_index + 1]
        bear_poc_lower = bear_bin_edges[bear_max_bin_index]
        bear_poc_upper = bear_bin_edges[bear_max_bin_index + 1]

        return (
            bear_poc_lower,
            bear_poc_upper,
            bull_poc_lower,
            bull_poc_upper,
        )

    def _detect_regular_bullish_divergence(self, dataframe: DataFrame) -> Series:
        return self._detect_divergence(dataframe, "pivot_low", "low", 1) & (
            dataframe["cvd"].diff().fillna(0) > self.divergence_threshold.value
        )

    def _detect_hidden_bullish_divergence(self, dataframe: DataFrame) -> Series:
        return self._detect_divergence(dataframe, "pivot_low", "low", -1) & (
            dataframe["cvd"].diff().fillna(0) < -self.divergence_threshold.value
        )

    def _detect_regular_bearish_divergence(self, dataframe: DataFrame) -> Series:
        return self._detect_divergence(dataframe, "pivot_high", "high", 1) & (
            dataframe["cvd"].diff().fillna(0) < -self.divergence_threshold.value
        )

    def _detect_hidden_bearish_divergence(self, dataframe: DataFrame) -> Series:
        return self._detect_divergence(dataframe, "pivot_high", "high", -1) & (
            dataframe["cvd"].diff().fillna(0) > self.divergence_threshold.value
        )

    def _calculate_vwap_and_bands(self, group: DataFrame) -> DataFrame:
        typical_price = (group["high"] + group["low"] + group["close"]) / 3

        cumulative_tp_v = (typical_price * group["volume"]).cumsum()
        cumulative_volume = group["volume"].cumsum()
        group["vwap"] = cumulative_tp_v / cumulative_volume
        squared_diff = (typical_price - group["vwap"]) ** 2
        std = np.sqrt((squared_diff * group["volume"]).cumsum() / cumulative_volume)

        group["vwap_upper_1"] = group["vwap"] + std
        group["vwap_lower_1"] = group["vwap"] - std
        group["vwap_upper_2"] = group["vwap"] + 2 * std
        group["vwap_lower_2"] = group["vwap"] - 2 * std

        return group

    def _daily_calculations(self, group: DataFrame) -> DataFrame:

        # Calculate CVD and related indicators
        group["cvd"] = group["delta"].cumsum()

        # Calculate pivot points with optimizable window
        group["pivot_low"] = self._find_pivot(
            group["cvd"],
            self.pivot_window.value // 2,
            self.pivot_window.value // 2,
            np.argmin,
        )
        group["pivot_high"] = self._find_pivot(
            group["cvd"],
            self.pivot_window.value // 2,
            self.pivot_window.value // 2,
            np.argmax,
        )

        # Calculate CVD moving average with optimizable window
        group["cvd_ma"] = (
            group["cvd"].rolling(window=self.cvd_ma_window.value, min_periods=1).mean()
        )

        group["divergence"] = np.select(
            [
                self._detect_regular_bullish_divergence(group),
                self._detect_hidden_bullish_divergence(group),
                self._detect_regular_bearish_divergence(group),
                self._detect_hidden_bearish_divergence(group),
            ],
            [2, 1, -2, -1],
            default=0,
        )

        # Calculate VWAP and bands
        group = self._calculate_vwap_and_bands(group)

        (
            group["bull_poc_lower"],
            group["bull_poc_upper"],
            group["bear_poc_lower"],
            group["bear_poc_upper"],
        ) = self._calculate_poc(group)

        return group

    def custom_stoploss(
        self,
        pair: str,
        trade: "Trade",
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        if trade.is_short:
            stoploss_price = last_candle["atr_stoploss_short"]
            stoploss = (stoploss_price / current_rate) - 1
        else:
            stoploss_price = last_candle["atr_stoploss_long"]
            stoploss = 1 - (stoploss_price / current_rate)

        return stoploss

    def _calculate_position_size(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        side: str,
    ) -> float:  # Changed return type to float
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        # Calculate the stoploss percentage
        if self.dp.runmode.value in ("live", "dry_run"):
            stoploss_percent = abs(
                self.custom_stoploss(pair, None, current_time, current_rate, 0.0)
            )
        else:
            # For backtesting, use the ATR-based stoploss
            stoploss_price = (
                last_candle["atr_stoploss_long"]
                if side == "long"
                else last_candle["atr_stoploss_short"]
            )
            stoploss_percent = abs(1 - (stoploss_price / current_rate))

        # Ensure stoploss_percent is not zero to avoid division by zero
        stoploss_percent = max(stoploss_percent, 0.001)  # Minimum 0.1% stoploss

        # Calculate the risk amount based on the trading balance
        trading_balance = self.wallets.get_total_stake_amount()
        risk_amount = trading_balance * self.risk_percent

        # Calculate the position size based on the risk amount and stoploss
        position_size = risk_amount / stoploss_percent

        return position_size

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float,
        max_stake: float,
        **kwargs,
    ) -> float:
        side = "long" if proposed_stake > 0 else "short"

        position_size = self._calculate_position_size(
            pair, current_time, current_rate, side
        )

        # Ensure the position size is within allowed limits
        fixed_position_size = min(max(position_size, min_stake), max_stake)

        return fixed_position_size

    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: str,
        side: str,
        **kwargs,
    ) -> float:

        position_size = self._calculate_position_size(
            pair, current_time, current_rate, side
        )

        # Calculate required leverage
        required_leverage = position_size / self._allowance_per_trade

        # Ensure the leverage is within allowed limits
        leverage = min(max(required_leverage, 1), max_leverage)

        return leverage
