16 способов визуализации биржевых данных с помощью Matplotlib

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())

График цен акций Intel за последние 3 года с наложенными 20-дневными и 50-дневными скользящими средними

Рис. 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()

Объемный профиль цен акций Intel

Рис. 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)

График цен закрытия INTC с наложенными SMA-20 и SMA-50 и линиями, где произошло их пересечение

Рис. 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)

Графики дневной, кумулятивной доходности и 20-дневной годовой волатильности акций Intel

Рис. 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)

График динамики просадки инвестиций в акции Intel

Рис. 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)

Боксплоты распределения дневной доходности INTC по месяцам (период 3 года)

Рис. 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'])

Корреляиция доходностей акций Intel, Nvidia, AMD, Qualcomm, Apple

Рис. 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)

Пузырьковый график котировок Intel: цена, волатильность, объем

Рис. 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)

График дневных диапазонов цен акций Intel

Рис. 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)

График гэпов >2% дневных котировок Intel

Рис. 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-дневного ROC (Rate of Change)

Рис. 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: Диаграмма распределения активов в портфеле

Эта визуализация особенно полезна для инвесторов, которые хотят быстро оценить диверсификацию своего портфеля и выявить активы с наибольшим весом.

Читайте также:  Прогнозирование временных рядов с помощью ARIMA, SARIMA, ARFIMA

Визуализация ценовых каналов и коридоров волатильности

Ценовые каналы и коридоры волатильности — важный инструмент для понимания динамики цен и выявления аномальных движений на рынке.

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)

20-дневные ценовые каналы волатильности акций Intel с маркерами пробоев

Рис. 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)

Сравнение исторической волатильности Intel с индексом VIX

Рис. 16: Сравнение исторической волатильности Intel с индексом VIX

Эта визуализация позволяет сравнить фактическую историческую волатильность акции с рыночными ожиданиями, выраженными через VIX. Когда подразумеваемая волатильность превышает историческую, это может указывать на ожидание участниками рынка повышенной волатильности в будущем.

Заключение

В этой статье мы рассмотрели 16 различных способов визуализации биржевых данных с помощью библиотеки Matplotlib. От базовых графиков цен и скользящих средних до более сложных визуализаций, таких как объемные профили, тепловые карты корреляций и сравнение исторической волатильности с подразумеваемой.

Ключевые выводы:

  1. Matplotlib — мощный инструмент для анализа рынка. Несмотря на то что эта питоновская библиотека не заточена специально под финансовые данные, ее гибкость позволяет создавать множество типов графиков, полезных для трейдинга и инвестирования;
  2. Визуализация помогает быстро оценить рыночную ситуацию. Графики вроде ценовых каналов, гэпов или распределения доходностей дают интуитивно понятное представление о волатильности, трендах и аномалиях;
  3. Комбинирование разных типов графиков повышает информативность. Например, совмещение цены, объемов и волатильности на одном графике помогает лучше понять динамику рынка;
  4. Анализ корреляций и распределений важен для управления рисками. Тепловые карты и гистограммы доходностей показывают, насколько активы связаны между собой и как часто происходят экстремальные движения.

Вот почему Matplotlib остается одним из самых популярных инструментов для визуализации данных в Python, и его возможности далеко не исчерпываются примерами из этой статьи. Экспериментируйте, комбинируйте разные подходы и находите оптимальные способы представления данных для ваших задач.