Бот на языке Python с управлением через веб-интерфейсом для мониторинга плотностей криптовалют на Binance и торговли по стратегии "пробоя" и "отскока" от плотностей. Функции:
1) Автоматическое формирование списка криптовалютных пар по кнопке - бот запрашивает список криптовалют на фьючерсном и спотовом рынке с USDT. Те пары на спотовом рынке которые не имеют аналогов на фьючерсном рынке удаляются из списка для мониторинга. Стоит учитывать префиксы 1000 - например на спотовым рынке есть монета SHIBUSDT, а ее аналог на фьючерсном рынке 1000SHIBUSDT (в котором курс уже не за 1 монету а за 1000). 
2) Список исключение - поле в котором можно задать список монет которые будут удалены из списка мониторинга даже если были отобраны в автоматическом режиме - здесь вписывается не полная пара с USDT, а только монеты через запятую. (Пример: BTC, ETH, BNB (будут исключены все эти пары с USDT из мониторинга на спотовом и фьючерсном рынках)).
3) Ручной список монет - поле в котором можно задать список монет только которые будут мониториться - здесь вписывается не полная пара с USDT, а только монеты через запятую. (Пример: BTC, ETH, BNB (будут мониториться все эти пары с USDT на спотовом и фьючерсном рынках)). При включении ручного мониторинга во время работы автоматического мониторинга делается остановка мониторинга на 5 секунда с очисткой кеша, и запуск мониторинга по монетам введенным с поле с ручным списком.
4) Должна быть кнопка остановки мониторинга вручную.
5) Программа берет API ключ из файла binance_api.txt в той же папке с исполняемым файлом py, ключ будет в формате: 
API_KEY = 'ключ'
API_SECRET = 'секрет'
6) После запуска мониторинга через WebSocket ежесекундно получаем рыночные цены всех монет которые мониторятся в данный момент (они будут нужны для вычисления значения столбца 6 "До уровня %" нашей таблицы).
7) После запуска формирования автоматического списка или вручную списка криптовалют с USDT - программа через WebSocket получает начальную книгу ордеров по всем монетам на спотовом и фьючерсном рынке с максимальной глубиной для WebSocket (кажется 20). После получения первой начальной книги ордеров по всем монетам программа анализирует сполько для каждой монеты нужно шагов цены чтобы покрыть глубину ордеров 3% от текущей цены и последующие запросы выполняет через REST API на требуюмую глубину для каждой монеты чтобы покрыть 3%. Следует так же сформировать очередь запросов так как существует лимит запросов по спотовому и фьючерсному рынку через REST API в минуту, чтобы не превысить лимит. Я понимаю что получение книг ордеров по всем монетам через REST API с глубиной 3% не превышая лимит, займет несколько минут. Меня устроит полный период обновления по всем монетам раз в 5 минут.
8) После запуска мониторинга мы через WebSocket получаем 12 последних 5-минутных объемов торгов по каждый монете, вычисляем самый крупный объем из этих 12 5-ти минуток, 11 последних 5-ти минуток по каждой монете помещаем в кеш, а последнюю постоянно обновляем каждую 1 минуту, пока последняя 5-ти минутка не закроется (закрываются всегда в 00, 05, 10, 15 и тп минут по мировому времени) и не попадет в кеш и затем будет обновляться новая 5-ти минутка. Самый крупный объем из 5-ти минуток необходим для вычисления времени разъедания по конкретной монете, пример по монете BTC, самый крупный 5-ти минутный объем мы находим 10BTC, то есть если будет требоваться время разъедания не менее 10 минут, то это не менее 20BTC (10BTC/5минут*10минут).
8) На нашей веб форме будет темный фоновый стиль страницы, сверху будут кнопки управления торговлей и мониторингом, а ниже таблица состоящая из 6 стобцов, которая будет сдублирована (слева и справа - поровну по количеству плотностей в ней). Стобцы будут следующие: 1-й стобец - "Монета" (здесь будет монета по которой имеется плотность и название рынка, а также в скобках плечо (только для фьючерсного рынка), например: BTC(spot) или ETH(futures Х50), 2-й стобец "Объем" (здесь указывается объем найденной и отобранной плотностей в USDT ($) с округлением по тысяч $ (то есть не 256423, а 256k), а также % от ее начального объема при котором она была найденая впервые и помещена в кеш. То есть если плотность изначально была 200000$(100%), то это 100%, а потом плотность стала 2200000$(110%), так же в зависимости от объема окрашивать данную ячейку в цвет : более 100000$ - серый, более 250000$ - коричневый, более 500000$ - желтый, более 1000000$ - фиолетовый, более 3000000$ - бирюзовый), 3-й столбец "Цена" (здесь будет указана цена по которой стоит эта плотность в стакане заявок книги ордеров), 4-й стобец "Время разъедания" (здесь будет указано вычисленное время разъедания нашей плотности исходя из данных по максимальному объему 12 последних 5-ти минутных объемов торгов (указывал выше алгоритм вычисления), округленное до целых минут, 5-й столбец "Монет в уровне" (здесь будет указано количество монет в плотности, с округлением по такому правилу чтобы не было более 4-х цифр, то есть если число монет более 4-х цифр, то есть 5 и более то кругляется с приставой (K -тысячи, M-миллионы) (пример  монет 15443 - 15.44K, монет 3543523 - 3.543M.), 6-й стобец "До уровня %" (здесь расчитывается как далеко наша плотность находится от текущей рыночной цены (которая обновляется раз в секунду и соответственно значение этого столбца должно обновляться то же раз в 1 секунду с обновлением рыночной цены монеты) в процентах и если она находится ниже рыночной цены то процент будет со знакомы минус, если выше то просто число процента (пример: -1.54% или 2.45%), значение округлять до 2-х знаков после запятой, а так же если процент является отрицательным то ячейку "Монета" и данную "До уровня %" данной строки окрашивать в красный, если процент положительных то в зеленый.
9) Все значения в ячейках таблицы (цифры и названия монет - черным цветом).
10) По каждой монете выводить в таблицу не более 2-х плотностей (одна выше рыночной цены и одна ниже), для спотовой монеты и фьючерсной отдельные лимиты (например могут быть выведены 2 плотности по BTCUSDT(spot) и 2 плотности BTCUSDT(futures X50)), следует выводить плотности которые прошли отбор и наиболее близки к рыночной цене.
11) Сортировать данные в таблице по алфавитному порядку названия монеты
12) Элемент торговли пока отключен, мы его реализуем в будущем.
13) При написании кода ты должен обновить свои данные об актуальных методах, лимитах и остальной информации по использованию REST API Binance и WebSocket Binance
14) Мы ранее с тобой писали код, могу предоставить ниже код для файлов python и html которые мы сделали

