Перейти к содержимому

Глава 355: ResNet для Временных Рядов — Глубокие Остаточные Сети для Финансового Прогнозирования

Обзор

Остаточные сети (ResNet) произвели революцию в классификации изображений, позволив обучать очень глубокие сети благодаря skip-соединениям. Эта глава адаптирует архитектуру ResNet для анализа временных рядов в трейдинге, позволяя моделям улавливать как краткосрочные паттерны, так и долгосрочные зависимости в финансовых данных.

Ключевая идея: Традиционные глубокие сети страдают от затухающих градиентов и проблемы деградации. Skip-соединения в ResNet позволяют градиентам течь напрямую через сеть, обеспечивая эффективное обучение моделей с 50+ слоями для распознавания сложных финансовых паттернов.

Торговая Стратегия

Суть стратегии: Использование глубокой архитектуры ResNet для изучения иерархических временных признаков из OHLCV данных и прогнозирования направления цены с высокой точностью.

Факторы преимущества:

  1. Глубокие иерархии признаков улавливают паттерны на разных временных масштабах
  2. Остаточные соединения сохраняют детальную информацию через глубокие слои
  3. Skip-соединения действуют как ансамбль мелких и глубоких признаков
  4. Устойчивость к смене рыночных режимов благодаря абстрактным признакам

Целевые активы: Криптовалютные пары (BTC/USDT, ETH/USDT) с биржи Bybit

Техническая Основа

Почему ResNet для Временных Рядов?

Традиционные CNN для временных рядов сталкиваются с проблемами:

  • Затухающие градиенты в глубоких сетях
  • Потеря детальной временной информации
  • Сложность обучения тождественным отображениям

ResNet решает эти проблемы:

Обычный блок: x → [Conv] → [BN] → [ReLU] → [Conv] → [BN] → y
Остаточный блок: x → [Conv] → [BN] → [ReLU] → [Conv] → [BN] → (+) → y
| ↑
└──────────── Skip-соединение ─────────────────┘
Выход: y = F(x) + x (где F — выученная остаточная функция)

Математическое Обоснование

Для остаточного блока:

  • Вход: x
  • Целевое отображение: H(x)
  • Остаточная функция: F(x) = H(x) - x
  • Выход: y = F(x) + x

Почему это работает:

  • Если оптимально тождественное отображение, F(x) → 0 легче выучить, чем H(x) → x
  • Градиенты текут напрямую через skip-соединения: ∂L/∂x включает прямой путь
  • Сеть учит остаточные уточнения, а не полные преобразования

Адаптации для Временных Рядов

1D ResNet блок для временных рядов:
┌─────────────────────────────────────────────────────────────┐
│ │
│ Вход: [batch, каналы, временные_шаги] │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Conv1D(kernel=3, padding=1) → BatchNorm1D → ReLU │ │
│ │ Conv1D(kernel=3, padding=1) → BatchNorm1D │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ (сложение) ←─ Skip-соединение ←── Вход │
│ ↓ │
│ ReLU │
│ ↓ │
│ Выход: [batch, каналы, временные_шаги] │
│ │
└─────────────────────────────────────────────────────────────┘

Дизайн Архитектуры

ResNet-18 для Временных Рядов

┌──────────────────────────────────────────────────────────────────────┐
│ Архитектура ResNet-18 для Временных Рядов │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Входной слой │
│ ──────────── │
│ [batch, 5, 256] ← 5 признаков (OHLCV), 256 временных шагов │
│ ↓ │
│ Conv1D(5→64, k=7, s=2, p=3) → BN → ReLU → MaxPool(k=3, s=2, p=1) │
│ ↓ │
│ [batch, 64, 64] │
│ │
│ Слой 1 (64 канала) │
│ ────────────────── │
│ ResBlock × 2: [Conv1D(64→64) → BN → ReLU → Conv1D(64→64) → BN] + x │
│ ↓ │
│ [batch, 64, 64] │
│ │
│ Слой 2 (128 каналов) │
│ ──────────────────── │
│ ResBlock × 2 с понижением: stride=2, проекция для skip │
│ ↓ │
│ [batch, 128, 32] │
│ │
│ Слой 3 (256 каналов) │
│ ──────────────────── │
│ ResBlock × 2 с понижением: stride=2, проекция для skip │
│ ↓ │
│ [batch, 256, 16] │
│ │
│ Слой 4 (512 каналов) │
│ ──────────────────── │
│ ResBlock × 2 с понижением: stride=2, проекция для skip │
│ ↓ │
│ [batch, 512, 8] │
│ │
│ Выходная голова │
│ ────────────── │
│ AdaptiveAvgPool1D(1) → Flatten → Linear(512→3) │
│ ↓ │
│ [batch, 3] ← 3 класса: Вниз, Нейтрально, Вверх │
│ │
└──────────────────────────────────────────────────────────────────────┘

