import logging
import aiohttp
import pandas as pd
import numpy as np
import mplfinance as mpf
from datetime import datetime, timedelta, timezone
import telegram
from telegram import InputMediaPhoto
import asyncio
import config
from io import BytesIO
from ta.trend import MACD, SMAIndicator, EMAIndicator, IchimokuIndicator, ADXIndicator
from ta.momentum import RSIIndicator, StochasticOscillator
from ta.volatility import BollingerBands, AverageTrueRange
import os
import glob
from PIL import Image, __version__ as PILLOW_VERSION
from packaging import version
from telegram.error import TelegramError, RetryAfter, TimedOut
from concurrent.futures import ThreadPoolExecutor
from matplotlib import pyplot as plt

# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("bot.log"),
        logging.StreamHandler()
    ]
)

# Проверка версии Pillow и логирование
try:
    pillow_version = version.parse(PILLOW_VERSION)
    if pillow_version < version.parse("10.0.0"):
        logging.warning(f"Используется устаревшая версия Pillow ({PILLOW_VERSION}). Рекомендуется обновиться до версии 10.0.0 или выше.")
    else:
        logging.info(f"Используется Pillow версии {PILLOW_VERSION}.")
except Exception as e:
    logging.error(f"Не удалось определить версию Pillow: {e}")

MAX_TG_MESSAGE_LENGTH = 4000  # Немного меньше, чем лимит Telegram
MEDIA_GROUP_LIMIT_PER_SECOND = 20  # Настройте по необходимости

# Директория для сохранения графиков
GRAPHS_DIR = 'graphs'

# Создание директории для графиков, если она не существует
if not os.path.exists(GRAPHS_DIR):
    os.makedirs(GRAPHS_DIR)

# Функция для очистки предыдущих графиков
def clean_previous_graphs():
    try:
        files = glob.glob(os.path.join(GRAPHS_DIR, '*_72h.png'))
        for file in files:
            os.remove(file)
        logging.info(f"Удалено {len(files)} предыдущих графиков.")
    except Exception as e:
        logging.error(f"Ошибка при удалении предыдущих графиков: {e}")

# Асинхронная функция для получения списка всех фьючерсных пар USDT на Binance
async def get_future_symbols(session):
    try:
        logging.info("Запрос списка фьючерсных пар USDT от Binance.")
        url = 'https://fapi.binance.com/fapi/v1/exchangeInfo'
        async with session.get(url) as response:
            response.raise_for_status()
            data = await response.json()
            symbols = [s['symbol'] for s in data['symbols']
                       if s['quoteAsset'] == 'USDT' and s['contractType'] == 'PERPETUAL']
            logging.info(f"Найдено {len(symbols)} фьючерсных пар USDT.")
            return symbols
    except aiohttp.ClientError as e:
        logging.error(f"Ошибка при запросе списка символов: {e}")
        raise
    except Exception as e:
        logging.error(f"Неизвестная ошибка при обработке списка символов: {e}")
        raise

