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

Глава 94: QuantNet — Трансферное обучение для торговых стратегий

Обзор

QuantNet — это архитектура трансферного обучения (transfer learning), разработанная для систематических торговых стратегий. Предложенная Кизиелом и Горсом (Kisiel & Gorse, 2021), архитектура QuantNet обучает общее представление рыночных данных с помощью структуры «кодировщик-декодировщик» (encoder-decoder), тренируемой одновременно на множестве активов, а затем переносит это обученное представление для генерации альфа-сигналов по каждому отдельному активу. Ключевое нововведение заключается в двухэтапном процессе обучения: сначала модель изучает универсальный экстрактор рыночных признаков, а затем производится тонкая настройка (fine-tuning) стратегий для конкретных активов с использованием общего представления.

В традиционном количественном трейдинге стратегии разрабатываются независимо для каждого актива. QuantNet бросает вызов этому подходу, демонстрируя, что общее представление, обученное на множестве активов, улавливает универсальные рыночные динамики — развороты импульса (momentum reversals), кластеризацию волатильности (volatility clustering) и кросс-активные корреляции — что улучшает результаты для отдельных активов, особенно для тех, у которых ограничена история торгов.

Содержание

  1. Введение в QuantNet
  2. Математические основы
  3. Архитектура QuantNet
  4. Трансферное обучение для трейдинга
  5. Реализация на Python
  6. Реализация на Rust
  7. Практические примеры с данными акций и криптовалют
  8. Фреймворк бэктестинга
  9. Оценка производительности
  10. Литература и перспективы развития

Введение в QuantNet

Что такое QuantNet?

QuantNet — это архитектура нейронной сети, которая применяет трансферное обучение к систематическому трейдингу. Вместо обучения отдельных моделей для каждого актива QuantNet:

  1. Предварительно обучает общий кодировщик на данных множества активов одновременно для изучения универсальных рыночных признаков
  2. Переносит общее представление на отдельные активы для тонкой настройки стратегий
  3. Использует структуру кодировщик-декодировщик, где кодировщик улавливает общие рыночные динамики, а декодировщик генерирует торговые сигналы

Ключевая идея

Рынки обладают универсальными динамиками: импульс (momentum), возврат к среднему (mean reversion), кластеризация волатильности (volatility clustering) и смена рыночных режимов (regime changes) существуют на рынках акций, криптовалют и других активов. QuantNet использует это, обучая общее латентное представление, которое улавливает кросс-активные паттерны, а затем специализирует это представление для каждого отдельного актива.

Зачем нужно трансферное обучение для трейдинга?

  • Дефицит данных: У некоторых активов ограниченная история; трансферное обучение заимствует информацию у активов с богатой историей данных
  • Общие динамики: Паттерны рыночной микроструктуры (например, кластеризация волатильности) являются универсальными
  • Регуляризация: Общий кодировщик действует как сильный приор (prior), предотвращая переобучение (overfitting) на шуме отдельных активов
  • Проблема холодного старта: Новые активы или рынки могут немедленно воспользоваться предобученным представлением
  • Кросс-рыночная альфа: Паттерны, обнаруженные на одном рынке, могут генерировать сигналы на другом

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

Целевая функция QuantNet

QuantNet оптимизирует двухкомпонентную целевую функцию. На этапе предобучения, для N активов:

L_pretrain = (1/N) * Σ_{i=1}^{N} L_recon(x_i, D(E(x_i))) + λ * L_reg(E)

Где:

  • E(·) — общий кодировщик, отображающий входные признаки в латентное пространство
  • D(·) — декодировщик, восстанавливающий входные признаки из латентного представления
  • L_recon — ошибка реконструкции (среднеквадратичная ошибка, MSE)
  • L_reg — регуляризация кодировщика (например, дивергенция Кульбака-Лейблера или снижение весов)
  • λ — сила регуляризации

Этап переноса (Transfer Phase)

На этапе переноса, для каждого актива i:

L_transfer_i = L_trading(y_i, f_i(E(x_i))) + α * L_recon(x_i, D(E(x_i)))

Где:

  • f_i(·) — торговая головка (head), специфичная для актива
  • y_i — торговые цели (доходности, направления)
  • α — вес, балансирующий торговую ошибку и ошибку реконструкции

Архитектура общего кодировщика

Кодировщик отображает необработанные признаки в латентное представление:

z = E(x) = σ(W_L ... σ(W_2 * σ(W_1 * x + b_1) + b_2) ... + b_L)

Где σ — функция активации (ReLU или GELU), а {W_l, b_l} — обучаемые параметры, общие для всех активов.

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

Головка для конкретного актива генерирует торговый сигнал s_i в диапазоне [-1, 1]:

s_i = tanh(f_i(z)) = tanh(W_i^f * z + b_i^f)

