Кластеризация Volume Profile с DBSCAN для POC уровней | Python scikit-learn

Кластеризация Volume Profile с DBSCAN для POC уровней | Python scikit-learn Анализ данных и Бэктесты
Изучите, как использовать алгоритм DBSCAN из scikit-learn для кластеризации данных Volume Profile и автоматического определения ключевых уровней POC (Point of Control) на финансовых рынках с помощью Python. В статье представлен готовый скрипт и подробный разбор параметров.
Суть: Данная статья предоставляет Python-скрипт, использующий алгоритм DBSCAN из библиотеки scikit-learn для кластеризации данных Volume Profile. Цель — автоматическое определение ключевых уровней POC (Point of Control) путем группировки плотных областей объема и нахождения пиков внутри этих кластеров.

Исходный код

Представленный скрипт на Python демонстрирует, как можно использовать алгоритм DBSCAN для анализа Volume Profile и выявления значимых уровней POC (Point of Control). Скрипт включает функции для расчета Volume Profile на основе OHLCV данных, применения DBSCAN для кластеризации ценовых уровней с высоким объемом и визуализации полученных результатов. Мы будем кластеризовать ценовые уровни, где объем превышает определенный порог, а затем внутри каждого кластера находить уровень с максимальным объемом, который и будет нашим POC.


import pandas as pd
import numpy as np
from sklearn.cluster import DBSCAN
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

def calculate_volume_profile(df: pd.DataFrame, price_bins: int = 100) -> pd.DataFrame:
    """
    Рассчитывает Volume Profile для заданного DataFrame OHLCV данных.

    Args:
        df (pd.DataFrame): DataFrame с колонками 'High', 'Low', 'Close', 'Volume'.
        price_bins (int): Количество ценовых интервалов (бинов) для Volume Profile.

    Returns:
        pd.DataFrame: DataFrame с колонками 'price_mid' (середина бина) и 'volume' (объем в бине).
    """
    min_price = df['Low'].min()
    max_price = df['High'].max()
    
    # Создаем бины для цен
    bins = np.linspace(min_price, max_price, price_bins + 1)
    
    # Инициализируем Volume Profile
    volume_profile = pd.DataFrame(0.0, index=range(price_bins), columns=['volume'])
    volume_profile['price_mid'] = [(bins[i] + bins[i+1]) / 2 for i in range(price_bins)]
    
    # Распределяем объем по бинам
    for _, row in df.iterrows():
        high, low, volume = row['High'], row['Low'], row['Volume']
        
        # Определяем диапазон цен для текущей свечи
        price_range_bins = np.where((bins[:-1] < high) & (bins[1:] > low))[0]
        
        if len(price_range_bins) == 0:
            continue
            
        # Упрощенное распределение: равномерно по всем затронутым бинам
        volume_per_bin = volume / len(price_range_bins)
        for bin_idx in price_range_bins:
            volume_profile.loc[bin_idx, 'volume'] += volume_per_bin
            
    return volume_profile[volume_profile['volume'] > 0].reset_index(drop=True)

def find_poc_clusters(volume_profile_df: pd.DataFrame, eps: float, min_samples: int, 
                      volume_threshold_percentile: float = 75) -> list:
    """
    Находит POC (Point of Control) с помощью DBSCAN кластеризации Volume Profile.

    Args:
        volume_profile_df (pd.DataFrame): DataFrame Volume Profile с 'price_mid' и 'volume'.
        eps (float): Максимальное расстояние между двумя выборками, чтобы одна считалась
                     находящейся в окрестности другой.
        min_samples (int): Количество выборок (или общая масса весов) в окрестности,
                           чтобы точка считалась основной.
        volume_threshold_percentile (float): Процентный порог для фильтрации бинов по объему.
                                             Только бины с объемом выше этого порога будут
                                             использованы для кластеризации.

    Returns:
        list: Список словарей, каждый из которых содержит 'price' и 'volume' для найденных POC.
    """
    # Фильтруем бины с низким объемом, чтобы сосредоточиться на значимых областях
    volume_threshold = volume_profile_df['volume'].quantile(volume_threshold_percentile / 100)
    significant_volume_df = volume_profile_df[volume_profile_df['volume'] >= volume_threshold].copy()

    if significant_volume_df.empty:
        return []

    # Подготавливаем данные для DBSCAN: кластеризуем по ценовым уровням
    # Reshape для scikit-learn, так как DBSCAN ожидает 2D массив
    X = significant_volume_df[['price_mid']].values

    # Применяем DBSCAN
    db = DBSCAN(eps=eps, min_samples=min_samples).fit(X)
    labels = db.labels_

    n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
    pocs = []

    for i in range(n_clusters):
        cluster_indices = significant_volume_df[labels == i].index
        cluster_data = significant_volume_df.loc[cluster_indices]
        
        # Находим POC внутри кластера (цена с максимальным объемом)
        poc_row = cluster_data.loc[cluster_data['volume'].idxmax()]
        pocs.append({'price': poc_row['price_mid'], 'volume': poc_row['volume']})
        
    return pocs