# Асинхронная функция для получения исторических данных от Binance Futures
async def get_historical_klines(session, symbol, interval, hours):
    try:
        logging.info(f"Запрос данных для {symbol} с интервалом {interval} за последние {hours} часов.")
        end_time = datetime.now(timezone.utc)
        start_time = end_time - timedelta(hours=hours)
        start_timestamp = int(start_time.timestamp() * 1000)
        end_timestamp = int(end_time.timestamp() * 1000)
        url = 'https://fapi.binance.com/fapi/v1/klines'
        params = {
            'symbol': symbol,
            'interval': interval,
            'startTime': start_timestamp,
            'endTime': end_timestamp,
            'limit': 1000  # Максимальное количество данных
        }
        async with session.get(url, params=params) as response:
            response.raise_for_status()
            data = await response.json()
            if not data:
                logging.warning(f"Нет данных для символа {symbol}.")
                return None
            df = pd.DataFrame(data, columns=[
                'Open time', 'Open', 'High', 'Low', 'Close', 'Volume',
                'Close time', 'Quote asset volume', 'Number of trades',
                'Taker buy base asset volume', 'Taker buy quote asset volume', 'Ignore'
            ])
            df['Open time'] = pd.to_datetime(df['Open time'], unit='ms')
            df['Close time'] = pd.to_datetime(df['Close time'], unit='ms')
            df[['Open', 'High', 'Low', 'Close', 'Volume']] = df[['Open', 'High', 'Low', 'Close', 'Volume']].astype(float)
            df.sort_values('Open time', inplace=True)  # Убедитесь, что данные отсортированы по времени
            logging.info(f"Данные успешно получены для {symbol} (строк: {len(df)}).")
            return df[['Open time', 'Open', 'High', 'Low', 'Close', 'Volume']]
    except aiohttp.ClientError as e:
        logging.error(f"Ошибка при запросе данных для {symbol}: {e}")
        return None
    except Exception as e:
        logging.error(f"Неизвестная ошибка при обработке данных для {symbol}: {e}")
        return None

# Функция для вычисления технических индикаторов
def compute_indicators(df):
    try:
        logging.info("Вычисление технических индикаторов.")

        # RSI
        rsi = RSIIndicator(close=df['Close'], window=14)
        df['RSI'] = rsi.rsi()

        # MACD
        macd = MACD(close=df['Close'])
        df['MACD'] = macd.macd()
        df['MACD_Signal'] = macd.macd_signal()
        df['MACD_Diff'] = macd.macd_diff()

        # Скользящие средние (SMA50 и EMA20)
        sma50 = SMAIndicator(close=df['Close'], window=50)
        df['SMA50'] = sma50.sma_indicator()

        ema20 = EMAIndicator(close=df['Close'], window=20)
        df['EMA20'] = ema20.ema_indicator()

        # Полосы Боллинджера
        bb = BollingerBands(close=df['Close'], window=20, window_dev=2)
        df['BB_High'] = bb.bollinger_hband()
        df['BB_Low'] = bb.bollinger_lband()

        # Стохастический осциллятор
        stochastic = StochasticOscillator(high=df['High'], low=df['Low'], close=df['Close'], window=14, smooth_window=3)
        df['Stoch_%K'] = stochastic.stoch()
        df['Stoch_%D'] = stochastic.stoch_signal()

        # Ichimoku Cloud
        ichimoku = IchimokuIndicator(high=df['High'], low=df['Low'], window1=9, window2=26, window3=52)
        df['Ichimoku_A'] = ichimoku.ichimoku_a()
        df['Ichimoku_B'] = ichimoku.ichimoku_b()

        # ATR
        atr = AverageTrueRange(high=df['High'], low=df['Low'], close=df['Close'], window=14)
        df['ATR'] = atr.average_true_range()

        # VWAP
        df['VWAP'] = (df['Volume'] * (df['High'] + df['Low'] + df['Close']) / 3).cumsum() / df['Volume'].cumsum()

        # ADX
        adx = ADXIndicator(high=df['High'], low=df['Low'], close=df['Close'], window=14)
        df['ADX'] = adx.adx()

        logging.info("Технические индикаторы успешно вычислены.")
        return df
    except Exception as e:
        logging.error(f"Ошибка при вычислении индикаторов: {e}")
        raise

