# source: https://raw.githubusercontent.com/Aatif-qmr/cipher/f3735aaf3655923bb1c8ee70c054c6a1a0d8831d/strategies/archive/ScalpV1.py
# directory_url: https://github.com/Aatif-qmr/cipher/blob/main/strategies/archive/
# User: Aatif-qmr
# Repository: cipher
# --------------------import json
import logging
import sys
from datetime import datetime, timedelta
from pathlib import Path

from pandas import DataFrame

# Resolve project root from this file's location (works on any machine)
_BASE = Path(__file__).resolve().parent.parent.parent
if str(_BASE) not in sys.path:
    sys.path.insert(0, str(_BASE))

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

from indicators.macro_merge import merge_macro_data
from qnt.oracle.hmm_regime import detect_regime, get_regime_for_strategy
from qnt.thesis.thesis_reader import read_thesis
from risk.correlation_guard import is_blocked as corr_blocked
from risk.risk_manager import run_all_checks
from risk.stake_sizer import get_stake_multiplier
from sentiment.reader import get_current_sentiment, get_funding_rate, get_sentiment_signal

logger = logging.getLogger(__name__)

_partial_exits_done: set = set()  # trade IDs that already had 50% exit


class Github_Aatif_qmr_cipher__ScalpV1__20260901_081406(IStrategy):
    """
    5-minute scalping strategy.
    Entry: RSI < 30 + Price < Lower BB + Vol > Avg Vol
    Exit: RSI > 60 or Price > Mid BB
    Includes multi-timeframe confirmation (15m RSI, 1h EMA).
    """

    INTERFACE_VERSION = 3

    timeframe = "5m"
    informative_timeframes = ["15m", "1h"]

    stoploss = -0.02
    trailing_stop = True

    # R:R fixed: first target equals stoploss (1:1), then decreases to exit slow trades
    minimal_roi = {
        "0": 0.02,
        "30": 0.015,
        "60": 0.01,
        "120": 0.005,
    }

    # Hyperoptable / Dynamic parameters
    buy_rsi = IntParameter(20, 40, default=30, space="buy")
    sell_rsi = IntParameter(60, 80, default=60, space="sell")

    def load_dynamic_params(self):
        self.buy_rsi_val = self.buy_rsi.value
        self.sell_rsi_val = self.sell_rsi.value

        try:
            path1 = _BASE / "config/dynamic_params.json"
            path2 = _BASE / "config/dynamic_params.json"
            path = path1 if path1.exists() else path2

            if path.exists():
                with open(path) as f:
                    params = json.load(f)

                strat_name = self.__class__.__name__
                strat_params = params.get(strat_name, params)

                if "buy_rsi" in strat_params:
                    self.buy_rsi_val = int(strat_params["buy_rsi"])
                if "sell_rsi" in strat_params:
                    self.sell_rsi_val = int(strat_params["sell_rsi"])

                logger.info(
                    f"[{strat_name}] Dynamically loaded parameters: buy_rsi={self.buy_rsi_val}, sell_rsi={self.sell_rsi_val}"
                )
        except Exception as e:
            logger.warning(f"Failed to load dynamic parameters, using defaults: {e}")

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

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Load dynamic parameters first
        self.load_dynamic_params()

        from qnt.polars_indicators import add_bollinger_bands, add_ema, add_rsi, add_sma
        from qnt.polars_ohlcv import ohlcv_to_pandas, pandas_to_polars

        # Convert to Polars
        df_pl = pandas_to_polars(dataframe)

        # 5m Indicators
        df_pl = add_rsi(df_pl, period=14, alias="rsi")
        df_pl = add_bollinger_bands(df_pl, period=20, std_dev=2.0, prefix="bb")
        df_pl = add_sma(df_pl, period=20, column="volume", alias="volume_avg")
        df_pl = df_pl.rename({"bb_mid": "bb_middle"})

        # Informative timeframes
        if getattr(self, "config", {}).get("runmode", {}).value in ("live", "dry_run"):
            for tf in self.informative_timeframes:
                inf_df_pd = self.dp.get_pair_dataframe(metadata["pair"], tf)
                if inf_df_pd is not None and not inf_df_pd.empty:
                    inf_df_pl = pandas_to_polars(inf_df_pd)

                    if tf == "15m":
                        inf_df_pl = add_rsi(inf_df_pl, period=14, alias="rsi")
                    elif tf == "1h":
                        inf_df_pl = add_ema(inf_df_pl, period=200, alias="ema_200")

                    # Back to Pandas for Freqtrade merge
                    inf_df_pd = ohlcv_to_pandas(inf_df_pl)

                dataframe = merge_informative_pair(
                    ohlcv_to_pandas(df_pl), inf_df_pd, self.timeframe, tf, ffill=True
                )
                df_pl = pandas_to_polars(dataframe)

        # Convert back to Pandas for merge_macro_data
        dataframe = ohlcv_to_pandas(df_pl)
        dataframe = merge_macro_data(dataframe)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        self.load_dynamic_params()

        is_live = self.config.get("runmode", {}).value in ("live", "dry_run")

        # External signals only available in live/dry_run — default to permissive in backtesting
        sentiment_ok = True
        regime_ok = True
        if is_live:
            sentiment = get_current_sentiment()
            sentiment_ok = sentiment["score"] >= -0.3
            regime = detect_regime(dataframe, metadata["pair"])
            regime_ok = get_regime_for_strategy("Github_Aatif_qmr_cipher__ScalpV1__20260901_081406", regime)

        dataframe.loc[
            (
                (dataframe["rsi"] < self.buy_rsi_val)
                & (dataframe["rsi"] > dataframe["rsi"].shift(1))
                & (dataframe["close"] < dataframe["bb_lower"])
                & (dataframe["volume"] > dataframe["volume_avg"])
                & (dataframe.get("rsi_15m", 50) < 60)
                & (dataframe.get("close", 0) > dataframe.get("ema_200_1h", 0))
                & (sentiment_ok)
                & (regime_ok)
            ),
            "enter_long",
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        self.load_dynamic_params()

        raw_exit = (dataframe["rsi"] > self.sell_rsi_val) | (
            dataframe["close"] > dataframe["bb_middle"]
        )
        # Require signal on 2 consecutive candles — kills one-tick RSI spikes that exit trades at a loss
        dataframe.loc[raw_exit & raw_exit.shift(1).fillna(False), "exit_long"] = 1
        return dataframe

    def adjust_trade_position(
        self,
        trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        min_stake,
        max_stake,
        current_entry_rate: float,
        current_exit_rate: float,
        current_entry_profit: float,
        current_exit_profit: float,
        **kwargs,
    ):
        if trade.id in _partial_exits_done:
            return None
        if current_profit >= 0.010:
            _partial_exits_done.add(trade.id)
            logger.info(
                f"[PARTIAL EXIT] Github_Aatif_qmr_cipher__ScalpV1__20260901_081406 {trade.pair} profit={current_profit:.2%} — exiting 50%"
            )
            return -(trade.stake_amount * 0.5)
        return None

    def custom_exit(
        self,
        pair: str,
        trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ):
        hours_open = (current_time - trade.open_date_utc).total_seconds() / 3600

        # Stale scalp: cut if negative after 4 hours
        if hours_open >= 4 and current_profit < -0.003:
            return "time_stop_4h"

        # Dynamic profit target by regime — bear bounces fade fast, take less
        try:
            dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if dataframe is not None and not dataframe.empty:
                regime = detect_regime(dataframe, pair)
                if regime == "BEAR" and current_profit >= 0.008:
                    return "bear_bounce_target"
        except Exception:
            pass

        return None

    def custom_stake_amount(
        self,
        current_time,
        current_rate,
        proposed_stake,
        min_stake,
        max_stake,
        leverage,
        entry_tag,
        side,
        **kwargs,
    ):
        multiplier = get_stake_multiplier("Github_Aatif_qmr_cipher__ScalpV1__20260901_081406")
        stake = proposed_stake * multiplier
        if min_stake is not None:
            stake = max(stake, min_stake)
        return min(stake, max_stake)

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: str,
        side: str,
        **kwargs,
    ) -> bool:
        if self.config.get("runmode", {}).value not in ("live", "dry_run"):
            return True

        # --- LAYER 0: THESIS GATE ---
        thesis = read_thesis(pair)
        if thesis["bias"] == "SELL":
            logger.info(
                f"[THESIS BLOCK] {pair} bias=SELL confidence={thesis['confidence']:.2f} — {thesis['reasoning']}"
            )
            return False
        stake_modifier = thesis.get("stake_modifier", 1.0)

        # --- LAYER 1: RISK & SENTIMENT CHECKS ---
        try:
            total_balance = self.wallets.get_total("USDT")

            # Fetch recent trades for loss counting
            recent_trades = [
                {
                    "profit_ratio": float(
                        getattr(t, "close_profit", None) or getattr(t, "profit_ratio", None) or 0.0
                    ),
                    "close_date": getattr(t, "close_date", None),
                }
                for t in Trade.get_trades_proxy(is_open=False)
            ][:10]

            # Count trades in the last hour
            one_hour_ago = current_time - timedelta(hours=1)
            trades_last_hour = len(
                [
                    t
                    for t in Trade.get_trades_proxy(is_open=False)
                    if t.close_date and t.close_date >= one_hour_ago
                ]
            )

            # Load balance state for drawdown checks
            state_file = _BASE / "risk/balance_state.json"
            if state_file.exists():
                with open(state_file) as f:
                    state = json.load(f)
                start_of_day = state.get("start_of_day", total_balance)
                start_of_week = state.get("start_of_week", total_balance)
            else:
                start_of_day = total_balance
                start_of_week = total_balance

            # Github_Aatif_qmr_cipher__ScalpV1__20260901_081406 requires at least NEUTRAL sentiment
            risk_result = run_all_checks(
                current_balance=total_balance,
                start_of_day_balance=start_of_day,
                start_of_week_balance=start_of_week,
                trade_amount_usdt=amount * rate * stake_modifier,
                trades_last_hour=trades_last_hour,
                recent_trades=recent_trades,
                min_sentiment="NEUTRAL",
            )

            if not risk_result["safe_to_trade"]:
                logger.info(
                    f"[RISK/SENTIMENT BLOCK] Scalp blocked for {pair}. Reasons: {risk_result['blocking_reasons']}"
                )
                return False

            # Log sentiment for visibility
            sentiment = get_current_sentiment()
            signal = get_sentiment_signal()
            logger.info(
                f"[Sentiment Check] {pair} | Score: {sentiment['score']:.3f} | Signal: {signal}"
            )

            # --- LAYER 2: SKEPTIC AGENT (final gate) ---
            try:
                import sys

                sys.path.insert(0, str(_BASE / "qnt/agents"))
                from strategist import summarize_signal
                from trade_gate import evaluate_trade

                # Get analyzed dataframe
                dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

                signal_summary = summarize_signal(dataframe, "Github_Aatif_qmr_cipher__ScalpV1__20260901_081406", pair)

                gate_result = evaluate_trade(
                    {
                        **signal_summary,
                        "sentiment_score": sentiment["score"],
                        "hmm_regime": detect_regime(dataframe),
                        "stake_amount": amount * rate,
                    }
                )

                if gate_result["decision"] == "BLOCK":
                    logger.info(
                        f"[SKEPTIC BLOCK] {pair} "
                        f"Confidence: {gate_result['failure_confidence']:.0%} "
                        f"Reason: {gate_result['primary_concern']}"
                    )
                    return False
                else:
                    logger.info(f"[SKEPTIC ALLOW] {pair} | Proceeding with trade.")
            except Exception as e:
                logger.error(f"[SKEPTIC ERROR] {e} — proceeding")

        except Exception as e:
            logger.error(f"[RISK WARNING] Risk check error: {e}")

        base = pair.split("/")[0]
        if corr_blocked(base, side):
            logger.info(f"[CORR BLOCK] Github_Aatif_qmr_cipher__ScalpV1__20260901_081406 {pair} — too many concurrent longs on {base}")
            return False

        if side == "long":
            funding = get_funding_rate()
            if funding < -0.5:
                logger.info(
                    f"[FUNDING BLOCK] Github_Aatif_qmr_cipher__ScalpV1__20260901_081406 {pair} funding={funding:.2f} — extreme negative funding"
                )
                return False

        return True
