Свёрточные сети в финансовом анализе: от компьютерного зрения к прогнозированию рынков

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет, Хабр!

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги. 

Мы привыкли связывать сверточные нейросети (CNN) с задачами, относящимися к компьютерному зрению, например, к поиску лиц, классификации картинок или беспилотным авто.

Но сегодня те же архитектуры, которые когда‑то отличали кота от собаки на изображении, успешно предсказывают движение цен, находят устойчивые паттерны временных рядов и анализируют рыночные новости.

Умение «видеть» в хаосе котировок повторяющиеся структуры и извлекать эти полезные признаки в процессе обучения и есть суперсила сверточных сетей.

И сегодня мы поговорим об этом мощнейшем инструменте финансового анализа.

Псевдоизображения в финансах

Для начала давайте рассмотрим классическое определение CNN. Свёрточные нейронные сети представляют собой класс нейронных сетей, который специализируется на обработке данных с пространственной структурой, прежде всего изображений и видео.

Их ключевая особенность — использование математической операции свёртки в одном или нескольких слоях, что позволяет эффективно извлекать визуальные признаки.

В целом свёрточные нейросети были спроектированы под три индуктивных смещения.

  • Локальность — важные признаки формируются в окрестности точки, а не по всему входу сразу.

  • Трансляционная инвариантность — один и тот же паттерн может появиться в любом месте входа.

  • Иерархия масштабов — простые признаки комбинируются в сложные по мере углубления.

Но в финансах эти смещения будут работать частично. Давайте разберём несколько конкретных примеров использования CNN. И начнём с рассмотрения временного ряда как «псевдоизображения» (1 D‑CNN).

Перед нами стоит задача классифицировать 60-минутные окна BTC‑USD, разбив их на три класса:

  • «Рост > +0.5%».

  • «Падение < −0.5%».

  • «Боковик».

В качестве горизонта мы будем рассматривать следующие 30 минут.

Здесь самое важное — определить, что нам нужно подать на вход. Использование сырых цен — это очень плохой вариант. Дело в том, что они нестационарны, то есть абсолютный уровень и волатильность дрейфуют во времени, и CNN выучивает не форму паттерна, а конкретные значения цены в тренировочном окне, из‑за чего трансляционная инвариантность свёртки превращается из полезного свойства в источник ложных срабатываний.

Вместо этого мы используем лог‑доходности и нормализацию:

import numpy as np
import pandas as pd

def build_features(df, window=60):
    """
    df: DataFrame с колонками open, high, low, close, volume
    Возвращает массив окон признаков и метки.
    """

    # 1. Лог-доходности вместо цен (убираем нестационарность уровня)
    df['ret'] = np.log(df['close'] / df['close'].shift(1))
    df['hl_range'] = np.log(df['high'] / df['low'])
    df['vol_z'] = (df['volume'] - df['volume'].rolling(200).mean()) / df['volume'].rolling(200).std()

    # 2. Z-score по скользящему окну (адаптация к режиму волатильности)
    for col in ['ret', 'hl_range', 'vol_z']:
        roll = df[col].rolling(200)
        df[f'{col}_z'] = (df[col] - roll.mean()) / (roll.std() + 1e-8)

    features = df[['ret_z', 'hl_range_z', 'vol_z_z']].dropna().values

    # 3. Метки: forward return через 30 шагов
    fwd = np.log(df['close'].shift(-30) / df['close'])
    labels = np.where(fwd > 0.005, 2, np.where(fwd < -0.005, 0, 1))

    X, y = [], []

    for i in range(window, len(features) - 30):
        X.append(features[i - window:i].T)  # (channels, time)
        y.append(labels[i])
    return np.array(X), np.array(y)

Архитектура

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

Так, нормализация считается на скользящем окне, а не по всему датасету, потому что иначе у нас будет утечка будущего (look‑ahead bias), то есть ситуация, когда модель при обучении или оценке использует информацию, которая фактически недоступна в момент реального принятия решения.

Также метки здесь считаются через shift(-30), то есть последние 30 строк должны быть отброшены, иначе тренируемся на будущем.

И в обязательном порядке должен использоваться Z‑score по волатильности, так как без него модель выучит «высокая волатильность = сигнал», а это режимный артефакт, а не паттерн.

import torch
import torch.nn as nn
 
