# source: https://raw.githubusercontent.com/ntsd/freqtrade-configs/da8948618d8d727c5b41f748bad6936611633e4d/binance/usdt/MyStrategyNew/MyStrategyNew6.py
# github_ntsd_freqtrade_configs__MyStrategyNew6__20220130_211015
# Author: @ntsd (Jirawat Boonkumnerd)
# Github: https://github.com/ntsd
# V2 Update: Add periods for each timeframe
# V3 Update: Add operators
# V6 Update: Optimise by categories
# freqtrade download-data --exchange binance -t 5m --days 500
# freqtrade download-data --exchange binance -t 15m --days 500
# freqtrade download-data --exchange binance -t 30m --days 500
# freqtrade download-data --exchange binance -t 1h --days 500
# freqtrade download-data --exchange binance -t 4h --days 500
# ShortTradeDurHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, OnlyProfitHyperOptLoss
# freqtrade hyperopt --hyperopt-loss OnlyProfitHyperOptLoss --spaces buy sell --timeframe 5m -e 2000 --timerange 20210301-20210813 --strategy github_ntsd_freqtrade_configs__MyStrategyNew6__20220130_211015
# freqtrade backtesting --timeframe 5m --timerange 20200807-20210807 --strategy github_ntsd_freqtrade_configs__MyStrategyNew6__20220130_211015

from typing import Tuple
from freqtrade.strategy import IStrategy, CategoricalParameter, DecimalParameter, IntParameter, merge_informative_pair
from pandas import DataFrame, Series

import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from functools import reduce
import numpy as np
import itertools

########################### Static Parameters ###########################

# Timeframes available for the exchange `Binance`: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
TIMEFRAMES = ('5m', '15m', '30m', '1h', '4h')
BASE_TIMEFRAME = TIMEFRAMES[0]
INFO_TIMEFRAMES = TIMEFRAMES[1:]

PERIODS = (5, 8, 13, 24, 39, 48)  # (5, 6, 8, 9, 13, 18, 24, 31, 39, 48)
MAX_CONDITIONS = 3

########################### Operators ###########################


def greater_operator(dataframe: DataFrame, first_indicator: str, second_indicator: str):
    return (dataframe[first_indicator] >
            dataframe[second_indicator])


OPERATORS = {
    '>': greater_operator
}


def apply_operator(dataframe: DataFrame, first_indicator, second_indicator, operator) -> tuple[Series, DataFrame]:
    condition = OPERATORS[operator](dataframe, first_indicator, second_indicator)
    return condition, dataframe

########################### Condition ###########################


NONE_CONDITION = 'NONE'


def generate_condition_set() -> set:
    """ generate_condition_set wil return possible indicators with operator
        {indicator_with_period}-{operator}-{indicator_with_period}
        None condition mean do nothing
    """
    # indicators_with_period will be format {indicator_name}_{period}_{timeframe}
    indicator_with_periods = set()
    for timeframe in TIMEFRAMES:
        for periods in PERIODS:
            indicator_with_periods.add(f'ema_{periods}_{timeframe}')

    # indicator_with_operators will be format {indicator_with_period}-{operator}-{indicator_with_period}
    possible_condition = set()
    possible_condition.add(f'x-{NONE_CONDITION}-x')
    indicators_permutations = itertools.permutations(indicator_with_periods, 2)
    for first_indicator, second_indicator in indicators_permutations:
        for operator in OPERATORS:
            possible_condition.add(f'{first_indicator}-{operator}-{second_indicator}')

    return possible_condition


def unpack_condition(condition: str) -> tuple[str, str, str]:
    """ Return first_indicator, operator, second_indicator"""
    first_indicator, operator, second_indicator = condition.split('-')
    return first_indicator, operator, second_indicator


condition_set = generate_condition_set()
#print('condition_set legth:', len(condition_set))


class github_ntsd_freqtrade_configs__MyStrategyNew6__20220130_211015(IStrategy):
    # ROI table:
    minimal_roi = {"0": 1}

    # Trailing stop:
    trailing_stop = True
    trailing_stop_positive = 0.15
    trailing_stop_positive_offset = 0.197
    trailing_only_offset_is_reached = True

    # Stoploss
    stoploss = -1

    # Timeframe
    timeframe = BASE_TIMEFRAME

    # Hyperopt parameters
    buy_condition_0 = CategoricalParameter(condition_set, space='buy')
    buy_condition_1 = CategoricalParameter(condition_set, space='buy')
    buy_condition_2 = CategoricalParameter(condition_set, space='buy')

    sell_condition_0 = CategoricalParameter(condition_set, space='sell')
    sell_condition_1 = CategoricalParameter(condition_set, space='sell')
    sell_condition_2 = CategoricalParameter(condition_set, space='sell')

    def apply_indicator(self, dataframe: DataFrame, key: str, period: int):
        if key not in dataframe.keys():
            dataframe[key] = ta.EMA(dataframe, timeperiod=period)

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        assert self.dp, "DataProvider is required for multiple timeframes."

        avalidable_info_timeframes = set()
        avalidable_periods = set()

        run_mode = self.dp.runmode.value
        if run_mode in ('backtest', 'live', 'dry_run'):
            # TODO add only using timeframe and period
            avalidable_info_timeframes = INFO_TIMEFRAMES
            avalidable_periods = PERIODS
        else:
            avalidable_info_timeframes = INFO_TIMEFRAMES
            avalidable_periods = PERIODS

        # apply info timeframe indicator
        for info_timeframe in avalidable_info_timeframes:
            info_dataframe = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=info_timeframe)
            for period in avalidable_periods:
                self.apply_indicator(info_dataframe, f'ema_{period}', period)
            dataframe = merge_informative_pair(dataframe, info_dataframe, self.timeframe, info_timeframe, ffill=True)

        # apply base timeframe indicator
        for period in avalidable_periods:
            self.apply_indicator(dataframe, f'ema_{period}_{BASE_TIMEFRAME}', period)

        return dataframe

    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = list()

        for condition_idx in range(MAX_CONDITIONS):
            condition_name = getattr(self, f'buy_condition_{condition_idx}').value
            first_indicator, operator, second_indicator = unpack_condition(condition_name)
            if operator == NONE_CONDITION:
                continue
            condition, dataframe = apply_operator(dataframe, first_indicator, second_indicator, operator)
            conditions.append(condition)

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

        return dataframe

    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        conditions = list()

        for condition_idx in range(MAX_CONDITIONS):
            condition_name = getattr(self, f'sell_condition_{condition_idx}').value
            first_indicator, operator, second_indicator = unpack_condition(condition_name)
            if operator == NONE_CONDITION:
                continue
            condition, dataframe = apply_operator(dataframe, first_indicator, second_indicator, operator)
            conditions.append(condition)

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

        return dataframe
