# technical_analysis.py

import asyncio
import logging
import time
from typing import Dict, List, Tuple
from datetime import datetime, timedelta

import pandas as pd
import numpy as np
from ta.momentum import (
    RSIIndicator,
    StochasticOscillator,
    AroonOscillator,
    MoneyFlowIndexIndicator,
    RelativeStrengthIndexIndicator,
    TSIIndicator,
)
from ta.trend import (
    ADXIndicator,
    CCIIndicator,
    MACD,
    SMAIndicator,
    EMAIndicator,
    IchimokuIndicator,
    KeltnerChannel,
    VortexIndicator,
)
from ta.volatility import BollingerBands, AverageTrueRange
from ta.volume import (
    OnBalanceVolumeIndicator,
    ChaikinMoneyFlowIndicator,
    AccDistIndexIndicator,
    EaseOfMovementIndicator,
    MFIIndicator,
)
from functools import wraps
from async_timeout import timeout

# Django setup
import os
import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "binancebot.settings")
django.setup()

from .models import Kline, Signal, TradingPair  # Adjust the import according to your project structure

# Logger setup with enhanced formatting
logger = logging.getLogger(__name__)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Global settings for retry mechanism and circuit breaker
MAX_RETRIES = 5
CIRCUIT_BREAKER_THRESHOLD = 3  # Number of consecutive failures before opening the circuit
CIRCUIT_BREAKER_TIMEOUT = 60  # Time in seconds to keep the circuit open

def retry_with_exponential_backoff(coroutine):
    """
    Decorator for retrying a coroutine with exponential backoff.
    """
    @wraps(coroutine)
    async def wrapper(*args, **kwargs):
        retries = 0
        delay = 1
        while retries < MAX_RETRIES:
            try:
                result = await coroutine(*args, **kwargs)
                return result
            except Exception as e:
                retries += 1
                logger.warning(f"Retry {retries}/{MAX_RETRIES} for {coroutine.__name__}: {e}")
                await asyncio.sleep(delay)
                delay *= 2  # Exponential backoff
        logger.error(f"Max retries exceeded for {coroutine.__name__}")
        raise Exception(f"Max retries exceeded for {coroutine.__name__}")
    return wrapper

