# source: https://raw.githubusercontent.com/aminak58/freqai-futures-strategy/51e416f59663c2e2f6dcda726c3765ad5886da59/user_data/strategies/FreqAIHybridStrategy.py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np
import pandas as pd
from pandas import DataFrame
from datetime import datetime
from typing import Optional, Union
from functools import reduce

from freqtrade.strategy import (
    IStrategy,
    Trade,
    Order,
    PairLocks,
    informative,
    DecimalParameter,
    IntParameter,
    CategoricalParameter
)

# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import pandas_ta as pta
from technical import qtpylib


class Github_aminak58_freqai_futures_strategy__FreqAIHybridStrategy__20251012_231904(IStrategy):
    """
    Hybrid Futures Leverage Strategy with FreqAI
    - Market Regime Detection (Situation Awareness)
    - Dynamic Indicator Windows
    - Multi-Model Ensemble Support
    - RL Agent Ready
    - LONG/SHORT Trading for Futures with Leverage
    
    Author: Strategy Team
    Version: 1.0.0 MVP (Futures)
    """
    
    INTERFACE_VERSION = 3
    
    # Optimal timeframe for the strategy
    timeframe = '5m'
    
    # Can this strategy go short?
    can_short: bool = True
    
    # Startup candle count
    startup_candle_count: int = 200
    
    # ROI table - Dynamic based on predictions
    minimal_roi = {
        "0": 0.10,
        "10": 0.05,
        "30": 0.02,
        "60": 0.01,
    }
    
    # Stoploss
    stoploss = -0.05
    
    # Trailing stop
    trailing_stop = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02
    trailing_only_offset_is_reached = True
    
    # Hyperopt parameters
    buy_di_threshold = DecimalParameter(0.0, 1.0, default=0.5, space='buy', optimize=True)
    sell_di_threshold = DecimalParameter(0.0, 1.0, default=0.5, space='sell', optimize=True)
    
    # Market regime thresholds
    trend_threshold = DecimalParameter(0.001, 0.01, default=0.005, space='buy', optimize=True)
    volatility_threshold = DecimalParameter(0.5, 2.0, default=1.0, space='buy', optimize=True)
    
    # Process only new candles
    process_only_new_candles = True
    
    # Plot config
    plot_config = {
        'main_plot': {
            'tema': {},
        },
        'subplots': {
            "Regime": {
                'regime': {'color': 'blue'},
            },
            "Predictions": {
                '&-prediction': {'color': 'green'},
                'do_predict': {'color': 'red'},
            }
        }
    }
    
    def informative_pairs(self):
        """
        Define additional informative pairs
        """
        whitelist_pairs = self.dp.current_whitelist()
        corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
        informative_pairs = []
        
        for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]:
            for pair in whitelist_pairs:
                informative_pairs.append((pair, tf))
            for pair in corr_pairs:
                if pair in whitelist_pairs:
                    continue
                informative_pairs.append((pair, tf))
        
        return informative_pairs
    
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Main indicator population - FreqAI will be called here
        """
        # Call FreqAI
        dataframe = self.freqai.start(dataframe, metadata, self)
        
        # Add some basic indicators for strategy logic (not for FreqAI)
        dataframe['ema_50'] = ta.EMA(dataframe, timeperiod=50)
        dataframe['ema_200'] = ta.EMA(dataframe, timeperiod=200)
        
        # Replace inf/nan with 0 for all columns
        dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
        dataframe = dataframe.fillna(0)
        
        return dataframe
    
    # ============ FreqAI Feature Engineering ============
    
    def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame:
        """
        Features that will be auto-expanded based on:
        - indicator_periods_candles
        - include_timeframes  
        - include_shifted_candles
        - include_corr_pairlist
        
        This function is called once per period defined in config
        """
        # Price-based features
        dataframe[f"%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
        dataframe[f"%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
        dataframe[f"%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
        dataframe[f"%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
        dataframe[f"%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
        
        # Momentum indicators
        dataframe[f"%-mom-period"] = ta.MOM(dataframe, timeperiod=period)
        dataframe[f"%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
        
        # Volatility
        bollinger = ta.BBANDS(dataframe, timeperiod=period, nbdevup=2.0, nbdevdn=2.0)
        dataframe[f"%-bb_lowerband-period"] = bollinger['lowerband']
        dataframe[f"%-bb_middleband-period"] = bollinger['middleband']
        dataframe[f"%-bb_upperband-period"] = bollinger['upperband']
        # Handle division by zero
        dataframe[f"%-bb_width-period"] = np.where(
            bollinger['middleband'] != 0,
            (bollinger['upperband'] - bollinger['lowerband']) / bollinger['middleband'],
            0
        )
        
        # ATR for volatility
        dataframe[f"%-atr-period"] = ta.ATR(dataframe, timeperiod=period)
        
        # MACD
        macd = ta.MACD(dataframe, fastperiod=int(period/2), slowperiod=period, signalperiod=int(period/3))
        dataframe[f"%-macd-period"] = macd['macd']
        dataframe[f"%-macdsignal-period"] = macd['macdsignal']
        dataframe[f"%-macdhist-period"] = macd['macdhist']
        
        return dataframe
    
    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
        """
        Features that will be expanded based on:
        - include_timeframes
        - include_shifted_candles  
        - include_corr_pairlist
        
        NOT expanded by indicator_periods_candles
        """
        # Price change features
        dataframe["%-pct-change"] = dataframe["close"].pct_change()
        dataframe["%-raw_volume"] = dataframe["volume"]
        dataframe["%-raw_price"] = dataframe["close"]
        
        # Price volatility (rolling std)
        dataframe["%-volatility"] = dataframe["close"].rolling(window=20).std()
        
        # Volume features
        dataframe["%-volume_mean_20"] = dataframe["volume"].rolling(window=20).mean()
        dataframe["%-volume_std_20"] = dataframe["volume"].rolling(window=20).std()
        
        return dataframe
    
    def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
        """
        Features that are NOT auto-expanded
        Use this for custom features that should appear only once
        
        This is where we add Market Regime Detection (Situation Awareness)
        """
        # Time-based features
        dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
        dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
        
        # ========== MARKET REGIME DETECTION ==========
        
        # Trend detection (EMA crossover based)
        ema_short = ta.EMA(dataframe, timeperiod=20)
        ema_long = ta.EMA(dataframe, timeperiod=50)
        # Handle division by zero
        dataframe["%-trend_strength"] = np.where(
            ema_long != 0,
            (ema_short - ema_long) / ema_long,
            0
        )
        
        # Volatility regime (ATR normalized)
        atr_20 = ta.ATR(dataframe, timeperiod=20)
        # Handle division by zero
        dataframe["%-volatility_regime"] = np.where(
            dataframe["close"] != 0,
            atr_20 / dataframe["close"],
            0
        )
        
        # Volume regime
        volume_ma = dataframe["volume"].rolling(window=20).mean()
        # Handle division by zero
        dataframe["%-volume_regime"] = np.where(
            volume_ma != 0,
            dataframe["volume"] / volume_ma,
            1
        )
        
        # Market regime classification
        # 0 = Range, 1 = Trending Up, 2 = Trending Down, 3 = High Volatility
        dataframe["%-market_regime"] = 0  # Default: Range
        
        trend_up = dataframe["%-trend_strength"] > self.trend_threshold.value
        trend_down = dataframe["%-trend_strength"] < -self.trend_threshold.value
        high_vol = dataframe["%-volatility_regime"] > self.volatility_threshold.value * 0.02
        
        dataframe.loc[trend_up & ~high_vol, "%-market_regime"] = 1  # Trending Up
        dataframe.loc[trend_down & ~high_vol, "%-market_regime"] = 2  # Trending Down
        dataframe.loc[high_vol, "%-market_regime"] = 3  # High Volatility
        
        # Regime indicators for different time horizons
        dataframe["%-regime_short"] = dataframe["%-market_regime"].rolling(window=10).mean()
        dataframe["%-regime_medium"] = dataframe["%-market_regime"].rolling(window=50).mean()
        dataframe["%-regime_long"] = dataframe["%-market_regime"].rolling(window=200).mean()
        
        return dataframe
    
    def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
        """
        Define prediction targets for the model
        
        We use multiple targets for ensemble predictions
        """
        # Target 1: Future price change (main target)
        future_close = (
            dataframe["close"]
            .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
            .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
            .mean()
        )
        # Handle division by zero
        dataframe["&-s_close"] = np.where(
            dataframe["close"] != 0,
            (future_close / dataframe["close"]) - 1,
            0
        )
        
        # Target 2: Future volatility (for risk management)
        future_volatility = (
            dataframe["close"]
            .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
            .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
            .std()
        )
        # Handle division by zero
        dataframe["&-s_volatility"] = np.where(
            dataframe["close"] != 0,
            future_volatility / dataframe["close"],
            0
        )
        
        # Target 3: Future volume surge (for confirmation)
        future_volume = (
            dataframe["volume"]
            .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
            .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
            .mean()
        )
        # Handle division by zero
        dataframe["&-s_volume"] = np.where(
            dataframe["volume"] != 0,
            (future_volume / dataframe["volume"]) - 1,
            0
        )
        
        # Clean up inf/nan in targets
        dataframe = dataframe.replace([np.inf, -np.inf], np.nan)
        dataframe["&-s_close"] = dataframe["&-s_close"].fillna(0)
        dataframe["&-s_volatility"] = dataframe["&-s_volatility"].fillna(0)
        dataframe["&-s_volume"] = dataframe["&-s_volume"].fillna(0)
        
        return dataframe
    
    # ============ Entry/Exit Logic ============
    
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Entry signal based on FreqAI predictions and regime awareness
        Supports both LONG and SHORT entries for leveraged futures trading
        """
        # Dynamic thresholds based on market regime
        dataframe['regime'] = dataframe.get('%-market_regime', 0)
        
        # Calculate dynamic thresholds based on prediction statistics
        dataframe['target_roi'] = (
            dataframe["&-s_close_mean"] + 
            dataframe["&-s_close_std"] * 1.25
        )
        dataframe['target_loss'] = (
            dataframe["&-s_close_mean"] - 
            dataframe["&-s_close_std"] * 1.25
        )
        
        # LONG Entry conditions
        long_conditions = []
        long_conditions.append(dataframe["&-s_close"] > dataframe['target_roi'])
        long_conditions.append(dataframe["do_predict"] == 1)
        long_conditions.append(dataframe["DI_values"] < self.buy_di_threshold.value)
        long_conditions.append(dataframe['regime'] != 3)  # Avoid high volatility
        long_conditions.append(dataframe["&-s_volume"] > 0)
        
        if long_conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, long_conditions),
                'enter_long'] = 1
        
        # SHORT Entry conditions (inverse logic)
        short_conditions = []
        short_conditions.append(dataframe["&-s_close"] < dataframe['target_loss'])
        short_conditions.append(dataframe["do_predict"] == 1)
        short_conditions.append(dataframe["DI_values"] < self.sell_di_threshold.value)
        short_conditions.append(dataframe['regime'] != 3)  # Avoid high volatility
        short_conditions.append(dataframe["&-s_volume"] > 0)
        
        if short_conditions:
            dataframe.loc[
                reduce(lambda x, y: x & y, short_conditions),
                'enter_short'] = 1
        
        return dataframe
    
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Exit signal based on FreqAI predictions
        """
        # Calculate dynamic exit thresholds
        dataframe['sell_roi'] = (
            dataframe["&-s_close_mean"] - 
            dataframe["&-s_close_std"] * 1.25
        )
        dataframe['buy_roi'] = (
            dataframe["&-s_close_mean"] + 
            dataframe["&-s_close_std"] * 1.25
        )
        
        # LONG exit conditions
        long_exit_conditions = []
        long_exit_conditions.append(dataframe["&-s_close"] < dataframe['sell_roi'])
        long_exit_conditions.append(dataframe["do_predict"] == 1)
        long_exit_conditions.append(dataframe.get('regime', 0) == 3)
        
        if long_exit_conditions:
            dataframe.loc[
                reduce(lambda x, y: x | y, long_exit_conditions),
                'exit_long'] = 1
        
        # SHORT exit conditions (inverse of long)
        short_exit_conditions = []
        short_exit_conditions.append(dataframe["&-s_close"] > dataframe['buy_roi'])
        short_exit_conditions.append(dataframe["do_predict"] == 1)
        short_exit_conditions.append(dataframe.get('regime', 0) == 3)
        
        if short_exit_conditions:
            dataframe.loc[
                reduce(lambda x, y: x | y, short_exit_conditions),
                'exit_short'] = 1
        
        return dataframe
    
    # ============ Custom Methods ============
    
    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 based on market regime and model confidence
        - Conservative: 3x in normal markets
        - Moderate: 5x in trending markets
        - Safe: 2x in volatile markets
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) > 0:
            last_candle = dataframe.iloc[-1].squeeze()
            regime = last_candle.get('%-market_regime', 0)
            di_value = last_candle.get('DI_values', 1.0)
            
            # High volatility regime - use minimum leverage
            if regime == 3 or di_value > 1.5:
                return 2.0
            # Trending regime with good confidence
            elif regime in [1, 2] and di_value < 0.5:
                return 5.0
            # Normal regime
            else:
                return 3.0
        
        # Default conservative leverage
        return 3.0
    
    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, 
                   current_rate: float, current_profit: float, **kwargs):
        """
        Custom exit logic - can be used for advanced risk management
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()
        
        # Exit if entering high volatility regime with profit
        if last_candle.get('regime', 0) == 3 and current_profit > 0.01:
            return 'high_volatility_exit'
        
        # Exit if model confidence drops (high DI values)
        if last_candle.get('DI_values', 0) > 2.0:
            return 'low_confidence_exit'
        
        return None
