from datetime import datetime
from pandas import DataFrame
from models.PyCryptoBot import PyCryptoBot
from models.AppState import AppState
from models.helper.LogHelper import Logger

class Strategy():
    def __init__(self, app: PyCryptoBot=None, state: AppState=AppState, df: DataFrame=DataFrame, iterations: int=0) -> None:
        if not isinstance(df, DataFrame):
            raise TypeError("'df' not a Pandas dataframe")

        if len(df) == 0:
            raise ValueError("'df' is empty")

        self._action = 'WAIT'
        self.app = app
        self.state = state
        self._df = df
        self._df_last = app.getInterval(df, iterations)

    def isBuySignal(self, now: datetime = datetime.today().strftime('%Y-%m-%d %H:%M:%S'), price: float=0.0) -> bool:
        # required technical indicators or candle sticks for buy signal strategy
        required_indicators = [ 'ema12gtema26co', 'macdgtsignal', 'goldencross', 'obv_pc', 'eri_buy' ]

        for indicator in required_indicators:
            if indicator not in self._df_last:
                raise AttributeError(f"'{indicator}' not in Pandas dataframe")

        # buy signal exclusion (if disabled, do not buy within 3% of the dataframe close high)
        if self.state.last_action == 'SELL' and self.app.disableBuyNearHigh() is True and (price > (self._df['close'].max() * 0.97)):
            log_text = str(now) + ' | ' + self.app.getMarket() + ' | ' + self.app.printGranularity() + ' | Ignoring Buy Signal (price ' + str(price) + ' within 3% of high ' + str(self._df['close'].max()) + ')'
            Logger.warning(log_text)

            return False

        # criteria for a buy signal 1
        if bool(self._df_last['ema12gtema26co'].values[0]) is True \
                and (bool(self._df_last['macdgtsignal'].values[0]) is True or self.app.disableBuyMACD()) \
                and (bool(self._df_last['goldencross'].values[0]) is True or self.app.disableBullOnly()) \
                and (float(self._df_last['obv_pc'].values[0]) > -5 or self.app.disableBuyOBV()) \
                and (bool(self._df_last['eri_buy'].values[0]) is True or self.app.disableBuyElderRay()) \
                and self.state.last_action != 'BUY': # required for all strategies

            Logger.debug('*** Buy Signal ***')
            for indicator in required_indicators:
                Logger.debug(f'{indicator}: {self._df_last[indicator].values[0]}')
            Logger.debug(f'last_action: {self.state.last_action}')

            return True

        # criteria for buy signal 2 (optionally add additional buy singals)
        elif bool(self._df_last['ema12gtema26co'].values[0]) is True \
                and bool(self._df_last['macdgtsignalco'].values[0]) is True \
                and (bool(self._df_last['goldencross'].values[0]) is True or self.app.disableBullOnly()) \
                and (float(self._df_last['obv_pc'].values[0]) > -5 or self.app.disableBuyOBV()) \
                and (bool(self._df_last['eri_buy'].values[0]) is True or self.app.disableBuyElderRay()) \
                and self.state.last_action != 'BUY': # required for all strategies

            Logger.debug('*** Buy Signal ***')
            for indicator in required_indicators:
                Logger.debug(f'{indicator}: {self._df_last[indicator].values[0]}')
            Logger.debug(f'last_action: {self.state.last_action}')

            return True

        return False


    def isSellSignal(self) -> bool:
        # required technical indicators or candle sticks for buy signal strategy
        required_indicators = [ 'ema12ltema26co', 'macdltsignal' ]

        for indicator in required_indicators:
            if indicator not in self._df_last:
                raise AttributeError(f"'{indicator}' not in Pandas dataframe")

        # criteria for a sell signal 1
        if bool(self._df_last['ema12ltema26co'].values[0]) is True \
            and (bool(self._df_last['macdltsignal'].values[0]) is True or self.app.disableBuyMACD()) \
            and self.state.last_action not in ['', 'SELL']:

            Logger.debug('*** Sell Signal ***')
            for indicator in required_indicators:
                Logger.debug(f'{indicator}: {self._df_last[indicator].values[0]}')
            Logger.debug(f'last_action: {self.state.last_action}')

            return True

        return False


    def isSellTrigger(self, price: float=0.0, price_exit: float=0.0, margin: float=0.0, change_pcnt_high: float=0.0, obv_pc: float=0.0, macdltsignal: bool=False) -> bool:
        # loss failsafe sell at fibonacci band
        if self.app.disableFailsafeFibonacciLow() is False and self.app.allowSellAtLoss() and self.app.sellLowerPcnt() is None and self.state.fib_low > 0 and self.state.fib_low >= float(price):
            log_text = '! Loss Failsafe Triggered (Fibonacci Band: ' + str(self.state.fib_low) + ')'
            Logger.warning(log_text)
            self.app.notifyTelegram(self.app.getMarket() + ' (' + self.app.printGranularity() + ') ' + log_text)
            return True

        # loss failsafe sell at trailing_stop_loss
        if self.app.trailingStopLoss() != None and change_pcnt_high < self.app.trailingStopLoss() and (self.app.allowSellAtLoss() or margin > 0):
            log_text = '! Trailing Stop Loss Triggered (< ' + str(self.app.trailingStopLoss()) + '%)'
            Logger.warning(log_text)
            self.app.notifyTelegram(self.app.getMarket() + ' (' + self.app.printGranularity() + ') ' + log_text)
            return True

        # loss failsafe sell at sell_lower_pcnt
        elif self.app.disableFailsafeLowerPcnt() is False and self.app.allowSellAtLoss() and self.app.sellLowerPcnt() != None and margin < self.app.sellLowerPcnt():
            log_text = '! Loss Failsafe Triggered (< ' + str(self.app.sellLowerPcnt()) + '%)'
            Logger.warning(log_text)
            self.app.notifyTelegram(self.app.getMarket() + ' (' + self.app.printGranularity() + ') ' + log_text)
            return True

        # profit bank at sell_upper_pcnt
        if self.app.disableProfitbankUpperPcnt() is False and self.app.sellUpperPcnt() != None and margin > self.app.sellUpperPcnt():
            log_text = '! Profit Bank Triggered (> ' + str(self.app.sellUpperPcnt()) + '%)'
            Logger.warning(log_text)
            self.app.notifyTelegram(self.app.getMarket() + ' (' + self.app.printGranularity() + ') ' + log_text)
            return True

        # profit bank when strong reversal detected
        if self.app.disableProfitbankReversal() is False and margin > 3 and obv_pc < 0 and macdltsignal is True:
            log_text = '! Profit Bank Triggered (Strong Reversal Detected)'
            Logger.warning(log_text)
            self.app.notifyTelegram(self.app.getMarket() + ' (' + self.app.printGranularity() + ') ' + log_text)
            return True

        # profit bank when strong reversal detected
        if self.app.sellAtResistance() is True and margin >= 2 and price > 0 and price != price_exit:
            log_text = '! Profit Bank Triggered (Selling At Resistance)'
            Logger.warning(log_text)
            if not (not self.app.allowSellAtLoss() and margin <= 0):
                self.app.notifyTelegram(self.app.getMarket() + ' (' + self.app.printGranularity() + ') ' + log_text)
            return True

        return False

    def isWaitTrigger(self, margin: float=0.0):
        # configuration specifies to not sell at a loss
        if self.state.action == 'SELL' and not self.app.allowSellAtLoss() and margin <= 0:
            log_text = '! Ignore Sell Signal (No Sell At Loss)'
            Logger.warning(log_text)
            return True

        return False

    def getAction(self):
        if self.isBuySignal():
            return 'BUY'
        elif self.isSellSignal():
            return 'SELL'
        else:
            return 'WAIT'