class CircuitBreaker:
    """
    A simple circuit breaker implementation.
    """
    failure_count = 0
    circuit_open = False
    last_failure_time = None

    @staticmethod
    def call(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            if CircuitBreaker.circuit_open:
                elapsed_time = time.time() - CircuitBreaker.last_failure_time
                if elapsed_time > CIRCUIT_BREAKER_TIMEOUT:
                    CircuitBreaker.circuit_open = False
                    CircuitBreaker.failure_count = 0
                    logger.info("Circuit breaker closed, resuming operation.")
                else:
                    logger.error("Circuit breaker is open. Call rejected.")
                    raise Exception("Circuit breaker is open.")
            try:
                result = await func(*args, **kwargs)
                CircuitBreaker.failure_count = 0  # Reset on success
                return result
            except Exception as e:
                CircuitBreaker.failure_count += 1
                CircuitBreaker.last_failure_time = time.time()
                if CircuitBreaker.failure_count >= CIRCUIT_BREAKER_THRESHOLD:
                    CircuitBreaker.circuit_open = True
                    logger.error("Circuit breaker opened due to repeated failures.")
                raise e
        return wrapper

class CryptoBot:
    def __init__(
        self,
        timeframes: List[str] = ["1m", "5m", "15m", "1h"],
        num_candles: int = 500,
    ):
        self.timeframes = timeframes
        self.num_candles = num_candles
        logger.info(
            "CryptoBot initialized with timeframes: %s and num_candles: %d",
            timeframes,
            num_candles,
        )

    @retry_with_exponential_backoff
    async def get_candles_df(self, pair: str, timeframe: str) -> pd.DataFrame:
        """
        Fetches the most recent `num_candles` records for the specified trading pair and timeframe.
        Includes data preprocessing to handle missing values and outliers.
        """
        try:
            klines = (
                Kline.objects.filter(symbol=pair, timeframe=timeframe)
                .order_by("-timestamp")[: self.num_candles]
                .values()
            )
            df = pd.DataFrame.from_records(klines)
            df.set_index("timestamp", inplace=True)
            df.sort_index(inplace=True)  # Ensure chronological order for analysis

            # Data preprocessing
            df.replace([np.inf, -np.inf], np.nan, inplace=True)
            df.fillna(method="ffill", inplace=True)  # Forward-fill missing values
            df.dropna(inplace=True)  # Drop any remaining NaNs

            # Outlier detection and handling using Z-score
            z_scores = ((df - df.mean()) / df.std()).abs()
            df = df[(z_scores < 3).all(axis=1)]

            return df
        except Exception as e:
            logger.error(f"Error fetching candles for {pair} ({timeframe}): {e}")
            raise

    async def calculate_technical_indicators_set1(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Calculates the first set of technical indicators and adds them to the DataFrame.
        """
        try:
            # First set of indicators
            df["rsi"] = RSIIndicator(close=df["close"], window=14).rsi()
            macd = MACD(
                close=df["close"], window_slow=26, window_fast=12, window_sign=9
            )
            df["macd"] = macd.macd()
            df["macd_signal"] = macd.macd_signal()
            df["sma_fast"] = SMAIndicator(close=df["close"], window=14).sma_indicator()
            df["sma_slow"] = SMAIndicator(close=df["close"], window=28).sma_indicator()
            bollinger = BollingerBands(
                close=df["close"], window=20, window_dev=2
            )
            df["bollinger_mavg"] = bollinger.bollinger_mavg()
            df["bollinger_hband"] = bollinger.bollinger_hband()
            df["bollinger_lband"] = bollinger.bollinger_lband()
            # Additional preprocessing
            df.replace([np.inf, -np.inf], np.nan, inplace=True)
            df.fillna(method="ffill", inplace=True)
            df.dropna(inplace=True)
            return df
        except Exception as e:
            logger.error(f"Error calculating indicators set 1: {e}")
            raise

    async def calculate_technical_indicators_set2(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Calculates the second set of technical indicators and adds them to the DataFrame.
        """
        try:
            # Second set of indicators
            df["mfi"] = MoneyFlowIndexIndicator(
                high=df["high"], low=df["low"], close=df["close"], volume=df["volume"], window=14
            ).money_flow_index()
            df["rvi"] = self.calculate_rvi(df)
            keltner = KeltnerChannel(
                high=df["high"], low=df["low"], close=df["close"], window=20
            )
            df["keltner_mavg"] = keltner.keltner_channel_mband()
            df["keltner_hband"] = keltner.keltner_channel_hband()
            df["keltner_lband"] = keltner.keltner_channel_lband()
            df["adx"] = ADXIndicator(
                high=df["high"], low=df["low"], close=df["close"], window=14
            ).adx()
            # Additional preprocessing
            df.replace([np.inf, -np.inf], np.nan, inplace=True)
            df.fillna(method="ffill", inplace=True)
            df.dropna(inplace=True)
            return df
        except Exception as e:
            logger.error(f"Error calculating indicators set 2: {e}")
            raise

    def calculate_rvi(self, df: pd.DataFrame) -> pd.Series:
        """
        Calculates the Relative Vigor Index (RVI).
        """
        close_open = df["close"] - df["open"]
        high_low = df["high"] - df["low"]
        numerator = close_open.rolling(window=10).mean()
        denominator = high_low.rolling(window=10).mean()
        rvi = numerator / denominator
        return rvi

    @CircuitBreaker.call
    async def generate_signal(
        self, pair: str, timeframe: str, higher_timeframe: str = "1h"
    ) -> Tuple[str, Dict[str, float]]:
        try:
            df = await self.get_candles_df(pair, timeframe)
            if df.empty:
                return "No signal", {}

            df_set1 = await self.calculate_technical_indicators_set1(df.copy())
            df_set2 = await self.calculate_technical_indicators_set2(df.copy())

            # Get higher timeframe data for trend confirmation
            higher_df = await self.get_candles_df(pair, higher_timeframe)
            if higher_df.empty:
                return "No signal", {}

            higher_df = await self.calculate_technical_indicators_set1(higher_df)

            current_price = df["close"].iloc[-1]
            current_time = pd.Timestamp.now()

            # Calculate 24h change
            change_24h = self.calculate_24h_change(df)

            # Generate signals from both sets
            signal1, reasons1 = self.generate_signal_from_set1(df_set1, higher_df)
            signal2, reasons2 = self.generate_signal_from_set2(df_set2, higher_df)

            # Combine signals
            combined_signal, success_rate = self.combine_signals(signal1, signal2)

            # Calculate support and resistance
            support_level, resistance_level = self.calculate_support_and_resistance(df)

            # Calculate stop-loss and take-profit
            stop_loss, take_profit = self.calculate_stop_loss_take_profit(
                combined_signal, current_price, df
            )

            # Determine the result (Won, Lost, Pending)
            result = "Pending"  # Since real-time price movement is needed

            metrics = {
                "time_generated": current_time.strftime("%Y-%m-%d %H:%M:%S"),
                "timestamp": df.index[-1],
                "symbol": pair,
                "price": current_price,
                "change_24h": change_24h,
                "signal": combined_signal,
                "stop_loss": stop_loss,
                "take_profit": take_profit,
                "success_rate": success_rate,
                "result": result,
                "reasons": reasons1 + reasons2,
            }

            logger.info(
                f"Generated signal for {pair} ({timeframe}): {combined_signal} (Success Rate: {success_rate}%)"
            )

            return combined_signal, metrics
        except Exception as e:
            logger.error(f"Error generating signal for {pair} ({timeframe}): {str(e)}")
            raise

    def calculate_24h_change(self, df: pd.DataFrame) -> float:
        """
        Calculates the percentage change over the last 24 hours.
        """
        try:
            current_time = df.index[-1]
            past_time = current_time - timedelta(hours=24)
            past_price = df[df.index >= past_time]["close"].iloc[0]
            current_price = df["close"].iloc[-1]
            change = ((current_price - past_price) / past_price) * 100
            return round(change, 2)
        except Exception as e:
            logger.error(f"Error calculating 24h change: {e}")
            return 0.0

    def generate_signal_from_set1(
        self, df: pd.DataFrame, higher_df: pd.DataFrame
    ) -> Tuple[str, List[str]]:
        """
        Generates signal based on the first set of indicators.
        """
        signal_strength = 0
        reasons = []

        # RSI
        if df["rsi"].iloc[-1] < 30:
            signal_strength += 1
            reasons.append("RSI indicates oversold conditions.")
        elif df["rsi"].iloc[-1] > 70:
            signal_strength -= 1
            reasons.append("RSI indicates overbought conditions.")

        # MACD crossover
        if df["macd"].iloc[-1] > df["macd_signal"].iloc[-1]:
            signal_strength += 1
            reasons.append("MACD bullish crossover.")
        elif df["macd"].iloc[-1] < df["macd_signal"].iloc[-1]:
            signal_strength -= 1
            reasons.append("MACD bearish crossover.")

        # SMA crossover
        if df["sma_fast"].iloc[-1] > df["sma_slow"].iloc[-1]:
            signal_strength += 1
            reasons.append("SMA bullish crossover.")
        elif df["sma_fast"].iloc[-1] < df["sma_slow"].iloc[-1]:
            signal_strength -= 1
            reasons.append("SMA bearish crossover.")

        # Bollinger Bands
        if df["close"].iloc[-1] < df["bollinger_lband"].iloc[-1]:
            signal_strength += 1
            reasons.append("Price below Bollinger lower band.")
        elif df["close"].iloc[-1] > df["bollinger_hband"].iloc[-1]:
            signal_strength -= 1
            reasons.append("Price above Bollinger upper band.")

        # Higher timeframe trend confirmation
        if higher_df["sma_fast"].iloc[-1] > higher_df["sma_slow"].iloc[-1]:
            signal_strength += 1
            reasons.append("Higher timeframe trend is bullish.")
        else:
            signal_strength -= 1
            reasons.append("Higher timeframe trend is bearish.")

        if signal_strength >= 3:
            signal = "Strong Buy"
        elif signal_strength == 2:
            signal = "Buy"
        elif signal_strength <= -3:
            signal = "Strong Sell"
        elif signal_strength == -2:
            signal = "Sell"
        else:
            signal = "No signal"

        return signal, reasons

    def generate_signal_from_set2(
        self, df: pd.DataFrame, higher_df: pd.DataFrame
    ) -> Tuple[str, List[str]]:
        """
        Generates signal based on the second set of indicators.
        """
        signal_strength = 0
        reasons = []

        # MFI
        if df["mfi"].iloc[-1] < 20:
            signal_strength += 1
            reasons.append("MFI indicates oversold conditions.")
        elif df["mfi"].iloc[-1] > 80:
            signal_strength -= 1
            reasons.append("MFI indicates overbought conditions.")

        # RVI
        if df["rvi"].iloc[-1] > 0:
            signal_strength += 1
            reasons.append("RVI indicates bullish momentum.")
        else:
            signal_strength -= 1
            reasons.append("RVI indicates bearish momentum.")

        # Keltner Channels
        if df["close"].iloc[-1] < df["keltner_lband"].iloc[-1]:
            signal_strength += 1
            reasons.append("Price below Keltner lower band.")
        elif df["close"].iloc[-1] > df["keltner_hband"].iloc[-1]:
            signal_strength -= 1
            reasons.append("Price above Keltner upper band.")

        # ADX
        if df["adx"].iloc[-1] > 25:
            signal_strength += 1
            reasons.append("ADX indicates a strong trend.")
        else:
            signal_strength -= 1
            reasons.append("ADX indicates a weak trend.")

        # Higher timeframe trend confirmation
        if higher_df["sma_fast"].iloc[-1] > higher_df["sma_slow"].iloc[-1]:
            signal_strength += 1
            reasons.append("Higher timeframe trend is bullish.")
        else:
            signal_strength -= 1
            reasons.append("Higher timeframe trend is bearish.")

        if signal_strength >= 3:
            signal = "Strong Buy"
        elif signal_strength == 2:
            signal = "Buy"
        elif signal_strength <= -3:
            signal = "Strong Sell"
        elif signal_strength == -2:
            signal = "Sell"
        else:
            signal = "No signal"

        return signal, reasons

    def combine_signals(self, signal1: str, signal2: str) -> Tuple[str, float]:
        """
        Combines signals from both sets and calculates success rate.
        """
        # Mapping of signal strength
        signal_strength_mapping = {
            "Strong Buy": 4,
            "Buy": 2,
            "Sell": -2,
            "Strong Sell": -4,
            "No signal": 0,
        }

        strength1 = signal_strength_mapping.get(signal1, 0)
        strength2 = signal_strength_mapping.get(signal2, 0)

        combined_strength = strength1 + strength2

        # Calculate success rate based on combined strength
        success_rate = min(abs(combined_strength) * 10 + 50, 100)

        if combined_strength >= 6:
            combined_signal = "Strong Buy"
        elif combined_strength >= 3:
            combined_signal = "Buy"
        elif combined_strength <= -6:
            combined_signal = "Strong Sell"
        elif combined_strength <= -3:
            combined_signal = "Sell"
        elif -2 <= combined_strength <= 2:
            combined_signal = "Neutral"
        else:
            combined_signal = "No signal"

        return combined_signal, success_rate

    def calculate_support_and_resistance(self, df: pd.DataFrame) -> Tuple[float, float]:
        """
        Calculates support and resistance levels as the recent lows and highs.
        """
        try:
            recent_lows = df["low"].rolling(window=20, min_periods=1).min()
            recent_highs = df["high"].rolling(window=20, min_periods=1).max()
            support_level = recent_lows.iloc[-1]
            resistance_level = recent_highs.iloc[-1]
            return support_level, resistance_level
        except Exception as e:
            logger.error(f"Error calculating support and resistance: {str(e)}")
            return (
                df["low"].iloc[-1],
                df["high"].iloc[-1],
            )  # Fallback to last row values in case of error.

    def calculate_stop_loss_take_profit(
        self, signal: str, current_price: float, df: pd.DataFrame
    ) -> Tuple[float, float]:
        """
        Uses ATR to calculate dynamic stop-loss and take-profit levels based on volatility.
        """
        try:
            atr = AverageTrueRange(
                high=df["high"], low=df["low"], close=df["close"], window=14
            ).average_true_range().iloc[-1]
            risk_to_reward_ratio = 1.5  # Desired risk-to-reward ratio

            if signal in ["Buy", "Strong Buy"]:
                stop_loss = current_price - (atr * 1.5)
                take_profit = current_price + (atr * 1.5 * risk_to_reward_ratio)
            elif signal in ["Sell", "Strong Sell"]:
                stop_loss = current_price + (atr * 1.5)
                take_profit = current_price - (atr * 1.5 * risk_to_reward_ratio)
            else:
                stop_loss = current_price - (atr * 2)
                take_profit = current_price + (atr * 2)

            return round(stop_loss, 2), round(take_profit, 2)
        except Exception as e:
            logger.error(f"Error calculating stop-loss and take-profit: {e}")
            return current_price, current_price

    @retry_with_exponential_backoff
    async def save_signal(
        self, pair: str, timeframe: str, signal: str, metrics: Dict[str, float], set_number: int
    ):
        """
        Saves the generated signal to the database with detailed information.
        """
        try:
            Signal.objects.create(
                symbol=pair,
                time_generated=metrics.get("time_generated"),
                timestamp=metrics.get("timestamp"),
                timeframe=timeframe,
                signal_type=signal,
                price=metrics.get("price"),
                change_24h=metrics.get("change_24h"),
                stop_loss=metrics.get("stop_loss"),
                take_profit=metrics.get("take_profit"),
                success_rate=metrics.get("success_rate"),
                result=metrics.get("result"),
                reasons=metrics.get("reasons"),
                set_number=set_number,  # Indicates from which set the signal originated
            )
            logger.info(f"Signal for {pair} {timeframe} (Set {set_number}) saved to the database.")
        except Exception as e:
            logger.error(f"Error saving signal for {pair} {timeframe}: {e}")
            raise

    async def run_bot(self):
        """
        Main loop of the bot. It runs continuously, processing symbols asynchronously.
        """
        while True:
            logger.info("Starting bot cycle...")
            start_time = time.time()

            tasks = []
            symbols = TradingPair.objects.values_list("symbol", flat=True)

            for symbol in symbols:
                for timeframe in self.timeframes:
                    task = self.process_symbol(symbol, timeframe)
                    tasks.append(task)

            await asyncio.gather(*tasks)

            end_time = time.time()
            execution_time = end_time - start_time
            sleep_time = max(60 - execution_time, 0)
            logger.info(f"Bot cycle complete. Sleeping for {sleep_time} seconds.")
            await asyncio.sleep(sleep_time)

    async def process_symbol(self, symbol: str, timeframe: str):
        """
        Processes a single symbol and timeframe, generating and saving signals from both sets.
        """
        try:
            # Generate signal
            combined_signal, metrics = await self.generate_signal(symbol, timeframe)

            # Save signals from both sets
            if combined_signal != "No signal":
                await asyncio.gather(
                    self.save_signal(symbol, timeframe, combined_signal, metrics, set_number=1),
                    self.save_signal(symbol, timeframe, combined_signal, metrics, set_number=2),
                )
            else:
                logger.info(
                    f"No significant signal for {symbol} ({timeframe}). Signal: {combined_signal}"
                )
        except Exception as e:
            logger.error(f"Error processing symbol {symbol} ({timeframe}): {e}")

if __name__ == "__main__":
    bot = CryptoBot()
    try:
        asyncio.run(bot.run_bot())
    except KeyboardInterrupt:
        logger.info("Stopping bot...")

Can you analyze the  code above and tell em a rough estimate of the accuracy of signals it will generate in percentage based on features and engineering of my bot. What else can we do to increase the accuracy of the bot with complicating it and causing overfitting