# source: https://raw.githubusercontent.com/sisxmax/Freqtrade-Strategy-Library/e1315c78f00c4d813217ced1e7bc9f8364d3a06d/MultiWmaMacd.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake13: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple
import logging

logger = logging.getLogger(__name__)

from freqtrade.strategy import (
    IStrategy,
    Trade,
    Order,
    PairLocks,
    informative,  # @informative decorator
    # Hyperopt Parameters
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IntParameter,
    RealParameter,
    # timeframe helpers
    timeframe_to_minutes,
    timeframe_to_next_date,
    timeframe_to_prev_date,
    # Strategy helper functions
    merge_informative_pair,
    stoploss_from_absolute,
    stoploss_from_open,
)

# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import pandas_ta as pta
from technical import qtpylib

# ==========================================
# 13-21-34 WMA & MACD Strategy made 293% Profit! (Full Tutorial)
# YouTube Link: https://youtu.be/Rm8bcBPKoJA
# ==========================================

# ================================
# Freqtrade Version
# ================================

"""
freqtrade -V

Operating System:       Linux-6.10.14-linuxkit-aarch64-with-glibc2.36
Python Version:         Python 3.13.5
CCXT Version:           4.4.96

Freqtrade Version:      freqtrade 2025.7
"""

# ================================
# Download Historical Data
# ================================

"""
freqtrade download-data \
    -c user_data/binance_futures_Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247.json \
    --timerange 20230101- \
    -t 1m 5m 15m 30m 1h 4h 1d
"""

# ================================
# Hyperopt Optimization
# ================================

"""
freqtrade hyperopt \
    --strategy Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247 \
    --config user_data/binance_futures_Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247.json \
    --timeframe 1h \
    --timerange 20240801-20250401 \
    --hyperopt-loss MultiMetricHyperOptLoss \
    --spaces buy\
    -e 50 \
    --j -2 \
    --random-state 9319 \
    --min-trades 30 \
    --max-open-trades 1 \
    -p ICP/USDT:USDT
"""

# ================================
# Backtesting
# ================================

"""
freqtrade backtesting \
    --strategy Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247 \
    --timeframe 1h \
    --timerange 20240801-20250801 \
    --breakdown month \
    -c user_data/binance_futures_Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247.json \
    --max-open-trades 1 \
    --cache none \
    --timeframe-detail 5m \
    -p ICP/USDT:USDT
"""

# ================================
# Start FreqUI Web Interface
# ================================

"""
freqtrade webserver \
    --config user_data/binance_futures_Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247.json
"""


