# source: https://raw.githubusercontent.com/deyogaid/freqtrade/bf4379a65c4000b3251f74edf4de1abe1496f058/user_data/strategies/AIEnhancedStrategy_analysis.py
# directory_url: https://github.com/deyogaid/freqtrade/blob/develop/user_data/strategies/
# User: deyogaid
# Repository: freqtrade
# --------------------"""
Github_deyogaid_freqtrade__AIEnhancedStrategy_analysis__20260913_184200 — Strategi Trading Berbasis Probabilitas AI
===============================================================
Strategi ini menggabungkan analisis teknikal klasik dengan konsensus AI (Grok)
untuk meningkatkan probabilitas keberhasilan setiap trade.

Arsitektur 3 Lapis:
┌─────────────────────────────────────────────────────┐
│  Layer 1: Signal Generator (EMA Cross + RSI + MACD) │
│           → Kandidat entry/exit awal                 │
├─────────────────────────────────────────────────────┤
│  Layer 2: AI Consensus Filter                        │
│           → Baca ai_signals/consensus.json dari Grok │
│           → Filter berdasarkan probabilitas & risiko  │
├─────────────────────────────────────────────────────┤
│  Layer 3: Adaptive Position Sizing                   │
│           → Sesuaikan stake berdasarkan keyakinan AI  │
│           → Trailing stop adaptif per kondisi pasar   │
└─────────────────────────────────────────────────────┘

Cara kerja:
1. `populate_entry_trend()` → sinyal teknikal (EMA/RSI/MACD)
2. `confirm_trade_entry()` → filter AI — cek consensus.json
3. `custom_stake_amount()` → stake adaptif berdasarkan probabilitas AI
4. `custom_exit()` → exit dini jika AI mendeteksi risiko tinggi
5. `custom_stoploss()` → stoploss dinamis berdasarkan volatilitas

Konfigurasi di config.json:
  "hermes_params": {
    "ai_min_probability": 0.65,   // threshold masuk
    "ai_use_filter": true,         // aktifkan filter AI
    "ai_stake_scale": true,        // sesuaikan stake dgn probabilitas
    "ai_dynamic_exit": true        // exit dini jika AI negatif
  }