class TemporalCNN(nn.Module):
    def init(self, n_channels=3, n_classes=3):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv1d(n_channels, 32, kernel_size=5, padding=2),
            nn.BatchNorm1d(32),
            nn.ReLU(),
            nn.MaxPool1d(2),
            nn.Conv1d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.MaxPool1d(2),
            nn.Conv1d(64, 64, kernel_size=3, padding=1, dilation=2),  # dilated — расширяем рецептивное поле
            nn.ReLU(),
            nn.AdaptiveAvgPool1d(1),
        )
        self.head = nn.Linear(64, n_classes)

    def forward(self, x):
        return self.head(self.net(x).squeeze(-1))

В примере ниже функция строит схему кросс‑валидации для временных рядов, разбивая данные на последовательные блоки так, чтобы на каждом шаге обучение шло только на прошлом до train_end, а тест — на следующем блоке после небольшого зазора embargo.

Причём между концом обучения и началом теста не остаётся пересечений, так как этот зазор нужен, чтобы метки обучающих примеров (которые считаются по будущим значениям, например forward return через 30 шагов) не «подглядывали» в тестовый период, а двигающееся окно обучения эмулировало реальное переобучение модели по мере поступления новых данных.

def walk_forward_splits(n_samples, n_splits=5, embargo=30):
    """Purged walk-forward CV (López de Prado)."""

    fold_size = n_samples // (n_splits + 1)
    for k in range(n_splits):
        train_end = fold_size * (k + 1)
        test_start = train_end + embargo
        test_end = min(test_start + fold_size, n_samples)
        if test_end <= test_start:
            break
        yield np.arange(0, train_end), np.arange(test_start, test_end)

2 D‑представления ряда

Главный недостаток подхода 1 D‑CNN на финансовых рядах — это ограниченное рецептивное поле и слабая работа с дальними зависимостями, так как свёртка видит только локальную окрестность. Поэтому «эхо» событий спустя сотни шагов, долгую память и режимные сдвиги она улавливает плохо.

В качестве альтернативы мы рассмотрим популярные способы превратить 1 D‑ряд в «картинку».

Начнём с STFT, или спектрограммы.

  • Плюс такого подхода в том, что мы физически осмысленно можем ловить смену частот (ускорение/замедление трендов, кластеры волатильности).

  • Минус — необходимость вручную подбирать фиксированное разрешение по частоте и времени (принцип неопределённости). Соответственно, параметр nperseg подбираем под горизонт.

import scipy.signal as ss

def stft_image(series, nperseg=32, noverlap=24):
    f, t, Z = ss.stft(series, nperseg=nperseg, noverlap=noverlap)
    return np.abs(Z)  # (freq_bins, time_bins)

Multi‑asset correlation map хорошо ловит режимные сдвиги (risk‑on/risk‑off), что бывает реально полезно для портфельных задач и для BTC как рискового актива. При этом он не работает на одном активе, а данные нужно синхронизировать.

def corr_image(returns_df, window=60):
    """Матрица корреляций по окну — 'картинка' кросс-активных связей."""
    return returns_df.iloc[-window:].corr().values

Таким образом, выбор подхода зависит от задачи:

  • Для одного актива с коротким горизонтом используем 1 D‑CNN на нормализованных доходностях.

  • В случае сильной волатильности применяем STFT + CNN.

  • Для портфельных и кросс‑активных сигналов нужны correlation maps + CNN или GNN.

GAF — академически красиво, но дорого

Использование превращения 1 D‑ряда в «картинку» плохо тем, что оно чаще всего приносит сложность без добавления информации. Например, STFT навязывает фиксированное разрешение по времени и частоте.

В качестве альтернативы можно рассмотреть метод GAF (Gramian Angular Field), в котором мы будем кодировать одномерный временной ряд в двумерное изображение. Идея в том, что исходный ряд сначала нормализуется, затем переводится в полярные координаты (угол кодирует значение, радиус — время), после чего попарные отношения между точками ряда формируют матрицу‑«картинку».

Это самый недооценённый сценарий, так как вместо того чтобы предсказывать цену CNN‑ом, мы используем его как энкодер признаков, а решение принимает градиентный бустинг.

import lightgbm as lgb
from sklearn.metrics import roc_auc_score
 
# Обучаем CNN на вспомогательной задаче (например, предсказание знака forward return)
cnn = TemporalCNN().cuda()
# ... обучение ...
 