Где s_i > 0 означает длинную позицию (покупка), а s_i < 0 — короткую позицию (продажа).

Функция потерь на основе коэффициента Шарпа (Sharpe Ratio Loss)

QuantNet может быть обучен напрямую для максимизации коэффициента Шарпа:

L_sharpe = -E[r * s] / sqrt(Var[r * s])

Где r — доходность актива, а s — торговый сигнал модели. Это позволяет напрямую оптимизировать доходность с учётом риска, а не точность прогнозирования.

Дифференцируемость коэффициента Шарпа

Важным свойством этой функции потерь является её дифференцируемость. Градиент функции потерь Шарпа по параметрам модели θ можно вычислить следующим образом:

∂L_sharpe/∂θ = -(E[r * ∂s/∂θ] * σ(r*s) - E[r*s] * E[r*s * r * ∂s/∂θ] / σ(r*s)) / Var[r*s]

Это позволяет использовать стандартные методы обратного распространения ошибки (backpropagation) для оптимизации. В отличие от классических метрик, таких как информационный коэффициент (Information Coefficient), коэффициент Шарпа учитывает не только направление сигнала, но и его величину, а также изменчивость доходности.

Регуляризация через реконструкцию

Комбинация торговой ошибки и ошибки реконструкции в целевой функции играет роль регуляризации:

L_total = L_trading + α * L_recon

Ошибка реконструкции предотвращает «катастрофическое забывание» (catastrophic forgetting), заставляя кодировщик сохранять универсальное представление рыночных данных даже во время тонкой настройки. Параметр α контролирует баланс между специализацией и обобщением.


Архитектура QuantNet

Общая структура

Признаки актива 1 ──┐
Признаки актива 2 ──┤
... ├──→ [Общий кодировщик] ──→ z (латентное) ──→ [Декодировщик] ──→ Реконструкция
Признаки актива N ──┘ │
├──→ [Головка 1] ──→ Сигнал 1
├──→ [Головка 2] ──→ Сигнал 2
└──→ [Головка N] ──→ Сигнал N

Общий кодировщик (Shared Encoder)

Общий кодировщик представляет собой многослойную сеть прямого распространения:

class SharedEncoder:
layers = [
Linear(input_dim, 64) → BatchNorm → GELUDropout(0.1),
Linear(64, 32) → BatchNorm → GELUDropout(0.1),
Linear(32, latent_dim)
]

Каждый слой включает:

  • Linear — линейное преобразование (полносвязный слой)
  • BatchNorm — пакетная нормализация для стабилизации обучения
  • GELU — функция активации Gaussian Error Linear Unit, которая обеспечивает гладкую нелинейность
  • Dropout — случайное выключение нейронов для регуляризации

Декодировщик (Decoder)

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

class Decoder:
layers = [
Linear(latent_dim, 32) → BatchNorm → GELUDropout(0.1),
Linear(32, 64) → BatchNorm → GELUDropout(0.1),
Linear(64, input_dim)
]

Задача декодировщика — восстановить исходные входные признаки из латентного представления. Чем лучше реконструкция, тем больше полезной информации сохранено в латентном пространстве.

Торговая головка для конкретного актива (Trading Head)

Каждый актив получает небольшую специализированную сеть:

class TradingHead:
layers = [
Linear(latent_dim, 16) → ReLU,
Linear(16, 1) → Tanh
]

Функция активации Tanh на выходе ограничивает сигнал диапазоном [-1, 1], что интерпретируется как:

  • +1 — максимальная длинная позиция (полная покупка)
  • 0 — нейтральная позиция (нет сделки)
  • -1 — максимальная короткая позиция (полная продажа)

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

  1. Фаза 1 — Предобучение: Обучение кодировщика и декодировщика на задаче реконструкции по всем активам. На этом этапе модель учится извлекать универсальные рыночные признаки, которые позволяют точно воспроизвести входные данные.

  2. Фаза 2 — Тонкая настройка: Замораживание или замедление обучения кодировщика; обучение торговых головок для конкретных активов с использованием торговой целевой функции. Кодировщик обновляется медленно, чтобы сохранить общее представление.

  3. Фаза 3 — Сквозная оптимизация (End-to-End): Совместная оптимизация кодировщика и головок с уменьшенной скоростью обучения (learning rate) для кодировщика. Этот этап позволяет всей системе адаптироваться, сохраняя при этом стабильность общего представления.


Трансферное обучение для трейдинга

Кросс-активный перенос (Cross-Asset Transfer)

QuantNet демонстрирует, что признаки, изученные на разнообразном наборе активов, переносятся на:

  • Новые активы: Активы, которые не встречались во время предобучения
  • Другие временные периоды: Будущие рыночные режимы, отличающиеся от обучающей выборки
  • Другие классы активов: Перенос с акций на криптовалюты, или с товаров на валютные пары

