отсортируй монеты только с 100% вероятностью # bot.py

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
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()

        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]

        signals = {
            'RSI': None,
            'MACD': None,
            'EMA20': None,
            'Bollinger Bands': None,
            'Stochastic Oscillator': None,
            'Ichimoku': None,
            'VWAP': None
        }

        # RSI
        if latest['RSI'] > 70:
            signals['RSI'] = 'Overbought'
        elif latest['RSI'] < 30:
            signals['RSI'] = 'Oversold'

        # MACD
        if latest['MACD'] > latest['MACD_Signal']:
            signals['MACD'] = 'Bullish Crossover'
        elif latest['MACD'] < latest['MACD_Signal']:
            signals['MACD'] = 'Bearish Crossover'

        # EMA20 vs. Close Price
        if latest['Close'] > latest['EMA20']:
            signals['EMA20'] = 'Price Above EMA20'
        else:
            signals['EMA20'] = 'Price Below EMA20'

        # Bollinger Bands
        if latest['Close'] > latest['BB_High']:
            signals['Bollinger Bands'] = 'Price Breaks Upper Band'
        elif latest['Close'] < latest['BB_Low']:
            signals['Bollinger Bands'] = 'Price Breaks Lower Band'

        # Stochastic Oscillator
        if latest['Stoch_%K'] > latest['Stoch_%D']:
            signals['Stochastic Oscillator'] = 'Bullish Signal'
        elif latest['Stoch_%K'] < latest['Stoch_%D']:
            signals['Stochastic Oscillator'] = 'Bearish Signal'

        # Ichimoku Cloud
        if latest['Close'] > latest['Ichimoku_A'] and latest['Close'] > latest['Ichimoku_B']:
            signals['Ichimoku'] = 'Above Cloud - Bullish'
        elif latest['Close'] < latest['Ichimoku_A'] and latest['Close'] < latest['Ichimoku_B']:
            signals['Ichimoku'] = 'Below Cloud - Bearish'
        else:
            signals['Ichimoku'] = 'Within Cloud - Neutral'

        # VWAP
        if latest['Close'] > latest['VWAP']:
            signals['VWAP'] = 'Price Above VWAP'
        else:
            signals['VWAP'] = 'Price Below VWAP'

        # Подсчет сигналов
        bullish_signals = [
            'Bullish Crossover', 'Price Above EMA20',
            'Price Breaks Upper Band', 'Bullish Signal',
            'Above Cloud - Bullish', 'Price Above VWAP'
        ]
        bearish_signals = [
            'Bearish Crossover', 'Price Below EMA20',
            'Price Breaks Lower Band', 'Bearish Signal',
            'Below Cloud - Bearish', 'Price Below VWAP'
        ]

        bullish = sum(1 for signal in signals.values() if signal in bullish_signals)
        bearish = sum(1 for signal in signals.values() if signal in bearish_signals)

        total = bullish + bearish

        if total == 0:
            direction = "Нейтрально"
            probability = "N/A"
        else:
            if bullish > bearish:
                direction = "Бычий тренд"
                probability = f"{(bullish / total) * 100:.2f}% вероятность"
            elif bearish > bullish:
                direction = "Медвежий тренд"
                probability = f"{(bearish / total) * 100:.2f}% вероятность"
            else:
                direction = "Нейтрально"
                probability = "50% вероятность"

        # Определение точки входа и тейк-профита на основе ATR
        entry_price = None
        take_profit = None

        if direction == "Бычий тренд":
            entry_price = latest['Close']
            take_profit = latest['Close'] + (latest['ATR'] * 2)  # Тейк-профит на 2 ATR выше
        elif direction == "Медвежий тренд":
            entry_price = latest['Close']
            take_profit = latest['Close'] - (latest['ATR'] * 2)  # Тейк-профит на 2 ATR ниже

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

        return 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):
    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 entry and take_profit:
            ax = axlist[0]  # Основной график свечей
            ax.axhline(entry, linestyle='--', color='blue', linewidth=1)
            ax.axhline(take_profit, linestyle='--', color='orange', linewidth=1)

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

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

        # Создание подписи для графика
        if direction == "Бычий тренд":
            emoji = "📈"
        elif direction == "Медвежий тренд":
            emoji = "📉"
        else:
            emoji = "🔍"  # Нейтральный тренд

        if entry and take_profit:
            caption = (
                f"{emoji} Монета: {symbol}\n"
                f"🔺 Направление: {direction}\n"
                f"🔢 Вероятность: {probability}\n"
                f"📍 Точка входа: {entry:.2f} USDT\n"
                f"🎯 Тейк-профит: {take_profit:.2f} 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)

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

            # Проверка вероятности продолжения тренда
            if probability != "N/A":
                try:
                    prob_value = float(probability.split('%')[0])
                except ValueError:
                    logging.warning(f"Некорректное значение вероятности для {symbol}: {probability}. Пропуск.")
                    return None

                if prob_value >= 80.0:
                    logging.info(f"Монета {symbol} соответствует критерию (prob={probability}).")

                    # Построение графика с точками входа и тейк-профита (ограничено потоком для безопасности с Matplotlib и PIL)
                    image_path = await loop.run_in_executor(executor, plot_candlestick, df, symbol, entry_price, take_profit)
                    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
                    }

            return None

        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)
            bullish_symbols = []
            bearish_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
                if result['direction'] == "Бычий тренд":
                    bullish_symbols.append(result)
                elif result['direction'] == "Медвежий тренд":
                    bearish_symbols.append(result)

            if bullish_symbols or bearish_symbols:
                # Отправка отчетных графиков в Telegram
                logging.info(f"Отправка отчета с {len(bullish_symbols)} монетами для роста и {len(bearish_symbols)} монетами для падения.")
                await send_telegram_report(config.TELEGRAM_TOKEN, config.CHAT_ID, bullish_symbols, bearish_symbols)
            else:
                logging.info("Нет монет, соответствующих критериям для отчета.")

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


if __name__ == '__main__':
    asyncio.run(main())