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

import json
import logging
from datetime import datetime
from pathlib import Path
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_Scalping_Playbook__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 (Supports up to 10x for micro capital $30-$50 USDT)
    max_leverage = 10.0
    min_leverage = 1.0

    # Risk Management — margin-space values scaled for leverage
    stoploss = -0.15
    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=20.0, space="buy", optimize=True)
    sl_buffer_atr = DecimalParameter(1.0, 3.5, default=2.5, space="buy", optimize=True)
    max_hold_min = IntParameter(60, 240, default=200, space="sell", optimize=True)

    @property
    def current_mode(self) -> str:
        """Mode strategi dinamis: 'baseline' (SMC asli) atau 'fibonacci' (SMC + Fib Gating)."""
        mode = self.config.get("strategy_mode")
        if mode in ("baseline", "fibonacci"):
            return mode
        try:
            user_dir = self.config.get("user_data_dir")
            candidate_paths = [
                Path(user_dir) / "strategy_mode.json" if user_dir else None,
                Path("user_data/strategy_mode.json"),
                Path("/home/kartini/freqtrade/user_data/strategy_mode.json"),
                Path("/mnt/d/openclaw/freqtrade/user_data/strategy_mode.json"),
            ]
            for p in candidate_paths:
                if p and p.exists():
                    with open(p, "r") as f:
                        data = json.load(f)
                        return data.get("mode", "baseline")
        except Exception:
            pass
        return "baseline"

    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:
        custom_lev = self.config.get("custom_leverage")
        if custom_lev:
            return float(min(custom_lev, max_leverage))
        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 SL: mode 'baseline' menggunakan ATR, mode 'fibonacci' menggunakan level invalidasi Fib (1.272 ext).
        `stoploss_from_absolute` menangani short & leverage.
        """
        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

        if self.current_mode == "fibonacci":
            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:
                sl_price = (
                    trade.open_rate + atr * self.sl_buffer_atr.value
                    if trade.is_short
                    else trade.open_rate - atr * self.sl_buffer_atr.value
                )
        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, scales with leverage to protect 1.0-1.5% price room.
    @property
    def sl_max_pct(self) -> float:
        custom_lev = self.config.get("custom_leverage", 3.0)
        if custom_lev >= 8.0:
            return -0.15
        elif custom_lev >= 5.0:
            return -0.08
        return -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)

        # 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
        dataframe["fib_long_0500"] = dataframe["swing_high"] - (0.500 * dataframe["fib_range"])
        dataframe["fib_long_0618"] = dataframe["swing_high"] - (0.618 * dataframe["fib_range"])
        dataframe["fib_sl_long"] = dataframe["swing_low"] - (0.272 * dataframe["fib_range"])
        dataframe["fib_tp_long_1"] = dataframe["swing_high"]

        dataframe["fib_short_0500"] = dataframe["swing_low"] + (0.500 * dataframe["fib_range"])
        dataframe["fib_short_0618"] = dataframe["swing_low"] + (0.618 * dataframe["fib_range"])
        dataframe["fib_sl_short"] = dataframe["swing_high"] + (0.272 * dataframe["fib_range"])
        dataframe["fib_tp_short_1"] = dataframe["swing_low"]

        # 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,
        ]

        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,
        ]

        cond_short = np.logical_and.reduce(base_short)
        cond_long = np.logical_and.reduce(base_long)

        if self.current_mode == "fibonacci":
            cond_short = (
                cond_short
                & (dataframe["close"] >= dataframe["fib_short_0618"])
                & (dataframe["fib_rr_short"] >= 1.4)
                & (dataframe["close"] < dataframe["ema200"])
                & (dataframe["cmf"] < 0.0)
            )
            cond_long = (
                cond_long
                & (dataframe["close"] <= dataframe["fib_long_0618"])
                & (dataframe["fib_rr_long"] >= 1.4)
                & (dataframe["close"] > dataframe["ema200"])
                & (dataframe["cmf"] > 0.0)
            )

        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 (aktif jika mode fibonacci)
        if getattr(self, "current_mode", "smc_standard") == "fibonacci" and 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