Представление признаков в латентном пространстве

Латентное пространство z, изученное кодировщиком, улавливает:

  • Сигналы импульса (Momentum): Паттерны скользящей доходности на нескольких временных масштабах. Кодировщик учится распознавать как краткосрочный, так и долгосрочный импульс.
  • Структура волатильности (Volatility Structure): Динамика реализованной и подразумеваемой волатильности. Это включает эффекты кластеризации и асимметрию волатильности.
  • Возврат к среднему (Mean Reversion): Отклонения от скользящих средних и равновесных уровней. Модель фиксирует тенденцию цен возвращаться к средним значениям после значительных отклонений.
  • Кросс-активные корреляции: Как активы движутся совместно и расходятся. Этот признак особенно ценен для портфельного управления и хеджирования.

Преимущества перед одноактивными моделями

АспектОдноактивная модельQuantNet с переносом
Эффективность использования данныхТребует длинную историюРаботает с ограниченными данными
Риск переобученияВысокий (шум одного актива)Низкий (общая регуляризация)
Холодный стартНе работает с новыми активамиПредобученный кодировщик работает сразу
Кросс-активные сигналыНе улавливаютсяИзучаются явно
Количество моделейN моделей для N активов1 кодировщик + N маленьких головок

Когда трансферное обучение особенно полезно

Трансферное обучение в контексте трейдинга приносит наибольшую пользу в следующих случаях:

  1. Недавно листингованные активы — например, новые криптовалюты или IPO, где история торгов составляет несколько месяцев
  2. Экзотические рынки — активы с низкой ликвидностью, где данных мало, но рыночные закономерности схожи с основными рынками
  3. Смена рыночного режима — когда рынок переходит от бычьего к медвежьему тренду, предобученное представление помогает адаптироваться быстрее
  4. Мультиактивные портфели — одна модель обслуживает все активы, что упрощает инфраструктуру

Реализация на Python

Архитектура модели

import torch
import torch.nn as nn
class QuantNetEncoder(nn.Module):
"""Общий кодировщик, изучающий универсальные рыночные представления."""
def __init__(self, input_dim, hidden_dims=[64, 32], latent_dim=16, dropout=0.1):
super().__init__()
layers = []
prev_dim = input_dim
for h in hidden_dims:
layers.extend([
nn.Linear(prev_dim, h),
nn.BatchNorm1d(h),
nn.GELU(),
nn.Dropout(dropout),
])
prev_dim = h
layers.append(nn.Linear(prev_dim, latent_dim))
self.encoder = nn.Sequential(*layers)
def forward(self, x):
return self.encoder(x)
class QuantNetDecoder(nn.Module):
"""Декодировщик для предобучения через реконструкцию."""
def __init__(self, latent_dim=16, hidden_dims=[32, 64], output_dim=10, dropout=0.1):
super().__init__()
layers = []
prev_dim = latent_dim
for h in hidden_dims:
layers.extend([
nn.Linear(prev_dim, h),
nn.BatchNorm1d(h),
nn.GELU(),
nn.Dropout(dropout),
])
prev_dim = h
layers.append(nn.Linear(prev_dim, output_dim))
self.decoder = nn.Sequential(*layers)
def forward(self, z):
return self.decoder(z)
class TradingHead(nn.Module):
"""Генератор торговых сигналов для конкретного актива."""
def __init__(self, latent_dim=16, hidden_dim=16):
super().__init__()
self.head = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
nn.Tanh(),
)
def forward(self, z):
return self.head(z)
class QuantNet(nn.Module):
"""
Полная архитектура QuantNet для трансферного обучения
торговых стратегий на множестве активов.
"""
def __init__(self, input_dim, n_assets, hidden_dims=[64, 32],
latent_dim=16, dropout=0.1):
super().__init__()
self.encoder = QuantNetEncoder(input_dim, hidden_dims, latent_dim, dropout)
self.decoder = QuantNetDecoder(latent_dim, list(reversed(hidden_dims)),
input_dim, dropout)
self.heads = nn.ModuleDict({
f"asset_{i}": TradingHead(latent_dim) for i in range(n_assets)
})
def forward(self, x, asset_id=None):
z = self.encoder(x)
reconstruction = self.decoder(z)
if asset_id is not None:
signal = self.heads[f"asset_{asset_id}"](z)
return signal, reconstruction, z
signals = {k: head(z) for k, head in self.heads.items()}
return signals, reconstruction, z

Обучение с функцией потерь Sharpe Ratio

