Парсинг Funding Rate Bybit V5 в ClickHouse на Python: Руководство для Quant Developer

Парсинг Funding Rate Bybit V5 в ClickHouse на Python: Руководство для Quant Developer Python & API
Подробное руководство по извлечению исторических данных Funding Rate с Bybit V5 REST API и их сохранению в ClickHouse с использованием Python. Идеально для количественных разработчиков.
Суть: В этой статье мы разработаем Python-скрипт для получения исторических данных Funding Rate с Bybit V5 REST API, обработки пагинации и эффективного сохранения этих данных в аналитической базе данных ClickHouse. Это критически важно для количественного анализа и разработки торговых стратегий.

Исходный код

Представленный ниже Python-скрипт демонстрирует полный цикл: от запроса данных Funding Rate к Bybit V5 API до их структурированного сохранения в ClickHouse. Мы используем библиотеку requests для HTTP-запросов и clickhouse_driver для взаимодействия с ClickHouse. Особое внимание уделено обработке пагинации для извлечения полных исторических рядов.


import requests
import time
from datetime import datetime, timedelta, timezone
from clickhouse_driver import Client

# --- Конфигурация Bybit API ---
BYBIT_API_BASE_URL = 'https://api.bybit.com'
FUNDING_RATE_HISTORY_ENDPOINT = '/v5/market/funding/history'
API_REQUEST_LIMIT = 200 # Максимальное количество записей за один запрос

# --- Конфигурация ClickHouse ---
CLICKHOUSE_HOST = 'localhost'
CLICKHOUSE_PORT = 9000
CLICKHOUSE_USER = 'default'
CLICKHOUSE_PASSWORD = ''
CLICKHOUSE_DB = 'bybit_data'

# --- Вспомогательные функции ---
def get_bybit_timestamp_ms(dt: datetime) -> int:
    """Преобразует datetime объект в Unix-таймстамп в миллисекундах."""
    return int(dt.timestamp() * 1000)

def convert_timestamp_ms_to_datetime(timestamp_ms: int) -> datetime:
    """Преобразует Unix-таймстамп в миллисекундах в datetime объект."""
    return datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)

def fetch_funding_rates(symbol: str, start_time_dt: datetime, end_time_dt: datetime) -> list:
    """Извлекает исторические данные Funding Rate для заданного символа и временного диапазона.
    Обрабатывает пагинацию.
    """
    all_funding_rates = []
    current_end_timestamp_ms = get_bybit_timestamp_ms(end_time_dt)
    start_timestamp_ms = get_bybit_timestamp_ms(start_time_dt)

    print(f"Начинаем извлечение Funding Rate для {symbol} с {start_time_dt} до {end_time_dt}")

    while True:
        params = {
            'category': 'linear',
            'symbol': symbol,
            'limit': API_REQUEST_LIMIT,
            'endTime': current_end_timestamp_ms
        }
        if start_timestamp_ms:
            params['startTime'] = start_timestamp_ms

        url = f"{BYBIT_API_BASE_URL}{FUNDING_RATE_HISTORY_ENDPOINT}"
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status() # Вызывает исключение для HTTP ошибок (4xx или 5xx)
            data = response.json()

            if data['retCode'] != 0:
                print(f"Ошибка API Bybit: {data['retMsg']}")
                break

            result = data['result']
            list_data = result.get('list', [])

            if not list_data:
                print("Нет данных в текущем запросе. Завершаем.")
                break

            # Bybit возвращает данные в порядке убывания fundingRateTimestamp
            # Мы хотим собрать данные в хронологическом порядке, но для пагинации
            # нам нужен самый старый timestamp из текущего набора.
            # Фильтруем данные, чтобы не добавлять те, что старше start_time_dt
            filtered_list_data = []
            for item in list_data:
                item_timestamp_ms = int(item['fundingRateTimestamp'])
                if item_timestamp_ms >= start_timestamp_ms:
                    filtered_list_data.append(item)
                else:
                    # Если мы встретили запись старше start_timestamp_ms,
                    # значит, все последующие (более старые) записи тоже будут старше.
                    # Можно обрезать список и прекратить добавление.
                    break
            
            # Добавляем отфильтрованные данные. Они уже отсортированы по убыванию времени.
            all_funding_rates.extend(filtered_list_data)

            # Определяем новый endTime для следующего запроса
            oldest_timestamp_in_batch_ms = int(list_data[-1]['fundingRateTimestamp'])
            
            # Если самый старый таймстамп в текущем батче уже меньше или равен
            # начальному таймстампу, то мы собрали все необходимые данные.
            if oldest_timestamp_in_batch_ms <= start_timestamp_ms:
                print("Достигнут начальный таймстамп. Завершаем извлечение.")
                break
            
            # Устанавливаем новый endTime на 1 миллисекунду раньше самого старого
            # таймстампа, чтобы избежать дублирования первой записи в следующем запросе.
            current_end_timestamp_ms = oldest_timestamp_in_batch_ms - 1
            print(f"Извлечено {len(filtered_list_data)} записей. Продолжаем с endTime: {convert_timestamp_ms_to_datetime(current_end_timestamp_ms)}")

            # Задержка для соблюдения лимитов API
            time.sleep(0.1)

        except requests.exceptions.RequestException as e:
            print(f"Ошибка при запросе к Bybit API: {e}")
            break
        except ValueError as e:
            print(f"Ошибка парсинга JSON: {e}")
            break
        except Exception as e:
            print(f"Неизвестная ошибка: {e}")
            break
    
    # Сортируем данные по возрастанию времени перед возвратом
    all_funding_rates.sort(key=lambda x: int(x['fundingRateTimestamp']))
    return all_funding_rates

