← К списку статей
March 14, 2026
5 мин. чтения

Корреляция сигналов: сколько пар нужно мониторить

Корреляция сигналов: сколько пар нужно мониторить
#алготрейдинг
#корреляция
#диверсификация
#портфель
#оркестрация
#крипто

Вы запускаете стратегию на 10 крипто-парах: BTC/USDT, ETH/USDT, SOL/USDT, AVAX/USDT и ещё шесть альтов. Логика кажется железной: если стратегия активна 5% времени на одной паре, то на 10 парах хотя бы одна должна быть активна 10.9510=40%1 - 0.95^{10} = 40\% времени. Четырёхкратный прирост утилизации.

На практике утилизация оказывается 15-16%, а не 40%. Ваши 10 пар ведут себя как 3. Капитал простаивает, fill_efficiency проседает, а эффективная доходность портфеля оказывается втрое ниже расчётной.

Причина — корреляция сигналов. И в криптовалютах она катастрофически высока.

Иллюзия диверсификации в крипто

Иллюзия диверсификации на криптовалютных рынках

В классических финансах диверсификация работает, потому что акции Apple и нефтяного ETF реагируют на разные факторы. В криптовалютном рынке всё иначе.

BTC — доминирующий фактор. Когда биткоин падает на 5%, ETH падает на 6-8%, SOL — на 8-12%, альткоины — на 10-20%. Корреляции дневных доходностей в крипто-рынке стабильно выше 0.6, а в периоды паники приближаются к 1.0.

Но для нас — алготрейдеров — важна не корреляция цен, а корреляция сигналов. Если стратегия основана на моментуме, и BTC запускает сигнал на вход — с высокой вероятностью ETH и SOL запустят аналогичный сигнал в ту же минуту. Все пары входят в лонг одновременно, все выходят одновременно. Десять позиций — но, по сути, одна ставка.

Почему 10 пар ≠ 10x диверсификация

10 коррелированных пар схлопываются в 3 эффективных

Формальная постановка

Пусть стратегия на каждой из NN пар активна pp долю времени. Если бы сигналы были полностью независимы, вероятность того, что хотя бы одна пара активна:

P(1 active)=1(1p)NP(\geq 1\ \text{active}) = 1 - (1 - p)^N

Для Strategy B (p=0.05p = 0.05, N=10N = 10):

P(1)=10.9510=10.598740.1%P(\geq 1) = 1 - 0.95^{10} = 1 - 0.5987 \approx 40.1\%

Но сигналы не независимы. Криптовалюты движутся синхронно — а значит, сигналы возникают и гаснут кластерами.

Корреляция превращает 10 пар в 3

Интуиция такая: если 10 пар коррелированы, они несут информацию не 10 независимых источников, а, скажем, 3-4. Формализуем это через effective_N:

Neff=NCfN_{eff} = \frac{N}{C_f}

где CfC_fcorrelation factor, отражающий среднюю попарную корреляцию сигналов. При Cf=1C_f = 1 пары полностью независимы, при Cf=NC_f = N — идентичны.

Для крипто-пар типичный Cf3C_f \approx 3. Тогда:

Neff=1033.3N_{eff} = \frac{10}{3} \approx 3.3

P(1)=10.953.310.844=15.6%P(\geq 1) = 1 - 0.95^{3.3} \approx 1 - 0.844 = 15.6\%

Не 40%, а 15.6%. Разница в 2.5 раза. Fill efficiency падает соответственно, и вместе с ней — эффективная доходность всего портфеля (см. PnL по активному времени).

Корреляция в крипто-рынках

Crypto signal correlation matrix heatmap

BTC как доминирующий фактор

Крипто-рынок имеет ярко выраженную факторную структуру. BTC объясняет 60-80% дисперсии дневных доходностей большинства альткоинов. Это хорошо видно через PCA (Principal Component Analysis):

import numpy as np
from sklearn.decomposition import PCA

