#priceaction.py
import random
import pandas as pd
import numpy as np
from django.utils import timezone
from django.db.models import Max
from .models import Kline  # Adjust this import based on your Django app structure
import logging
import traceback
from concurrent.futures import ThreadPoolExecutor
from scipy.signal import argrelextrema
from collections import deque
import time
import asyncio
import psycopg2
from psycopg2 import sql
import telegram
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class PriceActionBot:
    def __init__(self, intervals, lookback, db_config, telegram_token=None, email_config=None):
        self.intervals = intervals
        self.lookback = lookback
        self.db_config = db_config
        self.telegram_token = telegram_token
        self.email_config = email_config
        self.dfs = {}
        self.volatility = {}
        self.performance_metrics = {}

    async def get_real_time_data(self, symbol, interval):
        try:
            end_time = timezone.now()
            start_time = end_time - timezone.timedelta(days=self.lookback)
            
            klines = await Kline.objects.filter(
                symbol=symbol,
                interval=interval,
                open_time__gte=start_time,
                open_time__lte=end_time
            ).order_by('open_time').to_list()

            data = []
            for kline in klines:
                data.append([
                    kline.open_time,
                    kline.open_price,
                    kline.high_price,
                    kline.low_price,
                    kline.close_price,
                    kline.volume,
                    kline.close_time,
                    kline.quote_asset_volume,
                    kline.number_of_trades,
                    kline.taker_buy_base_asset_volume,
                    kline.taker_buy_quote_asset_volume
                ])

            df = pd.DataFrame(data, columns=[
                'timestamp', 'open', 'high', 'low', 'close', 'volume',
                'close_time', 'quote_asset_volume', 'number_of_trades',
                'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume'
            ])
            df['timestamp'] = pd.to_datetime(df['timestamp'])
            for col in ['open', 'high', 'low', 'close', 'volume']:
                df[col] = df[col].astype(float)
            df.set_index('timestamp', inplace=True)
            return df
        except Exception as e:
            logger.error(f"Error fetching real-time data for {symbol} on {interval}: {e}")
            return None

    async def calculate_adaptive_thresholds(self, df):
        volatility = df['close'].pct_change().rolling(window=20).std().iloc[-1]
        return {
            'weak': volatility * 0.5,
            'medium': volatility,
            'strong': volatility * 2
        }

    def detect_candlestick_patterns(self, df):
        patterns = {}
        
        # Existing patterns
        patterns['doji'] = abs(df['open'] - df['close']) <= (0.1 * (df['high'] - df['low']))
        patterns['hammer'] = ((df['high'] - df['low']) > 3 * (df['open'] - df['close'])) & \
                             ((df['close'] - df['low']) / (0.001 + df['high'] - df['low']) > 0.6) & \
                             ((df['open'] - df['low']) / (0.001 + df['high'] - df['low']) > 0.6)
        patterns['shooting_star'] = ((df['high'] - df['low']) > 3 * (df['open'] - df['close'])) & \
                                    ((df['high'] - df['close']) / (0.001 + df['high'] - df['low']) > 0.6) & \
                                    ((df['high'] - df['open']) / (0.001 + df['high'] - df['low']) > 0.6)
        patterns['bullish_engulfing'] = (df['open'].shift(1) > df['close'].shift(1)) & \
                                        (df['close'] > df['open'].shift(1)) & \
                                        (df['open'] < df['close'].shift(1)) & \
                                        (df['close'] > df['open'])
        patterns['bearish_engulfing'] = (df['close'].shift(1) > df['open'].shift(1)) & \
                                        (df['open'] > df['close'].shift(1)) & \
                                        (df['close'] < df['open'].shift(1)) & \
                                        (df['open'] > df['close'])
        patterns['bullish_harami'] = (df['open'].shift(1) > df['close'].shift(1)) & \
                                     (df['open'] > df['close']) & \
                                     (df['open'] <= df['close'].shift(1)) & \
                                     (df['close'] >= df['open'].shift(1))
        patterns['bearish_harami'] = (df['close'].shift(1) > df['open'].shift(1)) & \
                                     (df['close'] > df['open']) & \
                                     (df['open'] >= df['close'].shift(1)) & \
                                     (df['close'] <= df['open'].shift(1))
        
        # New patterns
        patterns['long_legged_doji'] = (abs(df['open'] - df['close']) <= (0.1 * (df['high'] - df['low']))) & \
                                       ((df['high'] - df['low']) > 3 * abs(df['open'] - df['close']))
        patterns['gravestone_doji'] = (abs(df['open'] - df['close']) <= (0.1 * (df['high'] - df['low']))) & \
                                      ((df['high'] - df['close']) > 3 * (df['close'] - df['low']))
        patterns['dragonfly_doji'] = (abs(df['open'] - df['close']) <= (0.1 * (df['high'] - df['low']))) & \
                                     ((df['close'] - df['low']) > 3 * (df['high'] - df['close']))
        patterns['hanging_man'] = ((df['high'] - df['low']) > 3 * (df['open'] - df['close'])) & \
                                  ((df['close'] - df['low']) / (0.001 + df['high'] - df['low']) > 0.6) & \
                                  ((df['open'] - df['low']) / (0.001 + df['high'] - df['low']) > 0.6) & \
                                  (df['close'].shift(1) > df['open'].shift(1))
        patterns['belt_hold'] = (abs(df['open'] - df['low']) <= (0.1 * (df['high'] - df['low']))) & \
                                (df['close'] > df['open'])
        patterns['marubozu'] = (abs(df['open'] - df['high']) <= (0.1 * (df['high'] - df['low']))) & \
                               (abs(df['close'] - df['low']) <= (0.1 * (df['high'] - df['low'])))
        patterns['three_white_soldiers'] = (df['close'] > df['open']) & \
                                           (df['close'].shift(1) > df['open'].shift(1)) & \
                                           (df['close'].shift(2) > df['open'].shift(2)) & \
                                           (df['open'] > df['close'].shift(1)) & \
                                           (df['open'].shift(1) > df['close'].shift(2))
        patterns['rising_three_methods'] = (df['close'].shift(4) > df['open'].shift(4)) & \
                                           (df['close'] > df['open']) & \
                                           (df['close'] > df['close'].shift(4)) & \
                                           (df['open'].shift(1) < df['close'].shift(4)) & \
                                           (df['close'].shift(1) < df['close'].shift(4)) & \
                                           (df['open'].shift(2) < df['close'].shift(4)) & \
                                           (df['close'].shift(2) < df['close'].shift(4)) & \
                                           (df['open'].shift(3) < df['close'].shift(4)) & \
                                           (df['close'].shift(3) < df['close'].shift(4))
        patterns['piercing_line'] = (df['close'].shift(1) < df['open'].shift(1)) & \
                                    (df['open'] < df['low'].shift(1)) & \
                                    (df['close'] > (df['open'].shift(1) + df['close'].shift(1)) / 2) & \
                                    (df['close'] < df['open'].shift(1))
        patterns['inverted_hammer'] = ((df['high'] - df['low']) > 3 * (df['open'] - df['close'])) & \
                                      ((df['high'] - df['open']) / (0.001 + df['high'] - df['low']) > 0.6) & \
                                      ((df['high'] - df['close']) / (0.001 + df['high'] - df['low']) > 0.6)
        
        return pd.DataFrame(patterns)

    def detect_head_and_shoulders(self, df, window=20):
        highs = df['high'].values
        lows = df['low'].values
        
        left_shoulder = argrelextrema(highs[:window], np.greater)[0]
        head = argrelextrema(highs[window:2*window], np.greater)[0] + window
        right_shoulder = argrelextrema(highs[2*window:], np.greater)[0] + 2*window
        
        if len(left_shoulder) > 0 and len(head) > 0 and len(right_shoulder) > 0:
            if highs[head] > highs[left_shoulder] and highs[head] > highs[right_shoulder]:
                neckline = min(lows[left_shoulder], lows[right_shoulder])
                if df['close'].iloc[-1] < neckline:
                    return 'Head and Shoulders Top'
        
        left_shoulder = argrelextrema(lows[:window], np.less)[0]
        head = argrelextrema(lows[window:2*window], np.less)[0] + window
        right_shoulder = argrelextrema(lows[2*window:], np.less)[0] + 2*window
        
        if len(left_shoulder) > 0 and len(head) > 0 and len(right_shoulder) > 0:
            if lows[head] < lows[left_shoulder] and lows[head] < lows[right_shoulder]:
                neckline = max(highs[left_shoulder], highs[right_shoulder])
                if df['close'].iloc[-1] > neckline:
                    return 'Head and Shoulders Bottom'
        
        return None

    def detect_triangle_pattern(self, df, window=20):
        highs = df['high'].rolling(window=5).max()
        lows = df['low'].rolling(window=5).min()
        
        upper_trend = np.polyfit(range(window), highs[-window:], 1)
        lower_trend = np.polyfit(range(window), lows[-window:], 1)
        
        if abs(upper_trend[0]) < 0.1 and abs(lower_trend[0]) < 0.1:
            return 'Symmetrical Triangle'
        elif upper_trend[0] < -0.1 and abs(lower_trend[0]) < 0.1:
            return 'Descending Triangle'
        elif abs(upper_trend[0]) < 0.1 and lower_trend[0] > 0.1:
            return 'Ascending Triangle'
        
        return None

    def detect_flags_pennants(self, df, window=20):
        trend = np.polyfit(range(window), df['close'][-window:], 1)[0]
        
        if abs(trend) > 0.1:
            highs = df['high'].rolling(window=5).max()
            lows = df['low'].rolling(window=5).min()
            
            upper_trend = np.polyfit(range(window), highs[-window:], 1)
            lower_trend = np.polyfit(range(window), lows[-window:], 1)
            
            if abs(upper_trend[0] - lower_trend[0]) < 0.05:
                return 'Flag'
            elif abs(upper_trend[0]) < abs(lower_trend[0]):
                return 'Pennant'
        
        return None

    def fibonacci_retracement(self, df, window=100):
        high = df['high'][-window:].max()
        low = df['low'][-window:].min()
        diff = high - low
        levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]
        retracements = [high - l * diff for l in levels]
        return retracements

    def volume_analysis(self, df, window=20):
        volume = df['volume'].values
        price = df['close'].values
        
        avg_volume = np.mean(volume[-window:])
        volume_trend = 'Increasing' if volume[-1] > avg_volume else 'Decreasing'
        
        obv = np.cumsum(np.where(price > np.roll(price, 1), volume, -volume))
        obv_trend = 'Bullish' if obv[-1] > obv[-2] else 'Bearish'
        
        return volume_trend, obv_trend
    async def advanced_price_action_analysis(self, df, symbol, interval):
        try:
            recent_df = df.iloc[-200:].copy()
            recent_df.dropna(inplace=True)

            if len(recent_df) < 50:
                return None

            thresholds = await self.calculate_adaptive_thresholds(recent_df)


            candlestick_patterns = self.detect_candlestick_patterns(recent_df)
            recent_df = pd.concat([recent_df, candlestick_patterns], axis=1)

            head_and_shoulders = self.detect_head_and_shoulders(recent_df)
            triangle = self.detect_triangle_pattern(recent_df)
            flag_pennant = self.detect_flags_pennants(recent_df)

            short_term_trend = np.polyfit(range(20), recent_df['close'].values[-20:], 1)[0]
            medium_term_trend = np.polyfit(range(50), recent_df['close'].values[-50:], 1)[0]
            long_term_trend = np.polyfit(range(100), recent_df['close'].values[-100:], 1)[0]

            support = np.min(recent_df['low'].values[-20:])
            resistance = np.max(recent_df['high'].values[-20:])

            fib_levels = self.fibonacci_retracement(recent_df)

            volume_trend, obv_trend = self.volume_analysis(recent_df)

            signal = 'NO_SIGNAL'
            strength = 0

            # Trend-based signals
            if short_term_trend > 0 and medium_term_trend > 0 and long_term_trend > 0:
                signal = 'BUY'
                strength += 1
            elif short_term_trend < 0 and medium_term_trend < 0 and long_term_trend < 0:
                signal = 'SELL'
                strength += 1

            # Candlestick pattern signals
            bullish_patterns = ['bullish_engulfing', 'hammer', 'bullish_harami', 'three_white_soldiers', 'piercing_line', 'inverted_hammer']
            bearish_patterns = ['bearish_engulfing', 'shooting_star', 'bearish_harami', 'hanging_man', 'gravestone_doji']
            
            for pattern in bullish_patterns:
                if recent_df[pattern].iloc[-1]:
                    if signal == 'BUY':
                        strength += 1
                    else:
                        signal = 'BUY'
                        strength = 1
                    break
            
            for pattern in bearish_patterns:
                if recent_df[pattern].iloc[-1]:
                    if signal == 'SELL':
                        strength += 1
                    else:
                        signal = 'SELL'
                        strength = 1
                    break

            # Chart pattern signals
            if head_and_shoulders == 'Head and Shoulders Top':
                if signal == 'SELL':
                    strength += 2
                else:
                    signal = 'SELL'
                    strength = 2
            elif head_and_shoulders == 'Head and Shoulders Bottom':
                if signal == 'BUY':
                    strength += 2
                else:
                    signal = 'BUY'
                    strength = 2

            if triangle == 'Ascending Triangle' and signal == 'BUY':
                strength += 1
            elif triangle == 'Descending Triangle' and signal == 'SELL':
                strength += 1

            if flag_pennant in ['Flag', 'Pennant']:
                if signal == 'BUY' and short_term_trend > 0:
                    strength += 1
                elif signal == 'SELL' and short_term_trend < 0:
                    strength += 1

            # Volume confirmation
            if volume_trend == 'Increasing' and obv_trend == 'Bullish' and signal == 'BUY':
                strength += 1
            elif volume_trend == 'Increasing' and obv_trend == 'Bearish' and signal == 'SELL':
                strength += 1

            # Categorize signal strength

            if strength >= thresholds['strong']:
                signal = f'STRONG_{signal}'
            elif strength >= thresholds['medium']:
                signal = f'{signal}'
            elif strength >= thresholds['weak']:
                signal = f'WEAK_{signal}'
            else:
                signal = 'NO_SIGNAL'

            last_row = recent_df.iloc[-1].copy()
            last_row['Signal'] = signal
            last_row['Signal_Strength'] = strength
            last_row['Symbol'] = symbol
            last_row['Interval'] = interval

            return last_row.to_frame().T

        except Exception as e:
            logger.error(f"Error in advanced price action analysis: {e}\n{traceback.format_exc()}")
            return None

    async def save_signal_to_db(self, signal_data):
        try:
            conn = psycopg2.connect(**self.db_config)
            cur = conn.cursor()
            
            columns = signal_data.columns.tolist()
            values = [signal_data[col].iloc[0] for col in columns]
            
            insert_query = sql.SQL("INSERT INTO signals ({}) VALUES ({})").format(
                sql.SQL(', ').join(map(sql.Identifier, columns)),
                sql.SQL(', ').join(sql.Placeholder() * len(columns))
            )
            
            cur.execute(insert_query, values)
            conn.commit()
            
            cur.close()
            conn.close()
            
            logger.info(f"Signal saved to database: {signal_data['Signal'].iloc[0]}")
        except Exception as e:
            logger.error(f"Error saving signal to database: {e}\n{traceback.format_exc()}")

    async def send_telegram_alert(self, message):
        if self.telegram_token:
            try:
                bot = telegram.Bot(token=self.telegram_token)
                await bot.send_message(chat_id=self.telegram_chat_id, text=message)
                logger.info("Telegram alert sent successfully")
            except Exception as e:
                logger.error(f"Error sending Telegram alert: {e}\n{traceback.format_exc()}")

    async def send_email_alert(self, subject, body):
        if self.email_config:
            try:
                msg = MIMEMultipart()
                msg['From'] = self.email_config['sender']
                msg['To'] = self.email_config['recipient']
                msg['Subject'] = subject
                
                msg.attach(MIMEText(body, 'plain'))
                
                server = smtplib.SMTP(self.email_config['smtp_server'], self.email_config['smtp_port'])
                server.starttls()
                server.login(self.email_config['username'], self.email_config['password'])
                server.send_message(msg)
                server.quit()
                
                logger.info("Email alert sent successfully")
            except Exception as e:
                logger.error(f"Error sending email alert: {e}\n{traceback.format_exc()}")

    async def analyze_interval(self, interval):
        while True:
            try:
                df = await self.get_real_time_data(interval)
                if df is not None:
                    result = await self.advanced_price_action_analysis(df, interval)
                    if result is not None:
                        await self.save_signal_to_db(result)
                        
                        signal = result['Signal'].iloc[0]
                        strength = result['Signal_Strength'].iloc[0]
                        
                        if signal != 'NO_SIGNAL':
                            message = f"New signal for {self.symbol} on {interval} timeframe: {signal} (Strength: {strength})"
                            await self.send_telegram_alert(message)
                            await self.send_email_alert("New Trading Signal", message)
                    
                        logger.info(f"Analysis Result for {interval}:\n{result}")
                    else:
                        logger.warning(f"No analysis result generated for {interval}")
                else:
                    logger.warning(f"No historical data available for {interval}")
                
                await asyncio.sleep(self.get_interval_seconds(interval))
                
            except Exception as e:
                logger.error(f"Error in analysis loop for {interval}: {e}\n{traceback.format_exc()}")
                await asyncio.sleep(60)

    def get_interval_seconds(self, interval):
        interval_map = {
            '1m': 60,
            '5m': 300,
            '15m': 900,
            '30m': 1800,
            '1h': 3600,
            '4h': 14400,
            '1d': 86400
        }
        return interval_map.get(interval, 300)

    async def analyze_pair(self, symbol, interval):
        while True:
            try:
                df = await self.get_real_time_data(symbol, interval)
                if df is not None:
                    result = await self.advanced_price_action_analysis(df, symbol, interval)
                    if result is not None:
                        await self.save_signal_to_db(result)
                        
                        signal = result['Signal'].iloc[0]
                        strength = result['Signal_Strength'].iloc[0]
                        
                        if signal != 'NO_SIGNAL':
                            message = f"New signal for {symbol} on {interval} timeframe: {signal} (Strength: {strength})"
                            await self.send_telegram_alert(message)
                            await self.send_email_alert("New Trading Signal", message)
                    
                        logger.info(f"Analysis Result for {symbol} on {interval}:\n{result}")
                        
                        # Update performance metrics
                        await self.update_performance_metrics(symbol, interval, result)
                    else:
                        logger.warning(f"No analysis result generated for {symbol} on {interval}")
                else:
                    logger.warning(f"No real-time data available for {symbol} on {interval}")
                
                await asyncio.sleep(self.get_interval_seconds(interval))
                
            except Exception as e:
                logger.error(f"Error in analysis loop for {symbol} on {interval}: {e}\n{traceback.format_exc()}")
                await asyncio.sleep(60)

    async def update_performance_metrics(self, symbol, interval, result):
        if symbol not in self.performance_metrics:
            self.performance_metrics[symbol] = {}
        
        if interval not in self.performance_metrics[symbol]:
            self.performance_metrics[symbol][interval] = {
                'total_signals': 0,
                'correct_signals': 0,
                'incorrect_signals': 0,
                'profit_loss': 0,
            }
        
        metrics = self.performance_metrics[symbol][interval]
        metrics['total_signals'] += 1
        
        # Simulated performance tracking (replace with actual logic based on your requirements)
        if result['Signal'].iloc[0] != 'NO_SIGNAL':
            # Simulate a trade outcome (replace with actual trade tracking logic)
            trade_outcome = random.choice(['win', 'loss'])
            if trade_outcome == 'win':
                metrics['correct_signals'] += 1
                metrics['profit_loss'] += random.uniform(0.5, 2.0)
            else:
                metrics['incorrect_signals'] += 1
                metrics['profit_loss'] -= random.uniform(0.5, 1.0)
        
        logger.info(f"Updated performance metrics for {symbol} on {interval}: {metrics}")

    async def run_bot(self):
        # Fetch all USDT trading pairs from the database
        usdt_pairs = await self.get_usdt_pairs()
        
        tasks = []
        for symbol in usdt_pairs:
            for interval in self.intervals:
                tasks.append(self.analyze_pair(symbol, interval))
        
        await asyncio.gather(*tasks)

    async def get_usdt_pairs(self):
        try:
            # Fetch all unique symbols ending with USDT from the Kline model
            usdt_pairs = await Kline.objects.filter(symbol__endswith='USDT').values_list('symbol', flat=True).distinct()
            return list(usdt_pairs)
        except Exception as e:
            logger.error(f"Error fetching USDT pairs: {e}\n{traceback.format_exc()}")
            return []

if __name__ == "__main__":
    intervals = ["1m", "5m", "15m", "1h", "4h", "1d"]
    lookback = 30  # days

    db_config = {
        "host": "localhost",
        "database": "trading_bot",
        "user": "your_username",
        "password": "your_password"
    }

    telegram_token = "your_telegram_bot_token"
    telegram_chat_id = "your_telegram_chat_id"

    email_config = {
        "sender": "your_email@example.com",
        "recipient": "recipient@example.com",
        "smtp_server": "smtp.example.com",
        "smtp_port": 587,
        "username": "your_email@example.com",
        "password": "your_email_password"
    }

    bot = PriceActionBot(intervals, lookback, db_config, telegram_token, email_config)
    asyncio.run(bot.run_bot())
Analyze the code and tell me its features and what we can do to improve it