# source: https://raw.githubusercontent.com/maxakak1998/trading_bot/2f2b6f828fd9d8ec91172587cd835c95ca4d0f25/user_data/strategies/IndicatorStrategy.py
"""
Github_maxakak1998_trading_bot__IndicatorStrategy__20251223_154239 - Pure Rule-Based Strategy (No AI/FreqAI)

This strategy uses only technical indicators for entry/exit signals:
- SMC: Order Blocks (simplified)
- Momentum: RSI, MFI, ADX
- Volatility: ATR, Bollinger Bands
- Structure: EMA trend

NO AI MODEL - NO FREQAI - SIMPLE AND FAST
"""

from datetime import datetime
from typing import Optional
import logging
import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy, merge_informative_pair

logger = logging.getLogger(__name__)


class Github_maxakak1998_trading_bot__IndicatorStrategy__20251223_154239(IStrategy):
    """
    Pure Indicator-Based Strategy - No AI/FreqAI Required
    
    Entry based on confluence of:
    1. HTF Order Block testing
    2. Momentum (RSI, MFI)
    3. Trend direction (EMA alignment)
    4. Volatility confirmation (ATR breakout)
    5. MTF Confirmation (1h Trend)
    """
    
    INTERFACE_VERSION = 3
    
    # =====================================================
    # FIXED PARAMETERS (No Hyperopt needed)
    # =====================================================

    # ... (skipping unchanged params) ...

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Entry signals based on indicator confluence
        """
        # Calculate scores
        long_score = self.calculate_entry_score(dataframe, is_long=True)
        short_score = self.calculate_entry_score(dataframe, is_long=False)
        
        # Store for debugging
        dataframe['long_score'] = long_score
        dataframe['short_score'] = short_score
        
        # Basic filters
        has_volume = dataframe['volume'] > 0
        not_overbought = dataframe['rsi'] < self.rsi_overbought
        not_oversold = dataframe['rsi'] > self.rsi_oversold
        trending = dataframe['adx'] > self.adx_threshold
        
        # 1h Trend Filters (MTF)
        # Check if 1h columns exist (safe access)
        bullish_1h = (dataframe['trend_bullish_1h'] == 1) if 'trend_bullish_1h' in dataframe.columns else True
        bearish_1h = (dataframe['trend_bearish_1h'] == 1) if 'trend_bearish_1h' in dataframe.columns else True
        
        # LONG entry
        # STRICT TREND FILTER: 
        # 1. 5m Trend Bullish (Price > EMA50 > EMA200)
        # 2. 1h Trend Bullish (EMA50_1h > EMA200_1h) (MTF Confluence)
        dataframe.loc[
            has_volume & 
            not_overbought &
            trending &
            (dataframe['trend_bullish'] == 1) &  # 5m Trend
            bullish_1h &                         # 1h Trend (MTF)
            (long_score >= self.entry_score_threshold),
            'enter_long'
        ] = 1
        
        # SHORT entry
        # STRICT TREND FILTER: 
        # 1. 5m Trend Bearish
        # 2. 1h Trend Bearish (MTF Confluence)
        dataframe.loc[
            has_volume &
            not_oversold &
            trending &
            (dataframe['trend_bearish'] == 1) &  # 5m Trend
            bearish_1h &                         # 1h Trend (MTF)
            (short_score >= self.entry_score_threshold),
            'enter_short'
        ] = 1
        
        # Log entries
        long_entries = (dataframe['enter_long'] == 1).sum()
        short_entries = (dataframe['enter_short'] == 1).sum()
        logger.info(f"[{metadata['pair']}] LONG entries: {long_entries}, SHORT entries: {short_entries}")
        
        return dataframe
    
    # Entry threshold (score-based) - INCREASED for quality
    entry_score_threshold = 0.45  # Minimum score to enter (was 0.30)
    
    # RSI thresholds
    rsi_overbought = 70
    rsi_oversold = 30
    
    # ADX threshold for trending market
    adx_threshold = 25
    
    # Stoploss and ROI
    # Adjusted for 2x Leverage: 
    # Stake $50 * 2 = $100 Position
    # Risk $5 (1R) = 5% of Position
    stoploss = -0.10  # 10% stoploss (User Request)
    
    minimal_roi = {
        "0": 0.15,      # 15% target immediately
        "30": 0.10,     # 10% after 30 min
        "60": 0.05,     # 5% after 1 hour
        "120": 0.02     # 2% after 2 hours
    }
    
    # Trailing stop
    trailing_stop = True
    trailing_stop_positive = 0.02  # Start trailing at 2% profit
    trailing_stop_positive_offset = 0.04  # Trail by 4%
    trailing_only_offset_is_reached = True
    
    # Trading settings
    timeframe = '5m'
    can_short = True
    use_exit_signal = True
    exit_profit_only = False
    
    # Startup candles needed for indicators
    startup_candle_count = 200
    
    # Risk Management
    max_risk_per_trade = 0.20  # User accepted $10 risk on $50 stake = 20%
    
    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:
        """
        Option B: Force 2x Leverage
        - Enables BTC trading (Pos > $100)
        - Risk per trade ~ $10 (with 10% SL)
        """
        return 2.0

    def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
                        current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Use fixed stoploss from strategy config (-0.10)
        Trailing stop handles profit locking.
        """
        return -0.10

    def informative_pairs(self):
        """
        Request 1h data for Trend Confluence
        """
        pairs = self.dp.current_whitelist()
        informative_pairs = [(pair, '1h') for pair in pairs]
        return informative_pairs

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add all indicators to dataframe (including 1h confluence)
        """
        # ==================== 1h CONFLUENCE (SMC + Sonic R) ====================
        if self.dp:
            inf_tf = '1h'
            informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
            
            # 1. Sonic R (Dragon)
            # EMA 34 (High/Low/Close) - Dynamic Support/Resistance
            informative['ema_34_high'] = ta.EMA(informative['high'], timeperiod=34)
            informative['ema_34_low'] = ta.EMA(informative['low'], timeperiod=34)
            informative['ema_89'] = ta.EMA(informative['close'], timeperiod=89) # Baseline
            
            # 2. SMC (Swing High/Low) - Static Support/Resistance
            lookback = 20
            informative['swing_high'] = informative['high'].rolling(lookback).max()
            informative['swing_low'] = informative['low'].rolling(lookback).min()
            
            # 3. RSI 1h (Momemtum context)
            informative['rsi'] = ta.RSI(informative['close'], timeperiod=14)
            
            # Merge 1h into 5m
            dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)

        # ==================== 5m MOMENTUM ====================
        dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
        dataframe['mfi'] = ta.MFI(dataframe['high'], dataframe['low'], 
                                   dataframe['close'], dataframe['volume'], timeperiod=14)
        dataframe['adx'] = ta.ADX(dataframe['high'], dataframe['low'], 
                                   dataframe['close'], timeperiod=14)
        
        # ==================== 5m TREND ====================
        dataframe['ema_50'] = ta.EMA(dataframe['close'], timeperiod=50)
        dataframe['ema_200'] = ta.EMA(dataframe['close'], timeperiod=200)
        
        # Trend direction (5m)
        dataframe['trend_bullish'] = (
            (dataframe['close'] > dataframe['ema_50']) & 
            (dataframe['ema_50'] > dataframe['ema_200'])
        ).astype(int)
        dataframe['trend_bearish'] = (
            (dataframe['close'] < dataframe['ema_50']) & 
            (dataframe['ema_50'] < dataframe['ema_200'])
        ).astype(int)
        
        # ==================== VOLATILITY ====================
        dataframe['atr'] = ta.ATR(dataframe['high'], dataframe['low'], 
                                   dataframe['close'], timeperiod=14)
        dataframe['atr_pct'] = dataframe['atr'] / dataframe['close']
        
        # ATR breakout (volatility expanding)
        atr_ma = dataframe['atr'].rolling(20).mean()
        dataframe['atr_breakout'] = (dataframe['atr'] > atr_ma * 1.2).astype(int)
        
        # Bollinger Bands
        bb_upper, bb_middle, bb_lower = ta.BBANDS(
            dataframe['close'], timeperiod=20, nbdevup=2.0, nbdevdn=2.0
        )
        dataframe['bb_upper'] = bb_upper
        dataframe['bb_middle'] = bb_middle
        dataframe['bb_lower'] = bb_lower
        dataframe['bb_width'] = (bb_upper - bb_lower) / bb_middle
        
        # ==================== SIMPLIFIED SMC (5m) ====================
        # Swing highs/lows for support/resistance
        lookback = 10
        dataframe['swing_high'] = dataframe['high'].rolling(lookback).max()
        dataframe['swing_low'] = dataframe['low'].rolling(lookback).min()
        
        # "Order Block" proxy: Price near recent swing high/low with momentum reversal
        # Bullish OB: Price at support (near swing low) + green candle
        dataframe['testing_bull_ob'] = (
            (dataframe['close'] <= dataframe['swing_low'] * 1.02) &  # Near swing low
            (dataframe['close'] > dataframe['open'])  # Bullish candle
        ).astype(int)
        
        # Bearish OB: Price at resistance (near swing high) + red candle
        dataframe['testing_bear_ob'] = (
            (dataframe['close'] >= dataframe['swing_high'] * 0.98) &  # Near swing high
            (dataframe['close'] < dataframe['open'])  # Bearish candle
        ).astype(int)
        
        return dataframe
    
    def calculate_entry_score(self, dataframe: DataFrame, is_long: bool) -> pd.Series:
        """
        Calculate entry score based on indicator confluence (5m)
        Returns score from 0 to 1
        """
        score = pd.Series(0.0, index=dataframe.index)
        
        if is_long:
            # 1. Order Block (25%)
            if 'testing_bull_ob' in dataframe.columns:
                score += (dataframe['testing_bull_ob'] > 0).astype(float) * 0.25
            
            # 2. Trend alignment (25%)
            score += dataframe['trend_bullish'].astype(float) * 0.25
            
            # 3. Momentum (RSI > 40 but < 70) (20%)
            good_rsi = ((dataframe['rsi'] > 40) & (dataframe['rsi'] < 70)).astype(float)
            score += good_rsi * 0.20
            
            # 4. Volume/MFI confirmation (15%)
            score += (dataframe['mfi'] > 40).astype(float) * 0.15
            
            # 5. ATR breakout (volatility) (15%)
            score += dataframe['atr_breakout'].astype(float) * 0.15
            
        else:  # SHORT
            # 1. Order Block (25%)
            if 'testing_bear_ob' in dataframe.columns:
                score += (dataframe['testing_bear_ob'] > 0).astype(float) * 0.25
            
            # 2. Trend alignment (25%)
            score += dataframe['trend_bearish'].astype(float) * 0.25
            
            # 3. Momentum (RSI < 60 but > 30) (20%)
            good_rsi = ((dataframe['rsi'] < 60) & (dataframe['rsi'] > 30)).astype(float)
            score += good_rsi * 0.20
            
            # 4. Volume/MFI confirmation (15%)
            score += (dataframe['mfi'] < 60).astype(float) * 0.15
            
            # 5. ATR breakout (volatility) (15%)
            score += dataframe['atr_breakout'].astype(float) * 0.15
        
        return score
    
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Entry signals: 5m Confluence + 1h Zone Validation
        """
        # Calculate scores
        long_score = self.calculate_entry_score(dataframe, is_long=True)
        short_score = self.calculate_entry_score(dataframe, is_long=False)
        
        # Store for debugging
        dataframe['long_score'] = long_score
        dataframe['short_score'] = short_score
        
        # Basic filters
        has_volume = dataframe['volume'] > 0
        not_overbought = dataframe['rsi'] < self.rsi_overbought
        not_oversold = dataframe['rsi'] > self.rsi_oversold
        trending = dataframe['adx'] > self.adx_threshold
        
        # ==================== MTF ZONE FILTERS ====================
        # Determine if we are in a "Buy Zone" (Demand) or "Sell Zone" (Supply) on 1h
        
        if 'ema_34_low_1h' in dataframe.columns:
            # LONG ZONE:
            # 1. Sonic R: Price is testing EMA 34 Low (Dragon Belly) or above EMA 89 (Trend)
            #    Actually, "Testing" means Price is NEAR EMA 34.
            # 2. SMC: Price is within 2% of Swing Low 1h.
            
            # Simple Zone: Close is NOT extended.
            # "Buy the Dip": Close < EMA 34 High 1h? Or Close near Support.
            
            # FILTER 1: Trend Alignment (Price > EMA 89 1h) - OPTIONAL but good for Sonic R
            bullish_trend_1h = (dataframe['close'] > dataframe['ema_89_1h'])
            bearish_trend_1h = (dataframe['close'] < dataframe['ema_89_1h'])

            # FILTER 2: Value Zone (Pullback)
            # Long: Price should be close to EMA 34 Low OR Swing Low
            # Distance check: Within 2%
            dist_ema34_low = abs(dataframe['close'] - dataframe['ema_34_low_1h']) / dataframe['close']
            dist_swing_low = abs(dataframe['close'] - dataframe['swing_low_1h']) / dataframe['close']
            
            in_demand_zone = (
                (dist_ema34_low < 0.02) |      # Near Dynamic Support
                (dist_swing_low < 0.02)        # Near Static Support
            )
            
            # Short: Price close to EMA 34 High OR Swing High
            dist_ema34_high = abs(dataframe['close'] - dataframe['ema_34_high_1h']) / dataframe['close']
            dist_swing_high = abs(dataframe['close'] - dataframe['swing_high_1h']) / dataframe['close']
            
            in_supply_zone = (
                (dist_ema34_high < 0.02) |
                (dist_swing_high < 0.02)
            )
            
            # Combine Filters
            # Require Trend Alignment + Value Zone (Pullback) to enter?
            # Or just Value Zone? User wants "Important Zones".
            # Let's use strict: Must be in Zone.
            
            is_valid_long_zone = in_demand_zone & bullish_trend_1h
            is_valid_short_zone = in_supply_zone & bearish_trend_1h
            
        else:
            is_valid_long_zone = True
            is_valid_short_zone = True
        
        # LONG entry
        dataframe.loc[
            has_volume & 
            not_overbought &
            trending &
            (dataframe['trend_bullish'] == 1) &  # 5m Trigger
            is_valid_long_zone &                 # 1h Zone Test (Pullback)
            (long_score >= self.entry_score_threshold),
            'enter_long'
        ] = 1
        
        # SHORT entry
        dataframe.loc[
            has_volume &
            not_oversold &
            trending &
            (dataframe['trend_bearish'] == 1) &  # 5m Trigger
            is_valid_short_zone &                # 1h Zone Test (Pullback)
            (short_score >= self.entry_score_threshold),
            'enter_short'
        ] = 1
        
        logger.info(f"[{metadata['pair']}] LONG: {(dataframe['enter_long']==1).sum()}, SHORT: {(dataframe['enter_short']==1).sum()}")
        
        return dataframe
    
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Exit signals: Signs of Reversal
        """
        # Exit LONG when logic tells us Reversal:
        # - RSI overbought (>80) is a sign
        # - Trend turns bearish (Price crosses below EMA)
        
        dataframe.loc[
            (dataframe['rsi'] > 80) |  # Extreme Overbought
            (dataframe['trend_bearish'] == 1), # Trend flipped to Bearish
            'exit_long'
        ] = 1
        
        # Exit SHORT when:
        # - RSI oversold (<20)
        # - Trend turns bullish
        
        dataframe.loc[
             (dataframe['rsi'] < 20) | # Extreme Oversold
             (dataframe['trend_bullish'] == 1), # Trend flipped to Bullish
            'exit_short'
        ] = 1
        
        return dataframe
