# source: https://raw.githubusercontent.com/rmallarapu-bc/brahma/9287745fc036c2f4c00c586e598290885c2883b9/archive/SnowMass.py
from freqtrade.persistence import Trade, Order
import logging
import pathlib
import pandas as pd
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
from freqtrade.persistence import Trade, LocalTrade
from datetime import datetime
from typing import Optional
import warnings
import confirmation_signals as cs
import json
import requests
from client_binance import BinanceClient
import talib.abstract as ta
from HoldsCache import HoldsCache
from freqtrade.strategy import stoploss_from_open, merge_informative_pair, DecimalParameter, IntParameter, \
    CategoricalParameter

log = logging.getLogger(__name__)
log.setLevel(logging.ERROR)
warnings.simplefilter(action='ignore', category=pd.errors.PerformanceWarning)


def EWO(dataframe, ema_length=5, ema2_length=35):
    df = dataframe.copy()
    ema1 = ta.EMA(df, timeperiod=ema_length)
    ema2 = ta.EMA(df, timeperiod=ema2_length)
    emadif = (ema1 - ema2) / df['close'] * 100
    return emadif


class Github_rmallarapu_bc_brahma__SnowMass__20240229_213751(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = '15m'
    can_short: bool = False
    minimal_roi = {
        "0": 0.1
    }
    stoploss = -0.72

    # Buy hyperspace params:
    buy_params = {
        "base_nb_candles_buy": 14,
        "ewo_high": 2.327,
        "ewo_low": -19.988,
        "low_offset": 0.975,
        "rsi_buy": 69
    }

    # Sell hyperspace params:
    sell_params = {
        "base_nb_candles_sell": 24,
        "high_offset": 0.991,
        "high_offset_2": 0.997
    }

    binance_client = BinanceClient('BINANCE_FUTURES_USDM')

    headers = {
        'accept': 'application/json',
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZWNoLnJhdmkuZWxlbWVudHMiLCJpYXQiOjE2OTM0ODI3MzMsImV4cCI6MTY5MzQ4MzMzMywic3ViIjoiMiJ9.mJs0xGDh98yl20k7dxbLzFh2xuvDgOQ01SI1ODv8jYA',
    }

    long_tech_indicators_dca = False
    short_tech_indicators_dca = False

    # Protection
    fast_ewo = 50
    slow_ewo = 200
    ewo_low = DecimalParameter(-20.0, -8.0,
                               default=buy_params['ewo_low'], space='buy', optimize=True)
    ewo_high = DecimalParameter(
        2.0, 12.0, default=buy_params['ewo_high'], space='buy', optimize=True)

    # leverage
    leverage_num = 1

    # Trailing stoploss
    trailing_stop = True
    trailing_only_offset_is_reached = True
    trailing_stop_positive = 0.001
    trailing_stop_positive_offset = 0.003

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

    # These values can be overridden in the config.
    use_exit_signal = False
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

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

    # Strategy parameters
    position_adjustment_enable = True
    max_entry_position_adjustment = 10
    max_auto_adjustment = 1
    recurring_profit = 0.025
    recurring_loss = -0.10
    incremental_order_size = 0.5
    initial_order_size = 0.5

    # take profit levels
    take_profit_3 = 0.06
    take_profit_2 = 0.04
    take_profit_1 = 0.02

    # file to hold specific trades
    hold_support_enabled = True
    hold_file = f"snowmass-hold-trades.json"
    hold_trades_cache = None
    target_profit_cache = None

    # Optional order type mapping.
    order_types = {
        'entry': 'limit',
        'exit': 'limit',
        'emergency_exit': 'limit',
        'stoploss': 'limit',
        'stoploss_on_exchange': False
    }

    # Optional order time in force.
    order_time_in_force = {
        'entry': 'GTC',
        'exit': 'GTC'
    }

    unfilledtimeout = {
        'entry': 10,
        'exit': 10
    }

    slippage_protection = {
        'retries': 3,
        'max_slippage': -0.003
    }

    @property
    def protections(self):
        return [
            {
                "method": "CooldownPeriod",
                "stop_duration_candles": 4
            },
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 24,
                "trade_limit": 4,
                "stop_duration_candles": 2,
                "only_per_pair": False
            },

        ]

        # Protection
        # fast_ewo = 50
        # slow_ewo = 200
        # ewo_low = DecimalParameter(-20.0, -8.0,
        # default = buy_params['ewo_low'], space = 'buy', optimize = True)
        # ewo_high = DecimalParameter(
        #     2.0, 12.0, default=buy_params['ewo_high'], space='buy', optimize=True)
        # rsi_buy = IntParameter(30, 70, default=buy_params['rsi_buy'], space='buy', optimize=True)

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

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[(dataframe['volume'] > 0), 'enter_long'] = 0
        dataframe.loc[(dataframe['volume'] > 0), 'enter_short'] = 0
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[(dataframe['volume'] > 0), 'exit_long'] = 0
        dataframe.loc[(dataframe['volume'] > 0), 'exit_short'] = 0
        return dataframe

    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: Optional[float], max_stake: float,
                            leverage: float, entry_tag: Optional[str], side: str,
                            **kwargs) -> float:
        return proposed_stake * self.initial_order_size

    def validate_dca(self, pair):
        long_tech_indicators_dca = short_tech_indicators_dca = False

        # log.info(f"API response: {response}")
        recommendation = list()
        try:
            if self.dp.runmode.value not in ('backtest', 'hyperopt'):
                response = [
                    requests.get(f'http://api.goofy.bcdcpoc.com/api/v3/summary/{pair}/4h', headers=self.headers),
                    requests.get(f'http://api.goofy.bcdcpoc.com/api/v3/summary/{pair}/1h', headers=self.headers),
                    requests.get(f'http://api.goofy.bcdcpoc.com/api/v3/summary/{pair}/30m', headers=self.headers),
                ]
            else:
                response = []

            for r in response:
                # log.info(f"response text: {r.text}")
                recommendation.append(json.loads(r.text)["value"][0]["RECOMMENDATION"])

                # log.info(f"recommendation: {recommendation}")
                if len(set(recommendation)) == 1:
                    if recommendation[0] in ["STRONG_BUY", "BUY"]:
                        long_tech_indicators_dca = True
                    elif recommendation[0] in ["STRONG_SELL", "SELL"]:
                        short_tech_indicators_dca = True

                if len(set(recommendation)) == 2:
                    if recommendation[0] in ["STRONG_BUY", "BUY"] and recommendation[1] in ["STRONG_BUY", "BUY"]:
                        long_tech_indicators_dca = True
                    elif recommendation[0] in ["STRONG_SELL", "SELL"] and recommendation[1] in ["STRONG_SELL",
                                                                                                "SELL"]:
                        short_tech_indicators_dca = True
        except:
            long_tech_indicators_dca = short_tech_indicators_dca = False

        # log.info(f"Pair: {pair}")
        try:
            if self.dp.runmode.value not in ('backtest', 'hyperopt'):
                oi_signal, long_signals, short_signals = cs.oi_sniper(
                    self.binance_client,
                    coin=pair,
                    data_interval=self.timeframe,
                )
            else:
                oi_signal = 0
        except:
            oi_signal = 0

        return oi_signal, long_tech_indicators_dca, short_tech_indicators_dca

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float,
                              min_stake: Optional[float], max_stake: float,
                              current_entry_rate: float, current_exit_rate: float,
                              current_entry_profit: float, current_exit_profit: float,
                              **kwargs) -> Optional[float]:
        filled_entries = trade.select_filled_orders(trade.entry_side)
        count_of_entries = trade.nr_of_successful_entries
        stake_amount = filled_entries[0].cost

        # Obtain pair dataframe (just to show how to access it)
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        current_candle = dataframe.iloc[-1].squeeze()
        pair = str(trade.pair).replace("/", "").split(":")[0]

        # average of four candles
        prev_candle_1 = dataframe.iloc[-2].squeeze()
        prev_candle_2 = dataframe.iloc[-3].squeeze()
        prev_candle_3 = dataframe.iloc[-4].squeeze()

        # for long position, ensure market is going up before DCA
        if (current_candle['close'] > prev_candle_1['close'] and
            current_candle['close'] > prev_candle_2['close'] and
            current_candle['close'] > prev_candle_3['close']) \
                and not trade.is_short \
                and current_profit < (self.recurring_loss * count_of_entries) \
                and count_of_entries <= self.max_auto_adjustment:
            oi_signal, long_tech_indicators_dca, short_tech_indicators_dca = self.validate_dca(pair=pair)
            if oi_signal > 0 and long_tech_indicators_dca:
                new_stake = stake_amount * self.incremental_order_size
            else:
                new_stake = None
            log.info(f"pair = {trade.pair}, filled_entries = {filled_entries}")
            log.info(f"Direction trade.is_short = {trade.is_short}")
            log.info(
                f"current_candle['close'] = {current_candle['close']}, "
                f"prev_candle_1['close'] = {prev_candle_1['close']}, "
                f"prev_candle_2['close'] = {prev_candle_2['close']}, "
                f"prev_candle_3['close'] = {prev_candle_3['close']}")
            log.info(
                f"DCA during loss: pair = {trade.pair}, "
                f"current_profit = {current_profit}, "
                f"open_rate = {trade.open_rate}, current_rate = {current_rate}, "
                f"self.recurring_loss = {self.recurring_loss}, count_of_entries ={count_of_entries}, "
                f"self.max_auto_adjustment = {self.max_auto_adjustment}, "
                f"old stake = {stake_amount}, new stake = {new_stake}")
            return new_stake

        # for short position, ensure market is falling before DCA
        if (current_candle['close'] < prev_candle_1['close'] and
            current_candle['close'] < prev_candle_2['close'] and
            current_candle['close'] < prev_candle_3['close']) \
                and trade.is_short \
                and current_profit < (self.recurring_loss * count_of_entries) \
                and count_of_entries <= self.max_auto_adjustment:
            oi_signal, long_tech_indicators_dca, short_tech_indicators_dca = self.validate_dca(pair=pair)
            if oi_signal < 0 and short_tech_indicators_dca:
                new_stake = stake_amount * self.incremental_order_size
            else:
                new_stake = None
            log.info(f"pair = {trade.pair}, filled_entries = {filled_entries}")
            log.info(f"Direction trade.is_short = {trade.is_short}")
            log.info(
                f"current_candle['close'] = {current_candle['close']}, "
                f"prev_candle_1['close'] = {prev_candle_1['close']}, "
                f"prev_candle_2['close'] = {prev_candle_2['close']}, "
                f"prev_candle_3['close'] = {prev_candle_3['close']}")
            log.info(
                f"DCA during loss: pair = {trade.pair}, "
                f"current_profit = {current_profit}, "
                f"open_rate = {trade.open_rate}, current_rate = {current_rate}, "
                f"self.recurring_loss = {self.recurring_loss}, count_of_entries ={count_of_entries}, "
                f"self.max_auto_adjustment = {self.max_auto_adjustment}, "
                f"old stake = {stake_amount}, new stake = {new_stake}")
            return new_stake

        # DCA during profit -- keep adding more money
        if (current_candle['close'] > prev_candle_1['close'] and
            current_candle['close'] > prev_candle_2['close'] and
            current_candle['close'] > prev_candle_3['close']) \
                and not trade.is_short \
                and current_profit >= self.recurring_profit \
                and count_of_entries <= self.max_auto_adjustment:

            oi_signal, long_tech_indicators_dca, short_tech_indicators_dca = self.validate_dca(pair=pair)
            if oi_signal > 0 and long_tech_indicators_dca:
                new_stake = stake_amount * self.incremental_order_size
            else:
                new_stake = None
            # new_stake = stake_amount * self.incremental_order_size
            log.info(f"DCA on Profit")
            log.info(f"pair = {trade.pair}, filled_entries = {filled_entries}")
            log.info(f"Direction trade.is_short = {trade.is_short}")
            log.info(
                f"current_candle['close'] = {current_candle['close']}, prev_candle_1['close'] = {prev_candle_1['close']}, prev_candle_2['close'] = {prev_candle_2['close']}, prev_candle_3['close'] = {prev_candle_3['close']}")
            log.info(
                f"DCA during profit: pair = {trade.pair}, current_profit = {current_profit}, open_rate = {trade.open_rate}, current_rate = {current_rate}, self.recurring_loss = {self.recurring_loss}, count_of_entries ={count_of_entries}, self.max_auto_adjustment = {self.max_auto_adjustment}, old stake = {stake_amount}, new stake = {new_stake}")
            return new_stake

        # DCA during profit -- keep adding more money
        if (current_candle['close'] < prev_candle_1['close'] and
            current_candle['close'] < prev_candle_2['close'] and
            current_candle['close'] < prev_candle_3['close']) \
                and trade.is_short \
                and current_profit >= self.recurring_profit \
                and count_of_entries <= self.max_auto_adjustment:

            oi_signal, long_tech_indicators_dca, short_tech_indicators_dca = self.validate_dca(pair=pair)
            if oi_signal < 0 and short_tech_indicators_dca:
                new_stake = stake_amount * self.incremental_order_size
            else:
                new_stake = None
            # new_stake = stake_amount * self.incremental_order_size
            log.info(f"DCA on Profit")
            log.info(f"pair = {trade.pair}, filled_entries = {filled_entries}")
            log.info(f"Direction not trade.is_short = {not trade.is_short}")
            log.info(
                f"current_candle['close'] = {current_candle['close']}, prev_candle_1['close'] = {prev_candle_1['close']}, prev_candle_2['close'] = {prev_candle_2['close']}, prev_candle_3['close'] = {prev_candle_3['close']}")
            log.info(
                f"DCA during profit: pair = {trade.pair}, current_profit = {current_profit}, open_rate = {trade.open_rate}, current_rate = {current_rate}, self.recurring_loss = {self.recurring_loss}, count_of_entries ={count_of_entries}, self.max_auto_adjustment = {self.max_auto_adjustment}, old stake = {stake_amount}, new stake = {new_stake}")
            return new_stake

        # take profits
        if current_profit > self.take_profit_3:
            log.info(f"Completed TP3: current_profit = {current_profit}, self.take_profit_3 = {self.take_profit_3} ")
            return -(trade.stake_amount / 1)
        elif current_profit > self.take_profit_2:
            log.info(f"Completed TP2: current_profit = {current_profit}, self.take_profit_3 = {self.take_profit_2} ")
            return -(trade.stake_amount / 2)
        elif current_profit > self.take_profit_1:
            log.info(f"Completed TP1: current_profit = {current_profit}, self.take_profit_3 = {self.take_profit_1} ")
            return -(trade.stake_amount / 3)

        return None

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                            time_in_force: str, current_time: datetime, entry_tag: Optional[str],
                            **kwargs) -> bool:
        # allow force entries
        if entry_tag == 'force_entry':
            return True

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

        if (len(dataframe) < 1):
            return False

        dataframe = dataframe.iloc[-1].squeeze()

        if rate > dataframe['close']:
            slippage = ((rate / dataframe['close']) - 1.0)

            if slippage < abs(self.slippage_protection['max_slippage']):
                return True
            else:
                log.info(f"Cancelling buy for {pair} due to slippage {(slippage * 100.0):.2f}%")
                return False

        return True

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

        if exit_reason == 'force_exit':
            return True

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        candle = dataframe.iloc[-1].squeeze()

        # slippage
        try:
            state = self.slippage_protection['__pair_retries']
        except KeyError:
            state = self.slippage_protection['__pair_retries'] = {}

        slippage = (rate / candle['close']) - 1.0 # (100 / 101) - 1.0 -> (100 / 101) - 1.0
        if (slippage < self.slippage_protection['max_slippage'] and trade.is_short) or \
                (slippage > self.slippage_protection['max_slippage'] and not trade.is_short):
        # if slippage < self.slippage_protection['max_slippage'] or slippage > self.slippage_protection['max_slippage']:
            log.info(f"Slippage Warning: "
                        f"Pair: {trade.pair} "
                        f"Expected Profit: {trade.calc_profit_ratio(rate)} "
                        f"Current close rate:  {candle['close']}, "
                        f"Requested close rate: {rate}, "
                        f"Open rate: {trade.open_rate}, "
                        f"Slippage: {slippage}, "
                        f"Exit_reason: {exit_reason}, "
                        f"Slippage protection: {self.slippage_protection['max_slippage']}, "
                        f"Trade: {trade}")
            pair_retries = state.get(pair, 0)
            if pair_retries < self.slippage_protection['retries']:
                state[pair] = pair_retries + 1
                return False

        # if pair_retries >= self.slippage_protection['retries']:
        #     return True

        state[pair] = 0

        # Allow force exits
        if self._should_hold_trade(trade, rate, exit_reason):
            log.info(f"Holding position: trade: {trade}, rate: {rate}, exit_reason: {exit_reason}")
            return False

        # Disallow automatic stop_loss
        if exit_reason == 'stop_loss':
            log.info(f"Avoiding Stoploss: trade: {trade}, rate: {rate}, exit_reason: {exit_reason}")
            return False

        # Avoid accepting trailing stoploss when it turns negative
        if exit_reason == 'trailing_stop_loss' and trade.calc_profit_ratio(rate) < 0:
            log.info(f"Avoid accepting trailing stop loss turning into a loss: "
                        f"trade: {trade}, "
                        f"profit: {trade.calc_profit_ratio(rate)}, "
                        f"reason: {exit_reason}")
            return False

        return True

    def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
                            current_time: datetime, **kwargs) -> bool:
        ob = self.dp.orderbook(pair, 1)
        current_price = ob['bids'][0][0]
        # Cancel buy order if price is more than 0.2% above the order.
        if current_price > order.price * 1.002:
            return True
        return False

    def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
                           current_time: datetime, **kwargs) -> bool:
        ob = self.dp.orderbook(pair, 1)
        current_price = ob['asks'][0][0]
        # Cancel sell order if price is more than 0.2% below the order.
        if current_price < order.price * 0.998:
            return True
        return False

    def get_hold_trades_config_file(self):
        proper_holds_file_path = self.config["user_data_dir"].resolve() / self.hold_file
        if proper_holds_file_path.is_file():
            return proper_holds_file_path

        strat_file_path = pathlib.Path(__file__)
        # The resolved path does not exist, is it a symlink?
        hold_trades_config_file_absolute = strat_file_path.absolute().parent / self.hold_file
        if hold_trades_config_file_absolute.is_file():
            log.info(
                "Please move %s to %s which is now the expected path for the holds file",
                hold_trades_config_file_absolute,
                proper_holds_file_path,
            )
            return hold_trades_config_file_absolute

    def load_hold_trades_config(self):
        if self.hold_trades_cache is None:
            hold_trades_config_file = self.get_hold_trades_config_file()
            if hold_trades_config_file:
                log.info("Loading hold support data from %s", hold_trades_config_file)
                self.hold_trades_cache = HoldsCache(hold_trades_config_file)

        if self.hold_trades_cache:
            self.hold_trades_cache.load()

    def _should_hold_trade(self, trade: "Trade", rate: float, sell_reason: str) -> bool:
        if self.config['runmode'].value not in ('live', 'dry_run'):
            return False

        if not self.hold_support_enabled:
            return False

        # Just to be sure our hold data is loaded, should be a no-op call after the first bot loop
        self.load_hold_trades_config()

        if not self.hold_trades_cache:
            # Cache hasn't been setup, likely because the corresponding file does not exist, sell
            return False

        if not self.hold_trades_cache.data:
            # We have no pairs we want to hold until profit, sell
            return False

        # By default, no hold should be done
        hold_trade = False

        trade_ids: dict = self.hold_trades_cache.data.get("trade_ids")
        if trade_ids and trade.id in trade_ids:
            trade_profit_ratio = trade_ids[trade.id]
            profit = 0.0
            if (trade.realized_profit != 0.0):
                profit = ((rate - trade.open_rate) / trade.open_rate) * trade.stake_amount * (1 - trade.fee_close)
                profit = profit + trade.realized_profit
                profit = profit / trade.stake_amount
            else:
                profit = trade.calc_profit_ratio(rate)
            current_profit_ratio = profit
            if sell_reason == "force_sell":
                formatted_profit_ratio = f"{trade_profit_ratio * 100}%"
                formatted_current_profit_ratio = f"{current_profit_ratio * 100}%"
                log.info(
                    "Force selling %s even though the current profit of %s < %s",
                    trade, formatted_current_profit_ratio, formatted_profit_ratio
                )
                return False
            elif current_profit_ratio >= trade_profit_ratio:
                # This pair is on the list to hold, and we reached minimum profit, sell
                formatted_profit_ratio = f"{trade_profit_ratio * 100}%"
                formatted_current_profit_ratio = f"{current_profit_ratio * 100}%"
                log.info(
                    "Selling %s because the current profit of %s >= %s",
                    trade, formatted_current_profit_ratio, formatted_profit_ratio
                )
                return False

            # This pair is on the list to hold, and we haven't reached minimum profit, hold
            hold_trade = True

        trade_pairs: dict = self.hold_trades_cache.data.get("trade_pairs")
        if trade_pairs and trade.pair in trade_pairs:
            trade_profit_ratio = trade_pairs[trade.pair]
            profit = 0.0
            if (trade.realized_profit != 0.0):
                profit = ((rate - trade.open_rate) / trade.open_rate) * trade.stake_amount * (1 - trade.fee_close)
                profit = profit + trade.realized_profit
                profit = profit / trade.stake_amount
            else:
                profit = trade.calc_profit_ratio(rate)
            current_profit_ratio = profit
            if sell_reason == "force_sell":
                formatted_profit_ratio = f"{trade_profit_ratio * 100}%"
                formatted_current_profit_ratio = f"{current_profit_ratio * 100}%"
                log.info(
                    "Force selling %s even though the current profit of %s < %s",
                    trade, formatted_current_profit_ratio, formatted_profit_ratio
                )
                return False
            elif current_profit_ratio >= trade_profit_ratio:
                # This pair is on the list to hold, and we reached minimum profit, sell
                formatted_profit_ratio = f"{trade_profit_ratio * 100}%"
                formatted_current_profit_ratio = f"{current_profit_ratio * 100}%"
                log.info(
                    "Selling %s because the current profit of %s >= %s",
                    trade, formatted_current_profit_ratio, formatted_profit_ratio
                )
                return False

            # This pair is on the list to hold, and we haven't reached minimum profit, hold
            hold_trade = True

        return hold_trade