def create_clickhouse_table(client: Client):
    """Создает таблицу в ClickHouse для хранения данных Funding Rate."""
    create_table_sql = f"""
    CREATE TABLE IF NOT EXISTS {CLICKHOUSE_DB}.bybit_funding_rates (
        symbol String,
        funding_rate Float64,
        funding_rate_timestamp DateTime64(3),
        funding_rate_time DateTime('UTC')
    )
    ENGINE = MergeTree()
    ORDER BY (symbol, funding_rate_timestamp)
    TTL funding_rate_time + INTERVAL 1 YEAR
    """
    try:
        client.execute(create_table_sql)
        print(f"Таблица {CLICKHOUSE_DB}.bybit_funding_rates успешно создана или уже существует.")
    except Exception as e:
        print(f"Ошибка при создании таблицы ClickHouse: {e}")

def insert_data_to_clickhouse(client: Client, data: list):
    """Вставляет список данных Funding Rate в ClickHouse."""
    if not data:
        print("Нет данных для вставки в ClickHouse.")
        return

    insert_sql = f"""
    INSERT INTO {CLICKHOUSE_DB}.bybit_funding_rates (
        symbol, funding_rate, funding_rate_timestamp, funding_rate_time
    ) VALUES
    """
    
    # Преобразуем данные в формат, подходящий для ClickHouse
    rows_to_insert = []
    for item in data:
        try:
            symbol = item['symbol']
            funding_rate = float(item['fundingRate'])
            funding_rate_timestamp_ms = int(item['fundingRateTimestamp'])
            funding_rate_dt = convert_timestamp_ms_to_datetime(funding_rate_timestamp_ms)
            
            rows_to_insert.append((
                symbol,
                funding_rate,
                funding_rate_dt, # DateTime64(3) автоматически обрабатывает миллисекунды
                funding_rate_dt.replace(tzinfo=None) # Для DateTime без таймзоны
            ))
        except (ValueError, KeyError) as e:
            print(f"Ошибка при обработке записи: {item}. Пропускаем. Ошибка: {e}")
            continue

    if not rows_to_insert:
        print("После фильтрации не осталось валидных данных для вставки.")
        return

    try:
        client.execute(insert_sql, rows_to_insert)
        print(f"Успешно вставлено {len(rows_to_insert)} записей в ClickHouse.")
    except Exception as e:
        print(f"Ошибка при вставке данных в ClickHouse: {e}")


