# source: https://raw.githubusercontent.com/ak4noir/freqtrade-strategies/2f02b6510af8dddbdfeabb09bdc8c5609de8ff53/user_data/strategies/smart_money_strategy.py
import logging
from functools import reduce

import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np
import pandas as pd
import talib.abstract as ta
from freqtrade.strategy import CategoricalParameter, IntParameter, informative
from freqtrade.strategy.interface import IStrategy
from mixins import SafetyOrderMixin, TrailingTakeProfitMixin
from pandas import DataFrame

logger = logging.getLogger(__name__)


def detect_pullback(dataframe: DataFrame, periods=30, method="pct_outlier"):
    """
    Pullback & Outlier Detection
    Know when a sudden move and possible reversal is coming

    Method 1: StDev Outlier (z-score)
    Method 2: Percent-Change Outlier (z-score)
    Method 3: Candle Open-Close %-Change

    outlier_threshold - Recommended: 2.0 - 3.0

    df['pullback_flag']: 1 (Outlier Up) / -1 (Outlier Down)
    """
    df = dataframe.copy()
    if method == "stdev_outlier":
        outlier_threshold = 2.0
        df["dif"] = df["close"] - df["close"].shift(1)
        df["dif_squared_sum"] = (df["dif"] ** 2).rolling(window=periods + 1).sum()
        df["std"] = np.sqrt(
            (df["dif_squared_sum"] - df["dif"].shift(0) ** 2) / (periods - 1)
        )
        df["z"] = df["dif"] / df["std"]
        df["pullback_flag"] = np.where(df["z"] >= outlier_threshold, 1, 0)
        df["pullback_flag"] = np.where(
            df["z"] <= -outlier_threshold, -1, df["pullback_flag"]
        )

    if method == "pct_outlier":
        outlier_threshold = 2.0
        df["pb_pct_change"] = df["close"].pct_change()
        df["pb_zscore"] = qtpylib.zscore(df, window=periods, col="pb_pct_change")
        df["pullback_flag"] = np.where(df["pb_zscore"] >= outlier_threshold, 1, 0)
        df["pullback_flag"] = np.where(
            df["pb_zscore"] <= -outlier_threshold, -1, df["pullback_flag"]
        )

    if method == "candle_body":
        pullback_pct = 1.0
        df["change"] = df["close"] - df["open"]
        df["pullback"] = (df["change"] / df["open"]) * 100
        df["pullback_flag"] = np.where(df["pullback"] >= pullback_pct, 1, 0)
        df["pullback_flag"] = np.where(
            df["pullback"] <= -pullback_pct, -1, df["pullback_flag"]
        )

    return df


def smi_trend(
    dataframe: DataFrame, k_length=9, d_length=3, smoothing_type="EMA", smoothing=10
):
    """
    Stochastic Momentum Index (SMI) Trend Indicator

    SMI > 0 and SMI > MA: (2) Bull
    SMI < 0 and SMI > MA: (1) Possible Bullish Reversal

    SMI > 0 and SMI < MA: (-1) Possible Bearish Reversal
    SMI < 0 and SMI < MA: (-2) Bear

    Returns:
        pandas.Series: New feature generated
    """
    df = dataframe.copy()
    ll = df["low"].rolling(window=k_length).min()
    hh = df["high"].rolling(window=k_length).max()

    diff = hh - ll
    rdiff = df["close"] - (hh + ll) / 2

    avgrel = rdiff.ewm(span=d_length).mean().ewm(span=d_length).mean()
    avgdiff = diff.ewm(span=d_length).mean().ewm(span=d_length).mean()

    smi = np.where(avgdiff != 0, (avgrel / (avgdiff / 2) * 100), 0)

    if smoothing_type == "SMA":
        smi_ma = ta.SMA(smi, timeperiod=smoothing)
    elif smoothing_type == "EMA":
        smi_ma = ta.EMA(smi, timeperiod=smoothing)
    elif smoothing_type == "WMA":
        smi_ma = ta.WMA(smi, timeperiod=smoothing)
    elif smoothing_type == "DEMA":
        smi_ma = ta.DEMA(smi, timeperiod=smoothing)
    elif smoothing_type == "TEMA":
        smi_ma = ta.TEMA(smi, timeperiod=smoothing)
    else:
        raise ValueError("Choose an MA Type: 'SMA', 'EMA', 'WMA', 'DEMA', 'TEMA'")

    conditions = [
        (np.greater(smi, 0) & np.greater(smi, smi_ma)),  # (2) Bull
        (np.less(smi, 0) & np.greater(smi, smi_ma)),  # (1) Possible Bullish Reversal
        (np.greater(smi, 0) & np.less(smi, smi_ma)),  # (-1) Possible Bearish Reversal
        (np.less(smi, 0) & np.less(smi, smi_ma)),  # (-2) Bear
    ]

    smi_trend = np.select(conditions, [2, 1, -1, -2])

    return smi, smi_ma, smi_trend


