Библиотека mplfinance для Python — это мощный инструмент, благодаря которому аналитики и трейдеры получают множество инсайтов при визуализации рыночных данных. В этой статье я расскажу о 23 различных способах визуализации котировок с использованием библиотеки mplfinance, начиная от базовых графиков до продвинутых техник.
Основы работы с mplfinance
Перед тем как перейти к конкретным методам визуализации, давайте рассмотрим основы работы с библиотекой mplfinance. Эта библиотека является специализированным расширением популярной библиотеки matplotlib и предназначена для визуализации финансовых данных.
Для начала работы с mplfinance необходимо установить библиотеку. Это можно сделать с помощью pip:
!pip install mplfinance
Помимо mplfinance, нам также понадобится библиотека для загрузки финансовых данных. В этой статье мы будем использовать yfinance:
!pip install yfinance
Теперь давайте рассмотрим базовый пример использования mplfinance для создания стандартного свечного графика:
import yfinance as yf
import mplfinance as mpf
import pandas as pd
# Загружаем котировки за последний год
ticker = "F" # Ford Motor
data = yf.download(ticker, start="2024-05-16", end="2025-05-16")
# Удаляем мультииндекс из данных
data.columns = data.columns.droplevel(1)
# Строим свечной график
mpf.plot(
data,
type='candle',
style='charles',
title=f'Ford Motor (F) Stock Price',
ylabel='Price ($)',
volume=True,
figsize=(18, 8)
)
Рис. 1: Свечной график дневных цен акций Ford Motor
Для работы с mplfinance необходимо использовать данные в формате DataFrame из библиотеки pandas. При этом датафрейм должен иметь определенную структуру: индекс должен представлять собой даты (DatetimeIndex), а столбцы должны включать открытие (Open), максимум (High), минимум (Low) и закрытие (Close) — так называемые OHLC-данные. Опционально можно включить объем торгов (Volume).
После выполнения этого кода мы получим классический свечной график цен акций Ford за последние 12 месяцев.
Этот пример демонстрирует базовое использование mplfinance, однако, разумеется, возможности библиотеки гораздо шире. Мы можем строить другие типы биржевых графиков, либо кастомизировать их под лучшее восприятие. Например, построить график в кастомной цветовой гамме, как показано ниже:
# Настраиваем цвета свечей
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Создаем свечной график
mpf.plot(data, type='candle', style=s,
title=f'Ford Motor (F) Stock Price',
ylabel='Цена ($)',
volume=True,
figsize=(18, 8))
Рис. 2: Свечной график котировок в серой цветовой гамме
Этот код создает свечной график с черными свечами для растущих дней и серыми для падающих. График также включает объем торгов в нижней части.
Также мы можем совсем «избавиться» от свечей и построить график баров, так называемый OHLC-график. В отличие от свечного графика, здесь используются вертикальные линии для отображения диапазона цены и горизонтальные тики для отображения цен открытия и закрытия.
# Настраиваем цвета
mc = mpf.make_marketcolors(up='black', down='gray')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Создаем OHLC-график
mpf.plot(data, type='ohlc', style=s,
title=f'OHLC-график котировок Ford',
ylabel='Цена ($)',
volume=False,
figsize=(18, 8))
Рис. 3: Барный (OHLC) график котировок Ford
OHLC-график может быть предпочтительнее свечного в ситуациях, когда важно четко видеть соотношение между ценами открытия и закрытия.
Иногда для анализа тренда достаточно просто отслеживать цены закрытия. Линейный график закрытия — самый простой тип графика, но он может быть очень эффективным для выявления долгосрочных трендов.
# Создаем линейный график
mpf.plot(data, type='line', style='charles',
title=f'Линейный график закрытия дневных цен акций Ford',
ylabel='Цена закрытия ($)',
figsize=(12, 6))
Рис. 4: Линейный график закрытия дневных цен
Линейный график особенно полезен при анализе долгосрочных трендов, когда дневные колебания цены менее важны, чем общее направление движения.
Наложение на график скользящих средних
Добавление скользящих средних к свечному графику может предоставить дополнительную информацию о тренде. Скользящие средние часто используются для сглаживания ценовых колебаний и выявления направления тренда.
# Рассчитываем скользящие средние
data['SMA10'] = data['Close'].rolling(window=10).mean()
data['SMA50'] = data['Close'].rolling(window=50).mean()
# Создаем дополнительные графики
apds = [
mpf.make_addplot(data['SMA10'], color='blue'),
mpf.make_addplot(data['SMA50'], color='red'),
]
# Настраиваем цвета свечей
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Создаем свечной график со скользящими средними
mpf.plot(data, type='candle', style=s,
title=f'Свечной график {ticker} со скользящими средними',
ylabel='Цена ($)',
addplot=apds,
volume=False,
figsize=(18, 8))
Рис. 5: Свечной график Ford с наложенными SMA-10 и SMA-50
В этом примере мы добавили две скользящие средние: 10-дневную (синяя линия) и 50-дневную (красная линия).
Объемный профиль (Volume Profile)
Объемный профиль — это расширенный метод анализа, который показывает распределение объема торгов по ценовым уровням. Он помогает определить уровни поддержки и сопротивления, основанные на активности трейдеров.
import matplotlib.pyplot as plt
import numpy as np
# Создаем функцию для расчета объемного профиля
def volume_profile(df, bins=100):
price_range = df['High'].max() - df['Low'].min()
bin_size = price_range / bins
price_bins = np.arange(df['Low'].min(), df['High'].max() + bin_size, bin_size)
volume_by_price = np.zeros(len(price_bins) - 1)
for i, row in df.iterrows():
for j in range(len(price_bins) - 1):
if row['Low'] <= price_bins[j+1] and row['High'] >= price_bins[j]:
overlap = min(row['High'], price_bins[j+1]) - max(row['Low'], price_bins[j])
price_range_day = row['High'] - row['Low']
if price_range_day > 0:
volume_by_price[j] += row['Volume'] * (overlap / price_range_day)
return pd.DataFrame({
'Price': (price_bins[:-1] + price_bins[1:]) / 2,
'Volume': volume_by_price
})
# Рассчитываем объемный профиль
vp_data = volume_profile(data)
# Цвета: градиент от зеленого к красному в зависимости от объема
volumes = vp_data['Volume'].values
norm = plt.Normalize(volumes.min(), volumes.max())
colors = [plt.cm.coolwarm(x) for x in norm(volumes)] # coolwarm: от сине-голубого до красного
# Основной график без дат на оси X
fig, axes = mpf.plot(
data,
type='candle',
style='yahoo', # более светлый стиль
title=f'Объемный профиль котировок Ford за последние 12 месяцев',
ylabel='Цена ($)',
volume=False,
figsize=(12, 8),
xrotation=0, # поворот дат
returnfig=True
)
# Убираем даты на оси X
axes[0].xaxis.set_ticklabels([]) # Скрываем подписи дат
# Добавляем объемный профиль справа
ax_vp = axes[0].twinx()
ax_vp.barh(
vp_data['Price'],
vp_data['Volume'],
height=vp_data['Price'].diff().median(),
color=colors,
edgecolor='none'
)
ax_vp.set_xlim([0, vp_data['Volume'].max() * 1.5]) # Уменьшаем пустое пространство
ax_vp.grid(False)
ax_vp.set_axisbelow(True)
ax_vp.set_ylabel('')
ax_vp.set_yticklabels([])
# Отображаем график
plt.tight_layout()
plt.show()
Рис. 6: Объемный профиль котировок Ford за последние 12 месяцев
Объемный профиль помогает определить ключевые ценовые уровни, где происходили значительные торговые активности. Области с наибольшей концентрацией объема могут служить уровнями поддержки или сопротивления в будущем.
Кстати, можно модернизировать этот профиль и наложить на него линию текущих цен акций и линию, где были цены 30 периодов назад.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
# Функция объемного профиля
def volume_profile(df, bins=100):
price_range = df['High'].max() - df['Low'].min()
bin_size = price_range / bins
price_bins = np.arange(df['Low'].min(), df['High'].max() + bin_size, bin_size)
volume_by_price = np.zeros(len(price_bins) - 1)
for i, row in df.iterrows():
for j in range(len(price_bins) - 1):
if row['Low'] <= price_bins[j+1] and row['High'] >= price_bins[j]:
overlap = min(row['High'], price_bins[j+1]) - max(row['Low'], price_bins[j])
price_range_day = row['High'] - row['Low']
if price_range_day > 0:
volume_by_price[j] += row['Volume'] * (overlap / price_range_day)
return pd.DataFrame({
'Price': (price_bins[:-1] + price_bins[1:]) / 2,
'Volume': volume_by_price
})
vp_data = volume_profile(data)
# Создаем пользовательский градиент: от темно-серого до синего
colors = ["#2e2e2e", "#5f7fe8"]
cmap = LinearSegmentedColormap.from_list("dark_grey_blue", colors, N=256)
# Нормализация объема для применения градиента
volumes = vp_data['Volume'].values
norm = plt.Normalize(volumes.min(), volumes.max())
vp_colors = [cmap(x) for x in norm(volumes)]
# Основной график
fig, axes = mpf.plot(
data,
type='candle',
style='yahoo',
title=f'Объемный профиль котировок Ford за последние 12 месяцев',
ylabel='Цена ($)',
volume=False,
figsize=(12, 8),
xrotation=0,
returnfig=True
)
# Убираем даты на оси X
axes[0].xaxis.set_ticklabels([])
# Получаем цены
last_close = data['Close'][-1]
close_30_periods_ago = data['Close'][-31]
# Линия Last Close
axes[0].axhline(y=last_close, color='orange', linestyle='-', linewidth=1.5)
axes[0].text(
x=len(data) - 2,
y=last_close,
s=' Last Close',
color='orange',
fontsize=10,
verticalalignment='center'
)
# Линия Close 30 периодов назад
axes[0].axhline(y=close_30_periods_ago, color='dimgray', linestyle='-', linewidth=1.5)
axes[0].text(
x=len(data) - 2,
y=close_30_periods_ago,
s=' Close 30 periods ago',
color='dimgray',
fontsize=10,
verticalalignment='center'
)
# Добавляем объемный профиль справа
ax_vp = axes[0].twinx()
ax_vp.barh(
vp_data['Price'],
vp_data['Volume'],
height=vp_data['Price'].diff().median(),
color=vp_colors,
edgecolor='none'
)
ax_vp.set_xlim([0, vp_data['Volume'].max() * 1.5])
ax_vp.grid(False)
ax_vp.set_axisbelow(True)
ax_vp.set_ylabel('')
ax_vp.set_yticklabels([])
# Выравниваем шкалу Y с основным графиком
ymin, ymax = axes[0].get_ylim()
ax_vp.set_ylim(ymin, ymax) # Устанавливаем тот же диапазон Y
# Горизонтальные линии на графике объемного профиля
ax_vp.axhline(y=last_close, color='orange', linestyle='-', linewidth=1.5)
ax_vp.axhline(y=close_30_periods_ago, color='dimgray', linestyle='-', linewidth=1.5)
# Отображение графика
plt.tight_layout()
plt.show()
Рис. 7: Объемный профиль котировок с линиями текущих цен и цены закрытия 30 периодов назад
Лично я нахожу такие профили очень удобными для быстрого скрининга текущих цен акций и оценке их волатильности.
Оранжевая линия показывает по каким ценам сейчас идут торги, а черная линия — где цена была 30 периодов назад. Если оранжевая линия ниже черной — значит акцию распродают, если выше — откупают. Если оранжевая линия находится в области большой концентрации объемного профиля, то это означает что именно тут акцию и «ждут». Если линия ушла из области концентрации, и это актив с исторически флэтовой динамикой, то есть высокая вероятность отката назад.
Heikin-Ashi график
Heikin-Ashi — это модифицированный тип свечного графика, с помощью которого можно изменить логику построения свечей на свою, с целью сглаживания волатильности и выявления трендов.
# Преобразуем стандартные свечи в Heikin-Ashi
def heikin_ashi(df):
ha_df = pd.DataFrame(index=df.index)
ha_df['Open'] = (df['Open'].shift(1) + df['Close'].shift(1)) / 2
ha_df.loc[df.index[0], 'Open'] = (df.loc[df.index[0], 'Open'] + df.loc[df.index[0], 'Close']) / 2
ha_df['Close'] = (df['Open'] + df['High'] + df['Low'] + df['Close']) / 4
ha_df['High'] = df[['High', 'Open', 'Close']].max(axis=1)
ha_df['Low'] = df[['Low', 'Open', 'Close']].min(axis=1)
ha_df['Volume'] = df['Volume']
return ha_df
ha_data = heikin_ashi(data)
# Настраиваем цвета свечей
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Создаем Heikin-Ashi график
mpf.plot(ha_data, type='candle', style=s,
title=f'Heikin-Ashi график котировок акций Ford',
ylabel='Цена ($)',
volume=False,
figsize=(18, 8))
Рис. 8: График свечей Heikin-Ashi
Графики Heikin-Ashi зачастую более информативны для определения тренда и его силы, чем классические свечные графики. Последовательные свечи одного цвета указывают на сильный тренд, а смена цвета свечей может сигнализировать о потенциальном развороте.
В отличие от обычного свечного графика, где каждая свеча отражает реальные данные закрытия и открытия, Heikin-Ashi использует усредненные значения, что позволяет нагляднее видеть направление движения цены и снижает количество ложных сигналов.
Визуализация вероятностного распределения доходности (Return Distribution)
Квантовые аналитики часто используют вероятностные модели для оценки рисков. Давайте создадим визуализацию, которая показывает распределение дневной доходности и сравнивает его с нормальным распределением:
from scipy import stats
from matplotlib.gridspec import GridSpec
# Рассчитываем дневную доходность
data['Daily Return'] = data['Close'].pct_change() * 100
# Создаем фигуру с сеткой
fig = plt.figure(figsize=(18, 12))
gs = GridSpec(2, 2, figure=fig, height_ratios=[2, 1])
# Свечной график в верхней части
ax1 = fig.add_subplot(gs[0, :])
mpf.plot(
data,
type='candle',
style='charles',
ax=ax1,
volume=False,
ylabel='Цена ($)',
returnfig=False
)
ax1.set_title(f'Вероятностное распределение доходности акций {ticker}', fontsize=14)
# График распределения доходности
ax2 = fig.add_subplot(gs[1, 0])
returns = data['Daily Return'].dropna()
bins = np.linspace(returns.min(), returns.max(), 50)
ax2.hist(returns, bins=bins, alpha=0.6, color='gray', density=True)
# Накладываем нормальное распределение
mu, sigma = stats.norm.fit(returns)
x = np.linspace(returns.min(), returns.max(), 100)
pdf = stats.norm.pdf(x, mu, sigma)
ax2.plot(x, pdf, 'r-', linewidth=2)
# Добавляем вертикальные линии для стандартных отклонений
for i in range(1, 4):
ax2.axvline(mu + i * sigma, color='blue', linestyle='--', alpha=0.5)
ax2.axvline(mu - i * sigma, color='blue', linestyle='--', alpha=0.5)
ax2.set_xlabel('Дневная доходность (%)')
ax2.set_ylabel('Плотность вероятности')
ax2.set_title('Распределение дневной доходности')
ax2.text(0.05, 0.95, f'μ = {mu:.2f}%, σ = {sigma:.2f}%',
transform=ax2.transAxes, verticalalignment='top')
# QQ-Plot для проверки нормальности распределения
ax3 = fig.add_subplot(gs[1, 1])
stats.probplot(returns, dist="norm", plot=ax3)
ax3.set_title('QQ-Plot дневной доходности')
plt.tight_layout()
plt.show()
# Вычисляем статистики и печатаем их
skewness = stats.skew(returns)
kurtosis = stats.kurtosis(returns)
jarque_bera = stats.jarque_bera(returns)
print(f"Статистика распределения доходности {ticker}:")
print(f"Среднедневная доходность: {mu:.2f}%")
print(f"Стандартное отклонение (волатильность): {sigma:.2f}%")
print(f"Коэффициент асимметрии (Skewness): {skewness:.2f}")
print(f"Коэффициент эксцесса (Kurtosis): {kurtosis:.2f}")
print(f"p-значение теста Jarque-Bera: {jarque_bera.pvalue:.4f} (Нормальность распределения)")
Рис. 9: Визуализация графика цен акций, вероятностного распредения дневной доходностий и Q-Q plot
Статистика распределения доходности F:
Среднедневная доходность: 0.00%
Стандартное отклонение (волатильность): 2.39%
Коэффициент асимметрии (Skewness): -1.98
Коэффициент эксцесса (Kurtosis): 14.44
p-значение теста Jarque-Bera: 0.0000 (Нормальность распределения)
Эта визуализация сочетает свечной график с анализом распределения доходности. Гистограмма показывает фактическое распределение дневной доходности в сравнении с нормальным распределением (красная линия). Вертикальные пунктирные линии отмечают стандартные отклонения, которые часто используются для оценки рисков.
QQ-plot справа позволяет визуально оценить, насколько распределение доходности соответствует нормальному.
Отклонения от прямой линии (особенно на хвостах распределения) указывают на «толстые хвосты» — то есть вероятность экстремальных движений цены выше, чем предсказывает нормальное распределение. Эта информация критически важна при разработке моделей риска и алгоритмов торговли.
Renko график
Графики Renko фокусируются исключительно на движении цены и игнорируют время и объем. Они создают блоки фиксированного размера, которые меняют направление только при значительном движении цены.
# Создаем Renko данные
def renko_data(df, brick_size=None):
if brick_size is None:
# Если размер блока не указан, рассчитываем его как 1% от средней цены
brick_size = df['Close'].mean() * 0.01
renko_df = pd.DataFrame(columns=['Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Direction'])
current_brick = df['Close'].iloc[0]
direction = 0 # 0 - начальный, 1 - вверх, -1 - вниз
for i, row in df.iterrows():
close = row['Close']
volume = row['Volume']
date = i
# Определяем, нужно ли создать новый блок
if direction == 1: # Если предыдущее движение было вверх
if close >= current_brick + brick_size:
# Добавляем столько блоков, сколько возможно
while close >= current_brick + brick_size:
renko_df = pd.concat([renko_df, pd.DataFrame({
'Date': [date],
'Open': [current_brick],
'High': [current_brick + brick_size],
'Low': [current_brick],
'Close': [current_brick + brick_size],
'Volume': [volume],
'Direction': [1]
})], ignore_index=True)
current_brick += brick_size
elif close <= current_brick - brick_size:
# Разворот вниз
renko_df = pd.concat([renko_df, pd.DataFrame({
'Date': [date],
'Open': [current_brick],
'High': [current_brick],
'Low': [current_brick - brick_size],
'Close': [current_brick - brick_size],
'Volume': [volume],
'Direction': [-1]
})], ignore_index=True)
current_brick -= brick_size
direction = -1
elif direction == -1: # Если предыдущее движение было вниз
if close <= current_brick - brick_size:
# Добавляем столько блоков, сколько возможно
while close <= current_brick - brick_size: renko_df = pd.concat([renko_df, pd.DataFrame({ 'Date': [date], 'Open': [current_brick], 'High': [current_brick], 'Low': [current_brick - brick_size], 'Close': [current_brick - brick_size], 'Volume': [volume], 'Direction': [-1] })], ignore_index=True) current_brick -= brick_size elif close >= current_brick + brick_size:
# Разворот вверх
renko_df = pd.concat([renko_df, pd.DataFrame({
'Date': [date],
'Open': [current_brick],
'High': [current_brick + brick_size],
'Low': [current_brick],
'Close': [current_brick + brick_size],
'Volume': [volume],
'Direction': [1]
})], ignore_index=True)
current_brick += brick_size
direction = 1
else: # Начальное состояние
if close >= current_brick + brick_size:
# Первый блок вверх
renko_df = pd.concat([renko_df, pd.DataFrame({
'Date': [date],
'Open': [current_brick],
'High': [current_brick + brick_size],
'Low': [current_brick],
'Close': [current_brick + brick_size],
'Volume': [volume],
'Direction': [1]
})], ignore_index=True)
current_brick += brick_size
direction = 1
elif close <= current_brick - brick_size:
# Первый блок вниз
renko_df = pd.concat([renko_df, pd.DataFrame({
'Date': [date],
'Open': [current_brick],
'High': [current_brick],
'Low': [current_brick - brick_size],
'Close': [current_brick - brick_size],
'Volume': [volume],
'Direction': [-1]
})], ignore_index=True)
current_brick -= brick_size
direction = -1
# Устанавливаем дату как индекс
if not renko_df.empty:
renko_df.set_index('Date', inplace=True)
return renko_df
# Создаем Renko данные с размером блока 2% от средней цены
brick_size = data['Close'].mean() * 0.02
renko = renko_data(data, brick_size)
# Настраиваем цвета блоков
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Создаем Renko график
if not renko.empty:
mpf.plot(renko, type='candle', style=s,
title=f'Renko график акций Ford (размер блока: {brick_size:.2f}$)',
ylabel='Цена ($)',
volume=False,
figsize=(18, 8))
else:
print("Нет данных для построения Renko графика с указанным размером блока.")
Рис. 10: Renko график акций Ford
Графики Renko помогают отфильтровать «шум» рынка и сосредоточиться только на значительных движениях цены. Они упрощают идентификацию трендов и точек разворота.
График Point and Figure (PnF)
Point and Figure (PnF) — это еще один тип графика, который фокусируется только на движении цены, игнорируя время и объем. PnF использует столбцы X и O для обозначения повышения и понижения цены соответственно.
# Создаем функцию для генерации данных Point and Figure
def point_and_figure(df, box_size=None, reversal_size=3):
if box_size is None:
# Если размер бокса не указан, рассчитываем его как 1% от средней цены
box_size = df['Close'].mean() * 0.01
# Инициализируем переменные
pnf_data = []
current_price = df['Close'].iloc[0]
current_direction = None
column_data = []
current_column_price = current_price
for i, row in df.iterrows():
close = row['Close']
date = i
if current_direction is None:
# Первое движение
if close > current_price + box_size:
current_direction = 'X'
# Добавляем столько X, сколько возможно
boxes = int((close - current_price) / box_size)
for j in range(boxes):
column_data.append((current_price + j * box_size, 'X', date))
current_price += boxes * box_size
elif close < current_price - box_size: current_direction = 'O' # Добавляем столько O, сколько возможно boxes = int((current_price - close) / box_size) for j in range(boxes): column_data.append((current_price - j * box_size, 'O', date)) current_price -= boxes * box_size elif current_direction == 'X': if close > current_price + box_size:
# Продолжаем добавлять X
boxes = int((close - current_price) / box_size)
for j in range(boxes):
column_data.append((current_price + j * box_size, 'X', date))
current_price += boxes * box_size
elif close < current_price - box_size * reversal_size:
# Разворот на O
if column_data:
pnf_data.append(column_data)
current_direction = 'O'
column_data = []
boxes = int((current_price - close) / box_size)
for j in range(boxes):
column_data.append((current_price - j * box_size, 'O', date))
current_price -= boxes * box_size
elif current_direction == 'O':
if close < current_price - box_size: # Продолжаем добавлять O boxes = int((current_price - close) / box_size) for j in range(boxes): column_data.append((current_price - j * box_size, 'O', date)) current_price -= boxes * box_size elif close > current_price + box_size * reversal_size:
# Разворот на X
if column_data:
pnf_data.append(column_data)
current_direction = 'X'
column_data = []
boxes = int((close - current_price) / box_size)
for j in range(boxes):
column_data.append((current_price + j * box_size, 'X', date))
current_price += boxes * box_size
# Добавляем последний столбец, если он не пуст
if column_data:
pnf_data.append(column_data)
return pnf_data
# Рассчитываем размер бокса как 1% от средней цены
box_size = data['Close'].mean() * 0.01
pnf_data = point_and_figure(data, box_size, reversal_size=3)
# Создаем график Point and Figure
fig, ax = plt.subplots(figsize=(12, 8))
x_pos = 0
min_price = float('inf')
max_price = float('-inf')
for column in pnf_data:
prices = [price for price, symbol, _ in column]
symbols = [symbol for _, symbol, _ in column]
min_price = min(min_price, min(prices))
max_price = max(max_price, max(prices))
for price, symbol, _ in column:
if symbol == 'X':
ax.text(x_pos, price, 'X', ha='center', va='center', color='black', fontweight='bold')
else:
ax.text(x_pos, price, 'O', ha='center', va='center', color='gray', fontweight='bold')
x_pos += 1
# Настройка графика
ax.set_xlim(-0.5, x_pos - 0.5)
ax.set_ylim(min_price * 0.98, max_price * 1.02)
ax.set_title(f'Point and Figure график {ticker} (размер бокса: {box_size:.2f}$, разворот: 3 бокса)')
ax.set_ylabel('Цена ($)')
ax.set_xlabel('Столбцы')
ax.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.show()
Рис. 11: Point and Figure (PnF) график цен акций Ford
Графики Point and Figure позволяют аналитикам фокусироваться исключительно на движении цены, отфильтровывая незначительные колебания. Они особенно полезны для определения ключевых уровней поддержки и сопротивления, а также для выявления прорывов и разворотов тренда.
График с визуализацией объема в виде профиля цены-времени (TPO)
Профиль цены-времени (Time-Price Opportunity, TPO) — это метод визуализации, который показывает распределение объема торгов по времени и цене.
# Функция генерации TPO профиля
def tpo_profile(df, price_bins=50, time_bins=30):
price_min = df['Low'].min()
price_max = df['High'].max()
price_bin_edges = np.linspace(price_min, price_max, price_bins + 1)
tpo_matrix = np.zeros((price_bins, len(df)))
for i, (idx, row) in enumerate(df.iterrows()):
low_idx = np.digitize(row['Low'], price_bin_edges) - 1
high_idx = np.digitize(row['High'], price_bin_edges)
high_idx = min(high_idx, price_bins)
for j in range(low_idx, high_idx):
tpo_matrix[j, i] = 1
if len(df) > time_bins:
time_step = len(df) // time_bins
tpo_matrix_resampled = np.zeros((price_bins, time_bins))
for i in range(time_bins):
start_idx = i * time_step
end_idx = (i + 1) * time_step if i < time_bins - 1 else len(df) tpo_matrix_resampled[:, i] = np.sum(tpo_matrix[:, start_idx:end_idx], axis=1) else: tpo_matrix_resampled = tpo_matrix return tpo_matrix_resampled, price_bin_edges # Генерируем TPO-данные tpo_data, price_bins = tpo_profile(data) # Создаем фигуру с двумя subplot'ами fig, axes = plt.subplots(1, 2, figsize=(18, 8), gridspec_kw={'width_ratios': [3, 2]}) ax_volume, ax_tpo = axes[0], axes[1] # Левый график: только объем mpf.plot( data, type='line', style='charles', ax=ax_volume, volume=ax_volume.twinx(), ylabel='', returnfig=False, show_nontrading=False ) # Получаем ось объема ax_vol_only = ax_volume.twinx() # Скрываем основную ось (с ценой) ax_volume.set_visible(False) # Чистим ось объема от лишних подписей и линий ax_vol_only.spines['left'].set_visible(False) ax_vol_only.spines['top'].set_visible(False) ax_vol_only.spines['right'].set_visible(True) ax_vol_only.spines['bottom'].set_visible(True) # Убираем метки Y ax_vol_only.tick_params(left=False, right=False, labelleft=False) ax_vol_only.yaxis.set_ticks([]) # Подпись оси X и заголовок ax_vol_only.set_title('Объём торгов', fontsize=14) ax_vol_only.set_xlabel('Дата') # Правый график: TPO профиль colors = [(0.580, 0.647, 0.867), (1, 1, 0)] # #94a5dd -> жёлтый
custom_cmap = LinearSegmentedColormap.from_list('custom_cyan_yellow', colors, N=256)
im = ax_tpo.imshow(tpo_data, aspect='auto', origin='lower', cmap=custom_cmap, interpolation='none')
ax_tpo.set_title('TPO Профиль', fontsize=14)
ax_tpo.set_xlabel('Временные периоды')
ax_tpo.set_ylabel('Цена ($)')
# Настраиваем метки по оси Y (цены)
price_step = max(1, len(price_bins) // 10)
price_ticks = np.arange(0, tpo_data.shape[0], price_step)
price_labels = [f"{price_bins[i]:.2f}" for i in price_ticks]
ax_tpo.set_yticks(price_ticks)
ax_tpo.set_yticklabels(price_labels)
# Добавляем colorbar для наглядности
cbar = plt.colorbar(im, ax=ax_tpo, orientation='vertical', fraction=0.03, pad=0.04)
cbar.set_label('TPO Count', rotation=270, labelpad=15)
plt.tight_layout()
plt.show()
Рис. 12: График TPO для Ford (профиля цены-времени)
TPO профиль помогает понять, где и когда происходила наибольшая торговая активность. Области с высокой плотностью TPO указывают на ценовые уровни, которые рынок считает справедливыми, и могут служить уровнями поддержки или сопротивления.
Визуализация гэпов (Price Gaps)
Гэпы (ценовые разрывы) — важный элемент технического анализа котировок акций. Давайте создадим визуализацию, которая автоматически выявляет и выделяет ценовые гэпы:
# Функция для определения гэпов
def find_gaps(df, threshold_pct=2.0): # Порог 2%
gaps = pd.DataFrame(index=df.index)
gaps['Gap_Up'] = (df['Low'] > df['Open'].shift(1)) & \
((df['Low'] - df['Open'].shift(1)) / df['Open'].shift(1) * 100 > threshold_pct)
gaps['Gap_Down'] = (df['High'] < df['Open'].shift(1)) & \ ((df['Open'].shift(1) - df['High']) / df['Open'].shift(1) * 100 > threshold_pct)
gaps['Gap_Size'] = 0.0
gaps.loc[gaps['Gap_Up'], 'Gap_Size'] = (df['Low'] - df['Open'].shift(1)) / df['Open'].shift(1) * 100
gaps.loc[gaps['Gap_Down'], 'Gap_Size'] = (df['Open'].shift(1) - df['High']) / df['Open'].shift(1) * 100
return gaps
gaps = find_gaps(data)
# Создаем Series для маркеров
gap_up_annotations = pd.Series(np.nan, index=data.index)
gap_down_annotations = pd.Series(np.nan, index=data.index)
for idx in gaps.index:
if gaps.loc[idx, 'Gap_Up']:
gap_up_annotations.loc[idx] = data.loc[idx, 'Low'] * 0.975 # немного ниже
elif gaps.loc[idx, 'Gap_Down']:
gap_down_annotations.loc[idx] = data.loc[idx, 'High'] * 1.025 # немного выше
# Настройка стиля графика
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Добавляем маркеры гэпов
apds = [
mpf.make_addplot(gap_up_annotations, type='scatter', marker='^', markersize=150, color='darkgreen'),
mpf.make_addplot(gap_down_annotations, type='scatter', marker='v', markersize=150, color='darkred'),
]
# Строим график
fig, axes = mpf.plot(data, type='candle', style=s,
title=f'Визуализация гэпов на графике {ticker} (порог: 2%)',
ylabel='Цена ($)',
addplot=apds,
volume=False,
figsize=(18, 8),
returnfig=True)
# Добавляем аннотации с размерами гэпов
ax_main = axes[0]
for idx in gaps.index:
if gaps.loc[idx, 'Gap_Up']:
gap_size = gaps.loc[idx, 'Gap_Size']
price = gap_up_annotations.loc[idx]
ax_main.annotate(f'+{gap_size:.1f}%',
xy=(idx, price),
xytext=(0, 0), textcoords='offset points',
color='darkgreen',
fontweight='bold',
ha='center')
elif gaps.loc[idx, 'Gap_Down']:
gap_size = gaps.loc[idx, 'Gap_Size']
price = gap_down_annotations.loc[idx]
ax_main.annotate(f'-{gap_size:.1f}%',
xy=(idx, price),
xytext=(0, 0), textcoords='offset points',
color='darkred',
fontweight='bold',
ha='center')
plt.tight_layout()
plt.show()
Рис. 13: Свечной график Ford с подсвечиванием гэпов >2%
Этот график автоматически выявляет и выделяет значительные ценовые гэпы (разрывы) между торговыми сессиями. Зеленые треугольники указывают на гэпы вверх, а красные — на гэпы вниз. Гэпы часто являются важным сигналом изменения рыночного настроения и могут предвещать продолжение или разворот тренда.
Визуализация внутридневной волатильности (High-Low Range)
Создадим визуализацию, которая показывает внутридневную волатильность (диапазон High-Low) как процент от цены закрытия:
# Рассчитываем внутридневную волатильность
data['HL_Range'] = (data['High'] - data['Low']) / data['Close'] * 100 # в процентах
# Создаем скользящее среднее диапазона для сравнения
data['HL_Range_MA15'] = data['HL_Range'].rolling(window=15).mean()
# Создаем график
fig, axes = plt.subplots(2, 1, figsize=(18, 12), gridspec_kw={'height_ratios': [3, 1]})
# Верхний график - свечной
mpf.plot(data, type='candle', style='charles', ax=axes[0])
axes[0].set_ylabel('Цена ($)')
axes[0].set_title('Внутридневная волатильность акций Ford')
# Нижний график - волатильность
axes[1].bar(np.arange(len(data)), data['HL_Range'], color='darkblue', alpha=0.6, width=0.8)
axes[1].plot(np.arange(len(data)), data['HL_Range_MA15'], color='red', linewidth=2, label='15-дневное скользящее среднее')
axes[1].set_ylabel('HL Range (%)')
axes[1].set_xlabel('Дата')
axes[1].legend()
# Настраиваем оси X
step = max(1, len(data) // 10)
date_indices = range(0, len(data), step)
date_labels = [data.index[i].strftime('%Y-%m-%d') for i in date_indices]
axes[1].set_xticks(list(date_indices))
axes[1].set_xticklabels(date_labels, rotation=45)
axes[0].set_xticks(list(date_indices))
axes[0].set_xticklabels([])
# Добавляем среднюю линию волатильности
avg_volatility = data['HL_Range'].mean()
axes[1].axhline(y=avg_volatility, color='green', linestyle='--', alpha=0.7,
label=f'Средняя волатильность: {avg_volatility:.2f}%')
axes[1].text(len(data)-1, avg_volatility, f' {avg_volatility:.2f}%',
va='center', ha='left', color='green', fontweight='bold')
# Находим дни с экстремальной волатильностью (выше 2 стандартных отклонений)
std_volatility = data['HL_Range'].std()
extreme_volatility = data['HL_Range'] > (avg_volatility + 2 * std_volatility)
# Выделяем дни с экстремальной волатильностью
if extreme_volatility.any():
extreme_indices = np.where(extreme_volatility)[0]
extreme_values = data['HL_Range'].iloc[extreme_indices]
axes[1].scatter(extreme_indices, extreme_values, color='red', s=100, zorder=5,
label=f'Экстремальная волатильность (>{avg_volatility + 2*std_volatility:.2f}%)')
# Выделяем соответствующие свечи на основном графике
for idx in extreme_indices:
axes[0].axvspan(idx-0.5, idx+0.5, color='red', alpha=0.2)
axes[1].legend()
plt.tight_layout()
plt.show()
Рис. 14: Свечной график с подсветкой дней повышенной внутридневной волатильности
Этот график визуализирует внутридневную волатильность как процент от цены закрытия. Нижняя панель показывает диапазон High-Low в процентах, 15-дневное скользящее среднее волатильности и среднюю волатильность за весь период.
Дни с экстремальной волатильностью (выше двух стандартных отклонений от среднего) выделены красными точками на графике волатильности и красной подсветкой на свечном графике. Такая визуализация помогает выявить периоды повышенной неопределенности на рынке и потенциальные точки разворота тренда.
Кластерный график (Cluster Chart)
С помощью mplfinance можно создать кластерный график котировок интересующей акции, который группирует свечи OHLC по их характеристикам:
import mplfinance as mpf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from matplotlib.colors import LinearSegmentedColormap
from sklearn.preprocessing import StandardScaler
# Создаем признаки для кластеризации
features = pd.DataFrame(index=data.index)
features['HL_Range'] = (data['High'] - data['Low']) / data['Close'] * 100 # Диапазон High-Low (%)
features['OC_Range'] = abs(data['Open'] - data['Close']) / data['Close'] * 100 # Диапазон Open-Close (%)
features['WickLength'] = ((data['High'] - data['Low']) - abs(data['Open'] - data['Close'])) / data['Close'] * 100 # Длина теней (%)
features['BodyToWick'] = abs(data['Open'] - data['Close']) / ((data['High'] - data['Low']) + 0.001) # Отношение тела к полной длине
features['PriceChange'] = data['Close'].pct_change() * 100 # Изменение цены (%)
features['Volume_Rel'] = data['Volume'] / data['Volume'].rolling(20).mean() # Относительный объем
# Заполняем NaN значения
features = features.fillna(0)
# Нормализуем признаки
scaler = StandardScaler()
features_scaled = scaler.fit_transform(features)
# Определяем количество кластеров
n_clusters = 3
# Выполняем кластеризацию
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
data['Cluster'] = kmeans.fit_predict(features_scaled)
# Создаем цветовую палитру для кластеров
cluster_colors = plt.cm.tab10(np.linspace(0, 1, n_clusters))
# Строим график
fig, axes = plt.subplots(2, 1, figsize=(18, 12), gridspec_kw={'height_ratios': [3, 1]})
mpf.plot(data, type='candle', style='charles', ax=axes[0])
axes[0].set_ylabel('Цена ($)')
axes[0].set_title('Кластерный анализ свечей акций Ford')
# Добавляем цветные полосы, обозначающие кластеры
for i in range(len(data)):
cluster = data['Cluster'].iloc[i]
color = cluster_colors[cluster]
axes[0].axvspan(i-0.4, i+0.4, alpha=0.3, color=color)
# Нижний график - профили кластеров
cluster_centers = kmeans.cluster_centers_
cluster_features = features.columns
# Для каждого кластера показываем профиль признаков
bar_width = 0.8 / n_clusters
x = np.arange(len(cluster_features))
for i in range(n_clusters):
axes[1].bar(x + i * bar_width, cluster_centers[i], bar_width,
color=cluster_colors[i], alpha=0.7,
label=f'Кластер {i}')
axes[1].set_xticks(x + bar_width * (n_clusters - 1) / 2)
axes[1].set_xticklabels(cluster_features, rotation=45)
axes[1].set_ylabel('Стандартизированное значение')
axes[1].set_xlabel('Признак')
axes[1].legend(loc='upper left', ncol=n_clusters)
# Анализируем характеристики кластеров
cluster_summary = pd.DataFrame(index=range(n_clusters), columns=['Count', 'Avg_Return', 'Up_Days', 'Down_Days'])
for i in range(n_clusters):
cluster_data = data[data['Cluster'] == i]
next_day_returns = data['Close'].pct_change().shift(-1)
cluster_summary.loc[i, 'Count'] = len(cluster_data)
cluster_summary.loc[i, 'Avg_Return'] = next_day_returns[cluster_data.index].mean() * 100
cluster_summary.loc[i, 'Up_Days'] = (next_day_returns[cluster_data.index] > 0).mean() * 100
cluster_summary.loc[i, 'Down_Days'] = (next_day_returns[cluster_data.index] < 0).mean() * 100
# Добавляем таблицу с характеристиками кластеров
table_ax = fig.add_axes([0.15, 0.01, 0.7, 0.15])
table_ax.axis('off')
table_data = []
for i in range(n_clusters):
table_data.append([
f'Кластер {i}',
f'{cluster_summary.loc[i, "Count"]}',
f'{cluster_summary.loc[i, "Avg_Return"]:.2f}%',
f'{cluster_summary.loc[i, "Up_Days"]:.1f}%',
f'{cluster_summary.loc[i, "Down_Days"]:.1f}%'
])
table = table_ax.table(
cellText=table_data,
colLabels=['Кластер', 'Количество', 'Средняя доходность', 'Рост на след. день', 'Падение на след. день'],
loc='center',
cellLoc='center',
colWidths=[0.15, 0.15, 0.2, 0.2, 0.2]
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 1.5)
plt.tight_layout(rect=[0, 0.15, 1, 1])
plt.show()
Рис. 15: Кластерный анализ свечей акций Ford
Этот график использует алгоритм кластеризации KMeans для группировки свечей по их характеристикам, таким как внутридневной диапазон, соотношение тела к теням, изменение цены и объем.
Каждый кластер представлен своим цветом на основном графике. Нижняя панель показывает профили признаков для каждого кластера, а таблица внизу предоставляет статистику по кластерам, включая среднюю доходность на следующий день. Такой анализ может выявить типы свечей, которые с большей вероятностью предшествуют росту или падению цены.
Относительная производительность акций (Relative Performance Chart)
Относительная производительность — отличный способ сравнить динамику конкретной акции с сектором или индексом. Этот график показывает, насколько лучше или хуже выглядит акция по сравнению с эталоном.
import yfinance as yf
import mplfinance as mpf
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Загрузка данных
ticker = "F" #Акции Ford
benchmark = "^GSPC" #Индекс SP500
# Загружаем данные
data = yf.download(ticker, start="2024-05-16", end="2025-05-16")
benchmark_data = yf.download(benchmark, start="2024-05-16", end="2025-05-16")
# Убираем MultiIndex, если есть
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.droplevel(1)
if isinstance(benchmark_data.columns, pd.MultiIndex):
benchmark_data.columns = benchmark_data.columns.droplevel(1)
# Проверяем, что данные не пустые
if data.empty or benchmark_data.empty:
raise ValueError("Не удалось загрузить данные.")
# Приводим данные к правильному формату
for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
data[col] = pd.to_numeric(data[col], errors='coerce')
benchmark_data[col] = pd.to_numeric(benchmark_data[col], errors='coerce')
# Убираем NaN
data.dropna(inplace=True)
benchmark_data.dropna(inplace=True)
# Вычисляем относительную производительность
data_close = data['Close'].squeeze()
bench_close = benchmark_data['Close'].squeeze()
rel_perf = pd.DataFrame(index=data.index)
rel_perf['RelPerf'] = (data_close / data_close.iloc[0]) / (bench_close / bench_close.iloc[0])
# Строим график
fig, axes = plt.subplots(2, 1, figsize=(18, 10), gridspec_kw={'height_ratios': [3, 1]})
# Верхний график - свечной график конкретной акции
mpf.plot(data, type='candle', style='yahoo', ax=axes[0], volume=False)
axes[0].set_ylabel('Цена ($)')
axes[0].set_title(f'{ticker} vs {benchmark} - Относительная производительность')
# Нижний график - относительная производительность
axes[1].plot(np.arange(len(rel_perf)), rel_perf['RelPerf'], color='blue', linewidth=2)
axes[1].axhline(y=1, color='gray', linestyle='--', alpha=0.7)
axes[1].set_ylabel('Относительная производительность')
axes[1].fill_between(np.arange(len(rel_perf)),
rel_perf['RelPerf'],
1,
where=(rel_perf['RelPerf'] >= 1),
color='green',
alpha=0.3,
label='Опережает рынок')
axes[1].fill_between(np.arange(len(rel_perf)),
rel_perf['RelPerf'],
1,
where=(rel_perf['RelPerf'] < 1),
color='red',
alpha=0.3,
label='Отстает от рынка')
axes[1].legend()
# Настройка оси X
step = max(1, len(data) // 10)
date_indices = range(0, len(data), step)
date_labels = [data.index[i].strftime('%Y-%m-%d') for i in date_indices]
axes[1].set_xticks(list(date_indices))
axes[1].set_xticklabels(date_labels, rotation=45)
axes[0].set_xticks(list(date_indices))
axes[0].set_xticklabels([])
plt.tight_layout()
plt.show()
Рис. 16: Относительная производительность акций Ford по сравнению с индексом S&P 500
Визуализация уровней поддержки и сопротивления
Вместо технических индикаторов можно использовать алгоритмический подход для выявления уровней поддержки и сопротивления на основе исторических максимумов и минимумов.
from scipy.signal import argrelextrema
# Функция для нахождения локальных минимумов и максимумов
def find_extrema(df, order=15):
df = df.copy()
# Находим локальные минимумы и максимумы
df['min_idx'] = df.index.isin(df.index[argrelextrema(df['Low'].values, np.less, order=order)[0]])
df['max_idx'] = df.index.isin(df.index[argrelextrema(df['High'].values, np.greater, order=order)[0]])
return df
# Находим экстремумы
extrema_data = find_extrema(data)
# Выделяем значительные уровни поддержки и сопротивления
def find_significant_levels(df, threshold_pct=1.0):
# Группируем близкие минимумы и максимумы
min_levels = df[df['min_idx']]['Low'].values
max_levels = df[df['max_idx']]['High'].values
# Убедимся, что массивы не пустые
if len(min_levels) == 0 or len(max_levels) == 0:
return [], []
# Кластеризуем уровни
avg_price = df['Close'].mean()
threshold = avg_price * threshold_pct / 100
clustered_mins = []
for level in min_levels:
level = float(level)
if not clustered_mins:
clustered_mins.append(level)
else:
distances = [abs(level - cl) for cl in clustered_mins]
if min(distances) > threshold:
clustered_mins.append(level)
clustered_maxs = []
for level in max_levels:
level = float(level)
if not clustered_maxs:
clustered_maxs.append(level)
else:
distances = [abs(level - cl) for cl in clustered_maxs]
if min(distances) > threshold:
clustered_maxs.append(level)
return clustered_mins, clustered_maxs
# Находим значимые уровни
support_levels, resistance_levels = find_significant_levels(extrema_data, threshold_pct=1.0)
# Создаем график с уровнями поддержки и сопротивления
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Рисуем график и получаем fig + axes
fig, axes = mpf.plot(data, type='candle', style=s,
title=f'Уровни поддержки и сопротивления для {ticker}',
ylabel='Цена ($)',
volume=False,
figsize=(18, 8),
returnfig=True)
# Определяем ось с графиком свечей
ax = axes[0] #
# Добавляем линии поддержки
for level in support_levels:
ax.axhline(y=level, color='green', linestyle='--', alpha=0.7, linewidth=1.5)
ax.text(len(data)-1, level, f' S: {level:.2f}',
va='center', ha='left', color='green', fontweight='bold')
# Добавляем линии сопротивления
for level in resistance_levels:
ax.axhline(y=level, color='red', linestyle='--', alpha=0.7, linewidth=1.5)
ax.text(len(data)-1, level, f' R: {level:.2f}',
va='center', ha='left', color='red', fontweight='bold')
plt.tight_layout()
plt.show()
Рис. 17: Свечной график акций с автоматически выявленными уровнями поддержки и сопротивления
Визуализация сезонности цен (Seasonality Chart)
Анализ сезонности помогает выявить повторяющиеся паттерны в движении цен акций:
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Загружаем котировки за несколько лет для сезонного анализа
ticker = "F" # Ford Motor
data = yf.download(ticker, start="2020-01-01", end="2025-05-16")
# Добавляем информацию о годе и дне года
data['Year'] = data.index.year
data['DayOfYear'] = data.index.dayofyear
# Нормализуем цены закрытия для каждого года (начальная цена = 100)
yearly_data = []
for year in data['Year'].unique():
year_data = data[data['Year'] == year].copy()
if not year_data.empty:
year_data['NormClose'] = year_data['Close'] / year_data['Close'].iloc[0] * 100
yearly_data.append(year_data)
# Создаем график сезонности
plt.figure(figsize=(18, 10))
# Строим линии для каждого года
colors = plt.cm.viridis(np.linspace(0, 1, len(yearly_data)))
for i, year_data in enumerate(yearly_data):
plt.plot(year_data['DayOfYear'], year_data['NormClose'],
linewidth=1.5, color=colors[i], alpha=0.7,
label=f'{year_data["Year"].iloc[0]}')
# Добавляем среднюю линию по всем годам
all_data = pd.concat(yearly_data)
avg_by_day = all_data.groupby('DayOfYear')['NormClose'].mean()
plt.plot(avg_by_day.index, avg_by_day.values,
linewidth=3, color='black', label='Среднее')
# Отмечаем кварталы
q_starts = [1, 91, 182, 274] # Примерные дни начала кварталов
for i, q in enumerate(q_starts):
plt.axvline(x=q, color='gray', linestyle='--', alpha=0.5)
plt.text(q+5, 80, f'Q{i+1}', fontsize=12)
plt.title(f'Сезонность цен акций {ticker} по годам')
plt.xlabel('День года')
plt.ylabel('Нормализованная цена (начало года = 100)')
plt.legend(loc='upper left', ncol=3)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Рис. 18: Сезонность цен акций Ford по годам
Визуализация дивергенции цены и объема (Price-Volume Divergence)
Визуализация Price-Volume Divergence может быть эффективной для выявления потенциальных разворотов тренда.
Основная идея заключается в сравнении динамики цены актива и соответствующего объема торгов: если цена растет, а объем падает (или наоборот), это может сигнализировать о слабости текущего тренда. Такие расхождения (дивергенции) могут указывать на скорое изменение направления движения цены, что особенно полезно при принятии решений о входе или выходе из позиции.
Вот код на Python для построения такой визуализации:
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import pearsonr
import mplfinance as mpf
# Загружаем котировки
ticker = "F" # Ford Motor
data = yf.download(ticker, start="2024-05-16", end="2025-05-16")
# Убираем MultiIndex, если есть
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.droplevel(1)
# Приводим все числовые колонки к типу float
for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
data[col] = pd.to_numeric(data[col], errors='coerce')
# Рассчитываем относительные изменения цены и объема
data['Price_Change'] = data['Close'].pct_change()
data['Volume_Change'] = data['Volume'].pct_change()
# Рассчитываем коэффициент корреляции в скользящем окне
window_size = 20
correlations = []
dates = []
for i in range(window_size, len(data)):
window_data = data.iloc[i-window_size:i]
corr, _ = pearsonr(
window_data['Price_Change'].fillna(0),
window_data['Volume_Change'].fillna(0)
)
correlations.append(corr)
dates.append(data.index[i])
# Создаем DataFrame с корреляциями
corr_df = pd.DataFrame({'Date': dates, 'Correlation': correlations})
corr_df.set_index('Date', inplace=True)
# Настраиваем стиль свечного графика
mc = mpf.make_marketcolors(up='black', down='gray',
edge='inherit',
wick={'up':'black', 'down':'gray'},
volume='inherit')
s = mpf.make_mpf_style(base_mpf_style='charles', marketcolors=mc)
# Создаем фигуру
fig, axes = plt.subplots(2, 1, figsize=(16, 12), gridspec_kw={'height_ratios': [3, 1]})
# Верхний график - только свечи
ax_main = axes[0]
# Строим свечной график БЕЗ объема
mpf.plot(data, type='candle', style=s, ax=ax_main)
ax_main.set_ylabel('Цена ($)')
ax_main.set_title(f'Дивергенция цены и объема для {ticker}')
# Нижний график - корреляция
axes[1].plot(np.arange(len(corr_df)), corr_df['Correlation'], color='blue', linewidth=2)
axes[1].axhline(y=0, color='gray', linestyle='--', alpha=0.7)
axes[1].set_ylabel('Корреляция цены и объема')
axes[1].set_ylim(-1.1, 1.1)
# Подсвечиваем зоны сильной корреляции
axes[1].fill_between(np.arange(len(corr_df)),
corr_df['Correlation'],
0,
where=(corr_df['Correlation'] >= 0.6),
color='green',
alpha=0.3,
label='Сильная положительная корреляция')
axes[1].fill_between(np.arange(len(corr_df)),
corr_df['Correlation'],
0,
where=(corr_df['Correlation'] <= -0.6),
color='red',
alpha=0.3,
label='Сильная отрицательная корреляция')
axes[1].legend(loc='lower right')
# Настраиваем ось X
step = max(1, len(data) // 10)
date_indices = range(0, len(data), step)
date_labels = [data.index[i].strftime('%Y-%m-%d') for i in date_indices]
axes[1].set_xticks(list(date_indices))
axes[1].set_xticklabels(date_labels, rotation=45)
plt.tight_layout()
plt.show()
Рис. 19: График дивергенции цены и объема торгов акциями
Построение графика корреляции между изменением цены и объемом позволяет наглядно отслеживать фазы рыночной активности: сильная положительная корреляция говорит о здоровом тренде, когда рост цен подкрепляется высоким объемом, а отрицательная корреляция часто предупреждает о возможном развороте.
Наличие свечного графика вверху и графика корреляции внизу делает анализ комплексным: можно одновременно наблюдать за движением цены и поведением объема, что особенно ценно при оценке рыночных импульсов и эмоций участников рынка. Такой подход широко используется как частными трейдерами, так и профессионалами для более точного прогнозирования движений цены.
Визуализация аномальных объемов торгов (Volume Anomalies)
Аномальные всплески объема часто предшествуют значительным движениям цены. Давайте создадим визуализацию, которая автоматически выявляет и отмечает аномальные объемы торгов:
# Рассчитываем медиану и стандартное отклонение объема (за 20 периодов)
rolling_median = data['Volume'].rolling(window=20).median()
rolling_std = data['Volume'].rolling(window=20).std()
# Определяем аномальные объемы (более 2 стандартных отклонений от медианы)
data['Volume_Z_Score'] = (data['Volume'] - rolling_median) / rolling_std
volume_anomalies = data[data['Volume_Z_Score'] > 2].index
# Создаем цветовую схему для подсвечивания дней с аномальным объемом
colors = ['lightgray' if i not in volume_anomalies else 'red' for i in data.index]
# Создаем визуализацию
fig, axes = mpf.plot(
data,
type='candle',
style='charles',
title=f'Аномальные объемы торгов - {ticker}',
ylabel='Цена ($)',
volume=True,
figsize=(18, 10),
returnfig=True
)
# Подсвечиваем дни с аномальным объемом на графике объема
ax_volume = axes[2]
for i, date in enumerate(data.index):
if date in volume_anomalies:
idx = data.index.get_loc(date)
ax_volume.axvspan(idx-0.4, idx+0.4, color='red', alpha=0.3)
# Добавляем текстовую метку с процентным изменением цены на следующий день
if idx+1 < len(data): next_day_change = (data['Close'].iloc[idx+1] / data['Close'].iloc[idx] - 1) * 100 direction = "↑" if next_day_change > 0 else "↓"
axes[0].annotate(f"{direction}{abs(next_day_change):.1f}%",
xy=(idx, data['High'].iloc[idx]),
xytext=(0, 10),
textcoords='offset points',
ha='center',
color='darkred',
fontweight='bold')
plt.show()
Рис. 20: Свечной график с выделением дней аномального объема торгов
Этот график автоматически выявляет и выделяет дни с аномально высоким объемом торгов (более двух стандартных отклонений от 20-дневной скользящей медианы). Для каждого такого дня на основном графике отображается процентное изменение цены на следующий торговый день, что позволяет оценить потенциальное влияние аномального объема на дальнейшее движение цены.
Визуализация возвратов к среднему (Mean Reversion)
Концепция возврата к среднему (Mean Reversion) — одна из популярных стратегий биржевого трейдинга. Она предполагает, что цена актива имеет тенденцию возвращаться к своему среднему значению после значительных отклонений.
Давайте создадим визуализацию с помощью Python и mplfinance, которая поможет определить потенциальные точки для стратегии возврата к среднему:
from matplotlib.patches import Patch
# Расчет экспоненциального скользящего среднего и стандартного отклонения
window = 30
data['EMA'] = data['Close'].ewm(span=window, adjust=False).mean()
data['STD'] = data['Close'].rolling(window=window).std()
# Расчет z-score
data['Z_Score'] = (data['Close'] - data['EMA']) / data['STD']
# Определение экстремальных отклонений
extreme_oversold = data[data['Z_Score'] < -2].index moderate_oversold = data[(data['Z_Score'] >= -2) & (data['Z_Score'] < -1.5)].index neutral_zone = data[(data['Z_Score'] >= -1.5) & (data['Z_Score'] <= 1.5)].index moderate_overbought = data[(data['Z_Score'] > 1.5) & (data['Z_Score'] <= 2)].index extreme_overbought = data[data['Z_Score'] > 2].index
# Цветовая функция
def calculate_reversion_color(row):
if row['Z_Score'] > 2:
return 'red' # Сильно перекуплен
elif row['Z_Score'] > 1.5:
return 'salmon' # Умеренно перекуплен
elif row['Z_Score'] < -2:
return 'green' # Сильно перепродан
elif row['Z_Score'] < -1.5:
return 'lightgreen' # Умеренно перепродан
else:
return 'lightgray' # Нейтральная зона
data['Color'] = data.apply(calculate_reversion_color, axis=1)
# Визуализация
fig, axes = mpf.plot(
data[['Open', 'High', 'Low', 'Close', 'Volume']],
type='candle',
style='charles',
title=f'Визуализация возврата к среднему (на основе EMA) - {ticker}',
ylabel='Цена ($)',
volume=False,
figsize=(18, 12),
panel_ratios=(6, 3),
addplot=[
mpf.make_addplot(data['EMA'], color='blue', width=1.5),
mpf.make_addplot(data['Z_Score'], panel=1, color='blue',
ylabel='Z-Score\n(отклонение от EMA)')
],
returnfig=True
)
# Добавляем горизонтальные линии для пороговых значений Z-Score
axes[2].axhline(y=0, color='black', linestyle='-', alpha=0.3)
axes[2].axhline(y=1.5, color='orange', linestyle='--', alpha=0.7)
axes[2].axhline(y=-1.5, color='orange', linestyle='--', alpha=0.7)
axes[2].axhline(y=2, color='red', linestyle='--', alpha=0.8)
axes[2].axhline(y=-2, color='green', linestyle='--', alpha=0.8)
# Выделяем области Z-Score на графике
for i in range(len(data)):
axes[2].fill_between([i-0.4, i+0.4], [data['Z_Score'].iloc[i]]*2, [0]*2,
color=data['Color'].iloc[i], alpha=0.5)
# Отмечаем экстремальные точки и будущую доходность через 5 дней
if i < len(data) - 5: current_index = data.index[i] if current_index in extreme_oversold or current_index in extreme_overbought: future_return = (data['Close'].iloc[i + 5] / data['Close'].iloc[i] - 1) * 100 direction = "↑" if future_return > 0 else "↓"
price_level = data['Low'].iloc[i] if current_index in extreme_oversold else data['High'].iloc[i]
offset = -20 if current_index in extreme_oversold else +10
color = 'darkgreen' if current_index in extreme_oversold else 'darkred'
axes[0].annotate(f"{direction}{abs(future_return):.1f}%",
xy=(i, price_level),
xytext=(0, offset),
textcoords='offset points',
ha='center',
color=color,
fontweight='bold',
fontsize=9)
# Легенда в нижней панели
legend_elements = [
Patch(facecolor='green', edgecolor='green', alpha=0.5, label='Сильно перепродан (Z < -2)'),
Patch(facecolor='lightgreen', edgecolor='lightgreen', alpha=0.5, label='Умеренно перепродан (-2 ≤ Z < -1.5)'),
Patch(facecolor='lightgray', edgecolor='lightgray', alpha=0.5, label='Нейтральная зона (-1.5 ≤ Z ≤ 1.5)'),
Patch(facecolor='salmon', edgecolor='salmon', alpha=0.5, label='Умеренно перекуплен (1.5 < Z ≤ 2)'), Patch(facecolor='red', edgecolor='red', alpha=0.5, label='Сильно перекуплен (Z > 2)'),
]
axes[2].legend(handles=legend_elements, loc='upper left', fontsize=9, bbox_to_anchor=(0, 1))
plt.tight_layout()
plt.show()
Рис. 21: Визуализация стратегии возврата к среднему для акций Ford
Этот график показывает отклонение цены от 30-дневного экспоненциального скользящего среднего (EMA) в единицах стандартного отклонения (Z-Score). Экстремальные отклонения (Z > 2 или Z < -2) часто предшествуют возврату цены к среднему значению. На графике отмечен потенциальный доход через 5 дней после сигналов возврата к среднему, что позволяет оценить эффективность данной стратегии.
Анализ импульсных движений цены
Импульсные движения цены (price thrusts) — это быстрые однонаправленные движения, которые часто указывают на начало нового тренда или продолжение существующего. Создадим визуализацию, выявляющую и анализирующую такие движения:
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import mplfinance as mpf
from matplotlib.patches import Rectangle
# Функция для выявления импульсных движений
def detect_price_thrusts(df, min_days=2, min_return=3.0):
df = df.copy()
df['returns'] = df['Close'].pct_change() * 100
thrusts = []
in_thrust = False
thrust_start = None
thrust_direction = None
for i in range(1, len(df)):
if not in_thrust:
# Начало нового импульса — значительное однодневное движение (>1%)
if abs(df['returns'].iloc[i]) > 1.0:
in_thrust = True
thrust_start = df.index[i]
thrust_direction = 'up' if df['returns'].iloc[i] > 0 else 'down'
start_price = df['Close'].iloc[i]
start_idx = i
else:
current_price = df['Close'].iloc[i]
max_price = df['Close'].iloc[start_idx:i+1].max()
min_price = df['Close'].iloc[start_idx:i+1].min()
if thrust_direction == 'up':
if current_price < max_price * 0.99 or (i - start_idx) >= 10:
end_price = df['Close'].iloc[i-1]
total_return = (end_price / start_price - 1) * 100
days = i - start_idx
if days >= min_days and abs(total_return) >= min_return:
thrusts.append({
'start_date': thrust_start,
'end_date': df.index[i-1],
'direction': thrust_direction,
'days': days,
'return': total_return,
'start_idx': start_idx,
'end_idx': i-1
})
in_thrust = False
else: # down
if current_price > min_price * 1.01 or (i - start_idx) >= 10:
end_price = df['Close'].iloc[i-1]
total_return = (end_price / start_price - 1) * 100
days = i - start_idx
if days >= min_days and abs(total_return) >= min_return:
thrusts.append({
'start_date': thrust_start,
'end_date': df.index[i-1],
'direction': thrust_direction,
'days': days,
'return': total_return,
'start_idx': start_idx,
'end_idx': i-1
})
in_thrust = False
# Проверяем последний активный импульс
if in_thrust:
end_price = df['Close'].iloc[-1]
total_return = (end_price / start_price - 1) * 100
days = len(df) - start_idx
if days >= min_days and abs(total_return) >= min_return:
thrusts.append({
'start_date': thrust_start,
'end_date': df.index[-1],
'direction': thrust_direction,
'days': days,
'return': total_return,
'start_idx': start_idx,
'end_idx': len(df) - 1
})
return pd.DataFrame(thrusts)
# Обнаружение импульсных движений
thrusts_df = detect_price_thrusts(data, min_days=1, min_return=3.0)
# Построение графика без объема
fig, axes = mpf.plot(
data,
type='candle',
style='charles',
title=f'{ticker} - Анализ импульсных движений цены',
figsize=(18, 8),
volume=False,
returnfig=True
)
ax_main = axes[0]
# Выделение импульсных движений на основном графике
for _, thrust in thrusts_df.iterrows():
color = 'green' if thrust['direction'] == 'up' else 'red'
low_in_thrust = data['Low'].iloc[thrust['start_idx']:thrust['end_idx']+1].min()
high_in_thrust = data['High'].iloc[thrust['start_idx']:thrust['end_idx']+1].max()
rect = Rectangle(
(thrust['start_idx'], low_in_thrust * 0.98),
thrust['end_idx'] - thrust['start_idx'],
high_in_thrust * 1.04 - low_in_thrust * 0.98,
color=color, alpha=0.1
)
ax_main.add_patch(rect)
arrow = "^" if thrust['direction'] == 'up' else "v"
ax_main.text(
thrust['start_idx'] + (thrust['end_idx'] - thrust['start_idx']) / 2,
high_in_thrust * 1.05,
f"{arrow} {thrust['return']:.1f}% за {thrust['days']} д.",
fontsize=10, ha='center', color=color, weight='bold'
)
plt.tight_layout()
plt.show()
# Вывод статистики
if not thrusts_df.empty:
stats = thrusts_df.groupby('direction').agg(
count=('days', 'count'),
avg_days=('days', 'mean'),
avg_return=('return', 'mean'),
min_return=('return', 'min'),
max_return=('return', 'max')
).round(2)
stats.index.name = ''
stats.columns = ['Кол-во', 'Ср. длительность', 'Ср. доходность', 'Мин.', 'Макс.']
print("\n Статистика импульсных движений:")
print(stats.to_string())
Рис. 22: Анализ импульсных движений цен акций Ford
Статистика импульсных движений:
Кол-во Ср. длительность Ср. доходность Мин. Макс.
down 6 6.00 -9.82 -28.46 -4.28
up 7 6.71 7.07 4.52 11.88
Этот график выявляет и анализирует импульсные движения цены — быстрые, однонаправленные движения, которые часто указывают на начало нового тренда. Зеленые области показывают импульсы вверх, а красные — импульсы вниз. Для каждого импульса указывается процентное изменение цены и длительность в днях. В таблице представлена статистика импульсных движений и их результативность.
Визуализация ключевых аспектов рыночной динамики
Код на Python, представленный ниже, визуализирует комплексный анализ поведения цен и объемов акций на финансовом рынке с использованием нескольких графиков, отображающих разные аспекты торговли.
Основное внимание уделено 3-м ключевым элементам: свечному графику, объемному профилю и эволюции плотности цен во времени. Все графики строятся в единой визуальной композиции с помощью библиотек matplotlib и mplfinance, что позволяет получить целостное представление о текущей рыночной ситуации.
Рис. 23: Визуализация объемного профиля цен акций Ford
Этот код строит три графика для визуального анализа цен и объемов выбранной акции: свечной график для отслеживания динамики цены, объемный профиль с разделением на покупки и продажи, и тепловую карту плотности цен по кварталам, чтобы показать, как менялись ключевые ценовые уровни за год.
Заключение
В данной статье были рассмотрены десятки способов визуализации котировок с помощью библиотеки mplfinance. Ключевые выводы и практическая ценность представленных методов заключаются в следующем:
- Разнообразие типов графиков – от классических свечных и OHLC-графиков до специализированных, таких как Heikin-Ashi, Renko и Point & Figure, – можно адаптировать визуализацию под конкретные задачи трейдера или аналитика. Например, Renko и PnF помогают отфильтровать рыночный шум, а Heikin-Ashi упрощает идентификацию трендов;
- Расширенный анализ объемов (Volume Profile, TPO, аномалии объемов) дает понимание ключевых ценовых уровней, где сосредоточена максимальная ликвидность, что критически важно для определения поддержки и сопротивления;
- Статистические и вероятностные методы (распределение доходности, Q-Q plot, кластерный анализ) позволяют оценить риски, выявить аномалии и определить статистически значимые паттерны в поведении цены;
- Специализированные техники (визуализация гэпов, импульсных движений, возврата к среднему) помогают находить точки входа и выхода, прогнозировать развороты трендов и оценивать силу рыночных движений;
- Комплексные визуализации (корреляция цены и объема, сезонность, относительная производительность) обеспечивают более глубокое понимание рыночной динамики, позволяя сравнивать активы с эталонными индексами и выявлять скрытые закономерности.
Практическая ценность этих методов заключается в их применимости для:
- Технического анализа – идентификации трендов, уровней поддержки/сопротивления, точек разворота.
- Количественного анализа – построения торговых стратегий на основе статистических закономерностей.
- Риск-менеджмента – оценки волатильности, вероятности экстремальных движений и распределения объемов.
Таким образом, примение Pytho-библиотеки mplfinance значительно упрощает процесс визуализации биржевых данных, делая их доступными как для начинающих трейдеров, так и для профессиональных аналитиков. Применение рассмотренных методов позволяет не только улучшить восприятие рыночных данных, но и принимать более обоснованные торговые решения на основе комплексного анализа ценовых движений.