Мы используем версию python-binance-1.0.22 и код нужно писать под эту версию.
Вот код python:
import os
import sys
import asyncio
import threading
import time
import logging
from datetime import datetime, timedelta
from collections import deque
from binance import AsyncClient, BinanceSocketManager
from binance.exceptions import BinanceAPIException
from flask import Flask, render_template, request, redirect, url_for, jsonify
import queue
from binance.client import Client  # Корректный импорт

# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Отключение логгера Flask для предотвращения вывода сообщений о запросах
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.setLevel(logging.ERROR)

# Параметры бота
MAX_OPEN_POSITIONS = 5
POSITION_SIZE_USDT = 10.0
TRADING_ALLOWED = False
monitoring_active = False
min_density_volume = 100000.0  # Объем плотности в $
min_consumption_time = 10.0    # Время разъедания в минутах

# Инициализация глобальных переменных
selected_symbols = []
excluded_symbols = []
symbols_to_monitor = []
order_book_data_spot = {}
order_book_data_futures = {}
open_positions = {}
density_history = {}
current_price_cache = {}
symbol_leverages = {}
max_5min_volumes = {}
five_min_volumes = {}
order_book_update_queue_spot = queue.Queue()
order_book_update_queue_futures = queue.Queue()

# Лимиты запросов для спота и фьючерсов
SPOT_REQUEST_LIMIT = 1200  # Запросов в минуту
FUTURES_REQUEST_LIMIT = 2400  # Запросов в минуту

# Флаг для ручного мониторинга
manual_monitoring = False

# Блокировка для синхронизации доступа к общим данным
data_lock = threading.Lock()

# Глобальные переменные для асинхронных клиентов и менеджеров сокетов
async_client = None
binance_socket_manager = None
asyncio_loop = None

# Функция для загрузки API-ключей из файла binance_api.txt
def load_api_keys():
    api_key = None
    api_secret = None
    try:
        with open('binance_api.txt', 'r') as f:
            for line in f:
                line = line.strip()
                if line.startswith('API_KEY'):
                    api_key = line.split('=')[1].strip().strip("'\"")
                elif line.startswith('API_SECRET'):
                    api_secret = line.split('=')[1].strip().strip("'\"")
        if not api_key or not api_secret:
            raise ValueError("API_KEY или API_SECRET не найдены в файле binance_api.txt")
        return api_key, api_secret
    except FileNotFoundError:
        logger.error("Файл binance_api.txt не найден")
        sys.exit(1)
    except Exception as e:
        logger.error(f"Ошибка при чтении файла binance_api.txt: {e}")
        sys.exit(1)

# Загрузка API-ключей
API_KEY, API_SECRET = load_api_keys()

# Инициализация синхронных клиентов
try:
    client_sync = Client(API_KEY, API_SECRET)  # Спотовый клиент
    client_futures_sync = Client(API_KEY, API_SECRET, options={'defaultType': 'future'})  # Фьючерсный клиент
except TypeError as e:
    logger.error(f"Ошибка инициализации Client: {e}")
    sys.exit(1)
except Exception as e:
    logger.error(f"Неожиданная ошибка при инициализации Client: {e}")
    sys.exit(1)

# Создание приложения Flask
app = Flask(__name__)

# Асинхронная функция для подписки на потоки
async def subscribe_to_streams(client, symbols_to_monitor):
    global binance_socket_manager
    binance_socket_manager = BinanceSocketManager(client)
    
    tasks = []
    
    for pair in symbols_to_monitor:
        symbol_spot = pair.get('spot')
        symbol_futures = pair.get('futures')
        
        if symbol_spot:
            # Подписка на поток цен спота
            tasks.append(binance_socket_manager.symbol_ticker_socket(symbol_spot.lower(), handle_price_update_spot))
            
            # Подписка на свечи 5m для спота
            tasks.append(binance_socket_manager.kline_socket(symbol_spot.lower(), handle_kline_spot, '5m'))
            
            # Подписка на поток книги ордеров с глубиной 20 для спота
            tasks.append(binance_socket_manager.depth_socket(symbol_spot.lower(), handle_initial_depth_stream_spot, depth=20))
        
        if symbol_futures:
            # Подписка на поток цен фьючерсов
            tasks.append(binance_socket_manager.symbol_ticker_socket(symbol_futures.lower(), handle_price_update_futures))
            
            # Подписка на свечи 5m для фьючерсов
            tasks.append(binance_socket_manager.kline_socket(symbol_futures.lower(), handle_kline_futures, '5m'))
            
            # Подписка на поток книги ордеров с глубиной 20 для фьючерсов
            tasks.append(binance_socket_manager.depth_socket(symbol_futures.lower(), handle_initial_depth_stream_futures, depth=20))
    
    await asyncio.gather(*tasks)

# Функции-обработчики WebSocket сообщений

def handle_price_update_spot(msg):
    try:
        symbol = msg['s']
        price = float(msg['c'])
        with data_lock:
            current_price_cache[symbol.upper()] = price
    except Exception as e:
        logger.error(f"Ошибка в handle_price_update_spot: {e}")

def handle_price_update_futures(msg):
    try:
        symbol = msg['s']
        price = float(msg['c'])
        with data_lock:
            current_price_cache[symbol.upper()] = price
    except Exception as e:
        logger.error(f"Ошибка в handle_price_update_futures: {e}")