Архитектура Bottleneck (ResNet-50+)

Bottleneck блок:
┌────────────────────────────────────────────────────┐
│ │
│ Вход: [batch, 256, T] │
│ ↓ │
│ Conv1D(256→64, k=1) → BN → ReLU (сжатие) │
│ ↓ │
│ Conv1D(64→64, k=3, p=1) → BN → ReLU (обработка) │
│ ↓ │
│ Conv1D(64→256, k=1) → BN (расширение) │
│ ↓ │
│ (сложение) ←── Skip-соединение │
│ ↓ │
│ ReLU │
│ │
└────────────────────────────────────────────────────┘
Преимущество: в 3 раза меньше параметров при той же глубине

Инженерия Признаков для Трейдинга

Входные Признаки

┌────────────────────────────────────────────────────────────────┐
│ Инженерия Признаков │
├────────────────────────────────────────────────────────────────┤
│ │
│ Сырые OHLCV данные: │
│ ─────────────────── │
│ • Open, High, Low, Close, Volume (5 каналов) │
│ │
│ Производные от цены: │
│ ──────────────────── │
│ • Доходность: (close[t] - close[t-1]) / close[t-1] │
│ • Лог-доходность: log(close[t] / close[t-1]) │
│ • Диапазон High-Low: (high - low) / close │
│ • Коэффициент тела: |close - open| / (high - low) │
│ │
│ Технические индикаторы (как каналы): │
│ ───────────────────────────────────── │
│ • RSI (14): Индекс относительной силы │
│ • MACD: Схождение-расхождение скользящих средних │
│ • Bollinger Band %B │
│ • ATR: Средний истинный диапазон (нормализованный) │
│ │
│ Признаки объёма: │
│ ──────────────── │
│ • Изменение объёма: volume[t] / SMA(volume, 20) │
│ • Направление OBV: знак изменения балансового объёма │
│ • Отклонение от VWAP: (price - VWAP) / VWAP │
│ │
│ Итоговая форма входа: [batch, 15, 256] │
│ (15 каналов признаков, 256 временных шагов) │
│ │
└────────────────────────────────────────────────────────────────┘

Генерация Меток

def generate_labels(prices, forward_window=12, threshold=0.002):
"""
Генерация 3-классовых меток на основе будущей доходности
Классы:
- 0: Вниз (доходность < -threshold)
- 1: Нейтрально (|доходность| <= threshold)
- 2: Вверх (доходность > threshold)
"""
future_returns = prices.shift(-forward_window) / prices - 1
labels = np.where(
future_returns > threshold, 2,
np.where(future_returns < -threshold, 0, 1)
)
return labels

Процедура Обучения

Конвейер Данных

┌────────────────────────────────────────────────────────────────┐
│ Конвейер Данных для Обучения │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. Загрузка данных с Bybit │
│ └─ BTCUSDT, ETHUSDT: 1-минутные свечи, 1 год │
│ │
│ 2. Инженерия признаков │
│ └─ Вычисление 15 каналов признаков на каждый шаг │
│ │
│ 3. Создание последовательностей │
│ └─ Скользящее окно: 256 шагов вход → 1 метка │
│ │
│ 4. Разделение Train/Val/Test │
│ └─ 70% / 15% / 15% (хронологически, без утечки) │
│ │
│ 5. Нормализация │
│ └─ Z-нормализация по каналам, обучение только на train │
│ │
│ 6. Аугментация данных │
│ ├─ Гауссовский шум (σ=0.01) │
│ ├─ Временное искажение (лёгкое растяжение/сжатие) │
│ └─ Масштабирование амплитуды (0.9-1.1×) │
│ │
└────────────────────────────────────────────────────────────────┘