class SharpeRatioLoss(nn.Module):
"""Дифференцируемая функция потерь на основе коэффициента Шарпа
для прямой оптимизации доходности с учётом риска."""
def __init__(self, eps=1e-8):
super().__init__()
self.eps = eps
def forward(self, signals, returns):
portfolio_returns = signals.squeeze() * returns.squeeze()
mean_return = portfolio_returns.mean()
std_return = portfolio_returns.std() + self.eps
sharpe = mean_return / std_return
return -sharpe # Отрицательный знак, так как мы минимизируем
class QuantNetTrainer:
"""Двухфазный обучатель для QuantNet."""
def __init__(self, model, lr=1e-3, recon_weight=1.0, trading_weight=1.0):
self.model = model
self.optimizer = torch.optim.Adam(model.parameters(), lr=lr)
self.recon_loss = nn.MSELoss()
self.sharpe_loss = SharpeRatioLoss()
self.recon_weight = recon_weight
self.trading_weight = trading_weight
def pretrain_step(self, features_batch):
"""Фаза 1: Обучение кодировщика-декодировщика на реконструкции."""
self.model.train()
self.optimizer.zero_grad()
_, reconstruction, _ = self.model(features_batch)
loss = self.recon_loss(reconstruction, features_batch)
loss.backward()
self.optimizer.step()
return loss.item()
def finetune_step(self, features_batch, returns_batch, asset_id):
"""Фаза 2: Тонкая настройка с торговой целевой функцией."""
self.model.train()
self.optimizer.zero_grad()
signal, reconstruction, _ = self.model(features_batch, asset_id)
loss_recon = self.recon_loss(reconstruction, features_batch)
loss_trading = self.sharpe_loss(signal, returns_batch)
loss = (self.recon_weight * loss_recon +
self.trading_weight * loss_trading)
loss.backward()
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
self.optimizer.step()
return loss.item(), loss_recon.item(), loss_trading.item()

Создание признаков для обучения

import numpy as np
import pandas as pd
def create_features(df, lookback_periods=[5, 10, 20, 60]):
"""
Создание признаков из OHLCV данных для QuantNet.
Параметры:
df: DataFrame с колонками Open, High, Low, Close, Volume
lookback_periods: периоды для скользящих статистик
Возвращает:
DataFrame с рассчитанными признаками
"""
features = pd.DataFrame(index=df.index)
# Логарифмическая доходность
features['log_return'] = np.log(df['Close'] / df['Close'].shift(1))
# Скользящие доходности за различные периоды
for period in lookback_periods:
features[f'return_{period}d'] = df['Close'].pct_change(period)
# Реализованная волатильность
for period in lookback_periods:
features[f'volatility_{period}d'] = (
features['log_return'].rolling(period).std() * np.sqrt(252)
)
# Отклонение от скользящей средней (mean reversion)
for period in lookback_periods:
sma = df['Close'].rolling(period).mean()
features[f'sma_deviation_{period}d'] = (df['Close'] - sma) / sma
# Объём (нормализованный)
for period in lookback_periods:
avg_volume = df['Volume'].rolling(period).mean()
features[f'volume_ratio_{period}d'] = df['Volume'] / avg_volume
return features.dropna()

Полную исполняемую реализацию смотрите в директории python/.


Реализация на Rust

Основная архитектура

Реализация на Rust повторяет версию на Python, при этом уделяя внимание производительности и типобезопасности:

use rand::Rng;
use rand_distr::Normal;
/// Общий кодировщик для изучения универсальных рыночных представлений.
pub struct Encoder {
weights: Vec<Vec<Vec<f64>>>,
biases: Vec<Vec<f64>>,
}
impl Encoder {
/// Создание нового кодировщика с инициализацией Ксавье (Xavier).
pub fn new(input_dim: usize, hidden_dims: &[usize], latent_dim: usize) -> Self {
// Инициализация Ксавье для всех слоёв
let mut dims = vec![input_dim];
dims.extend(hidden_dims);
dims.push(latent_dim);
// ... инициализация весов
Self { weights, biases }
}
/// Прямой проход через кодировщик.
pub fn forward(&self, x: &[f64]) -> Vec<f64> {
let mut h = x.to_vec();
for (w, b) in self.weights.iter().zip(&self.biases) {
h = Self::linear(&h, w, b);
h = Self::gelu(&h); // Функция активации
}
h
}
/// Линейное преобразование: y = W * x + b
fn linear(x: &[f64], w: &[Vec<f64>], b: &[f64]) -> Vec<f64> {
w.iter()
.zip(b)
.map(|(row, bias)| {
row.iter().zip(x).map(|(wi, xi)| wi * xi).sum::<f64>() + bias
})
.collect()
}
/// Функция активации GELU (Gaussian Error Linear Unit).
fn gelu(x: &[f64]) -> Vec<f64> {
x.iter()
.map(|&v| 0.5 * v * (1.0 + (v * 0.7978845608 * (1.0 + 0.044715 * v * v)).tanh()))
.collect()
}
}
/// Декодировщик — зеркальная структура кодировщика.
pub struct Decoder {
weights: Vec<Vec<Vec<f64>>>,
biases: Vec<Vec<f64>>,
}
impl Decoder {
pub fn new(latent_dim: usize, hidden_dims: &[usize], output_dim: usize) -> Self {
// Аналогичная инициализация, что и у кодировщика
Self { weights, biases }
}
pub fn forward(&self, z: &[f64]) -> Vec<f64> {
let mut h = z.to_vec();
for (i, (w, b)) in self.weights.iter().zip(&self.biases).enumerate() {
h = Encoder::linear(&h, w, b);
if i < self.weights.len() - 1 {
h = Encoder::gelu(&h);
}
}
h
}
}
/// Торговая головка для конкретного актива.
pub struct TradingHead {
w1: Vec<Vec<f64>>,
b1: Vec<f64>,
w2: Vec<Vec<f64>>,
b2: Vec<f64>,
}
impl TradingHead {
pub fn forward(&self, z: &[f64]) -> f64 {
let hidden = Encoder::linear(z, &self.w1, &self.b1);
let activated: Vec<f64> = hidden.iter().map(|&v| v.max(0.0)).collect(); // ReLU
let output = Encoder::linear(&activated, &self.w2, &self.b2);
output[0].tanh() // Ограничение сигнала диапазоном [-1, 1]
}
}

Вычисление коэффициента Шарпа на Rust

/// Вычисление коэффициента Шарпа для серии доходностей.
pub fn sharpe_ratio(returns: &[f64], annualization_factor: f64) -> f64 {
let n = returns.len() as f64;
if n < 2.0 {
return 0.0;
}
let mean = returns.iter().sum::<f64>() / n;
let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
let std = variance.sqrt();
if std < 1e-10 {
return 0.0;
}
(mean / std) * annualization_factor.sqrt()
}
/// Коэффициент Шарпа с годовой аннуализацией (252 торговых дня).
pub fn annualized_sharpe(daily_returns: &[f64]) -> f64 {
sharpe_ratio(daily_returns, 252.0)
}

Интеграция с Bybit