def analyze_crypto_factor_structure(returns_matrix: np.ndarray, pair_names: list) -> dict:
    """
    PCA-анализ факторной структуры крипто-доходностей.

    Args:
        returns_matrix: матрица доходностей [n_days x n_pairs]
        pair_names: список названий пар
    """
    pca = PCA()
    pca.fit(returns_matrix)

    explained = pca.explained_variance_ratio_
    cumulative = np.cumsum(explained)

    print("Factor structure:")
    for i, (var, cum) in enumerate(zip(explained[:5], cumulative[:5])):
        print(f"  PC{i+1}: {var:.1%} variance (cumulative: {cum:.1%})")

    loadings = pca.components_[0]
    print("\nPC1 loadings (BTC factor):")
    for name, load in sorted(zip(pair_names, loadings), key=lambda x: -abs(x[1])):
        print(f"  {name}: {load:.3f}")

    return {
        "explained_variance": explained,
        "n_effective_factors": int(np.searchsorted(cumulative, 0.90)) + 1,
        "pc1_loadings": dict(zip(pair_names, loadings)),
    }

Типичный результат для портфеля из 10 крипто-пар:

Компонент Объяснённая дисперсия Кумулятивная
PC1 (BTC) 65% 65%
PC2 12% 77%
PC3 8% 85%
PC4 5% 90%
PC5-PC10 10% 100%

Четыре фактора объясняют 90% дисперсии. Из 10 пар «независимых» — не больше 4.

Корреляция сигналов vs. корреляция цен

Здесь важный нюанс. Корреляция цен и корреляция сигналов — разные вещи. Цены BTC и ETH коррелированы на 0.85, но сигналы конкретной стратегии могут быть коррелированы на 0.95 или на 0.50 — в зависимости от логики входа.

Пример: стратегия на основе RSI overbought/oversold. RSI на BTC пересекает 30 (oversold) — вход в лонг. ETH в тот же момент тоже может быть oversold (корреляция сигналов ~0.90). А может не быть, если ETH падал медленнее (корреляция сигналов ~0.40).

Правильный подход — измерять корреляцию именно сигналов, а не ценовых рядов:

import numpy as np
from itertools import combinations

def signal_correlation_matrix(
    signals: dict,  # {pair: np.array of 0/1 per minute}
    method: str = "pearson",
) -> np.ndarray:
    """
    Расчёт матрицы корреляции сигналов (бинарных: 0 = вне позиции, 1 = в позиции).

    Args:
        signals: словарь {pair_name: binary_signal_array}
        method: метод корреляции ("pearson", "jaccard")
    """
    pairs = sorted(signals.keys())
    n = len(pairs)
    corr_matrix = np.ones((n, n))

    for i, j in combinations(range(n), 2):
        s_i = signals[pairs[i]]
        s_j = signals[pairs[j]]

        if method == "pearson":
            corr = np.corrcoef(s_i, s_j)[0, 1]
        elif method == "jaccard":
            intersection = np.sum(s_i & s_j)
            union = np.sum(s_i | s_j)
            corr = intersection / union if union > 0 else 0
        else:
            raise ValueError(f"Unknown method: {method}")

        corr_matrix[i, j] = corr
        corr_matrix[j, i] = corr

    return corr_matrix, pairs


def estimate_correlation_factor(corr_matrix: np.ndarray) -> float:
    """
    Оценка correlation_factor из матрицы корреляции сигналов.

    correlation_factor = 1 + (N-1) * mean_pairwise_correlation

    При корреляции 0 → C_f = 1 (все независимы).
    При корреляции 1 → C_f = N (все идентичны).
    """
    n = corr_matrix.shape[0]
    upper_triangle = corr_matrix[np.triu_indices(n, k=1)]
    mean_corr = np.mean(upper_triangle)

    correlation_factor = 1 + (n - 1) * mean_corr
    return correlation_factor

Временная корреляция: спокойствие vs. паника

Временные режимы корреляции: спокойный рынок vs. паника

Корреляция не статична. В спокойные периоды BTC и альты могут расходиться — ETH растёт на новостях Ethereum, SOL — на новостях Solana. В кризис всё коллапсирует в один фактор: risk-on/risk-off.

def rolling_correlation_factor(
    signals: dict,
    window_days: int = 30,
    step_days: int = 7,
) -> list:
    """
    Скользящий correlation_factor для обнаружения смены режимов.
    """
    pairs = sorted(signals.keys())
    minutes_per_day = 1440
    window = window_days * minutes_per_day
    step = step_days * minutes_per_day

    total_minutes = len(signals[pairs[0]])
    results = []

    for start in range(0, total_minutes - window, step):
        end = start + window
        window_signals = {p: signals[p][start:end] for p in pairs}

        corr_matrix, _ = signal_correlation_matrix(window_signals)
        cf = estimate_correlation_factor(corr_matrix)

        results.append({
            "start_minute": start,
            "end_minute": end,
            "correlation_factor": cf,
            "effective_n": len(pairs) / cf,
        })

    return results

