Зоны концентрации ликвидности — это ценовые уровни, на которых скапливается значительный объем лимитных ордеров. Эти зоны формируются, когда множество участников рынка размещают заявки в узком ценовом диапазоне, создавая барьеры для движения цены. Обнаружение таких зон позволяет прогнозировать уровни поддержки и сопротивления на основе реального распределения объемов, а не графических паттернов.
Кластерные графики отображают объемы торгов с детализацией до каждого ценового уровня внутри бара. В отличие от стандартных OHLCV данных, кластеры показывают, на каких именно ценах происходила торговля и с каким объемом. Это дает представление о микроструктуре рынка: где покупатели агрессивно покупали по ask, а где продавцы сбрасывали позиции по bid.
В этой статье мы рассмотрим 3 метода обнаружения зон концентрации:
- Скользящее окно: метод использует простую эвристику — ищет диапазоны, где сосредоточено более 50% объема бара;
- Метод точки контроля и зоны стоимости (Point of Control with Value Area): индустриальный стандарт анализа профилей объемов;
- Ядерная оценка плотности распределения (Kernel Density Estimation): метод применяет статистическое сглаживание для выявления пиков плотности распределения объемов.
Каждый подход имеет свои преимущества в зависимости от задачи: скорость работы, точность определения границ зоны или статистическая корректность.
Реализация на Python
Для анализа требуются детализированные данные о распределении объема по ценовым уровням внутри каждого бара. Биржи предоставляют доступ к этим данным через API в формате снимков стакана (order book snapshots) или последовательных записей сделок (trade-by-trade). В этой статье для демонстрации методов я буду использовать симуляцию кластерной структуры на основе стандартных OHLCV данных.
Загрузка 5-минутных данных Bitcoin выполняется через yfinance. После получения OHLCV создается синтетическое распределение объема по ценам внутри каждого бара. Для 30% баров генерируются зоны концентрации, где 55-65% объема сосредоточено в диапазоне 25-30% от общего ценового размаха свечи. Остальные бары имеют равномерное распределение объема.
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from scipy.stats import gaussian_kde
from scipy.signal import find_peaks
import warnings
warnings.filterwarnings('ignore')
def load_bitcoin_data(days_back=1):
"""
Загружает 5-минутные данные Bitcoin и создает кластерную структуру
Returns:
DataFrame с MultiIndex (timestamp, price) и колонками:
- buyers: объем агрессивных покупок
- sellers: объем агрессивных продаж
- quantity: общий объем
"""
print(f"Загрузка Bitcoin данных за {days_back} дней...")
end_date = datetime.now()
start_date = end_date - timedelta(days=days_back)
# Загрузка данных
btc = yf.download('BTC-USD', start=start_date, end=end_date,
interval='5m', progress=False)
if btc.empty:
raise ValueError("Не удалось загрузить данные")
# Убираем MultiIndex из колонок если он есть
if isinstance(btc.columns, pd.MultiIndex):
btc.columns = btc.columns.get_level_values(0)
print(f"Загружено {len(btc)} баров")
# Создаем кластерную структуру
cluster_data = simulate_cluster_data(btc)
return cluster_data
def simulate_cluster_data(ohlcv_df):
"""
Симулирует кластерные данные из OHLCV
Создает распределение объема по ценовым уровням
"""
data = []
bars_processed = 0
bars_skipped = 0
for timestamp, row in ohlcv_df.iterrows():
try:
high = float(row['High'])
low = float(row['Low'])
volume = float(row['Volume'])
except (KeyError, TypeError, ValueError):
bars_skipped += 1
continue
# Пропускаем бары с нулевым объемом или диапазоном
if pd.isna(high) or pd.isna(low) or pd.isna(volume):
bars_skipped += 1
continue
if volume == 0 or high == low:
bars_skipped += 1
continue
# Определяем количество ценовых уровней
n_ticks = max(30, int(volume / 1000000))
n_ticks = min(n_ticks, 200)
# 30% баров имеют зоны концентрации
has_concentration = np.random.random() < 0.3
if has_concentration:
# Зона концентрации: 25-30% диапазона
concentration_center = np.random.uniform(low, high)
concentration_range = (high - low) * np.random.uniform(0.25, 0.30)
concentration_low = max(low, concentration_center - concentration_range / 2)
concentration_high = min(high, concentration_center + concentration_range / 2)
# 55-65% объема в зоне
n_concentrated = int(n_ticks * np.random.uniform(0.55, 0.65))
n_random = n_ticks - n_concentrated
concentrated_prices = np.random.uniform(
concentration_low,
concentration_high,
n_concentrated
)
random_prices = np.random.uniform(low, high, n_random)
prices = np.concatenate([concentrated_prices, random_prices])
# Агрессивность в зоне: преобладание покупок или продаж
buy_bias = np.random.choice([0.65, 0.35])
else:
# Равномерное распределение
prices = np.random.uniform(low, high, n_ticks)
buy_bias = 0.5
prices = np.round(prices, 2)
# Генерируем объемы для каждого уровня
for price in prices:
tick_volume = np.random.uniform(0.001, 0.01)
is_buy = np.random.random() < buy_bias
data.append({
'timestamp': timestamp,
'price': price,
'buyers': tick_volume if is_buy else 0,
'sellers': tick_volume if not is_buy else 0,
'quantity': tick_volume
})
bars_processed += 1
print(f"Обработано: {bars_processed}, пропущено: {bars_skipped}")
if len(data) == 0:
raise ValueError("Нет данных для анализа")
# Создаем датафрейм с группировкой
df = pd.DataFrame(data)
df = df.groupby(['timestamp', 'price']).agg({
'buyers': 'sum',
'sellers': 'sum',
'quantity': 'sum'
})
print(f"Создано {len(df)} ценовых уровней в {df.index.get_level_values(0).nunique()} барах")
return df
Функция load_bitcoin_data() загружает данные и обрабатывает MultiIndex в колонках, который yfinance иногда создает. Функция simulate_cluster_data() преобразует OHLCV в кластерную структуру: для каждого бара генерируется распределение объема по ценовым уровням с возможной концентрацией в узком диапазоне.
Результат — датафрейм с двухуровневым индексом (timestamp, price) и тремя колонками: buyers (агрессивные покупки), sellers (агрессивные продажи), quantity (общий объем). Эта структура аналогична реальным кластерным данным, где каждая строка представляет один ценовой уровень в конкретном баре.
# Загружаем данные
data = load_bitcoin_data(days_back=1)
# Визуализируем один бар
timestamps = sorted(data.index.get_level_values(0).unique())
first_bar = data.loc[timestamps[0]]
fig, ax = plt.subplots(figsize=(10, 8))
# Volume profile
ax.barh(first_bar.index, first_bar['quantity'],
alpha=0.4, color='gray', label='Total Volume')
ax.barh(first_bar.index, first_bar['buyers'],
alpha=0.6, color='green', label='Buyers')
ax.barh(first_bar.index, first_bar['sellers'],
alpha=0.6, color='red', label='Sellers')
ax.set_ylabel('Price', fontsize=12)
ax.set_xlabel('Volume', fontsize=12)
ax.set_title(f'Volume Profile: {timestamps[0]}', fontsize=14)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"\nСтатистика бара:")
print(f"High: {first_bar.index.max():.2f}")
print(f"Low: {first_bar.index.min():.2f}")
print(f"Range: {first_bar.index.max() - first_bar.index.min():.2f}")
print(f"Total Volume: {first_bar['quantity'].sum():.4f} BTC")
print(f"Buyers: {first_bar['buyers'].sum():.4f} BTC")
print(f"Sellers: {first_bar['sellers'].sum():.4f} BTC")
Загрузка Bitcoin данных за 1 дней...
Загружено 285 баров
Обработано: 93, пропущено: 192
Создано 15150 ценовых уровней в 93 барах
Статистика бара:
High: 101743.79
Low: 101629.98
Range: 113.81
Total Volume: 0.4366 BTC
Buyers: 0.2020 BTC
Sellers: 0.2346 BTC