def handle_kline_spot(msg):
    try:
        symbol = msg['s']
        is_candle_closed = msg['k']['x']
        volume = float(msg['k']['q'])  # Объем в quoteAsset (USDT)
        with data_lock:
            if symbol.upper() not in five_min_volumes:
                five_min_volumes[symbol.upper()] = deque(maxlen=12)
            if is_candle_closed:
                five_min_volumes[symbol.upper()].append(volume)
                max_5min_volumes[symbol.upper()] = max(five_min_volumes[symbol.upper()])
            else:
                # Обновляем текущую свечу
                if len(five_min_volumes[symbol.upper()]) == 0:
                    five_min_volumes[symbol.upper()].append(volume)
                else:
                    five_min_volumes[symbol.upper()][-1] = volume
                max_5min_volumes[symbol.upper()] = max(five_min_volumes[symbol.upper()])
    except Exception as e:
        logger.error(f"Ошибка в handle_kline_spot: {e}")

def handle_kline_futures(msg):
    try:
        symbol = msg['s']
        is_candle_closed = msg['k']['x']
        volume = float(msg['k']['q'])  # Объем в quoteAsset (USDT)
        with data_lock:
            if symbol.upper() not in five_min_volumes:
                five_min_volumes[symbol.upper()] = deque(maxlen=12)
            if is_candle_closed:
                five_min_volumes[symbol.upper()].append(volume)
                max_5min_volumes[symbol.upper()] = max(five_min_volumes[symbol.upper()])
            else:
                # Обновляем текущую свечу
                if len(five_min_volumes[symbol.upper()]) == 0:
                    five_min_volumes[symbol.upper()].append(volume)
                else:
                    five_min_volumes[symbol.upper()][-1] = volume
                max_5min_volumes[symbol.upper()] = max(five_min_volumes[symbol.upper()])
    except Exception as e:
        logger.error(f"Ошибка в handle_kline_futures: {e}")

def handle_initial_depth_stream_spot(msg):
    try:
        symbol = msg['s']
        bids = msg['b']
        asks = msg['a']
        with data_lock:
            order_book_data_spot[symbol.upper()] = {'bids': bids, 'asks': asks}
            # Добавляем в очередь на обновление книги ордеров через REST API
            order_book_update_queue_spot.put(symbol.upper())
    except Exception as e:
        logger.error(f"Ошибка в handle_initial_depth_stream_spot: {e}")

def handle_initial_depth_stream_futures(msg):
    try:
        symbol = msg['s']
        bids = msg['b']
        asks = msg['a']
        with data_lock:
            order_book_data_futures[symbol.upper()] = {'bids': bids, 'asks': asks}
            # Добавляем в очередь на обновление книги ордеров через REST API
            order_book_update_queue_futures.put(symbol.upper())
    except Exception as e:
        logger.error(f"Ошибка в handle_initial_depth_stream_futures: {e}")

# Функции для обработки очередей обновления книг ордеров

def process_order_book_updates_spot():
    while monitoring_active:
        try:
            symbol = order_book_update_queue_spot.get(timeout=1)
            update_order_book(symbol, 'spot')
            time.sleep(60.0 / SPOT_REQUEST_LIMIT)  # Ждем, чтобы не превысить лимит
        except queue.Empty:
            continue

def process_order_book_updates_futures():
    while monitoring_active:
        try:
            symbol = order_book_update_queue_futures.get(timeout=1)
            update_order_book(symbol, 'futures')
            time.sleep(60.0 / FUTURES_REQUEST_LIMIT)  # Ждем, чтобы не превысить лимит
        except queue.Empty:
            continue

async def update_order_book_async(client, symbol, market_type):
    try:
        # Получаем текущую цену
        with data_lock:
            current_price = current_price_cache.get(symbol)
        if not current_price:
            logger.warning(f"Текущая цена для {symbol} не найдена")
            return

        # Рассчитываем цену на 3% от текущей
        price_threshold_high = current_price * 1.03
        price_threshold_low = current_price * 0.97

        # Максимальный допустимый limit
        max_limit = 1000

        # Инициализируем depth
        depth = None

        levels = 20  # Начальное значение

        while levels <= max_limit:
            # Получаем книгу ордеров с заданным уровнем глубины
            if market_type == 'spot':
                depth = await client.get_order_book(symbol=symbol, limit=levels)
            else:
                depth = await client.futures_order_book(symbol=symbol, limit=levels)

            # Проверяем, покрывает ли книга ордеров 3% диапазон
            bids = depth.get('bids', [])
            asks = depth.get('asks', [])

            if not bids or not asks:
                break

            # Рассчитываем объем на 3%
            cumulative_bid = sum(float(bid[1]) for bid in bids if float(bid[0]) >= price_threshold_low)
            cumulative_ask = sum(float(ask[1]) for ask in asks if float(ask[0]) <= price_threshold_high)

            if (cumulative_bid >= min_density_volume * 0.03 and
                cumulative_ask >= min_density_volume * 0.03):
                break  # Достигли необходимой глубины

            levels *= 2  # Увеличиваем уровни

        # Сохраняем обновленную книгу ордеров
        with data_lock:
            if market_type == 'spot':
                order_book_data_spot[symbol] = depth
            else:
                order_book_data_futures[symbol] = depth

        logger.info(f"Обновлена книга ордеров для {symbol} ({market_type}) с глубиной {levels}")

    except BinanceAPIException as e:
        logger.error(f"BinanceAPIException при обновлении книги ордеров для {symbol} ({market_type}): {e}")
    except Exception as e:
        logger.error(f"Ошибка при обновлении книги ордеров для {symbol} ({market_type}): {e}")

def update_order_book(symbol, market_type):
    if not async_client or async_client.is_closed:
        logger.warning(f"AsyncClient закрыт. Невозможно обновить книгу ордеров для {symbol} ({market_type})")
        return
    asyncio.run_coroutine_threadsafe(update_order_book_async(async_client, symbol, market_type), asyncio_loop)

# Функции для получения символов