"""

import json
import logging
from datetime import datetime, timezone
from pathlib import Path

import pandas as pd
import pandas_ta as ta  # type: ignore
from freqtrade.persistence import Trade
from freqtrade.strategy import (
    DecimalParameter,
    IntParameter,
    IStrategy,
    merge_informative_pair,
)

logger = logging.getLogger(__name__)

# Path ke file konsensus AI (ditulis oleh hermes_agent/ai_analyst.py)
AI_CONSENSUS_FILE = Path(__file__).parent.parent / "ai_signals" / "consensus.json"


class Github_deyogaid_freqtrade__AIEnhancedStrategy_analysis__20260913_184200(IStrategy):
    """
    Strategi trading dengan filter AI probabilistik.

    Technical: EMA Cross + RSI + MACD + Bollinger Bands
    AI Filter: Grok consensus dari hermes-agent background task
    """

    INTERFACE_VERSION = 3

    # ─── Timeframe ────────────────────────────────────────────────────────────
    timeframe = "5m"
    informative_timeframe = "1h"  # Context jangka panjang dari TF lebih besar

    # ─── Risk Management ──────────────────────────────────────────────────────
    stoploss = -0.05  # -5% stoploss dasar
    trailing_stop = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.015
    trailing_only_offset_is_reached = True

    # ROI per durasi candle
    minimal_roi = {
        "0": 0.05,  # 5% kapan saja
        "15": 0.03,  # 3% setelah 15 candle (75 menit)
        "30": 0.02,  # 2% setelah 30 candle (150 menit)
        "60": 0.01,  # 1% setelah 60 candle (300 menit)
    }

    # ─── Hyperopt Parameters (dapat dioptimasi) ───────────────────────────────
    # Entry
    buy_ema_fast = IntParameter(5, 20, default=9, space="buy", optimize=True)
    buy_ema_slow = IntParameter(20, 60, default=26, space="buy", optimize=True)
    buy_rsi_low = IntParameter(20, 45, default=35, space="buy", optimize=True)
    buy_macd_min = DecimalParameter(-0.01, 0.01, default=0.0, space="buy", optimize=True)
    buy_bb_width_min = DecimalParameter(0.01, 0.05, default=0.02, space="buy", optimize=True)

    # Exit
    sell_rsi_high = IntParameter(60, 85, default=70, space="sell", optimize=True)
    sell_ema_cross = IntParameter(5, 20, default=9, space="sell", optimize=True)

    # AI Filter
    ai_min_prob = DecimalParameter(0.5, 0.9, default=0.65, space="buy", optimize=False)

    # ─── Strategy Config ──────────────────────────────────────────────────────
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = True
    process_only_new_candles = False  # Proses data historis untuk test
    startup_candle_count = 60

    # ─── Helpers ──────────────────────────────────────────────────────────────
    def _load_ai_consensus(self) -> dict:
        """Baca konsensus AI dari file. Return default jika kedaluwarsa/tidak ada."""
        default = {
            "tren_pasar": "unknown",
            "risiko_pasar": "medium",
            "sentimen_score": 0.5,
            "probabilitas_entry": {},
            "pairs_direkomendasikan": [],
            "pairs_dihindari": [],
            "alasan": "Fallback — file konsensus tidak tersedia",
            "max_trades": 5,
            "stake_modifier": 1.0,
            "source": "fallback",
        }
        try:
            if not AI_CONSENSUS_FILE.exists():
                return default
            data = json.loads(AI_CONSENSUS_FILE.read_text())
            ts = datetime.fromisoformat(data.get("timestamp", "2000-01-01T00:00:00+00:00"))
            valid_min = data.get("valid_until_minutes", 30)
            age_min = (datetime.now(timezone.utc) - ts).total_seconds() / 60
            if age_min > valid_min:
                logger.debug(f"Konsensus AI kedaluwarsa ({age_min:.0f} mnt), pakai fallback")
                return default
            return data
        except Exception as e:
            logger.warning(f"Gagal baca AI consensus: {e}")
            return default

    def _get_hermes_params(self, key: str, default):
        """Ambil parameter dari section hermes_params di config."""
        return self.config.get("hermes_params", {}).get(key, default)

    def _ai_filter_active(self) -> bool:
        return bool(self._get_hermes_params("ai_use_filter", True))

    # ─── Informative Pairs (TF 1 jam sebagai context) ─────────────────────────
    def informative_pairs(self):
        pairs = self.dp.current_whitelist()
        informative = [(pair, self.informative_timeframe) for pair in pairs]
        return informative

    # ─── Indicators 1h (konteks pasar) ───────────────────────────────────────
    def populate_indicators_1h(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """Indikator pada timeframe 1 jam sebagai filter tren besar."""
        dataframe["ema_50"] = ta.ema(dataframe["close"], length=50)
        dataframe["ema_200"] = ta.ema(dataframe["close"], length=200)
        dataframe["rsi_1h"] = ta.rsi(dataframe["close"], length=14)

        # MACD 1h
        macd = ta.macd(dataframe["close"], fast=12, slow=26, signal=9)
        if macd is not None and not macd.empty:
            dataframe["macd_1h"] = macd["MACD_12_26_9"]
            dataframe["macd_signal_1h"] = macd["MACDs_12_26_9"]

        # Tren 1h: True jika harga di atas EMA 200
        dataframe["uptrend_1h"] = dataframe["close"] > dataframe["ema_200"]

        return dataframe

    # ─── Indikator Utama (5m) ─────────────────────────────────────────────────
    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """Hitung semua indikator teknikal."""
        pair = metadata["pair"]

        # ── EMA ──
        dataframe["ema_fast"] = ta.ema(dataframe["close"], length=self.buy_ema_fast.value)
        dataframe["ema_slow"] = ta.ema(dataframe["close"], length=self.buy_ema_slow.value)
        dataframe["ema_9"] = ta.ema(dataframe["close"], length=9)
        dataframe["ema_21"] = ta.ema(dataframe["close"], length=21)

        # ── RSI ──
        dataframe["rsi"] = ta.rsi(dataframe["close"], length=14)

        # ── MACD ──
        macd = ta.macd(dataframe["close"], fast=12, slow=26, signal=9)
        if macd is not None and not macd.empty:
            dataframe["macd"] = macd["MACD_12_26_9"]
            dataframe["macd_signal"] = macd["MACDs_12_26_9"]
            dataframe["macd_hist"] = macd["MACDh_12_26_9"]

        # ── Bollinger Bands ──
        bb = ta.bbands(dataframe["close"], length=20, std=2)
        if bb is not None and not bb.empty:
            dataframe["bb_lower"] = bb["BBL_20_2.0"]
            dataframe["bb_middle"] = bb["BBM_20_2.0"]
            dataframe["bb_upper"] = bb["BBU_20_2.0"]
            dataframe["bb_width"] = (dataframe["bb_upper"] - dataframe["bb_lower"]) / dataframe[
                "bb_middle"
            ]
            dataframe["bb_percent"] = (dataframe["close"] - dataframe["bb_lower"]) / (
                dataframe["bb_upper"] - dataframe["bb_lower"]
            )

        # ── Volume ──
        dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean()
        dataframe["volume_surge"] = dataframe["volume"] > dataframe["volume_mean"] * 1.5

        # ── ATR (volatilitas) ──
        dataframe["atr"] = ta.atr(
            dataframe["high"], dataframe["low"], dataframe["close"], length=14
        )

        # ── Stochastic RSI ──
        stoch = ta.stochrsi(dataframe["close"], length=14)
        if stoch is not None and not stoch.empty:
            cols = stoch.columns.tolist()
            if len(cols) >= 2:
                dataframe["stoch_k"] = stoch[cols[0]]
                dataframe["stoch_d"] = stoch[cols[1]]

        # ── Gabung TF 1 jam ──
        try:
            informative_1h = self.dp.get_pair_dataframe(
                pair=pair, timeframe=self.informative_timeframe
            )
            if not informative_1h.empty:
                informative_1h = self.populate_indicators_1h(informative_1h, metadata)
                dataframe = merge_informative_pair(
                    dataframe,
                    informative_1h,
                    self.timeframe,
                    self.informative_timeframe,
                    ffill=True,
                )
        except Exception as e:
            logger.debug(f"Tidak bisa merge 1h untuk {pair}: {e}")
            dataframe["uptrend_1h"] = True  # default allow

        return dataframe

    # ─── Entry Signal ─────────────────────────────────────────────────────────
    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        LAYER 1: Sinyal teknikal — kandidat entry.

        Improved: Lebih banyak sinyal dengan kondisi yang lebih fleksibel.
        Filter AI diterapkan di confirm_trade_entry().
        """
        from functools import reduce

        conditions = []

        # EMA Cross: fast memotong slow dari bawah ke atas (Golden Cross)
        ema_cross_up = (dataframe["ema_fast"] > dataframe["ema_slow"]) & (
            dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1)
        )
        conditions.append(ema_cross_up)

        # RSI tidak overbought (ada ruang naik) -放宽 ke 45
        conditions.append(dataframe["rsi"] < 45)

        # MACD histogram positif atau netral (momentum tidak bearish)
        if "macd_hist" in dataframe.columns:
            conditions.append(dataframe["macd_hist"] > -0.001)

        # Harga di atas BB middle (tren positif) - JADI OPSIONAL
        # if "bb_middle" in dataframe.columns:
        #     conditions.append(dataframe["close"] > dataframe["bb_middle"])

        # Volume di atas rata-rata (OPSIONAL - kurangi strictness)
        # conditions.append(dataframe["volume_surge"])

        # Tren 1h positif — (OPSIONAL jika ada)
        # if "uptrend_1h" in dataframe.columns:
        #     conditions.append(dataframe["uptrend_1h"])

        # Harga tidak terlalu jauh dari EMA slow (hindari entry saat spike)
        conditions.append(
            (dataframe["close"] - dataframe["ema_slow"]) / dataframe["ema_slow"] < 0.05
        )

        if conditions:
            combined = reduce(lambda a, b: a & b, conditions)
            dataframe.loc[combined, "enter_long"] = 1
            dataframe.loc[combined, "enter_tag"] = "ema_cross_v3"

        return dataframe

    # ─── Exit Signal ──────────────────────────────────────────────────────────
    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """Sinyal exit teknikal dasar."""
        conditions_exit = []

        # EMA cross ke bawah
        ema_cross_down = (dataframe["ema_fast"] < dataframe["ema_slow"]) & (
            dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1)
        )
        conditions_exit.append(ema_cross_down)

        # RSI overbought
        conditions_exit.append(dataframe["rsi"] > self.sell_rsi_high.value)

        if conditions_exit:
            dataframe.loc[pd.concat(conditions_exit, axis=1).any(axis=1), "exit_long"] = 1

        return dataframe

    # ─── LAYER 2: AI Consensus Filter ─────────────────────────────────────────
    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> bool:
        """
        LAYER 2: Filter AI sebelum eksekusi.
        Return True = izinkan trade, False = blokir.

        Fallback logic: Jika AI tidak tersedia atau gagal, tetap izinkan
        trade teknis (kecuali risiko ekstrem).
        """
        if not self._ai_filter_active():
            return True  # Filter dimatikan, izinkan semua

        consensus = self._load_ai_consensus()
        min_prob = self._get_hermes_params("ai_min_probability", self.ai_min_prob.value)

        # ── Cek risiko pasar ekstrem ──
        market_risk = consensus.get("risiko_pasar", "medium")
        if market_risk == "extreme":
            logger.info(f"[AI BLOKIR] Risiko pasar ekstrem — semua entry diblokir")
            return False

        # ── Jika AI gagal (fallback) — izinkan trade teknis tapi kurangi stake ──
        if consensus.get("source") == "fallback":
            logger.info(f"[AI FALLBACK] {pair}: AI tidak tersedia, izinkan dengan mitigasi")
            # Set stake modifier ke 0.8 via consensus untuk kurangi eksposur
            return True  # IZINKAN tapi risk management handle di custom_stake_amount

        # ── Cek pair di daftar hindari ──
        avoided = consensus.get("pairs_dihindari", [])
        if pair in avoided:
            logger.info(
                f"[AI BLOKIR] {pair} ada di daftar hindari AI "
                f"(risiko: {consensus.get('risiko_pasar')})"
            )
            return False

        # ── Cek probabilitas entry per pair ──
        probs = consensus.get("probabilitas_entry", {})
        pair_prob = probs.get(pair, consensus.get("sentimen_score", 0.5))

        # Fallback ke 0.5 jika sentiment tidak valid
        if pair_prob < 0.4:
            pair_prob = 0.5

        if pair_prob < min_prob:
            logger.info(
                f"[AI BLOKIR] {pair}: probabilitas {pair_prob:.2f} < {min_prob:.2f}"
                f" | tren={consensus.get('tren_pasar')} | risiko={market_risk}"
            )
            return False

        # ── Cek batas max trades dari AI ──
        max_ai_trades = consensus.get("max_trades", 5)
        open_trade_count = Trade.get_open_trade_count()
        if open_trade_count >= max_ai_trades:
            logger.info(
                f"[AI BLOKIR] {pair}: sudah {open_trade_count} trade terbuka "
                f"(AI limit: {max_ai_trades})"
            )
            return False

        # ── IZINKAN ──
        logger.info(
            f"[AI IZIN] {pair}: prob={pair_prob:.2f} ≥ {min_prob:.2f} | "
            f"tren={consensus.get('tren_pasar')} | risiko={market_risk} | "
            f"source={consensus.get('source', 'unknown')}"
        )
        return True

    # ─── LAYER 3: Adaptive Stake (ukuran posisi adaptif) ─────────────────────
    def custom_stake_amount(
        self,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> float:
        """
        LAYER 3: Sesuaikan ukuran posisi berdasarkan keyakinan AI.
        Probabilitas tinggi → stake lebih besar.
        """
        if not self._get_hermes_params("ai_stake_scale", True):
            return proposed_stake

        consensus = self._load_ai_consensus()
        stake_modifier = consensus.get("stake_modifier", 1.0)

        # Clamp: minimum 50%, maksimum 150% dari stake normal
        stake_modifier = max(0.5, min(1.5, stake_modifier))
        adjusted = proposed_stake * stake_modifier

        # Pastikan tidak melebihi max_stake
        adjusted = min(adjusted, max_stake)
        if min_stake:
            adjusted = max(adjusted, min_stake)

        if stake_modifier != 1.0:
            logger.info(
                f"Stake disesuaikan: {proposed_stake:.2f} → {adjusted:.2f} "
                f"(modifier AI: {stake_modifier:.2f})"
            )
        return adjusted

    # ─── Custom Exit (exit dini jika AI negatif) ──────────────────────────────
    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> str | None:
        """
        Exit dini jika:
        1. AI mendeteksi risiko ekstrem
        2. Pair masuk daftar hindari AI
        3. Profit sudah cukup + AI menjadi negatif
        """
        if not self._get_hermes_params("ai_dynamic_exit", True):
            return None

        consensus = self._load_ai_consensus()
        market_risk = consensus.get("risiko_pasar", "medium")
        avoided = consensus.get("pairs_dihindari", [])
        probs = consensus.get("probabilitas_entry", {})
        pair_prob = probs.get(pair, consensus.get("sentimen_score", 0.5))

        # Exit jika risiko ekstrem dan sudah profit
        if market_risk == "extreme" and current_profit > 0.005:
            return f"ai_exit_extreme_risk (profit: {current_profit:.2%})"

        # Exit jika pair masuk daftar hindari dan ada profit
        if pair in avoided and current_profit > 0:
            return f"ai_exit_avoided_pair (profit: {current_profit:.2%})"

        # Exit jika probabilitas AI turun drastis dan ada profit
        min_prob = self._get_hermes_params("ai_min_probability", 0.65)
        if pair_prob < (min_prob * 0.6) and current_profit > 0.01:
            return f"ai_exit_low_prob ({pair_prob:.2f}) (profit: {current_profit:.2%})"

        # Exit dini jika tren berbalik menurut AI dan rugi kecil (cut loss)
        if (
            consensus.get("tren_pasar") == "bearish"
            and market_risk in ("high", "extreme")
            and current_profit > -0.02
        ):
            return f"ai_exit_bearish_market (profit: {current_profit:.2%})"

        return None

    # ─── Custom Stoploss Dinamis ───────────────────────────────────────────────
    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float:
        """
        Stoploss adaptif berdasarkan risiko AI:
        - Low risk → stoploss lebih longgar (-7%)
        - High risk → stoploss lebih ketat (-3%)
        - Extreme risk → stoploss sangat ketat (-2%)
        """
        consensus = self._load_ai_consensus()
        market_risk = consensus.get("risiko_pasar", "medium")

        stoploss_map = {
            "low": -0.07,
            "medium": -0.05,
            "high": -0.03,
            "extreme": -0.02,
        }
        return stoploss_map.get(market_risk, self.stoploss)
