Matplotlib — одна из самых мощных и гибких библиотек Python для визуализации, без импорта которой не обходится, пожалуй, ни одно исследование. В этой статье я поделюсь 16 интересными примерами визуализации биржевых данных с использованием исключительно библиотеки Matplotlib. Мы рассмотрим как базовые графики, так и более продвинутые визуализации, которые применяются в квантовых исследованиях и алгоритмической торговле.
Базовый чарт с историей котировок
Прежде чем приступить к самим методам визуализации, важно правильно подготовить данные. В финансовом анализе мы обычно работаем с временными рядами цен акций, которые включают открытие (Open), максимум (High), минимум (Low), закрытие (Close) и объем торгов (Volume). Поскольку библиотека Matplotlib не заточена на специфические способы отображения биржевых цен (свечи, бары, стаканы и проч.) без установки дополнительных модулей, то для примеров ниже я буду использовать только цены закрытия и иногда объем. Целью данной статьи является исследование возможностей конкретной библиотеки.
Любая визуализация начинается с загрузки данных. В нашем случае это биржевые котировки цен на определенный актив. Их можно загрузить как из файла, так и по API. Ниже мы будем использовать библиотеку alpha_vantage для получения биржевых котировок Intel за последние 3 года:
!pip install alpha_vantage
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
from alpha_vantage.timeseries import TimeSeries
# Настройка графиков
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = (14, 8)
plt.rcParams['axes.grid'] = True
# Настройки
API_KEY = '_____________' # Замените на свой ключ https://www.alphavantage.co/support/#api-key
TICKER = 'INTC' # Акции Intel
# Даты
start_date = datetime(2022, 5, 1)
end_date = datetime.now()
# Загрузка данных через Alpha Vantage
ts = TimeSeries(key=API_KEY, output_format='pandas')
data, meta_data = ts.get_daily(symbol=TICKER, outputsize='full') # 'full' = ~20 лет данных
# Переименовываем и фильтруем по дате
data.index = pd.to_datetime(data.index)
data = data.sort_index()
data = data[(data.index >= start_date) & (data.index <= end_date)]
# Переименование колонок для удобства
data.columns = ['Open', 'High', 'Low', 'Close', 'Volume']
# Расчет индикаторов
data['Returns'] = data['Close'].pct_change() # Дневная доходность
data['Log_Returns'] = np.log(data['Close'] / data['Close'].shift(1)) # Логарифмическая доходность
data['MA20'] = data['Close'].rolling(window=20).mean() # 20-дневная скользящая средняя
data['MA50'] = data['Close'].rolling(window=50).mean() # 50-дневная скользящая средняя
data['Volatility'] = data['Returns'].rolling(window=20).std() * np.sqrt(252) # Годовая волатильность
# График цены закрытия и скользящих средних
plt.figure(figsize=(14, 6))
plt.plot(data['Close'], label='Цена закрытия', color='blue')
plt.plot(data['MA20'], label='Скользящая средняя (20)', color='orange')
plt.plot(data['MA50'], label='Скользящая средняя (50)', color='green')
plt.title(f'{TICKER} - Цена и скользящие средние')
plt.xlabel('Дата')
plt.ylabel('Цена ($)')
plt.legend()
plt.grid(True)
plt.show()
# Вывод последних строк
print("Последние данные:")
print(data.tail())
Рис. 1: График цен акций Intel за последние 3 года с наложенными 20-дневными и 50-дневными скользящими средними
Последние данные:
Open High Low Close Volume Returns Log_Returns \
date
2025-05-02 20.26 20.78 20.210 20.62 63298512.0 0.032032 0.031530
2025-05-05 20.39 20.58 20.235 20.27 44236981.0 -0.016974 -0.017120
2025-05-06 19.92 20.12 19.770 19.94 51330772.0 -0.016280 -0.016414
2025-05-07 19.97 20.37 19.820 20.31 61134293.0 0.018556 0.018386
2025-05-08 21.01 21.24 20.640 21.00 71651698.0 0.033973 0.033409
MA20 MA50 Volatility
date
2025-05-02 19.9525 21.7444 0.996941
2025-05-05 19.9735 21.6524 0.906674
2025-05-06 19.9920 21.5658 0.907236
2025-05-07 20.1010 21.5122 0.862736
2025-05-08 20.0745 21.4618 0.553273
Библиотека matplotlib очень хорошо заточена под табличные данные и, в частности, датафреймы Pandas. И в большинстве случаев дополнительная предобработка данных не требуется. Кроме ситуаций, когда вы хотите привнести в данные уже какие-то нестандартные вещи, например визуализации прогресса кумулятивной доходности и максимальной просадки.
Визуализация объемного профиля с помощью Matplotlib
# Расчет кумулятивной доходности
data['Cumulative_Returns'] = (1 + data['Returns']).cumprod()
# Расчет максимальной просадки
def calculate_drawdowns(return_series):
wealth_index = (1 + return_series).cumprod()
previous_peaks = wealth_index.cummax()
drawdowns = (wealth_index - previous_peaks) / previous_peaks
return drawdowns
data['Drawdowns'] = calculate_drawdowns(data['Returns'])
# Подготовка данных для объемного профиля
price_levels = np.linspace(data['Low'].min(), data['High'].max(), 50)
# Убедимся, что price_levels одномерный и содержит float
price_levels = np.squeeze(price_levels).astype(float)
volume_profile = pd.DataFrame(index=price_levels)
volume_profile['Volume'] = 0
for level in price_levels:
# Явно преобразуем level в float
level = float(level)
mask = (data['Low'] <= level) & (data['High'] >= level)
total_volume = data.loc[mask, 'Volume'].sum()
volume_profile.loc[level, 'Volume'] = total_volume
# График объемного профиля
plt.figure(figsize=(6, 6))
plt.barh(volume_profile.index, volume_profile['Volume'], height=0.5, color='dimgray')
plt.title('Объемный профиль цен акций Intel')
plt.xlabel('Объем')
plt.ylabel('Цена')
plt.grid(True)
plt.show()
Рис. 2: Объемный профиль цен акций Intel
На графике выше хорошо видны области долгосрочного присутствия цен и там, где акции торговались недолго. Такой профиль позволяет быстро понять размах цен на актив и потенциальные возможности длинных или коротких сделок.
График с несколькими скользящими средними и выделением областей их пересечения
Скользящие средние часто используются для выявления трендов в техническом анализе. Считается, что они могут давать сигналы к сделкам на пересечении линий разной длины. Метод конечно так себе (поскольку все типы МА безнадежно запаздывают), но до сих пор крайне популярен среди розничных трейдеров. Поэтому такие визуализации тоже надо строить уметь.
Удобство подхода ниже заключается в том, что мы не просто накладываем на график SMA, но еще рисуем линии, где они дают сигналы на продажу или покупку. Таким образом, мы можем быстро визуально оценить прибыльность / убыточность стратегии.
def plot_price_with_mas(data, ticker, short_window=20, long_window=50):
plt.figure(figsize=(14, 7))
# Основной график цены закрытия
plt.plot(data.index, data['Close'], label='Цена закрытия', linewidth=1.5, alpha=0.8, color='black')
# Добавление скользящих средних
plt.plot(data.index, data[f'MA{short_window}'], label=f'{short_window}-дневная SMA',
linewidth=1.5, alpha=0.8, color='green')
plt.plot(data.index, data[f'MA{long_window}'], label=f'{long_window}-дневная SMA',
linewidth=1.5, alpha=0.8, color='red')
# Выделение областей, где короткая SMA выше длинной (бычий тренд)
bullish = data[f'MA{short_window}'] > data[f'MA{long_window}']
# Найти начало и конец бычьих трендов
trend_changes = bullish.diff().fillna(0) != 0
trend_start_dates = data.index[trend_changes & bullish]
trend_end_dates = data.index[trend_changes & ~bullish]
# Добавление вертикальных линий для обозначения пересечений
for date in trend_start_dates:
plt.axvline(x=date, color='green', linestyle='--', alpha=0.5)
for date in trend_end_dates:
plt.axvline(x=date, color='red', linestyle='--', alpha=0.5)
plt.title(f'{ticker} - Цена закрытия с {short_window}- и {long_window}-дневными SMA', fontsize=16)
plt.xlabel('Дата', fontsize=12)
plt.ylabel('Цена ($)', fontsize=12)
plt.grid(True, alpha=0.3)
plt.legend()
# Форматирование даты на оси X
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=2))
plt.gcf().autofmt_xdate()
plt.tight_layout()
plt.show()
plot_price_with_mas(data, ticker)
Рис. 3: График цен закрытия INTC с наложенными SMA-20 и SMA-50 и линиями, где произошло их пересечение
Зеленые линии указывают на начало потенциального бычьего тренда (когда короткая SMA пересекает длинную снизу вверх), а красные — на возможное начало медвежьего тренда. Чем удобно построение такого графика? Экономией времени. Здесь очевидно, что стратегия убыточна, и можно не тратить время на дальнейшние расчеты.
Сравнение объемов торгов с динамикой цен акций
Объем торгов дает важную информацию о силе ценового движения. Комбинация графика цены и объема позволяет лучше понять динамику рынка.
def plot_price_with_volume(data, ticker):
# Создание графика с двумя подграфиками: для цены и для объема
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 1]}, sharex=True)
# Верхний график: цена закрытия
ax1.plot(data.index, data['Close'], label='Цена закрытия', color='black', linewidth=2)
ax1.set_title(f'{ticker} - Цена закрытия и объем торгов', fontsize=16)
ax1.set_ylabel('Цена ($)', fontsize=12)
ax1.grid(True, alpha=0.3)
ax1.legend()
# Нижний график: объем
# Цветовое кодирование объема: зеленый при росте цены, красный при падении
colors = ['green' if data['Close'].iloc[i] > data['Close'].iloc[i-1] else 'red'
for i in range(1, len(data))]
colors.insert(0, 'gray') # Для первого дня используем нейтральный цвет
ax2.bar(data.index, data['Volume'], color=colors, alpha=0.8, width=2)
ax2.set_ylabel('Объем', fontsize=12)
ax2.set_xlabel('Дата', fontsize=12)
ax2.grid(True, alpha=0.3)
# Форматирование оси X с датами
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
fig.autofmt_xdate()
# Выделение дней с аномально высоким объемом (например, > 2 стандартных отклонений)
volume_mean = data['Volume'].mean()
volume_std = data['Volume'].std()
high_volume_days = data[data['Volume'] > volume_mean + 2*volume_std]
for date in high_volume_days.index:
ax1.axvline(x=date, color='purple', linestyle='--', alpha=0.3)
ax2.axvline(x=date, color='purple', linestyle='--', alpha=0.3)
plt.tight_layout()
plt.show()
plot_price_with_volume(data, ticker)
Рис. 4: Совмещенный график котировок и объемов торгов с подсветкой линиями дней с аномальным объемом
В этом примере я добавил выделение дней с аномально высоким объемом торгов (превышающим среднее значение на 2 стандартных отклонения), что может указывать на значимые события или потенциальные точки разворота тренда.
Графики доходности и волатильности
Анализ доходности и волатильности может дать представление о рискованности инвестиций и потенциальной прибыли.
def plot_returns_and_volatility(data, ticker):
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(14, 12), sharex=True)
# График дневной доходности
ax1.plot(data.index, data['Returns'] * 100, color='blue', alpha=0.7)
ax1.axhline(y=0, color='black', linestyle='-', alpha=0.3)
ax1.fill_between(data.index, data['Returns'] * 100, 0,
where=(data['Returns'] >= 0), color='green', alpha=0.3)
ax1.fill_between(data.index, data['Returns'] * 100, 0,
where=(data['Returns'] < 0), color='red', alpha=0.3) ax1.set_title(f'{ticker} - Дневная доходность', fontsize=16) ax1.set_ylabel('Доходность (%)', fontsize=12) ax1.grid(True, alpha=0.3) # График кумулятивной доходности ax2.plot(data.index, (data['Cumulative_Returns'] - 1) * 100, color='green', linewidth=2) ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3) ax2.set_title('Кумулятивная доходность', fontsize=16) ax2.set_ylabel('Доходность (%)', fontsize=12) ax2.grid(True, alpha=0.3) # График волатильности (20-дневное скользящее стандартное отклонение) ax3.plot(data.index, data['Volatility'] * 100, color='red', linewidth=2) ax3.set_title('20-дневная волатильность (годовая)', fontsize=16) ax3.set_ylabel('Волатильность (%)', fontsize=12) ax3.set_xlabel('Дата', fontsize=12) ax3.grid(True, alpha=0.3) # Форматирование оси X с датами ax3.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) ax3.xaxis.set_major_locator(mdates.MonthLocator(interval=3)) fig.autofmt_xdate() # Добавление аннотаций для периодов высокой волатильности high_vol_threshold = data['Volatility'].mean() + data['Volatility'].std() high_vol_periods = data[data['Volatility'] > high_vol_threshold]
for date in high_vol_periods.index[::20]: # Отображаем не все точки, а каждую 20-ю для избежания перегруженности
ax3.annotate('!',
xy=(date, high_vol_periods.loc[date, 'Volatility'] * 100),
xytext=(0, 15), textcoords='offset points',
arrowprops=dict(arrowstyle='->', color='black'),
fontsize=12, color='black')
plt.tight_layout()
plt.show()
plot_returns_and_volatility(data, ticker)
Рис. 5: Графики дневной, кумулятивной доходности и 20-дневной годовой волатильности акций Intel
Этот код создает три графика: дневная доходность, кумулятивная доходность и волатильность. Особенно интересно наблюдать, как периоды высокой волатильности часто соответствуют периодам значительных изменений в доходности.
График максимальной просадки
Максимальная просадка — важный показатель риска, который показывает максимальное процентное снижение от пика до впадины.
def plot_drawdown(data, ticker):
plt.figure(figsize=(14, 7))
# График просадки
plt.fill_between(data.index, data['Drawdowns'] * 100, 0, color='red', alpha=0.3)
plt.plot(data.index, data['Drawdowns'] * 100, color='red', linewidth=1)
# Вычисление и отображение максимальной просадки
max_drawdown = data['Drawdowns'].min()
max_drawdown_date = data['Drawdowns'].idxmin()
plt.scatter(max_drawdown_date, max_drawdown * 100, color='darkred', s=100, zorder=5)
plt.annotate(f'Макс. просадка: {max_drawdown:.2%}',
xy=(max_drawdown_date, max_drawdown * 100),
xytext=(max_drawdown_date + pd.Timedelta(days=30), max_drawdown * 100 * 0.5),
arrowprops=dict(facecolor='black', shrink=0.05),
fontsize=12)
plt.title(f'{ticker} - График просадки', fontsize=16)
plt.xlabel('Дата', fontsize=12)
plt.ylabel('Просадка (%)', fontsize=12)
plt.grid(True, alpha=0.3)
# Форматирование оси X с датами
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=3))
plt.gcf().autofmt_xdate()
# Добавим нулевую линию
plt.axhline(y=0, color='black', linestyle='-', alpha=0.3)
plt.tight_layout()
plt.show()
plot_drawdown(data, ticker)
Рис. 6: График динамики просадки инвестиций в акции Intel
График просадки наглядно демонстрирует периоды наибольших потерь и может помочь в оценке рисков инвестиций или эффективности торговой стратегии.
Боксплот доходности по месяцам
Визуализация сезонных паттернов доходности может помочь выявить месяцы с лучшей или худшей исторической доходностью.
def plot_monthly_returns_boxplot(data, ticker):
# Добавление столбца с номером месяца
data['Month'] = data.index.month
# Группировка доходностей по месяцам
monthly_returns = []
month_names = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн',
'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']
for i in range(1, 13):
monthly_returns.append(data[data['Month'] == i]['Returns'].dropna() * 100)
plt.figure(figsize=(12, 7))
# Создание боксплота
box = plt.boxplot(monthly_returns, patch_artist=True,
labels=month_names,
medianprops={'color': 'black', 'linewidth': 1.5})
# Настройка цветов боксов: зеленый для положительной медианы, красный для отрицательной
for i, patch in enumerate(box['boxes']):
if monthly_returns[i].median() >= 0:
patch.set_facecolor('lightgreen')
else:
patch.set_facecolor('lightcoral')
# Добавление точек (фактических значений доходности)
for i, month_data in enumerate(monthly_returns):
if not month_data.empty:
plt.scatter([i+1] * len(month_data), month_data,
color='blue', alpha=0.3, s=20)
plt.title(f'{ticker} - Распределение дневной доходности по месяцам', fontsize=16)
plt.ylabel('Доходность (%)', fontsize=12)
plt.grid(True, alpha=0.3, axis='y')
# Добавление нулевой линии
plt.axhline(y=0, color='black', linestyle='-', alpha=0.3)
# Добавление текста с медианным значением для каждого месяца
for i, month_data in enumerate(monthly_returns):
if not month_data.empty:
median = month_data.median()
plt.text(i+1, median + (0.5 if median >= 0 else -0.5),
f'{median:.2f}%', ha='center', fontsize=9)
plt.tight_layout()
plt.show()
plot_monthly_returns_boxplot(data, ticker)
Рис. 7: Боксплоты распределения дневной доходности INTC по месяцам (период 3 года)
Этот график наглядно показывает, как распределяется доходность по месяцам, что может быть полезно для выявления сезонных аномалий или паттернов.
Тепловая карта корреляций для нескольких акций
Для анализа взаимосвязей между различными акциями полезно использовать тепловую карту корреляций.
import time
def plot_correlation_heatmap(tickers):
# Период данных
start_date = pd.to_datetime('2022-05-01')
end_date = pd.to_datetime('today')
all_data = pd.DataFrame()
for ticker in tickers:
try:
print(f"Загрузка данных для {ticker}...")
data, meta_data = ts.get_daily(symbol=ticker, outputsize='full')
data.index = pd.to_datetime(data.index) # преобразуем индекс в datetime
data = data[data.index >= start_date] # фильтр по дате
all_data[ticker] = data['4. close'] # берем цену закрытия
print(f"Успешно загружены данные для {ticker}")
except Exception as e:
print(f"Ошибка при загрузке данных для {ticker}: {e}")
# Делаем паузу, чтобы не превысить лимит API (5 запросов/мин)
time.sleep(12) # ~12 секунд между запросами (~5 в минуту)
if all_data.empty:
print("Не удалось загрузить данные ни по одной акции.")
return
# Расчет доходностей
returns_data = all_data.pct_change().dropna()
# Корреляционная матрица
correlation_matrix = returns_data.corr()
# Тепловая карта
plt.figure(figsize=(12, 10))
cmap = plt.cm.RdBu_r
im = plt.imshow(correlation_matrix, cmap=cmap, vmin=-1, vmax=1)
plt.colorbar(im, label='Корреляция')
plt.grid(False)
tick_marks = np.arange(len(correlation_matrix.columns))
plt.xticks(tick_marks, correlation_matrix.columns, rotation=45, ha='right')
plt.yticks(tick_marks, correlation_matrix.columns)
# Подпись значений
for i in range(len(correlation_matrix.columns)):
for j in range(len(correlation_matrix.columns)):
text_color = 'white' if abs(correlation_matrix.iloc[i, j]) > 0.6 else 'black'
plt.text(j, i, f'{correlation_matrix.iloc[i, j]:.2f}',
ha='center', va='center', color=text_color, fontsize=11)
plt.title('Корреляция доходностей различных акций', fontsize=16)
plt.tight_layout()
plt.show()
# Пример использования
plot_correlation_heatmap(['INTC', 'NVDA', 'AMD', 'QCOM', 'AAPL'])
Рис. 8: Корреляиция доходностей акций Intel, Nvidia, AMD, Qualcomm, Apple
Тепловая карта корреляций позволяет быстро выявить группы акций, которые движутся синхронно или, наоборот, имеют отрицательную корреляцию, что полезно для построения диверсифицированного портфеля.
Пузырьковый график объема и волатильности
Пузырьковый график (bubble-chart) позволяет одновременно отображать три измерения данных: в нашем случае это цена, волатильность и объем торгов.
def plot_bubble_volume_volatility(data, ticker):
# Рассчитаем недельную волатильность и средний объем
weekly_data = data.resample('W').agg({
'Close': 'last',
'Volume': 'mean',
'Returns': lambda x: x.std() * np.sqrt(5) # Недельная волатильность
}).dropna()
# Нормализация объема для масштабирования размера пузырьков
weekly_data['NormalizedVolume'] = weekly_data['Volume'] / weekly_data['Volume'].max() * 300
# Создание пузырькового графика
plt.figure(figsize=(14, 8))
# Определение цвета по тренду (зеленый для роста, красный для падения)
weekly_returns = weekly_data['Close'].pct_change()
colors = ['green' if ret > 0 else 'red' for ret in weekly_returns]
# Создание пузырькового графика
scatter = plt.scatter(weekly_data.index,
weekly_data['Close'],
s=weekly_data['NormalizedVolume'],
c=colors,
alpha=0.6,
edgecolors='black')
# Добавление линии цены
plt.plot(weekly_data.index, weekly_data['Close'], 'k-', alpha=0.3)
# Добавление текста для некоторых интересных точек
# Например, для недель с самой высокой волатильностью
high_vol_weeks = weekly_data.nlargest(3, 'Returns')
for date, row in high_vol_weeks.iterrows():
plt.annotate(f'Волат.:{row["Returns"]:.2%}',
xy=(date, row['Close']),
xytext=(10, 10), textcoords='offset points',
arrowprops=dict(arrowstyle='->', color='black'),
fontsize=10)
plt.title(f'{ticker} - Пузырьковый график: Цена, Волатильность и Объем', fontsize=16)
plt.xlabel('Дата', fontsize=12)
plt.ylabel('Цена закрытия ($)', fontsize=12)
plt.grid(True, alpha=0.3)
# Добавление легенды для размера пузырьков
legend_sizes = [weekly_data['Volume'].max(), weekly_data['Volume'].max()/2, weekly_data['Volume'].max()/4]
legend_labels = [f'Объем: {size:.1e}' for size in legend_sizes]
# Создание невидимых точек для легенды
for size, label in zip([300, 150, 75], legend_labels):
plt.scatter([], [], s=size, c='gray', alpha=0.6, edgecolors='black', label=label)
plt.legend(scatterpoints=1, frameon=False, labelspacing=1)
plt.tight_layout()
plt.show()
plot_bubble_volume_volatility(data, TICKER)
Рис. 9: Пузырьковый график котировок Intel: цена, волатильность, объем
Этот график предоставляет интуитивное представление о взаимосвязи между ценой, объемом и волатильностью. Большие пузырьки указывают на повышенный объем торгов, а цвет показывает направление движения цены.
График дневных диапазонов (Daily Range Chart)
График дневных диапазонов позволяет визуализировать диапазон колебаний цены внутри дня, что может быть полезно для анализа волатильности и стратегий внутридневной торговли.
def plot_daily_ranges(data, ticker, days=60):
# Выбираем последние N дней
recent_data = data.tail(days)
# Рассчитываем дневной диапазон
recent_data['DailyRange'] = recent_data['High'] - recent_data['Low']
recent_data['RangePercent'] = recent_data['DailyRange'] / recent_data['Low'] * 100
# Создаем фигуру с двумя графиками
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 1]}, sharex=True)
# Верхний график: OHLC в виде вертикальных линий
for i, (date, row) in enumerate(recent_data.iterrows()):
# Определение цвета (зеленый для роста, красный для падения)
color = 'green' if row['Close'] >= row['Open'] else 'red'
# Рисуем вертикальную линию для диапазона High-Low
ax1.plot([date, date], [row['Low'], row['High']], color=color, linewidth=1.5)
# Добавляем маленькие горизонтальные черточки для Open и Close
ax1.plot([date - pd.Timedelta(hours=6), date + pd.Timedelta(hours=6)],
[row['Open'], row['Open']], color=color, linewidth=1.5)
ax1.plot([date - pd.Timedelta(hours=6), date + pd.Timedelta(hours=6)],
[row['Close'], row['Close']], color=color, linewidth=1.5)
# Добавление скользящей средней на график цены
ax1.plot(recent_data.index, recent_data['MA20'], 'b--', label='20-дневная SMA', linewidth=1.5, alpha=0.7)
ax1.set_title(f'{ticker} - Дневные диапазоны цен', fontsize=16)
ax1.set_ylabel('Цена ($)', fontsize=12)
ax1.grid(True, alpha=0.3)
ax1.legend()
# Нижний график: дневной диапазон в процентах
ax2.bar(recent_data.index, recent_data['RangePercent'], color='purple', alpha=0.7)
ax2.axhline(y=recent_data['RangePercent'].mean(), color='black', linestyle='--',
label=f'Средний диапазон: {recent_data["RangePercent"].mean():.2f}%')
ax2.set_title('Дневной диапазон (%)', fontsize=16)
ax2.set_xlabel('Дата', fontsize=12)
ax2.set_ylabel('Диапазон (%)', fontsize=12)
ax2.grid(True, alpha=0.3)
ax2.legend()
# Форматирование оси X
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
ax2.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1))
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
plot_daily_ranges(stock_data, ticker)
Рис. 10: График дневных диапазонов цен акций Intel
Этот график помогает визуализировать не только абсолютные значения цен, но и относительные дневные диапазоны, что может быть полезно для выявления дней с аномальной волатильностью.
График гэпов (Gap Chart)
Гэпы (разрывы) между ценами закрытия предыдущего дня и открытия следующего часто имеют важное значение для технического анализа. Существует даже целое направление gap-стратегий, базирующихся на том, что рынок всегда стремится закрыть такие гэпы, а значит цена через какое-то время вернется в такие области.
def plot_gaps(data, ticker, days=120):
# Выбираем последние N дней
recent_data = data.tail(days).copy()
# Рассчитываем гэпы (разрывы)
recent_data['PrevClose'] = recent_data['Close'].shift(1)
recent_data['Gap'] = (recent_data['Open'] - recent_data['PrevClose']) / recent_data['PrevClose'] * 100
recent_data = recent_data.dropna()
# Определяем значительные гэпы (например, более 2%)
significant_gap_threshold = 2.0
recent_data['SignificantGap'] = np.abs(recent_data['Gap']) > significant_gap_threshold
# Создаем график
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 1]}, sharex=True)
# Верхний график: цена закрытия
ax1.plot(recent_data.index, recent_data['Close'], label='Цена закрытия', color='blue', linewidth=1.5)
# Отмечаем дни со значительными гэпами
gap_up_days = recent_data[(recent_data['SignificantGap']) & (recent_data['Gap'] > 0)]
gap_down_days = recent_data[(recent_data['SignificantGap']) & (recent_data['Gap'] < 0)] # Добавляем маркеры для гэпов вверх и вниз if not gap_up_days.empty: ax1.scatter(gap_up_days.index, gap_up_days['Close'], color='green', s=80, marker='^', label='Гэп вверх >2%')
if not gap_down_days.empty:
ax1.scatter(gap_down_days.index, gap_down_days['Close'], color='red', s=80, marker='v',
label='Гэп вниз >2%')
ax1.set_title(f'{ticker} - График гэпов', fontsize=16)
ax1.set_ylabel('Цена ($)', fontsize=12)
ax1.grid(True, alpha=0.3)
ax1.legend()
# Нижний график: величина гэпов в процентах
bars = ax2.bar(recent_data.index, recent_data['Gap'],
color=['green' if x >= 0 else 'red' for x in recent_data['Gap']], alpha=0.7)
# Добавляем горизонтальные линии для порогов значительных гэпов
ax2.axhline(y=significant_gap_threshold, color='green', linestyle='--', alpha=0.7)
ax2.axhline(y=-significant_gap_threshold, color='red', linestyle='--', alpha=0.7)
ax2.axhline(y=0, color='black', linestyle='-', alpha=0.2)
ax2.set_title('Размер гэпов (%)', fontsize=16)
ax2.set_xlabel('Дата', fontsize=12)
ax2.set_ylabel('Гэп (%)', fontsize=12)
ax2.grid(True, alpha=0.3)
# Для наиболее значительных гэпов добавляем аннотации
largest_gaps = pd.concat([
recent_data.nlargest(3, 'Gap'),
recent_data.nsmallest(3, 'Gap')
])
for date, row in largest_gaps.iterrows():
ax2.annotate(f'{row["Gap"]:.2f}%',
xy=(date, row['Gap']),
xytext=(0, 10 if row['Gap'] > 0 else -15),
textcoords='offset points',
ha='center',
fontsize=9)
# Форматирование
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Дополнительная статистика по гэпам
print(f"Статистика гэпов для {ticker}:")
print(f"Среднее значение гэпа: {recent_data['Gap'].mean():.4f}%")
print(f"Медианное значение гэпа: {recent_data['Gap'].median():.4f}%")
print(f"Стандартное отклонение гэпов: {recent_data['Gap'].std():.4f}%")
print(f"Максимальный гэп вверх: {recent_data['Gap'].max():.4f}%")
print(f"Максимальный гэп вниз: {recent_data['Gap'].min():.4f}%")
print(f"Количество значительных гэпов вверх: {len(gap_up_days)}")
print(f"Количество значительных гэпов вниз: {len(gap_down_days)}")
plot_gaps(data, TICKER)
Рис. 11: График гэпов >2% дневных котировок Intel
Статистика гэпов для INTC:
Среднее значение гэпа: 0.0483%
Медианное значение гэпа: -0.1519%
Стандартное отклонение гэпов: 2.4671%
Максимальный гэп вверх: 13.6364%
Максимальный гэп вниз: -8.1899%
Количество значительных гэпов вверх: 14
Количество значительных гэпов вниз: 13
График гэпов помогает выявить и анализировать ценовые разрывы, которые могут быть вызваны важными новостями или событиями. Анализ гэпов может быть полезен не только для разработки торговых стратегий, но и еще для понимания факторов, которые их вызвали. Например:
- экстремально плохая или отличная финансовая отчетность;
- объявление о будущей M&A сделке;
- возможных крупных покупках и продажах инсайдерами.
График скорости изменения цены (ROC)
Скорость изменения цены (Rate of Change, ROC) — технический индикатор, измеряющий процентное изменение цены за определенный период.
def plot_price_roc(data, ticker, period=14):
# Создаём копию, чтобы не менять оригинальный DataFrame
data = data.copy()
# Проверка типа индекса
if not isinstance(data.index, pd.DatetimeIndex):
data.index = pd.to_datetime(data.index)
# Проверка достаточности данных
if len(data) < period: raise ValueError(f"Недостаточно данных для расчёта ROC за период {period} дней.") # Рассчитываем ROC data['ROC'] = data['Close'].pct_change(period) * 100 data.dropna(subset=['ROC'], inplace=True) # График fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), gridspec_kw={'height_ratios': [3, 1]}, sharex=True) # Цена закрытия ax1.plot(data.index, data['Close'], color='black', linewidth=1.5) ax1.set_title(f'{ticker} - Цена закрытия и ROC ({period}-дневный)', fontsize=16) ax1.set_ylabel('Цена ($)', fontsize=12) ax1.grid(True, alpha=0.3) # ROC ax2.plot(data.index, data['ROC'], color='purple', linewidth=1.5, label=f'ROC-{period}') ax2.axhline(y=0, color='black', linestyle='-', alpha=0.3) ax2.axhline(y=10, color='red', linestyle='--', alpha=0.5, label='Перекупленность') ax2.axhline(y=-10, color='green', linestyle='--', alpha=0.5, label='Перепроданность') # Заполнение областей ax2.fill_between(data.index, 10, data['ROC'], where=(data['ROC'] > 10), color='red', alpha=0.2)
ax2.fill_between(data.index, -10, data['ROC'], where=(data['ROC'] < -10), color='green', alpha=0.2)
ax2.set_title(f'{period}-дневный ROC (Rate of Change)', fontsize=16)
ax2.set_ylabel('ROC (%)', fontsize=12)
ax2.set_xlabel('Дата', fontsize=12)
ax2.grid(True, alpha=0.3)
ax2.legend()
# Форматирование оси X
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Возвращаем изменённый DataFrame
return data
period = 12
data_with_roc = plot_price_roc(data, TICKER, period)
# Вывод статистики после вызова
print(f"Статистика ROC-{period} для {TICKER}:")
print(f"Текущее значение: {data_with_roc['ROC'].iloc[-1]:.2f}%")
print(f"Среднее значение: {data_with_roc['ROC'].mean():.2f}%")
print(f"Максимальное значение: {data_with_roc['ROC'].max():.2f}%")
print(f"Минимальное значение: {data_with_roc['ROC'].min():.2f}%")
Рис. 12: График цен закрытия и 12-дневного ROC (Rate of Change)
Статистика ROC-12 для INTC:
Текущее значение: 7.64%
Среднее значение: -0.65%
Максимальное значение: 36.88%
Минимальное значение: -43.09%
Считается, что этот индикатор может помочь определить скорость и направление изменения цены акции, что делает его полезным для выявления перекупленных или перепроданных состояний рынка. Еще он прост в интерпретации, что делает его популярным среди розничных инвесторов. Увы, но как и все осцилляторы, он эффективен лишь при боковых движениях рынка, так как при сильном тренде его сигналы будут приводить к сплошным убыткам.
Круговая диаграмма долей акций в портфеле
Круговая диаграмма (Pie Chart) может эффективно отображать распределение ценных бумаг в портфеле:
def plot_portfolio_pie_chart(tickers, weights, title="Распределение портфеля"):
# Проверка соответствия длины списков
if len(tickers) != len(weights):
raise ValueError("Списки тикеров и весов должны иметь одинаковую длину")
# Проверка суммы весов
total_weight = sum(weights)
if abs(total_weight - 1.0) > 0.01 and abs(total_weight - 100.0) > 0.01:
print(f"Предупреждение: Сумма весов ({total_weight}) отличается от 1.0 или 100.0")
# Нормализация весов, если они указаны в процентах
if abs(total_weight - 100.0) < 0.01: weights = [w / 100.0 for w in weights] # Настройка цветов и эксплозии (для выделения наиболее важных секторов) colors = plt.cm.tab20.colors[:len(tickers)] explode = [0.05 if w > max(weights) * 0.8 else 0 for w in weights]
# Создание круговой диаграммы
plt.figure(figsize=(12, 8))
patches, texts, autotexts = plt.pie(
weights,
labels=tickers,
explode=explode,
colors=colors,
autopct='%1.1f%%',
shadow=True,
startangle=90,
wedgeprops={'linewidth': 1, 'edgecolor': 'white'}
)
# Настройка текста
for autotext in autotexts:
autotext.set_fontsize(10)
autotext.set_fontweight('bold')
autotext.set_color('white')
for text in texts:
text.set_fontsize(12)
# Добавление заголовка и настройка параметров диаграммы
plt.title(title, fontsize=16, pad=20)
plt.axis('equal') # Чтобы круг был идеально круглым
# Добавление дополнительной информации
current_date = datetime.now().strftime("%Y-%m-%d")
plt.annotate(f"Дата: {current_date}", xy=(0.02, 0.02), xycoords='figure fraction')
plt.tight_layout()
plt.show()
# Пример использования
sample_portfolio = {
'INTC': 0.20,
'AAPL': 0.30,
'MSFT': 0.15,
'NVDA': 0.10,
'AMD': 0.10,
'GOOG': 0.15
}
plot_portfolio_pie_chart(
list(sample_portfolio.keys()),
list(sample_portfolio.values()),
"Распределение активов в технологическом портфеле"
)
Рис. 13: Диаграмма распределения активов в портфеле
Эта визуализация особенно полезна для инвесторов, которые хотят быстро оценить диверсификацию своего портфеля и выявить активы с наибольшим весом.
Визуализация ценовых каналов и коридоров волатильности
Ценовые каналы и коридоры волатильности — важный инструмент для понимания динамики цен и выявления аномальных движений на рынке.
def plot_price_channels(data, ticker, window=20):
# Создаем копию данных
df = data.copy()
# Рассчитываем скользящее среднее и стандартное отклонение
df['MA'] = df['Close'].rolling(window=window).mean()
df['STD'] = df['Close'].rolling(window=window).std()
# Верхняя и нижняя границы канала волатильности (2 стандартных отклонения)
df['Upper_Band'] = df['MA'] + 2 * df['STD']
df['Lower_Band'] = df['MA'] - 2 * df['STD']
# Визуализация
plt.figure(figsize=(14, 7))
plt.plot(df.index, df['Close'], label='Цена закрытия', color='blue', alpha=0.7)
plt.plot(df.index, df['MA'], label=f'{window}-дневная SMA', color='black', alpha=0.8)
plt.plot(df.index, df['Upper_Band'], label='Верхний канал (+2σ)', color='red', linestyle='--', alpha=0.7)
plt.plot(df.index, df['Lower_Band'], label='Нижний канал (-2σ)', color='green', linestyle='--', alpha=0.7)
# Заливка области между каналами
plt.fill_between(df.index, df['Upper_Band'], df['Lower_Band'], color='gray', alpha=0.1)
# Выделение точек выхода за границы каналов
breaks_upper = df[df['Close'] > df['Upper_Band']]
breaks_lower = df[df['Close'] < df['Lower_Band']]
plt.scatter(breaks_upper.index, breaks_upper['Close'], marker='^', color='darkred', s=50,
label='Пробой верхней границы')
plt.scatter(breaks_lower.index, breaks_lower['Close'], marker='v', color='darkgreen', s=50,
label='Пробой нижней границы')
plt.title(f'{ticker} - Ценовые каналы волатильности ({window}-дневный период)', fontsize=16)
plt.xlabel('Дата', fontsize=12)
plt.ylabel('Цена ($)', fontsize=12)
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
# Статистика о выходах за границы каналов
upper_breaks_pct = len(breaks_upper) / len(df.dropna()) * 100
lower_breaks_pct = len(breaks_lower) / len(df.dropna()) * 100
print(f"Статистика выходов за границы каналов для {ticker}:")
print(f"Пробои верхней границы: {len(breaks_upper)} раз ({upper_breaks_pct:.2f}% времени)")
print(f"Пробои нижней границы: {len(breaks_lower)} раз ({lower_breaks_pct:.2f}% времени)")
print(f"Теоретически ожидаемые пробои для нормального распределения: 4.55% времени")
plot_price_channels(data, TICKER, window=20)
Рис. 14: 20-дневные ценовые каналы волатильности акций Intel с маркерами пробоев
Статистика выходов за границы каналов для INTC:
Пробои верхней границы: 42 раз (5.92% времени)
Пробои нижней границы: 43 раз (6.06% времени)
Теоретически ожидаемые пробои для нормального распределения: 4.55% времени
Эта визуализация показывает не только тренд и волатильность, но и моменты, когда цена ведет себя аномально. Статистически, если доходности распределены нормально, то за границы 2-сигма каналов цена должна выходить примерно в 4.55% случаев. Если фактическое значение сильно отличается, это может говорить о ненормальном распределении доходностей и потенциально высоком риске.
Распределение доходностей с наложением нормального распределения
Для оценки характера распределения доходностей и выявления «толстых хвостов» полезно сравнить фактическое распределение с теоретическим нормальным:
from scipy import stats
def plot_returns_distribution(data, ticker):
returns = data['Returns'].dropna() * 100 # Переводим в проценты
# Статистические характеристики
mean_return = returns.mean()
std_return = returns.std()
skew = returns.skew()
kurt = returns.kurtosis() # Эксцесс
# Генерируем точки для нормального распределения
x = np.linspace(returns.min(), returns.max(), 100)
norm_pdf = stats.norm.pdf(x, mean_return, std_return)
# Создаем гистограмму
plt.figure(figsize=(12, 8))
# Гистограмма фактических доходностей
n, bins, patches = plt.hist(returns, bins=50, density=True, alpha=0.7, color='lightblue',
label='Фактическое распределение')
# Накладываем теоретическое нормальное распределение
plt.plot(x, norm_pdf, 'r-', linewidth=2, label='Нормальное распределение')
# Добавляем вертикальные линии для стандартных отклонений
plt.axvline(mean_return, color='black', linestyle='-', alpha=0.7, label='Среднее')
plt.axvline(mean_return + std_return, color='green', linestyle='--', alpha=0.7, label='+1σ')
plt.axvline(mean_return - std_return, color='green', linestyle='--', alpha=0.7)
plt.axvline(mean_return + 2*std_return, color='orange', linestyle='--', alpha=0.7, label='+2σ')
plt.axvline(mean_return - 2*std_return, color='orange', linestyle='--', alpha=0.7)
plt.axvline(mean_return + 3*std_return, color='red', linestyle='--', alpha=0.7, label='+3σ')
plt.axvline(mean_return - 3*std_return, color='red', linestyle='--', alpha=0.7)
# Выделяем хвосты распределения
left_tail = returns[returns < mean_return - 3*std_return] right_tail = returns[returns > mean_return + 3*std_return]
# Подсчитываем точки в хвостах
left_tail_pct = len(left_tail) / len(returns) * 100
right_tail_pct = len(right_tail) / len(returns) * 100
# Статистика
stats_text = (
f"Среднее: {mean_return:.4f}%\n"
f"Стд. отклонение: {std_return:.4f}%\n"
f"Асимметрия: {skew:.4f}\n"
f"Эксцесс: {kurt:.4f}\n"
f"Левый хвост (< -3σ): {left_tail_pct:.2f}%\n" f"Правый хвост (> +3σ): {right_tail_pct:.2f}%\n"
f"Теоретически для норм. распр.: 0.27%"
)
plt.annotate(stats_text, xy=(0.02, 0.96), xycoords='axes fraction',
bbox=dict(boxstyle="round,pad=0.5", fc="white", alpha=0.8),
va='top', fontsize=10)
plt.title(f'{ticker} - Распределение дневных доходностей', fontsize=16)
plt.xlabel('Доходность (%)', fontsize=12)
plt.ylabel('Плотность вероятности', fontsize=12)
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
plot_returns_distribution(data, TICKER)
Рис. 15: Диаграмма распределения дневных доходностей с наложением нормального распределения и границами сигм
Этот график позволяет оценить «толщину хвостов» распределения доходностей. Если фактическое распределение имеет более «толстые хвосты» по сравнению с нормальным, это указывает на повышенную вероятность экстремальных движений цены, что является важной информацией для управления рисками.
График сравнения исторической и подразумеваемой волатильности
Сравнение исторической волатильности анализируемой акции с подразумеваемой волатильностью фьючерсов на индекс SP500 (VIX) дает важную информацию о рыночных ожиданиях:
# Загрузка данных через Alpha Vantage
ts = TimeSeries(key=API_KEY, output_format='pandas')
vix_data, meta_data = ts.get_daily(symbol='VIXL.FRK', outputsize='full') # S&P 500 VIX Short-term Futures Index
# Переименовываем и фильтруем по дате
start_date = datetime(2024, 8, 1)
end_date = datetime(2025, 5, 9)
vix_data.index = pd.to_datetime(vix_data.index)
vix_data = vix_data.sort_index()
vix_data = vix_data[(vix_data.index >= start_date) & (vix_data.index <= end_date)] # Переименование колонок для удобства vix_data.columns = ['Open', 'High', 'Low', 'Close', 'Volume'] def plot_historical_vs_implied_volatility(data, ticker, vix_data): # Пересекаем данные по датам merged_data = pd.merge( data[['Close', 'Volatility']], vix_data, left_index=True, right_index=True, how='inner' ) # Расчет корреляции correlation = merged_data['Volatility'].corr(merged_data['VIX']) # Создаем график fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(14, 12), sharex=True, gridspec_kw={'height_ratios': [3, 1, 1]}) # Верхний график: цена акции ax1.plot(merged_data.index, merged_data['Close'], color='blue', label='Цена закрытия') ax1.set_title(f'{ticker} - Цена и волатильность', fontsize=16) ax1.set_ylabel('Цена ($)', fontsize=12) ax1.grid(True, alpha=0.3) ax1.legend(loc='upper left') # Добавляем вторую ось Y для VIX ax1_twin = ax1.twinx() ax1_twin.plot(merged_data.index, merged_data['VIX'], color='red', linestyle='--', label='VIX (правая ось)') ax1_twin.set_ylabel('VIX', fontsize=12, color='red') ax1_twin.tick_params(axis='y', colors='red') ax1_twin.legend(loc='upper right') # Средний график: историческая волатильность vs VIX ax2.plot(merged_data.index, merged_data['Volatility'] * 100, color='purple', label='Историческая волатильность (20д)') ax2.plot(merged_data.index, merged_data['VIX'], color='orange', label='Подразуемая волатильность (VIX)') ax2.set_title(f'Историческая vs Подразумеваемая волатильность (корреляция: {correlation:.2f})', fontsize=16) ax2.set_ylabel('Волатильность (%)', fontsize=12) ax2.grid(True, alpha=0.3) ax2.legend() # Нижний график: спред между волатильностями vol_spread = merged_data['VIX'] - (merged_data['Volatility'] * 100) ax3.fill_between(merged_data.index, vol_spread, 0, where=(vol_spread >= 0), color='red', alpha=0.3,
label='VIX > Ист. волатильность')
ax3.fill_between(merged_data.index, vol_spread, 0,
where=(vol_spread < 0), color='green', alpha=0.3,
label='VIX < Ист. волатильность')
ax3.plot(merged_data.index, vol_spread, color='black', alpha=0.7)
ax3.axhline(y=0, color='black', linestyle='-', alpha=0.3)
ax3.set_title('Спред: VIX - Историческая волатильность', fontsize=16)
ax3.set_xlabel('Дата', fontsize=12)
ax3.set_ylabel('Спред (%)', fontsize=12)
ax3.grid(True, alpha=0.3)
ax3.legend()
plt.tight_layout()
plt.show()
# Пример загрузки данных VIX
vix_data = vix_data[['Close']].rename(columns={'Close': 'VIX'})
plot_historical_vs_implied_volatility(data, TICKER, vix_data)
Рис. 16: Сравнение исторической волатильности Intel с индексом VIX
Эта визуализация позволяет сравнить фактическую историческую волатильность акции с рыночными ожиданиями, выраженными через VIX. Когда подразумеваемая волатильность превышает историческую, это может указывать на ожидание участниками рынка повышенной волатильности в будущем.
Заключение
В этой статье мы рассмотрели 16 различных способов визуализации биржевых данных с помощью библиотеки Matplotlib. От базовых графиков цен и скользящих средних до более сложных визуализаций, таких как объемные профили, тепловые карты корреляций и сравнение исторической волатильности с подразумеваемой.
Ключевые выводы:
- Matplotlib — мощный инструмент для анализа рынка. Несмотря на то что эта питоновская библиотека не заточена специально под финансовые данные, ее гибкость позволяет создавать множество типов графиков, полезных для трейдинга и инвестирования;
- Визуализация помогает быстро оценить рыночную ситуацию. Графики вроде ценовых каналов, гэпов или распределения доходностей дают интуитивно понятное представление о волатильности, трендах и аномалиях;
- Комбинирование разных типов графиков повышает информативность. Например, совмещение цены, объемов и волатильности на одном графике помогает лучше понять динамику рынка;
- Анализ корреляций и распределений важен для управления рисками. Тепловые карты и гистограммы доходностей показывают, насколько активы связаны между собой и как часто происходят экстремальные движения.
Вот почему Matplotlib остается одним из самых популярных инструментов для визуализации данных в Python, и его возможности далеко не исчерпываются примерами из этой статьи. Экспериментируйте, комбинируйте разные подходы и находите оптимальные способы представления данных для ваших задач.