# Извлекаем эмбеддинги
cnn.eval()
embeddings = []
with torch.no_grad():
    for batch in loader:
        emb = cnn.net(batch.cuda()).squeeze(-1).cpu().numpy()
        embeddings.append(emb)

X_emb = np.vstack(embeddings)  # (n, 64)

Дальше мы можем использовать стандартный пайплайн: LightGBM + walk‑forward + калибровка вероятностей.

Этот алгоритм работает потому, что CNN выучивает локальные паттерны, которые трудно захардкодить. Также бустинг устойчив к шуму, не переобучается на 64 признаках так, как CNN на сыром ряде.

На практике такая гибридная схема на BTC‑данных стабильно даёт Sharpe выше, чем end‑to‑end CNN, и, что важнее, устойчивее к смене режима.

Детектор аномалий / микроструктурных сигналов

А вот здесь CNN будет ближе всего к «честному» компьютерному зрению. Здесь наша задача заключается в том, чтобы найти на «карте» ордербука локальные структуры, такие как стенки, iceberg‑паттерны или спуфинг.

Для практической реализации нам потребуется следующий код: 

class OrderBookCNN(nn.Module):
    """Вход: (batch, 1, price_levels, time_steps) — 2D-снимок стакана."""

    def init(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
            nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
            nn.AdaptiveAvgPool2d(1),
        )
        self.head = nn.Linear(32, 2)  # аномалия / норма

    def forward(self, x):
        return self.head(self.features(x).flatten(1))

Здесь аналогия с CV работает почти буквально, так как есть:

  • Локальность по уровням цены и по времени.

  • Трансляционная инвариантность по уровням (стенка на 5-м уровне = стенка на 15-м).

  • Иерархия «тик → паттерн → режим».

Итог: как строить пайплайн

Для построения пайплайна с использованием CNN в финансовом анализе необходимо начать с baseline без CNN, то есть использовать логистическую регрессию или LightGBM на инженерных признаках (доходности, волатильность, RSI, режимные индикаторы).

Затем добавьте 1 D‑CNN на нормализованных доходностях как отдельную модель и сравните с baseline на purge/embargo CV.

Если CNN выигрывает, то используйте её как энкодер признаков, а финальное решение отдайте бустингу. Ну а 2 D‑представления (STFT, correlation maps) добавляйте только в том случае, если у вас есть гипотеза, что именно мультимасштабная или кросс‑активная структура несёт сигнал. Иначе это будет усложнением без выигрыша.

Заключительный тезис заключается в том, что сила CNN в финансах — не в «предсказании курса биткоина», а в извлечении локальных мультимасштабных признаков, которые потом используются в более устойчивых моделях.

Свёрточное зрение здесь действительно работает, но как компонент пайплайна, а не как волшебная кнопка.

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

На бесплатных вебинарах от преподавателей Otus вы проследите путь от распознавания паттернов в данных до принятия торговых решений с помощью нейросетей — чтобы использовать машинное обучение для решения реальных задач финансового анализа.

  • 27 октября, 20:00. «Свёрточные сети: от распознавания изображений до прогнозирования курса биткоина». Записаться

  • 18 ноября, 20:00. «Нейросети в алгоритмической торговле — учим модель принимать торговые решения». Записаться

Источник: https://habr.com/ru/companies/otus/articles/1096898/


Интересные статьи

Интересные статьи

BMW запатентовала круглые винты с головкой в виде логотипа. Заявка подана в июне 2024 года, но опубликована недавно. Если в автомобилях будет использоваться новая технология, самостоятельный ремонт ма...
История ИТ представляет собой череду волн автоматизации, каждая из которых радикально меняла требования рынка и подходы к разработке. Эти волны не просто внедряли новые технологии, они трансформировал...
Начнем с небольшого предисловия. Битрикс24 позволяет автоматизировать множество разных процессов внутри компании. В частности в Битрикс есть большой и сильный блок автоматизации процессов работы с кли...
Представляю вашему вниманию цикл статей по работе с пока еще мало знакомым многим битрикс-разработчикам инструментом оперирования данными с CRM Битрикс24 через абстрактные фабрики. В первой статье мы ...
Хочу поделиться опытом автоматизации экспорта заказов из Aliexpress в несколько CRM. Приведенные примеры написаны на PHP, но библиотеки для работы с Aliexpress есть и для...