Типичная картина для 10 крипто-пар:

Режим рынка Средняя корреляция сигналов CfC_f NeffN_{eff}
Боковик (low vol) 0.15-0.25 2.4-3.3 3.0-4.2
Тренд вверх 0.25-0.40 3.3-4.6 2.2-3.0
Тренд вниз 0.30-0.50 3.7-5.5 1.8-2.7
Паника (crash) 0.60-0.90 6.4-9.1 1.1-1.6

В панике 10 пар сжимаются до 1-2 эффективных. Именно тогда, когда диверсификация нужнее всего, она исчезает. Это криптовалютный аналог классического «correlations go to 1 in a crisis».

effective_N: ключевая концепция

Effective N: снижение размерности из коррелированных пар

Формула и вывод

Идея effective_N заимствована из статистики, где effective sample size учитывает автокорреляцию наблюдений. Для наших целей:

Neff=N1+(N1)ρˉN_{eff} = \frac{N}{1 + (N - 1) \cdot \bar{\rho}}

где ρˉ\bar{\rho} — средняя попарная корреляция сигналов. Упрощённая запись:

Neff=NCf,Cf=1+(N1)ρˉN_{eff} = \frac{N}{C_f}, \quad C_f = 1 + (N - 1) \cdot \bar{\rho}

Свойства:

  • При ρˉ=0\bar{\rho} = 0: Cf=1C_f = 1, Neff=NN_{eff} = N — полная независимость
  • При ρˉ=1\bar{\rho} = 1: Cf=NC_f = N, Neff=1N_{eff} = 1 — все пары идентичны
  • При ρˉ=0.25\bar{\rho} = 0.25 и N=10N = 10: Cf=3.25C_f = 3.25, Neff=3.08N_{eff} = 3.08

Как оценить correlation_factor из данных

На практике есть три подхода:

1. Из матрицы корреляции сигналов (точный).

Прогоняете стратегию на всех парах, получаете бинарные сигналы (0/1 для каждой минуты), строите матрицу корреляции, вычисляете CfC_f по формуле выше.

2. Из PCA ценовых доходностей (приближённый).

Если сигналы сильно зависят от ценовой динамики (моментум, mean-reversion), можно оценить NeffN_{eff} как число компонент PCA, объясняющих 90% дисперсии.

3. Из эвристики по классам активов (грубый).

Класс активов Типичный CfC_f
Крипто (топ-10) 2.5-4.0
Крипто (с DeFi/мемкоинами) 2.0-3.0
Форекс (majors) 1.5-2.5
Акции (один сектор) 2.0-3.5
Акции (кросс-секторные) 1.2-1.8

Для крипто-портфеля из BTC, ETH, SOL, AVAX, MATIC, DOGE, DOT, LINK, UNI, ATOM безопасная оценка — Cf3C_f \approx 3.

Моделирование утилизации слотов

Дашборд утилизации слотов оркестратора

Формула P(1 active)P(\geq 1\ \text{active})

Базовая формула с учётом корреляции:

P(1 active)=1(1p)NeffP(\geq 1\ \text{active}) = 1 - (1 - p)^{N_{eff}}

Таблица для разных стратегий и числа пар (Cf=3C_f = 3):

Strategy pp (trading time) 5 пар (Neff=1.7N_{eff}=1.7) 10 пар (Neff=3.3N_{eff}=3.3) 20 пар (Neff=6.7N_{eff}=6.7) 50 пар (Neff=16.7N_{eff}=16.7)
Strategy B 5% 8.2% 15.6% 29.1% 58.0%
Strategy A 15% 23.6% 41.8% 65.9% 92.8%
Strategy C 45% 67.1% 89.0% 98.8% ~100%

Для Strategy B с 5% активности нужно 50 пар, чтобы хотя бы половину времени иметь хоть одну активную позицию. А ведь это ещё без учёта того, что 50 крипто-пар коррелированы сильнее, чем 10.

Мульти-слотовый оркестратор

Реальный оркестратор управляет несколькими слотами одновременно. Если у вас 5 слотов и 10 пар, утилизация считается иначе:

utilization=min(E[active],max_slots)max_slots\text{utilization} = \frac{\min(E[\text{active}], \text{max\_slots})}{\text{max\_slots}}

E[active]=NeffpE[\text{active}] = N_{eff} \cdot p

def estimate_fill_efficiency(
    trading_time_pct: float,
    n_pairs: int,
    correlation_factor: float = 3.0,
    max_slots: int = 1,
) -> dict:
    """
    Аналитическая оценка fill efficiency для мульти-слотового оркестратора.

    Args:
        trading_time_pct: доля активного времени одной стратегии на одной паре
        n_pairs: количество торговых пар
        correlation_factor: коэффициент корреляции сигналов
        max_slots: максимальное число одновременных позиций

    Returns:
        dict с метриками утилизации
    """
    effective_n = n_pairs / correlation_factor

    p_at_least_one = 1 - (1 - trading_time_pct) ** effective_n

    expected_active = effective_n * trading_time_pct

    utilization = min(expected_active, max_slots) / max_slots

    fill_efficiency = min(p_at_least_one, utilization)

    return {
        "effective_n": effective_n,
        "p_at_least_one": p_at_least_one,
        "expected_active": expected_active,
        "utilization": utilization,
        "fill_efficiency": fill_efficiency,
    }


configs = [
    ("Strategy B, 10 пар, 1 слот", 0.05, 10, 3.0, 1),
    ("Strategy B, 10 пар, 3 слота", 0.05, 10, 3.0, 3),
    ("Strategy B, 30 пар, 1 слот", 0.05, 30, 3.0, 1),
    ("Strategy A, 10 пар, 1 слот", 0.15, 10, 3.0, 1),
    ("Strategy C, 10 пар, 1 слот", 0.45, 10, 3.0, 1),
    ("Strategy C, 10 пар, 5 слотов", 0.45, 10, 3.0, 5),
]

for name, p, n, cf, slots in configs:
    result = estimate_fill_efficiency(p, n, cf, slots)
    print(f"{name}:")
    print(f"  N_eff = {result['effective_n']:.1f}")
    print(f"  P(≥1 active) = {result['p_at_least_one']:.1%}")
    print(f"  E[active] = {result['expected_active']:.2f}")
    print(f"  fill_efficiency = {result['fill_efficiency']:.1%}")
    print()

Ожидаемый вывод:

Strategy B, 10 пар, 1 слот:
  N_eff = 3.3
  P(≥1 active) = 15.6%
  E[active] = 0.17
  fill_efficiency = 15.6%

Strategy B, 10 пар, 3 слота:
  N_eff = 3.3
  P(≥1 active) = 15.6%
  E[active] = 0.17
  fill_efficiency = 5.6%

Strategy B, 30 пар, 1 слот:
  N_eff = 10.0
  P(≥1 active) = 40.1%
  E[active] = 0.50
  fill_efficiency = 40.1%

Strategy A, 10 пар, 1 слот:
  N_eff = 3.3
  P(≥1 active) = 41.8%
  E[active] = 0.50
  fill_efficiency = 41.8%

Strategy C, 10 пар, 1 слот:
  N_eff = 3.3
  P(≥1 active) = 89.0%
  E[active] = 1.50
  fill_efficiency = 89.0%

Strategy C, 10 пар, 5 слотов:
  N_eff = 3.3
  P(≥1 active) = 89.0%
  E[active] = 1.50
  fill_efficiency = 30.0%

Обратите внимание: Strategy B с 3 слотами и 10 парами показывает fill_efficiency 5.6%. Три слота бессмысленны, если ожидаемое число активных пар всего 0.17. Слоты следует назначать пропорционально ожидаемой загрузке.

Симуляция из реальных данных

Аналитическая модель — приближение. Для точной оценки нужна симуляция на реальных сигналах:

import numpy as np