if __name__ == '__main__':
    # --- Параметры для извлечения ---
    TARGET_SYMBOL = 'BTCUSDT'
    # Извлекаем данные за последние 7 дней
    END_DATE = datetime.now(timezone.utc).replace(microsecond=0)
    START_DATE = END_DATE - timedelta(days=7)

    # --- Инициализация клиента ClickHouse ---
    ch_client = None
    try:
        ch_client = Client(
            host=CLICKHOUSE_HOST,
            port=CLICKHOUSE_PORT,
            user=CLICKHOUSE_USER,
            password=CLICKHOUSE_PASSWORD,
            database=CLICKHOUSE_DB
        )
        print("Успешное подключение к ClickHouse.")

        # Создаем таблицу, если она не существует
        create_clickhouse_table(ch_client)

        # Извлекаем данные Funding Rate
        funding_rates_data = fetch_funding_rates(TARGET_SYMBOL, START_DATE, END_DATE)
        print(f"Всего извлечено {len(funding_rates_data)} записей Funding Rate.")

        # Вставляем данные в ClickHouse
        if funding_rates_data:
            insert_data_to_clickhouse(ch_client, funding_rates_data)

    except Exception as e:
        print(f"Критическая ошибка в основном блоке: {e}")
    finally:
        if ch_client:
            ch_client.disconnect()
            print("Отключение от ClickHouse.")

Разбор параметров

  • BYBIT_API_BASE_URL: Базовый URL для Bybit REST API V5.
  • FUNDING_RATE_HISTORY_ENDPOINT: Относительный путь к эндпоинту для получения исторических данных Funding Rate.
  • API_REQUEST_LIMIT: Максимальное количество записей, которое можно запросить за один раз у Bybit API (обычно 200 для этого эндпоинта).
  • CLICKHOUSE_HOST, CLICKHOUSE_PORT, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, CLICKHOUSE_DB: Параметры для подключения к вашей инсталляции ClickHouse. Убедитесь, что они соответствуют вашей конфигурации.
  • symbolfetch_funding_rates): Торговый символ (например, 'BTCUSDT'), для которого извлекаются данные Funding Rate.
  • start_time_dt, end_time_dtfetch_funding_rates): Объекты datetime, определяющие начальную и конечную точки временного диапазона для извлечения данных.
  • category (в запросе Bybit): Категория продукта. Для бессрочных фьючерсов это 'linear'.
  • limit (в запросе Bybit): Количество записей, возвращаемых за один запрос. Должно быть не более API_REQUEST_LIMIT.
  • startTime, endTime (в запросе Bybit): Unix-таймстампы в миллисекундах, определяющие временной диапазон для фильтрации данных.

Как запустить

Для успешного запуска скрипта и сбора данных Funding Rate, следуйте этим шагам:

  1. Установите Python: Убедитесь, что у вас установлен Python 3.8+.

  2. Установите ClickHouse: Если у вас еще нет ClickHouse, установите его. Вы можете использовать Docker для быстрой установки:

    
    docker run -d --name clickhouse-server \
    -p 8123:8123 -p 9000:9000 \
    --ulimit nofile=262144:262144 \
    clickhouse/clickhouse-server
    

    После запуска контейнера, вы можете подключиться к ClickHouse через localhost:9000 (для TCP) или localhost:8123 (для HTTP).

  3. Создайте базу данных в ClickHouse (опционально): По умолчанию скрипт использует базу данных bybit_data. Если ее нет, ClickHouse клиент может создать ее при первом подключении, или вы можете создать ее вручную:

    
    CREATE DATABASE IF NOT EXISTS bybit_data;
    
  4. Установите необходимые Python-библиотеки:

    
    pip install requests clickhouse-driver
    
  5. Настройте параметры в скрипте: Откройте файл со скриптом и при необходимости измените следующие константы в начале файла:

    • CLICKHOUSE_HOST, CLICKHOUSE_PORT, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, CLICKHOUSE_DB, если ваша конфигурация ClickHouse отличается.
    • TARGET_SYMBOL, START_DATE, END_DATE для определения интересующего вас актива и временного диапазона.
  6. Запустите скрипт:

    
    python your_script_name.py
    
  7. Проверьте данные в ClickHouse: После завершения работы скрипта вы можете подключиться к ClickHouse и убедиться, что данные были успешно импортированы:

    
    SELECT * FROM bybit_data.bybit_funding_rates LIMIT 10;
    SELECT count() FROM bybit_data.bybit_funding_rates WHERE symbol = 'BTCUSDT';
    

Этот подход к сбору и хранению данных Funding Rate является фундаментальным для любого количественного аналитика или разработчика, работающего с деривативами. Подобные методы сбора данных применимы и к другим источникам, например, при мониторинге балансов пулов ликвидности, как описано в статье Мониторинг баланса пулов Uniswap V2 с Web3.py: Руководство для Quant Developer, что подчеркивает универсальность навыков работы с API и базами данных в сфере DeFi и традиционных финансов.

Оцените статью
FinFluct