# source: https://raw.githubusercontent.com/TheoBrigitte/freqtrade/82c1c1e1f5f3457fa5376f65124042aff88d7493/strategies/sponsors/zorkv7_0_0.py
from datetime import datetime, timedelta
from typing import Optional, Union
import random

import talib.abstract as ta
import pandas as pd
from pandas import DataFrame
import numpy as np

from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter, CategoricalParameter, merge_informative_pair
from freqtrade.exchange import timeframe_to_prev_date
from technical.util import resample_to_interval, resampled_merge
from technical.indicators import zema, vfi
from sklearn import preprocessing

import freqtrade.vendor.qtpylib.indicators as qtpylib

# Hyperopt instructions
# 0) set ROI to "0": 1, set stoploss to -0.3, and comment out trailing stoploss
# 1) hyperopt on buy and sell spaces only and update params
# 2) hyperopt on ROI only and update table
# 3) hyperopt on stoploss and update
# 4) hyperopt on trailing only if required
#
# author @jimmynixx and his special sauce - inspired by @rextea, rk2 version by @rk, roi and custom_sell from @your_secret_admirer, zema7 exit check from @zx, munged into being by @froggleston with Solipsis inspiration and memey goodness from @werkkrew

class Github_TheoBrigitte_freqtrade__zorkv7_0_0__20241203_205711(IStrategy):
    # ROI table:
    minimal_roi = {
        "0":1
        # "0": 0.078,
        # "40": 0.062,
        # "99": 0.039,
        # "218": 0
    }

    stoploss = -0.1

    # Buy hyperspace params:
    buy_params = {
        'low_offset': 0.964,
        'wma_low_offset': 0.979,
        'ma_len_buy': 51,
        'buy_ma_type': 'zema',
    }

    # Sell hyperspace params:
    # sell_params = {
    #     "max_roi_time": 95,
    #     "max_stoploss_time": 204,
    #     "roi_factor_bear": 1.145,
    #     "roi_factor_bull": 6.21,
    #     "sl_factor_bear": 1.054,
    #     "sl_factor_bull": 9.22,
    #     "high_offset": 1.004,  # value loaded from strategy
    #     "ma_len_sell": 72,  # value loaded from strategy
    #     "msq_normabs_sell": 1.95,  # value loaded from strategy
    #     "sell_ma_type": "zema",  # value loaded from strategy
    #     "trail_amount": 0.006,  # value loaded from strategy
    #     "wma_high_offset": 1.04,  # value loaded from strategy
    # }

    # Sell hyperspace params:
    sell_params = {
        "max_roi_time": 36,
        "max_stoploss_time": 65,
        "roi_factor_bear": 4.045,
        "roi_factor_bull": 4.75,
        "sl_factor_bear": 2.932,
        "sl_factor_bull": 8.648,
        "high_offset": 1.004,  # value loaded from strategy
        "ma_len_sell": 72,  # value loaded from strategy
        "msq_normabs_sell": 1.95,  # value loaded from strategy
        "sell_ma_type": "zema",  # value loaded from strategy
        "trail_amount": 0.006,  # value loaded from strategy
        "wma_high_offset": 1.04,  # value loaded from strategy
    }




    # Trailing stop:
    trailing_stop = False
    trailing_stop_positive = 0.02 # 0.02
    trailing_stop_positive_offset = 0.045 # 0.045
    trailing_only_offset_is_reached = False

    # Custom stoploss
    use_custom_stoploss = False

    use_sell_signal = True
    sell_profit_only = True
    ignore_roi_if_buy_signal = True

    # Run "populate_indicators()" only for new candle.
    process_only_new_candles = False

    # Number of candles the strategy requires before producing valid signals
    startup_candle_count: int = 300

    timeframe = '5m'
    informative_timeframe = '5m'
    pair_informative_timeframe = '1h'
    informative_coin = "ETH"

    high_offset = DecimalParameter(1, 1.20, default=1.004, space='sell', optimize=False)
    wma_high_offset = DecimalParameter(1, 1.20, default=1.01, space='sell', optimize=False)
    low_offset = DecimalParameter(0.80, 0.99, default=0.974, space='buy', optimize=False)
    wma_low_offset = DecimalParameter(0.80, 0.99, default=0.944, space='buy', optimize=False)
    ma_len_buy = IntParameter(30, 90, default=72, space='buy', optimize=False)
    ma_len_sell = IntParameter(30, 90, default=51, space='sell', optimize=False)
    buy_ma_type = CategoricalParameter(['zema', 'wma'], space='buy', default='zema', optimize=False)
    sell_ma_type = CategoricalParameter(['zema', 'wma'], space='sell', default='zema', optimize=False)
    msq_normabs_sell = DecimalParameter(1.0, 4.0, default=2, space='sell', optimize=False)

    roi_factor_bear = DecimalParameter(0.5, 5, default=2, space='sell', optimize=True, load=True)
    roi_factor_bull = DecimalParameter(0.5, 5, default=2, space='sell', optimize=True, load=True)
    sl_factor_bear = DecimalParameter(1, 10, default=3, space='sell', optimize=True, load=True)
    sl_factor_bull = DecimalParameter(1, 10, default=3, space='sell', optimize=True, load=True)
    max_stoploss_time = IntParameter(30, 240, default=100, space='sell', optimize=True, load=True)
    max_roi_time = IntParameter(30, 240, default=100, space='sell', optimize=True, load=True)

    trail_amount = DecimalParameter(0.005, 0.03, default=0.01, space='sell', load=True, optimize=False)

    market_df = None

    plot_config = {
        'main_plot': {
            'ma_buy': {'color': 'green'},
            'ma_sell': {'color': 'red'},
        },
        'subplots': {
        }
    }

    def get_ticker_indicator(self):
        return int(self.timeframe[:-1])

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        informative_pairs = [(pair, self.pair_informative_timeframe) for pair in pairs]
        return informative_pairs

    def inf_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.pair_informative_timeframe)
        informative['ema_50'] = ta.EMA(informative, timeperiod=50)
        informative['ema_200'] = ta.EMA(informative, timeperiod=200)

        return informative

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        pinformative = self.inf_indicators(dataframe, metadata)
        dataframe = merge_informative_pair(dataframe, pinformative, self.timeframe, self.pair_informative_timeframe, ffill=True)

        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        dataframe['zema7'] = zema(dataframe, period=7)
        dataframe['zema100'] = ta.EMA(dataframe, timeperiod=100)
        dataframe['wma100'] = ta.WMA(dataframe, timeperiod=300)

        informative = self.dp.get_pair_dataframe(f'{self.informative_coin}/USDT', self.informative_timeframe)
        informative['sma50'] = ta.SMA(informative, timeperiod=50)
        informative['sma100'] = ta.SMA(informative, timeperiod=100)
        informative['sma200'] = ta.SMA(informative, timeperiod=200)
        informative['rsi'] = ta.RSI(informative, timeperiod=14)

        # Bollinger Bands because obviously
        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(informative), window=20, stds=1)
        informative['bb_lowerband'] = bollinger['lower']
        informative['bb_middleband'] = bollinger['mid']
        informative['bb_upperband'] = bollinger['upper']

        # informative SAR        if roi_decay<0.005: roi_estimate = 0.005
        informative[f'{self.informative_coin}_sar'] = ta.SAR(informative)

        informative[f'{self.informative_coin}_dmi_plus'] = ta.PLUS_DI(informative, timeperiod=14)
        informative[f'{self.informative_coin}_dmi_minus'] = ta.MINUS_DI(informative, timeperiod=14)
        informative[f'{self.informative_coin}_adx'] = ta.ADX(informative, timeperiod=9)

        informative[f'{self.informative_coin}_roc'] = ta.ROC(informative, timeperiod=6)

        dataframe = merge_informative_pair(dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True)

        for length in self.ma_len_buy.range:
            dataframe[f'zema_{length}'] = zema(dataframe, period=length)
            dataframe[f'wma_{length}'] = ta.WMA(dataframe, timeperiod=length)
        for length in self.ma_len_sell.range:
            if f'zema_{length}' not in dataframe:
                dataframe[f'zema_{length}'] = zema(dataframe, period=length)
                dataframe[f'wma_{length}'] = ta.WMA(dataframe, timeperiod=length)

        dataframe['zema_buy'] = dataframe[f'zema_{self.ma_len_buy.value}'] * self.low_offset.value
        dataframe['zema_sell'] = dataframe[f'zema_{self.ma_len_sell.value}'] * self.high_offset.value
        dataframe['wma_buy'] = dataframe[f'wma_{self.ma_len_buy.value}'] * self.wma_low_offset.value
        dataframe['wma_sell'] = dataframe[f'wma_{self.ma_len_sell.value}'] * self.wma_high_offset.value

        msq_normabs = self.MadSqueeze(dataframe, period=14, ref=13, sqzlen=5)
        dataframe['msq_normabs'] = msq_normabs

        dataframe['sar'] = ta.SAR(dataframe)

        dataframe['mfi'] = ta.MFI(dataframe)

        #dataframe_macro = resample_to_interval(dataframe, self.get_ticker_indicator())
        dataframe['high-low'] = dataframe['high'] - dataframe['low']
        dataframe['open-close'] = dataframe['open'] - dataframe['close']
        dataframe['avg_candle_size'] = dataframe['high-low'].rolling(20).mean()

        dataframe['pct_change'] = (dataframe['close'] - dataframe['close'].shift(1)) / dataframe['close']
        #dataframe = resampled_merge(dataframe, dataframe_macro)

        dataframe = self.custom_roi_stoploss_bmsb_ATR(dataframe,12)

        dataframe = self.get_market_trend(dataframe)

        return dataframe

    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        ma_buy = f'{self.buy_ma_type.value}_{self.ma_len_buy.value}'

        outside_range = 4
        dataframe.loc[
            (
                ## JIMMY'S SPECIAL SAUCE DO NOT TOUCH
                (dataframe['ema_50_1h'] > dataframe['ema_200_1h'])
                &
                (dataframe[f'{self.informative_coin}_dmi_minus_5m'] > dataframe[f'{self.informative_coin}_dmi_plus_5m'])
                &
                (dataframe[f'{self.informative_coin}_dmi_minus_5m'] > 25)
                &
                (dataframe[f'{self.informative_coin}_dmi_plus_5m'] < 10)
                &
                (
                    (
                        ((dataframe['open'] < (dataframe[ma_buy] * self.low_offset.value)).rolling(window=outside_range).sum() == outside_range)
                        &
                        ((dataframe['close'] < (dataframe[ma_buy] * self.low_offset.value)).rolling(window=outside_range).sum() == outside_range)
                        &
                        (dataframe['close'] < dataframe[f'{self.buy_ma_type.value}100'])
                    )
                    |
                    (
                        (dataframe['close'] < (dataframe[ma_buy] * self.low_offset.value))
                        &
                        (dataframe['close'] > dataframe[f'{self.buy_ma_type.value}100'])
                    )
                )
                &
                (dataframe['volume'] > 0)
            ),
            'buy'] = 1

        ## comment out to be less strict
        dataframe.loc[
            (
                ## don't buy shitecoins - thanks @rextea!
                (dataframe['up-ratio'] <= 0.15)
                |
                # low activity sideways periods - reduce to allow more buys
                (dataframe['msq_normabs'] < 1.2)
                |
                (
                    # wacky shit happening
                    (dataframe['zema_sell'] > dataframe['wma_sell'])
                    |
                    (dataframe['zema_buy'] > dataframe['wma_buy'])
                )
                &
                (
                    # informative guards (BTC/ETH)
                    (dataframe[f'{self.informative_coin}_roc_5m'] < -5)
                    &
                    (dataframe[f'{self.informative_coin}_roc_5m'] < dataframe[f'{self.informative_coin}_roc_5m'].shift())
                )
            ),'buy'] = 0

        return dataframe

    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        ma_sell = f'{self.sell_ma_type.value}_{self.ma_len_sell.value}'

        #stock zemaoff_rk sell
        dataframe.loc[
            (
                (dataframe['close'] > (dataframe[ma_sell] * self.high_offset.value))
                &
                (dataframe['close'] < dataframe[f'{self.sell_ma_type.value}100'])
                &
                (dataframe['volume'] > 0)

            ),
            'sell'] = 1

        ## comment out to be less strict
        dataframe.loc[
            (
                (dataframe['close'] < dataframe['wma100'])
                |
                (
                    (dataframe['close'] < dataframe['zema_sell'])
                    &
                    (dataframe['close'] < dataframe['sar'])
                )
            ),
            'sell'] = 0

        return dataframe

    ## drafty's custom_sell from golden_corralation
    def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                    current_profit: float, **kwargs):

        dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
        current_candle = dataframe.iloc[-1].squeeze()
        current_profit = trade.calc_profit_ratio(current_candle['close'])

        trade_date = timeframe_to_prev_date("5m",trade.open_date_utc)
        trade_candle = dataframe.loc[(dataframe['date'] == trade_date)]

        if trade_candle.empty: return None
        trade_candle = trade_candle.squeeze()

        open_price = (1 - current_profit) * current_rate
        roi_price =  trade_candle['roi_price']
        roi_estimate = ( roi_price / open_price ) - 1
        if roi_estimate > 0.08: roi_estimate=0.08
        custom_roi = roi_estimate

        stoploss_price =  trade_candle['custom_stop_loss']
        if stoploss_price<0: stoploss_price = -stoploss_price
        stoploss_estimate = 1 - (stoploss_price / open_price)
        if stoploss_estimate>-self.stoploss: stoploss_estimate = -self.stoploss
        stoploss_decay = stoploss_estimate * ( 1 - ((current_time - trade.open_date_utc).seconds) / (self.max_stoploss_time.value*60)) # linear decay
        if (current_profit < -stoploss_decay):
            if trade_candle['resample_{}_bmsb'.format(self.get_ticker_indicator()*12)] == 0:
                return 'custom_bear_stop_loss'
            else: return 'custom_bull_stop_loss'

        roi_decay = roi_estimate * ( 1 - ((current_time - trade.open_date_utc).seconds) / (self.max_roi_time.value*60)) # linear decay
        #roi_decay = roi_estimate * np.exp(-(current_time - trade.open_date_utc).seconds) / (120*60) # exponential decay
        if roi_decay < 0.005: roi_estimate = 0.005
        else: roi_estimate = roi_decay

        if current_profit > roi_estimate:
            if trade_candle['resample_{}_bmsb'.format(self.get_ticker_indicator()*12)] == 0:
                return 'roi_custom_bear'
            else: return 'roi_custom_bull'

    def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
                           rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()
        # if rate < trade.stop_loss: return True
        if (last_candle is not None):
            current_profit = trade.calc_profit_ratio(rate)
            max_profit = max(0, trade.calc_profit_ratio(trade.max_rate))
            pullback_value = max(0, (max_profit - self.trail_amount.value))

            if (sell_reason in ('force_sell', 'sell_signal')) or (sell_reason.endswith('stop_loss')):
                return True

            # Ignore ROI if seems to go up:
            if sell_reason.startswith('roi'):
                if last_candle['buy']: return False

                if (current_profit >= pullback_value):
                    if (last_candle['rsi'] > 40):
                        if (last_candle['close'] > last_candle['wma100']) & (last_candle['close'] > last_candle['zema7']):
                            return False

                        if (last_candle['mfi'] < 70):
                            return False
                return True
            return False
        return True

