# source: https://raw.githubusercontent.com/BillGatesIII/Vires-in-Numeris/31c8ee6fc813fd76da18f3d3fede129a4c50a165/vin.py
from heapq import nlargest
from itertools import count
from math import ceil
from pandas.core.series import Series
from sqlalchemy import false
from freqtrade.strategy import IStrategy, informative
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.persistence import Trade
import logging
import numpy as np
from pandas import DataFrame, concat
from functools import reduce
from datetime import datetime, timedelta
import locale
locale.setlocale(category=locale.LC_ALL, locale='')
log = logging.getLogger(__name__)

class github_BillGatesIII_Vires_in_Numeris__vin__20220210_164401(IStrategy):
    INTERFACE_VERSION = 2

    def version(self) -> str:
        return 'v4.3.0'

    min_day_listed: int = 5
    top_volume: int = 60
    df_market = DataFrame()
    custom_market_info = {}
    lb_buy_lo = range(24, 49)
    lb_buy_up = range(10, 35)

    minimal_roi = {"0": 100}
    stoploss = -1
    stoploss_on_exchange = False
    trailing_stop = False
    use_custom_stoploss = False
    timeframe = '5m'
    process_only_new_candles = True
    use_sell_signal = True
    sell_profit_only = False
    startup_candle_count: int = 48

    @property
    def protections(self):
        return [
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": 36
            }
        ]

    @informative('1d')
    def populate_indicators_1d(self, df: DataFrame, metadata: dict) -> DataFrame:
        i = self.min_day_listed
        df['candle_count'] = df['volume'].rolling(window=i, min_periods=i).count()
        return df

    def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame:
        green = (df['close'] - df['open']).ge(0)
        bodysize = (df['close'] / df['open']).where(green, df['open'] / df['close'])
        hi_adj = df['close'].where(green, df['open']) + (df['high'] - df['close']).where(green, (df['high'] - df['open'])) / bodysize.pow(0.5)
        lo_adj = df['open'].where(green, df['close']) - (df['open'] - df['low']).where(green, (df['close'] - df['low'])) / bodysize.pow(0.5)
        df['hc2'] = (hi_adj + df['close']) / 2
        df['lc2'] = (lo_adj + df['close']) / 2
        ho2_adj = (hi_adj + df['open']) / 2
        df['hlc3'] = (hi_adj + lo_adj + df['close']) / 3
        df['hc2c'] = df['hc2'] / df['close']
        df['lc2c'] = df['lc2'] / df['close']
        df['ho2o'] = ho2_adj / df['open']
        df_hlc3_ch = df['hlc3'] - df['hlc3'].shift(1)
        s = (1, 2, 3)
        for i in s:
            df['updown'] = np.where(df_hlc3_ch.rolling(window=i, min_periods=i).sum().gt(0), 1, np.where(df_hlc3_ch.rolling(window=i, min_periods=i).sum().lt(0), -1, 0))
            df[f"str_{i}"] = df['updown'].groupby((df['updown'].ne(df['updown'].shift(1))).cumsum()).cumsum()
        df['str_min'] = df[[f"str_{i}" for i in s]].min(axis=1)
        df['str_min_ch'] = df['hlc3'] / df['hlc3'].to_numpy()[df.index.to_numpy() - df['str_min'].abs().to_numpy()]
        df['str_max'] = df[[f"str_{i}" for i in s]].max(axis=1)
        df['str_max_ch'] = df['hlc3'] / df['hlc3'].to_numpy()[df.index.to_numpy() - df['str_max'].abs().to_numpy()]
        df.drop(columns=['updown'], inplace=True)
        for i in {*self.lb_buy_lo, *self.lb_buy_up}:
            df[f"vol_chg_{i}"] = df['volume'] / df['volume'].shift(1).rolling(window=i, min_periods=i).mean()
        df['vol_chg_max'] = df[[f"vol_chg_{i}" for i in {*self.lb_buy_lo, *self.lb_buy_up}]].max(axis=1)
        for i in self.lb_buy_lo:
            # df[f"lc2_pct_{i}"] = df['lc2'].pct_change(i)
            # pct_mean: DataFrame = df[f"lc2_pct_{i}"].rolling(window=i, min_periods=i).mean()
            # pct_std: DataFrame = df[f"lc2_pct_{i}"].rolling(window=i, min_periods=i).std()
            lc2_pct = df['lc2'].pct_change(i)
            pct_mean: DataFrame = lc2_pct.rolling(window=i, min_periods=i).mean()
            pct_std: DataFrame = lc2_pct.rolling(window=i, min_periods=i).std()
            bb_pct_up = pct_mean.add(pct_std.mul(2))
            bb_pct_lo = pct_mean.sub(pct_std.mul(2))
            df[f"bb_w_lo_{i}"] = bb_pct_up.sub(bb_pct_lo)
            # df[f"bb_lo_{i}"] = df[f"lc2_pct_{i}"].sub(bb_pct_lo)
            df[f"bb_lo_{i}"] = lc2_pct.sub(bb_pct_lo)
        for i in self.lb_buy_up:
            # df[f"hc2_pct_{i}"] = df['hc2'].pct_change(i)
            # pct_mean: DataFrame = df[f"hc2_pct_{i}"].rolling(window=i, min_periods=i).mean()
            # pct_std: DataFrame = df[f"hc2_pct_{i}"].rolling(window=i, min_periods=i).std()
            hc2_pct = df['hc2'].pct_change(i)
            pct_mean: DataFrame = hc2_pct.rolling(window=i, min_periods=i).mean()
            pct_std: DataFrame = hc2_pct.rolling(window=i, min_periods=i).std()
            bb_pct_up = pct_mean.add(pct_std.mul(2))
            bb_pct_lo = pct_mean.sub(pct_std.mul(2))
            df[f"bb_w_up_{i}"] = bb_pct_up.sub(bb_pct_lo)
            # df[f"bb_up_{i}"] = df[f"hc2_pct_{i}"].sub(bb_pct_up)
            df[f"bb_up_{i}"] = hc2_pct.sub(bb_pct_up)
        df['bb_w_lo_min'] = df[[f"bb_w_lo_{i}" for i in self.lb_buy_lo]].min(axis=1)
        df['bb_w_up_min'] = df[[f"bb_w_up_{i}" for i in self.lb_buy_up]].min(axis=1)
        df['bb_w_lo_max'] = df[[f"bb_w_lo_{i}" for i in self.lb_buy_lo]].max(axis=1)
        df['bb_w_up_max'] = df[[f"bb_w_up_{i}" for i in self.lb_buy_up]].max(axis=1)
        df['bb_lo_min'] = df[[f"bb_lo_{i}" for i in self.lb_buy_lo]].min(axis=1)
        df['bb_up_min'] = df[[f"bb_up_{i}" for i in self.lb_buy_up]].min(axis=1)
        df['bb_lo_max'] = df[[f"bb_lo_{i}" for i in self.lb_buy_lo]].max(axis=1)
        df['bb_up_max'] = df[[f"bb_up_{i}" for i in self.lb_buy_up]].max(axis=1)
        df['hlc3_vol'] = df['hlc3'] * df['volume']
        df['hlc3_vol_8'] = df['hlc3_vol'].rolling(window=8, min_periods=8).sum()
        df['pct_3'] = df['hlc3'].pct_change(3)
        df['pair'] = metadata['pair']
        self.df_market = concat([self.df_market, df[['date', 'pair', 'hlc3_vol', 'hlc3_vol_8', 'pct_3']]], ignore_index=True, copy=False)
        if len(self.dp.current_whitelist()) == 1:
            f = df['bb_w_lo_min'].notna() & df['vol_chg_max'].ge(1) & df['str_min'].between(-4, -1) & df['str_max'].le(1) & df['lc2c'].le(0.998) & df['bb_lo_max'].lt(0)
            #print(df.loc[f, ['pair', 'date', 'str_min_ch', 'str_max_ch', 'str_1', 'str_2', 'str_3', 'hc2c', 'lc2c', 'bb_w_lo_min', 'bb_w_lo_max', 'bb_lo_min', 'bb_lo_max', 'close', 'volume']].to_string())
            #print(df.loc[f, [f"vol_chg_{i}" for i in self.lb_buy_lo]].to_string())
            #print(df.loc[f, [f"bb_lo_{i}" for i in self.lb_buy_lo]].to_string())
        #     #print(df.loc[:, [f"pct_{i}" for i in self.lb_buy_lo]].to_string())
        #     #print(df.loc[:, [f"bb_w_{i}" for i in self.lb_buy_lo]].to_string())
            f = df['bb_w_up_min'].notna() & df['vol_chg_max'].ge(1) & df['str_min'].ge(1) & df['str_max'].between(1, 4) & df['hc2c'].le(1.002) & df['bb_up_max'].gt(0)
            #print(df.loc[f, ['pair', 'date', 'str_min_ch', 'str_max_ch', 'str_1', 'str_2', 'str_3', 'hc2c', 'lc2c', 'bb_w_up_min', 'bb_w_up_max', 'bb_up_min', 'bb_up_max', 'close', 'volume']].to_string())
            #print(df.loc[f, [f"vol_chg_{i}" for i in self.lb_buy_up]].to_string())
            #print(df.loc[f, [f"bb_up_{i}" for i in self.lb_buy_up]].to_string())
            # #print(df.loc[:, [f"hc2_pct_{i}" for i in self.lb_buy_up]].to_string())
            # #print(df.loc[:, [f"bb_w_up_{i}" for i in self.lb_buy_up]].to_string())
        return df.copy()

    def populate_buy_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        df.loc[:, 'custom_buy_tag'] = ''
        df.loc[:, 'buy_tag_bb_lo'] = ''
        df.loc[:, 'buy_tag_bb_up'] = ''
        for i in self.lb_buy_lo:
            c = i / 300 - 0.01
            buy_bb_lo = [
                df['candle_count_1d'].ge(self.min_day_listed),
                df['str_min'].between(-4, -1),
                df['str_max'].le(1),
                df['str_min_ch'].between(0.95, 0.99),
                df['str_max_ch'].between(0.96, 1.01),
                df['lc2c'].between(0.992, 0.998),
                df['bb_w_lo_min'].ge(0.017),
                df['bb_w_lo_max'].le(0.21),
                df['bb_lo_min'].ge(-0.015),
                df['bb_lo_max'].le(0.055),
                # df[f"lc2_pct_{i}"].between(-c, 0),
                # df[f"bb_w_lo_{i}"].between(0.02, 0.32),
                df[f"vol_chg_{i}"].between(1.2, 7.2), #2.7),
                # df[f"bb_lo_{i}"].shift(1).le(df[f"bb_lo_{i}"]),
                df[f"bb_lo_{i}"].between(-0.004, -0.002)
            ]
            df.loc[reduce(lambda x, y: x & y, buy_bb_lo), 'buy_tag_bb_lo'] += f"{i} "
        for i in self.lb_buy_up:
            c = i / 100 - 0.01
            buy_bb_up = [
                df['candle_count_1d'].ge(self.min_day_listed),
                df['str_min'].ge(1),
                df['str_max'].between(1, 4),
                df['str_min_ch'].between(1.005, 1.015),
                df['str_max_ch'].between(1.005, 1.025),
                df['hc2c'].le(1.002),
                df['bb_w_up_min'].ge(0.01),
                df['bb_w_up_max'].le(0.26),
                df['bb_up_min'].ge(-0.20),
                df['bb_up_max'].le(0.01),
                # df[f"hc2_pct_{i}"].between(-c, c),
                # df[f"bb_w_up_{i}"].between(0.02, 0.32),
                df[f"vol_chg_{i}"].between(1.55, 2.05),
                df[f"bb_up_{i}"].shift(1).lt(0),
                # df[f"bb_up_{i}"].shift(1).le(df[f"bb_up_{i}"]),
                df[f"bb_up_{i}"].between(0.002, 0.008)
            ]
            df.loc[reduce(lambda x, y: x & y, buy_bb_up), 'buy_tag_bb_up'] += f"{i} "
        buy_bb_lo = df['buy_tag_bb_lo'].ne('') & df['buy_tag_bb_up'].eq('')
        buy_bb_up = df['buy_tag_bb_up'].ne('') & df['buy_tag_bb_lo'].eq('')
        df.loc[buy_bb_lo, 'custom_buy_tag'] = 'lo ' + df['buy_tag_bb_lo'].str.strip()
        df.loc[buy_bb_up, 'custom_buy_tag'] = 'up ' + df['buy_tag_bb_up'].str.strip()
        df.loc[df['custom_buy_tag'].ne(''), 'buy'] = True
        df['buy_tag'] = df['custom_buy_tag']
        if len(self.dp.current_whitelist()) <= 6:
            if not df.loc[buy_bb_lo].empty:
                lb = (36, 39, 42, 44) #self.lb_buy_lo
                #print(df.loc[buy_bb_lo, ['pair', 'date', 'str_min_ch', 'str_max_ch', 'str_1', 'str_2', 'str_3', 'hc2c', 'lc2c', 'bb_w_lo_min', 'bb_w_lo_max', 'bb_lo_min', 'bb_lo_max', 'close', 'volume', 'buy_tag']].to_string())
                #print(df.loc[buy_bb_lo, [f"vol_chg_{i}" for i in lb]].to_string())
                # #print(df.loc[buy_bb_lo, [f"lc2_pct_{i}" for i in self.lb_buy_lo]].to_string())
                # #print(df.loc[buy_bb_lo, [f"bb_w_lo_{i}" for i in lb]].to_string())
                #print(df.loc[buy_bb_lo, [f"bb_lo_{i}" for i in lb]].to_string())
            if not df.loc[buy_bb_up].empty:
                lb = self.lb_buy_up
                #print(df.loc[buy_bb_up, ['pair', 'date', 'str_min_ch', 'str_max_ch', 'str_1', 'str_2', 'str_3', 'hc2c', 'lc2c', 'bb_w_up_min', 'bb_w_up_max', 'bb_up_min', 'bb_up_max', 'close', 'volume', 'buy_tag']].to_string())
                # #print(df.loc[buy_bb_up, [f"vol_chg_{i}" for i in lb]].to_string())
                # #print(df.loc[buy_bb_up, [f"hc2_pct_{i}" for i in self.lb_buy_up]].to_string())
                # #print(df.loc[buy_bb_up, [f"bb_w_up_{i}" for i in self.lb_buy_up]].to_string())
                #print(df.loc[buy_bb_up, [f"bb_up_{i}" for i in lb]].to_string())
        return df.copy()

    def populate_sell_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        df.loc[:, ['sell', 'exit_tag']] = (False, None)
        return df

    def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
                    current_profit: float, **kwargs):
        df: DataFrame = self.dp.get_analyzed_dataframe(pair, self.timeframe)[0]
        trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
        df_trade = df.loc[df['date'].ge(trade_open_date)]
        trade_len = len(df_trade)
        candle_1 = df_trade.iloc[-1]
        sell_date = candle_1['date']
        if trade_len <= 2:
            return None
        d = sell_date.strftime('%Y-%m-%d %H:%M')
        u: str = trade.buy_tag[:2]
        cp = (candle_1['close'] - trade.open_rate) / trade.open_rate
        df_m = self.df_market.loc[self.df_market['date'].eq(sell_date)]
        if df_m.empty:
            log.error(f"{sell_date} custom_sell: No volume info for sell candle.")
            return 'sell error'
        candle_2 = df_trade.iloc[-2]
        str_max = candle_1['str_1']
        if candle_1['str_min'] >= 1 and candle_1['hc2c'] >= 1.04 - str_max * 0.0015 and candle_1['volume'] / candle_2['volume'] <= min(1, 0.5 + str_max * 0.05):
            # #print(str_max, candle_1['hc2c'])
            log.info(f"{d} custom_sell: hc2c {u} {cp:.2f} for pair {pair} with trade len {trade_len}.")
            return f"hc2c {u}"
        if candle_1['str_min'] >= -1 or candle_1['str_max'] >= 1:
            return None
        elif candle_1['ho2o'] >= 1.02 and candle_1['volume'] / candle_2['volume'] >= 1:
            log.info(f"{d} custom_sell: ho2o {u} {cp:.2f} for pair {pair} with trade len {trade_len}.")
            return f"ho2o {u}"
        candle_min = df_trade.loc[df_trade['hlc3'] <= 1.001 * df_trade['hlc3'].min()].iloc[-1]
        candle_max = df_trade.loc[df_trade['hlc3'] >= 0.999 * df_trade['hlc3'].max()].iloc[-1]
        rise = candle_max['hlc3'] / candle_min['hlc3'] if candle_max['date'] > candle_min['date'] else 1
        fall = candle_max['hlc3'] / candle_1['hlc3']
        if trade_len <= 9:
            max_fall = 1.04 if u == 'up' else 1.08
        else:
            max_fall = max(1.09, pow(min(1.5, rise), 0.5))
        if fall > max_fall:
            log.info(f"{d} custom_sell: fall {u} {cp:.2f} for pair {pair} with trade len {trade_len}.")
            return f"fall {u}"
        elif fall > 1.02:
            df_top_vol = df_m.nlargest(self.top_volume, 'hlc3_vol_8')
            if len(df_m) >= 5 and self.market_down(df_top_vol):
                log.info(f"{d} custom_sell: market down sell {cp:.2f} for pair {pair} with trade len {trade_len}.")
                return f"market down {u}"
            if trade_len > 54:
                # ma = df_trade['hlc3'].tail(36).pct_change(1).max()
                # mi = df_trade['hlc3'].tail(36).pct_change(1).min()
                sk = df_trade['hlc3'].tail(54).pct_change(1).skew()
                ku = df_trade['hlc3'].tail(54).pct_change(1).kurt()
                # #print(f"{d} rise: {rise} fall:{fall} max fall:{max_fall} pct_max:{ma} pct_min:{mi} skew:{sk} kurt:{ku} cp: {cp:.2f} mm: {ma - mi}")
                if sk <= -0.4 and ku <= -0.4:
                    log.info(f"{d} custom_sell: side {u} {cp:.2f} for pair {pair} with skew {sk:.2f}, kurtosis {ku:.2f} and trade len {trade_len}.")
                    return f"side {u}"
        return None

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                            time_in_force: str, current_time: datetime, **kwargs) -> bool:
        df = self.dp.get_analyzed_dataframe(pair, self.timeframe)[0]
        buy_date = df.iloc[-1]['date']
        buy_tag = df.iloc[-1]['custom_buy_tag']
        u: str = buy_tag[:2]
        d = buy_date.strftime('%Y-%m-%d %H:%M')
        df_m = self.df_market.loc[self.df_market['date'].eq(buy_date)]
        if df_m.empty:
            log.error(f"{buy_date} confirm_trade_entry: No volume info for buy candle.")
            return False
        elif len(df_m) >= 5:
            df_top_vol = df_m.nlargest(self.top_volume, 'hlc3_vol_8') if u == 'lo' else df_m.nlargest(self.top_volume, 'hlc3_vol')
            df_pair = df_top_vol.loc[df_top_vol['pair'] == pair]
            if df_pair.empty:
                # log.info(f"{d} confirm_trade_entry: Cancel buy {u} for pair {pair}. Volume not in top {self.top_volume}.")
                return False
            if buy_date not in self.custom_market_info:
                self.custom_market_info[buy_date] = {}
                self.custom_market_info[buy_date]['lo'] = None
                self.custom_market_info[buy_date]['up'] = None
            b = self.custom_market_info[buy_date][f"{u}"]
            if b == None:
                if self.market_down(df_top_vol):
                    log.info(f"{d} confirm_trade_entry: Cancel buy {u}. Market down.")
                    self.custom_market_info[buy_date][f"{u}"] = False
                    return False
                df_top_vol = df_top_vol.loc[df_m['pair'] != pair]
                count_pairs = len(df_top_vol)
                if u == 'up':
                    n1_up = df_top_vol.loc[df_top_vol['pct_3'] >= 0.001].count()[0]
                    if n1_up <= count_pairs * 0.1:
                        log.info(f"{d} confirm_trade_entry: Cancel buy {u}. Not enough pairs up. There are {n1_up} of the {count_pairs} pairs over 0.1% up in the last candle.")
                        self.custom_market_info[buy_date][f"{u}"] = False
                        return False
                if u == 'lo':
                    n1_lo = df_top_vol.loc[df_top_vol['pct_3'] <= -0.02].count()[0]
                    n1_1_lo = df_top_vol.loc[df_top_vol['pct_3'].shift(1) <= -0.02].count()[0]
                    if (n1_lo >= count_pairs * 0.4) or (n1_1_lo >= count_pairs * 0.4 and n1_lo >= count_pairs * 0.2):
                        log.info(f"{d} confirm_trade_entry: Cancel buy {u}. Too much pairs down. There are {n1_lo} and {n1_1_lo} of the {count_pairs} pairs over 2% down in the last and previous candle.")
                        self.custom_market_info[buy_date][f"{u}"] = False
                        return False
                self.custom_market_info[buy_date][f"{u}"] = True
                log.info(f"{d} confirm_trade_entry: Buy for pair {pair} with buy tag {buy_tag}.")
                return True
            if b:
                log.info(f"{d} confirm_trade_entry: Buy for pair {pair} with buy tag {buy_tag}.")
                return True
            else:
                return False
        log.info(f"{d} confirm_trade_entry: Buy for pair {pair} with buy tag {buy_tag}.")
        return True

    def market_down(self, df: DataFrame) -> bool: # add period parameter
        cnt_pairs = len(df)
        df_neg = df.loc[df['pct_3'] <= -0.01]
        sum_neg = (df_neg['hlc3_vol'] * df_neg['pct_3']).sum()
        cnt_neg = len(df_neg)
        #print(f"sum neg: {sum_neg} cnt_neg: {cnt_neg}")
        if sum_neg < 0 and cnt_neg >= cnt_pairs // 8:
            df_pos = df.loc[df['pct_3'] >= 0.001]
            sum_pos = (df_pos['hlc3_vol'] * df_pos['pct_3']).sum()
            cnt_pos = len(df_pos)
            #print(f"sum pos: {sum_pos} cnt_pos: {cnt_pos}")
            return cnt_pos <= cnt_pairs // 8 and (sum_pos == 0 or sum_neg / sum_pos <= -4)
        else:
            return False

class lo(github_BillGatesIII_Vires_in_Numeris__vin__20220210_164401):
    lb_buy_lo = range(10, 45)
    lb_buy_up = range(1, 1)

    # def market_down(self, df: DataFrame) -> bool:
    #     return False

class up(github_BillGatesIII_Vires_in_Numeris__vin__20220210_164401):
    lb_buy_lo = range(1, 1)
    lb_buy_up = range(9, 49)