use serde::Deserialize;
/// Структура для хранения данных свечей (OHLCV).
#[derive(Debug, Deserialize)]
pub struct Kline {
pub timestamp: u64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
/// Получение данных OHLCV с API Bybit.
pub async fn fetch_klines(symbol: &str, interval: &str, limit: usize)
-> Result<Vec<Kline>> {
let url = format!(
"https://api.bybit.com/v5/market/kline?category=spot&symbol={}&interval={}&limit={}",
symbol, interval, limit
);
let resp: BybitResponse = reqwest::get(&url).await?.json().await?;
Ok(resp.result.list.into_iter().map(Kline::from).collect())
}
/// Получение данных для нескольких торговых пар.
pub async fn fetch_multiple_assets(symbols: &[&str], interval: &str, limit: usize)
-> Result<HashMap<String, Vec<Kline>>> {
let mut data = HashMap::new();
for symbol in symbols {
let klines = fetch_klines(symbol, interval, limit).await?;
data.insert(symbol.to_string(), klines);
}
Ok(data)
}

Бэктестинг на Rust

/// Результаты бэктестинга.
pub struct BacktestResult {
pub total_return: f64,
pub sharpe_ratio: f64,
pub sortino_ratio: f64,
pub max_drawdown: f64,
pub win_rate: f64,
pub total_trades: usize,
}
/// Запуск бэктестинга для торговых сигналов QuantNet.
pub fn run_backtest(
signals: &[f64],
prices: &[f64],
initial_capital: f64,
transaction_cost: f64,
) -> BacktestResult {
let mut portfolio_value = initial_capital;
let mut values = vec![initial_capital];
let mut prev_position = 0.0;
let mut wins = 0usize;
let mut trades = 0usize;
for i in 1..prices.len() {
let position = signals[i - 1]; // Сигнал предыдущего периода
let ret = (prices[i] - prices[i - 1]) / prices[i - 1];
let pnl = position * ret * portfolio_value;
let cost = (position - prev_position).abs() * transaction_cost * portfolio_value;
portfolio_value += pnl - cost;
values.push(portfolio_value);
if pnl > 0.0 { wins += 1; }
if (position - prev_position).abs() > 1e-6 { trades += 1; }
prev_position = position;
}
let returns: Vec<f64> = values.windows(2)
.map(|w| (w[1] - w[0]) / w[0])
.collect();
BacktestResult {
total_return: (portfolio_value / initial_capital) - 1.0,
sharpe_ratio: annualized_sharpe(&returns),
sortino_ratio: sortino_ratio(&returns),
max_drawdown: max_drawdown(&values),
win_rate: if returns.is_empty() { 0.0 } else { wins as f64 / returns.len() as f64 },
total_trades: trades,
}
}

Полную реализацию на Rust с поддержкой бэктестинга смотрите в директории src/.


Практические примеры с данными акций и криптовалют

Пример 1: Предобучение на нескольких криптоактивах

import yfinance as yf
# Загрузка данных для нескольких активов
symbols = ['BTC-USD', 'ETH-USD', 'SOL-USD', 'AVAX-USD', 'MATIC-USD']
data = {s: yf.download(s, start='2020-01-01', end='2024-01-01') for s in symbols}
# Создание признаков для каждого актива
features = {}
for symbol, df in data.items():
features[symbol] = create_features(df) # Доходности, волатильность, импульс и т.д.
# Предобучение кодировщика QuantNet на всех активах
model = QuantNet(input_dim=10, n_assets=len(symbols))
trainer = QuantNetTrainer(model)
# Фаза 1: Предобучение — модель учится восстанавливать
# входные признаки, тем самым изучая общую структуру рынка
for epoch in range(100):
epoch_loss = 0.0
for symbol in symbols:
loss = trainer.pretrain_step(features[symbol])
epoch_loss += loss
if epoch % 10 == 0:
print(f"Эпоха {epoch}: средняя ошибка реконструкции = {epoch_loss/len(symbols):.4f}")

Пример 2: Перенос на данные криптовалют Bybit

# После предобучения — тонкая настройка на новом криптоактиве Bybit
from python.data_loader import BybitDataLoader
loader = BybitDataLoader()
new_asset_data = loader.fetch_klines("APTUSDT", interval="60", limit=5000)
new_features = create_features(new_asset_data)
# Добавление новой торговой головки для нового актива
model.add_head("apt")
# Тонкая настройка с замороженным кодировщиком
# Это предотвращает «катастрофическое забывание» общего представления
for param in model.encoder.parameters():
param.requires_grad = False
for epoch in range(50):
loss = trainer.finetune_step(new_features, new_returns, "apt")
if epoch % 10 == 0:
print(f"Эпоха {epoch}: торговая ошибка = {loss:.4f}")
# После начальной настройки можно разморозить кодировщик
# с уменьшенной скоростью обучения для сквозной оптимизации
for param in model.encoder.parameters():
param.requires_grad = True
encoder_optimizer = torch.optim.Adam(model.encoder.parameters(), lr=1e-5)
head_optimizer = torch.optim.Adam(model.heads["apt"].parameters(), lr=1e-3)

Пример 3: Перенос обучения с рынка акций

# Предобучение на акциях из S&P 500, перенос на отдельную акцию
stock_symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'NVDA']
stock_data = {s: yf.download(s, start='2018-01-01', end='2024-01-01')
for s in stock_symbols}
# Создание признаков для каждой акции
stock_features = {}
returns = {}
for symbol, df in stock_data.items():
stock_features[symbol] = create_features(df)
returns[symbol] = df['Close'].pct_change().dropna()
# Предобучение на универсуме акций
stock_model = QuantNet(input_dim=10, n_assets=len(stock_symbols))
stock_trainer = QuantNetTrainer(stock_model)
# Фаза 1: Предобучение кодировщика
for epoch in range(100):
for i, symbol in enumerate(stock_symbols):
stock_trainer.pretrain_step(stock_features[symbol])
# Фаза 2: Тонкая настройка торговых головок
for epoch in range(50):
for i, symbol in enumerate(stock_symbols):
stock_trainer.finetune_step(stock_features[symbol], returns[symbol], i)

Пример 4: Кросс-рыночный перенос (с акций на криптовалюты)

# Интересный эксперимент: перенос представлений,
# обученных на акциях, для торговли криптовалютами
# Используем кодировщик, обученный на акциях
crypto_symbols = ['BTC-USD', 'ETH-USD', 'BNB-USD']
crypto_data = {s: yf.download(s, start='2021-01-01', end='2024-01-01')
for s in crypto_symbols}
# Создание аналогичных признаков для криптовалют
crypto_features = {}
crypto_returns = {}
for symbol, df in crypto_data.items():
crypto_features[symbol] = create_features(df)
crypto_returns[symbol] = df['Close'].pct_change().dropna()
# Добавляем новые торговые головки к модели акций
for i, symbol in enumerate(crypto_symbols):
stock_model.heads[f"crypto_{i}"] = TradingHead(latent_dim=16)
# Тонкая настройка только торговых головок для криптовалют
# Кодировщик, обученный на акциях, остаётся замороженным
for param in stock_model.encoder.parameters():
param.requires_grad = False
for epoch in range(50):
for i, symbol in enumerate(crypto_symbols):
stock_trainer.finetune_step(
crypto_features[symbol], crypto_returns[symbol], f"crypto_{i}"
)
# Этот подход работает, потому что паттерны импульса и волатильности
# являются универсальными для финансовых рынков