## comment in for @rextea's random coin purse sampler
#    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
#                            time_in_force: str, current_time: datetime, **kwargs) -> bool:
#        rand = random.randint(1, 10)
#        if rand == 1:
#            return False
#
#        return True

    ##

    def custom_roi_stoploss_bmsb_ATR(self, dataframe, window):

        dataframe_macro = resample_to_interval(dataframe, self.get_ticker_indicator()*window) # 1 hr
        dataframe_macro['ATR'] = ta.ATR(dataframe_macro, timeperiod=14)
        dataframe_macro['20sma'] = ta.SMA(dataframe_macro,timeperiod=20)
        dataframe_macro['21ema'] = ta.EMA(dataframe_macro,timeperiod=21)
        dataframe_macro['bmsb'] = np.where((dataframe_macro['20sma'] > dataframe_macro['21ema']) & (dataframe_macro['close'] > dataframe_macro['20sma']), 1, 0)
        dataframe = resampled_merge(dataframe, dataframe_macro)

        dataframe['custom_stop_loss'] = np.where(dataframe['resample_{}_bmsb'.format(self.get_ticker_indicator()*window)] == 0, dataframe['low'] - dataframe['resample_{}_ATR'.format(self.get_ticker_indicator()*window)] * self.sl_factor_bear.value, dataframe['low'] - dataframe['resample_{}_ATR'.format(self.get_ticker_indicator()*window)] * self.sl_factor_bull.value)
        dataframe['roi_price'] = np.where(dataframe['resample_{}_bmsb'.format(self.get_ticker_indicator()*window)] == 0, dataframe['close'] + dataframe['resample_{}_ATR'.format(self.get_ticker_indicator()*window)] * self.roi_factor_bear.value, dataframe['close'] + dataframe['resample_{}_ATR'.format(self.get_ticker_indicator()*window)] * self.roi_factor_bull.value)

        return dataframe


    def get_market_trend(self, dataframe):
        pairs = self.dp.current_whitelist()
        top20 = pairs[:20]

        if self.market_df is None:
            self.market_df = dataframe.copy()[['date']]
            self.market_df['up'] = 0
            for pair in top20:
                df = self.dp.get_pair_dataframe(pair=pair, timeframe=self.informative_timeframe)
                df['up'] = np.where(df['close'] > df['close'].shift(12), 1, 0)
                self.market_df['up'] = self.market_df['up'].add(df['up'])

        self.market_df['up-ratio'] = self.market_df['up'] / len(top20)
        dataframe = pd.merge(dataframe, self.market_df[['date', 'up', 'up-ratio']], on='date', how='left')
        return dataframe

    def SSLChannels_ATR(self, dataframe, length=7):
        """
        SSL Channels with ATR: https://www.tradingview.com/script/SKHqWzql-SSL-ATR-channel/
        Credit to @JimmyNixx for python
        """
        df = dataframe.copy()

        df['ATR'] = ta.ATR(df, timeperiod=14)
        df['smaHigh'] = df['high'].rolling(length).mean() + df['ATR']
        df['smaLow'] = df['low'].rolling(length).mean() - df['ATR']
        df['hlv'] = np.where(df['close'] > df['smaHigh'], 1, np.where(df['close'] < df['smaLow'], -1, np.NAN))
        df['hlv'] = df['hlv'].ffill()
        df['sslDown'] = np.where(df['hlv'] < 0, df['smaHigh'], df['smaLow'])
        df['sslUp'] = np.where(df['hlv'] < 0, df['smaLow'], df['smaHigh'])

        return df['sslDown'], df['sslUp']

    def SROC(self, dataframe, roclen=21, emalen=13, smooth=21):
        df = dataframe.copy()

        roc = ta.ROC(df, timeperiod=roclen)
        ema = ta.EMA(df, timeperiod=emalen)
        sroc = ta.ROC(ema, timeperiod=smooth)

        return sroc

    ## based on https://www.tradingview.com/script/9bUUSzM3-Madrid-Trend-Squeeze/
    def MadSqueeze(self, dataframe, period=34, ref=13, sqzlen=5):
        df = dataframe.copy()

        # min period force
        if period < 14:
            period = 14

        ma = ta.EMA(df['close'], period)

        closema = df['close'] - ma
        df['msq_closema'] = closema

        refma = ta.EMA(df['close'], ref) - ma
        df['msq_refma'] = refma

        sqzma = ta.EMA(df['close'], sqzlen) - ma
        df['msq_sqzma'] = sqzma

        ## Apply a non-parametric transformation to map the Madrid Trend Squeeze data to a Gaussian.
        ## We do this to even out the peaks across the dataframe, and end up with a normally distributed measure of the variance
        ## between ma, the reference EMA and the squeezed EMA
        ## The bigger the number, the bigger the peak detected. Buys and sells tend to happen at the peaks.

        # max samples 100000
        n_quants = 100000 if df.shape[0] >= 100000 else df.shape[0]-1

        quantt = preprocessing.QuantileTransformer(output_distribution='normal', random_state=0, n_quantiles=n_quants)
        df['msq_abs'] = (df['msq_closema'].fillna(0).abs() + df['msq_refma'].fillna(0).abs() + df['msq_sqzma'].fillna(0).abs())
        df['msq_normabs'] = quantt.fit_transform(df[['msq_abs']])

        return df['msq_normabs']