def plot_volume_profile_with_pocs(volume_profile_df: pd.DataFrame, pocs: list, title: str = 'Volume Profile with DBSCAN POCs'):
    """
    Визуализирует Volume Profile и найденные POC.

    Args:
        volume_profile_df (pd.DataFrame): DataFrame Volume Profile.
        pocs (list): Список найденных POC.
        title (str): Заголовок графика.
    """
    fig, ax = plt.subplots(figsize=(12, 8))

    # График Volume Profile
    # Высота бара должна соответствовать размеру бина
    bin_height = volume_profile_df['price_mid'].diff().mean() if len(volume_profile_df) > 1 else 1.0
    ax.barh(volume_profile_df['price_mid'], volume_profile_df['volume'], height=bin_height, 
            color='skyblue', edgecolor='gray', alpha=0.7, label='Volume Profile')

    # Отмечаем POCs
    for poc in pocs:
        ax.axhline(y=poc['price'], color='red', linestyle='--', linewidth=2, label=f'POC: {poc["price"]:.2f} (Vol: {poc["volume"]:.0f})')
        ax.text(ax.get_xlim()[1] * 0.95, poc['price'], f'POC {poc["price"]:.2f}', 
                verticalalignment='center', horizontalalignment='right', color='red', fontsize=10)

    ax.set_title(title)
    ax.set_xlabel('Volume')
    ax.set_ylabel('Price')
    ax.grid(True, linestyle=':', alpha=0.6)
    ax.legend()
    plt.tight_layout()
    plt.show()

if __name__ == '__main__':
    # --- 1. Генерация синтетических данных для примера ---
    # В реальном сценарии вы бы загружали данные из файла или API
    # Например, с помощью pandas.read_csv или yfinance
    np.random.seed(42)
    num_candles = 500
    prices = np.cumsum(np.random.normal(0, 0.5, num_candles)) + 100 # Базовая цена
    volumes = np.random.randint(100, 1000, num_candles) # Базовый объем

    # Добавим несколько областей с повышенным объемом для демонстрации POC
    prices[100:150] += np.random.normal(0, 0.1, 50) # Флэт в районе 100-150 свечи
    volumes[100:150] *= 3 # Увеличиваем объем в этом флэте
    prices[300:350] += np.random.normal(0, 0.1, 50) # Еще один флэт
    volumes[300:350] *= 2.5 # Увеличиваем объем

    df_data = pd.DataFrame({
        'Open': prices,
        'High': prices + np.random.uniform(0.1, 0.5, num_candles),
        'Low': prices - np.random.uniform(0.1, 0.5, num_candles),
        'Close': prices + np.random.uniform(-0.2, 0.2, num_candles),
        'Volume': volumes
    })

    # --- 2. Расчет Volume Profile ---
    price_bins = 100 # Количество ценовых бинов
    volume_profile = calculate_volume_profile(df_data, price_bins=price_bins)
    print("\nVolume Profile Head:")
    print(volume_profile.head())

    # --- 3. Поиск POC с помощью DBSCAN ---
    # Параметры DBSCAN:
    # eps: Максимальное расстояние между точками для формирования кластера. 
    #      Должен быть подобран относительно размера ценового бина.
    #      Например, если бин ~0.5, то eps может быть 0.5-1.0.
    # min_samples: Минимальное количество бинов, чтобы сформировать кластер.
    #              Зависит от детализации Volume Profile и ожидаемого размера POC.
    
    # Пример подбора eps: можно взять средний размер бина или его часть.
    # Если price_bins = 100, а диапазон цен ~100, то размер бина ~1.
    price_range = df_data['High'].max() - df_data['Low'].min()
    avg_bin_size = price_range / price_bins
    
    dbscan_eps = avg_bin_size * 1.5 # Пример: 1.5x от среднего размера бина
    dbscan_min_samples = 3 # Минимум 3 бина для формирования кластера
    volume_filter_percentile = 70 # Фильтруем бины с объемом ниже 70-го перцентиля

    pocs = find_poc_clusters(volume_profile, 
                             eps=dbscan_eps, 
                             min_samples=dbscan_min_samples, 
                             volume_threshold_percentile=volume_filter_percentile)

    print("\nFound POCs:")
    for poc in pocs:
        print(f"  Price: {poc['price']:.2f}, Volume: {poc['volume']:.0f}")

    # --- 4. Визуализация результатов ---
    plot_volume_profile_with_pocs(volume_profile, pocs, title='Volume Profile with DBSCAN POCs (Synthetic Data)')

    # --- Пример использования с реальными данными (закомментировано) ---
    # try:
    #     import yfinance as yf
    #     ticker = "SPY"
    #     data = yf.download(ticker, start="2023-01-01", end="2023-03-01")
    #     data.columns = [col.capitalize() for col in data.columns] # Приводим к 'High', 'Low', 'Close', 'Volume'
    #     
    #     real_volume_profile = calculate_volume_profile(data, price_bins=150)
    #     
    #     real_price_range = data['High'].max() - data['Low'].min()
    #     real_avg_bin_size = real_price_range / 150
    #     
    #     real_dbscan_eps = real_avg_bin_size * 2.0 # Подбираем параметры для реальных данных
    #     real_dbscan_min_samples = 5
    #     real_volume_filter_percentile = 80
    #     
    #     real_pocs = find_poc_clusters(real_volume_profile, 
    #                                   eps=real_dbscan_eps, 
    #                                   min_samples=real_dbscan_min_samples, 
    #                                   volume_threshold_percentile=real_volume_filter_percentile)
    #     
    #     print("\nFound POCs for SPY:")
    #     for poc in real_pocs:
    #         print(f"  Price: {poc['price']:.2f}, Volume: {poc['volume']:.0f}")
    #         
    #     plot_volume_profile_with_pocs(real_volume_profile, real_pocs, title=f'Volume Profile with DBSCAN POCs ({ticker})')
    # except ImportError:
    #     print("\nБиблиотека yfinance не установлена. Пропустили пример с реальными данными.")
    #     print("Установите ее командой: pip install yfinance")

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

  • dfcalculate_volume_profile): Входной DataFrame, содержащий OHLCV данные. Ожидаются колонки ‘High’, ‘Low’, ‘Close’, ‘Volume’.
  • price_binscalculate_volume_profile): Целое число, определяющее количество ценовых интервалов (бинов), на которые будет разбит весь ценовой диапазон для построения Volume Profile. Большее число бинов дает более детализированный профиль, но может увеличить «шум».
  • volume_profile_dffind_poc_clusters): DataFrame, полученный в результате работы функции calculate_volume_profile. Содержит колонки ‘price_mid’ и ‘volume’.
  • epsfind_poc_clusters): Параметр алгоритма DBSCAN. Это максимальное расстояние между двумя выборками, чтобы одна считалась находящейся в окрестности другой. В нашем случае, это максимальное расстояние между ценовыми уровнями (бинами), чтобы они могли быть частью одного кластера. Его значение должно быть подобрано относительно среднего размера ценового бина. Слишком маленькое eps приведет к множеству мелких кластеров или шуму, слишком большое — к объединению несвязанных областей.
  • min_samplesfind_poc_clusters): Параметр алгоритма DBSCAN. Минимальное количество выборок (ценовых бинов) в окрестности, чтобы точка считалась «основной» (core point) и могла формировать кластер. Определяет минимальный размер «значимой» области объема. Меньшее значение может привести к обнаружению незначительных кластеров, большее — к пропуску важных, но небольших POC.
  • volume_threshold_percentilefind_poc_clusters): Процентный порог (от 0 до 100) для фильтрации бинов Volume Profile по объему. Только бины, объем которых превышает этот перцентиль от общего распределения объемов, будут рассматриваться для кластеризации. Это помогает сосредоточиться на наиболее активных ценовых уровнях и игнорировать «шум» с низким объемом.

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

