у меня есть скрипт , посмотри его и если возможно ускорить обработку монет пришли полный обновленный код # bot.py

import logging
import requests
import pandas as pd
import numpy as np  # Добавлен импорт numpy
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
from ta.momentum import RSIIndicator, StochasticOscillator
from ta.volatility import BollingerBands
import os
import glob
from PIL import Image, __version__ as PILLOW_VERSION
from packaging import version
from telegram.error import TelegramError, RetryAfter, TimedOut

# Настройка логирования
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
def get_future_symbols():
    try:
        logging.info("Запрос списка фьючерсных пар USDT от Binance.")
        url = 'https://fapi.binance.com/fapi/v1/exchangeInfo'
        response = requests.get(url)
        response.raise_for_status()
        data = 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 requests.exceptions.RequestException as e:
        logging.error(f"Ошибка при запросе списка символов: {e}")
        raise
    except Exception as e:
        logging.error(f"Неизвестная ошибка при обработке списка символов: {e}")
        raise


# Функция для получения исторических данных от Binance Futures
def get_historical_klines(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  # Максимальное количество данных
        }
        response = requests.get(url, params=params)
        response.raise_for_status()
        data = 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)
        logging.info(f"Данные успешно получены для {symbol} (строк: {len(df)}).")
        return df[['Open time', 'Open', 'High', 'Low', 'Close', 'Volume']]
    except requests.exceptions.RequestException 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)
        sma50 = SMAIndicator(close=df['Close'], window=50)
        df['SMA50'] = sma50.sma_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()

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


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

        signals = {
            'RSI': None,
            'MACD': None,
            'Скользящая средняя (SMA50)': None,
            'Полосы Боллинджера': None,
            'Стохастический осциллятор': None
        }

        # RSI
        if latest['RSI'] > 70:
            signals['RSI'] = 'Медвежий сигнал'
        elif latest['RSI'] < 30:
            signals['RSI'] = 'Бычий сигнал'

        # MACD
        if latest['MACD'] > latest['MACD_Signal']:
            signals['MACD'] = 'Бычий сигнал'
        elif latest['MACD'] < latest['MACD_Signal']:
            signals['MACD'] = 'Медвежий сигнал'

        # Скользящая средняя (SMA50)
        if latest['SMA50'] > latest['Close']:
            signals['Скользящая средняя (SMA50)'] = 'Медвежий сигнал'
        elif latest['SMA50'] < latest['Close']:
            signals['Скользящая средняя (SMA50)'] = 'Бычий сигнал'

        # Полосы Боллинджера
        if latest['Close'] > latest['BB_High']:
            signals['Полосы Боллинджера'] = 'Медвежий сигнал'
        elif latest['Close'] < latest['BB_Low']:
            signals['Полосы Боллинджера'] = 'Бычий сигнал'

        # Стохастический осциллятор
        if latest['Stoch_%K'] > latest['Stoch_%D']:
            signals['Стохастический осциллятор'] = 'Бычий сигнал'
        elif latest['Stoch_%K'] < latest['Stoch_%D']:
            signals['Стохастический осциллятор'] = 'Медвежий сигнал'

        # Подсчет сигналов
        bullish = sum(1 for signal in signals.values() if signal == 'Бычий сигнал')
        bearish = sum(1 for signal in signals.values() if signal == 'Медвежий сигнал')
        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% вероятность"

        logging.info(f"Определенное направление: {direction} с вероятностью {probability}.")
        return direction, probability

    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 compress_image(image_path, max_size=(800, 600), quality=70):
    """
    Сжимает изображение и сохраняет его обратно.

    :param image_path: Путь к изображению.
    :param max_size: Максимальный размер изображения.
    :param quality: Качество сжатия.
    :return: None
    """
    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}")
    except Exception as e:
        logging.error(f"Ошибка при сжатии изображения {image_path}: {e}")


# Функция для построения графика
def plot_candlestick(df, symbol):
    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)
        filename = os.path.join(GRAPHS_DIR, f"{symbol}_72h.png")

        # Построение свечей
        mpf.plot(
            df,
            type='candle',
            style=s,
            title=f'{symbol} - Последние 72 часа',
            ylabel='Цена (USDT)',
            volume=True,
            savefig=filename,
            tight_layout=True,
            returnfig=False,
            show_nontrading=False,
            figsize=(12, 8)
        )

        # Сжатие изображения перед отправкой
        compress_image(filename)

        logging.info(f"График сохранен как {filename}.")
        return filename
    except Exception as e:
        logging.error(f"Ошибка при построении графика для {symbol}: {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: Список символов с информацией (направление, вероятность, изображение).
    :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']

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

        # Создание подписи для графика
        caption = (
            f"📈 Монета: {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 main():
    try:
        # Очистка предыдущих графиков перед запуском
        clean_previous_graphs()

        symbols = get_future_symbols()
        bullish_symbols = []
        bearish_symbols = []

        for symbol in symbols:
            df = get_historical_klines(symbol, '1h', 72)
            if df is None or len(df) < 50:  # Убедиться, что данных достаточно для SMA50
                logging.warning(f"Недостаточно данных для анализа {symbol}. Пропуск.")
                continue

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

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

            df = compute_indicators(df)
            direction, probability = analyze_indicators(df)

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

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

                    # Построение графика
                    image_path = plot_candlestick(df, symbol)
                    if image_path is None:
                        logging.warning(f"График для {symbol} не создан. Пропуск.")
                        continue

                    # Добавление в соответствующий список
                    if direction == "Бычий тренд":
                        bullish_symbols.append({
                            'symbol': symbol,
                            'direction': direction,
                            'probability': probability,
                            'image': image_path
                        })
                    elif direction == "Медвежий тренд":
                        bearish_symbols.append({
                            'symbol': symbol,
                            'direction': direction,
                            'probability': probability,
                            'image': image_path
                        })

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