def get_common_symbols():
    spot_symbols = set()
    prefixes = ['1000']  # Возможные префиксы на фьючерсах

    # Получаем все спотовые символы, заканчивающиеся на USDT
    spot_info = client_sync.get_exchange_info()
    for s in spot_info['symbols']:
        if s['quoteAsset'] == 'USDT' and s['status'] == 'TRADING':
            spot_symbols.add(s['symbol'])

    # Получаем все фьючерсные символы, заканчивающиеся на USDT
    futures_info = client_futures_sync.futures_exchange_info()
    futures_symbol_map = {}
    for s in futures_info['symbols']:
        if s['quoteAsset'] == 'USDT' and s['status'] == 'TRADING':
            futures_symbol = s['symbol']
            base_asset = s['baseAsset']
            # Удаляем возможные префиксы
            base_asset_clean = base_asset
            for prefix in prefixes:
                if base_asset.startswith(prefix):
                    base_asset_clean = base_asset[len(prefix):]
                    break
            futures_symbol_map[base_asset_clean.upper()] = futures_symbol

    # Сопоставляем символы
    common_symbols = []
    for spot_symbol in spot_symbols:
        if spot_symbol.endswith('USDT'):
            base_asset = spot_symbol.replace('USDT', '').upper()
            # Проверяем наличие в фьючерсах
            if base_asset in futures_symbol_map:
                futures_symbol = futures_symbol_map[base_asset]
                common_symbols.append({
                    'spot': spot_symbol.upper(),
                    'futures': futures_symbol.upper()
                })

    return common_symbols

def get_manual_symbols():
    symbols = []
    for symbol in selected_symbols:
        if symbol not in excluded_symbols:
            spot_symbol = f"{symbol}USDT".upper()
            # Проверяем наличие соответствующего фьючерсного символа
            futures_symbol = f"1000{symbol}USDT".upper()
            # Проверка существует ли такой символ
            try:
                client_futures_sync.futures_exchange_info(symbol=futures_symbol)
                symbols.append({
                    'spot': spot_symbol,
                    'futures': futures_symbol
                })
            except BinanceAPIException:
                symbols.append({
                    'spot': spot_symbol,
                    'futures': None  # Не существует фьючерсного символа
                })
    return symbols

def get_symbols_to_monitor():
    if selected_symbols:
        return get_manual_symbols()
    else:
        return get_common_symbols()

# Асинхронная функция мониторинга и торговли

async def monitor_and_trade_async():
    while monitoring_active:
        densities = find_top_densities()
        update_density_history(densities)
        # Трейдинговая логика (отключена)
        if TRADING_ALLOWED:
            for density in densities:
                if check_entry_conditions(density):
                    if len(open_positions) < MAX_OPEN_POSITIONS:
                        open_position(density)
        await asyncio.sleep(1)

def run_tasks():
    global monitoring_active, async_client, asyncio_loop, symbols_to_monitor
    monitoring_active = True
    asyncio_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(asyncio_loop)
    asyncio_loop.create_task(run_async_tasks())
    asyncio_thread = threading.Thread(target=asyncio_loop.run_forever, daemon=True)
    asyncio_thread.start()
    # Запуск обработчиков очередей
    order_book_spot_thread = threading.Thread(target=process_order_book_updates_spot, daemon=True)
    order_book_futures_thread = threading.Thread(target=process_order_book_updates_futures, daemon=True)
    order_book_spot_thread.start()
    order_book_futures_thread.start()

def stop_tasks():
    global monitoring_active
    monitoring_active = False
    if asyncio_loop and not asyncio_loop.is_closed():
        # Запуск асинхронной функции закрытия клиентов
        asyncio.run_coroutine_threadsafe(shutdown_async_clients(), asyncio_loop)
        # Дать время для закрытия клиентов
        time.sleep(2)
        asyncio_loop.call_soon_threadsafe(asyncio_loop.stop)
    logger.info("Мониторинг остановлен")

async def shutdown_async_clients():
    global binance_socket_manager, async_client, client_sync, client_futures_sync
    try:
        if binance_socket_manager:
            binance_socket_manager = None  # Нет метода close, просто удаляем ссылку
        if async_client:
            await async_client.close_connection()
            async_client = None
        if client_sync:
            client_sync.close()  # Спотовый клиент закрывается корректно
        if client_futures_sync:
            client_futures_sync.close()  # Фьючерсный клиент закрывается корректно
    except Exception as e:
        logger.error(f"Ошибка при закрытии клиентов: {e}")

async def run_async_tasks():
    global async_client
    try:
        async_client = await AsyncClient.create(API_KEY, API_SECRET)
        subscribe_task = asyncio.create_task(subscribe_to_streams(async_client, symbols_to_monitor))
        monitor_task = asyncio.create_task(monitor_and_trade_async())
        await asyncio.gather(subscribe_task, monitor_task)
    except Exception as e:
        logger.error(f"Ошибка в run_async_tasks: {e}")

def update_density_history(densities):
    # Очищаем текущую историю
    with data_lock:
        density_history.clear()
        # Лимитируем до 2 плотностей на монету (одна выше, одна ниже)
        symbol_densities = {}
        for density in densities:
            symbol = density['symbol']
            if symbol not in symbol_densities:
                symbol_densities[symbol] = {'above': None, 'below': None}
            if density['distance'] > 0 and not symbol_densities[symbol]['above']:
                symbol_densities[symbol]['above'] = density
            elif density['distance'] < 0 and not symbol_densities[symbol]['below']:
                symbol_densities[symbol]['below'] = density

        # Заполняем density_history
        for symbol, d in symbol_densities.items():
            if d['above']:
                density_history[f"{symbol}_above"] = d['above']
            if d['below']:
                density_history[f"{symbol}_below"] = d['below']

