# source: https://raw.githubusercontent.com/imsatoshi/GeneTrader/7c75f0ab930d79c7e841cf8f65ae42dde66f4dfb/strategy/template.py
from string import Template

strategy_template = Template("""
import json
import logging
import os
import sys
from functools import reduce
from threading import Thread

import numpy
import numpy as np  # noqa
import pandas as pd  # noqa
from pandas import DataFrame, Series
from typing import Optional, Union, List, Tuple

from pandas_ta import stdev

from freqtrade.enums import ExitCheckTuple
from freqtrade.persistence import Trade, Order, CustomDataWrapper
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, IStrategy, IntParameter,
                                informative)
import datetime
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import pandas_ta as pta


class Github_imsatoshi_GeneTrader__template__20240907_134550(IStrategy):
    INTERFACE_VERSION = 3

    minimal_roi = {
        "0": 0.05,
        "30": 0.04,
        "60": 0.03,
        "120": 0.02
    }

    stoploss = -0.15

    timeframe_hierarchy = {
        '1m': '5m',
        '5m': '15m',
        '15m': '1h',
        '1h': '4h',
        '4h': '1d',
        '1d': '1w',
        '1w': '1M'
    }

    order_types = {
        'entry': 'market',
        'exit': 'market',
        'stoploss': 'market',
        'stoploss_on_exchange': False
    }

    use_exit_signal = True
    exit_profit_only = False

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

    dca_attempts = {}
    position_adjustment_enable = True
    candle_open_prices = {}
    last_dca_candle_index = {}

    last_dca_price = {}
    csl = {}
    commands = []
    initial_entry_ratio = DecimalParameter(0.4, 1.0, default=$initial_entry_ratio, space='buy', optimize=True)

    new_sl_coef = DecimalParameter(0.3, 0.9, default=$new_sl_coef, space='sell', optimize=False)

    # TTF
    lookback_length = IntParameter(1, 30, default=$lookback_length, space='buy', optimize=True)
    upper_trigger_level = IntParameter(1, 300, default=$upper_trigger_level, space='buy', optimize=True)
    lower_trigger_level = IntParameter(-300, -1, default=$lower_trigger_level, space='buy', optimize=True)

    # Hyperoptable parameters
    buy_rsi = IntParameter(25, 60, default=$buy_rsi, space='buy', optimize=False)
    sell_rsi = IntParameter(50, 70, default=$sell_rsi, space='sell', optimize=False)

    # ATR based stop loss parameters
    atr_multiplier = DecimalParameter(1.0, 3.0, default=$atr_multiplier, space='stoploss', optimize=False)

    # SWINGS
    swing_window = IntParameter(10, 50, default=$swing_window, space='buy', optimize=False)
    swing_min_periods = IntParameter(1, 10, default=$swing_min_periods, space='buy', optimize=False)
    swing_buffer = DecimalParameter(0.01, 0.1, default=$swing_buffer, space='buy', optimize=False)

    buy_macd = DecimalParameter(-0.02, 0.02, default=$buy_macd, space='buy', optimize=False)
    buy_ema_short = IntParameter(5, 50, default=$buy_ema_short, space='buy', optimize=False)
    buy_ema_long = IntParameter(50, 200, default=$buy_ema_long, space='buy', optimize=False)

    sell_macd = DecimalParameter(-0.02, 0.02, default=$sell_macd, space='sell', optimize=False)
    sell_ema_short = IntParameter(5, 50, default=$sell_ema_short, space='sell', optimize=False)
    sell_ema_long = IntParameter(50, 200, default=$sell_ema_long, space='sell', optimize=False)

    volume_dca_int = IntParameter(1, 30, default=$volume_dca_int, space='buy', optimize=False)
    a_vol_coef = DecimalParameter(1, 2, default=$a_vol_coef, space='buy', optimize=False)
    dca_candles_modulo = IntParameter(1, 100, default=$dca_candles_modulo, space='buy', optimize=True)
    dca_threshold = DecimalParameter(0.01, 0.5, default=$dca_threshold, space='buy', optimize=False)

    dca_multiplier = DecimalParameter(1.0, 2.0, default=$dca_multiplier, space='buy', optimize=True)
    max_dca_orders = IntParameter(1, 5, default=$max_dca_orders, space='buy', optimize=True)
    dca_profit_threshold = DecimalParameter(-0.20, -0.05, default=$dca_profit_threshold, space='buy', optimize=True)

    def __init__(self, config):
        return super().__init__(config)

    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: Optional[float], max_stake: float,
                            entry_tag: Optional[str], side: str,
                            **kwargs) -> float:
        available_balance = self.wallets.get_available_stake_amount()
        max_stake_for_trade = available_balance * self.initial_entry_ratio.value
        stake_amount = min(proposed_stake, max_stake_for_trade)
        return stake_amount

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        macd = ta.MACD(dataframe)
        dataframe['macd'] = macd['macd']
        dataframe['macdsignal'] = macd['macdsignal']
        dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=self.buy_ema_short.value)
        dataframe['ema_long'] = ta.EMA(dataframe, timeperiod=self.buy_ema_long.value)
        dataframe['previous_close'] = dataframe['close'].shift(1)
        dataframe['max_since_buy'] = dataframe['high'].cummax()
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)

        # Calculate Pivot Points and Resistance/Support Levels
        pp, r1, s1 = self.calculate_pivots(dataframe)
        dataframe['pivot_point'] = pp
        dataframe['resistance_1'] = r1
        dataframe['support_1'] = s1

        swing_low, swing_high = self.calculate_swing(dataframe)
        dataframe['swing_low'] = swing_low
        dataframe['swing_high'] = swing_high

        # Add a resistance signal (for example, price approaching or crossing R1)
        dataframe['resistance_signal'] = ((dataframe['close'] > dataframe['resistance_1']) & (
                dataframe['close'] > dataframe['previous_close']))

        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
        dataframe['bb_lowerband'] = bollinger['lower']
        dataframe['bb_middleband'] = bollinger['mid']
        dataframe['bb_upperband'] = bollinger['upper']
        # CustomDataWrapper.set_custom_data(trade_id=40, key='test', value='ahoj')
        # t = CustomDataWrapper.get_custom_data(trade_id=40, key='test')[0].value

        # Calculate highest and lowest
        dataframe['hh'] = dataframe['close'].rolling(window=self.lookback_length.value).max()
        dataframe['ll'] = dataframe['close'].rolling(window=self.lookback_length.value).min()

        # Calculate buy and sell power
        dataframe['buyPower'] = dataframe['hh'] - dataframe['ll'].shift(self.lookback_length.value)
        dataframe['sellPower'] = dataframe['hh'].shift(self.lookback_length.value) - dataframe['ll']

        # Calculate TTF
        dataframe['ttf'] = 200 * (dataframe['buyPower'] - dataframe['sellPower']) / (
                dataframe['buyPower'] + dataframe['sellPower'])

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if len(self.commands) > 0:
            pair = self.commands[-1]['pair']
            if pair == metadata['pair']:
                command = self.commands[-1]['command']
                if command == 'BUY':
                    self.commands = [s for s in self.commands if s['pair'] != pair]
                    dataframe.loc[(dataframe['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'trigger_buy')
                    return dataframe

        # Conditions list can be used to store various buying conditions
        conditions = [
            # Basic condition: MACD crossover and EMA crossover
            (dataframe['macd'] > dataframe['macdsignal']) & (dataframe['ema_short'] > dataframe['ema_long']) |
            (dataframe['resistance_signal']) & (dataframe['volume'] > 0) |
            (dataframe['ttf'] > self.upper_trigger_level.value)
        ]

        # Získání dat vyššího časového rámce pro analýzu na více časových rámcích
        level = self.timeframe_hierarchy[self.timeframe]
        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=level)

        if not informative.empty:
            # Ujistěte se, že 'informative' je zarovnán s hlavním dataframe
            informative = informative.reindex(dataframe.index, method='nearest')
            # Nyní bezpečně porovnávejte uzavírací ceny, protože jsou zarovnané
            conditions.append(dataframe['close'] < informative['close'].shift(1))
        else:
            logging.info(f"No data available for {metadata['pair']} in '{level}' timeframe. Skipping this condition.")

        # Check if all conditions are pandas Series and apply logical AND reduction to get the final condition
        if all(isinstance(cond, pd.Series) for cond in conditions):
            final_condition = np.logical_and.reduce(conditions)
            dataframe.loc[final_condition, ['enter_long', 'enter_tag']] = (1, 'multi_timeframe_cross')
        else:
            logging.error("Not all conditions are pandas Series.")

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if len(self.commands) > 0:
            pair = self.commands[-1]['pair']
            if pair == metadata['pair']:
                command = self.commands[-1]['command']
                if command == 'SELL':
                    self.commands = [s for s in self.commands if s['pair'] != pair]
                    dataframe.loc[(dataframe['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'trigger_sell')
                    return dataframe

        # Příprava podmínek
        conditions = [
            (
                    (dataframe['close'] > dataframe['swing_high']) |
                    (
                            (dataframe['macd'] < dataframe['macdsignal']) &
                            (dataframe['ema_short'] < dataframe['ema_long'])
                    ) |
                    (dataframe['ttf'] < self.lower_trigger_level.value)
            ),
            (dataframe['volume'] > 0)
        ]
        exit_condition = np.logical_and.reduce([cond.values for cond in conditions if isinstance(cond, pd.Series)])
        dataframe.loc[exit_condition, ['exit_long', 'exit_tag']] = (1, 'macd_ema_exit')
        return dataframe

    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:

        profit_ratio = trade.calc_profit_ratio(rate)
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

        if ('macd_ema_exit' in exit_reason) and (profit_ratio >= 0.005):
            # logging.info(f"[CTE] {pair}, Exit reason {exit_reason}, confirmed profit: {profit_ratio}")
            return True

        if (('trailing' in exit_reason) or ('roi' in exit_reason)) and (profit_ratio >= 0.005):
            # logging.info(f"[CTE] {pair}, Exit reason {exit_reason}, confirmed profit: {profit_ratio}")
            return True

        if 'force' in exit_reason or 'trigger' in exit_reason:
            return True
        return False

    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
                    current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
        sl = self.get_mk_sl(trade)
        if sl is not None and current_rate <= sl:
            return f"custom_stop_loss_{sl}"
        return None

    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        informative_pairs = [(pair, timeframe) for pair in pairs for timeframe in self.timeframe_hierarchy.keys()]
        return informative_pairs

    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 = len(filled_entries)
        
        if count_of_entries >= self.max_dca_orders.value:
            return None

        if current_profit <= self.dca_profit_threshold.value:
            stake_amount = trade.stake_amount * self.dca_multiplier.value
            
            stake_amount = min(stake_amount, max_stake)
            
            dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
            last_candle = dataframe.iloc[-1]
            
            if (last_candle['rsi'] < 30 and 
                last_candle['close'] < last_candle['bb_lowerband']):                  
                self.log_dca_event(trade, stake_amount, current_rate, current_profit)
                
                return stake_amount

        return None

    def log_dca_event(self, trade: Trade, stake_amount: float, current_rate: float, current_profit: float):
        # 记录DCA事件
        logging.info(f"DCA triggered for {trade.pair}")
        logging.info(f"Current profit: {current_profit:.2%}")
        logging.info(f"Adding stake amount: {stake_amount}")
        logging.info(f"Current rate: {current_rate}")

    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],
                        side: str, **kwargs) -> bool:
    
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        
        volume_mean = dataframe['volume'].rolling(window=24).mean()
        
        last_candle = dataframe.iloc[-1]
        
        if (last_candle['volume'] > volume_mean.iloc[-1] and
            last_candle['close'] > last_candle['ema_long']):
            return True
        
        return False

    def get_dca_list(self, trade):
        try:
            dcas = CustomDataWrapper.get_custom_data(trade_id=trade.id, key="DCA")[0].value
            return dcas
        except Exception as ex:
            pass
        return []

    def get_mk_sl(self, trade):
        try:
            sl = CustomDataWrapper.get_custom_data(trade_id=trade.id, key="SL")[0].value
            return sl
        except Exception as ex:
            pass
        return trade.stop_loss

    def set_mk_sl(self, trade, current_rate):
        sl = current_rate * self.new_sl_coef.value
        CustomDataWrapper.set_custom_data(trade_id=trade.id, key="SL", value=sl)

    def confirm_dca(self, current_rate, trade):
        dcas = self.get_dca_list(trade)
        dcas.append(current_rate)
        self.set_mk_sl(trade, current_rate)
        CustomDataWrapper.set_custom_data(trade_id=trade.id, key="DCA", value=dcas)

    def calculate_swing(self, dataframe: DataFrame) -> Tuple[Series, Series]:
        swing_low = dataframe['low'].rolling(window=self.swing_window.value, min_periods=self.swing_min_periods.value).min()
        swing_high = dataframe['high'].rolling(window=self.swing_window.value, min_periods=self.swing_min_periods.value).max()
        return swing_low, swing_high

    def calculate_pivots(self, dataframe: DataFrame) -> Tuple[Series, Series, Series]:
        pivot_point = (dataframe['high'] + dataframe['low'] + dataframe['close']) / 3
        resistance_1 = 2 * pivot_point - dataframe['low']
        support_1 = 2 * pivot_point - dataframe['high']
        return pivot_point, resistance_1, support_1
""")

strategy_params = {
    'strategy_name': "GeneTrader",
    'initial_entry_ratio': 0.5,
    'new_sl_coef': 0.6,
    'lookback_length': 15,
    'upper_trigger_level': 150,
    'lower_trigger_level': -150,
    'buy_rsi': 40,
    'sell_rsi': 60,
    'atr_multiplier': 2.0,
    'swing_window': 30,
    'swing_min_periods': 5,
    'swing_buffer': 0.05,
    'buy_macd': 0.0,
    'buy_ema_short': 25,
    'buy_ema_long': 100,
    'sell_macd': 0.0,
    'sell_ema_short': 25,
    'sell_ema_long': 100,
    'volume_dca_int': 15,
    'a_vol_coef': 1.5,
    'dca_candles_modulo': 50,
    'dca_threshold': 0.25,
    'dca_multiplier': 1.5,
    'max_dca_orders': 3,
    'dca_profit_threshold': -0.10,  
}


def render_strategy(params):
    return strategy_template.substitute(params)


if __name__ == "__main__":
    print(render_strategy(strategy_params))