Для запуска скрипта и анализа Volume Profile с помощью DBSCAN, выполните следующие шаги:

  1. Установите необходимые библиотеки: Если у вас еще нет, установите pandas, numpy, scikit-learn и matplotlib. Вы можете сделать это с помощью pip:

    
    pip install pandas numpy scikit-learn matplotlib
    

    Если вы хотите протестировать пример с реальными данными (закомментирован в коде), также установите yfinance:

    
    pip install yfinance
    
  2. Сохраните код: Скопируйте предоставленный выше Python-код и сохраните его в файл с расширением .py, например, dbscan_volume_profile.py.

  3. Подготовьте данные: Скрипт по умолчанию генерирует синтетические OHLCV данные для демонстрации. В реальном сценарии вам потребуется загрузить свои данные. Убедитесь, что ваш DataFrame содержит колонки ‘High’, ‘Low’, ‘Close’, ‘Volume’. Вы можете использовать pandas.read_csv() для загрузки данных из CSV-файла или API для получения данных в реальном времени. Например, для оптимизации загрузки и обработки данных для множества тикеров, можно использовать подходы, описанные в статье Оптимизация стратегий на 100+ тикерах: Multiprocessing в Python.

  4. Настройте параметры: В секции if __name__ == '__main__': вы найдете переменные price_bins, dbscan_eps, dbscan_min_samples и volume_filter_percentile. Настройте их значения в соответствии с вашими данными и желаемой чувствительностью поиска POC. Помните, что eps должен быть сопоставим с размером ценового бина.

  5. Запустите скрипт: Откройте терминал или командную строку, перейдите в директорию, где вы сохранили файл, и выполните команду:

    
    python dbscan_volume_profile.py
    
  6. Проанализируйте результаты: Скрипт выведет в консоль найденные POC и отобразит график Volume Profile с отмеченными POC. Эти уровни могут служить важными точками поддержки/сопротивления или зонами интереса для торговых стратегий. Для дальнейшего анализа рыночных взаимосвязей, например, между BTC и S&P 500, может быть полезна статья Скользящая корреляция BTC и S&P 500 на Python в Pandas. А для разработки более сложных торговых стратегий, основанных на выявленных уровнях, рассмотрите методы, описанные в Бэктестинг стратегии на скрытых марковских моделях (HMM) в Python.

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