# Функция для определения направления и фазы рынка на основе индикаторов и расчета точек входа и тейк-профита
def analyze_indicators(df):
    try:
        logging.info("Анализ индикаторов для определения направления движения, фазы рынка и расчета точек входа/тейк-профита.")
        latest = df.iloc[-1]
        previous = df.iloc[-2]

        # Определение фазы рынка на основе ADX
        if latest['ADX'] < 20:
            phase = "Баланс"
        elif latest['ADX'] > 25:
            phase = "Импульс"
        else:
            phase = "Нейтрально"

        if phase != "Баланс":
            logging.info(f"Фаза рынка: {phase}. Требуется фаза баланса. Пропуск анализа.")
            return phase, None, None, None, None

        # Определение границ диапазона баланса на основе последних 24 часов
        window_hours = 24
        df_recent = df.tail(window_hours)
        support = df_recent['Low'].min()
        resistance = df_recent['High'].max()

        # Логика определения точек входа
        entry_price = None
        take_profit = None
        direction = None
        probability = None  # В данном случае вероятность фиксирована, так как вы нацелены на 100%

        # Определение позиции закрытия относительно диапазона баланса
        close_position = (latest['Close'] - support) / (resistance - support)

        # Установка порогов для определения отскока
        bullish_threshold = 0.3  # 30% от диапазона ближе к поддержке
        bearish_threshold = 0.7  # 70% от диапазона ближе к сопротивлению

        if 0.0 <= close_position <= bullish_threshold:
            # Бычий сигнал: отскок от поддержки
            entry_price = support
            take_profit = resistance
            direction = "Бычий сигнал"
            probability = "100.0000% вероятность"
            logging.info(f"Бычий сигнал на уровне поддержки: {support:.4f} USDT")
        elif bearish_threshold <= close_position <= 1.0:
            # Медвежий сигнал: отскок от сопротивления
            entry_price = resistance
            take_profit = support
            direction = "Медвежий сигнал"
            probability = "100.0000% вероятность"
            logging.info(f"Медвежий сигнал на уровне сопротивления: {resistance:.4f} USDT")
        else:
            logging.info("Неопределённый сигнал. Пропуск анализа.")
            return phase, None, None, None, None

        logging.info(f"Фаза рынка: {phase}. Определённое направление: {direction} с вероятностью {probability}.")
        logging.info(f"Точка входа: {entry_price:.4f}, Тейк-профит: {take_profit:.4f}")

        return phase, direction, probability, entry_price, take_profit

    except Exception as e:
        logging.error(f"Ошибка при анализе индикаторов: {e}")
        raise

# Функция для проверки достаточности объема торгов
def is_volume_sufficient(df, threshold=1000):
    """
    Проверяет, достаточен ли средний объем торгов за период.

    :param df: DataFrame с историческими данными.
    :param threshold: Минимальный средний объем торгов.
    :return: True если объем достаточен, иначе False.
    """
    average_volume = df['Volume'].mean()
    return average_volume >= threshold

# Функция для проверки достаточной вариации цен
def has_sufficient_price_variation(df, min_variation=0.5):
    """
    Проверяет, есть ли достаточная вариация цен.

    :param df: DataFrame с историческими данными.
    :param min_variation: Мин. вариация в процентах.
    :return: True если вариация достаточна, иначе False.
    """
    price_diff = df['High'].max() - df['Low'].min()
    if df['Low'].min() == 0:
        return False  # Избежать деления на ноль
    percentage_variation = (price_diff / df['Low'].min()) * 100
    return percentage_variation >= min_variation

# Функция для проверки, является ли изображение пустым (белым) на основе вариации
def is_image_blank(image_path, variance_threshold=100.0):
    """
    Проверяет, является ли изображение почти полностью белым или однотонным.

    :param image_path: Путь к изображению.
    :param variance_threshold: Порог вариативности. Меньше значения считает изображение пустым.
    :return: True если изображение пустое, иначе False.
    """
    try:
        with Image.open(image_path) as img:
            grayscale = img.convert('L')  # Преобразование в оттенки серого
            np_img = np.array(grayscale)
            variance = np.var(np_img)
            logging.debug(f"Variance for {image_path}: {variance}")
            return variance < variance_threshold
    except Exception as e:
        logging.error(f"Ошибка при проверке изображения {image_path}: {e}")
        return False

