# source: https://raw.githubusercontent.com/Bederf/lea-freqai-system/d7e7971d6830e7cccc789f4b7ef44a29648ffbc1/user_data/strategies/BanditMetaStrategy.py
"""
Contextual Bandit Meta-Strategy
Selects between LeaFreqAI and HybridAI strategies based on market context

This is a meta-strategy that:
1. Observes market context (volatility, trend, time)
2. Selects the best strategy for that context (epsilon-greedy)
3. Uses the selected strategy's entry/exit logic
4. Logs decisions for offline learning

Run meta_learner.py daily to update Q-values from trade outcomes.
"""
import json
import logging
import numpy as np
from pathlib import Path
from functools import reduce
from datetime import datetime
from typing import Optional

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

from freqtrade.strategy import IStrategy

logger = logging.getLogger(__name__)


class Github_Bederf_lea_freqai_system__BanditMetaStrategy__20251224_051928(IStrategy):
    """
    Contextual bandit strategy selector

    Chooses between two strategies based on context:
    - LeaFreqAIStrategy: Conservative (tight stops, quick exits)
    - HybridAIStrategy: Aggressive (wider stops, patient exits)

    Context dimensions:
    - Market volatility (low/med/high)
    - Pair trend (down/flat/up)
    - Time of day (morning/day/evening)

    Selection: Epsilon-greedy (90% exploit best Q-value, 10% explore)
    """

    # Strategy metadata
    INTERFACE_VERSION = 3
    can_short = False

    # Timeframe (matches both sub-strategies)
    timeframe = "5m"

    # Startup candles
    startup_candle_count = 200

    # Risk parameters (will be overridden by selected strategy logic)
    # Using LEA's conservative defaults as base
    minimal_roi = {
        "0": 0.015,
        "30": 0.01,
        "60": 0.008,
        "120": 0.005
    }

    stoploss = -0.05
    trailing_stop = True
    trailing_stop_positive = 0.005
    trailing_stop_positive_offset = 0.01

    # Exit settings
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # Process only new candles
    process_only_new_candles = True

    # Order types
    order_types = {
        "entry": "limit",
        "exit": "limit",
        "stoploss": "market",
        "stoploss_on_exchange": False
    }

    order_time_in_force = {
        "entry": "GTC",
        "exit": "GTC"
    }

    def __init__(self, config: dict):
        super().__init__(config)

        # Load bandit selector
        self.selector_path = Path("user_data/bandit_selector.json")
        self.load_selector()

        # Track strategy selection per pair (for exit logic)
        self.pair_strategy_map = {}

        logger.info("Github_Bederf_lea_freqai_system__BanditMetaStrategy__20251224_051928 initialized")
        logger.info(f"Selector loaded: {len(self.selector.get('contexts', {}))} contexts")

    def load_selector(self):
        """Load bandit selection table or initialize with defaults"""
        if self.selector_path.exists():
            with open(self.selector_path) as f:
                self.selector = json.load(f)
                logger.info(f"Loaded selector from {self.selector_path}")
        else:
            # Initialize with uniform priors (no knowledge yet)
            self.selector = {
                "contexts": {},
                "epsilon": 0.1,  # 10% exploration
                "alpha": 0.1,    # Learning rate (not used in strategy, only meta_learner)
                "last_updated": None,
                "total_trades_processed": 0
            }
            logger.warning(f"No selector found at {self.selector_path}, initialized with defaults")
            logger.warning("Run trades and meta_learner.py to build Q-values")

    def get_context(self, dataframe: DataFrame, pair: str) -> str:
        """
        Extract current market context

        Returns context string like: "vol_low_trend_up_hour_day"
        """
        if len(dataframe) < 50:
            return "vol_med_trend_flat_hour_day"  # Default for insufficient data

        last = dataframe.iloc[-1]

        # 1. Market volatility (from BTC correlation feature)
        if "%market_vol" in dataframe.columns:
            market_vol = last["%market_vol"]
            if pd.notna(market_vol):
                if market_vol < 0.02:
                    vol_regime = "low"
                elif market_vol > 0.05:
                    vol_regime = "high"
                else:
                    vol_regime = "med"
            else:
                vol_regime = "med"
        else:
            # Fallback: use pair's own volatility
            returns = dataframe["close"].pct_change()
            vol = returns.rolling(48).std().iloc[-1]
            if pd.notna(vol):
                vol_regime = "low" if vol < 0.02 else ("high" if vol > 0.05 else "med")
            else:
                vol_regime = "med"

        # 2. Pair trend strength
        if "ema_50" in dataframe.columns:
            ema50 = last["ema_50"]
            close = last["close"]
            if pd.notna(ema50) and ema50 > 0:
                trend = (close - ema50) / ema50
                if trend < -0.02:
                    trend_regime = "down"
                elif trend > 0.02:
                    trend_regime = "up"
                else:
                    trend_regime = "flat"
            else:
                trend_regime = "flat"
        else:
            trend_regime = "flat"

        # 3. Time of day
        try:
            if hasattr(last.name, 'hour'):
                hour = last.name.hour
            else:
                hour = datetime.now().hour

            if hour < 8:
                time_regime = "morning"
            elif hour < 16:
                time_regime = "day"
            else:
                time_regime = "evening"
        except Exception:
            time_regime = "day"

        # Construct context key
        context = f"vol_{vol_regime}_trend_{trend_regime}_hour_{time_regime}"
        return context

    def select_strategy(self, context: str, pair: str) -> str:
        """
        Epsilon-greedy strategy selection

        Returns:
            "lea" or "hybrid"
        """
        epsilon = self.selector.get("epsilon", 0.1)

        # Exploration: random choice (10% of time)
        if np.random.random() < epsilon:
            selected = np.random.choice(["lea", "hybrid"])
            logger.debug(f"[{pair}] EXPLORE: Selected {selected} (ε={epsilon:.2f})")
            return selected

        # Exploitation: choose best Q-value
        if context not in self.selector["contexts"]:
            # Unknown context: default to conservative LEA
            logger.debug(f"[{pair}] Unknown context '{context}', defaulting to LEA")
            return "lea"

        ctx_data = self.selector["contexts"][context]

        # Get Q-values for each strategy
        lea_q = ctx_data.get("LeaFreqAIStrategy", {}).get("q_value", 0.0)
        hybrid_q = ctx_data.get("HybridAIStrategy", {}).get("q_value", 0.0)

        # Select best
        if lea_q >= hybrid_q:
            selected = "lea"
            logger.debug(f"[{pair}] EXPLOIT: LEA (Q={lea_q:.4f}) > Hybrid (Q={hybrid_q:.4f})")
        else:
            selected = "hybrid"
            logger.debug(f"[{pair}] EXPLOIT: Hybrid (Q={hybrid_q:.4f}) > LEA (Q={lea_q:.4f})")

        return selected

    def log_selection(self, pair: str, context: str, strategy: str):
        """
        Log strategy selection for offline learning

        Creates a JSONL file with selections that meta_learner.py can use
        to correlate with trade outcomes.
        """
        log_file = Path("user_data/bandit_selections.jsonl")

        entry = {
            "timestamp": datetime.now().isoformat(),
            "pair": pair,
            "context": context,
            "strategy": strategy
        }

        try:
            with open(log_file, "a") as f:
                f.write(json.dumps(entry) + "\n")
        except Exception as e:
            logger.error(f"Failed to log selection: {e}")

    # ========================================================================
    # FREQAI FEATURE ENGINEERING (same as LeaFreqAIStrategy)
    # ========================================================================

    def feature_engineering_expand_all(
        self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
    ) -> DataFrame:
        """Create stationary features for FreqAI"""
        # Price returns (stationary)
        dataframe[f"%ret_1"] = dataframe["close"].pct_change(1)
        dataframe[f"%ret_3"] = dataframe["close"].pct_change(3)
        dataframe[f"%ret_12"] = dataframe["close"].pct_change(12)

        # Volatility (ATR-based, relative)
        dataframe["atr14"] = ta.ATR(dataframe, timeperiod=14)
        dataframe[f"%atr14_rel"] = dataframe["atr14"] / dataframe["close"]

        # Range (stationary)
        dataframe[f"%rng_24"] = (
            dataframe["high"].rolling(24).max() - dataframe["low"].rolling(24).min()
        ) / dataframe["close"]

        # Z-score (mean reversion)
        returns = dataframe["close"].pct_change()
        dataframe[f"%z_48"] = (returns - returns.rolling(48).mean()) / returns.rolling(48).std()

        # Volume indicators
        dataframe[f"%vol_z_48"] = (
            (dataframe["volume"] - dataframe["volume"].rolling(48).mean())
            / dataframe["volume"].rolling(48).std()
        )

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

        # MACD
        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]
        dataframe["macdhist"] = macd["macdhist"]

        # Bollinger Bands
        from technical import qtpylib
        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
        dataframe["bb_lowerband"] = bollinger["lower"]
        dataframe["bb_middleband"] = bollinger["mid"]
        dataframe["bb_upperband"] = bollinger["upper"]
        dataframe["%bb_width"] = (
            (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]
        )

        # EMAs for trend
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)

        return dataframe

    def feature_engineering_expand_basic(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        """Basic feature engineering for main timeframe"""
        dataframe = self.feature_engineering_expand_all(dataframe, period=1, metadata=metadata)
        return dataframe

    def feature_engineering_standard(
        self, dataframe: DataFrame, metadata: dict, **kwargs
    ) -> DataFrame:
        """Market regime features (BTC correlation)"""
        # Only add BTC features if this is NOT the BTC pair itself
        if metadata.get("pair") != "BTC/USDT" and self.dp:
            btc_dataframe = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe=self.timeframe)
            if not btc_dataframe.empty and len(btc_dataframe) > 50:
                # BTC trend strength
                btc_ema = ta.EMA(btc_dataframe["close"], timeperiod=50)
                btc_trend = (btc_dataframe["close"] - btc_ema) / btc_ema

                # Market volatility
                btc_vol = btc_dataframe["close"].pct_change().rolling(48).std()

                # Add to dataframe with proper alignment
                dataframe["%btc_trend"] = btc_trend.reindex(dataframe.index, method='ffill')
                dataframe["%market_vol"] = btc_vol.reindex(dataframe.index, method='ffill')
        else:
            # For BTC pair or if data unavailable
            dataframe["%btc_trend"] = 0.0
            dataframe["%market_vol"] = dataframe["close"].pct_change().rolling(48).std()

        return dataframe

    def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
        """
        Define prediction target
        Target: Future return over next 12 candles (1 hour at 5m)
        """
        dataframe["&-target"] = dataframe["close"].shift(-12).pct_change(12)
        return dataframe

    # ========================================================================
    # INDICATOR POPULATION (FreqAI predictions)
    # ========================================================================

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Populate FreqAI predictions
        Both strategies use the same predictions, they differ only in entry/exit logic
        """
        # Run FreqAI
        dataframe = self.freqai.start(dataframe, metadata, self)

        # Add indicators needed for entry/exit logic
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)

        # MACD (for Hybrid strategy)
        macd = ta.MACD(dataframe)
        dataframe["macd"] = macd["macd"]
        dataframe["macdsignal"] = macd["macdsignal"]

        return dataframe

    # ========================================================================
    # ENTRY LOGIC (selects strategy, then applies its logic)
    # ========================================================================

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Select strategy based on context, then apply its entry logic
        """
        pair = metadata["pair"]

        # Check if predictions available
        if "&-target" not in dataframe.columns:
            logger.warning(f"[{pair}] No FreqAI predictions, no entries")
            dataframe["enter_long"] = 0
            return dataframe

        # Extract context
        context = self.get_context(dataframe, pair)

        # Select strategy (epsilon-greedy)
        selected = self.select_strategy(context, pair)

        # Store selection for exit logic
        self.pair_strategy_map[pair] = selected

        # Log selection
        self.log_selection(pair, context, selected)

        # Apply selected strategy's entry logic
        if selected == "lea":
            dataframe = self._populate_entry_lea(dataframe, metadata, context)
        else:
            dataframe = self._populate_entry_hybrid(dataframe, metadata, context)

        return dataframe

    def _populate_entry_lea(
        self, dataframe: DataFrame, metadata: dict, context: str
    ) -> DataFrame:
        """
        LEA strategy entry logic
        Conservative: tight filters, high confidence threshold
        """
        conditions = []

        # ML prediction must be positive (0.5% threshold)
        conditions.append(dataframe["&-target"] > 0.005)

        # DI filter (if available)
        if "do_predict" in dataframe.columns:
            conditions.append(dataframe["do_predict"] == 1)

        # Trend: price above 50 EMA
        conditions.append(dataframe["close"] > dataframe["ema_50"])

        # RSI: avoid overbought
        conditions.append(dataframe["rsi"] < 70)

        # Volume: above average
        conditions.append(dataframe["volume"] > dataframe["volume"].rolling(20).mean())

        # Combine
        if conditions:
            dataframe.loc[reduce(lambda x, y: x & y, conditions), "enter_long"] = 1

        # Tag with strategy and context for tracking
        dataframe["enter_tag"] = f"lea_bandit_ctx_{context}"

        return dataframe

    def _populate_entry_hybrid(
        self, dataframe: DataFrame, metadata: dict, context: str
    ) -> DataFrame:
        """
        Hybrid strategy entry logic
        Aggressive: more lenient filters, lower threshold
        """
        conditions = []

        # ML prediction (lower threshold: 0.1%)
        conditions.append(dataframe["&-target"] > 0.001)

        # Trend: EMA 50 above EMA 200 (broader uptrend)
        conditions.append(dataframe["ema_50"] > dataframe["ema_200"])

        # RSI: not overbought
        conditions.append(dataframe["rsi"] < 70)

        # MACD: bullish
        conditions.append(dataframe["macd"] > dataframe["macdsignal"])

        # BTC trend filter (if available)
        if "%btc_trend" in dataframe.columns:
            conditions.append(dataframe["%btc_trend"] > -0.05)

        # Volume
        conditions.append(dataframe["volume"] > 0)

        # Combine
        if conditions:
            dataframe.loc[reduce(lambda x, y: x & y, conditions), "enter_long"] = 1

        # Tag
        dataframe["enter_tag"] = f"hybrid_bandit_ctx_{context}"

        return dataframe

    # ========================================================================
    # EXIT LOGIC (uses same strategy as entry)
    # ========================================================================

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Exit logic based on which strategy was used for entry
        """
        pair = metadata["pair"]

        # Check predictions
        if "&-target" not in dataframe.columns:
            dataframe["exit_long"] = 0
            return dataframe

        # Use same strategy as entry (stored in map)
        selected = self.pair_strategy_map.get(pair, "lea")

        if selected == "lea":
            dataframe = self._populate_exit_lea(dataframe, metadata)
        else:
            dataframe = self._populate_exit_hybrid(dataframe, metadata)

        return dataframe

    def _populate_exit_lea(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        LEA exit: only on strong negative prediction
        (ROI and stoploss handle most exits)
        """
        dataframe.loc[dataframe["&-target"] < -0.004, "exit_long"] = 1
        return dataframe

    def _populate_exit_hybrid(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Hybrid exit: negative prediction OR technical signals
        """
        # AI exit
        ai_exit = dataframe["&-target"] < -0.001

        # MACD bearish
        macd_exit = dataframe["macd"] < dataframe["macdsignal"]

        # RSI overbought
        rsi_exit = dataframe["rsi"] > 80

        # Combine (OR logic)
        dataframe.loc[ai_exit | macd_exit | rsi_exit, "exit_long"] = 1

        return dataframe

    # ========================================================================
    # TRADE CONFIRMATION (uses selected strategy's logic)
    # ========================================================================

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time,
        entry_tag,
        side: str,
        **kwargs
    ) -> bool:
        """Final trade confirmation"""
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1]

        # Check predictions
        if "&-target" not in dataframe.columns:
            return False

        # Determine which strategy was selected (from entry_tag)
        if "lea" in entry_tag.lower():
            # LEA confirmation: stricter
            if last_candle["&-target"] <= 0.005:
                return False

            # Confirm uptrend
            if last_candle["close"] <= last_candle["ema_50"]:
                return False

        else:
            # Hybrid confirmation: more lenient
            if last_candle["&-target"] <= 0.0005:
                return False

        # Volume check (both strategies)
        avg_volume = dataframe["volume"].rolling(20).mean().iloc[-1]
        if last_candle["volume"] < avg_volume * 0.3:
            return False

        return True
