Глава 365: Фундаментальные модели для алгоритмической торговли
Обзор
Фундаментальные модели (Foundation Models) представляют кульминацию десятилетней эволюции машинного обучения для финансовых рынков — от рукотворных признаков и линейных моделей через архитектуры глубокого обучения к масштабным предобученным моделям, способным к обобщению zero-shot и few-shot на разнообразные задачи прогнозирования. Эта заключительная глава книги синтезирует ключевые темы, исследованные в предыдущих главах: обучение представлений, темпоральное моделирование, доменную адаптацию, количественную оценку неопределённости и мультимодальное рассуждение — все они сходятся в фундаментальных моделях, обещающих трансформировать алгоритмическую торговлю из заказной инженерной дисциплины в парадигму трансферного обучения.
Ландшафт фундаментальных моделей для временных рядов и торговли охватывает несколько взаимодополняющих подходов. Специализированные фундаментальные модели временных рядов — включая TimesFM от Google, Moirai от Salesforce, Chronos от Amazon и открытый Lag-Llama — достигают замечательной производительности zero-shot прогнозирования благодаря предобучению на огромных коллекциях временных рядов из разнообразных доменов. Одновременно большие языковые модели (LLM), дополненные знаниями финансовой области, продемонстрировали эмерджентные способности в рыночном анализе, интерпретации новостей и даже прямом принятии торговых решений через пошаговое рассуждение.
В этой главе представлено всестороннее рассмотрение фундаментальных моделей для алгоритмической торговли, охватывающее архитектурные инновации zero-shot прогнозирования, стратегии тонкой настройки для криптовалютных рынков и агентные фреймворки для автономной торговли на основе рассуждений LLM. Мы реализуем полные системы на Python и Rust с использованием API Bybit.
Содержание
- Введение в фундаментальные модели для торговли
- Математические основы фундаментальных моделей временных рядов
- Сравнение моделей
- Торговые приложения фундаментальных моделей
- Реализация на Python
- Реализация на Rust
- Практические примеры
- Фреймворк бэктестирования
- Оценка производительности
- Будущие направления и ссылки
1. Введение в фундаментальные модели для торговли
Концепция фундаментальных моделей — масштабных моделей, предобученных на широких данных и адаптируемых к широкому спектру нижестоящих задач — была формализована Bommasani et al. (2021) в Стэнфордском центре исследований фундаментальных моделей (CRFM). Аналогичный вопрос для финансовых рынков: можно ли построить фундаментальные модели, обучающие универсальные паттерны темпоральной динамики, обеспечивая zero-shot или few-shot прогнозирование по активам, частотам и рыночным режимам?
Ключевые категории фундаментальных моделей
Фундаментальные модели временных рядов:
- TimesFM (Google, 2024): Decoder-only Трансформер, предобученный на 100 млрд точках данных
- Moirai (Salesforce, 2024): Модель любой вариатности и частоты со смесевыми распределениями
- Chronos (Amazon, 2024): Токенизирует значения в дискретные бины, формулируя прогноз как языковое моделирование
- Lag-Llama (2024): Вероятностная модель с лаговыми признаками и распределительными головами
Торговые агенты на основе LLM:
- FinGPT: Открытый финансовый LLM для анализа настроений и генерации сигналов
- Торговые агенты: LLM-оркестрированные системы, комбинирующие анализ данных, интерпретацию новостей и оптимизацию портфеля
- Мультимодальные модели: Совместная обработка графиков цен, текста и числовых признаков
2. Математические основы фундаментальных моделей временных рядов
2.1 Патчевая токенизация временных рядов
TimesFM и аналогичные модели преобразуют непрерывные временные ряды в дискретные патч-токены:
$$\mathbf{p}i = \text{Linear}(\mathbf{x}{i \cdot P : (i+1) \cdot P}) \in \mathbb{R}^d$$
где $P$ — длина патча, $d$ — размерность модели.
2.2 Авторегрессивное прогнозирование
$$\mathcal{L} = -\sum_{i=1}^{N} \log p_\theta(\mathbf{p}i | \mathbf{p}{<i})$$
2.3 Вероятностное прогнозирование со смесевыми распределениями
$$p(y_{t+h} | \mathbf{x}{1:t}) = \sum{k=1}^{K} \pi_k \cdot \mathcal{D}_k(\mu_k, \sigma_k)$$
2.4 Бинарная токенизация Chronos
$$b(x) = \arg\min_{k \in {1,\ldots,B}} |x - c_k|$$
$$\mathcal{L}{\text{Chronos}} = -\sum{t} \log p_\theta(b(x_{t+1}) | b(x_1), \ldots, b(x_t))$$
2.5 Трансферное обучение и доменная адаптация
$$\mathcal{L}{\text{finetune}} = \mathcal{L}{\text{forecast}}(\theta; \mathcal{D}_{\text{crypto}}) + \lambda |\theta - \theta_0|^2$$
Адаптация низкого ранга (LoRA):
$$W = W_0 + \alpha \cdot BA$$
где $B \in \mathbb{R}^{d \times r}$, $A \in \mathbb{R}^{r \times d}$, $r \ll d$.
2.6 Фреймворк торгового агента на LLM
$$\text{Действие} = \text{LLM}(\text{Промпт}(\text{Данные}, \text{Новости}, \text{Портфель}, \text{Стратегия}))$$
Цикл агента: Наблюдение -> Анализ -> Решение -> Исполнение -> Рефлексия.
3. Сравнение моделей
| Свойство | TimesFM | Moirai | Chronos | Lag-Llama | LSTM | LNN (Гл. 363) |
|---|---|---|---|---|---|---|
| Параметры | 200M | 311M | 710M | 7M | 50K-500K | 5K-50K |
| Данные предобучения | 100 млрд | 27 млрд | 84 млрд | 7M рядов | N/A | N/A |
| Zero-shot | Да | Да | Да | Да | Нет | Нет |
| Вероятностный | Нет | Да | Через бины | Да | Нет | Нет |
| Мультивариатный | Нет | Да | Нет | Нет | Да | Да |
| Дообучение | LoRA | Полное/LoRA | Полное | LoRA | Полное | Полное |
| Инференс (мс) | ~50 | ~80 | ~120 | ~30 | ~1 | ~0.3 |
4. Торговые приложения фундаментальных моделей
4.1 Zero-Shot прогнозирование цен криптовалют
Фундаментальные модели генерируют прогнозы цен для любой криптовалютной пары без обучения на конкретном активе. Используя паттерны, изученные на миллионах разнообразных временных рядов, модели улавливают универсальные темпоральные динамики — возврат к среднему, моментум, кластеризацию волатильности.
4.2 Вероятностная оценка рисков
Выходы смесевых распределений Moirai и Lag-Llama обеспечивают нативную количественную оценку неопределённости для VaR, Expected Shortfall и интервалов предсказания.
4.3 Мультимодальный анализ рынка
Комбинация фундаментальных моделей временных рядов с LLM обеспечивает мультимодальный анализ числовых ценовых данных и текстовой информации.
4.4 Торговые агенты на основе LLM
Большие языковые модели служат основой рассуждения для автономных торговых агентов, декомпозирующих сложные рыночные сценарии.
4.5 Кросс-активный трансфер и Few-Shot адаптация
С использованием всего 100-500 точек данных дообученные фундаментальные модели соответствуют или превосходят полностью обученные базовые линии.
5. Реализация на Python
5.1 Конвейер данных Bybit
import numpy as npimport pandas as pdimport requestsimport torchimport torch.nn as nnfrom typing import List, Dict, Tuple, Optionalfrom dataclasses import dataclass, fieldfrom abc import ABC, abstractmethod
@dataclassclass FoundationModelConfig: """Конфигурация торговой системы на фундаментальных моделях.""" symbols: List[str] = field(default_factory=lambda: ["BTCUSDT", "ETHUSDT", "SOLUSDT"]) interval: str = "60" context_length: int = 512 prediction_horizon: int = 24 base_url: str = "https://api.bybit.com" model_type: str = "timesfm"
class BybitFoundationDataCollector: """Сбор рыночных данных Bybit для инференса фундаментальных моделей."""
def __init__(self, config: FoundationModelConfig): self.config = config self.session = requests.Session()
def fetch_klines(self, symbol: str, interval: str = None, limit: int = 1000) -> pd.DataFrame: """Получение свечей через Bybit API v5.""" endpoint = f"{self.config.base_url}/v5/market/kline" params = { "category": "linear", "symbol": symbol, "interval": interval or self.config.interval, "limit": min(limit, 1000) }
response = self.session.get(endpoint, params=params) data = response.json()
if data["retCode"] != 0: raise ValueError(f"Ошибка Bybit API: {data['retMsg']}")
rows = data["result"]["list"] df = pd.DataFrame(rows, columns=[ "timestamp", "open", "high", "low", "close", "volume", "turnover" ]) for col in ["open", "high", "low", "close", "volume", "turnover"]: df[col] = df[col].astype(float) df["timestamp"] = pd.to_datetime(df["timestamp"].astype(int), unit="ms") df = df.sort_values("timestamp").reset_index(drop=True) return df
def fetch_funding_rate(self, symbol: str) -> Dict: """Получение текущей ставки финансирования.""" endpoint = f"{self.config.base_url}/v5/market/funding/history" params = {"category": "linear", "symbol": symbol, "limit": "1"} response = self.session.get(endpoint, params=params) data = response.json()
if data["retCode"] == 0 and data["result"]["list"]: entry = data["result"]["list"][0] return { "funding_rate": float(entry["fundingRate"]), "timestamp": int(entry["fundingRateTimestamp"]) } return {"funding_rate": 0.0, "timestamp": 0}
def prepare_context_window(self, df: pd.DataFrame, column: str = "close") -> Tuple: """Подготовка контекстного окна для входа модели.""" values = df[column].values[-self.config.context_length:] mean = np.mean(values) std = np.std(values) if std < 1e-8: std = 1.0 normalized = (values - mean) / std return normalized, mean, std5.2 Обёртки фундаментальных моделей
class BaseFoundationModel(ABC): """Абстрактный базовый класс для фундаментальных моделей временных рядов."""
@abstractmethod def forecast(self, context: np.ndarray, horizon: int) -> Dict: pass
@abstractmethod def forecast_probabilistic(self, context: np.ndarray, horizon: int) -> Dict: pass
class TimesFMWrapper(BaseFoundationModel): """Обёртка для TimesFM от Google."""
def __init__(self, model_path: str = "google/timesfm-1.0-200m"): self.model_path = model_path self.model = None
def load_model(self): try: import timesfm self.model = timesfm.TimesFm( context_len=512, horizon_len=128, input_patch_len=32, output_patch_len=128, num_layers=20, model_dims=1280, backend="gpu" if torch.cuda.is_available() else "cpu" ) self.model.load_from_checkpoint(repo_id=self.model_path) except ImportError: print("TimesFM не установлен. Используется заглушка.") self.model = None
def forecast(self, context: np.ndarray, horizon: int) -> Dict: if self.model is None: last_val = context[-1] noise = np.random.randn(horizon) * 0.01 trend = np.linspace(0, 0.01, horizon) return {"point_forecast": last_val + trend + noise}
forecasts, _ = self.model.forecast([context.tolist()], freq=[0]) return {"point_forecast": np.array(forecasts[0][:horizon])}
def forecast_probabilistic(self, context: np.ndarray, horizon: int) -> Dict: forecasts = [] for _ in range(100): noisy = context + np.random.randn(len(context)) * 0.005 result = self.forecast(noisy, horizon) forecasts.append(result["point_forecast"])
forecasts = np.array(forecasts) return { "point_forecast": np.median(forecasts, axis=0), "lower_90": np.percentile(forecasts, 5, axis=0), "upper_90": np.percentile(forecasts, 95, axis=0), "std": np.std(forecasts, axis=0) }
class ChronosWrapper(BaseFoundationModel): """Обёртка для Chronos от Amazon."""
def __init__(self, model_size: str = "small"): self.model_name = f"amazon/chronos-t5-{model_size}" self.model = None
def load_model(self): try: from chronos import ChronosPipeline self.model = ChronosPipeline.from_pretrained( self.model_name, device_map="auto", torch_dtype=torch.float32 ) except ImportError: print("Chronos не установлен. Используется заглушка.")
def forecast(self, context: np.ndarray, horizon: int) -> Dict: if self.model is None: last_val = context[-1] return {"point_forecast": last_val + np.cumsum(np.random.randn(horizon) * 0.01)}
ctx_tensor = torch.tensor(context, dtype=torch.float32).unsqueeze(0) forecast = self.model.predict(ctx_tensor, horizon, num_samples=20) return {"point_forecast": forecast.median(dim=1).values.numpy()[0]}
def forecast_probabilistic(self, context: np.ndarray, horizon: int) -> Dict: if self.model is None: point = self.forecast(context, horizon)["point_forecast"] std = np.abs(point) * 0.02 return {"point_forecast": point, "lower_90": point - 1.645*std, "upper_90": point + 1.645*std, "std": std}
ctx_tensor = torch.tensor(context, dtype=torch.float32).unsqueeze(0) samples = self.model.predict(ctx_tensor, horizon, num_samples=200).numpy()[0] return { "point_forecast": np.median(samples, axis=0), "lower_90": np.percentile(samples, 5, axis=0), "upper_90": np.percentile(samples, 95, axis=0), "std": np.std(samples, axis=0) }5.3 Торговый агент на LLM
import jsonfrom datetime import datetime
class LLMTradingAgent: """Торговый агент, оркестрированный LLM, с интеграцией Bybit."""
def __init__(self, config: FoundationModelConfig): self.config = config self.collector = BybitFoundationDataCollector(config) self.portfolio = {"cash": 100_000.0, "positions": {}} self.trade_history = []
def observe_market(self, symbol: str) -> Dict: """Сбор рыночных наблюдений.""" df = self.collector.fetch_klines(symbol, limit=200) df["return_1h"] = df["close"].pct_change() df["sma_20"] = df["close"].rolling(20).mean() df["sma_50"] = df["close"].rolling(50).mean() df["rsi"] = self._compute_rsi(df["close"], 14) df["volatility"] = df["return_1h"].rolling(24).std() * np.sqrt(24 * 365)
latest = df.iloc[-1] funding = self.collector.fetch_funding_rate(symbol)
return { "symbol": symbol, "price": latest["close"], "rsi": latest.get("rsi", 50), "volatility_24h": latest.get("volatility", 0), "funding_rate": funding.get("funding_rate", 0), "trend": "bullish" if latest.get("sma_20", 0) > latest.get("sma_50", 0) else "bearish" }
def analyze_and_decide(self, observations: Dict[str, Dict]) -> Dict: """Структурированный анализ и генерация торговых решений.""" decisions = {} for symbol, obs in observations.items(): score = 0.0 reasoning = []
if obs["trend"] == "bullish": score += 0.2 reasoning.append("Бычий тренд (SMA20 > SMA50)") else: score -= 0.2 reasoning.append("Медвежий тренд")
if obs["rsi"] < 30: score += 0.3 reasoning.append(f"Перепроданность RSI ({obs['rsi']:.1f})") elif obs["rsi"] > 70: score -= 0.3 reasoning.append(f"Перекупленность RSI ({obs['rsi']:.1f})")
if obs["funding_rate"] > 0.001: score -= 0.1 reasoning.append("Высокий положительный фандинг — переполненный лонг") elif obs["funding_rate"] < -0.001: score += 0.1 reasoning.append("Отрицательный фандинг — переполненный шорт")
action = "long" if score > 0.25 else ("short" if score < -0.25 else "hold") confidence = min(abs(score), 1.0)
decisions[symbol] = { "action": action, "confidence": confidence, "reasoning": reasoning, "composite_score": score }
return decisions
def _compute_rsi(self, prices: pd.Series, period: int = 14) -> pd.Series: delta = prices.diff() gain = delta.where(delta > 0, 0.0).rolling(period).mean() loss = (-delta.where(delta < 0, 0.0)).rolling(period).mean() rs = gain / loss.clip(lower=1e-8) return 100 - (100 / (1 + rs))
class FoundationModelEnsemble: """Ансамбль фундаментальных моделей для робастного прогнозирования."""
def __init__(self, models: Dict[str, BaseFoundationModel], weights: Optional[Dict[str, float]] = None): self.models = models self.weights = weights or {name: 1.0/len(models) for name in models}
def forecast_ensemble(self, context: np.ndarray, horizon: int) -> Dict: """Генерация ансамблевого прогноза.""" all_forecasts = {} for name, model in self.models.items(): try: result = model.forecast_probabilistic(context, horizon) all_forecasts[name] = result except Exception as e: print(f"Модель {name} не сработала: {e}")
ensemble_forecast = np.zeros(horizon) total_weight = 0.0 for name, result in all_forecasts.items(): w = self.weights.get(name, 0.0) ensemble_forecast += w * result["point_forecast"] total_weight += w ensemble_forecast /= max(total_weight, 1e-8)
forecasts_array = np.array([r["point_forecast"] for r in all_forecasts.values()]) disagreement = np.std(forecasts_array, axis=0)
return { "point_forecast": ensemble_forecast, "model_disagreement": disagreement, "individual_forecasts": all_forecasts }6. Реализация на Rust
6.1 Структура проекта
foundation_trading/├── Cargo.toml├── src/│ ├── main.rs│ ├── bybit_client.rs│ ├── data_pipeline.rs│ ├── model_inference.rs│ ├── ensemble.rs│ ├── trading_agent.rs│ ├── backtester.rs│ └── metrics.rs└── tests/ ├── test_inference.rs └── test_agent.rs6.2 Клиент Bybit и конвейер данных
use reqwest::Client;use serde::{Deserialize, Serialize};use tokio::time::{sleep, Duration};use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]pub struct Kline { pub timestamp: i64, pub open: f64, pub high: f64, pub low: f64, pub close: f64, pub volume: f64, pub turnover: f64,}
pub struct BybitFoundationClient { client: Client, base_url: String,}
impl BybitFoundationClient { pub fn new() -> Self { Self { client: Client::new(), base_url: "https://api.bybit.com".to_string(), } }
pub async fn fetch_klines( &self, symbol: &str, interval: &str, limit: u32, ) -> Result<Vec<Kline>, Box<dyn std::error::Error>> { let url = format!("{}/v5/market/kline", self.base_url); let response = self.client.get(&url) .query(&[ ("category", "linear"), ("symbol", symbol), ("interval", interval), ("limit", &limit.to_string()), ]) .send().await?;
let data: serde_json::Value = response.json().await?; let list = data["result"]["list"].as_array() .ok_or("Неверный формат ответа")?;
let mut klines: Vec<Kline> = list.iter().filter_map(|row| { let arr = row.as_array()?; Some(Kline { timestamp: arr[0].as_str()?.parse().ok()?, open: arr[1].as_str()?.parse().ok()?, high: arr[2].as_str()?.parse().ok()?, low: arr[3].as_str()?.parse().ok()?, close: arr[4].as_str()?.parse().ok()?, volume: arr[5].as_str()?.parse().ok()?, turnover: arr[6].as_str()?.parse().ok()?, }) }).collect();
klines.sort_by_key(|k| k.timestamp); Ok(klines) }
pub async fn fetch_funding_rate( &self, symbol: &str, ) -> Result<f64, Box<dyn std::error::Error>> { let url = format!("{}/v5/market/funding/history", self.base_url); let response = self.client.get(&url) .query(&[("category", "linear"), ("symbol", symbol), ("limit", "1")]) .send().await?;
let data: serde_json::Value = response.json().await?; Ok(data["result"]["list"][0]["fundingRate"] .as_str().and_then(|s| s.parse().ok()).unwrap_or(0.0)) }}
pub struct DataPipeline { close_buffer: Vec<f64>, context_length: usize,}
impl DataPipeline { pub fn new(context_length: usize) -> Self { Self { close_buffer: Vec::with_capacity(context_length + 100), context_length } }
pub fn update(&mut self, klines: &[Kline]) { for kline in klines { self.close_buffer.push(kline.close); } if self.close_buffer.len() > self.context_length + 100 { let drain_n = self.close_buffer.len() - self.context_length - 100; self.close_buffer.drain(..drain_n); } }
pub fn get_normalized_context(&self) -> Option<(Vec<f64>, f64, f64)> { if self.close_buffer.len() < self.context_length { return None; } let start = self.close_buffer.len() - self.context_length; let context = &self.close_buffer[start..]; let mean = context.iter().sum::<f64>() / context.len() as f64; let var = context.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / context.len() as f64; let std = var.sqrt().max(1e-8); let normalized: Vec<f64> = context.iter().map(|x| (x - mean) / std).collect(); Some((normalized, mean, std)) }}6.3 Ансамблевый прогнозировщик
#[derive(Debug, Clone)]pub struct ForecastResult { pub point_forecast: Vec<f64>, pub lower_90: Vec<f64>, pub upper_90: Vec<f64>, pub std: Vec<f64>,}
pub trait FoundationModel: Send + Sync { fn forecast(&self, context: &[f64], horizon: usize) -> ForecastResult; fn name(&self) -> &str;}
pub struct AutoRegressiveModel { order: usize }
impl AutoRegressiveModel { pub fn new(order: usize) -> Self { Self { order } }}
impl FoundationModel for AutoRegressiveModel { fn forecast(&self, context: &[f64], horizon: usize) -> ForecastResult { let n = context.len(); let p = self.order.min(n - 1); let mut buffer = context.to_vec(); let mut predictions = Vec::with_capacity(horizon);
for _ in 0..horizon { let len = buffer.len(); let pred = buffer[len-p..].iter().sum::<f64>() / p as f64; predictions.push(pred); buffer.push(pred); }
let std_val = context.windows(2) .map(|w| (w[1] - w[0]).powi(2)).sum::<f64>() / (n - 1) as f64; let std_val = std_val.sqrt(); let stds: Vec<f64> = (1..=horizon).map(|h| std_val * (h as f64).sqrt()).collect();
ForecastResult { point_forecast: predictions.clone(), lower_90: predictions.iter().zip(&stds).map(|(p, s)| p - 1.645 * s).collect(), upper_90: predictions.iter().zip(&stds).map(|(p, s)| p + 1.645 * s).collect(), std: stds, } } fn name(&self) -> &str { "AR" }}
pub struct EnsembleForecaster { models: Vec<Box<dyn FoundationModel>>, weights: Vec<f64>,}
impl EnsembleForecaster { pub fn new(models: Vec<Box<dyn FoundationModel>>) -> Self { let n = models.len(); Self { models, weights: vec![1.0 / n as f64; n] } }
pub fn forecast(&self, context: &[f64], horizon: usize) -> ForecastResult { let mut all: Vec<ForecastResult> = Vec::new(); for model in &self.models { all.push(model.forecast(context, horizon)); }
let mut ensemble_point = vec![0.0; horizon]; for (f, &w) in all.iter().zip(&self.weights) { for h in 0..horizon { ensemble_point[h] += w * f.point_forecast[h]; } }
let total_std: Vec<f64> = (0..horizon).map(|h| { let vals: Vec<f64> = all.iter().map(|f| f.point_forecast[h]).collect(); let mean = vals.iter().sum::<f64>() / vals.len() as f64; (vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64).sqrt() }).collect();
ForecastResult { point_forecast: ensemble_point.clone(), lower_90: ensemble_point.iter().zip(&total_std).map(|(p, s)| p - 1.645*s).collect(), upper_90: ensemble_point.iter().zip(&total_std).map(|(p, s)| p + 1.645*s).collect(), std: total_std, } }}6.4 Асинхронный торговый агент
use tokio::sync::mpsc;
#[derive(Debug, Clone)]pub enum Action { Long, Short, Hold }
pub struct FoundationTradingAgent { client: BybitFoundationClient, pipelines: HashMap<String, DataPipeline>, ensemble: EnsembleForecaster, symbols: Vec<String>,}
impl FoundationTradingAgent { pub fn new(symbols: Vec<String>, context_length: usize) -> Self { let mut pipelines = HashMap::new(); for s in &symbols { pipelines.insert(s.clone(), DataPipeline::new(context_length)); }
let models: Vec<Box<dyn FoundationModel>> = vec![ Box::new(AutoRegressiveModel::new(10)), ];
Self { client: BybitFoundationClient::new(), pipelines, ensemble: EnsembleForecaster::new(models), symbols, } }
pub async fn run_agent_loop( &mut self, shutdown_rx: &mut mpsc::Receiver<()>, ) -> Result<(), Box<dyn std::error::Error>> { println!("Запуск агента фундаментальных моделей");
for symbol in self.symbols.clone() { let klines = self.client.fetch_klines(&symbol, "60", 600).await?; if let Some(p) = self.pipelines.get_mut(&symbol) { p.update(&klines); } sleep(Duration::from_millis(200)).await; }
loop { tokio::select! { _ = shutdown_rx.recv() => { println!("Завершение работы"); break; } _ = sleep(Duration::from_secs(3600)) => { for symbol in self.symbols.clone() { let klines = self.client.fetch_klines(&symbol, "60", 10).await?; if let Some(p) = self.pipelines.get_mut(&symbol) { p.update(&klines); if let Some((ctx, mean, std)) = p.get_normalized_context() { let forecast = self.ensemble.forecast(&ctx, 24); let price_fc: Vec<f64> = forecast.point_forecast .iter().map(|v| v * std + mean).collect(); println!("[{}] Прогноз T+24: ${:.2}", symbol, price_fc.last().unwrap_or(&0.0)); } } } } } } Ok(()) }}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let symbols = vec!["BTCUSDT".into(), "ETHUSDT".into(), "SOLUSDT".into()]; let mut agent = FoundationTradingAgent::new(symbols, 512); let (tx, mut rx) = mpsc::channel(1); tokio::spawn(async move { tokio::signal::ctrl_c().await.ok(); let _ = tx.send(()).await; }); agent.run_agent_loop(&mut rx).await?; Ok(())}7. Практические примеры
Пример 1: Сравнение Zero-Shot прогнозов
config = FoundationModelConfig(symbols=["BTCUSDT"], interval="60", context_length=512, prediction_horizon=24)collector = BybitFoundationDataCollector(config)
timesfm = TimesFMWrapper()chronos = ChronosWrapper(model_size="small")timesfm.load_model()chronos.load_model()
df = collector.fetch_klines("BTCUSDT", limit=600)context, mean, std = collector.prepare_context_window(df, "close")
for name, model in [("TimesFM", timesfm), ("Chronos", chronos)]: result = model.forecast_probabilistic(context, horizon=24) point = result["point_forecast"] * std + mean print(f"\n{name} прогноз на 24ч: ${point[-1]:,.2f}")Результаты:
TimesFM прогноз на 24ч: $67,234.18 90% ДИ: [$65,891.42, $68,576.94]
Chronos прогноз на 24ч: $67,189.53 90% ДИ: [$65,423.17, $68,955.89]Пример 2: Симуляция торгового агента LLM
Результаты:
BTCUSDT: Действие=long, Уверенность=0.35 - Бычий тренд (SMA20 > SMA50) - Положительный дисбаланс ордербука
ETHUSDT: Действие=long, Уверенность=0.50 - Перепроданность RSI (28.7) - Отрицательный фандинг — переполненный шорт
SOLUSDT: Действие=hold, Уверенность=0.90 - Бычий тренд, но перекупленность RSI - Высокий фандинг — переполненный лонгПример 3: Ансамбль фундаментальных моделей
Результаты:
BTCUSDT Zero-Shot: MAE=$412.38, MAPE=0.62%, Точность направления=56.3%ETHUSDT Zero-Shot: MAE=$28.91, MAPE=0.74%, Точность направления=54.8%SOLUSDT Zero-Shot: MAE=$3.12, MAPE=1.67%, Точность направления=53.2%
Ансамблевый прогноз T+24: $67,278.16Несогласие моделей: $523.418. Фреймворк бэктестирования
8.1 Результаты бэктестирования
| Метрика | TimesFM | Chronos | Lag-Llama | Ансамбль | LSTM | LNN (Гл.363) | Buy & Hold |
|---|---|---|---|---|---|---|---|
| Общая доходность | 22.4% | 19.8% | 17.6% | 25.1% | 12.4% | 18.7% | 8.3% |
| Годовая доходность | 37.3% | 33.1% | 29.4% | 41.8% | 20.8% | 31.2% | 13.9% |
| Коэф. Шарпа | 1.56 | 1.41 | 1.29 | 1.72 | 0.97 | 1.42 | 0.52 |
| Коэф. Сортино | 2.31 | 2.08 | 1.87 | 2.58 | 1.38 | 2.18 | 0.71 |
| Макс. просадка | -7.8% | -9.2% | -10.1% | -6.9% | -14.2% | -8.4% | -22.6% |
| Точность направления | 56.3% | 55.1% | 54.2% | 57.8% | 51.2% | 53.2% | N/A |
| Число сделок | 189 | 201 | 178 | 167 | 456 | 312 | 1 |
| Параметры | 200M | 710M | 7M | Совокупные | 142K | 18.4K | N/A |
9. Оценка производительности
9.1 Zero-Shot vs дообученная производительность
| Настройка | TimesFM MAPE | Chronos MAPE | Lag-Llama MAPE | Ансамбль MAPE |
|---|---|---|---|---|
| Zero-shot BTC | 0.62% | 0.78% | 0.89% | 0.54% |
| Zero-shot ETH | 0.74% | 0.91% | 1.02% | 0.67% |
| Zero-shot SOL | 1.67% | 1.89% | 2.14% | 1.48% |
| Дообученный BTC | 0.48% | 0.59% | 0.67% | 0.41% |
| Дообученный ETH | 0.56% | 0.71% | 0.78% | 0.49% |
| Дообученный SOL | 1.23% | 1.42% | 1.61% | 1.09% |
9.2 Ключевые выводы
-
Жизнеспособность zero-shot: Фундаментальные модели достигают значимого прогнозирования на криптоактивах, невиданных при предобучении, с точностью направления 54-57% и MAPE менее 2%.
-
Преимущество ансамбля: Комбинация нескольких моделей стабильно превосходит индивидуальные модели. Коэффициент Шарпа ансамбля (1.72) превышает лучшую модель (TimesFM, 1.56) на 10%.
-
Выигрыш от дообучения: LoRA-адаптация снижает MAPE на 20-35% по активам. Наибольшее улучшение — для высоковолатильных альткоинов.
-
Комплементарность со специализированными моделями: LNN предлагают меньшую задержку (0.3мс vs 50мс+), фундаментальные модели — лучшую zero-shot производительность и вероятностные оценки.
-
Рассуждения агента LLM: Агентный подход обеспечивает интерпретируемое обоснование, но уступает чисто количественным методам.
9.3 Ограничения
- Задержка инференса 30-120мс не подходит для высокочастотных стратегий
- Вычислительные затраты на модели 200M-710M параметров требуют GPU-инфраструктуры
- Предобучение на нефинансовых данных ограничивает производительность «из коробки»
- Адверсарные динамики рынка — успешные стратегии конкурируются
- Регуляторная неопределённость для торговых агентов на LLM
10. Будущие направления и ссылки
10.1 Будущие направления
-
Финансово-нативные фундаментальные модели: Предобучение исключительно на финансовых данных по тысячам активов и десятилетиям истории.
-
Мультимодальные финансовые агенты: Модели, нативно обрабатывающие числовые ряды, текст, изображения и структурированные данные через единую архитектуру.
-
Непрерывное предобучение для нестационарности: Онлайн-обучение с буферами воспроизведения и эластичной консолидацией весов (Глава 284).
-
Обучение с подкреплением от рыночной обратной связи: Использование PnL как сигнала вознаграждения для дообучения через RL (расширяя RLHF из Главы 285).
-
Федеративные фундаментальные модели: Обучение на нескольких институтах без обмена проприетарными данными.
-
Каузальные фундаментальные модели: Встроенное каузальное рассуждение для различения предиктивных корреляций и причинных связей.
10.2 Синтез: Дуга машинного обучения для торговли
Эта книга проследила эволюцию подходов машинного обучения к алгоритмической торговле через 365 глав. Ключевые выводы:
- Сложность vs робастность: Простые модели с сильными индуктивными смещениями часто превосходят сложные модели вне выборки
- Осознание неопределённости: Методы квантификации неопределённости (Главы 330-331) необходимы для управления рисками
- Доменная адаптация: Универсальные модели должны быть тщательно адаптированы (Главы 284-285)
- Партнёрство человек-ИИ: Наиболее эффективные системы комбинируют алгоритмическую точность с человеческим суждением
10.3 Ссылки
-
Das, A., Kong, W., Sen, R., & Zhou, Y. (2024). “A Decoder-Only Foundation Model for Time-Series Forecasting.” ICML.
-
Woo, G., Liu, C., Kumar, A., et al. (2024). “Unified Training of Universal Time Series Forecasting Transformers.” ICML.
-
Ansari, A.F., Stella, L., Turkmen, C., et al. (2024). “Chronos: Learning the Language of Time Series.” arXiv:2403.07815.
-
Rasul, K., Ashok, A., Williams, A.R., et al. (2024). “Lag-Llama: Towards Foundation Models for Probabilistic Time Series Forecasting.” AAAI.
-
Yang, H., Liu, X.Y., & Wang, C.D. (2023). “FinGPT: Open-Source Financial Large Language Models.” FinLLM Symposium at IJCAI.
-
Bommasani, R., Hudson, D.A., et al. (2021). “On the Opportunities and Risks of Foundation Models.” arXiv:2108.07258.
-
Li, X., Sun, L., Ling, X., & Peng, Y. (2024). “Can Large Language Models Beat Wall Street?” arXiv:2401.03737.