def simulate_fill_efficiency(
    all_signals: dict,       # {(strategy, pair): [(entry_min, exit_min), ...]}
    max_slots: int = 10,
    test_period_minutes: int = 750 * 24 * 60,  # 750 дней
    priority_fn=None,        # функция приоритета для выбора позиций
) -> dict:
    """
    Симуляция реальной загрузки слотов оркестратора.

    Для каждой минуты: считаем, сколько пар хотят войти в позицию,
    и сколько слотов реально заняты (с учётом лимита).

    Args:
        all_signals: сигналы по парам и стратегиям
        max_slots: максимальное число одновременных позиций
        test_period_minutes: длина тестового периода в минутах
        priority_fn: если None — FIFO; иначе — функция ранжирования
    """
    demand_timeline = np.zeros(test_period_minutes, dtype=np.int32)
    capped_timeline = np.zeros(test_period_minutes, dtype=np.int32)

    for signals in all_signals.values():
        for entry_min, exit_min in signals:
            if entry_min < test_period_minutes:
                end = min(exit_min, test_period_minutes)
                demand_timeline[entry_min:end] += 1

    capped_timeline = np.minimum(demand_timeline, max_slots)

    total_demand = np.sum(demand_timeline)
    total_filled = np.sum(capped_timeline)
    time_with_any_active = np.sum(demand_timeline > 0)

    fill_efficiency = np.mean(capped_timeline) / max_slots
    demand_fill_ratio = total_filled / total_demand if total_demand > 0 else 0
    time_utilization = time_with_any_active / test_period_minutes

    slot_distribution = {}
    for s in range(max_slots + 1):
        slot_distribution[s] = np.mean(capped_timeline == s)

    return {
        "fill_efficiency": fill_efficiency,
        "demand_fill_ratio": demand_fill_ratio,
        "time_utilization": time_utilization,
        "avg_demand": np.mean(demand_timeline),
        "avg_filled": np.mean(capped_timeline),
        "slot_distribution": slot_distribution,
        "overflow_pct": np.mean(demand_timeline > max_slots),
    }

Симуляция на реальных данных часто показывает ещё более низкую утилизацию, чем аналитическая оценка, потому что учитывает кластеризацию сигналов во времени: все пары входят одновременно в кластере, создавая overflow, а затем все замолкают, создавая пустоту.

Сколько пар мониторить? Анализ убывающей отдачи

Effective N and diminishing returns curve

Ключевой вопрос: при какой NN добавление ещё одной пары перестаёт заметно увеличивать fill_efficiency?

import numpy as np

def diminishing_returns_analysis(
    trading_time_pct: float,
    correlation_factor: float = 3.0,
    max_pairs: int = 100,
    target_utilization: float = 0.80,
) -> dict:
    """
    Анализ убывающей отдачи от добавления новых пар.
    """
    results = []
    target_n = None

    for n in range(1, max_pairs + 1):
        n_eff = n / correlation_factor
        p_active = 1 - (1 - trading_time_pct) ** n_eff
        marginal = 0
        if n > 1:
            prev_eff = (n - 1) / correlation_factor
            prev_p = 1 - (1 - trading_time_pct) ** prev_eff
            marginal = p_active - prev_p

        results.append({
            "n_pairs": n,
            "n_effective": n_eff,
            "p_at_least_one": p_active,
            "marginal_gain": marginal,
        })

        if target_n is None and p_active >= target_utilization:
            target_n = n

    return {
        "results": results,
        "target_n_for_utilization": target_n,
    }


analysis_b = diminishing_returns_analysis(0.05, correlation_factor=3.0, target_utilization=0.80)
print(f"Strategy B: нужно {analysis_b['target_n_for_utilization']} пар для 80% P(≥1)")

for r in analysis_b["results"]:
    if r["n_pairs"] in [1, 3, 5, 10, 20, 30, 50, 80]:
        print(f"  N={r['n_pairs']:3d}: N_eff={r['n_effective']:.1f}, "
              f"P(≥1)={r['p_at_least_one']:.1%}, "
              f"marginal={r['marginal_gain']:.2%}")

Результат для Strategy B (p=0.05p = 0.05, Cf=3C_f = 3):

NN пар NeffN_{eff} P(1)P(\geq 1) Marginal gain
1 0.3 1.7%
3 1.0 5.0% +1.7%
5 1.7 8.2% +1.6%
10 3.3 15.6% +1.4%
20 6.7 29.1% +1.1%
30 10.0 40.1% +0.9%
50 16.7 58.0% +0.6%
80 26.7 74.5% +0.4%

Для Strategy B достичь 80% утилизации одним слотом невозможно даже при 100 парах (нужно ~96 пар). Это фундаментальное ограничение: стратегия с 5% trading time не подходит для работы на одном слоте — ей нужен портфельный подход с несколькими стратегиями.

Для Strategy A (p=0.15p = 0.15, Cf=3C_f = 3):