# Функция для сжатия изображений
def compress_image(image_path, max_size=(800, 600), quality=70):
    """
    Сжимает изображение и сохраняет его обратно.

    :param image_path: Путь к изображению.
    :param max_size: Максимальный размер изображения.
    :param quality: Качество сжатия.
    :return: True если сжатие прошло успешно и изображение не пустое, иначе False
    """
    try:
        with Image.open(image_path) as img:
            # Определяем метод ресэмплинга в зависимости от версии Pillow
            if version.parse(PILLOW_VERSION) >= version.parse("10.0.0"):
                resample_method = Image.Resampling.LANCZOS
            else:
                resample_method = Image.ANTIALIAS

            img.thumbnail(max_size, resample_method)
            img.save(image_path, optimize=True, quality=quality)
        logging.info(f"Изображение сжато и сохранено: {image_path}")

        # Проверка, не стало ли изображение пустым после сжатия
        if is_image_blank(image_path):
            logging.warning(f"Изображение {image_path} оказалось пустым после сжатия.")
            os.remove(image_path)
            return False

        return True
    except Exception as e:
        logging.error(f"Ошибка при сжатии изображения {image_path}: {e}")
        return False

# Функция для построения графика с отметками точек входа и тейк-профита
def plot_candlestick(df, symbol, entry=None, take_profit=None, support=None, resistance=None):
    try:
        logging.info(f"Построение графика свечей для {symbol}.")

        if df.empty:
            logging.warning(f"Пустой DataFrame для {symbol}. Пропуск построения графика.")
            return None

        # Проверка наличия достаточного количества данных
        if df.shape[0] < 10:
            logging.warning(f"Недостаточно данных для построения графика {symbol}. (строк: {df.shape[0]})")
            return None

        # Проверка на вариацию цен
        if df['Close'].nunique() == 1:
            logging.warning(f"Все значения цены закрытия одинаковы для {symbol}. Пропуск построения графика.")
            return None

        logging.info(f"Данные для построения графика {symbol}:\n{df.tail()}")

        df = df.copy()
        df.set_index('Open time', inplace=True)
        mc = mpf.make_marketcolors(up='g', down='r', inherit=True)
        s = mpf.make_mpf_style(marketcolors=mc)

        # Используем matplotlib фигуру для добавления аннотаций
        fig, axlist = mpf.plot(
            df,
            type='candle',
            style=s,
            title=f'{symbol} - Последние 72 часа',
            ylabel='Цена (USDT)',
            volume=True,
            addplot=[],
            returnfig=True,
            figsize=(12, 8)
        )

        # Добавление линий поддержки и сопротивления
        if support and resistance:
            ax = axlist[0]  # Основной график свечей
            ax.axhline(support, linestyle='--', color='blue', linewidth=1, label='Поддержка')
            ax.axhline(resistance, linestyle='--', color='orange', linewidth=1, label='Сопротивление')

            # Добавление легенды
            ax.legend()

            # Добавление линий входа и тейк-профита с аннотациями
            if entry and take_profit:
                ax.axhline(entry, linestyle='-', color='green', linewidth=1, label='Точка Входа')
                ax.axhline(take_profit, linestyle='-', color='red', linewidth=1, label='Тейк-Профит')

                # Добавление аннотаций рядом с последней свечой
                ax.annotate('Entry', xy=(df.index[-1], entry), xytext=(0, 10),
                            textcoords="offset points", arrowprops=dict(arrowstyle='->', color='green'),
                            fontsize=8, color='green', ha='right')
                ax.annotate('Take Profit', xy=(df.index[-1], take_profit), xytext=(0, -15),
                            textcoords="offset points", arrowprops=dict(arrowstyle='->', color='red'),
                            fontsize=8, color='red', ha='right')

        # Сохранение графика в буфер
        buffer = BytesIO()
        fig.savefig(buffer, format='png', dpi=100, bbox_inches='tight')
        plt.close(fig)
        buffer.seek(0)
        img = Image.open(buffer)

        # Проверка, не является ли изображение пустым
        grayscale = img.convert('L')  # Преобразование в оттенки серого
        np_img = np.array(grayscale)
        variance = np.var(np_img)
        logging.debug(f"Variance for {symbol}: {variance}")
        if variance < 100.0:  # Порог вариативности, подберите оптимальное значение
            logging.warning(f"График для {symbol} является пустым (variance={variance}). Пропуск.")
            return None

        # Сохранение изображения в файл
        filename = os.path.join(GRAPHS_DIR, f"{symbol}_72h.png")
        img.save(filename)

        # Сжатие изображения перед отправкой
        compression_success = compress_image(filename)
        if not compression_success:
            logging.warning(f"Изображение {filename} не прошло проверку на пустоту после сжатия.")
            return None

        logging.info(f"График сохранен как {filename}.")
        return filename
    except Exception as e:
        logging.error(f"Ошибка при построении графика для {symbol}: {e}")
        # Удаление файла в случае ошибки, если он был создан
        filename = os.path.join(GRAPHS_DIR, f"{symbol}_72h.png")
        if os.path.exists(filename):
            try:
                os.remove(filename)
                logging.info(f"Удален некорректный файл графика: {filename}")
            except Exception as remove_e:
                logging.error(f"Ошибка при удалении файла {filename}: {remove_e}")
        return None

