# source: https://raw.githubusercontent.com/h5un/freqtrade-strategies/ff7e2afe6a186f30bfee36ae87428fe8e11a8346/user_data/strategies/Pull_Back_Strategy.py
# pragma: no cover
from typing import Dict, Tuple
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame

from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, RealParameter
from freqtrade.persistence import Trade

import talib


class Github_h5un_freqtrade_strategies__Pull_Back_Strategy__20260112_111946(IStrategy):
    """
    多空皆可的 Pull back (1h + 4h)策略：
      Long 進場（下一根）需同時滿足：
        1) 4h RSI > threshold
        2) 1h ADX > threshold
        3) 4h ADX > threshold
        4) 1h 收盤 < BB 下軌
      Short 進場（下一根）需同時滿足：
        1) 4h RSI < threshold
        2) 1h ADX > threshold
        3) 4h ADX > threshold
        4) 1h 收盤 > BB 上軌

      出場：
        Long:1h 收盤 > BB 上軌
        Short:1h 收盤 < BB 下軌

      自訂停損:以「訊號K」的價格與 ATR 決定，並在開倉後固定（不追蹤）。
      倉位 sizing:根據 ATR 停損距離與單筆風險比例(risk_per_trade)動態計算。
    """

    # --- 基本設定 ---
    timeframe = "1h"
    informative_timeframe = "4h"
    can_short = True

    minimal_roi = {"0": 1.0}   # 不使用 ROI 強制出場（全靠 exit_signal / stoploss）
    stoploss = -0.99           # 全域保險底線，實際用 custom_stoploss
    use_custom_stoploss = True
    process_only_new_candles = True
    startup_candle_count = 200

    # --- 超參數（可 hyperopt）---
    bb_period = IntParameter(10, 60, default=20, space="buy", optimize=True)
    bb_stds   = RealParameter(1.0, 3.5, step=0.1, default=2.0, space="buy", optimize=True)

    htf_rsi_threshold = IntParameter(30, 70, default=45, space="buy", optimize=True)   # 4h RSI 閾值
    htf_adx_threshold = IntParameter(15, 40, default=25, space="buy", optimize=True)   # 4h ADX 閾值
    adx_threshold     = IntParameter(10, 40, default=20, space="buy", optimize=True)   # 1h ADX 閾值

    atr_mult = RealParameter(2.0, 6.0, default=4.5, step=0.1, space="sell", optimize=True)
    
    # risk_per_trade_hp = RealParameter(0.002, 0.02, default=0.01, step=0.005, space="sell", optimize=True)

    # 固定視窗
    adx_window = 14
    atr_window = 14

    # 風險參數（單筆最大風險佔用戶總資金比例）
    risk_per_trade: float = 0.01

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 用來存每筆 trade 的固定停損比例
        self._sl_cache = {}

    # --------- 工具函式（計算指標）---------
    @staticmethod
    def _add_bbands(df: DataFrame, period: int, stdevs: float, prefix: str = "") -> DataFrame:
        upper, middle, lower = talib.BBANDS(
            df["close"], timeperiod=period, nbdevup=stdevs, nbdevdn=stdevs, matype=0
        )
        df[f"{prefix}bb_up"]  = upper
        df[f"{prefix}bb_mid"] = middle
        df[f"{prefix}bb_low"] = lower
        return df

    @staticmethod
    def _add_adx(df: DataFrame, window: int, prefix: str = "") -> DataFrame:
        df[f"{prefix}adx"] = talib.ADX(df["high"], df["low"], df["close"], timeperiod=window)
        return df

    @staticmethod
    def _add_atr(df: DataFrame, window: int, prefix: str = "") -> DataFrame:
        df[f"{prefix}atr"] = talib.ATR(df["high"], df["low"], df["close"], timeperiod=window)
        return df

    @staticmethod
    def _add_rsi(df: DataFrame, window: int, colname: str = "rsi") -> DataFrame:
        df[colname] = talib.RSI(df["close"], timeperiod=window)
        return df

    # --------- 將 4h 指標合併到 1h ---------
    @staticmethod
    def _merge_4h_into_1h(df_1h: DataFrame, df_4h: DataFrame) -> DataFrame:
        """
        將 4h 的 rsi_4h / adx_4h 透過時間對齊 forward-fill 進 1h dataframe。
        支援 'date' 欄或 datetime index 兩種格式。
        """
        inf = df_4h.copy()
        if "date" not in inf.columns:
            inf["date"] = inf.index

        inf_small = inf[["date", "rsi_4h", "adx_4h"]].dropna(how="all")

        if "date" in df_1h.columns:
            one_sorted = df_1h.sort_values(by="date")
            inf_sorted = inf_small.sort_values(by="date")
            merged = pd.merge_asof(one_sorted, inf_sorted, on="date", direction="backward")
            merged = merged.ffill().sort_index()
            return merged
        else:
            out = df_1h.sort_index().copy()
            inf2 = inf_small.set_index("date").sort_index()
            out[["rsi_4h", "adx_4h"]] = inf2[["rsi_4h", "adx_4h"]].reindex(out.index, method="ffill")
            return out

    def informative_pairs(self):
        return [(pair, self.informative_timeframe) for pair in self.dp.current_whitelist()]

    def merge_informative(self, df: DataFrame, metadata: Dict) -> DataFrame:
        pair = metadata["pair"]
        inf = self.dp.get_pair_dataframe(pair=pair, timeframe=self.informative_timeframe)

        # 計算 4h 指標
        inf = self._add_rsi(inf, 14, colname="rsi_4h")
        inf = self._add_adx(inf, self.adx_window, prefix="")
        inf.rename(columns={"adx": "adx_4h"}, inplace=True)

        # 合併到 1h
        df = self._merge_4h_into_1h(df, inf)
        return df

    # --------- 指標計算 ---------
    def populate_indicators(self, df: DataFrame, metadata: Dict) -> DataFrame:
        df = self.merge_informative(df, metadata)

        df = self._add_bbands(df, period=int(self.bb_period.value), stdevs=float(self.bb_stds.value))
        df = self._add_adx(df, window=self.adx_window)
        df = self._add_atr(df, window=self.atr_window)

        df["volume_ok"] = df["volume"] > 0
        return df

    # --------- 進出場條件 ---------
    def _long_entry_condition(self, df: DataFrame) -> pd.Series:
        return (
            (df["rsi_4h"] > int(self.htf_rsi_threshold.value)) &
            (df["adx"]    > int(self.adx_threshold.value)) &
            (df["adx_4h"] > int(self.htf_adx_threshold.value)) &
            (df["close"]  < df["bb_low"]) &
            (df["volume_ok"])
        )

    def _short_entry_condition(self, df: DataFrame) -> pd.Series:
        return (
            (df["rsi_4h"] < int(self.htf_rsi_threshold.value)) &
            (df["adx"]    > int(self.adx_threshold.value)) &
            (df["adx_4h"] > int(self.htf_adx_threshold.value)) &
            (df["close"]  > df["bb_up"]) &
            (df["volume_ok"])
        )

    def populate_entry_trend(self, df: DataFrame, metadata: Dict) -> DataFrame:
        df.loc[self._long_entry_condition(df),  ["enter_long",  "enter_tag"]] = (1, "rsi4h_adx_bb_long")
        df.loc[self._short_entry_condition(df), ["enter_short", "enter_tag"]] = (1, "rsi4h_adx_bb_short")
        return df

    def populate_exit_trend(self, df: DataFrame, metadata: Dict) -> DataFrame:
        df.loc[(df["close"] > df["bb_up"]), "exit_long"] = 1
        df.loc[(df["close"] < df["bb_low"]), "exit_short"] = 1
        return df

    # ===============================
    # 倉位計算小工具（根據停損距離）
    # ===============================
    def position_size(self, balance: float, entry_price: float, stop_price: float, risk_per_trade: float = 0.01) -> float:
        """
        根據停損距離 & 風險承受度，自動計算倉位大小。
        回傳值：應投入的 USDT 數量
        """
        max_loss = balance * risk_per_trade            # 允許的最大虧損（USDT）
        risk_per_unit = abs(entry_price - stop_price)  # 每單位風險（USDT）
        if risk_per_unit <= 0:
            return 0.0
        position_units = max_loss / risk_per_unit
        stake_amount = position_units * entry_price
        return float(stake_amount)

    # ===============================
    # Freqtrade callback: 自訂倉位大小
    # ===============================
    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float,
        max_stake: float,
        **kwargs,
    ) -> float:
        """
        動態計算倉位大小：
          - 以 ATR(1h) * atr_mult 當作停損距離
          - 單筆風險 = risk_per_trade(預設 1%)
        """
        try:
            balance = float(self.wallets.get_total_stake_balance())
        except Exception:
            # 回退：若無法讀取錢包（回測理論上可以），用 10000 模擬
            balance = 10000.0

        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df is None or df.empty or "atr" not in df.columns:
            # 沒資料就使用系統建議
            stake = proposed_stake
        else:
            atr = float(df["atr"].iloc[-1])
            stop_price = current_rate - float(self.atr_mult.value) * atr
            # risk = float(self.risk_per_trade_hp.value)
            risk = self.risk_per_trade
            stake = self.position_size(
                balance=balance,
                entry_price=current_rate,
                stop_price=stop_price,
                risk_per_trade=risk,
            )

        # 夾在交易所允許範圍內
        if max_stake is not None:
            stake = min(stake, max_stake)
        if min_stake is not None:
            stake = max(stake, min_stake)
        return float(stake)

    # ===============================
    # 自訂停損（固定，不追蹤）
    # ===============================
    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        """
        以「訊號K」計算固定相對停損:
        Long: stop = 訊號收盤 - atr_mult * ATR(1h)
        Short: stop = 訊號最高 + atr_mult * ATR(1h)
        回傳 *相對損失比*（負值）。首次計算後鎖定於 _sl_cache, 之後不再更新。
        """

        # 若已計算，直接回傳
        if trade.id in self._sl_cache:
            return self._sl_cache[trade.id]

        # 取得 dataframe
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df is None or df.empty or "atr" not in df.columns:
            return 1

        # 找到訊號K位置
        if "date" in df.columns:
            idx = df.index[df["date"] >= trade.open_date_utc]
        else:
            idx = df.index[df.index >= trade.open_date_utc]
        if len(idx) == 0:
            return 1

        entry_loc = idx[0]
        try:
            sig_pos = df.index.get_loc(entry_loc) - 1
            if sig_pos < 0:
                return 1
        except Exception:
            return 1

        row_sig = df.iloc[sig_pos]
        atr_mult = float(self.atr_mult.value)

        # 判斷方向
        direction = getattr(trade, "direction", None)
        if direction is None:
            direction = "short" if getattr(trade, "is_short", False) else "long"

        entry_price = float(trade.open_rate)
        if direction == "long":
            stop_price = float(row_sig["close"]) - atr_mult * float(row_sig["atr"])
            rel_sl = (stop_price - entry_price) / entry_price
        else:
            stop_price = float(row_sig["high"]) + atr_mult * float(row_sig["atr"])
            rel_sl = (entry_price - stop_price) / entry_price

        # 停損比例需為負值
        rel_sl = min(rel_sl, -0.001)

        # 鎖定在內部 cache，而不是 trade.user_data
        self._sl_cache[trade.id] = rel_sl

        return rel_sl


    # 槓桿（若使用合約）
    def leverage(self, *args, **kwargs) -> float:
        return 3.0

    # 繪圖設定 (Freqtrade plot-dataframe 用)
    plot_config = {
        "main_plot": {
            # K 線上畫的
            "bb_up": {"color": "red"},
            "bb_mid": {"color": "orange"},
            "bb_low": {"color": "green"},
        },
        "subplots": {
            # 額外開小圖
            "RSI (4h)": {
                "rsi_4h": {"color": "blue"},
            },
            "ADX (1h)": {
                "adx": {"color": "purple"},
            },
            "ADX (4h)": {
                "adx_4h": {"color": "pink"},
            },
            "ATR (1h)": {
                "atr": {"color": "black"},
            },
        }
    }


# --- Strategy idea ---
# https://www.youtube.com/watch?v=c9-SIpy3dEw