def find_top_densities():
    densities = []
    with data_lock:
        symbols = symbols_to_monitor.copy()
    for pair in symbols:
        for market_type in ['spot', 'futures']:
            symbol = pair.get(market_type)
            if not symbol:
                continue
            with data_lock:
                if market_type == 'spot':
                    depth = order_book_data_spot.get(symbol)
                else:
                    depth = order_book_data_futures.get(symbol)
            if not depth:
                continue
            bids = depth.get('bids', [])
            asks = depth.get('asks', [])
            if not bids or not asks:
                continue
            with data_lock:
                current_price = current_price_cache.get(symbol)
            if not current_price:
                continue

            # Для bid (отскок)
            cumulative_bid = 0
            for bid in bids:
                price = float(bid[0])
                qty = float(bid[1])
                volume = price * qty
                cumulative_bid += volume
                percentage_diff = ((current_price - price) / current_price) * 100
                if percentage_diff >= 3:
                    break
                if cumulative_bid >= min_density_volume:
                    with data_lock:
                        max_volume = max_5min_volumes.get(symbol, 0)
                    if max_volume == 0:
                        break
                    consumption_time = (cumulative_bid / max_volume) * 5  # В минутах
                    if consumption_time >= min_consumption_time:
                        densities.append({
                            'symbol': symbol,
                            'market_type': market_type,
                            'price': price,
                            'qty': qty,
                            'total_value': round(cumulative_bid, 2),
                            'distance': round(percentage_diff, 2),
                            'consumption_time': round(consumption_time, 2),
                            'leverage': symbol_leverages.get(symbol, 1)
                        })
                    break

            # Для ask (пробой)
            cumulative_ask = 0
            for ask in asks:
                price = float(ask[0])
                qty = float(ask[1])
                volume = price * qty
                cumulative_ask += volume
                percentage_diff = ((price - current_price) / current_price) * 100
                if percentage_diff >= 3:
                    break
                if cumulative_ask >= min_density_volume:
                    with data_lock:
                        max_volume = max_5min_volumes.get(symbol, 0)
                    if max_volume == 0:
                        break
                    consumption_time = (cumulative_ask / max_volume) * 5  # В минутах
                    if consumption_time >= min_consumption_time:
                        densities.append({
                            'symbol': symbol,
                            'market_type': market_type,
                            'price': price,
                            'qty': qty,
                            'total_value': round(cumulative_ask, 2),
                            'distance': round(percentage_diff, 2),
                            'consumption_time': round(consumption_time, 2),
                            'leverage': symbol_leverages.get(symbol, 1)
                        })
                    break

    # Сортировка по алфавиту
    densities_sorted = sorted(densities, key=lambda x: x['symbol'])
    return densities_sorted

def check_entry_conditions(density):
    if density['total_value'] >= min_density_volume and density['consumption_time'] >= min_consumption_time:
        return True
    return False

def open_position(density):
    symbol = density['symbol']
    market_type = density['market_type']
    side = 'BUY' if density['distance'] > 0 else 'SELL'
    quantity = POSITION_SIZE_USDT / density['price']

    try:
        if market_type == 'spot':
            if side == 'BUY':
                order = asyncio.run_coroutine_threadsafe(
                    async_client.order_market_buy(symbol=symbol, quantity=quantity), asyncio_loop
                ).result()
            else:
                order = asyncio.run_coroutine_threadsafe(
                    async_client.order_market_sell(symbol=symbol, quantity=quantity), asyncio_loop
                ).result()
        else:
            if side == 'BUY':
                order = asyncio.run_coroutine_threadsafe(
                    async_client.futures_create_order(
                        symbol=symbol,
                        side='BUY',
                        type='MARKET',
                        quantity=quantity
                    ), asyncio_loop
                ).result()
            else:
                order = asyncio.run_coroutine_threadsafe(
                    async_client.futures_create_order(
                        symbol=symbol,
                        side='SELL',
                        type='MARKET',
                        quantity=quantity
                    ), asyncio_loop
                ).result()
        with data_lock:
            open_positions[symbol] = {
                'symbol': symbol,
                'side': side,
                'quantity': round(quantity, 8),
                'entry_price': density['price'],
                'current_price': density['price'],
                'pnl_percent': 0.0,
                'market_type': market_type
            }
        logger.info(f"Открыта позиция по {symbol} на {market_type} в сторону {side}")
    except BinanceAPIException as e:
        logger.error(f"BinanceAPIException при открытии позиции по {symbol}: {e}")
    except Exception as e:
        logger.error(f"Ошибка при открытии позиции по {symbol}: {e}")

def close_position(symbol):
    with data_lock:
        position = open_positions.get(symbol)
    if position:
        market_type = position.get('market_type', 'spot')
        side = 'SELL' if position['side'] == 'BUY' else 'BUY'
        quantity = position['quantity']

        try:
            if market_type == 'spot':
                if side == 'BUY':
                    order = asyncio.run_coroutine_threadsafe(
                        async_client.order_market_buy(symbol=symbol, quantity=quantity), asyncio_loop
                    ).result()
                else:
                    order = asyncio.run_coroutine_threadsafe(
                        async_client.order_market_sell(symbol=symbol, quantity=quantity), asyncio_loop
                    ).result()
            else:
                if side == 'BUY':
                    order = asyncio.run_coroutine_threadsafe(
                        async_client.futures_create_order(
                            symbol=symbol,
                            side='BUY',
                            type='MARKET',
                            quantity=quantity
                        ), asyncio_loop
                    ).result()
                else:
                    order = asyncio.run_coroutine_threadsafe(
                        async_client.futures_create_order(
                            symbol=symbol,
                            side='SELL',
                            type='MARKET',
                            quantity=quantity
                        ), asyncio_loop
                    ).result()
            with data_lock:
                del open_positions[symbol]
            logger.info(f"Закрыта позиция по {symbol} на {market_type}")
        except BinanceAPIException as e:
            logger.error(f"BinanceAPIException при закрытии позиции по {symbol}: {e}")
        except Exception as e:
            logger.error(f"Ошибка при закрытии позиции по {symbol}: {e}")

# Маршруты Flask

@app.route('/')
def index():
    with data_lock:
        densities = list(density_history.values())
        open_pos = open_positions.copy()
    return render_template('index.html',
                           densities=densities,
                           open_positions=open_pos,
                           trading_allowed=TRADING_ALLOWED,
                           position_size=POSITION_SIZE_USDT,
                           max_open_positions=MAX_OPEN_POSITIONS,
                           monitored_symbols=", ".join(selected_symbols),
                           excluded_symbols=", ".join(excluded_symbols),
                           monitoring_active=monitoring_active,
                           min_density_volume=min_density_volume,
                           min_consumption_time=min_consumption_time)

@app.route('/start')
def start_trading():
    global TRADING_ALLOWED
    TRADING_ALLOWED = True
    logger.info("Торговля разрешена")
    return redirect(url_for('index'))

@app.route('/stop')
def stop_trading():
    global TRADING_ALLOWED
    TRADING_ALLOWED = False
    logger.info("Торговля запрещена")
    return redirect(url_for('index'))

