# source: https://raw.githubusercontent.com/werkkrew/freqtrade-strategies/582e9649a88623b6f85f03a63bfa68410cd8ad8a/strategies/archived/Schism-v1.6.py
import numpy as np
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import arrow

from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy import merge_informative_pair
from typing import Dict, List, Optional, Tuple
from pandas import DataFrame, Series
from functools import reduce
from datetime import datetime, timedelta
from freqtrade.persistence import Trade
from technical.indicators import RMI, PMAX
from statistics import mean
from cachetools import TTLCache

import pandas as pd  
pd.options.mode.chained_assignment = None  # default='warn'

"""
TODO: 
    - Continue to hunt for a better all around buy signal.
        - Existing signal to "buy the dip" seems to work well but would it be nice to have an additional
          buy signal for strong upward trends without dips?
        - Prevent buys when potential for strong downward trend and not just a dip?
            - Fibonacci Retracement for resistance/support levels?
            - TD Sequential?
    - Tweak ROI Trend Ride
        - Maybe use free_slots as a factor in how eager we are to sell?
        - Maybe further exploit the ROI table itself using trade data?
    - Tweak sell signal
        - Continue to evaluate good circumstances to bail and sell vs hold
    - Optimize Custom Stop Loss
    - Figure out a way to directly feed a daily hyperopt output into a running strategy and reload it?
    - Per-pair automatic/dynamic ROI based on ADR/ATR or something similar?

NOTES:
    - It is recommended to configure protections as you will use in live and run hyperopt/backtest with
      --enable-protections as this strategy will hit a lot of stoplosses so the stoploss protection is helpful
      to test.
    - Keep in mind the sell signal (dynamic bailout) does not function in backtest and this strategy should be
      validated and tested in dry-run before live. If you do not want to use the sell and only rely on the bits
      of the strategy that can be backtested be sure to turn use_sell_signal = False.
    - Keep in mind that due to the dynamic ROI trend ride this strategy implements that most sells for ROI will
      actually sell for more profit than the ROI table dictates and you can assume that your average profit from 
      ROI based sells will be higher than the backtest shows.
"""

