Привет, Хабр!
Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.
Мы привыкли связывать сверточные нейросети (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. «Нейросети в алгоритмической торговле — учим модель принимать торговые решения». Записаться