Конфигурация Обучения

config = {
# Модель
'model': 'ResNet18',
'input_channels': 15,
'num_classes': 3,
'dropout': 0.3,
# Обучение
'batch_size': 64,
'epochs': 100,
'learning_rate': 1e-3,
'weight_decay': 1e-4,
# Планировщик
'scheduler': 'CosineAnnealingLR',
'T_max': 100,
'eta_min': 1e-6,
# Ранняя остановка
'patience': 15,
'min_delta': 1e-4,
# Веса классов (для несбалансированных данных)
'class_weights': [1.2, 0.8, 1.2], # Вверх/Вниз с большим весом
}

Функции Потерь

# Стандартная кросс-энтропия с весами классов
criterion = nn.CrossEntropyLoss(weight=torch.tensor([1.2, 0.8, 1.2]))
# Альтернатива: Focal Loss для сложных примеров
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, pred, target):
ce_loss = F.cross_entropy(pred, target, reduction='none')
pt = torch.exp(-ce_loss)
focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
return focal_loss.mean()

Реализация Торговой Стратегии

Генерация Сигналов

class ResNetTradingStrategy:
def __init__(self, model, threshold=0.6, position_sizing='kelly'):
self.model = model
self.threshold = threshold
self.position_sizing = position_sizing
def generate_signal(self, features):
"""
Генерация торгового сигнала из предсказания модели
"""
with torch.no_grad():
probs = F.softmax(self.model(features), dim=1)
prob_down, prob_neutral, prob_up = probs[0].numpy()
# Только сигналы с высокой уверенностью
if prob_up > self.threshold:
return 'LONG', prob_up
elif prob_down > self.threshold:
return 'SHORT', prob_down
else:
return 'NEUTRAL', prob_neutral
def calculate_position_size(self, signal, confidence, portfolio_value):
"""
Размер позиции по критерию Келли
"""
if self.position_sizing == 'kelly':
# Упрощённый Келли: f = p - (1-p)/b
# где p = вероятность выигрыша, b = соотношение выигрыш/проигрыш
edge = confidence - 0.5
kelly_fraction = max(0, min(0.25, edge)) # Ограничение 25%
return portfolio_value * kelly_fraction
else:
return portfolio_value * 0.1 # Фиксированные 10%

Управление Рисками

┌────────────────────────────────────────────────────────────────┐
│ Правила Управления Рисками │
├────────────────────────────────────────────────────────────────┤
│ │
│ Размер позиции: │
│ ─────────────── │
│ • Максимальная позиция: 25% от портфеля │
│ • Масштаб по уверенности: size = base × (confidence - 0.5) × 2│
│ │
│ Стоп-лосс: │
│ ────────── │
│ • Фиксированный: -2% от входа │
│ • На основе ATR: -2 × ATR(14) │
│ • Трейлинг: -1.5 × ATR(14) от пика │
│ │
│ Тейк-профит: │
│ ──────────── │
│ • Фиксированный: +3% от входа (соотношение риск-доход 1:1.5) │
│ • Динамический: при развороте сигнала или падении уверенности │
│ │
│ Выходы по времени: │
│ ────────────────── │
│ • Максимальное удержание: 12 часов │
│ • Принудительный выход при новом окне предсказания │
│ │
│ Защита от просадки: │
│ ─────────────────── │
│ • Уменьшить размер на 50% при дневной просадке > 3% │
│ • Прекратить торговлю при недельной просадке > 10% │
│ │
└────────────────────────────────────────────────────────────────┘

Метрики Производительности

Метрики Классификации

МетрикаЦельОписание
Accuracy> 45%Общая точность предсказаний (3 класса)
Precision (Вверх)> 55%Истинные предсказания роста / Все предсказания роста
Precision (Вниз)> 55%Истинные предсказания падения / Все предсказания падения
F1-Score> 0.50Гармоническое среднее precision и recall
AUC-ROC> 0.60Площадь под кривой ROC

Торговые Метрики