class Github_sisxmax_Freqtrade_Strategy_Library__MultiWmaMacd__20260316_095247(IStrategy):

    # Strategy interface version - allow new iterations of the strategy interface.
    # Check the documentation or the Sample strategy to get the latest version.
    INTERFACE_VERSION = 3

    # Optimal timeframe for the strategy.
    timeframe = "1h"

    # Can this strategy go short?
    can_short: bool = True

    # Minimal ROI designed for the strategy.
    # This attribute will be overridden if the config file contains "minimal_roi".
    minimal_roi = {}
    
    # Optimal stoploss designed for the strategy.
    # This attribute will be overridden if the config file contains "stoploss".
    stoploss = -0.25

    # Trailing stoploss
    trailing_stop = False
    # trailing_only_offset_is_reached = False
    # trailing_stop_positive = 0.01
    # trailing_stop_positive_offset = 0.0  # Disabled / not configured

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

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

    # Number of candles the strategy requires before producing valid signals
    startup_candle_count: int = 200
    
    # Strategy parameters
    wma_rolling_window = CategoricalParameter([4, 6], default=4, space="buy")
    macd_rolling_window = CategoricalParameter([13, 10], default=13, space="buy")
    risk_ratio = CategoricalParameter([2, 2.5, 3], default=2, space="buy")    
    atr_mult = CategoricalParameter([2, 2.5, 3], default=2.5, space="buy")
    
    leverage_level = IntParameter(1, 10, default=1, space="buy", optimize=False, load=False)
        
    @property
    def plot_config(self):
        
        return {
            "main_plot": {
                "wma13": {"color": "red"},
                "wma21": {"color": "blue"},
                "wma34": {"color": "green"},
            },
            "subplots": {
                
                "MACD": {
                    "macd": {"color": "#2962ff", "fill_to": "macdhist"},
                    "macdsignal": {"color": "#ff6d00"},
                    "macdhist": {"type": "bar", "plotly": {"opacity": 0.9}}
                    }
            }
        }

    def informative_pairs(self):
        """
        Define additional, informative pair/interval combinations to be cached from the exchange.
        These pair/interval combinations are non-tradeable, unless they are part
        of the whitelist as well.
        For more information, please consult the documentation
        :return: List of tuples in the format (pair, interval)
            Sample: return [("ETH/USDT", "5m"),
                            ("BTC/USDT", "15m"),
                            ]
        """
        
        return []


    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        
        dataframe["wma13"] = ta.WMA(dataframe, timeperiod=13)
        dataframe["wma21"] = ta.WMA(dataframe, timeperiod=21)
        dataframe["wma34"] = ta.WMA(dataframe, timeperiod=34)
        
        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"] = macd["macdhist"]

        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        
        return dataframe

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

        dataframe.loc[
            (

                (dataframe["wma13"] > dataframe["wma21"]) &
                (dataframe["wma21"] > dataframe["wma34"]) &

                # Check if WMA(13) crossed above WMA(34) in the rolling window
                (dataframe["wma13"].rolling(window=self.wma_rolling_window.value).apply(
                    lambda x: any(qtpylib.crossed_above(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &
                
                # Check if WMA(21) crossed above WMA(34) in the rolling window
                (dataframe["wma21"].rolling(window=self.wma_rolling_window.value).apply(
                    lambda x: any(qtpylib.crossed_above(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &

                (dataframe["macd"] > dataframe["macdsignal"]) &

                # Check if MACD crossed above MACD signal in the rolling window
                (dataframe["macd"].rolling(window=self.macd_rolling_window.value).apply(
                    lambda x: any(qtpylib.crossed_above(x, dataframe["macdsignal"].iloc[x.index[0]:x.index[-1]+1])) 
                )) &

                (dataframe["volume"] > 0)
            ),
            "enter_long"] = 1

        dataframe.loc[
            (

                (dataframe["wma13"] < dataframe["wma21"]) &
                (dataframe["wma21"] < dataframe["wma34"]) &

                # Check if WMA(13) crossed below WMA(21) in the rolling window
                (dataframe["wma13"].rolling(window=self.wma_rolling_window.value).apply(
                    lambda x: any(qtpylib.crossed_below(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &
                
                # Check if WMA(21) crossed below WMA(34) in the rolling window
                (dataframe["wma21"].rolling(window=self.wma_rolling_window.value).apply(
                    lambda x: any(qtpylib.crossed_below(x, dataframe["wma34"].iloc[x.index[0]:x.index[-1]+1])))) &

                (dataframe['macd'] < dataframe['macdsignal']) &
                
                # Check if MACD crossed below MACD signal in the rolling window
                (dataframe['macd'].rolling(window=self.macd_rolling_window.value).apply(
                    lambda x: any(qtpylib.crossed_below(x, dataframe['macdsignal'].iloc[x.index[0]:x.index[-1]+1])) 
                )) &
                
                (dataframe["volume"] > 0)
            ),
            "enter_short"] = 1 

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        
        dataframe.loc[:, "exit_long"] = 0
        dataframe.loc[:, "exit_short"] = 0
        
        return dataframe
    
    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
            current_profit: float, **kwargs):
        
        side = -1 if trade.is_short else 1

        # Retrieve TP and SL from custom data
        take_profit = trade.get_custom_data('take_profit')
        stop_loss = trade.get_custom_data('stop_loss')
        
        # If TP or SL is not set, initialize them
        if take_profit is None or stop_loss is None:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

            # Get the date just before trade opened
            trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)

            # Filter dataframe to candles before the trade opened
            signal_data = dataframe.loc[dataframe["date"] < trade_date]

            if signal_data.empty:
                logger.warning(f"[{pair}] No signal candle found. Skip setting TP/SL.")
                return None

            signal_candle = signal_data.iloc[-1]
            
            # Calculate TP and SL
            atr = signal_candle["atr"]
            close = signal_candle["close"]

            take_profit = close + side * self.atr_mult.value * atr * self.risk_ratio.value
            stop_loss = close - side * self.atr_mult.value * atr
            
            # Save to trade's custom data
            trade.set_custom_data('take_profit', take_profit)
            trade.set_custom_data('stop_loss', stop_loss)

            # logger.info(f"[{pair}] TP/SL set. TP: {take_profit:.2f}, SL: {stop_loss:.2f}")

        # Check exit conditions
        if (trade.is_short and current_rate <= take_profit) or \
        (not trade.is_short and current_rate >= take_profit):
            # logger.info(f"[{pair}] Take Profit hit! Close: {current_close:.2f}, TP: {take_profit:.2f}")
            return "take_profit_achieved"

        if (trade.is_short and current_rate >= stop_loss) or \
        (not trade.is_short and current_rate <= stop_loss):
            # logger.info(f"[{pair}] Stop Loss hit! Close: {current_close:.2f}, SL: {stop_loss:.2f}")
            return "stop_loss_achieved"

        return None
        
    def leverage(self, pair: str, current_time: datetime, current_rate: float,
                 proposed_leverage: float, max_leverage: float, side: str,
                 **kwargs) -> float:

        return self.leverage_level.value