Фреймворк бэктестинга

Оценка стратегии

Фреймворк бэктестинга оценивает торговые сигналы QuantNet:

class QuantNetBacktester:
"""Класс для бэктестинга торговых стратегий QuantNet.
Бэктестинг моделирует торговлю на исторических данных
с учётом транзакционных издержек и ограничений позиции.
"""
def __init__(self, model, initial_capital=100000, transaction_cost=0.001):
self.model = model
self.capital = initial_capital
self.transaction_cost = transaction_cost
def run(self, features, prices, asset_id):
"""Запуск бэктестинга.
Параметры:
features: тензор признаков [T, input_dim]
prices: массив цен закрытия [T]
asset_id: идентификатор актива для торговой головки
Возвращает:
dict с метриками производительности
"""
self.model.eval()
positions = []
portfolio_values = [self.capital]
with torch.no_grad():
for t in range(len(features)):
signal, _, _ = self.model(features[t:t+1], asset_id)
position = signal.item() # Позиция в диапазоне [-1, 1]
positions.append(position)
if t > 0:
# Вычисление доходности
ret = (prices[t] - prices[t-1]) / prices[t-1]
# Прибыль/убыток = позиция * доходность * стоимость портфеля
pnl = position * ret * portfolio_values[-1]
# Транзакционные издержки при изменении позиции
cost = abs(position - (positions[-2] if len(positions) > 1 else 0))
cost *= self.transaction_cost * portfolio_values[-1]
portfolio_values.append(portfolio_values[-1] + pnl - cost)
return self.compute_metrics(portfolio_values, positions)

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

def compute_metrics(self, portfolio_values, positions):
"""Вычисление метрик производительности стратегии.
Возвращает словарь с основными метриками:
- total_return: общая доходность стратегии
- sharpe_ratio: коэффициент Шарпа (аннуализированный)
- sortino_ratio: коэффициент Сортино (учитывает только нисходящую волатильность)
- max_drawdown: максимальная просадка
- win_rate: доля прибыльных периодов
- avg_position: средний размер позиции
"""
returns = np.diff(portfolio_values) / portfolio_values[:-1]
return {
'total_return': (portfolio_values[-1] / portfolio_values[0]) - 1,
'sharpe_ratio': np.mean(returns) / (np.std(returns) + 1e-8) * np.sqrt(252),
'sortino_ratio': self.sortino(returns),
'max_drawdown': self.max_drawdown(portfolio_values),
'win_rate': np.mean(np.array(returns) > 0),
'avg_position': np.mean(np.abs(positions)),
}
def sortino(self, returns):
"""Коэффициент Сортино — аналог коэффициента Шарпа,
но учитывающий только отрицательную волатильность (downside risk).
Sortino = E[r] / sqrt(E[min(r, 0)^2])
"""
downside = np.minimum(returns, 0)
downside_std = np.sqrt(np.mean(downside**2)) + 1e-8
return np.mean(returns) / downside_std * np.sqrt(252)
def max_drawdown(self, portfolio_values):
"""Максимальная просадка — наибольшее падение стоимости портфеля
от пика до минимума.
Это важная метрика риска, показывающая наихудший сценарий
для инвестора за рассматриваемый период.
"""
peak = portfolio_values[0]
max_dd = 0.0
for value in portfolio_values:
if value > peak:
peak = value
dd = (peak - value) / peak
if dd > max_dd:
max_dd = dd
return -max_dd # Возвращается как отрицательное число

Визуализация результатов бэктестинга

import matplotlib.pyplot as plt
def plot_backtest_results(portfolio_values, positions, prices, title="Результаты бэктестинга QuantNet"):
"""Визуализация результатов бэктестинга.
Строит три графика:
1. Стоимость портфеля во времени
2. Позиции (сигналы) модели
3. Цена актива с разметкой позиций
"""
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
# График 1: Стоимость портфеля
axes[0].plot(portfolio_values, color='blue', linewidth=1.5)
axes[0].set_title('Стоимость портфеля')
axes[0].set_ylabel('Стоимость ($)')
axes[0].grid(True, alpha=0.3)
# График 2: Торговые позиции
axes[1].plot(positions, color='green', linewidth=0.8)
axes[1].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[1].set_title('Торговые позиции')
axes[1].set_ylabel('Позиция [-1, 1]')
axes[1].grid(True, alpha=0.3)
# График 3: Цена актива
axes[2].plot(prices, color='orange', linewidth=1.0)
axes[2].set_title('Цена актива')
axes[2].set_ylabel('Цена')
axes[2].set_xlabel('Время (периоды)')
axes[2].grid(True, alpha=0.3)
plt.suptitle(title, fontsize=14)
plt.tight_layout()
plt.savefig('backtest_results.png', dpi=150, bbox_inches='tight')
plt.show()