@app.route('/set_position_size', methods=['POST'])
def set_position_size():
    global POSITION_SIZE_USDT
    try:
        POSITION_SIZE_USDT = float(request.form['position_size'])
        logger.info(f"Размер позиции установлен на: {POSITION_SIZE_USDT} USDT")
    except ValueError:
        logger.error("Некорректный ввод для размера позиции")
    return redirect(url_for('index'))

@app.route('/set_max_positions', methods=['POST'])
def set_max_positions():
    global MAX_OPEN_POSITIONS
    try:
        MAX_OPEN_POSITIONS = int(request.form['max_positions'])
        logger.info(f"Максимальное количество позиций установлено на: {MAX_OPEN_POSITIONS}")
    except ValueError:
        logger.error("Некорректный ввод для максимального количества позиций")
    return redirect(url_for('index'))

@app.route('/set_density_params', methods=['POST'])
def set_density_params():
    global min_density_volume, min_consumption_time
    try:
        min_density_volume = float(request.form['min_density_volume'])
        min_consumption_time = float(request.form['min_consumption_time'])
        logger.info(f"Параметры плотности установлены: объем={min_density_volume} USDT, время разъедания={min_consumption_time} минут")
    except ValueError:
        logger.error("Некорректный ввод для параметров плотности")
    return redirect(url_for('index'))

@app.route('/set_symbols', methods=['POST'])
def set_symbols():
    global selected_symbols, symbols_to_monitor, monitoring_active, density_history, current_price_cache, five_min_volumes, max_5min_volumes, manual_monitoring

    symbols_input = request.form.get('symbols', '')
    if symbols_input.strip():
        selected_symbols = [s.strip().upper() for s in symbols_input.split(',') if s.strip()]
        manual_monitoring = True
    else:
        selected_symbols = []
        manual_monitoring = False

    # Останавливаем текущий мониторинг
    stop_tasks()

    # Получаем список символов для мониторинга
    symbols_to_monitor = get_symbols_to_monitor()
    logger.info(f"Список символов для мониторинга обновлен: {selected_symbols}")

    # Очищаем кеши
    with data_lock:
        density_history.clear()
        current_price_cache.clear()
        order_book_data_spot.clear()
        order_book_data_futures.clear()
        five_min_volumes.clear()
        max_5min_volumes.clear()

    # Запускаем задачи
    run_tasks()

    return redirect(url_for('index'))

@app.route('/set_excluded_symbols', methods=['POST'])
def set_excluded_symbols():
    global excluded_symbols, symbols_to_monitor, monitoring_active, density_history, current_price_cache, five_min_volumes, max_5min_volumes

    excluded_input = request.form.get('excluded_symbols', '')
    excluded_symbols = [s.strip().upper() for s in excluded_input.split(',') if s.strip()]
    logger.info(f"Символы, исключенные из мониторинга: {excluded_symbols}")

    # Останавливаем текущий мониторинг
    stop_tasks()

    # Получаем список символов для мониторинга
    symbols_to_monitor = get_symbols_to_monitor()
    logger.info(f"Список символов для мониторинга обновлен после исключения: {selected_symbols}")

    # Очищаем кеши
    with data_lock:
        density_history.clear()
        current_price_cache.clear()
        order_book_data_spot.clear()
        order_book_data_futures.clear()
        five_min_volumes.clear()
        max_5min_volumes.clear()

    # Запускаем задачи
    run_tasks()

    return redirect(url_for('index'))

@app.route('/monitor_all_usdt')
def monitor_all_usdt():
    global selected_symbols, symbols_to_monitor, monitoring_active, density_history, current_price_cache, five_min_volumes, max_5min_volumes, manual_monitoring

    logger.info("Функция monitor_all_usdt вызвана")

    # Останавливаем текущий мониторинг
    stop_tasks()

    # Получаем все общие символы USDT
    common_symbols = get_common_symbols()
    selected_symbols = [pair['spot'].replace('USDT', '') for pair in common_symbols]
    manual_monitoring = False

    # Обновляем символы для мониторинга
    symbols_to_monitor = common_symbols
    logger.info(f"Список символов для мониторинга обновлен (ВСЕ USDT): {selected_symbols}")

    # Очищаем кеши
    with data_lock:
        density_history.clear()
        current_price_cache.clear()
        order_book_data_spot.clear()
        order_book_data_futures.clear()
        five_min_volumes.clear()
        max_5min_volumes.clear()

    # Запускаем задачи
    run_tasks()

    return redirect(url_for('index'))

@app.route('/stop_monitoring')
def stop_monitoring():
    stop_tasks()
    logger.info("Мониторинг остановлен вручную")
    return redirect(url_for('index'))

@app.route('/get_densities')
def get_densities_route():
    with data_lock:
        densities = list(density_history.values())
        densities_sorted = sorted(densities, key=lambda x: x['symbol'])
    return jsonify(densities_sorted)

@app.route('/get_open_positions')
def get_open_positions_route():
    positions = []
    with data_lock:
        for symbol, position in open_positions.items():
            # Обновление текущей цены для позиции
            position['current_price'] = current_price_cache.get(symbol, position['entry_price'])
            # Расчет PnL
            if position['side'] == 'BUY':
                pnl = ((position['current_price'] - position['entry_price']) / position['entry_price']) * 100
            else:
                pnl = ((position['entry_price'] - position['current_price']) / position['entry_price']) * 100
            position['pnl_percent'] = round(pnl, 2)
            positions.append({
                'symbol': symbol,
                'side': position['side'],
                'quantity': round(position['quantity'], 8),
                'entry_price': round(position['entry_price'], 2),
                'current_price': round(position['current_price'], 2),
                'pnl_percent': position['pnl_percent']
            })
    return jsonify(positions)

@app.route('/open_position', methods=['POST'])
def handle_open_position():
    symbol = request.form['symbol']
    with data_lock:
        density_key_above = f"{symbol}_above"
        density_key_below = f"{symbol}_below"
        density = density_history.get(density_key_above) or density_history.get(density_key_below)
    if density and check_entry_conditions(density):
        open_position(density)
    return redirect(url_for('index'))