NN пар NeffN_{eff} P(1)P(\geq 1) Marginal gain
5 1.7 23.6%
10 3.3 41.8% +3.3%
20 6.7 65.9% +2.1%
30 10.0 80.3% +1.2%

Strategy A достигает 80% утилизации при ~30 парах. Marginal gain на 30-й паре — лишь +1.2%.

Для Strategy C (p=0.45p = 0.45, Cf=3C_f = 3):

NN пар NeffN_{eff} P(1)P(\geq 1)
3 1.0 45.0%
5 1.7 67.1%
10 3.3 89.0%
15 5.0 95.0%

Strategy C с 45% trading time достигает 90% утилизации уже при 10 парах. Добавлять больше — смысла нет.

Деградация edge на разных парах

Деградация edge по торговым парам

Есть ещё один фактор, который ограничивает число пар: деградация edge. Стратегия, разработанная и оптимизированная на BTC/USDT, может работать хуже на менее ликвидных альтах.

Причины деградации:

  • Ликвидность: проскальзывание на DOGE/USDT в разы выше, чем на BTC/USDT
  • Спреды: менее ликвидные пары имеют более широкий bid-ask спред
  • Микроструктура: паттерны стакана различаются между парами
  • Манипуляции: малоликвидные альты подвержены pump-and-dump
def edge_decay_analysis(
    strategy_results: dict,  # {pair: {"pnl_per_day": float, "n_trades": int}}
    min_trades: int = 30,
) -> list:
    """
    Ранжирование пар по edge с учётом деградации.
    """
    ranked = []
    for pair, metrics in strategy_results.items():
        if metrics["n_trades"] < min_trades:
            continue
        ranked.append({
            "pair": pair,
            "pnl_per_day": metrics["pnl_per_day"],
            "n_trades": metrics["n_trades"],
            "sharpe": metrics.get("sharpe", 0),
        })

    ranked.sort(key=lambda x: x["pnl_per_day"], reverse=True)

    cumulative_pnl = []
    running_sum = 0
    for i, r in enumerate(ranked):
        running_sum += r["pnl_per_day"]
        avg = running_sum / (i + 1)
        cumulative_pnl.append({
            "n_pairs": i + 1,
            "last_added": r["pair"],
            "last_pnl_per_day": r["pnl_per_day"],
            "avg_pnl_per_day": avg,
        })

    return cumulative_pnl

Типичная картина:

# пар Последняя добавленная PnL/day последней Средний PnL/day
1 BTC/USDT 0.89% 0.89%
2 ETH/USDT 0.82% 0.86%
3 SOL/USDT 0.71% 0.81%
5 AVAX/USDT 0.55% 0.73%
8 DOT/USDT 0.31% 0.61%
10 DOGE/USDT 0.12% 0.53%

Добавление 10-й пары снижает средний PnL/day портфеля. На 8-й паре edge уже вдвое ниже, чем на лучшей. Нужен баланс между fill_efficiency (растёт с числом пар) и средним edge (падает).

Оптимальное число пар: объединённая модель

Оптимальное число пар: пересечение fill efficiency и среднего edge

Объединим fill_efficiency и edge decay в одну метрику — expected portfolio PnL per day:

Portfolio PnL/day=avg_edge(N)×fill_efficiency(N)×365\text{Portfolio PnL/day} = \text{avg\_edge}(N) \times \text{fill\_efficiency}(N) \times 365

def optimal_pairs_count(
    pair_edges: list,           # PnL/day по убыванию: [0.89, 0.82, 0.71, ...]
    trading_time_pct: float,
    correlation_factor: float = 3.0,
    max_slots: int = 1,
) -> dict:
    """
    Поиск оптимального числа пар, максимизирующего portfolio PnL.
    """
    best_n = 0
    best_score = 0
    results = []

    for n in range(1, len(pair_edges) + 1):
        avg_edge = np.mean(pair_edges[:n])

        n_eff = n / correlation_factor
        p_active = 1 - (1 - trading_time_pct) ** n_eff
        expected_active = n_eff * trading_time_pct
        utilization = min(expected_active, max_slots) / max_slots
        fill_eff = min(p_active, utilization)

        portfolio_score = avg_edge * fill_eff * 365

        results.append({
            "n_pairs": n,
            "avg_edge": avg_edge,
            "fill_efficiency": fill_eff,
            "portfolio_annualized": portfolio_score,
        })

        if portfolio_score > best_score:
            best_score = portfolio_score
            best_n = n

    return {
        "optimal_n": best_n,
        "optimal_score": best_score,
        "results": results,
    }