Рис. 1: Объемный профиль для одного 5-минутного бара Bitcoin. Горизонтальная гистограмма показывает распределение объема по ценовым уровням. Зеленым выделены агрессивные покупки (market orders на ask), красным — агрессивные продажи (market orders на bid), серым — общий объем
Метод 1: Скользящее окно
Метод скользящего окна последовательно проверяет все участки ценового диапазона бара. Окно фиксированного размера (25-30% от диапазона High-Low) перемещается от минимума к максимуму, на каждом шаге подсчитывается доля объема внутри окна от общего объема бара. Если концентрация превышает порог (50%), участок фиксируется как зона концентрации ликвидности.
Дополнительно применяется фильтр по объему: анализируются только бары, где общий объем превышает скользящую среднюю с периодом 12 (1 час на 5-минутках). Такой подход исключает малоликвидные периоды, где концентрация объема может быть случайной. Для каждой найденной зоны рассчитывается buy pressure — доля агрессивных покупок от суммы покупок и продаж в зоне. Значение выше 60% указывает на преобладание покупателей, ниже 40% — продавцов.
def find_zones_sliding_window(data, window_pct=0.30,
volume_threshold=0.50,
volume_ma_period=12):
"""
Поиск зон концентрации методом скользящего окна
Parameters:
data: DataFrame с MultiIndex (timestamp, price)
window_pct: размер окна в % от диапазона (0.25-0.30)
volume_threshold: минимальная концентрация объема (0.50 = 50%)
volume_ma_period: период MA для фильтрации баров
Returns:
DataFrame с найденными зонами
"""
results = []
# Рассчитываем MA объема по барам
bar_volumes = data.groupby(level=0)['quantity'].sum()
volume_ma = bar_volumes.rolling(
window=volume_ma_period,
min_periods=1
).mean()
timestamps = sorted(data.index.get_level_values(0).unique())
bars_analyzed = 0
bars_filtered = 0
for idx, timestamp in enumerate(timestamps):
# Данные текущего бара
bar_data = data.loc[timestamp].copy()
# Фильтр по объему
current_volume = bar_volumes.iloc[idx]
current_ma = volume_ma.iloc[idx]
if current_volume < current_ma: bars_filtered += 1 continue high = bar_data.index.max() low = bar_data.index.min() price_range = high - low if price_range == 0: bars_filtered += 1 continue window_size = price_range * window_pct total_volume = bar_data['quantity'].sum() # Проверяем зоны с шагом test_prices = np.linspace(low, high, num=20) for price in test_prices: window_high = price + window_size / 2 window_low = price - window_size / 2 # Отбираем данные в окне zone_mask = (bar_data.index >= window_low) & (bar_data.index <= window_high) zone_data = bar_data[zone_mask] if len(zone_data) == 0: continue zone_volume = zone_data['quantity'].sum() concentration = zone_volume / total_volume # Проверка порога if concentration >= volume_threshold:
buyers_vol = zone_data['buyers'].sum()
sellers_vol = zone_data['sellers'].sum()
total_dir = buyers_vol + sellers_vol
results.append({
'timestamp': timestamp,
'method': 'Sliding Window',
'price_center': price,
'zone_low': window_low,
'zone_high': window_high,
'concentration': concentration,
'buy_pressure': buyers_vol / total_dir if total_dir > 0 else 0.5,
'zone_volume': zone_volume
})
bars_analyzed += 1
print(f" Проанализировано: {bars_analyzed}, отфильтровано: {bars_filtered}")
return pd.DataFrame(results)
# Применяем метод
zones_sw = find_zones_sliding_window(
data,
window_pct=0.30,
volume_threshold=0.50,
volume_ma_period=12
)
print(f"\nНайдено зон: {len(zones_sw)}")
if len(zones_sw) > 0:
print(f"Средняя концентрация: {zones_sw['concentration'].mean():.1%}")
print(f"Средний buy pressure: {zones_sw['buy_pressure'].mean():.1%}")
Проанализировано: 59, отфильтровано: 34
Найдено зон: 59
Средняя концентрация: 62.9%
Средний buy pressure: 47.9%
Функция принимает три параметра:
- Размер окна относительно диапазона бара;
- Порог концентрации объема;
- Период скользящей средней для фильтрации.
Внутренний цикл проходит по всем барам, для каждого создается 20 тестовых позиций окна от минимума до максимума. На каждой позиции подсчитывается доля объема в окне. Результаты сохраняются в датафрейм со всеми характеристиками зон.
# Визуализация найденных зон для одного бара
bar_timestamp = zones_sw['timestamp'].iloc[0] if len(zones_sw) > 0 else timestamps[0]
bar_data = data.loc[bar_timestamp]
bar_zones = zones_sw[zones_sw['timestamp'] == bar_timestamp]
fig, ax = plt.subplots(figsize=(12, 8))
# Volume profile
ax.barh(bar_data.index, bar_data['quantity'],
alpha=0.3, color='gray', label='Volume')
# Зоны концентрации
max_vol = bar_data['quantity'].max()
for _, zone in bar_zones.iterrows():
color = 'green' if zone['buy_pressure'] > 0.6 else ('red' if zone['buy_pressure'] < 0.4 else 'yellow') rect = Rectangle( (0, zone['zone_low']), max_vol * 1.1, zone['zone_high'] - zone['zone_low'], alpha=0.3, color=color, edgecolor='black', linewidth=2 ) ax.add_patch(rect) # Подпись label_text = f"{zone['concentration']:.0%}\n" label_text += 'BUY' if zone['buy_pressure'] > 0.6 else ('SELL' if zone['buy_pressure'] < 0.4 else 'NEU')
ax.text(
max_vol * 0.5,
zone['price_center'],
label_text,
ha='center',
va='center',
fontsize=10,
fontweight='bold',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)
ax.set_ylabel('Price', fontsize=12)
ax.set_xlabel('Volume', fontsize=12)
ax.set_title(f'Sliding Window Method: {bar_timestamp}', fontsize=14)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Рис. 2: Результаты метода скользящего окна. Цветные области показывают найденные зоны концентрации. Зеленый цвет указывает на преобладание агрессивных покупок (buy pressure > 60%), красный — продаж (< 40%). Процент внутри зоны показывает долю объема бара, сосредоточенную в данном участке
Метод простой в реализации и интуитивно понятный. Параметры легко настраиваются под разные инструменты и таймфреймы. Однако возможны ложные срабатывания: если объем распределен относительно равномерно, но случайно в одном участке концентрация немного выше, окно это зафиксирует как зону. Также метод чувствителен к выбору размера окна — слишком маленькое пропустит широкие зоны, слишком большое объединит несколько узких.
Метод 2: Точки контроля и зоны стоимости (PoC with Value Area)
Point of Control (POC) — ценовой уровень с максимальным объемом в баре. Это точка наибольшей активности участников, где цена провела больше всего времени и где произошло наибольшее количество сделок. POC выступает магнитом для цены: при возврате к этому уровню часто возникает консолидация или отскок.
Value Area (VA) — диапазон цен, содержащий 70% совокупного объема бара. Рассчитывается от POC:
- Сначала добавляются ближайшие ценовые уровни с максимальным объемом, пока кумулятивный объем не достигнет 70%;
- Value Area High (VAH) и Value Area Low (VAL) — границы этого диапазона;
- Зона за пределами VA считается низколиквидной, цена проходит ее быстрее.
Метод широко используется профессиональными трейдерами на фьючерсных рынках. CME Group публикует POC и VA для своих контрактов. Подход не требует настройки параметров, кроме процента объема для VA (стандартно 70%, иногда 68% или 75%).
def find_poc_value_area(data, value_area_pct=0.70):
"""
Расчет Point of Control и Value Area
Parameters:
data: DataFrame с MultiIndex (timestamp, price)
value_area_pct: доля объема в Value Area (0.70 = 70%)
Returns:
DataFrame с POC и границами VA для каждого бара
"""
results = []
timestamps = sorted(data.index.get_level_values(0).unique())
for timestamp in timestamps:
bar_data = data.loc[timestamp].copy()
if len(bar_data) == 0:
continue
# Point of Control - уровень с максимальным объемом
poc_price = bar_data['quantity'].idxmax()
# Value Area - сортируем по объему
sorted_by_volume = bar_data.sort_values('quantity', ascending=False)
total_volume = bar_data['quantity'].sum()
target_volume = total_volume * value_area_pct
# Накапливаем объем от максимального
sorted_by_volume['cumvol'] = sorted_by_volume['quantity'].cumsum()
va_prices = sorted_by_volume[sorted_by_volume['cumvol'] <= target_volume].index
if len(va_prices) == 0:
va_prices = [poc_price]
va_high = va_prices.max()
va_low = va_prices.min()
va_volume = sorted_by_volume[sorted_by_volume['cumvol'] <= target_volume]['quantity'].sum() # Buy pressure в Value Area va_data = bar_data[(bar_data.index >= va_low) & (bar_data.index <= va_high)] buyers_vol = va_data['buyers'].sum() sellers_vol = va_data['sellers'].sum() total_dir = buyers_vol + sellers_vol results.append({ 'timestamp': timestamp, 'method': 'POC/Value Area', 'price_center': poc_price, 'zone_low': va_low, 'zone_high': va_high, 'concentration': va_volume / total_volume, 'buy_pressure': buyers_vol / total_dir if total_dir > 0 else 0.5,
'zone_volume': va_volume
})
return pd.DataFrame(results)
# Применяем метод
zones_poc = find_poc_value_area(data, value_area_pct=0.70)
print(f"\nPOC/Value Area анализ:")
print(f"Обработано баров: {len(zones_poc)}")
if len(zones_poc) > 0:
avg_va_width = (zones_poc['zone_high'] - zones_poc['zone_low']).mean()
print(f"Средняя ширина VA: {avg_va_width:.2f}")
print(f"Средний buy pressure: {zones_poc['buy_pressure'].mean():.1%}")
POC/Value Area анализ:
Обработано баров: 93
Средняя ширина VA: 101.42
Средний buy pressure: 49.6%
Функция находит POC через idxmax() на колонке объема. Для VA данные сортируются по убыванию объема, затем кумулятивно суммируются до достижения целевой доли. Ценовые уровни, вошедшие в эту сумму, определяют границы VA. Дополнительно рассчитывается агрессивность покупателей и продавцов внутри зоны.
Метод детерминированный — всегда дает один и тот же результат для одних данных. Не требует подбора параметров, кроме процента VA. Однако POC может быть нерепрезентативным в барах с несколькими пиками объема: метод выберет только один максимум, проигнорировав другие значимые зоны. Также следует учитывать, что Value Area может быть фрагментированной, если объем распределен неравномерно с разрывами.
# Визуализация POC и Value Area
bar_poc = zones_poc.iloc[0] if len(zones_poc) > 0 else None
bar_data = data.loc[bar_poc['timestamp']] if bar_poc is not None else data.loc[timestamps[0]]
fig, ax = plt.subplots(figsize=(12, 8))
# Volume profile
ax.barh(bar_data.index, bar_data['quantity'],
alpha=0.3, color='gray', label='Volume')
if bar_poc is not None:
# POC линия
ax.axhline(
bar_poc['price_center'],
color='blue',
linewidth=3,
label='POC',
linestyle='-'
)
# Value Area зона
ax.axhspan(
bar_poc['zone_low'],
bar_poc['zone_high'],
alpha=0.2,
color='purple',
label=f'Value Area ({bar_poc["concentration"]:.0%})'
)
# Подписи границ
ax.text(
bar_data['quantity'].max() * 0.7,
bar_poc['zone_high'],
f"VAH: {bar_poc['zone_high']:.2f}",
fontsize=10,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)
ax.text(
bar_data['quantity'].max() * 0.7,
bar_poc['zone_low'],
f"VAL: {bar_poc['zone_low']:.2f}",
fontsize=10,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)
ax.set_ylabel('Price', fontsize=12)
ax.set_xlabel('Volume', fontsize=12)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Рис. 3: Точки контроля и зоны стоимости на объемном профиле BTC-USD. Синяя горизонтальная линия отмечает POC — уровень с максимальным объемом. Фиолетовая зона показывает Value Area, содержащую 70% объема бара. VAH (Value Area High) и VAL (Value Area Low) отмечают границы зоны. Цена за пределами VA (выше VAH или ниже VAL) считается низколиквидной областью, где движение происходит быстрее. В данном примере POC смещен к верхней границе, что указывает на активность покупателей
Метод точки контроля и зоны стоимости используется для размещения лимитных ордеров на откат, VA — для определения нормального торгового диапазона. Пробой за границы VA с объемом сигнализирует о потенциальном трендовом движении. Возврат в VA после пробоя часто приводит к консолидации.
Метод 3: Ядерная оценка плотности распределения (Kernel Density Estimation)
Kernel Density Estimation (KDE) строит непрерывную оценку плотности распределения объема по ценовым уровням. В отличие от гистограммы, где объем разбит на дискретные бины, KDE создает гладкую функцию плотности. Это позволяет точнее определить центры концентрации и их границы, особенно когда объем распределен с шумом или имеет несколько близко расположенных пиков.
Метод использует взвешенный KDE:
- Каждый ценовой уровень повторяется пропорционально объему на нем. Гауссово ядро сглаживает распределение;
- После построения функции плотности находятся локальные максимумы (пики) — они соответствуют зонам концентрации;
- Ширина каждой зоны определяется на уровне половины высоты пика (Full Width at Half Maximum, FWHM).
KDE широко применяется в статистическом анализе временных рядов и распознавании паттернов. В контексте микроструктуры рынка метод позволяет выделить уровни, где концентрируются лимитные ордера, без жесткой привязки к фиксированным размерам окон или процентным порогам.
def find_zones_kde(data, n_points=100, peak_threshold_percentile=75):
"""
Поиск зон концентрации через Kernel Density Estimation
Parameters:
data: DataFrame с MultiIndex (timestamp, price)
n_points: количество точек для интерполяции функции плотности
peak_threshold_percentile: перцентиль для отсечения незначимых пиков
Returns:
DataFrame с зонами концентрации
"""
results = []
timestamps = sorted(data.index.get_level_values(0).unique())
for timestamp in timestamps:
bar_data = data.loc[timestamp].copy()
if len(bar_data) < 3:
continue
prices = bar_data.index.values
volumes = bar_data['quantity'].values
# Взвешенное KDE: повторяем цены пропорционально объему
weighted_prices = np.repeat(prices, (volumes * 100).astype(int))
if len(weighted_prices) < 2: continue try: # Строим KDE kde = gaussian_kde(weighted_prices) price_grid = np.linspace(prices.min(), prices.max(), n_points) density = kde(price_grid) # Нормализация для сопоставимости density_norm = (density - density.min()) / (density.max() - density.min() + 1e-10) # Находим пики threshold = np.percentile(density_norm, peak_threshold_percentile) peaks, _ = find_peaks( density_norm, height=threshold, distance=int(n_points * 0.05) ) # Обрабатываем каждый пик for peak_idx in peaks: peak_price = price_grid[peak_idx] peak_density = density_norm[peak_idx] # Ширина на половине высоты (FWHM) half_height = peak_density / 2 left_idx = peak_idx while left_idx > 0 and density_norm[left_idx] > half_height:
left_idx -= 1
right_idx = peak_idx
while right_idx < len(density_norm) - 1 and density_norm[right_idx] > half_height:
right_idx += 1
peak_low = price_grid[left_idx]
peak_high = price_grid[right_idx]
# Buy pressure в зоне пика
zone_data = bar_data[
(bar_data.index >= peak_low) &
(bar_data.index <= peak_high) ] buyers_vol = zone_data['buyers'].sum() sellers_vol = zone_data['sellers'].sum() total_dir = buyers_vol + sellers_vol zone_volume = zone_data['quantity'].sum() results.append({ 'timestamp': timestamp, 'method': 'KDE', 'price_center': peak_price, 'zone_low': peak_low, 'zone_high': peak_high, 'concentration': peak_density, # нормализованная плотность 'buy_pressure': buyers_vol / total_dir if total_dir > 0 else 0.5,
'zone_volume': zone_volume
})
except:
continue
return pd.DataFrame(results)
# Применяем метод
zones_kde = find_zones_kde(
data,
n_points=100,
peak_threshold_percentile=75
)
print(f"\nKDE анализ:")
print(f"Найдено пиков: {len(zones_kde)}")
if len(zones_kde) > 0:
print(f"Средняя плотность пиков: {zones_kde['concentration'].mean():.3f}")
print(f"Средняя ширина зон: {(zones_kde['zone_high'] - zones_kde['zone_low']).mean():.2f}")
KDE анализ:
Найдено пиков: 43
Средняя плотность пиков: 0.961
Средняя ширина зон: 23.89
Функция создает взвешенный массив цен там, где каждый уровень повторяется пропорционально объему. Scipy gaussian_kde() строит функцию плотности на этом массиве. Функция интерполируется на равномерную сетку из 100 точек. Scipy find_peaks() находит локальные максимумы выше заданного перцентиля (75%). Для каждого пика определяются границы на уровне половины высоты.
Метод дает наиболее статистически корректную оценку зон концентрации. Гладкая функция плотности устраняет шум дискретных данных. Автоматическое определение границ зон через FWHM адаптируется к форме распределения.
Однако KDE требует достаточного количества точек данных — на барах с малым числом ценовых уровней результат ненадежен. Выбор параметра bandwidth ядра влияет на степень сглаживания, gaussian_kde подбирает его автоматически по правилу Скотта.
# Визуализация KDE и найденных пиков
bar_timestamp = zones_kde['timestamp'].iloc[0] if len(zones_kde) > 0 else timestamps[0]
bar_data = data.loc[bar_timestamp]
bar_kde_zones = zones_kde[zones_kde['timestamp'] == bar_timestamp]
# Пересчитываем KDE для визуализации
prices = bar_data.index.values
volumes = bar_data['quantity'].values
weighted_prices = np.repeat(prices, (volumes * 100).astype(int))
if len(weighted_prices) >= 2:
kde = gaussian_kde(weighted_prices)
price_grid = np.linspace(prices.min(), prices.max(), 100)
density = kde(price_grid)
density_norm = (density - density.min()) / (density.max() - density.min() + 1e-10)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))
# График 1: Volume profile с зонами
ax1.barh(bar_data.index, bar_data['quantity'],
alpha=0.3, color='gray', label='Volume')
for _, zone in bar_kde_zones.iterrows():
rect = Rectangle(
(0, zone['zone_low']),
bar_data['quantity'].max() * 1.1,
zone['zone_high'] - zone['zone_low'],
alpha=0.3,
color='orange',
edgecolor='black',
linewidth=2
)
ax1.add_patch(rect)
ax1.text(
bar_data['quantity'].max() * 0.5,
zone['price_center'],
f"Peak\n{zone['concentration']:.2f}",
ha='center',
va='center',
fontsize=10,
fontweight='bold',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)
)
ax1.set_ylabel('Price', fontsize=12)
ax1.set_xlabel('Volume', fontsize=12)
ax1.set_title('KDE Zones on Volume Profile', fontsize=14)
ax1.grid(True, alpha=0.3)
# График 2: KDE плотность
ax2.plot(density_norm, price_grid, linewidth=2, color='purple', label='KDE Density')
ax2.fill_betweenx(price_grid, 0, density_norm, alpha=0.3, color='purple')
# Отмечаем пики
for _, zone in bar_kde_zones.iterrows():
ax2.axhline(zone['price_center'], color='red', linestyle='--',
linewidth=2, alpha=0.7, label='Peak' if _ == bar_kde_zones.index[0] else '')
ax2.axhspan(zone['zone_low'], zone['zone_high'],
alpha=0.2, color='orange')
ax2.set_ylabel('Price', fontsize=12)
ax2.set_xlabel('Normalized Density', fontsize=12)
ax2.set_title('KDE Density Function', fontsize=14)
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.suptitle(f'KDE Analysis: {bar_timestamp}', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()

Рис. 4: Результаты KDE анализа. Левая панель показывает профиль объема Bitcoin с выделенными зонами концентрации, найденными через пики плотности. Правая панель отображает саму функцию плотности (фиолетовая кривая) и отмеченные пики (красные пунктирные линии). Оранжевые зоны соответствуют ширине пиков на половине высоты. Гладкая форма функции устраняет случайные колебания объема на отдельных уровнях
KDE подходит для баров с достаточной детализацией ценовых уровней. Метод автоматически определяет количество зон концентрации без предварительного задания параметров типа размера окна. Недостаток — вычислительная сложность выше, чем у скользящего окна или POC, что критично при анализе большого объема исторических данных.
Сравнение методов
Три рассмотренных метода решают одну задачу разными подходами:
- Скользящее окно — параметрический метод с явным контролем размера зоны и порога концентрации;
- POC/Value Area — индустриальный стандарт, детерминированный и не требующий настройки;
- KDE — статистически продвинутый подход с автоматическим обнаружением произвольного количества зон.
Выбор метода зависит от задачи:
- Для быстрой оценки значимых уровней в рамках дневной торговли подходит POC/Value Area — стабильный результат без настройки;
- Для поиска конкретных зон входа с учетом агрессивности участников предпочтительнее скользящее окно — параметры можно адаптировать под волатильность инструмента;
- Для детального анализа микроструктуры и обнаружения множественных уровней концентрации оптимален KDE — статистически корректный подход с автоматическим определением количества зон.
По скорости работы наиболее быстрый метод — скользящее окно. Что касаемо точности: KDE дает наименьшее количество ложных срабатываний на зашумленных данных благодаря сглаживанию. Скользящее окно чувствительно к параметрам — неправильный выбор размера окна приводит к пропуску зон или их избыточному дроблению. Также следует учитывать, что POC/VA всегда находит ровно одну зону, что может быть недостаточно для баров с несколькими отдельными кластерами активности.
Заключение
Зоны концентрации ликвидности — ключевой элемент микроструктурного анализа рынков. Обнаружение этих зон позволяет прогнозировать поведение цены на уровнях с высокой плотностью лимитных ордеров.
Алгоритмические стратегии используют эти зоны для размещения ордеров, расчета уровней стоп-лосс и тейк-профит, оценки вероятности пробоя или отскока. Институциональные участники применяют Value Area для определения справедливого диапазона цен, розничные трейдеры — для поиска точек входа на откатах к POC.
В высокочастотной торговле смещение ключевых ценовых зон отслеживается в режиме реального времени. Размеры позиций динамически корректируются в зависимости от изменений в книге ордеров. Комбинация нескольких методов повышает надежность сигналов: если несколько подходов указывают на один и тот же уровень, значит в текущий момент он действительно является наиболее значимым.