# source: https://raw.githubusercontent.com/PasinduUpendra/Binance-Futures-Trading/bb5406f750fd8f5ac2210c4b5cbe960850bede86/user_data/strategies/ClaudeQuantAdaptive.py
"""
Github_PasinduUpendra_Binance_Futures_Trading__ClaudeQuantAdaptive__20260315_145324 - Master Freqtrade Strategy

Bridges Claude agent decisions to Freqtrade execution engine.
Reads agent decisions from JSON state files in user_data/agent_state/.

The Claude agent system writes decision files, and this strategy
reads them to execute trades through Freqtrade's infrastructure.
"""

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

import numpy as np
import pandas as pd
import talib

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

logger = logging.getLogger(__name__)

AGENT_STATE_DIR = Path(__file__).parent.parent / "agent_state"


class Github_PasinduUpendra_Binance_Futures_Trading__ClaudeQuantAdaptive__20260315_145324(IStrategy):
    """
    Master strategy that delegates to Claude agent system.

    Reads agent decisions from JSON files and applies them through
    Freqtrade's callback system. Also provides standalone indicator-based
    signals as fallback when agent system is unavailable.
    """

    # Strategy parameters
    INTERFACE_VERSION = 3
    timeframe = "5m"
    can_short = True

    # Minimal ROI - managed by agent's take-profit logic
    minimal_roi = {
        "0": 0.15,    # 15% max if TP not hit
        "60": 0.10,   # 10% after 1 hour
        "120": 0.05,  # 5% after 2 hours
        "240": 0.02,  # 2% after 4 hours
    }

    # Stoploss - overridden by custom_stoploss callback
    stoploss = -0.05  # 5% hard floor, custom_stoploss handles the rest

    # Trailing stop
    trailing_stop = False  # Managed by custom_stoploss

    # Order types
    order_types = {
        "entry": "limit",
        "exit": "limit",
        "emergency_exit": "market",
        "force_entry": "market",
        "force_exit": "market",
        "stoploss": "market",
        "stoploss_on_exchange": True,
        "stoploss_on_exchange_interval": 60,
    }

    # Hyperopt parameters
    buy_adx_threshold = IntParameter(20, 35, default=25, space="buy")
    buy_rsi_lower = IntParameter(25, 40, default=30, space="buy")
    sell_rsi_upper = IntParameter(60, 80, default=70, space="sell")
    atr_sl_multiplier = DecimalParameter(1.0, 2.5, default=1.5, decimals=1, space="stoploss")

    def informative_pairs(self) -> list:
        """Additional pairs for correlation/confirmation."""
        return [
            ("BTC/USDT:USDT", "15m"),
            ("BTC/USDT:USDT", "1h"),
        ]

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """Calculate all technical indicators."""
        # EMAs
        for period in [9, 21, 50, 200]:
            dataframe[f"ema_{period}"] = talib.EMA(dataframe["close"], timeperiod=period)

        # RSI
        dataframe["rsi"] = talib.RSI(dataframe["close"], timeperiod=14)

        # MACD
        macd, signal, hist = talib.MACD(
            dataframe["close"], fastperiod=12, slowperiod=26, signalperiod=9
        )
        dataframe["macd"] = macd
        dataframe["macd_signal"] = signal
        dataframe["macd_hist"] = hist

        # ADX
        dataframe["adx"] = talib.ADX(
            dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14
        )
        dataframe["plus_di"] = talib.PLUS_DI(
            dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14
        )
        dataframe["minus_di"] = talib.MINUS_DI(
            dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14
        )

        # Bollinger Bands
        upper, middle, lower = talib.BBANDS(
            dataframe["close"], timeperiod=20, nbdevup=2, nbdevdn=2
        )
        dataframe["bb_upper"] = upper
        dataframe["bb_middle"] = middle
        dataframe["bb_lower"] = lower
        dataframe["bb_width"] = (upper - lower) / middle

        # ATR
        dataframe["atr"] = talib.ATR(
            dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14
        )

        # Supertrend
        dataframe = self._calculate_supertrend(dataframe, period=10, multiplier=3)

        # Volume SMA
        dataframe["volume_sma"] = talib.SMA(dataframe["volume"], timeperiod=20)
        dataframe["volume_ratio"] = dataframe["volume"] / dataframe["volume_sma"]

        # Z-score for mean reversion
        close_mean = dataframe["close"].rolling(20).mean()
        close_std = dataframe["close"].rolling(20).std()
        dataframe["zscore"] = (dataframe["close"] - close_mean) / close_std

        # Regime detection
        dataframe["regime"] = self._detect_regime(dataframe)

        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """Generate entry signals based on regime and agent decisions."""
        # Check for agent decision file
        agent_signal = self._read_agent_signal(metadata["pair"])

        if agent_signal and agent_signal.get("action") == "enter":
            if agent_signal.get("direction") == "long":
                dataframe.loc[dataframe.index[-1], "enter_long"] = 1
                dataframe.loc[dataframe.index[-1], "enter_tag"] = agent_signal.get(
                    "strategy", "agent"
                )
            elif agent_signal.get("direction") == "short":
                dataframe.loc[dataframe.index[-1], "enter_short"] = 1
                dataframe.loc[dataframe.index[-1], "enter_tag"] = agent_signal.get(
                    "strategy", "agent"
                )
            return dataframe

        # Fallback: standalone indicator signals
        # Long: Trend following
        dataframe.loc[
            (
                (dataframe["ema_9"] > dataframe["ema_21"])
                & (dataframe["adx"] > self.buy_adx_threshold.value)
                & (dataframe["rsi"] > self.buy_rsi_lower.value)
                & (dataframe["rsi"] < self.sell_rsi_upper.value)
                & (dataframe["supertrend_direction"] == 1)
                & (dataframe["volume_ratio"] > 0.8)
            ),
            "enter_long",
        ] = 1

        # Short: Trend following (reversed)
        dataframe.loc[
            (
                (dataframe["ema_9"] < dataframe["ema_21"])
                & (dataframe["adx"] > self.buy_adx_threshold.value)
                & (dataframe["rsi"] < self.sell_rsi_upper.value)
                & (dataframe["rsi"] > self.buy_rsi_lower.value)
                & (dataframe["supertrend_direction"] == -1)
                & (dataframe["volume_ratio"] > 0.8)
            ),
            "enter_short",
        ] = 1

        # Long: Mean reversion
        dataframe.loc[
            (
                (dataframe["close"] <= dataframe["bb_lower"])
                & (dataframe["rsi"] < 30)
                & (dataframe["adx"] < 20)
                & (dataframe["zscore"] < -2)
            ),
            "enter_long",
        ] = 1

        # Short: Mean reversion
        dataframe.loc[
            (
                (dataframe["close"] >= dataframe["bb_upper"])
                & (dataframe["rsi"] > 70)
                & (dataframe["adx"] < 20)
                & (dataframe["zscore"] > 2)
            ),
            "enter_short",
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """Generate exit signals."""
        agent_signal = self._read_agent_signal(metadata["pair"])

        if agent_signal and agent_signal.get("action") == "exit":
            if agent_signal.get("direction") == "long":
                dataframe.loc[dataframe.index[-1], "exit_long"] = 1
            elif agent_signal.get("direction") == "short":
                dataframe.loc[dataframe.index[-1], "exit_short"] = 1
            return dataframe

        # Exit long on EMA cross down or RSI overbought
        dataframe.loc[
            (
                (dataframe["ema_9"] < dataframe["ema_21"])
                | (dataframe["rsi"] > 75)
            ),
            "exit_long",
        ] = 1

        # Exit short on EMA cross up or RSI oversold
        dataframe.loc[
            (
                (dataframe["ema_9"] > dataframe["ema_21"])
                | (dataframe["rsi"] < 25)
            ),
            "exit_short",
        ] = 1

        return dataframe

    def leverage(
        self, pair: str, current_time: datetime, current_rate: float,
        proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
        side: str, **kwargs
    ) -> float:
        """Dynamic leverage from agent decision."""
        agent_signal = self._read_agent_signal(pair)
        if agent_signal and "leverage" in agent_signal:
            requested = agent_signal["leverage"]
            # Cap at max allowed
            return min(float(requested), float(max_leverage), 10.0)

        # Default conservative leverage based on ADX
        return 3.0

    def custom_stake_amount(
        self, pair: str, current_time: datetime, current_rate: float,
        proposed_stake: float, min_stake: Optional[float],
        max_stake: float, leverage: float, entry_tag: Optional[str],
        side: str, **kwargs
    ) -> float:
        """Dynamic position sizing from agent decision."""
        agent_signal = self._read_agent_signal(pair)
        if agent_signal and "stake_amount" in agent_signal:
            requested = float(agent_signal["stake_amount"])
            # Enforce bounds
            if min_stake and requested < min_stake:
                return min_stake
            return min(requested, max_stake)

        # Default: use proposed stake
        return proposed_stake

    def confirm_trade_entry(
        self, pair: str, order_type: str, amount: float, rate: float,
        time_in_force: str, current_time: datetime, entry_tag: Optional[str],
        side: str, **kwargs
    ) -> bool:
        """Final confirmation before trade entry."""
        # Always require agent approval for non-fallback trades
        agent_signal = self._read_agent_signal(pair)

        if agent_signal:
            # Agent explicitly approved
            if agent_signal.get("approved", False):
                logger.info(
                    f"Agent approved trade: {pair} {side} @ {rate} "
                    f"strategy={agent_signal.get('strategy', 'unknown')}"
                )
                return True
            # Agent explicitly rejected
            if agent_signal.get("rejected", False):
                logger.info(f"Agent rejected trade: {pair} {side}")
                return False

        # Fallback signals: allow if no agent decision exists
        return True

    def custom_stoploss(
        self, pair: str, trade: Trade, current_time: datetime,
        current_rate: float, current_profit: float, after_fill: bool,
        **kwargs
    ) -> float:
        """Dynamic stop-loss from agent decision."""
        agent_signal = self._read_agent_signal(pair)

        if agent_signal and "stop_loss_price" in agent_signal:
            sl_price = float(agent_signal["stop_loss_price"])
            if trade.is_short:
                # For shorts, SL is above entry
                sl_pct = (sl_price - trade.open_rate) / trade.open_rate
            else:
                # For longs, SL is below entry
                sl_pct = (trade.open_rate - sl_price) / trade.open_rate
            return -abs(sl_pct)

        # Default: ATR-based stop loss
        # Use the stoploss parameter as fallback
        return self.stoploss

    # ─── Helper Methods ───

    def _read_agent_signal(self, pair: str) -> Optional[dict]:
        """Read the latest agent decision for a pair."""
        if not pair:
            return None
        safe_pair = pair.replace("/", "_").replace(":", "_")
        signal_file = AGENT_STATE_DIR / f"signal_{safe_pair}.json"

        if not signal_file.exists():
            return None

        try:
            data = json.loads(signal_file.read_text())
            # Check freshness (must be < 10 minutes old)
            ts = datetime.fromisoformat(data.get("timestamp", "2000-01-01"))
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
            age = (datetime.now(timezone.utc) - ts).total_seconds()
            if age > 600:
                logger.debug(f"Stale agent signal for {pair} ({age:.0f}s old)")
                return None
            return data
        except (json.JSONDecodeError, KeyError, ValueError) as e:
            logger.warning(f"Failed to read agent signal for {pair}: {e}")
            return None

    def _calculate_supertrend(
        self, df: pd.DataFrame, period: int = 10, multiplier: int = 3
    ) -> pd.DataFrame:
        """Calculate Supertrend indicator."""
        atr = talib.ATR(df["high"], df["low"], df["close"], timeperiod=period)
        hl2 = (df["high"] + df["low"]) / 2

        upper_band = hl2 + (multiplier * atr)
        lower_band = hl2 - (multiplier * atr)

        supertrend = pd.Series(index=df.index, dtype=float)
        direction = pd.Series(index=df.index, dtype=int)

        supertrend.iloc[0] = upper_band.iloc[0]
        direction.iloc[0] = 1

        for i in range(1, len(df)):
            if df["close"].iloc[i] > upper_band.iloc[i - 1]:
                direction.iloc[i] = 1
            elif df["close"].iloc[i] < lower_band.iloc[i - 1]:
                direction.iloc[i] = -1
            else:
                direction.iloc[i] = direction.iloc[i - 1]

            if direction.iloc[i] == 1:
                supertrend.iloc[i] = max(lower_band.iloc[i], supertrend.iloc[i - 1]) \
                    if direction.iloc[i - 1] == 1 else lower_band.iloc[i]
            else:
                supertrend.iloc[i] = min(upper_band.iloc[i], supertrend.iloc[i - 1]) \
                    if direction.iloc[i - 1] == -1 else upper_band.iloc[i]

        df["supertrend"] = supertrend
        df["supertrend_direction"] = direction
        return df

    def _detect_regime(self, df: pd.DataFrame) -> pd.Series:
        """Simple regime detection for indicator-based signals."""
        regime = pd.Series("quiet", index=df.index)

        # Trending
        trending = (df["adx"] > 25)
        regime[trending] = "trending"

        # Ranging
        ranging = (df["adx"] < 20)
        regime[ranging] = "ranging"

        # Volatile - overrides if BB width is extreme
        bb_width_avg = df["bb_width"].rolling(100).mean()
        volatile = (df["bb_width"] > 1.5 * bb_width_avg) & (df["adx"] >= 15)
        regime[volatile] = "volatile"

        # Quiet - low everything
        quiet = (df["adx"] < 15) & (df["volume_ratio"] < 0.5)
        regime[quiet] = "quiet"

        return regime