@app.route('/close_position', methods=['POST'])
def handle_close_position():
    symbol = request.form['symbol']
    close_position(symbol)
    return redirect(url_for('index'))

# Запуск приложения Flask и асинхронных задач

def start_flask():
    app.run(host='0.0.0.0', port=4000, debug=False, use_reloader=False)  # Отключите debug для предотвращения перезапуска

if __name__ == '__main__':
    # Получение начального списка символов для мониторинга до запуска
    symbols_to_monitor = get_symbols_to_monitor()

    # Запуск Flask в отдельном потоке
    flask_thread = threading.Thread(target=start_flask, daemon=True)
    flask_thread.start()

    # Запуск асинхронного цикла в отдельном потоке
    run_tasks()

    # Ожидание завершения Flask потока
    flask_thread.join()

HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Binance Trading Bot</title>
    <style>
        /* Стили */
        body {
            font-family: Arial, sans-serif;
            background-color: #121212;
            color: #ffffff;
            margin: 0;
            padding: 20px;
        }
        h1, h2 {
            text-align: center;
        }
        .controls {
            text-align: center;
            margin-bottom: 20px;
        }
        .controls form, .controls a, .controls button {
            display: inline-block;
            margin: 5px 10px;
        }
        table {
            width: 100%;
            margin: 0 auto 20px;
            border-collapse: collapse;
            color: #ffffff;
        }
        th, td {
            border: 1px solid #555;
            padding: 8px;
            text-align: center;
        }
        th {
            background-color: #333333;
        }
        .status {
            font-weight: bold;
        }
        .button {
            padding: 5px 10px;
            font-size: 14px;
            text-decoration: none;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
        .green {
            background-color: #4CAF50;
            color: white;
        }
        .red {
            background-color: #f44336;
            color: white;
        }
        .yellow {
            background-color: #ff9800;
            color: white;
        }
        .form-input {
            padding: 5px;
            font-size: 14px;
            width: 150px;
            border: 1px solid #ccc;
            border-radius: 3px;
        }
        .black-text {
            color: black !important;
        }
        .spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: #ffffff;
            animation: spin 1s ease-in-out infinite;
            vertical-align: middle;
        }
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        .status-text {
            display: inline-block;
            vertical-align: middle;
            margin-left: 5px;
        }
        /* Дополнительные стили для окрашивания ячеек */
        .value-gray {
            background-color: gray;
            color: black;
        }
        .value-yellow {
            background-color: yellow;
            color: black;
        }
        .value-orange {
            background-color: orange;
            color: black;
        }
        .value-violet {
            background-color: #EE82EE; /* light violet */
            color: black;
        }
        .value-pink {
            background-color: pink;
            color: black;
        }
        .positive {
            background-color: green;
            color: white;
        }
        .negative {
            background-color: red;
            color: white;
        }
        /* Новый стиль для размещения таблиц бок о бок */
        .table-container {
            display: flex;
            justify-content: space-between;
            flex-wrap: wrap;
            width: 100%;
            margin: 0 auto;
        }
        .table-wrapper {
            width: 48%;
            margin-bottom: 20px;
        }
        @media (max-width: 800px) {
            .table-wrapper {
                width: 100%;
            }
        }
    </style>
