# source: https://raw.githubusercontent.com/thaodo2512/lstm_cnn/3ac994859b2fac3d384f05ac9558e53931dc53d9/freqtrade_user_data/strategies/NowoIchimoku5mV2.py
"""Github_thaodo2512_lstm_cnn__NowoIchimoku5mV2__20251004_121653 base strategy (non-FreqAI).

Ported from the public freqtrade_pub_strats repository so it can be bundled
directly with this project without cloning at runtime. The class name and
structure match the original implementation to keep compatibility with
wrappers such as FreqAIGithub_thaodo2512_lstm_cnn__NowoIchimoku5mV2__20251004_121653.

Note: This strategy depends on TA-Lib, qtpylib, technical and finta packages,
which are already included in the Freqtrade GPU image used by docker-compose.
"""

from __future__ import annotations

import math
from datetime import datetime
from typing import Any, Callable

import numpy as np
import pandas as pd
import talib.abstract as ta
from finta import TA
from numpy.typing import ArrayLike
from pandas import DataFrame, Series

import freqtrade.vendor.qtpylib.indicators as qtpylib
import technical.indicators as indicators
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.persistence import Trade
from freqtrade.strategy import DecimalParameter, IStrategy


def wma(series: Series, length: int) -> Series:
    norm = 0
    total = 0

    for i in range(1, length - 1):
        weight = (length - i) * length
        norm += weight
        total += series.shift(i) * weight

    return total / norm if norm else series


def hma(series: Series, length: int) -> Series:
    half = wma(series, math.floor(length / 2))
    full = wma(series, length)
    hull = 2 * half - full
    return wma(hull, math.floor(math.sqrt(length)))


def bollinger_bands(series: Series, moving_average: str = "sma", length: int = 20, mult: float = 2.0) -> DataFrame:
    if moving_average == "sma":
        basis = ta.SMA(series, length)
    elif moving_average == "hma":
        basis = hma(series, length)
    else:
        raise ValueError("moving_average has to be 'sma' or 'hma'")

    dev = mult * ta.STDDEV(series, length)
    return DataFrame({"upper": basis + dev})


class Github_thaodo2512_lstm_cnn__NowoIchimoku5mV2__20251004_121653(IStrategy):
    timeframe = "5m"
    startup_candle_count = 100

    use_exit_signal = False
    use_custom_stoploss = True
    trailing_stop = True

    minimal_roi = {
        "0": 0.427,
        "448": 0.202,
        "1123": 0.089,
        "2355": 0,
    }

    stoploss = -0.345

    plot_config = {
        "main_plot": {
            "upper": {"color": "blue"},
        },
        "subplots": {
            "Buy Allowed": {"buy_allowed": {"color": "blue"}},
            "Should Buy": {"should_buy": {"color": "green"}},
            "Is Cloud Green": {"is_cloud_green": {"color": "black"}},
        },
    }

    srsi_k_min_profit = DecimalParameter(0.01, 0.99, decimals=3, default=0.036, space="sell")
    above_upper_min_profit = DecimalParameter(0.001, 0.5, decimals=3, default=0.011, space="sell")
    limit_factor = DecimalParameter(0.5, 5, decimals=3, default=1.918, space="sell")
    lower_cloud_factor = DecimalParameter(0.5, 1.5, decimals=3, default=0.971, space="sell")
    close_above_shifted_upper_cloud = DecimalParameter(0.5, 2, decimals=3, default=0.603, space="buy")

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> float:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) < 2:
            return -0.99

        last_candle = dataframe.iloc[-1].squeeze()
        previous_candle = dataframe.iloc[-2].squeeze()
        if last_candle is None or previous_candle is None:
            return -0.99

        trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
        trade_candle = dataframe.loc[dataframe["date"] == trade_date]
        if trade_candle.empty:
            return -0.99
        trade_candle = trade_candle.squeeze()

        if last_candle["srsi_k"] > 80 and current_profit > self.srsi_k_min_profit.value:
            return -0.0001

        if (
            previous_candle["close"] < previous_candle["upper"]
            and current_rate > last_candle["upper"]
            and current_profit > self.above_upper_min_profit.value
        ):
            return -0.0001

        limit = trade.open_rate + (
            (trade.open_rate - trade_candle["shifted_lower_cloud"]) * self.limit_factor.value
        )
        if current_rate > limit:
            return -0.0001

        if current_rate < (trade_candle["shifted_lower_cloud"] * self.lower_cloud_factor.value):
            return -0.0001

        return -0.99

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe.copy()

        df["upper"] = bollinger_bands(df["close"], moving_average="hma", length=20, mult=2.5)["upper"]

        ichi = indicators.ichimoku(df)
        df["conversion_line"] = ichi["tenkan_sen"]
        df["base_line"] = ichi["kijun_sen"]
        df["lead_1"] = ichi["leading_senkou_span_a"]
        df["lead_2"] = ichi["leading_senkou_span_b"]
        df["cloud_green"] = ichi["cloud_green"]

        df["upper_cloud"] = df["lead_1"].where(df["lead_1"] > df["lead_2"], df["lead_2"])
        df["lower_cloud"] = df["lead_1"].where(df["lead_1"] < df["lead_2"], df["lead_2"])

        df["shifted_upper_cloud"] = df["upper_cloud"].shift(25)
        df["shifted_lower_cloud"] = df["lower_cloud"].shift(25)

        smooth_k = 3
        smooth_d = 3
        length_rsi = 14
        length_stoch = 14

        df["rsi"] = ta.RSI(df, timeperiod=length_rsi)
        stochrsi = (df["rsi"] - df["rsi"].rolling(length_stoch).min()) / (
            df["rsi"].rolling(length_stoch).max() - df["rsi"].rolling(length_stoch).min()
        )

        df["srsi_k"] = stochrsi.rolling(smooth_k).mean() * 100
        df["srsi_d"] = df["srsi_k"].rolling(smooth_d).mean()

        return df

    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe.copy()
        df["is_cloud_green"] = df["lead_1"] > df["lead_2"]

        double_shifted_upper_cloud = df["upper_cloud"].shift(50)

        close_above_shifted_upper = df["close"] > df["shifted_upper_cloud"] * self.close_above_shifted_upper_cloud.value
        close_above_shifted_lower = df["close"] > df["shifted_lower_cloud"]
        close_above_double_shifted_upper = df["close"] > double_shifted_upper_cloud

        conversion_above_base = df["conversion_line"] > df["base_line"]
        close_above_shifted_conversion = df["close"] > df["conversion_line"].shift(25)

        df["should_buy"] = (
            (df["close"] > df["open"])
            & close_above_shifted_upper
            & close_above_shifted_lower
            & df["is_cloud_green"]
            & conversion_above_base
            & close_above_shifted_conversion
            & close_above_double_shifted_upper
        )

        df["buy"] = False
        df["buy_allowed"] = True

        for row in df.itertuples():
            if row.Index <= 100:
                continue
            df.loc[row.Index, "buy_allowed"] = df.at[row.Index - 1, "buy_allowed"]

            if df.at[row.Index - 1, "buy"]:
                df.loc[row.Index, "buy_allowed"] = False

            if not df.at[row.Index, "is_cloud_green"]:
                df.loc[row.Index, "buy_allowed"] = True

            df.loc[row.Index, "buy"] = df.at[row.Index, "buy_allowed"] & df.at[row.Index, "should_buy"]

        return df

    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        df = dataframe.copy()
        df["sell"] = 0
        return df