# Функция для отправки отчета в Telegram с механизмом повторных попыток
async def send_telegram_report(token, chat_id, bullish_symbols, bearish_symbols):
    bot = telegram.Bot(token=token)
    try:
        # Отправка списка монет с высокой вероятностью роста
        if bullish_symbols:
            message_growth = "📈 **Список монет с высокой вероятностью роста (Фаза Баланса):**"
            logging.info("Отправка сообщения о монетах с высокой вероятностью роста.")
            await bot.send_message(chat_id=chat_id, text=message_growth, parse_mode='Markdown')
            logging.info(f"Отправка группы из {len(bullish_symbols)} графиков в Telegram для роста.")
            await send_media_group_with_retry(bot, chat_id, bullish_symbols)
        else:
            logging.info("Нет монет с высокой вероятностью роста для отправки.")

        # Отправка списка монет с высокой вероятностью падения
        if bearish_symbols:
            message_decline = "📉 **Список монет с высокой вероятностью падения (Фаза Баланса):**"
            logging.info("Отправка сообщения о монетах с высокой вероятностью падения.")
            await bot.send_message(chat_id=chat_id, text=message_decline, parse_mode='Markdown')
            logging.info(f"Отправка группы из {len(bearish_symbols)} графиков в Telegram для падения.")
            await send_media_group_with_retry(bot, chat_id, bearish_symbols)
        else:
            logging.info("Нет монет с высокой вероятностью падения для отправки.")

        # Очистка директории графиков после отправки
        try:
            logging.info("Удаление отправленных графиков.")
            files = glob.glob(os.path.join(GRAPHS_DIR, '*_72h.png'))
            for file in files:
                os.remove(file)
            logging.info(f"Удалено {len(files)} графиков.")
        except Exception as e:
            logging.error(f"Ошибка при удалении графиков: {e}")

        logging.info("Отчет успешно отправлен в Telegram.")
    except Exception as e:
        logging.error(f"Ошибка при отправке отчета в Telegram: {e}")