class Github_werkkrew_freqtrade_strategies__Schism_v1_6__20210604_160318(IStrategy):

    timeframe = '5m'
    inf_timeframe = '1h'

    buy_params = {
        'inf-guard': 'upper',
        'inf-pct-adr-bot': 0.08848,
        'inf-pct-adr-top': 0.82507,
        'base-mp': 68,
        'base-rmi-fast': 47,
        'base-rmi-slow': 16,
        'xtra-inf-stake-rmi': 54,
        'xtra-base-fiat-rsi': 69,
        'xtra-base-stake-rsi': 61
    }

    sell_params = {
        'sell-rmi-drop': 0.50
    }

    minimal_roi = {
        "0": 0.05,
        "30": 0.025,
        "120": 0.01,
        "360": 0.01,
        "1440": 0
    }

    dynamic_roi = {
        'dynamic_roi_enabled': True,
        'dynamic_roi_type': 'connect'
    }

    stoploss = -0.30
    use_custom_stoploss = True

    custom_stop = {
        'decay-time': 1080,   # minutes to reach end
        'decay-delay': 0,     # minutes to wait before decay starts
        'decay-start': -0.30, # should be the same as initial stoploss
        'decay-end': -0.03,   # ending value
    }

    # Recommended
    use_sell_signal = True
    sell_profit_only = False
    ignore_roi_if_buy_signal = True

    startup_candle_count: int = 72

    # Strategy Specific Variable Storage
    custom_trade_info = {}
    custom_fiat = "USD"
    custom_current_price_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
    
    """
    Informative Pair Definitions
    """
    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        informative_pairs = [(pair, self.inf_timeframe) for pair in pairs]
        
        # add extra informative pairs if the stake is BTC or ETH
        if self.config['stake_currency'] in ('BTC', 'ETH'):
            for pair in pairs:
                coin, stake = pair.split('/')
                coin_fiat = f"{coin}/{self.custom_fiat}"
                informative_pairs += [(coin_fiat, self.timeframe)]

            stake_fiat = f"{self.config['stake_currency']}/{self.custom_fiat}"
            informative_pairs += [(stake_fiat, self.timeframe)]
            informative_pairs += [(stake_fiat, self.inf_timeframe)]

        return informative_pairs

    """
    Indicator Definitions
    """ 
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        self.custom_trade_info[metadata['pair']] = self.populate_trades(metadata['pair'])
    
        # Base timeframe indicators
        dataframe['rmi-slow'] = RMI(dataframe, length=21, mom=5)
        dataframe['rmi-fast'] = RMI(dataframe, length=8, mom=4)

        # Momentum Pinball: 
        dataframe['roc'] = ta.ROC(dataframe, timeperiod=6)
        dataframe['mp']  = ta.RSI(dataframe['roc'], timeperiod=6)
  
        # RMI Trends and Peaks
        dataframe['rmi-up'] = np.where(dataframe['rmi-slow'] >= dataframe['rmi-slow'].shift(),1,0)      
        dataframe['rmi-dn'] = np.where(dataframe['rmi-slow'] <= dataframe['rmi-slow'].shift(),1,0)      
        dataframe['rmi-up-trend'] = np.where(dataframe['rmi-up'].rolling(3, min_periods=1).sum() >= 2,1,0)      
        dataframe['rmi-dn-trend'] = np.where(dataframe['rmi-dn'].rolling(3, min_periods=1).sum() >= 2,1,0)
        dataframe['rmi-max'] = dataframe['rmi-slow'].rolling(10, min_periods=1).max()

        # See: https://github.com/freqtrade/technical/blob/master/technical/indicators/indicators.py#L1059
        # matype = vidya, src = hl2
        dataframe = PMAX(dataframe, period=10, multiplier=3, length=10, MAtype=5, src=2)

        # Other stake specific informative indicators
        # TODO: Re-evaluate the value of these informative indicators
        if self.config['stake_currency'] in ('BTC', 'ETH'):
            coin, stake = metadata['pair'].split('/')
            fiat = self.custom_fiat
            coin_fiat = f"{coin}/{fiat}"
            stake_fiat = f"{stake}/{fiat}"

            coin_fiat_tf = self.dp.get_pair_dataframe(pair=coin_fiat, timeframe=self.timeframe)
            dataframe[f"{fiat}_rsi"] = ta.RSI(coin_fiat_tf, timeperiod=14)

            stake_fiat_tf = self.dp.get_pair_dataframe(pair=stake_fiat, timeframe=self.timeframe)
            stake_fiat_inf_tf = self.dp.get_pair_dataframe(pair=stake_fiat, timeframe=self.inf_timeframe)

            dataframe[f"{stake}_rsi"] = ta.RSI(stake_fiat_tf, timeperiod=14)
            dataframe[f"{stake}_rmi_{self.inf_timeframe}"] = RMI(stake_fiat_inf_tf, length=21, mom=5)

        # Base pair informative timeframe indicators
        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_timeframe)
        informative['rsi'] = ta.RSI(informative, timeperiod=14)
        
        # Get the "average day range" between the 1d high and 3d low to set up guards
        informative['1d_high'] = informative['close'].rolling(24).max()
        informative['3d_low'] = informative['close'].rolling(72).min()
        informative['adr'] = informative['1d_high'] - informative['3d_low']

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

        dataframe.to_csv('user_data/foo.csv')

        return dataframe

    """
    Buy Signal
    """ 
    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        params = self.get_pair_params(metadata['pair'], 'buy')
        trade_data = self.custom_trade_info[metadata['pair']]
        conditions = []

        """
        The primary "secret sauce" of Schism is to take advantage of the ignore_roi_if_buy_signal setting.
        Ideally, the ROI table is very tight and aggressive allowing for quick exits on ROI without reliance 
        on a sell signal. However, if certain criteria are met for an open trade, we stimulate a sticking buy
        signal on purpose to prevent the bot from selling to the ROI in the midst of an upward trend.

        This is not currently able to be backtested or hyperopted so all settings here must be tested and validated
        in dry-run or live. It is safe to assume that when backtesting, all sells for reason roi will have likely been 
        for higher profits in live trading than in backtest.
        """

        # If active trade, look at trend to persist a buy signal for ignore_roi_if_buy_signal
        if trade_data['active_trade']:
            profit_factor = (1 - (dataframe['rmi-slow'].iloc[-1] / 400))
            rmi_grow = self.linear_growth(30, 70, 180, 720, trade_data['open_minutes'])

            conditions.append(trade_data['current_profit'] > (trade_data['peak_profit'] * profit_factor))
            # TODO: Experiment with replacing or augmenting this with PMAX 
            conditions.append(dataframe['rmi-up-trend'] == 1)
            conditions.append(dataframe['rmi-slow'] >= rmi_grow)
        
        # Standard signals for entering new trades
        else:

            # Guards on informative timeframe
            if params['inf-guard'] == 'upper' or params['inf-guard'] == 'both':
                conditions.append(
                    (dataframe['close'] <= dataframe[f"3d_low_{self.inf_timeframe}"] + 
                    (params['inf-pct-adr-top'] * dataframe[f"adr_{self.inf_timeframe}"]))
                )

            if params['inf-guard'] == 'lower' or params['inf-guard'] == 'both':
                conditions.append(
                    (dataframe['close'] >= dataframe[f"3d_low_{self.inf_timeframe}"] + 
                    (params['inf-pct-adr-bot'] * dataframe[f"adr_{self.inf_timeframe}"]))
                )
   
            # Primary buy signal
            conditions.append(
                (dataframe[f"rsi_{self.inf_timeframe}"] >= params['inf-rsi']) &
                (dataframe['rmi-dn-trend'] == 1) &
                (dataframe['rmi-slow'] >= params['base-rmi-slow']) &
                (dataframe['rmi-fast'] <= params['base-rmi-fast']) &
                (dataframe['mp'] <= params['base-mp'])
            )
            
            # Extra conditions for BTC/ETH stakes on additional informative pairs
            if self.config['stake_currency'] in ('BTC', 'ETH'):
                conditions.append(
                    (dataframe[f"{self.config['stake_currency']}_rsi"] < params['xtra-base-stake-rsi']) | 
                    (dataframe[f"{self.custom_fiat}_rsi"] > params['xtra-base-fiat-rsi'])
                )
                conditions.append(dataframe[f"{self.config['stake_currency']}_rmi_{self.inf_timeframe}"] < params['xtra-inf-stake-rmi'])

        # Applies to active or new trades
        conditions.append(dataframe['volume'].gt(0))

        if conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, conditions),
                'buy'] = 1

        return dataframe

    """
    Sell Signal
    """
    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        params = self.get_pair_params(metadata['pair'], 'sell')
        trade_data = self.custom_trade_info[metadata['pair']]
        conditions = []
        
        """
        Previous Schism sell implementation has been *partially* replaced by custom_stoploss.
        Doing it there makes it able to be backtested more realistically but some of the functionality
        such as the ability to use other trades or the # of slots in our decision need to remain here.

        The current sell is meant to be a pre-stoploss bailout but most sells at a loss
        should happen due to the stoploss in live/dry.

        ** Only part of this signal functions in backtest/hyperopt. Can only be optimized in live/dry.
        ** Results in backtest/hyperopts will sell far more readily than in live due to the profit guards.
        """

        # If we are in an active trade (which is the only way a sell can occur...)
        # This is needed to block backtest/hyperopt from hitting the profit stuff and erroring out.
        if trade_data['active_trade']:  
            # Decay a loss cutoff point where we allow a sell to occur idea is this allows
            # a trade to go into the negative a little bit before we react to sell.
            loss_cutoff = self.linear_growth(-0.03, 0, 0, 240, trade_data['open_minutes'])
            conditions.append((trade_data['current_profit'] < loss_cutoff))

            # Primary sell trigger
            # TODO: Experiment with replacing or augmenting this with PMAX 
            rmi_drop = dataframe['rmi-max'] - (dataframe['rmi-max'] * 0.50)
            conditions.append(
                (dataframe['rmi-dn-trend'] == 1) &
                (qtpylib.crossed_below(dataframe['rmi-slow'], rmi_drop)) &
                (dataframe['volume'].gt(0))
            )
            
            # Examine the state of our other trades and free_slots to inform the decision
            if trade_data['other_trades']:
                if trade_data['free_slots'] > 0:
                    # If the average of all our other trades is below a certain threshold based
                    # on free slots available, hold and wait for market recovery.
                    max_market_down = -0.04 
                    hold_pct = (1/trade_data['free_slots']) * max_market_down
                    conditions.append(trade_data['avg_other_profit'] >= hold_pct)
                else:
                    # If we are out of free slots disregard the above and allow the biggest loser to sell.
                    conditions.append(trade_data['biggest_loser'] == True)
        else:
            # This is an impossible condition but needs to be here for some reason?
            conditions.append(dataframe['volume'].lt(0))

        """
        rmi_drop = dataframe['rmi-max'] - (dataframe['rmi-max'] * params['sell-rmi-drop'])

        conditions.append(
            (dataframe['rmi-dn-trend'] == 1) &
            (qtpylib.crossed_below(dataframe['rmi-slow'], rmi_drop)) &
            (dataframe['volume'].gt(0))
        )
        """
                           
        if conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, conditions),
                'sell'] = 1

        return dataframe

    """
    Custom Stoploss
    """ 
    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float:
        start = self.custom_stop['decay-start']
        end = self.custom_stop['decay-end']
        time = self.custom_stop['decay-time']
        delay = self.custom_stop['decay-delay']

        open_minutes: int = (current_time - trade.open_date).total_seconds() // 60

        decay_stoploss = self.linear_growth(start, end, delay, time, open_minutes)

        min_profit = trade.calc_profit_ratio(trade.min_rate)
        max_profit = trade.calc_profit_ratio(trade.max_rate)
        profit_diff = current_profit - min_profit

        # if trade is positive, immediately move the stoploss up to 1% behind it
        if current_profit > 0:
            return current_profit - 0.01

        # if we might be on a rebound, move the stoploss to the low point or keep it where it was
        if current_profit > min_profit:
            if profit_diff > 0.015:
                return min_profit
            return -1

        # otherwise return the time decaying stoploss
        return decay_stoploss

    """
    Trade Timeout Overrides
    """
    def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
        bid_strategy = self.config.get('bid_strategy', {})
        ob = self.dp.orderbook(pair, 1)
        current_price = ob[f"{bid_strategy['price_side']}s"][0][0]
        if current_price > order['price'] * 1.01:
            return True
        return False

    def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
        ask_strategy = self.config.get('ask_strategy', {})
        ob = self.dp.orderbook(pair, 1)
        current_price = ob[f"{ask_strategy['price_side']}s"][0][0]
        if current_price < order['price'] * 0.99:
            return True
        return False

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, **kwargs) -> bool:
        bid_strategy = self.config.get('bid_strategy', {})
        ob = self.dp.orderbook(pair, 1)
        current_price = ob[f"{bid_strategy['price_side']}s"][0][0]
        if current_price > rate * 1.01:
            return False
        return True

    """
    Freqtrade ROI Override for dynamic ROI functionality
    """
    def min_roi_reached_dynamic(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:

        dynamic_roi = self.dynamic_roi
        minimal_roi = self.minimal_roi

        if not dynamic_roi:
            return None, None

        if 'dynamic_roi_type' in dynamic_roi and dynamic_roi['dynamic_roi_type'] \
           in ['linear', 'exponential', 'connect']:
            roi_type = dynamic_roi['dynamic_roi_type']
            # linear decay: f(t) = start - (rate * t)
            if roi_type == 'linear':
                if 'dynamic_roi_start' in dynamic_roi and 'dynamic_roi_end' in dynamic_roi and \
                   'dynamic_roi_time' in dynamic_roi:
                    start = dynamic_roi['dynamic_roi_start']
                    end = dynamic_roi['dynamic_roi_end']
                    time = dynamic_roi['dynamic_roi_time']
                    rate = (start - end) / time
                    min_roi = max(end, start - (rate * trade_dur))
                else:
                    return None, None
            # exponential decay: f(t) = start * e^(-rate*t)
            elif roi_type == 'exponential':
                if 'dynamic_roi_start' in dynamic_roi and 'dynamic_roi_end' in dynamic_roi and \
                   'dynamic_roi_rate' in dynamic_roi:
                    start = dynamic_roi['dynamic_roi_start']
                    end = dynamic_roi['dynamic_roi_end']
                    rate = dynamic_roi['dynamic_roi_rate']
                    min_roi = max(end, start * np.exp(-rate*trade_dur))
                else:
                    return None, None
            # "connect the dots" between the points on the minima_roi table
            elif roi_type == 'connect':
                if not minimal_roi:
                    return None, None
                # figure out where we are in the defined roi table
                past_roi = list(filter(lambda x: x <= trade_dur, minimal_roi.keys()))
                next_roi = list(filter(lambda x: x > trade_dur, minimal_roi.keys()))
                # if we are past the final point in the table, use that key/vaule pair
                if not past_roi:
                    return None, None
                current_entry = max(past_roi)
                if not next_roi:
                    return current_entry, minimal_roi[current_entry]
                next_entry = min(next_roi)
                # use the slope-intercept formula between the two points
                # y = mx + b
                x1, y1 = current_entry, minimal_roi[current_entry]
                x2, y2 = next_entry, minimal_roi[next_entry]
                m = (y1-y2)/(x1-x2)
                b = (x1*y2 - x2*y1)/(x1-x2)
                min_roi = (m * trade_dur) + b
        else:
            return None, None

        return trade_dur, min_roi

    def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
        trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)

        if self.dynamic_roi and 'dynamic_roi_enabled' in self.dynamic_roi \
           and self.dynamic_roi['dynamic_roi_enabled']:
            _, roi = self.min_roi_reached_dynamic(trade_dur)
        else:
            _, roi = self.min_roi_reached_entry(trade_dur)
        if roi is None:
            return False
        else:
            return current_profit > roi

    """
    Schism Custom Methods
    """
    def populate_trades(self, pair: str) -> dict:
        """
        Query the database and populate the custom_trade_info dict.
        """
        if not pair in self.custom_trade_info:
            self.custom_trade_info[pair] = {}

        trade_data = {}
        trade_data['active_trade'] = trade_data['other_trades'] = trade_data['biggest_loser'] = False

        if self.config['runmode'].value in ('live', 'dry_run'):
            
            active_trade = Trade.get_trades([Trade.pair == pair, Trade.is_open.is_(True),]).all()

            if active_trade:
                current_rate = self.get_current_price(pair, True)
                active_trade[0].adjust_min_max_rates(current_rate)

                present = arrow.utcnow()
                trade_start  = arrow.get(active_trade[0].open_date)
                open_minutes = (present - trade_start).total_seconds() // 60

                trade_data['active_trade']   = True
                trade_data['current_profit'] = active_trade[0].calc_profit_ratio(current_rate)
                trade_data['peak_profit']    = max(0, active_trade[0].calc_profit_ratio(active_trade[0].max_rate))
                trade_data['open_minutes']   : int = open_minutes
                trade_data['open_candles']   : int = (open_minutes // active_trade[0].timeframe) # floor
            else: 
                trade_data['current_profit'] = trade_data['peak_profit']  = 0.0
                trade_data['open_minutes']   = trade_data['open_candles'] = 0

            other_trades = Trade.get_trades([Trade.pair != pair, Trade.is_open.is_(True),]).all()

            if other_trades:
                trade_data['other_trades'] = True
                other_profit = tuple(trade.calc_profit_ratio(self.get_current_price(trade.pair, False)) for trade in other_trades)
                trade_data['avg_other_profit'] = mean(other_profit) 
                if trade_data['current_profit'] < min(other_profit):
                    trade_data['biggest_loser'] = True
            else:
                trade_data['avg_other_profit'] = 0

            open_trades = len(Trade.get_open_trades())
            # this was lazy.
            trade_data['free_slots'] = max(0, self.config['max_open_trades'] - open_trades)

        return trade_data


    def get_current_price(self, pair: str, refresh: bool) -> float:
        """
        Query the exchange for current price information, used in profit calculations.
        Implements a cache to prevent excessive api calls.
        """
        if not refresh:
            rate = self.custom_current_price_cache.get(pair)
            if rate:
                return rate

        ask_strategy = self.config.get('ask_strategy', {})
        if ask_strategy.get('use_order_book', False):
            ob = self.dp.orderbook(pair, 1)
            rate = ob[f"{ask_strategy['price_side']}s"][0][0]
        else:
            ticker = self.dp.ticker(pair)
            rate = ticker['last']

        self.custom_current_price_cache[pair] = rate
        return rate

    def linear_growth(self, start: float, end: float, start_time: int, end_time: int, trade_time: int) -> float:
        """
        Simple linear growth function. Grows from start to end after end_time minutes (starts after start_time minutes)
        """
        time = max(0, trade_time - start_time)
        rate = (end - start) / (end_time - start_time)
        return min(end, start + (rate * time))

    def get_pair_params(self, pair: str, side: str) -> Dict:
        """
        Returns buy/sell params that are specific to pairs or groups of pairs if desired.
        TODO:
            - Make this function more robust so that one does not need to ever edit it.
            - Consider adding stoploss and/or ROI tables to the pair/group specific options
        """
        buy_params = self.buy_params
        sell_params = self.sell_params
  
        ### Stake: USD
        if pair in ('ABC/XYZ', 'DEF/XYZ'):
            buy_params = self.buy_params_GROUP1
            sell_params = self.sell_params_GROUP1
        elif pair in ('QRD/WTF'):
            buy_params = self.buy_params_QRD
            sell_params = self.sell_params_QRD

        if side == 'sell':
            return sell_params

        return buy_params

# Sub-strategy with parameters specific to BTC stake
class Github_werkkrew_freqtrade_strategies__Schism_v1_6__20210604_160318_BTC(Github_werkkrew_freqtrade_strategies__Schism_v1_6__20210604_160318):

    timeframe = '1h'
    inf_timeframe = '4h'

    buy_params = {
        'inf-rsi': 64,
        'mp': 55,
        'rmi-fast': 31,
        'rmi-slow': 16,
        'xinf-stake-rmi': 67,
        'xtf-fiat-rsi': 17,
        'xtf-stake-rsi': 57
    }

    minimal_roi = {
        "0": 0.05,
        "240": 0.025,
        "1440": 0.01,
        "4320": 0
    }

    stoploss = -0.30
    use_custom_stoploss = False

# Sub-strategy with parameters specific to ETH stake
class Github_werkkrew_freqtrade_strategies__Schism_v1_6__20210604_160318_ETH(Github_werkkrew_freqtrade_strategies__Schism_v1_6__20210604_160318):

    timeframe = '1h'
    inf_timeframe = '4h'

    buy_params = {
        'inf-rsi': 13,
        'inf-stake-rmi': 69,
        'mp': 40,
        'rmi-fast': 42,
        'rmi-slow': 17,
        'tf-fiat-rsi': 15,
        'tf-stake-rsi': 92
    }

    minimal_roi = {
        "0": 0.05,
        "240": 0.025,
        "1440": 0.01,
        "4320": 0
    }

    stoploss = -0.30
    use_custom_stoploss = False