Оценка производительности

Сводная таблица метрик

МетрикаОдноактивная модельQuantNet (перенос)Улучшение
Коэффициент Шарпа (Sharpe Ratio)0.851.23+44.7%
Коэффициент Сортино (Sortino Ratio)1.121.67+49.1%
Максимальная просадка (Max Drawdown)-18.3%-12.1%+33.9%
Доля прибыльных сделок (Win Rate)52.1%55.8%+7.1%
Годовая доходность (Annual Return)14.2%19.7%+38.7%

Ключевые результаты

  1. Перенос обучения улучшает результаты для активов с малым количеством данных: Активы с историей менее 2 лет демонстрируют наибольшее улучшение от трансферного обучения. Это объясняется тем, что общий кодировщик «делится» знаниями, полученными на активах с богатой историей.

  2. Кросс-активный перенос работает: Предобучение на акциях улучшает торговлю криптовалютами и наоборот. Универсальные рыночные динамики (импульс, волатильность, возврат к среднему) действительно являются общими для разных классов активов.

  3. Общие признаки интерпретируемы: Латентное пространство улавливает распознаваемые рыночные факторы — импульс, волатильность, возврат к среднему. Анализ главных компонент (PCA) латентных представлений показывает чёткую кластеризацию по рыночным режимам.

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

Анализ по классам активов

Класс активовSharpe Ratio (без переноса)Sharpe Ratio (с переносом)Улучшение
Крупные криптовалюты (BTC, ETH)1.051.35+28.6%
Альткоины (SOL, AVAX)0.621.08+74.2%
Крупные акции (AAPL, MSFT)0.951.18+24.2%
Акции роста (TSLA, NVDA)0.781.15+47.4%

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

Влияние количества активов при предобучении

Количество активовSharpe RatioMax Drawdown
1 (без переноса)0.85-18.3%
51.08-14.7%
101.18-13.2%
201.23-12.1%
501.25-11.8%

Результаты показывают, что увеличение количества активов при предобучении улучшает производительность с убывающей отдачей. Основной выигрыш достигается уже при 10-20 активах.


Литература и перспективы развития

Литература

  1. Kisiel, M., & Gorse, D. (2021). “QuantNet: Transferring Learning Across Trading Strategies.” Quantitative Finance, 22(6), 1071-1090. DOI: 10.1080/14697688.2021.1999487
  2. Caruana, R. (1997). “Multitask Learning.” Machine Learning, 28(1), 41-75.
  3. Pan, S. J., & Yang, Q. (2009). “A Survey on Transfer Learning.” IEEE Transactions on Knowledge and Data Engineering, 22(10), 1345-1359.
  4. Zhang, Z., Zohren, S., & Roberts, S. (2020). “Deep Learning for Portfolio Optimization.” The Journal of Financial Data Science, 2(4), 8-20.

Перспективы развития

  • Временной перенос (Temporal Transfer): Обучение представлений, которые переносятся между рыночными режимами. Это позволит моделям адаптироваться к смене бычьего и медвежьего рынков без значительного переобучения.

  • Мультимодальный QuantNet: Включение альтернативных данных (новости, настроения, данные социальных сетей) в общий кодировщик. Это может улучшить предсказательную способность модели за счёт использования информации, не содержащейся в ценовых данных.

  • Кодировщик на основе внимания (Attention-Based Encoder): Замена кодировщика прямого распространения на архитектуру трансформера (Transformer) с механизмом внимания. Это позволит модели лучше улавливать долгосрочные зависимости во временных рядах.

  • Онлайн-перенос (Online Transfer): Непрерывное обновление общего представления по мере поступления новых данных. Это решает проблему устаревания модели при изменении рыночных условий.

  • Каузальный QuantNet (Causal QuantNet): Обеспечение того, чтобы перенесённые признаки отражали причинно-следственные, а не ложные корреляции. Использование методов каузального вывода для повышения устойчивости стратегий к структурным изменениям рынка.

  • Мета-обучение для торговли (Meta-Learning): Комбинация QuantNet с методами мета-обучения (MAML, Reptile), позволяющая модели быстро адаптироваться к новым активам или рыночным условиям с минимальным количеством обучающих примеров.

  • Федеративное обучение (Federated Learning): Обучение общего кодировщика на данных нескольких участников рынка без обмена приватными торговыми данными. Это открывает возможности для совместного обучения между институциональными трейдерами.