# Функция для отправки медиа-группы с повторными попытками
async def send_media_group_with_retry(bot, chat_id, symbols, retries=3, delay=5):
    """
    Отправляет группу медиа-файлов с повторными попытками в случае ошибок.

    :param bot: Экземпляр бота telegram.Bot.
    :param chat_id: ID чата для отправки.
    :param symbols: Список символов с информацией (направление, вероятность, изображение, entry, take_profit, support, resistance).
    :param retries: Количество попыток повторной отправки.
    :param delay: Задержка между попытками.
    :return: None
    """
    media_group = []
    for symbol_info in symbols:
        symbol = symbol_info['symbol']
        direction = symbol_info['direction']
        probability = symbol_info['probability']
        image_path = symbol_info['image']
        entry = symbol_info.get('entry')
        take_profit = symbol_info.get('take_profit')
        support = symbol_info.get('support')
        resistance = symbol_info.get('resistance')

        if image_path is None:
            logging.warning(f"Нет данных изображения для {symbol}. Пропуск.")
            continue

        # Создание подписи для графика
        if direction == "Бычий сигнал":
            emoji = "📈"
            # Расчет прибыльности для бычьего сигнала
            profit_percent = ((take_profit - entry) / entry) * 100
        elif direction == "Медвежий сигнал":
            emoji = "📉"
            # Расчет прибыльности для медвежьего сигнала
            profit_percent = ((entry - take_profit) / entry) * 100
        else:
            emoji = "🔍"  # Нейтральный сигнал
            profit_percent = 0  # Если нейтрально, прибыльность не рассчитывается

        if entry and take_profit:
            caption = (
                f"{emoji} Монета: {symbol}\n"
                f"🔺 Направление: {direction}\n"
                f"🔢 Вероятность: {probability}\n"
                f"📍 Точка входа: {entry:.4f} USDT\n"
                f"🎯 Тейк-профит: {take_profit:.4f} USDT\n"
                f"💹 Потенциальная прибыльность: {profit_percent:.2f}%\n\n"
                f"📊 Поддержка: {support:.4f} USDT\n"
                f"📈 Сопротивление: {resistance:.4f} USDT"
            )
        else:
            caption = (
                f"{emoji} Монета: {symbol}\n"
                f"🔺 Направление: {direction}\n"
                f"🔢 Вероятность: {probability}"
            )

        # Добавление изображения и подписи в медиа-группу
        try:
            with open(image_path, 'rb') as photo_file:
                media_group.append(InputMediaPhoto(photo_file, caption=caption))
        except Exception as e:
            logging.error(f"Ошибка при открытии изображения {image_path}: {e}")
            continue

        # Проверка, достигло ли количество медиа в группе лимита (10)
        if len(media_group) == 10:
            logging.info(f"Отправка группы из {len(media_group)} графиков в Telegram.")
            await send_media_group_with_retry_helper(bot, chat_id, media_group, retries, delay)
            media_group = []
            await asyncio.sleep(2)  # Увеличенная задержка между отправками

    # Отправка оставшихся графиков
    if media_group:
        logging.info(f"Отправка последней группы из {len(media_group)} графиков в Telegram.")
        await send_media_group_with_retry_helper(bot, chat_id, media_group, retries, delay)
        media_group = []
        await asyncio.sleep(2)  # Увеличенная задержка

async def send_media_group_with_retry_helper(bot, chat_id, media_group, retries, delay):
    attempt = 0
    while attempt < retries:
        try:
            await bot.send_media_group(chat_id=chat_id, media=media_group)
            logging.info(f"Группа медиа успешно отправлена (Попытка {attempt + 1}).")
            return
        except RetryAfter as e:
            logging.warning(f"Превышен лимит отправки. Попытка {attempt + 1} через {e.retry_after} секунд.")
            await asyncio.sleep(e.retry_after)
        except TimedOut:
            logging.warning(f"Таймаут при отправке группы медиа. Попытка {attempt + 1} через {delay} секунд.")
            await asyncio.sleep(delay)
        except TelegramError as e:
            logging.error(f"TelegramError при отправке группы медиа: {e}. Попытка {attempt + 1} через {delay} секунд.")
            await asyncio.sleep(delay)
        except Exception as e:
            logging.error(f"Неизвестная ошибка при отправке группы медиа: {e}. Попытка {attempt + 1} через {delay} секунд.")
            await asyncio.sleep(delay)
        attempt += 1

    logging.error("Не удалось отправить группу медиа после нескольких попыток.")