def smart_money_index(dataframe: DataFrame):
    df = dataframe.copy()
    last_candle = df.iloc[-1].squeeze()

    # morning close
    df["morning_close"] = df.loc[(df["date"].dt.hour == 1), "close"]
    df["morning_close"] = df["morning_close"].ffill()

    # afternoon open
    df["afternoon_open"] = df.loc[(df["date"].dt.hour == 6), "open"]
    df["afternoon_open"] = df["afternoon_open"].ffill()

    # SMI = SMI[1] - (open - morning_close) + (aftcloseernoon_open - close)
    try:
        last_smi = last_candle["smart_money_index"]
    except KeyError:
        last_smi = 1

    df["smart_money_index"] = 1
    df["smart_money_index"] = (
        last_smi
        - (df["open"] - df["morning_close"])
        + (df["afternoon_open"] - df["close"])
    )

    return df


class Github_ak4noir_freqtrade_strategies__smart_money_strategy__20240407_180126(IStrategy):
    INTERFACE_VERSION = 3

    """
    PASTE OUTPUT FROM HYPEROPT HERE
    """
    # Stoploss:
    stoploss = -0.99  # value loaded from strategy
    """
    END HYPEROPT
    """

    # Sell signal
    use_exit_signal: bool = True
    exit_profit_only: bool = False
    exit_profit_offset: float = 0.01
    ignore_roi_if_entry_signal: bool = True

    # Optimal timeframe for the strategy
    timeframe: str = "5m"

    process_only_new_candles: bool = True
    startup_candle_count: int = 200

    # smart money index
    smart_money_index_fast_ma_buy = IntParameter(10, 100, default=20, space="buy")
    smart_money_index_slow_ma_buy = IntParameter(100, 350, default=150, space="buy")
    smart_money_index_fast_ma_sell = IntParameter(10, 100, default=50, space="sell")
    smart_money_index_slow_ma_sell = IntParameter(100, 300, default=150, space="sell")
    smart_money_exit_trigger = CategoricalParameter(
        ["fast", "slow", "disabled"], default="fast", space="sell"
    )

    entry_guard = CategoricalParameter(["ema", "disabled"], default="ema", space="buy")

    @property
    def plot_config(self):
        return {
            "main_plot": {
                "ema50": {
                    "color": "#26a269",
                },
                "ema50_1h": {
                    "color": "#a51d2d",
                },
            },
            "subplots": {
                "smart_money_index": {
                    "smart_money_index": {"color": "#26a269", "type": "line"},
                    f"smart_money_index_fast_ma_buy_{self.smart_money_index_fast_ma_buy.value}": {
                        "color": "#a51d2d",
                        "type": "line",
                    },
                },
                "smart_money_index_slow": {
                    "smart_money_index_1h": {"color": "#26a269", "type": "line"},
                    f"smart_money_index_slow_ma_buy_{self.smart_money_index_slow_ma_buy.value}_1h": {
                        "color": "#a51d2d",
                        "type": "line",
                    },
                },
            },
        }

    def populate_smart_money_indicators(self, dataframe: DataFrame) -> DataFrame:
        df = dataframe.copy()
        df = smart_money_index(df)

        # see https://www.freqtrade.io/en/stable/strategy-advanced/#performance-warning
        frames = [df]

        for val in self.smart_money_index_slow_ma_buy.range:
            frames.append(
                DataFrame(
                    {
                        f"smart_money_index_slow_ma_buy_{val}": ta.SMA(
                            df["smart_money_index"], val
                        )
                    }
                )
            )

        for val in self.smart_money_index_fast_ma_buy.range:
            frames.append(
                DataFrame(
                    {
                        f"smart_money_index_fast_ma_buy_{val}": ta.SMA(
                            df["smart_money_index"], val
                        )
                    }
                )
            )

        for val in self.smart_money_index_slow_ma_sell.range:
            frames.append(
                DataFrame(
                    {
                        f"smart_money_index_slow_ma_sell_{val}": ta.SMA(
                            df["smart_money_index"], val
                        )
                    }
                )
            )

        for val in self.smart_money_index_fast_ma_sell.range:
            frames.append(
                DataFrame(
                    {
                        f"smart_money_index_fast_ma_sell_{val}": ta.SMA(
                            df["smart_money_index"], val
                        )
                    }
                )
            )

        # combine all dataframes, and reassign the original dataframe column
        return pd.concat(frames, axis=1)

    @informative("1h")
    def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = self.populate_smart_money_indicators(dataframe)
        dataframe["smi"], dataframe["smi_ma"], dataframe["smi_trend"] = smi_trend(
            dataframe
        )
        dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = self.populate_smart_money_indicators(dataframe)
        dataframe["smi"], dataframe["smi_ma"], dataframe["smi_trend"] = smi_trend(
            dataframe
        )
        dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = []
        dataframe.loc[:, "enter_tag"] = ""

        smart_money_cross = (
            qtpylib.crossed_above(
                dataframe["smart_money_index"],
                dataframe[
                    f"smart_money_index_fast_ma_buy_{self.smart_money_index_fast_ma_buy.value}"
                ],
            )
            & (dataframe["volume"] > 0)
            & (dataframe["smi_trend_1h"] != 2)
            & (dataframe["close"] < dataframe["ema50_1h"])
        )
        dataframe.loc[smart_money_cross, "enter_tag"] += "+fast_smart_money_cross"
        conditions.append(smart_money_cross)

        inf_smart_money_cross = (
            qtpylib.crossed_above(
                dataframe["smart_money_index_1h"],
                dataframe[
                    f"smart_money_index_slow_ma_buy_{self.smart_money_index_slow_ma_buy.value}_1h"
                ],
            )
            & (dataframe["volume"] > 0)
            & (dataframe["smi_trend_1h"] != 2)
            & (dataframe["close"] < dataframe["ema50_1h"])
        )
        dataframe.loc[inf_smart_money_cross, "enter_tag"] += "+slow_smart_money_cross"
        conditions.append(inf_smart_money_cross)

        if conditions:
            dataframe.loc[reduce(lambda x, y: x | y, conditions), "enter_long"] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = []
        dataframe.loc[:, "exit_tag"] = ""

        if self.smart_money_exit_trigger.value == "fast":
            smart_money_cross = (
                qtpylib.crossed_below(
                    dataframe["smart_money_index"],
                    dataframe[
                        f"smart_money_index_fast_ma_sell_{self.smart_money_index_fast_ma_sell.value}"
                    ],
                )
                & (dataframe["volume"] > 0)
                & (dataframe["smi_trend_1h"] == 2)
            )
            dataframe.loc[smart_money_cross, "enter_tag"] += "+smart_money_cross"
            conditions.append(smart_money_cross)

        elif self.smart_money_exit_trigger.value == "slow":
            inf_smart_money_cross = (
                qtpylib.crossed_below(
                    dataframe["smart_money_index_1h"],
                    dataframe[
                        f"smart_money_index_slow_ma_sell_{self.smart_money_index_slow_ma_sell.value}_1h"
                    ],
                )
                & (dataframe["volume"] > 0)
                & (dataframe["smi_trend_1h"] == 2)
            )
            dataframe.loc[
                inf_smart_money_cross, "enter_tag"
            ] += "+inf_smart_money_cross"
            conditions.append(inf_smart_money_cross)

        if conditions:
            dataframe.loc[reduce(lambda x, y: x | y, conditions), "exit_long"] = 1

        return dataframe


class Github_ak4noir_freqtrade_strategies__smart_money_strategy__20240407_180126_DCA(SafetyOrderMixin, Github_ak4noir_freqtrade_strategies__smart_money_strategy__20240407_180126):
    pass


class Github_ak4noir_freqtrade_strategies__smart_money_strategy__20240407_180126_DCA_TTP(
    TrailingTakeProfitMixin, SafetyOrderMixin, Github_ak4noir_freqtrade_strategies__smart_money_strategy__20240407_180126
):
    pass