edges = [0.89, 0.82, 0.71, 0.65, 0.55, 0.48, 0.40, 0.31, 0.22, 0.12,
         0.08, 0.05, 0.02, -0.01, -0.05]

opt = optimal_pairs_count(edges, trading_time_pct=0.15, correlation_factor=3.0)
print(f"Оптимальное число пар: {opt['optimal_n']}")
print(f"Portfolio annualized: {opt['optimal_score']:.1f}%")

for r in opt["results"]:
    print(f"  N={r['n_pairs']:2d}: avg_edge={r['avg_edge']:.2f}%, "
          f"fill_eff={r['fill_efficiency']:.1%}, "
          f"portfolio={r['portfolio_annualized']:.1f}%")

Оптимум обычно находится в точке, где marginal fill_efficiency от добавления пары перестаёт компенсировать снижение среднего edge. Для типичного крипто-портфеля:

  • Strategy B (5% time): оптимум при 8-12 парах
  • Strategy A (15% time): оптимум при 6-10 парах
  • Strategy C (45% time): оптимум при 4-6 парах

Парадокс: стратегия с наименьшим trading time получает выгоду от наибольшего числа пар, но при этом fill_efficiency всё равно остаётся низким. Решение — не больше пар, а комбинация с другими стратегиями (см. Combo-стратегии).

Кросс-парная диверсификация: стратегии снижения корреляции

Сеть кросс-стратегической диверсификации

Если нельзя увеличить число пар бесконечно, можно снизить CfC_f — то есть повысить разнообразие сигналов.

Стратегия 1: микс ликвидных и DeFi-токенов

BTC, ETH, BNB коррелированы очень сильно. Но UNI (DEX), AAVE (lending), CRV (стейблкоины) могут иметь собственные драйверы. Добавление DeFi-токенов снижает средний ρˉ\bar{\rho} с 0.35 до 0.20-0.25:

Cf=1+9×0.20=2.8(vs. 3.25 при ρˉ=0.25)C_f = 1 + 9 \times 0.20 = 2.8 \quad \text{(vs. 3.25 при } \bar{\rho} = 0.25\text{)}

Стратегия 2: разные стратегии на одних парах

Вместо 10 пар с одной стратегией — 5 пар с двумя разными стратегиями. Если стратегии основаны на разных принципах (моментум vs. mean-reversion), их сигналы могут быть антикоррелированы:

  • Моментум входит в лонг, когда цена растёт
  • Mean-reversion входит в лонг, когда цена падает

ρˉcross-strategy<0    Cf<1    Neff>N\bar{\rho}_{\text{cross-strategy}} < 0 \implies C_f < 1 \implies N_{eff} > N

Это единственный способ получить Neff>NN_{eff} > N — использовать стратегии с отрицательной корреляцией сигналов.

Стратегия 3: включение spot vs. futures

Арбитраж funding rates и спотовая торговля имеют другую корреляционную структуру. Добавление арбитражных стратегий в портфель существенно снижает общий CfC_f, потому что арбитраж по определению эксплуатирует расхождения, а не совпадения.

Практические рекомендации по типам стратегий

High-frequency low-time стратегии (trading time < 10%)

Типичный представитель: Strategy B (5% time, 38 сделок за 750 дней).

  • Число пар: 10-15 (оптимум по balance edge/fill)
  • Проблема: fill_efficiency низкий даже при 15 парах (~20-25%)
  • Решение: обязательная комбинация с другими стратегиями в оркестраторе
  • CfC_f для крипто: 2.5-3.5
  • Мониторинг: скользящий CfC_f для адаптации числа пар к режиму рынка

Medium-time стратегии (trading time 10-30%)

Типичный представитель: Strategy A (15% time, 418 сделок за 750 дней).

  • Число пар: 6-10
  • Fill_efficiency при 10 парах: ~40%
  • Решение: комбинация 2-3 таких стратегий заполняет 80%+ времени
  • CfC_f для крипто: 2.5-3.5
  • Фокус: выбирать пары с максимальным edge, не гнаться за числом

High-time стратегии (trading time > 30%)

