# source: https://raw.githubusercontent.com/deyogaid/freqtrade/bf4379a65c4000b3251f74edf4de1abe1496f058/user_data/strategies/SMC_Fibonacci_Scalping.py
# directory_url: https://github.com/deyogaid/freqtrade/blob/develop/user_data/strategies/
# User: deyogaid
# Repository: freqtrade
# --------------------"""
SMC_Scalping_Playbook — Scalping Crypto Future SMC & Multi-Timeframe (15m, ATR SL, leverage terukur)
"""

import logging
from datetime import datetime
from typing import Any

import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame

from freqtrade.rpc.rpc import RPC
from freqtrade.enums import RPCMessageType
from freqtrade.persistence import Trade
from freqtrade.strategy import (
    DecimalParameter,
    IntParameter,
    IStrategy,
    merge_informative_pair,
    stoploss_from_absolute,
)


logger = logging.getLogger(__name__)


class Github_deyogaid_freqtrade__SMC_Fibonacci_Scalping__20260913_184200(IStrategy):
    INTERFACE_VERSION = 3

    # Futures settings
    can_short = True
    timeframe = "15m"
    informative_timeframe = "1h"

    # Execution performance
    process_only_new_candles = True
    startup_candle_count = 150
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # Dynamic Leverage Settings (5x - 10x untuk Modal Terbatas $30-$50 USDT)
    # Semua margin-space cap WAJIB di-scale proporsional agar price-space
    # breathing room ~1.0-1.2% terjaga di leverage tinggi.
    max_leverage = 3.0
    min_leverage = 1.0

    # Risk Management — margin-space values scaled for ~7x avg leverage
    # Price tolerance = |margin_pct| / leverage, target ~1.0% price room
    stoploss = -0.04
    trailing_stop = True
    trailing_stop_positive = 0.006
    trailing_stop_positive_offset = 0.015
    trailing_only_offset_is_reached = True
    use_custom_stoploss = True

    minimal_roi = {
        "0": 0.045,
        "30": 0.028,
        "60": 0.016,
    }

    # Dynamic Config Parameters
    sweep_lookback = IntParameter(10, 20, default=18, space="buy", optimize=True)
    adx_threshold = DecimalParameter(15.0, 25.0, default=17.09, space="buy", optimize=True)
    sl_buffer_atr = DecimalParameter(1.0, 3.5, default=3.211, space="buy", optimize=True)
    max_hold_min = IntParameter(60, 180, default=170, space="sell", optimize=True)

    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs: Any,
    ) -> float:
        if entry_tag and "high_conf" in entry_tag:
            return 3.0
        elif entry_tag and "med_conf" in entry_tag:
            return 2.0
        return 1.5

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs: Any,
    ) -> float:
        """Dynamic Fibonacci + ATR Stoploss.
        Terkunci pada level invalidasi Fibonacci saat entry (1.272 extension dari swing point).
        Adaptif terhadap volatilitas dan pergerakan harga dealing range.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return self.stoploss
        atr = dataframe["atr"].iloc[-1]
        if not np.isfinite(atr) or atr <= 0:
            return self.stoploss

        entry_rows = dataframe.loc[dataframe["date"] <= trade.open_date_utc]
        if not entry_rows.empty:
            entry_candle = entry_rows.iloc[-1]
            if trade.is_short:
                fib_sl = entry_candle.get(
                    "fib_sl_short", trade.open_rate + atr * self.sl_buffer_atr.value
                )
                sl_price = max(trade.open_rate + atr * self.sl_buffer_atr.value, fib_sl)
            else:
                fib_sl = entry_candle.get(
                    "fib_sl_long", trade.open_rate - atr * self.sl_buffer_atr.value
                )
                sl_price = min(trade.open_rate - atr * self.sl_buffer_atr.value, fib_sl)
        else:
            if trade.is_short:
                sl_price = trade.open_rate + atr * self.sl_buffer_atr.value
            else:
                sl_price = trade.open_rate - atr * self.sl_buffer_atr.value

        sl_value = stoploss_from_absolute(
            sl_price, current_rate, is_short=trade.is_short, leverage=trade.leverage
        )
        return max(-sl_value, self.sl_max_pct)

    # Cap keras SL per trade — margin-space, MUST scale with leverage.
    # At 7x avg: 8% margin = ~1.14% price room → 67.6% WR (validated).
    # Tighter -0.05 dropped WR to 58.9% — too tight for 15m candles at high leverage.
    sl_max_pct = -0.04

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

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 1. HTF (1h) Informative Data
        if self.dp:
            informative = self.dp.get_pair_dataframe(
                pair=metadata["pair"], timeframe=self.informative_timeframe
            )
            informative["htf_ema_fast"] = ta.EMA(informative, timeperiod=21)
            informative["htf_ema_slow"] = ta.EMA(informative, timeperiod=50)
            informative["htf_rsi"] = ta.RSI(informative, timeperiod=14)
            informative["htf_uptrend"] = (
                informative["htf_ema_fast"] > informative["htf_ema_slow"]
            ) & (informative["close"] > informative["htf_ema_fast"])
            informative["htf_downtrend"] = (
                informative["htf_ema_fast"] < informative["htf_ema_slow"]
            ) & (informative["close"] < informative["htf_ema_fast"])

            dataframe = merge_informative_pair(
                dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True
            )

        # 2. Base Timeframe (15m) Indicators
        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=21)
        dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema99"] = ta.EMA(dataframe, timeperiod=99)
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)

        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"] = macd["macdhist"]
        dataframe["macdhist_slope_bull"] = dataframe["macdhist"] > dataframe["macdhist"].shift(1)
        dataframe["macdhist_slope_bear"] = dataframe["macdhist"] < dataframe["macdhist"].shift(1)

        # Volume Activity & Surge
        dataframe["volume_mean_20"] = dataframe["volume"].rolling(20).mean()
        dataframe["volume_surge_high"] = dataframe["volume"] > (dataframe["volume_mean_20"] * 1.5)
        dataframe["volume_surge_med"] = dataframe["volume"] > (dataframe["volume_mean_20"] * 1.15)

        # Chaikin Money Flow (CMF 20) — Arus Modal Institusional
        mfv = (
            ((dataframe["close"] - dataframe["low"]) - (dataframe["high"] - dataframe["close"]))
            / (dataframe["high"] - dataframe["low"]).replace(0, 0.00000001)
            * dataframe["volume"]
        )
        dataframe["cmf"] = mfv.rolling(20).sum() / dataframe["volume"].rolling(20).sum().replace(
            0, 0.00000001
        )

        # 3. Candlestick Rejections
        body = (dataframe["close"] - dataframe["open"]).abs().replace(0, 0.00000001)
        lower_wick = np.minimum(dataframe["open"], dataframe["close"]) - dataframe["low"]
        upper_wick = dataframe["high"] - np.maximum(dataframe["open"], dataframe["close"])

        dataframe["bull_rejection"] = (lower_wick > (body * 1.5)) & (
            dataframe["close"] >= dataframe["open"]
        )
        dataframe["bear_rejection"] = (upper_wick > (body * 1.5)) & (
            dataframe["close"] <= dataframe["open"]
        )

        # 4. Liquidity Sweep & Reclaim Detection
        # Sweep + reclaim SAMA candle: wick tembus swing level, close kembali di atas/bawah.
        lookback = self.sweep_lookback.value
        dataframe["swing_low"] = dataframe["low"].rolling(window=lookback).min().shift(1)
        dataframe["swing_high"] = dataframe["high"].rolling(window=lookback).max().shift(1)
        dataframe["fib_range"] = (dataframe["swing_high"] - dataframe["swing_low"]).replace(
            0, 0.00000001
        )

        # Fibonacci Retracement & Targets
        # Bullish: Retracement dari swing_high ke swing_low
        dataframe["fib_long_0500"] = dataframe["swing_high"] - (
            0.500 * dataframe["fib_range"]
        )  # Equilibrium
        dataframe["fib_long_0618"] = dataframe["swing_high"] - (
            0.618 * dataframe["fib_range"]
        )  # Golden Pocket
        dataframe["fib_long_0786"] = dataframe["swing_high"] - (
            0.786 * dataframe["fib_range"]
        )  # OTE
        dataframe["fib_tp_long_1"] = dataframe["swing_high"]  # Range High (Target 1)
        dataframe["fib_tp_long_2"] = dataframe["swing_high"] + (
            0.272 * dataframe["fib_range"]
        )  # 1.272 Ext (Target 2)
        dataframe["fib_sl_long"] = dataframe["swing_low"] - (
            0.272 * dataframe["fib_range"]
        )  # Structural SL 1.272

        # Bearish: Retracement dari swing_low ke swing_high
        dataframe["fib_short_0500"] = dataframe["swing_low"] + (
            0.500 * dataframe["fib_range"]
        )  # Equilibrium
        dataframe["fib_short_0618"] = dataframe["swing_low"] + (
            0.618 * dataframe["fib_range"]
        )  # Golden Pocket
        dataframe["fib_short_0786"] = dataframe["swing_low"] + (
            0.786 * dataframe["fib_range"]
        )  # OTE
        dataframe["fib_tp_short_1"] = dataframe["swing_low"]  # Range Low (Target 1)
        dataframe["fib_tp_short_2"] = dataframe["swing_low"] - (
            0.272 * dataframe["fib_range"]
        )  # 1.272 Ext (Target 2)
        dataframe["fib_sl_short"] = dataframe["swing_high"] + (
            0.272 * dataframe["fib_range"]
        )  # Structural SL 1.272

        # Dynamic Fibonacci Expected R:R
        risk_long = (dataframe["close"] - dataframe["fib_sl_long"]).clip(lower=0.00000001)
        reward_long = (dataframe["fib_tp_long_1"] - dataframe["close"]).clip(lower=0)
        dataframe["fib_rr_long"] = reward_long / risk_long

        risk_short = (dataframe["fib_sl_short"] - dataframe["close"]).clip(lower=0.00000001)
        reward_short = (dataframe["close"] - dataframe["fib_tp_short_1"]).clip(lower=0)
        dataframe["fib_rr_short"] = reward_short / risk_short

        dataframe["sweep_bull"] = (dataframe["low"] < dataframe["swing_low"]) & (
            dataframe["close"] > dataframe["swing_low"]
        )
        dataframe["reclaim_bull"] = dataframe["close"] > dataframe["swing_low"]

        dataframe["sweep_bear"] = (dataframe["high"] > dataframe["swing_high"]) & (
            dataframe["close"] < dataframe["swing_high"]
        )
        dataframe["reclaim_bear"] = dataframe["close"] < dataframe["swing_high"]
        # 5. CONFLUENCE SCORING / PROBABILITY INDICATORS
        dataframe["long_score"] = 0
        dataframe["short_score"] = 0

        # Poin tambahan untuk Long
        dataframe.loc[dataframe["volume_surge_high"], "long_score"] += 1
        dataframe.loc[dataframe["bull_rejection"], "long_score"] += 1
        dataframe.loc[(dataframe["rsi"] >= 52) & (dataframe["rsi"] <= 65), "long_score"] += 1
        dataframe.loc[dataframe["adx"] > 25, "long_score"] += 1

        # Poin tambahan untuk Short
        dataframe.loc[dataframe["volume_surge_high"], "short_score"] += 1
        dataframe.loc[dataframe["bear_rejection"], "short_score"] += 1
        dataframe.loc[(dataframe["rsi"] <= 48) & (dataframe["rsi"] >= 35), "short_score"] += 1
        dataframe.loc[dataframe["adx"] > 25, "short_score"] += 1

        # 6. FreqAI — prediksi harga forward (filter entry tambahan) dipanggil di akhir
        if (
            self.config.get("freqai", {}).get("enabled", False)
            and "do_predict" not in dataframe.columns
        ):
            dataframe = self.freqai.start(dataframe, metadata, self)

        return dataframe

    # ================= FreqAI Feature Engineering =================
    def feature_engineering_expand_all(
        self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
    ) -> DataFrame:
        """Fitur yang di-expand per indicator_periods_candles (10, 20) + include_timeframes."""
        dataframe[f"%-rsi-period_{period}"] = ta.RSI(dataframe, timeperiod=period)
        dataframe[f"%-mfi-period_{period}"] = ta.MFI(dataframe, timeperiod=period)
        dataframe[f"%-adx-period_{period}"] = ta.ADX(dataframe, timeperiod=period)
        dataframe[f"%-ema-period_{period}"] = ta.EMA(dataframe, timeperiod=period)
        dataframe[f"%-atr-period_{period}"] = ta.ATR(dataframe, timeperiod=period) / (
            dataframe["close"] + 1e-8
        )

        # Swings & sweep flags — inti SMC sebagai fitur
        swing_low = dataframe["low"].rolling(period).min().shift(1)
        swing_high = dataframe["high"].rolling(period).max().shift(1)
        dataframe[f"%-swing_low-period_{period}"] = swing_low
        dataframe[f"%-swing_high-period_{period}"] = swing_high
        dataframe[f"%-dist_swing_low-period_{period}"] = (
            (dataframe["close"] - swing_low) / (swing_low + 1e-8)
        ).fillna(0)
        dataframe[f"%-dist_swing_high-period_{period}"] = (
            (dataframe["close"] - swing_high) / (swing_high + 1e-8)
        ).fillna(0)
        return dataframe

    def feature_engineering_expand_basic(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        """Fitur basic: tidak di-expand per period."""
        dataframe["%-pct_change"] = dataframe["close"].pct_change().fillna(0)
        vol_mean = dataframe["volume"].rolling(20).mean()
        dataframe["%-volume_ratio"] = (dataframe["volume"] / (vol_mean + 1e-8)).fillna(1.0)
        dataframe["%-relative_volume"] = dataframe["volume"].pct_change().fillna(0)
        return dataframe

    def feature_engineering_standard(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        """Fitur non-periodik: waktu dan replikasi struktur/skor SMC."""
        dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
        dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7

        # Replikasi Indikator & Sinyal SMC jika kolom sudah terbentuk
        if "long_score" in dataframe.columns:
            dataframe["%-smc_long_score"] = dataframe["long_score"].fillna(0)
        if "short_score" in dataframe.columns:
            dataframe["%-smc_short_score"] = dataframe["short_score"].fillna(0)
        if "sweep_bull" in dataframe.columns:
            dataframe["%-sweep_bull"] = dataframe["sweep_bull"].astype(float).fillna(0)
        if "sweep_bear" in dataframe.columns:
            dataframe["%-sweep_bear"] = dataframe["sweep_bear"].astype(float).fillna(0)
        if "bull_rejection" in dataframe.columns:
            dataframe["%-bull_rejection"] = dataframe["bull_rejection"].astype(float).fillna(0)
        if "bear_rejection" in dataframe.columns:
            dataframe["%-bear_rejection"] = dataframe["bear_rejection"].astype(float).fillna(0)
        if "volume_surge_high" in dataframe.columns:
            dataframe["%-volume_surge_high"] = (
                dataframe["volume_surge_high"].astype(float).fillna(0)
            )

        # Replikasi Indikator Aliran Modal (CMF) & Fibonacci OTE
        if "cmf" in dataframe.columns:
            dataframe["%-cmf"] = dataframe["cmf"].fillna(0)
        if "fib_long_0618" in dataframe.columns and "fib_range" in dataframe.columns:
            dataframe["%-dist_fib_long_0618"] = (
                (dataframe["close"] - dataframe["fib_long_0618"]) / (dataframe["fib_range"] + 1e-8)
            ).fillna(0)
        if "fib_short_0618" in dataframe.columns and "fib_range" in dataframe.columns:
            dataframe["%-dist_fib_short_0618"] = (
                (dataframe["close"] - dataframe["fib_short_0618"]) / (dataframe["fib_range"] + 1e-8)
            ).fillna(0)
        if "fib_rr_long" in dataframe.columns:
            dataframe["%-fib_rr_long"] = dataframe["fib_rr_long"].clip(upper=10).fillna(0)
        if "fib_rr_short" in dataframe.columns:
            dataframe["%-fib_rr_short"] = dataframe["fib_rr_short"].clip(upper=10).fillna(0)

        return dataframe

    def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
        """Target: return harga rata-rata label_period_candles candle ke depan (regresi)."""
        label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
        forward_close_mean = dataframe["close"].shift(-label_period).rolling(label_period).mean()
        dataframe["&-s_close"] = (forward_close_mean / dataframe["close"]) - 1
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        htf_bear_col = f"htf_downtrend_{self.informative_timeframe}"
        htf_bull_col = f"htf_uptrend_{self.informative_timeframe}"

        # FreqAI gate: hanya entry jika model prediksi arah sesuai & yakin (do_predict == 1).
        # Kolom 'do_predict' & '&-s_close' hanya ada saat freqai aktif — fallback ke True.
        if "do_predict" in dataframe.columns:
            ai_short_ok = (dataframe["do_predict"] == 1) & (dataframe["&-s_close"] < 0)
            ai_long_ok = (dataframe["do_predict"] == 1) & (dataframe["&-s_close"] > 0)
        else:
            ai_short_ok = pd.Series(True, index=dataframe.index)
            ai_long_ok = pd.Series(True, index=dataframe.index)

        base_short = [
            ai_short_ok,
            (dataframe[htf_bear_col] == 1)
            if htf_bear_col in dataframe.columns
            else (dataframe["close"] < dataframe["ema_slow"]),
            dataframe["sweep_bear"],
            dataframe["reclaim_bear"],
            dataframe["adx"] > self.adx_threshold.value,
            dataframe["volume_surge_med"],
            dataframe["rsi"] > 30,
            dataframe["rsi"] < 52,
            # Fibonacci Gate: Short hanya dieksekusi di zona Golden Pocket (>= 61.8% retracement)
            dataframe["close"] >= dataframe["fib_short_0618"],
            dataframe["fib_rr_short"] >= 1.4,
            dataframe["close"] < dataframe["ema200"],
            dataframe["cmf"] < 0.0,
        ]

        base_long = [
            ai_long_ok,
            (dataframe[htf_bull_col] == 1)
            if htf_bull_col in dataframe.columns
            else (dataframe["close"] > dataframe["ema_slow"]),
            dataframe["sweep_bull"],
            dataframe["reclaim_bull"],
            dataframe["adx"] > self.adx_threshold.value,
            dataframe["volume_surge_med"],
            dataframe["rsi"] > 48,
            dataframe["rsi"] < 70,
            # Fibonacci Gate: Long hanya dieksekusi di zona Golden Pocket (<= 61.8% retracement)
            dataframe["close"] <= dataframe["fib_long_0618"],
            dataframe["fib_rr_long"] >= 1.4,
            dataframe["close"] > dataframe["ema200"],
            dataframe["cmf"] > 0.0,
        ]

        cond_short = np.logical_and.reduce(base_short)

        cond_long = np.logical_and.reduce(base_long)
        short_score = dataframe["short_score"]
        long_score = dataframe["long_score"]

        # SHORT: Hanya ambil High Confidence (skor konfluensi >= 2)
        dataframe.loc[cond_short & (short_score >= 2), ["enter_short", "enter_tag"]] = (
            1,
            "smc_short_sweep_high_conf",
        )

        # LONG: Hanya ambil High Confidence (skor konfluensi >= 2)
        dataframe.loc[cond_long & (long_score >= 2), ["enter_long", "enter_tag"]] = (
            1,
            "smc_long_sweep_high_conf",
        )

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (dataframe["rsi"] > 82),
            ["exit_long", "exit_tag"],
        ] = (1, "exit_long_exhaustion")

        dataframe.loc[
            (dataframe["rsi"] < 18),
            ["exit_short", "exit_tag"],
        ] = (1, "exit_short_exhaustion")

        return dataframe

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs: Any,
    ) -> str | None:
        """Dynamic Fibonacci Target Exit + Time-based Cut Loser."""
        # 1. Fibonacci Target Exit
        if self.dp:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if dataframe is not None and not dataframe.empty:
                entry_rows = dataframe.loc[dataframe["date"] <= trade.open_date_utc]
                if not entry_rows.empty:
                    entry_candle = entry_rows.iloc[-1]
                    if not trade.is_short:
                        fib_tp1 = entry_candle.get("fib_tp_long_1", 0)
                        if fib_tp1 > 0 and current_rate >= fib_tp1 and current_profit > 0.008:
                            return "exit_fib_target_1"
                    else:
                        fib_tp1 = entry_candle.get("fib_tp_short_1", 0)
                        if fib_tp1 > 0 and current_rate <= fib_tp1 and current_profit > 0.008:
                            return "exit_fib_target_1"

        # 2. Time-based Cut Loser (Scalping 15m)
        elapsed_min = (current_time - trade.open_date_utc).total_seconds() / 60
        if elapsed_min >= self.max_hold_min.value and current_profit <= 0:
            return "exit_time_max_hold_cut_loser"
        return None