МетрикаЦельОписание
Коэффициент Шарпа> 1.5Доходность с поправкой на риск (годовая)
Коэффициент Сортино> 2.0Доходность с поправкой на нисходящий риск
Макс. просадка< 15%Максимальное падение от пика до дна
Процент выигрышей> 52%Процент прибыльных сделок
Профит-фактор> 1.3Валовая прибыль / Валовый убыток
Коэффициент Кальмара> 1.0Годовая доходность / Макс. просадка

Структура Проекта

355_resnet_timeseries/
├── README.md # Английская версия
├── README.ru.md # Этот файл
├── readme.simple.md # Простое объяснение (English)
├── readme.simple.ru.md # Простое объяснение (Русский)
└── rust_resnet/ # Реализация на Rust
├── Cargo.toml
└── src/
├── lib.rs # Корень библиотеки
├── api/ # Клиент API Bybit
│ ├── mod.rs
│ ├── client.rs
│ └── types.rs
├── model/ # Реализация ResNet
│ ├── mod.rs
│ ├── resnet.rs
│ ├── blocks.rs
│ └── layers.rs
├── data/ # Обработка данных
│ ├── mod.rs
│ ├── features.rs
│ ├── dataset.rs
│ └── preprocessing.rs
├── strategy/ # Торговая стратегия
│ ├── mod.rs
│ ├── signals.rs
│ └── risk.rs
├── utils/ # Утилиты
│ ├── mod.rs
│ └── metrics.rs
└── bin/ # Исполняемые примеры
├── fetch_data.rs
├── train_model.rs
├── predict.rs
└── backtest.rs

Ключевые Инновации

1. Многомасштабные Остаточные Блоки

// Обработка на нескольких временных масштабах одновременно
pub struct MultiScaleResBlock {
branch_3: Conv1d, // kernel_size = 3
branch_5: Conv1d, // kernel_size = 5
branch_7: Conv1d, // kernel_size = 7
fusion: Conv1d, // Объединение ветвей
}

2. Остаточные Блоки с Вниманием

// Добавление канального внимания к остаточным блокам
pub struct SEResBlock {
conv_block: ResidualBlock,
squeeze: AdaptiveAvgPool1d,
excitation: Sequential<Linear, ReLU, Linear, Sigmoid>,
}
// Перекалибровка каналов признаков по важности
fn forward(&self, x: Tensor) -> Tensor {
let residual = self.conv_block.forward(x);
let weights = self.excitation.forward(self.squeeze.forward(&residual));
residual * weights.unsqueeze(-1) + x
}

3. Временное Позиционное Кодирование

// Внедрение информации о позиции для осознания времени
pub fn positional_encoding(seq_len: usize, d_model: usize) -> Tensor {
let positions: Vec<f32> = (0..seq_len).map(|i| i as f32).collect();
let dims: Vec<f32> = (0..d_model)
.map(|i| 1.0 / 10000_f32.powf(i as f32 / d_model as f32))
.collect();
// PE(pos, 2i) = sin(pos / 10000^(2i/d))
// PE(pos, 2i+1) = cos(pos / 10000^(2i/d))
// ...
}

Ссылки

  1. Deep Residual Learning for Image Recognition (He et al., 2015)

  2. Time Series Classification from Scratch with Deep Neural Networks (Wang et al., 2017)

  3. InceptionTime: Finding AlexNet for Time Series Classification (Fawaz et al., 2019)

  4. Deep Learning for Time Series Forecasting (Brownlee, 2018)

    • Практическое руководство для финансовых приложений
  5. ResNet-based Cryptocurrency Price Prediction (Научные статьи 2021-2023)

    • Различные применения для криптовалютных рынков

Уровень Сложности

Средний-Продвинутый

Необходимые знания:

  • Понимание CNN и основ глубокого обучения
  • Знакомство с концепциями анализа временных рядов
  • Базовые знания о трейдинге и бэктестинге
  • Опыт программирования на Rust (для реализации)

Ориентировочное время изучения: 15-20 часов

Следующие Шаги

  1. Начните со сбора данных — запустите fetch_data для получения рыночных данных Bybit
  2. Изучите модель — изучите архитектуру ResNet в model/resnet.rs
  3. Обучите и оцените — используйте train_model и анализируйте метрики
  4. Проведите бэктест — запустите backtest для оценки торговой производительности
  5. Экспериментируйте — пробуйте разные архитектуры, признаки и параметры