Типичный представитель: Strategy C (45% time).

  • Число пар: 4-6
  • Fill_efficiency при 6 парах: ~80%
  • Проблема: overflow — несколько пар активны одновременно, но слотов меньше
  • Решение: увеличить max_slots или добавить приоритизацию пар
  • CfC_f для крипто: 2.5-4.0 (выше из-за длинных позиций, перекрывающих кризисы)

Сводная таблица

Параметр Strategy B (5%) Strategy A (15%) Strategy C (45%)
Рекомендуемое NN 10-15 6-10 4-6
NeffN_{eff} при Cf=3C_f=3 3.3-5.0 2.0-3.3 1.3-2.0
Fill eff. (1 слот) 15-23% 32-42% 77-89%
Нужна комбинация? Обязательно Желательно Нет
Bottleneck Мало сигналов Баланс Overflow

Связь с другими метриками серии

Эта статья — одиннадцатая в серии «Бэктесты без иллюзий». Корреляция сигналов напрямую влияет на метрики из предыдущих статей:

  • PnL по активному времени: fill_efficiency — ключевой множитель в формуле эффективной доходности. Если вы завысили fill_efficiency, проигнорировав корреляцию, ваш прогноз portfolio PnL будет слишком оптимистичным.

  • Funding rates: при высокой корреляции позиции открываются одновременно — и funding costs растут линейно с числом слотов. Overflow + funding = ускоренное сжигание капитала.

  • Арбитраж funding rates: арбитражные стратегии — естественный диверсификатор, снижающий CfC_f портфеля. Их сигналы слабо коррелированы с моментумными и mean-reversion стратегиями.

  • Combo-стратегии (следующая статья): как собрать портфель из стратегий с разными pp и CfC_f, чтобы достичь 90%+ утилизации. Каскадная оркестрация учитывает корреляцию сигналов при назначении приоритетов.

Заключение

Диверсификация в крипто — не о количестве пар. 10 коррелированных пар дают эффект 3-4 независимых. В панике и того меньше.

Четыре вывода:

  1. Считайте effective_N, а не N. Для крипто-пар Cf3C_f \approx 3. Десять пар — это ~3.3 эффективных. Планируйте fill_efficiency исходя из NeffN_{eff}, не из NN.

  2. Измеряйте корреляцию сигналов, а не цен. Ценовая корреляция — прокси, но не замена. Прогоните стратегию на всех парах и посчитайте матрицу корреляции бинарных сигналов.

  3. Учитывайте деградацию edge. Больше пар — ниже средний PnL/day. Оптимум — в точке, где marginal fill_efficiency от новой пары ещё компенсирует снижение edge.

  4. Снижайте CfC_f, а не увеличивайте NN. Комбинация разных стратегий на тех же парах эффективнее, чем одна стратегия на большем числе пар. Кросс-стратегийная диверсификация может дать Neff>NN_{eff} > N.

Correlation factor — это скрытая переменная, которая определяет реалистичность ваших прогнозов утилизации и доходности. Игнорировать его — значит строить портфель на иллюзиях.


Полезные ссылки

  1. Markowitz, H. — Portfolio Selection (1952)
  2. López de Prado — Advances in Financial Machine Learning: Denoising and Detoning
  3. Ledoit, O. & Wolf, M. — Honey, I Shrunk the Sample Covariance Matrix (2004)
  4. Laloux, L. et al. — Noise Dressing of Financial Correlation Matrices (1999)
  5. Cont, R. — Empirical Properties of Asset Returns: Stylized Facts and Statistical Issues
  6. Ernest Chan — Algorithmic Trading: Portfolio Construction and Risk Management
  7. Rebonato, R. & Jäckel, P. — The Most General Methodology for Creating a Valid Correlation Matrix

Цитирование

@article{soloviov2026signalcorrelation,
  author = {Soloviov, Eugen},
  title = {Корреляция сигналов: сколько пар нужно мониторить},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/signal-correlation-pairs},
  version = {0.1.0},
  description = {Почему 10 крипто-пар не дают 10-кратную диверсификацию, как рассчитать effective\_N через correlation\_factor, и сколько пар действительно нужно мониторить для 80-90\% утилизации слотов оркестратора.}
}
Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

MarketMaker.cc Team

Количественные исследования и стратегии

Обсудить в Telegram
Newsletter

Будьте в курсе событий

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

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.