# Функция для обработки каждого символа
async def process_symbol(session, symbol, semaphore, executor):
    async with semaphore:
        try:
            df = await get_historical_klines(session, symbol, '1h', 72)
            if df is None or len(df) < 50:  # Убедиться, что данных достаточно для SMA50
                logging.warning(f"Недостаточно данных для анализа {symbol}. Пропуск.")
                return None

            # Добавляем проверку объема торгов
            if not is_volume_sufficient(df):
                logging.info(f"Объем торгов для {symbol} ниже порога. Пропуск.")
                return None

            # Добавляем проверку вариации цен
            if not has_sufficient_price_variation(df):
                logging.info(f"Вариация цен для {symbol} ниже порога. Пропуск.")
                return None

            # Вычисление индикаторов (ограничено потоком для безопасности с Pandas и TA)
            loop = asyncio.get_event_loop()
            df = await loop.run_in_executor(executor, compute_indicators, df)

            # Анализ индикаторов
            phase, direction, probability, entry_price, take_profit = await loop.run_in_executor(executor, analyze_indicators, df)

            # Пропустить, если не в фазе баланса или вероятность не 100%
            if phase != "Баланс" or probability != "100.0000% вероятность":
                logging.info(f"Монета {symbol} не удовлетворяет критериям (фаза: {phase}, вероятность: {probability}). Пропуск.")
                return None

            # Определение границ диапазона баланса
            window_hours = 24
            df_recent = df.tail(window_hours)
            support = df_recent['Low'].min()
            resistance = df_recent['High'].max()

            # Построение графика с точками входа и тейк-профита (ограничено потоком для безопасности с Matplotlib и PIL)
            image_path = await loop.run_in_executor(executor, plot_candlestick, df, symbol, entry_price, take_profit, support, resistance)
            if image_path is None:
                logging.warning(f"График для {symbol} не создан. Пропуск.")
                return None

            # Формирование информации для Telegram
            return {
                'symbol': symbol,
                'direction': direction,
                'probability': probability,
                'image': image_path,
                'entry': entry_price,
                'take_profit': take_profit,
                'support': support,
                'resistance': resistance
            }

        except Exception as e:
            logging.error(f"Ошибка при обработке символа {symbol}: {e}")
            return None

# Основная функция
async def main():
    try:
        # Очистка предыдущих графиков перед запуском
        clean_previous_graphs()

        # Создание сессии aiohttp
        connector = aiohttp.TCPConnector(limit=100)  # Настройте лимит под ваши нужды
        async with aiohttp.ClientSession(connector=connector) as session:
            symbols = await get_future_symbols(session)
            balance_symbols = []

            # Ограничение количества одновременных задач
            semaphore = asyncio.Semaphore(20)  # Настройте под ваши нужды
            executor = ThreadPoolExecutor(max_workers=10)  # Настройте под ваши нужды

            # Создание списка задач для обработки символов
            tasks = [
                process_symbol(session, symbol, semaphore, executor) for symbol in symbols
            ]

            # Параллельное выполнение задач
            results = await asyncio.gather(*tasks)

            # Обработка результатов
            for result in results:
                if result is None:
                    continue
                balance_symbols.append(result)

            if balance_symbols:
                # Отправка отчетных графиков в Telegram
                logging.info(f"Отправка отчета с {len(balance_symbols)} монетами в фазе баланса.")
                bullish = [s for s in balance_symbols if s['direction'] == "Бычий сигнал"]
                bearish = [s for s in balance_symbols if s['direction'] == "Медвежий сигнал"]
                await send_telegram_report(config.TELEGRAM_TOKEN, config.CHAT_ID, bullish, bearish)
            else:
                logging.info("Нет монет, соответствующих критериям для отчета.")

            logging.info("Процесс завершен успешно.")
    except Exception as e:
        logging.error(f"Процесс завершился с ошибкой: {e}")

if __name__ == '__main__':
    asyncio.run(main()) убери с графика индикатор обьема