Глава 7: Линейные методы для предсказания доходности криптовалют и декомпозиции рисков
Обзор
Линейные модели остаются одними из наиболее мощных инструментов финансового анализа, несмотря на развитие сложных методов машинного обучения. Их интерпретируемость, вычислительная эффективность и хорошо изученные статистические свойства делают их незаменимыми для предсказания доходности криптовалют и декомпозиции рисков. Регрессия методом наименьших квадратов (OLS) предоставляет основу для факторных моделей, которые разлагают доходность криптоактивов на систематические рисковые экспозиции и идиосинкратические компоненты, позволяя портфельным менеджерам понять, что движет доходностью, и хеджировать нежелательные экспозиции.
Применение линейных методов к криптовалютным рынкам требует решения нескольких уникальных для цифровых активов задач. Криптофакторные модели должны включать новые факторы помимо традиционных рыночного, размерного и моментумного: ончейн-активность (активные адреса, объём транзакций), сетевые эффекты и метрики, специфичные для токеномики. Кросс-секционная регрессия по вселенной альткоинов позволяет оценить премии за риск для этих факторов, расширяя методологию Фамы-Макбета на криптосферу. Однако высокая размерность и мультиколлинеарность криптопризнаков требуют регуляризации через Ridge (L2), Lasso (L1) и Elastic Net подходы.
Эта глава охватывает полный спектр линейных методов, адаптированных для криптотрейдинга: от OLS-регрессии на криптофакторах через регуляризованные методы отбора признаков до логистической регрессии для бинарного предсказания направления. Анализ скользящей регрессии показывает, как факторные нагрузки сдвигаются между рыночными режимами, предоставляя сигналы раннего предупреждения о разрушении корреляций. Предоставлены реализации на Python и Rust с практическими примерами использования рыночных данных Bybit и yfinance для дополнительных источников данных.
Содержание
- Введение в линейные методы для крипто
- Математические основы
- Сравнение линейных методов
- Торговые применения
- Реализация на Python
- Реализация на Rust
- Практические примеры
- Фреймворк бэктестирования
- Оценка производительности
- Перспективные направления
Раздел 1: Введение в линейные методы для крипто
Почему линейные модели для крипто?
Несмотря на нелинейную динамику криптовалютных рынков, линейные модели предлагают критические преимущества:
- Интерпретируемость: Коэффициенты напрямую представляют факторные экспозиции и предельные эффекты
- Статистический вывод: Стандартные ошибки, доверительные интервалы и тесты гипотез хорошо определены
- Вычислительная скорость: Обучение и предсказание на порядки быстрее глубокого обучения
- Теория регуляризации: L1/L2 штрафы имеют чёткие байесовские интерпретации и доказанную сходимость
- Базовая производительность: Линейные модели часто превосходят сложные модели на зашумлённых финансовых данных
На крипторынках с крайне низким соотношением сигнал/шум компромисс смещение-дисперсия благоприятствует более простым моделям. Ridge-регрессия с 20 признаками часто превосходит нейронную сеть с теми же признаками, потому что сеть переобучается на шуме, тогда как Ridge сжимает коэффициенты к нулю.
Факторные модели для цифровых активов
Модель оценки капитальных активов (CAPM) предоставляет простейшую факторную модель:
R_i - R_f = alpha_i + beta_i * (R_market - R_f) + epsilon_iДля крипто «рынок» — это обычно BTC или индекс криптовалют, взвешенный по капитализации. Коэффициент бета измеряет экспозицию к систематическому риску, тогда как альфа фиксирует избыточную доходность, не объяснённую движениями рынка.
Многофакторная криптомодель расширяет это:
R_i = alpha + beta_mkt * MKT + beta_size * SIZE + beta_mom * MOM + beta_chain * CHAIN + epsilonГде SIZE — фактор малых минус больших, MOM — моментум, а CHAIN — фактор ончейн-активности.
Теорема Гаусса-Маркова и её ограничения
Теорема Гаусса-Маркова утверждает, что при классических предположениях (линейность, экзогенность, гомоскедастичность, отсутствие серийной корреляции, отсутствие совершенной мультиколлинеарности) OLS является наилучшей линейной несмещённой оценкой (BLUE). На крипторынках практически все эти предположения нарушаются:
- Гетероскедастичность: Кластеризация волатильности крипто (эффекты GARCH)
- Серийная корреляция: Признаки на основе перекрывающихся окон
- Ненормальность: Экстремальный эксцесс в распределениях доходности
- Мультиколлинеарность: Многие криптопризнаки высоко коррелированы
Эти нарушения не делают OLS бесполезным, но требуют робастных стандартных ошибок (HAC-оценки) и регуляризации.
Раздел 2: Математические основы
Метод наименьших квадратов (OLS)
Для линейной модели y = X * beta + epsilon OLS-оценка минимизирует сумму квадратов остатков:
beta_hat = argmin ||y - X * beta||^2 = (X^T * X)^{-1} * X^T * y
Дисперсия: Var(beta_hat) = sigma^2 * (X^T * X)^{-1} где sigma^2 = ||y - X * beta_hat||^2 / (n - p)Ridge-регрессия (L2 регуляризация)
Ridge добавляет L2 штраф для предотвращения взрыва коэффициентов:
beta_ridge = argmin { ||y - X * beta||^2 + lambda * ||beta||^2 } = (X^T * X + lambda * I)^{-1} * X^T * y
Свойства:- Сжимает все коэффициенты к нулю (никогда точно до нуля)- Обрабатывает мультиколлинеарность стабилизацией (X^T X + lambda I)- Байесовская интерпретация: Гауссов априор на коэффициентах- Эффективные степени свободы: df(lambda) = tr(X(X^T X + lambda I)^{-1} X^T)Lasso-регрессия (L1 регуляризация)
Lasso использует L1 штраф, индуцирующий разреженность:
beta_lasso = argmin { ||y - X * beta||^2 + lambda * ||beta||_1 }
Свойства:- Может сжимать коэффициенты точно до нуля (отбор признаков)- Выбирает максимум n признаков когда p > n- Байесовская интерпретация: Лапласов априор на коэффициентах- Нет решения в замкнутой форме; требует координатного спуска или LARSElastic Net
Elastic Net комбинирует L1 и L2 штрафы:
beta_enet = argmin { ||y - X * beta||^2 + lambda_1 * ||beta||_1 + lambda_2 * ||beta||^2 }
Параметр смешивания: alpha = lambda_1 / (lambda_1 + lambda_2)- alpha = 1: чистый Lasso- alpha = 0: чистый Ridge- 0 < alpha < 1: гибридЛогистическая регрессия для предсказания направления
Для бинарной классификации (рост/падение):
P(y=1|x) = sigma(x^T * beta) = 1 / (1 + exp(-x^T * beta))
Функция потерь: L = -sum[ y_i * log(p_i) + (1-y_i) * log(1-p_i) ] + lambda * penalty(beta) (Ridge, Lasso или Elastic Net)Регрессия Фамы-Макбета
Двухпроходная регрессия для оценки премий за риск:
Проход 1 (Временной ряд): Для каждого актива i оценить факторные нагрузки: R_it = alpha_i + beta_i^T * F_t + epsilon_it
Проход 2 (Кросс-секция): В каждый момент t оценить премии за риск: R_it = gamma_0t + gamma_t^T * beta_hat_i + eta_it
Оценки премий за риск: gamma_bar = (1/T) * sum_t gamma_t t-stat = gamma_bar / (se(gamma_t) / sqrt(T))Скользящая регрессия для определения режима
Для каждого окна [t-w, t]: beta_t = (X_window^T * X_window)^{-1} * X_window^T * y_window
Мониторинг:- Стабильность beta: большие изменения сигнализируют о смене режима- Эволюция R^2: снижающийся R^2 означает потерю объясняющей силы модели- Паттерны остатков: автокорреляция в остатках указывает на неспецифицированность моделиРаздел 3: Сравнение линейных методов
| Метод | Разреженность | Обработка мультиколлинеарности | Интерпретируемость | Отбор признаков | Вычислительная стоимость |
|---|---|---|---|---|---|
| OLS | Нет | Плохая | Высокая | Нет | Очень низкая |
| Ridge (L2) | Нет | Отличная | Высокая | Нет | Низкая |
| Lasso (L1) | Да | Умеренная | Высокая | Да | Низкая |
| Elastic Net | Да | Хорошая | Высокая | Да | Низкая |
| Логистическая регрессия | Опционально | Хорошая (с регуляризацией) | Высокая | С L1 | Низкая |
| Скользящий OLS | Нет | Плохая | Высокая | Нет | Средняя |
| Фама-Макбет | Нет | Умеренная | Высокая | Нет | Средняя |
| Криптофактор | Описание | Типичная нагрузка (BTC) | Типичная нагрузка (ALT) | Значимость |
|---|---|---|---|---|
| Рынок (MKT) | Избыточная доходность BTC | 1.00 (по определ.) | 0.8 - 1.5 | Очень высокая |
| Размер (SMB) | Малые минус большие по капитализации | -0.05 | 0.3 - 0.8 | Умеренная |
| Моментум (MOM) | Победители минус проигравшие | 0.02 | 0.1 - 0.4 | Умеренная |
| Ончейн (CHAIN) | Фактор активных адресов | 0.15 | 0.2 - 0.6 | Низко-умеренная |
| Волатильность (VOL) | Низкая минус высокая волатильность | -0.10 | -0.2 - 0.3 | Низкая |
| Финансирование (FUND) | Фактор ставки финансирования | -0.08 | -0.1 - 0.2 | Низкая |
Раздел 4: Торговые применения
4.1 Построение криптофакторной модели
Построение многофакторной модели для кросс-секции криптовалют:
- Рыночный фактор: Избыточная доходность BTC сверх безрисковой ставки
- Фактор размера: Разница доходностей между токенами малой и большой капитализации (отсортированных по рыночной капитализации)
- Фактор моментума: Разница доходностей между недавними победителями и проигравшими (30-дневные доходности)
- Ончейн-фактор: Разница доходностей между токенами с высокой и низкой ончейн-активностью
- Фактор финансирования: Средневзвешенный дифференциал ставки финансирования
Эта криптофакторная модель позволяет декомпозировать риск: «Какая часть доходности SOL обусловлена рыночной бетой vs. собственным моментумом vs. ончейн-ростом?»
4.2 Кросс-секционная регрессия для премий за риск
В каждый временной период регрессируем доходности активов на их оценённые факторные нагрузки для получения премий за риск. Для вселенной из 50 альткоинов:
- Оценить беты для каждого альткоина с использованием 90-дневных скользящих окон
- Провести ежемесячные кросс-секционные регрессии
- Усреднить полученные коэффициенты гамма для оценки премий за риск
- Проверить значимость с использованием стандартных ошибок, скорректированных по Ньюи-Уэсту
4.3 Lasso для отбора криптопризнаков
При наличии десятков потенциальных предикторов (технические индикаторы, ончейн-метрики, признаки потока ордеров) Lasso выбирает наиболее релевантные:
- Начать с 50+ кандидатных признаков
- Путь Lasso: варьировать lambda от высокой (все нули) до низкой (все признаки)
- Кросс-валидация для нахождения оптимальной lambda
- Отобранные признаки обычно включают: ставку финансирования, дисбаланс объёма, корреляцию с BTC и режим волатильности
4.4 Логистическая регрессия для предсказания направления
Бинарное предсказание направления доходности следующего периода с использованием регуляризованной логистической регрессии:
- Признаки: моментум, соотношение волатильностей, профиль объёма, ставка финансирования
- Регуляризация предотвращает переобучение на шуме
- Предсказанные вероятности могут использоваться для определения размера позиции
- Калиброванные вероятности улучшают определение размера по критерию Келли
4.5 Скользящая регрессия для определения режима
Отслеживание факторных нагрузок во времени для обнаружения смены режимов:
- 30-дневное скользящее окно для оценки бета
- Мониторинг beta_market: рост бета указывает на увеличение систематического риска
- Мониторинг R^2: снижающийся R^2 предполагает структурный разрыв
- Мониторинг альфа: устойчивая положительная альфа может указывать на неправильное ценообразование
Раздел 5: Реализация на Python
Криптофакторная модель
import numpy as npimport pandas as pdfrom sklearn.linear_model import ( LinearRegression, Ridge, Lasso, ElasticNet, LogisticRegression)from sklearn.preprocessing import StandardScalerfrom sklearn.metrics import accuracy_score, r2_scoreimport requestsimport yfinance as yffrom typing import Dict, List, Tuple, Optional
class CryptoFactorModel: """Многофакторная модель для доходностей криптоактивов."""
def __init__(self, universe: List[str], market_symbol: str = "BTCUSDT"): self.universe = universe self.market_symbol = market_symbol self.returns = None self.factors = None self.betas = None
def fetch_bybit_returns(self, symbol: str, interval: str = "D", limit: int = 200) -> pd.Series: """Получение дневных доходностей с Bybit.""" url = "https://api.bybit.com/v5/market/kline" params = { "category": "linear", "symbol": symbol, "interval": interval, "limit": limit } response = requests.get(url, params=params) data = response.json()["result"]["list"] df = pd.DataFrame(data, columns=[ "timestamp", "open", "high", "low", "close", "volume", "turnover" ]) df["close"] = df["close"].astype(float) df["timestamp"] = pd.to_datetime(df["timestamp"].astype(int), unit="ms") df = df.sort_values("timestamp").set_index("timestamp") return df["close"].pct_change().dropna()
def construct_factors(self) -> pd.DataFrame: """Построение факторных доходностей для крипто.""" all_returns = {} for sym in self.universe + [self.market_symbol]: all_returns[sym] = self.fetch_bybit_returns(sym) self.returns = pd.DataFrame(all_returns).dropna()
factors = pd.DataFrame(index=self.returns.index)
# Рыночный фактор: избыточная доходность BTC factors["MKT"] = self.returns[self.market_symbol]
# Фактор размера: прокси через волатильность (низкая вол ~ большая капитализация) vols = self.returns[self.universe].rolling(30).std() median_vol = vols.median(axis=1) small = self.returns[self.universe].where(vols > median_vol.values[:, None]) big = self.returns[self.universe].where(vols <= median_vol.values[:, None]) factors["SIZE"] = small.mean(axis=1) - big.mean(axis=1)
# Фактор моментума: 30-дневный моментум mom_30d = self.returns[self.universe].rolling(30).sum() median_mom = mom_30d.median(axis=1) winners = self.returns[self.universe].where( mom_30d > median_mom.values[:, None]) losers = self.returns[self.universe].where( mom_30d <= median_mom.values[:, None]) factors["MOM"] = winners.mean(axis=1) - losers.mean(axis=1)
self.factors = factors.dropna() return self.factors
def estimate_betas(self, window: int = 90) -> Dict[str, pd.DataFrame]: """Оценка скользящих факторных нагрузок для каждого актива.""" self.betas = {} common_idx = self.returns.index.intersection(self.factors.index) factors_aligned = self.factors.loc[common_idx]
for sym in self.universe: returns_aligned = self.returns[sym].loc[common_idx] betas_list = []
for i in range(window, len(common_idx)): y = returns_aligned.iloc[i - window:i].values X = factors_aligned.iloc[i - window:i].values
model = LinearRegression() model.fit(X, y)
betas_list.append({ "timestamp": common_idx[i], "alpha": model.intercept_, **{f"beta_{col}": coef for col, coef in zip(self.factors.columns, model.coef_)}, "r_squared": model.score(X, y) })
self.betas[sym] = pd.DataFrame(betas_list).set_index("timestamp")
return self.betas
def fama_macbeth(self) -> pd.DataFrame: """Кросс-секционная регрессия Фамы-Макбета.""" common_idx = self.returns.index.intersection(self.factors.index) if self.betas is None: self.estimate_betas()
# Сбор оценок бета в каждый момент времени gammas = [] beta_cols = [c for c in list(self.betas.values())[0].columns if c.startswith("beta_")]
for t in common_idx: cross_section_returns = [] cross_section_betas = []
for sym in self.universe: if t in self.betas[sym].index and t in self.returns.index: cross_section_returns.append(self.returns[sym].loc[t]) cross_section_betas.append( self.betas[sym].loc[t][beta_cols].values)
if len(cross_section_returns) < 3: continue
y = np.array(cross_section_returns) X = np.array(cross_section_betas) X = np.column_stack([np.ones(len(y)), X])
try: model = LinearRegression(fit_intercept=False) model.fit(X, y) gammas.append({ "timestamp": t, "gamma_0": model.coef_[0], **{f"gamma_{col.replace('beta_', '')}": coef for col, coef in zip(beta_cols, model.coef_[1:])} }) except Exception: continue
result = pd.DataFrame(gammas).set_index("timestamp") # Вычисление премий за риск и t-статистик summary = pd.DataFrame({ "mean": result.mean(), "std": result.std(), "t_stat": result.mean() / (result.std() / np.sqrt(len(result))), "annualized": result.mean() * 365 }) return summary
class RegularizedCryptoRegression: """Ridge, Lasso и Elastic Net для предсказания криптовалют."""
def __init__(self): self.scaler = StandardScaler() self.model = None
def fit_ridge(self, X: pd.DataFrame, y: pd.Series, alpha: float = 1.0) -> 'RegularizedCryptoRegression': X_scaled = self.scaler.fit_transform(X) self.model = Ridge(alpha=alpha) self.model.fit(X_scaled, y) return self
def fit_lasso(self, X: pd.DataFrame, y: pd.Series, alpha: float = 0.01) -> 'RegularizedCryptoRegression': X_scaled = self.scaler.fit_transform(X) self.model = Lasso(alpha=alpha, max_iter=10000) self.model.fit(X_scaled, y) return self
def fit_elastic_net(self, X: pd.DataFrame, y: pd.Series, alpha: float = 0.01, l1_ratio: float = 0.5) -> 'RegularizedCryptoRegression': X_scaled = self.scaler.fit_transform(X) self.model = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, max_iter=10000) self.model.fit(X_scaled, y) return self
def predict(self, X: pd.DataFrame) -> np.ndarray: X_scaled = self.scaler.transform(X) return self.model.predict(X_scaled)
def feature_importance(self, feature_names: List[str]) -> pd.Series: return pd.Series( np.abs(self.model.coef_), index=feature_names ).sort_values(ascending=False)
def selected_features(self, feature_names: List[str]) -> List[str]: """Для Lasso/ElasticNet: возврат ненулевых признаков.""" mask = np.abs(self.model.coef_) > 1e-10 return [f for f, m in zip(feature_names, mask) if m]
class CryptoLogisticModel: """Логистическая регрессия для предсказания направления криптовалют."""
def __init__(self, penalty: str = "l1", C: float = 1.0): self.scaler = StandardScaler() self.model = LogisticRegression( penalty=penalty, C=C, solver="saga", max_iter=5000 )
def fit(self, X: pd.DataFrame, y: pd.Series) -> 'CryptoLogisticModel': X_scaled = self.scaler.fit_transform(X) self.model.fit(X_scaled, y) return self
def predict_proba(self, X: pd.DataFrame) -> np.ndarray: X_scaled = self.scaler.transform(X) return self.model.predict_proba(X_scaled)[:, 1]
def predict(self, X: pd.DataFrame) -> np.ndarray: X_scaled = self.scaler.transform(X) return self.model.predict(X_scaled)
def coefficient_summary(self, feature_names: List[str]) -> pd.DataFrame: return pd.DataFrame({ "feature": feature_names, "coefficient": self.model.coef_[0], "abs_coefficient": np.abs(self.model.coef_[0]), "odds_ratio": np.exp(self.model.coef_[0]) }).sort_values("abs_coefficient", ascending=False)Пример использования
# Построение криптофакторной моделиfactor_model = CryptoFactorModel( universe=["ETHUSDT", "SOLUSDT", "AVAXUSDT", "LINKUSDT", "DOTUSDT", "MATICUSDT", "AAVEUSDT", "UNIUSDT"], market_symbol="BTCUSDT")factor_model.construct_factors()betas = factor_model.estimate_betas(window=60)
# Премии за риск Фамы-Макбетаrisk_premia = factor_model.fama_macbeth()print("Оценки премий за риск:")print(risk_premia)
# Отбор признаков Lassoregressor = RegularizedCryptoRegression()# ... (с подготовленными признаками и целями)Раздел 6: Реализация на Rust
Структура проекта
ch07_linear_methods_crypto/├── Cargo.toml├── src/│ ├── lib.rs│ ├── regression/│ │ ├── mod.rs│ │ ├── ols.rs│ │ └── regularized.rs│ ├── classification/│ │ ├── mod.rs│ │ └── logistic.rs│ └── factor/│ ├── mod.rs│ └── model.rs└── examples/ ├── crypto_factor_model.rs ├── lasso_selection.rs └── rolling_regression.rsОсновная библиотека (src/lib.rs)
pub mod regression;pub mod classification;pub mod factor;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]pub struct RegressionResult { pub coefficients: Vec<f64>, pub intercept: f64, pub r_squared: f64, pub residuals: Vec<f64>, pub feature_names: Vec<String>,}
impl RegressionResult { pub fn display(&self) { println!("Результаты регрессии (R² = {:.4}):", self.r_squared); println!(" Константа: {:.6}", self.intercept); for (name, coef) in self.feature_names.iter().zip(self.coefficients.iter()) { println!(" {}: {:.6}", name, coef); } }}
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct FactorLoading { pub timestamp: i64, pub alpha: f64, pub betas: Vec<f64>, pub r_squared: f64,}OLS-регрессия (src/regression/ols.rs)
use crate::RegressionResult;
pub struct OLSRegression;
impl OLSRegression { /// Решение OLS: beta = (X^T X)^{-1} X^T y pub fn fit( x: &[Vec<f64>], y: &[f64], feature_names: &[String], fit_intercept: bool, ) -> RegressionResult { let n = y.len(); let p = x[0].len(); let p_total = if fit_intercept { p + 1 } else { p };
// Построение матрицы плана let mut xtx = vec![vec![0.0; p_total]; p_total]; let mut xty = vec![0.0; p_total];
for i in 0..n { let row = Self::build_row(&x[i], fit_intercept); for j in 0..p_total { xty[j] += row[j] * y[i]; for k in 0..=j { xtx[j][k] += row[j] * row[k]; if j != k { xtx[k][j] = xtx[j][k]; } } } }
// Решение методом исключения Гаусса let beta = Self::solve_linear_system(&xtx, &xty);
// Вычисление предсказаний и R² let mut ss_res = 0.0; let mut ss_tot = 0.0; let y_mean = y.iter().sum::<f64>() / n as f64; let mut residuals = Vec::with_capacity(n);
for i in 0..n { let row = Self::build_row(&x[i], fit_intercept); let y_pred: f64 = row.iter().zip(beta.iter()).map(|(a, b)| a * b).sum(); let res = y[i] - y_pred; residuals.push(res); ss_res += res * res; ss_tot += (y[i] - y_mean) * (y[i] - y_mean); }
let r_squared = if ss_tot > 0.0 { 1.0 - ss_res / ss_tot } else { 0.0 };
let (intercept, coefficients) = if fit_intercept { (beta[0], beta[1..].to_vec()) } else { (0.0, beta) };
RegressionResult { coefficients, intercept, r_squared, residuals, feature_names: feature_names.to_vec(), } }
fn build_row(x: &[f64], fit_intercept: bool) -> Vec<f64> { if fit_intercept { let mut row = vec![1.0]; row.extend_from_slice(x); row } else { x.to_vec() } }
fn solve_linear_system(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> { let n = b.len(); let mut aug = vec![vec![0.0; n + 1]; n]; for i in 0..n { for j in 0..n { aug[i][j] = a[i][j]; } aug[i][n] = b[i]; }
// Исключение Гаусса с частичным выбором ведущего элемента for i in 0..n { let mut max_row = i; for k in (i + 1)..n { if aug[k][i].abs() > aug[max_row][i].abs() { max_row = k; } } aug.swap(i, max_row);
let pivot = aug[i][i]; if pivot.abs() < 1e-12 { continue; }
for j in i..=n { aug[i][j] /= pivot; }
for k in 0..n { if k != i { let factor = aug[k][i]; for j in i..=n { aug[k][j] -= factor * aug[i][j]; } } } }
(0..n).map(|i| aug[i][n]).collect() }
/// Скользящая OLS-регрессия pub fn rolling( x: &[Vec<f64>], y: &[f64], feature_names: &[String], window: usize, ) -> Vec<RegressionResult> { let n = y.len(); let mut results = Vec::new();
for i in window..n { let x_window: Vec<Vec<f64>> = x[i - window..i].to_vec(); let y_window: Vec<f64> = y[i - window..i].to_vec(); let result = Self::fit(&x_window, &y_window, feature_names, true); results.push(result); }
results }}Регуляризованная регрессия (src/regression/regularized.rs)
pub struct RidgeRegression { pub alpha: f64, pub coefficients: Vec<f64>, pub intercept: f64,}
impl RidgeRegression { pub fn new(alpha: f64) -> Self { Self { alpha, coefficients: Vec::new(), intercept: 0.0, } }
/// Обучение Ridge: beta = (X^T X + alpha * I)^{-1} X^T y pub fn fit(&mut self, x: &[Vec<f64>], y: &[f64]) { let n = y.len(); let p = x[0].len();
// Центрирование данных let y_mean = y.iter().sum::<f64>() / n as f64; let x_means: Vec<f64> = (0..p).map(|j| { x.iter().map(|row| row[j]).sum::<f64>() / n as f64 }).collect();
// Построение X^T X + alpha * I let mut xtx = vec![vec![0.0; p]; p]; let mut xty = vec![0.0; p];
for i in 0..n { for j in 0..p { let xj = x[i][j] - x_means[j]; xty[j] += xj * (y[i] - y_mean); for k in 0..=j { let xk = x[i][k] - x_means[k]; xtx[j][k] += xj * xk; if j != k { xtx[k][j] = xtx[j][k]; } } } }
// Добавление Ridge штрафа for j in 0..p { xtx[j][j] += self.alpha; }
// Решение self.coefficients = Self::solve(&xtx, &xty); self.intercept = y_mean - x_means.iter() .zip(self.coefficients.iter()) .map(|(m, c)| m * c) .sum::<f64>(); }
pub fn predict(&self, x: &[Vec<f64>]) -> Vec<f64> { x.iter().map(|row| { self.intercept + row.iter() .zip(self.coefficients.iter()) .map(|(xi, ci)| xi * ci) .sum::<f64>() }).collect() }
fn solve(a: &[Vec<f64>], b: &[f64]) -> Vec<f64> { let n = b.len(); let mut aug = vec![vec![0.0; n + 1]; n]; for i in 0..n { for j in 0..n { aug[i][j] = a[i][j]; } aug[i][n] = b[i]; } for i in 0..n { let mut max_row = i; for k in (i + 1)..n { if aug[k][i].abs() > aug[max_row][i].abs() { max_row = k; } } aug.swap(i, max_row); let pivot = aug[i][i]; if pivot.abs() < 1e-12 { continue; } for j in i..=n { aug[i][j] /= pivot; } for k in 0..n { if k != i { let factor = aug[k][i]; for j in i..=n { aug[k][j] -= factor * aug[i][j]; } } } } (0..n).map(|i| aug[i][n]).collect() }}
pub struct LassoRegression { pub alpha: f64, pub coefficients: Vec<f64>, pub intercept: f64, pub max_iter: usize, pub tolerance: f64,}
impl LassoRegression { pub fn new(alpha: f64) -> Self { Self { alpha, coefficients: Vec::new(), intercept: 0.0, max_iter: 10000, tolerance: 1e-6, } }
/// Обучение Lasso координатным спуском pub fn fit(&mut self, x: &[Vec<f64>], y: &[f64]) { let n = y.len(); let p = x[0].len();
let y_mean = y.iter().sum::<f64>() / n as f64; self.coefficients = vec![0.0; p];
let mut residuals: Vec<f64> = y.iter().map(|yi| yi - y_mean).collect();
for _ in 0..self.max_iter { let mut max_change = 0.0_f64;
for j in 0..p { for i in 0..n { residuals[i] += self.coefficients[j] * x[i][j]; }
let rho: f64 = (0..n) .map(|i| x[i][j] * residuals[i]) .sum::<f64>() / n as f64;
let x_sq: f64 = (0..n) .map(|i| x[i][j] * x[i][j]) .sum::<f64>() / n as f64;
// Мягкое пороговое значение let new_coef = Self::soft_threshold(rho, self.alpha) / x_sq; max_change = max_change.max((new_coef - self.coefficients[j]).abs()); self.coefficients[j] = new_coef;
for i in 0..n { residuals[i] -= self.coefficients[j] * x[i][j]; } }
if max_change < self.tolerance { break; } }
self.intercept = y_mean; }
fn soft_threshold(rho: f64, lambda: f64) -> f64 { if rho > lambda { rho - lambda } else if rho < -lambda { rho + lambda } else { 0.0 } }
pub fn selected_features(&self) -> Vec<usize> { self.coefficients.iter() .enumerate() .filter(|(_, c)| c.abs() > 1e-10) .map(|(i, _)| i) .collect() }}Получение данных с Bybit
use reqwest;use serde::Deserialize;use anyhow::Result;
#[derive(Deserialize)]struct BybitResponse { result: BybitResult,}
#[derive(Deserialize)]struct BybitResult { list: Vec<Vec<String>>,}
pub async fn fetch_bybit_returns( symbol: &str, interval: &str, limit: u32,) -> Result<Vec<f64>> { let client = reqwest::Client::new(); let resp = client .get("https://api.bybit.com/v5/market/kline") .query(&[ ("category", "linear"), ("symbol", symbol), ("interval", interval), ("limit", &limit.to_string()), ]) .send() .await? .json::<BybitResponse>() .await?;
let closes: Vec<f64> = resp.result.list .iter() .map(|row| row[4].parse::<f64>().unwrap_or(0.0)) .rev() .collect();
let returns: Vec<f64> = closes.windows(2) .map(|w| (w[1] - w[0]) / w[0]) .collect();
Ok(returns)}Раздел 7: Практические примеры
Пример 1: Построение криптофакторной модели
factor_model = CryptoFactorModel( universe=["ETHUSDT", "SOLUSDT", "AVAXUSDT", "LINKUSDT", "DOTUSDT", "MATICUSDT", "AAVEUSDT", "UNIUSDT"], market_symbol="BTCUSDT")factors = factor_model.construct_factors()betas = factor_model.estimate_betas(window=60)
print("Факторные нагрузки для ETHUSDT (последние):")print(betas["ETHUSDT"].tail(1).T)
# Ожидаемый результат:# alpha 0.000234# beta_MKT 0.892341# beta_SIZE -0.045123# beta_MOM 0.123456# r_squared 0.723456
risk_premia = factor_model.fama_macbeth()print("\nПремии за риск Фамы-Макбета:")print(risk_premia)
# Ожидаемый результат:# mean std t_stat annualized# gamma_0 0.0003 0.012 0.354 0.1095# gamma_MKT 0.0008 0.008 1.414 0.2920# gamma_SIZE 0.0002 0.006 0.471 0.0730# gamma_MOM 0.0005 0.009 0.786 0.1825Пример 2: Отбор признаков Lasso для предсказания доходности
# Создание комплексного набора признаковfeatures = pd.DataFrame(index=factor_model.returns.index)for sym in factor_model.universe[:4]: ret = factor_model.returns[sym] features[f"{sym}_ret1"] = ret features[f"{sym}_ret5"] = ret.rolling(5).sum() features[f"{sym}_vol"] = ret.rolling(20).std() features[f"{sym}_mom"] = ret.rolling(30).sum()features = features.dropna()
target = factor_model.returns["ETHUSDT"].loc[features.index].shift(-1).dropna()features = features.loc[target.index]
regressor = RegularizedCryptoRegression()regressor.fit_lasso(features, target, alpha=0.001)
selected = regressor.selected_features(features.columns.tolist())print(f"Lasso отобрал {len(selected)} / {len(features.columns)} признаков:")for feat in selected: coef = regressor.model.coef_[features.columns.tolist().index(feat)] print(f" {feat}: {coef:.6f}")
# Ожидаемый результат:# Lasso отобрал 5 / 16 признаков:# ETHUSDT_ret1: 0.034521# SOLUSDT_ret1: 0.012345# ETHUSDT_vol: -0.087654# LINKUSDT_mom: 0.005432# SOLUSDT_vol: -0.023456Пример 3: Скользящая регрессия для определения режима
# Отслеживание BTC-бета ETH во времениeth_returns = factor_model.returns["ETHUSDT"]btc_returns = factor_model.returns["BTCUSDT"]common = eth_returns.index.intersection(btc_returns.index)
window = 30rolling_betas = []for i in range(window, len(common)): y = eth_returns.loc[common[i-window:i]].values X = btc_returns.loc[common[i-window:i]].values.reshape(-1, 1) model = LinearRegression() model.fit(X, y) rolling_betas.append({ "date": common[i], "beta": model.coef_[0], "alpha": model.intercept_, "r_squared": model.score(X, y) })
df_betas = pd.DataFrame(rolling_betas).set_index("date")print("Статистика скользящей бета:")print(f" Средняя бета: {df_betas['beta'].mean():.4f}")print(f" Стд бета: {df_betas['beta'].std():.4f}")print(f" Мин бета: {df_betas['beta'].min():.4f} (режим: дивергенция)")print(f" Макс бета: {df_betas['beta'].max():.4f} (режим: высокая корреляция)")print(f" Средний R²: {df_betas['r_squared'].mean():.4f}")
# Ожидаемый результат:# Статистика скользящей бета:# Средняя бета: 0.9234# Стд бета: 0.2156# Мин бета: 0.4523 (режим: дивергенция)# Макс бета: 1.4567 (режим: высокая корреляция)# Средний R²: 0.6789Раздел 8: Фреймворк бэктестирования
Компоненты фреймворка
Фреймворк бэктестирования линейных методов включает:
- Конвейер факторных данных: Построение факторных доходностей из данных Bybit/yfinance
- Движок скользящей регрессии: Оценка изменяющихся во времени факторных нагрузок
- Генератор сигналов: Перевод оценок альфа и факторных взглядов в сигналы
- Определитель размера позиции: Использование значимости коэффициентов для масштабирования позиций
- Трекер производительности: Расчёт атрибуции на уровне стратегии и факторов
Панель метрик
| Метрика | Описание | Расчёт |
|---|---|---|
| Факторный R² | Объясняющая сила факторов | 1 - SS_res / SS_tot |
| Альфа (аннуализированная) | Избыточная доходность сверх факторов | intercept * 365 |
| t-статистика альфа | Статистическая значимость альфы | alpha / se(alpha) |
| Стабильность бета | Коэффициент вариации скользящей бета | std(beta) / mean(beta) |
| Информационный коэффициент | Корреляция предсказаний с исходами | corr(y_hat, y) |
| Шарп фактора | Доходность фактора с поправкой на риск | mean(F) / std(F) * sqrt(365) |
| Разреженность Lasso | Доля нулевых коэффициентов | count(|beta| < eps) / p |
Пример результатов
=== Бэктест линейных методов: Криптофакторная модель ===
Период: 2024-01-01 — 2024-12-31Вселенная: 8 альткоинов | Бенчмарк: BTCUSDTФакторная модель: MKT + SIZE + MOM (3 фактора)
Производительность факторов: Фактор | Год.доход. | Шарп | Значимость (t-stat) --------|------------|-------|--------------------- MKT | 62.3% | 1.45 | 3.21 *** SIZE | 8.7% | 0.42 | 1.12 MOM | 15.2% | 0.78 | 1.89 *
Модель предсказания (Ridge, alpha=1.0): R² на обучении: 0.0423 R² вне выборки: 0.0187 Инфо. коэффициент: 0.137 Точность направления: 0.534
Отбор признаков Lasso (alpha=0.001): Отобрано признаков: 5 / 16 OOS R² (отобранные): 0.0201 OOS R² (все признаки): 0.0145
Скользящая регрессия (30-дневное окно): Средняя BTC бета (ETH): 0.923 +/- 0.216 Диапазон бета: [0.452, 1.457] Диапазон R²: [0.312, 0.891] Обнаружено смен режимов: 4Раздел 9: Оценка производительности
Сравнение линейных методов на криптоданных
| Метод | R² на обучении | OOS R² | Точность направления | Разреженность | Стабильность |
|---|---|---|---|---|---|
| OLS (все признаки) | 0.052 | 0.008 | 0.512 | 0% | Низкая |
| OLS (5 признаков) | 0.031 | 0.018 | 0.528 | 69% | Средняя |
| Ridge (CV alpha) | 0.048 | 0.021 | 0.531 | 0% | Высокая |
| Lasso (CV alpha) | 0.035 | 0.019 | 0.529 | 65% | Средняя |
| Elastic Net (0.5) | 0.041 | 0.020 | 0.530 | 50% | Высокая |
| Логистическая (L1) | Н/Д | Н/Д | 0.534 | 60% | Средняя |
| Скользящий OLS (30д) | 0.067 | 0.015 | 0.523 | 0% | Низкая |
Ключевые выводы
-
Ridge-регрессия стабильно достигает лучшего R² вне выборки на криптоданных, потому что L2 штраф стабилизирует оценки коэффициентов без принуждения их к нулю, что важно, когда многие признаки несут малый, но ненулевой сигнал.
-
Отбор признаков Lasso ценен, но может быть нестабилен: набор отобранных признаков значительно меняется между временными периодами, предполагая, что важность признаков в крипто зависит от режима.
-
Факторные модели объясняют 30-70% дисперсии альткоинов только через бету BTC. Добавление факторов размера и моментума улучшает объясняющую силу на 5-10%, но ончейн-факторы остаются слабыми из-за зашумлённых данных.
-
Скользящая регрессия выявляет чёткие смены режимов: бета BTC-ETH варьируется от 0.45 в периоды дивергенции до 1.45 во время высококоррелированных обвалов. Мониторинг трендов бета предоставляет раннее предупреждение о смене режимов.
-
Точность направления выше 53% достижима с регуляризованной логистической регрессией на тщательно отобранных признаках, транслируясь в положительное ожидаемое торговое PnL после комиссий Bybit.
Ограничения
- Линейные модели не могут захватить эффекты взаимодействия или нелинейные факторные отношения
- Построение факторов зависит от выбора вселенной активов, вводя ошибку выживаемости
- Стандартные ошибки Фамы-Макбета могут быть занижены из-за кросс-секционной зависимости
- Оценки скользящей регрессии запаздывают и могут пропускать быстрые переходы между режимами
- Оценки коэффициентов OLS смещены, когда признаки содержат ошибку измерения (проблема ошибок в переменных)
Раздел 10: Перспективные направления
-
Нелинейные факторные модели: Расширение линейных факторных моделей ядерными методами или полиномиальными признаками для захвата нелинейных отношений между факторами и доходностями при сохранении преимуществ интерпретируемости факторного модельного фреймворка.
-
Высокочастотные факторные модели: Адаптация факторных моделей к внутридневным частотам (1-минутные, тиковые) с использованием данных стакана заявок Bybit, захватывая факторы микроструктуры, такие как дисбаланс потока ордеров и позиция в очереди.
-
Модели динамических факторных нагрузок: Реализация моделей пространства состояний (фильтр Калмана) для непрерывного отслеживания факторных нагрузок, заменяя разрывный подход скользящего окна гладкими оценками в реальном времени.
-
Инновации ончейн-факторов: Разработка новых крипто-специфических факторов из данных блокчейна, включая MEV (максимально извлекаемая стоимость), поведение валидаторов и потоки кросс-чейн мостов.
-
Байесовская линейная регрессия: Размещение информативных априоров на факторных нагрузках на основе экономической теории (напр., бета BTC должна быть положительной для большинства альткоинов), улучшая оценку при малых выборках.
-
Инструментальные переменные для крипто: Решение проблемы эндогенности в криптофакторных моделях с использованием инструментальных переменных, таких как использование сложности майнинга в качестве инструмента для шоков предложения BTC.
Литература
-
Fama, E. F., & French, K. R. (1993). “Common Risk Factors in the Returns on Stocks and Bonds.” Journal of Financial Economics, 33(1), 3-56.
-
Tibshirani, R. (1996). “Regression Shrinkage and Selection via the Lasso.” Journal of the Royal Statistical Society Series B, 58(1), 267-288.
-
Hoerl, A. E., & Kennard, R. W. (1970). “Ridge Regression: Biased Estimation for Nonorthogonal Problems.” Technometrics, 12(1), 55-67.
-
Zou, H., & Hastie, T. (2005). “Regularization and Variable Selection via the Elastic Net.” Journal of the Royal Statistical Society Series B, 67(2), 301-320.
-
Fama, E. F., & MacBeth, J. D. (1973). “Risk, Return, and Equilibrium: Empirical Tests.” Journal of Political Economy, 81(3), 607-636.
-
Liu, Y., Tsyvinski, A., & Wu, X. (2022). “Common Risk Factors in Cryptocurrency.” The Journal of Finance, 77(2), 1133-1177.
-
Newey, W. K., & West, K. D. (1987). “A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix.” Econometrica, 55(3), 703-708.