# source: https://raw.githubusercontent.com/deyogaid/freqtrade/bf4379a65c4000b3251f74edf4de1abe1496f058/user_data/strategies/strategy_SMC_CISD_Scalping.py
# directory_url: https://github.com/deyogaid/freqtrade/blob/develop/user_data/strategies/
# User: deyogaid
# Repository: freqtrade
# --------------------from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
from pandas import DataFrame, Series
import talib.abstract as ta
import numpy as np
import pandas as pd


class Github_deyogaid_freqtrade__strategy_SMC_CISD_Scalping__20260913_184200(IStrategy):
    """
    Smart Money Concepts + CISD (Change In State of Delivery) Confluence
    Mode Scalping 15m
    """

    # ──── Strategy Settings ────
    timeframe = "15m"
    can_short = True
    startup_candle_count = 50
    process_only_new_candles = False
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # ──── Mode‑dependent Parameters (Scalping) ────
    swing_len = 5
    rr_ratio = 1.5
    adx_threshold = 15.0
    sl_buffer_mult = 0.3
    max_bars_cisd = 10
    cooldown_normal = 3
    cooldown_loss = 6  # tambahan jika trade sebelumnya loss → total 9 bar
    fib_min = 0.618
    fib_max = 0.786

    # ──── User Override (bisa diubah lewat config) ────
    min_wick_ratio = 1.5
    use_adx = True
    use_cisd_entry = True
    use_fib_entry = True

    # ──── Internal State ────
    def __init__(self, config: dict) -> None:
        super().__init__(config)
        self._last_exit_info = {}  # pair -> {'exit_time': Timestamp, 'is_loss': bool}

    # ──── Indicators ────
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        pair = metadata["pair"]
        df = dataframe.copy()

        # 1. Swing High / Low
        left = right = self.swing_len
        df["swing_high"] = self._pivot_high(df["high"], left, right)
        df["swing_low"] = self._pivot_low(df["low"], left, right)

        df["swing_high_px"] = np.where(df["swing_high"], df["high"], np.nan)
        df["swing_low_px"] = np.where(df["swing_low"], df["low"], np.nan)

        # lastSH / lastSL dan prevSH / prevSL
        df["lastSH"] = df["swing_high_px"].ffill()
        df["lastSL"] = df["swing_low_px"].ffill()
        df["prevSH"] = (
            df["swing_high_px"].ffill().shift(1)
        )  # salah: shift setelah ffill bisa beda indeks
        # Perbaikan: gunakan offset dengan dropna
        sh_values = df["swing_high_px"].dropna()
        sl_values = df["swing_low_px"].dropna()
        df["prevSH"] = sh_values.reindex(df.index).ffill().shift(1)  # ini tidak tepat
        # Lebih aman: kita bangun ulang dengan iterasi ringan atau gunakan cumcount
        # Karena simpel, kita lakukan manual vektorisasi yang benar:
        # Gunakan group untuk mengisi prevSH sebagai nilai SH sebelumnya.
        # Trik: saat swing high muncul, isi kolom 'lastSH' dan 'prevSH' dengan shift internal.
        # Kita pakai loop kecil hanya untuk swing points (efisien)
        df["prevSH"] = np.nan
        df["prevSL"] = np.nan
        last_sh = None
        last_sl = None
        for i in df.index:
            if df.at[i, "swing_high"]:
                if last_sh is not None:
                    df.at[i, "prevSH"] = last_sh
                last_sh = df.at[i, "high"]
            else:
                df.at[i, "prevSH"] = last_sh if not np.isnan(df.at[i, "lastSH"]) else np.nan

            if df.at[i, "swing_low"]:
                if last_sl is not None:
                    df.at[i, "prevSL"] = last_sl
                last_sl = df.at[i, "low"]
            else:
                df.at[i, "prevSL"] = last_sl if not np.isnan(df.at[i, "lastSL"]) else np.nan

        # 2. Structure Bull/Bear
        has_structure = (
            df["lastSH"].notna()
            & df["lastSL"].notna()
            & df["prevSH"].notna()
            & df["prevSL"].notna()
        )
        df["isBullStructure"] = (
            has_structure & (df["lastSH"] > df["prevSH"]) & (df["lastSL"] > df["prevSL"])
        )
        df["isBearStructure"] = (
            has_structure & (df["lastSH"] < df["prevSH"]) & (df["lastSL"] < df["prevSL"])
        )

        # 3. Break of Structure (BOS)
        # Bullish BOS: close crosses over lastSH
        df["bullishBOS"] = (
            df["isBullStructure"]
            & (df["close"] > df["lastSH"])
            & (df["close"].shift(1) <= df["lastSH"].shift(1))
        )
        df["bearishBOS"] = (
            df["isBearStructure"]
            & (df["close"] < df["lastSL"])
            & (df["close"].shift(1) >= df["lastSL"].shift(1))
        )

        # 4. Trend Bias (stateful)
        df["trend_bias"] = 0
        df.loc[df["bullishBOS"], "trend_bias"] = 1
        df.loc[df["bearishBOS"], "trend_bias"] = -1
        df["trend_bias"] = df["trend_bias"].replace(0, np.nan).ffill().fillna(0).astype(int)

        # 5. Bars Since BOS
        df["bos_event"] = df["bullishBOS"] | df["bearishBOS"]
        df["bos_era"] = df["bos_event"].cumsum()
        df["bars_since_bos"] = df.groupby("bos_era").cumcount()

        # 6. Leg Impulse (legLow/legHigh static since last BOS)
        df["leg_low"] = np.nan
        df["leg_high"] = np.nan
        df.loc[df["bullishBOS"], "leg_low"] = df["lastSL"]
        df.loc[df["bullishBOS"], "leg_high"] = df["high"]
        df.loc[df["bearishBOS"], "leg_high"] = df["lastSH"]
        df.loc[df["bearishBOS"], "leg_low"] = df["low"]
        df["leg_low"] = df["leg_low"].ffill()
        df["leg_high"] = df["leg_high"].ffill()
        df["leg_range"] = df["leg_high"] - df["leg_low"]

        # 7. Fibonacci OTE Zones
        df["fibTopBull"] = np.where(
            df["trend_bias"] == 1, df["leg_high"] - df["leg_range"] * self.fib_min, np.nan
        )
        df["fibBotBull"] = np.where(
            df["trend_bias"] == 1, df["leg_high"] - df["leg_range"] * self.fib_max, np.nan
        )
        df["fibBotBear"] = np.where(
            df["trend_bias"] == -1, df["leg_low"] + df["leg_range"] * self.fib_min, np.nan
        )
        df["fibTopBear"] = np.where(
            df["trend_bias"] == -1, df["leg_low"] + df["leg_range"] * self.fib_max, np.nan
        )

        # 8. CISD (sweep + reclaim) – stateful, gunakan loop
        df["bullishCISD"] = False
        df["bearishCISD"] = False
        df["cisd_sl_long"] = np.nan
        df["cisd_sl_short"] = np.nan

        sweep_bull_active = False
        sweep_bear_active = False

        for i in df.index:
            # Reset on BOS
            if df.at[i, "bullishBOS"]:
                sweep_bull_active = False
            if df.at[i, "bearishBOS"]:
                sweep_bear_active = False

            # Sweep detection
            if (
                df.at[i, "trend_bias"] == 1
                and not sweep_bull_active
                and df.at[i, "low"] <= df.at[i, "lastSL"]
            ):
                sweep_bull_active = True
            if (
                df.at[i, "trend_bias"] == -1
                and not sweep_bear_active
                and df.at[i, "high"] >= df.at[i, "lastSH"]
            ):
                sweep_bear_active = True

            # Reclaim (CISD confirmation)
            if (
                sweep_bull_active
                and df.at[i, "close"] > df.at[i, "lastSL"]
                and df.at[i, "close"] > df.at[i, "open"]
                and df.at[i, "bars_since_bos"] <= self.max_bars_cisd
            ):
                df.at[i, "bullishCISD"] = True
                df.at[i, "cisd_sl_long"] = df.at[i, "lastSL"]
                sweep_bull_active = False

            if (
                sweep_bear_active
                and df.at[i, "close"] < df.at[i, "lastSH"]
                and df.at[i, "close"] < df.at[i, "open"]
                and df.at[i, "bars_since_bos"] <= self.max_bars_cisd
            ):
                df.at[i, "bearishCISD"] = True
                df.at[i, "cisd_sl_short"] = df.at[i, "lastSH"]
                sweep_bear_active = False

        # Forward fill CISD SL levels agar tersedia saat entry Fib
        df["cisd_sl_long"] = df["cisd_sl_long"].ffill()
        df["cisd_sl_short"] = df["cisd_sl_short"].ffill()

        # 9. ADX & ATR
        df["adx"] = ta.ADX(df, timeperiod=14)
        df["atr"] = ta.ATR(df, timeperiod=14)
        df["trending_market"] = (not self.use_adx) | (df["adx"] > self.adx_threshold)

        # 10. Rejection Candles
        body = abs(df["close"] - df["open"])
        lower_wick = np.minimum(df["open"], df["close"]) - df["low"]
        upper_wick = df["high"] - np.maximum(df["open"], df["close"])
        df["bull_rejection"] = (df["close"] > df["open"]) & (
            lower_wick > body * self.min_wick_ratio
        )
        df["bear_rejection"] = (df["close"] < df["open"]) & (
            upper_wick > body * self.min_wick_ratio
        )

        # 11. Cooldown berdasarkan exit sebelumnya
        exit_info = self._last_exit_info.get(pair)
        if exit_info is not None:
            exit_time = exit_info["exit_time"]
            timeframe_seconds = self._timeframe_to_seconds(self.timeframe)
            df["time_since_exit"] = (df["date"] - exit_time).dt.total_seconds() / timeframe_seconds
            required = self.cooldown_normal + (self.cooldown_loss if exit_info["is_loss"] else 0)
            df["cooldown_ok"] = df["time_since_exit"] >= required
        else:
            df["cooldown_ok"] = True

        # 12. Entry Condition Helpers
        inWindow = df["bars_since_bos"] <= self.max_bars_cisd
        inBullZone = (
            df["fibTopBull"].notna()
            & (df["low"] <= df["fibTopBull"])
            & (df["high"] >= df["fibBotBull"])
        )
        inBearZone = (
            df["fibBotBear"].notna()
            & (df["high"] >= df["fibBotBear"])
            & (df["low"] <= df["fibTopBear"])
        )

        beliCISD = (
            self.use_cisd_entry & df["bullishCISD"] & df["trending_market"] & df["cooldown_ok"]
        )
        jualCISD = (
            self.use_cisd_entry & df["bearishCISD"] & df["trending_market"] & df["cooldown_ok"]
        )
        beliFib = (
            self.use_fib_entry
            & (df["trend_bias"] == 1)
            & inBullZone
            & df["bull_rejection"]
            & df["trending_market"]
            & df["cooldown_ok"]
            & inWindow
        )
        jualFib = (
            self.use_fib_entry
            & (df["trend_bias"] == -1)
            & inBearZone
            & df["bear_rejection"]
            & df["trending_market"]
            & df["cooldown_ok"]
            & inWindow
        )

        df["kondisiBeli"] = beliCISD | beliFib
        df["kondisiJual"] = jualCISD | jualFib

        # 13. Hitung SL & TP harga untuk sinyal yang terjadi (untuk digunakan di confirm_trade_entry)
        df["entry_sl"] = np.nan
        df["entry_tp"] = np.nan
        atr = df["atr"]

        # Long entries
        long_mask = df["kondisiBeli"]
        sl_base_long = np.where(
            beliCISD & df["cisd_sl_long"].notna(),
            df["cisd_sl_long"],
            df["leg_low"].fillna(df["low"]),
        )
        sl_price_long = sl_base_long - atr * self.sl_buffer_mult
        risk_long = df["close"] - sl_price_long
        valid_risk = risk_long > 0
        df.loc[long_mask & valid_risk, "entry_sl"] = sl_price_long
        df.loc[long_mask & valid_risk, "entry_tp"] = df["close"] + risk_long * self.rr_ratio

        # Short entries
        short_mask = df["kondisiJual"]
        sl_base_short = np.where(
            jualCISD & df["cisd_sl_short"].notna(),
            df["cisd_sl_short"],
            df["leg_high"].fillna(df["high"]),
        )
        sl_price_short = sl_base_short + atr * self.sl_buffer_mult
        risk_short = sl_price_short - df["close"]
        valid_risk = risk_short > 0
        df.loc[short_mask & valid_risk, "entry_sl"] = sl_price_short
        df.loc[short_mask & valid_risk, "entry_tp"] = df["close"] - risk_short * self.rr_ratio

        # Simpan ke dataframe utama
        dataframe = df
        return dataframe

    # ──── Entry Signals ────
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[dataframe["kondisiBeli"], "enter_long"] = 1
        dataframe.loc[dataframe["kondisiJual"], "enter_short"] = 1
        return dataframe

    # ──── Exit Signals (Take Profit) ────
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Exit akan dipicu oleh custom_exit_price, tapi kita butuh sinyal.
        # Gunakan flag dari custom_info (diisi saat confirm_trade_entry)
        pair = metadata["pair"]
        if pair in self._custom_entry_info:
            info = self._custom_entry_info[pair]
            tp = info["tp"]
            is_long = info["is_long"]
            if is_long:
                # exit jika high >= tp
                dataframe.loc[dataframe["high"] >= tp, "exit_long"] = 1
            else:
                dataframe.loc[dataframe["low"] <= tp, "exit_short"] = 1
        return dataframe

    # ──── Custom Entry Confirmation ────
    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time,
        entry_tag,
        side: str,
        **kwargs,
    ) -> bool:
        # Ambil SL & TP dari dataframe terakhir
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1]
        sl = last_candle["entry_sl"]
        tp = last_candle["entry_tp"]

        if np.isnan(sl) or np.isnan(tp):
            return False  # jangan entry jika perhitungan tidak valid

        # Simpan ke custom_info untuk digunakan di exit dan stoploss
        self._custom_entry_info[pair] = {
            "sl": sl,
            "tp": tp,
            "entry": rate,
            "is_long": (side == "long"),
        }
        return True

    # ──── Custom Exit Confirmation (catat data loss/win, reset info) ────
    def confirm_trade_exit(
        self,
        pair: str,
        trade,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        exit_reason: str,
        current_time,
        **kwargs,
    ) -> bool:
        # Catat loss/win untuk cooldown
        profit_ratio = trade.calc_profit_ratio(rate)
        self._last_exit_info[pair] = {"exit_time": current_time, "is_loss": profit_ratio < 0}
        # Hapus custom entry info
        self._custom_entry_info.pop(pair, None)
        return True

    # ──── Custom Stoploss ────
    def custom_stoploss(
        self, pair: str, trade, current_time, current_rate, current_profit, **kwargs
    ) -> float:
        # Ambil SL dari custom info
        info = self._custom_entry_info.get(pair)
        if info is None:
            return None  # fallback ke stoploss default
        sl_price = info["sl"]
        if trade.is_short:
            stoploss_percent = (sl_price - trade.open_rate) / trade.open_rate
        else:
            stoploss_percent = (trade.open_rate - sl_price) / trade.open_rate
        # Pastikan nilai positif (persentase jarak dari open_rate ke SL)
        return max(stoploss_percent, 0.001)  # minimal 0.1%

    # ──── Custom Exit Price (Take Profit Limit) ────
    def custom_exit_price(
        self, pair: str, trade, current_time, proposed_rate, current_profit, **kwargs
    ) -> float:
        info = self._custom_entry_info.get(pair)
        if info:
            return info["tp"]
        return proposed_rate

    # ──── Helper Functions ────
    @staticmethod
    def _pivot_high(series, left, right):
        """Detect pivot high (left bars lower, right bars lower)"""
        max_win = series.rolling(window=left + right + 1, center=True).max()
        return series == max_win

    @staticmethod
    def _pivot_low(series, left, right):
        min_win = series.rolling(window=left + right + 1, center=True).min()
        return series == min_win

    @staticmethod
    def _timeframe_to_seconds(tf: str) -> int:
        """Convert timeframe string to seconds"""
        unit = tf[-1]
        val = int(tf[:-1])
        mult = {"m": 60, "h": 3600, "d": 86400}
        return val * mult[unit]
