Глава 355: ResNet для Временных Рядов — Глубокие Остаточные Сети для Финансового Прогнозирования
Обзор
Остаточные сети (ResNet) произвели революцию в классификации изображений, позволив обучать очень глубокие сети благодаря skip-соединениям. Эта глава адаптирует архитектуру ResNet для анализа временных рядов в трейдинге, позволяя моделям улавливать как краткосрочные паттерны, так и долгосрочные зависимости в финансовых данных.
Ключевая идея: Традиционные глубокие сети страдают от затухающих градиентов и проблемы деградации. Skip-соединения в ResNet позволяют градиентам течь напрямую через сеть, обеспечивая эффективное обучение моделей с 50+ слоями для распознавания сложных финансовых паттернов.
Торговая Стратегия
Суть стратегии: Использование глубокой архитектуры ResNet для изучения иерархических временных признаков из OHLCV данных и прогнозирования направления цены с высокой точностью.
Факторы преимущества:
- Глубокие иерархии признаков улавливают паттерны на разных временных масштабах
- Остаточные соединения сохраняют детальную информацию через глубокие слои
- Skip-соединения действуют как ансамбль мелких и глубоких признаков
- Устойчивость к смене рыночных режимов благодаря абстрактным признакам
Целевые активы: Криптовалютные пары (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)) // ...}Ссылки
-
Deep Residual Learning for Image Recognition (He et al., 2015)
- Оригинальная статья о ResNet: https://arxiv.org/abs/1512.03385
-
Time Series Classification from Scratch with Deep Neural Networks (Wang et al., 2017)
- ResNet для временных рядов: https://arxiv.org/abs/1611.06455
-
InceptionTime: Finding AlexNet for Time Series Classification (Fawaz et al., 2019)
- Комплексное сравнение: https://arxiv.org/abs/1909.04939
-
Deep Learning for Time Series Forecasting (Brownlee, 2018)
- Практическое руководство для финансовых приложений
-
ResNet-based Cryptocurrency Price Prediction (Научные статьи 2021-2023)
- Различные применения для криптовалютных рынков
Уровень Сложности
Средний-Продвинутый
Необходимые знания:
- Понимание CNN и основ глубокого обучения
- Знакомство с концепциями анализа временных рядов
- Базовые знания о трейдинге и бэктестинге
- Опыт программирования на Rust (для реализации)
Ориентировочное время изучения: 15-20 часов
Следующие Шаги
- Начните со сбора данных — запустите
fetch_dataдля получения рыночных данных Bybit - Изучите модель — изучите архитектуру ResNet в
model/resnet.rs - Обучите и оцените — используйте
train_modelи анализируйте метрики - Проведите бэктест — запустите
backtestдля оценки торговой производительности - Экспериментируйте — пробуйте разные архитектуры, признаки и параметры