</head>
<body>
    <h1>Binance Trading Bot</h1>
    <div class="controls">
        <a class="button green" href="/start">СТАРТ</a>
        <a class="button red" href="/stop">СТОП</a>
        <p class="status">Статус торговли: {{ 'Разрешена' if trading_allowed else 'Запрещена' }}</p>
    </div>
    <div class="controls">
        <form action="/set_position_size" method="post">
            Размер позиции (USDT):
            <input class="form-input" type="number" name="position_size" value="{{ position_size }}" min="1" step="any">
            <button class="button yellow" type="submit">SET</button>
        </form>
        <form action="/set_max_positions" method="post">
            Количество позиций:
            <input class="form-input" type="number" name="max_positions" value="{{ max_open_positions }}" min="1">
            <button class="button yellow" type="submit">SET</button>
        </form>
    </div>

    <!-- Форма для установки параметров плотности -->
    <div class="controls">
        <form action="/set_density_params" method="post">
            Объём плотности ($/USDT):
            <input class="form-input" type="number" name="min_density_volume" value="{{ min_density_volume }}" min="0" step="any">
            Время разъедания (мин.):
            <input class="form-input" type="number" name="min_consumption_time" value="{{ min_consumption_time }}" min="0" step="any">
            <button class="button yellow" type="submit">Установить</button>
        </form>
    </div>

    <!-- Форма для установки символов для исключения -->
    <div class="controls">
        <form action="/set_excluded_symbols" method="post">
            Символы для исключения (через запятую):
            <input class="form-input" type="text" name="excluded_symbols" placeholder="BTC, ETH, BNB" value="{{ excluded_symbols }}">
            <button class="button yellow" type="submit">Применить</button>
        </form>
    </div>

    <!-- Форма для ввода символов и кнопка СТОП МОНИТОРИНГ -->
    <div class="controls">
        <form id="monitoring-form" action="/set_symbols" method="post" style="display: inline-block;">
            Монеты для мониторинга (через запятую):
            <input class="form-input" type="text" name="symbols" placeholder="BTC, ETH, DOGE" value="{{ monitored_symbols }}">
            <button class="button yellow" type="submit">MONITORING</button>
        </form>
        <!-- Кнопка "Monitor ALL USDT" -->
        <a class="button yellow" href="/monitor_all_usdt">Monitor ALL USDT</a>
        <a class="button red" href="/stop_monitoring">СТОП МОНИТОРИНГ</a>
        <span id="monitoring-status" style="margin-left: 10px;">
            {% if monitoring_active %}
                <span class="spinner"></span>
                <span class="status-text" style="color: green;">Мониторинг активен</span>
            {% else %}
                <span class="status-text" style="color: red;">Мониторинг не запущен</span>
            {% endif %}
        </span>
    </div>

    <h2>Плотности</h2>
    <div class="table-container">
        <div class="table-wrapper">
            <table id="densities-table-left">
                <thead>
                    <tr>
                        <th>Монета</th>
                        <th>Объем</th>
                        <th>Цена</th>
                        <th>Время разъедания</th>
                        <th>Монет в уровне</th>
                        <th>До уровня %</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- Данные будут заполняться через JavaScript -->
                </tbody>
            </table>
        </div>
        <div class="table-wrapper">
            <table id="densities-table-right">
                <thead>
                    <tr>
                        <th>Монета</th>
                        <th>Объем</th>
                        <th>Цена</th>
                        <th>Время разъедания</th>
                        <th>Монет в уровне</th>
                        <th>До уровня %</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- Данные будут заполняться через JavaScript -->
                </tbody>
            </table>
        </div>
    </div>

    <h2>Открытые позиции</h2>
    <table id="positions-table">
        <thead>
            <tr>
                <th>Символ</th>
                <th>Сторона</th>
                <th>Количество</th>
                <th>Цена входа</th>
                <th>Текущая цена</th>
                <th>PnL (%)</th>
            </tr>
        </thead>
        <tbody>
            <!-- Данные будут заполняться через JavaScript -->
        </tbody>
    </table>

    <script>
        function fetchDensities() {
            fetch('/get_densities')
                .then(response => response.json())
                .then(data => {
                    const tableLeftBody = document.querySelector('#densities-table-left tbody');
                    const tableRightBody = document.querySelector('#densities-table-right tbody');
                    tableLeftBody.innerHTML = '';
                    tableRightBody.innerHTML = '';

                    // Разбиваем данные на две части
                    const middleIndex = Math.ceil(data.length / 2);
                    const leftData = data.slice(0, middleIndex);
                    const rightData = data.slice(middleIndex);

                    // Функция для создания строк таблицы
                    function createRows(densities, tbody) {
                        densities.forEach(density => {
                            const row = document.createElement('tr');

                            // Окрашивание ячеек и форматирование значений
                            let totalValueCellClass = '';
                            if (density.total_value > 3000000) {
                                totalValueCellClass = 'value-pink';
                            } else if (density.total_value > 1000000) {
                                totalValueCellClass = 'value-violet';
                            } else if (density.total_value > 500000) {
                                totalValueCellClass = 'value-orange';
                            } else if (density.total_value > 250000) {
                                totalValueCellClass = 'value-yellow';
                            } else {
                                totalValueCellClass = 'value-gray';
                            }

                            let distanceCellClass = '';
                            if (density.distance < 0) {
                                distanceCellClass = 'negative';
                            } else if (density.distance > 0) {
                                distanceCellClass = 'positive';
                            }

                            const totalValueFormatted = `${(density.total_value / 1000).toFixed(1)}k$ (${density.distance >= 0 ? '+' : ''}${density.distance.toFixed(2)}%)`;

                            const consumptionTimeFormatted = Math.round(density.consumption_time);

                            const priceFormatted = parseFloat(density.price).toFixed(2);

                            let symbolDisplay = '';
                            if (density.market_type === 'spot') {
                                symbolDisplay = `${density.symbol} (спот)`;
                            } else {
                                symbolDisplay = `${density.symbol} (фьючерс Х${density.leverage})`;
                            }

                            // Округление количества монет с K и M
                            let qty_display = density.qty;
                            if (qty_display >= 1000 && qty_display < 1000000) {
                                qty_display = (qty_display / 1000).toFixed(2) + 'K';
                            } else if (qty_display >= 1000000) {
                                qty_display = (qty_display / 1000000).toFixed(3) + 'M';
                            } else {
                                qty_display = parseFloat(qty_display.toFixed(4));
                            }

                            row.innerHTML = `
                                <td>${symbolDisplay}</td>
                                <td class="${totalValueCellClass}">${totalValueFormatted}</td>
                                <td>${priceFormatted}</td>
                                <td>${consumptionTimeFormatted}</td>
                                <td>${qty_display}</td>
                                <td class="${distanceCellClass}">${density.distance.toFixed(2)}%</td>
                            `;
                            tbody.appendChild(row);
                        });
                    }

                    // Заполняем левую и правую таблицы
                    createRows(leftData, tableLeftBody);
                    createRows(rightData, tableRightBody);
                })
                .catch(error => {
                    console.error('Ошибка при получении плотностей:', error);
                });
        }

        function fetchPositions() {
            fetch('/get_open_positions')
                .then(response => response.json())
                .then(data => {
                    const tbody = document.querySelector('#positions-table tbody');
                    tbody.innerHTML = '';
                    data.forEach(position => {
                        const row = document.createElement('tr');

                        // Форматирование PnL
                        let pnlClass = '';
                        if (position.pnl_percent < 0) {
                            pnlClass = 'negative';
                        } else if (position.pnl_percent > 0) {
                            pnlClass = 'positive';
                        }

                        row.innerHTML = `
                            <td>${position.symbol}</td>
                            <td>${position.side}</td>
                            <td>${position.quantity.toFixed(4)}</td>
                            <td>${position.entry_price.toFixed(2)}</td>
                            <td>${position.current_price.toFixed(2)}</td>
                            <td class="${pnlClass}">${position.pnl_percent.toFixed(2)}%</td>
                        `;
                        tbody.appendChild(row);
                    });
                })
                .catch(error => {
                    console.error('Ошибка при получении позиций:', error);
                });
        }

        // Обновление таблиц каждые 1 секунду
        setInterval(fetchDensities, 1000);
        setInterval(fetchPositions, 1000);

        // Инициализация данных при загрузке страницы
        document.addEventListener('DOMContentLoaded', () => {
            fetchDensities();
            fetchPositions();
        });
    </script>
</body>
</html>

Сейчас при запуске в консоли ошибка:
(trade_bot_test) user@ubuntuserver:~$ ./trade_bot_test.sh
Exception ignored in: <function Client.__del__ at 0x7ffbe2ba2e80>
Traceback (most recent call last):
  File "/home/user/trade_bot_test/lib/python3.12/site-packages/binance/client.py", line 10388, in __del__
    self.close_connection()
  File "/home/user/trade_bot_test/lib/python3.12/site-packages/binance/client.py", line 10384, in close_connection
    if self.session:
       ^^^^^^^^^^^^
AttributeError: 'Client' object has no attribute 'session'
ERROR:__main__:Ошибка инициализации Client: Client.__init__() got an unexpected keyword argument 'options'