Автокорреляция (ACF) и частичная автокорреляция (PACF) в биржевом анализе

Автокорреляция (ACF) и частичная автокорреляция (PACF) являются мощными инструментами для выявления скрытых паттернов в ценовых рядах. Многие трейдеры и аналитики ограничиваются поверхностным применением этих концепций, используя их лишь для идентификации параметров ARIMA-моделей. Однако реальная сила ACF и PACF раскрывается при их правильном применении в контексте разработки торговых алгоритмов и риск-менеджмента.

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

Математические основы автокорреляции

Автокорреляция представляет собой корреляцию временного ряда с самим собой, сдвинутым на определенное количество периодов. Если во временном ряду между текущими и прошлыми значениями существует устойчивая взаимосвязь, то автокорреляция на это укажет.

Автокорреляция особенно важна при анализе финансовых и экономических данных, так как позволяет выявить скрытые паттерны, циклы или сезонные эффекты:

  • При положительной автокорреляции значения временного ряда имеют тенденцию повторять предыдущее направление;
  • При отрицательной автокорреляции — изменять его на противоположное.

Выявление и корректный учет автокорреляции является ключевым этапом при построении прогностических моделей и выборе соответствующих методов анализа временных рядов.

Таким образом автокорреляция — это не просто математическая абстракция. Это мощный инструмент для понимания внутренней структуры финансовых данных. Когда я впервые столкнулся с автокорреляцией в контексте анализа валютных пар, меня поразило, насколько четко она может выявлять периодические паттерны, которые совершенно не видны на обычных графиках цен.

Формула

Математически автокорреляция для лага k определяется как:

ρ(k) = Cov(X_t, X_{t-k}) / Var(X_t)

где X_t представляет значение временного ряда в момент времени t.

Однако за этой простой формулой скрывается глубокая структура временных зависимостей. В отличие от обычной корреляции между двумя различными переменными, автокорреляция раскрывает «память» временного ряда – насколько сильно текущие значения зависят от прошлых.

Практическая интерпретация коэффициентов автокорреляции

Значения автокорреляции варьируются от -1 до +1, однако их интерпретация в контексте финансовых рынков требует особого подхода. Положительная автокорреляция указывает на тенденцию к продолжению движения (momentum), тогда как отрицательная – на склонность к реверсии (mean reversion). Однако в реальных торговых условиях эти паттерны часто проявляются не так очевидно, как в учебниках.

Из моего опыта, наиболее информативными являются не абсолютные значения коэффициентов, а их относительные изменения во времени и паттерны распределения по различным лагам. Например, при анализе внутридневных данных по EUR/USD я обнаружил, что автокорреляция на лаге 15 минут часто меняет знак в периоды высокой волатильности, что может служить сигналом для корректировки размера позиции.

Функция автокорреляции (ACF): практическое применение в трейдинге

Построение и анализ ACF для финансовых данных

Функция автокорреляции показывает, как изменяется корреляция временного ряда с самим собой при увеличении лага. Для практического применения в трейдинге важно понимать, что ACF не просто описывает статистические свойства данных, но может выявлять торговые возможности, недоступные традиционным методам анализа.

import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import acf, pacf
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

# Загружаем данные по S&P 500
ticker = "^GSPC"
data = yf.download(ticker, start="2022-06-01", end="2025-06-01")
returns = data['Close'].pct_change().dropna()

# Вычисляем ACF для доходностей
acf_values = acf(returns, nlags=50, fft=True)
confidence_interval = 1.96 / np.sqrt(len(returns))

# Визуализация ACF
plt.figure(figsize=(14, 6))
plt.subplot(1, 2, 1)
plt.plot(range(len(acf_values)), acf_values, 'ko-', linewidth=2, markersize=6, color='black')
plt.axhline(y=0, color='gray', linestyle='-', alpha=0.3)
plt.axhline(y=confidence_interval, color='red', linestyle='--', alpha=0.7)
plt.axhline(y=-confidence_interval, color='red', linestyle='--', alpha=0.7)
plt.title('Автокорреляционная функция (ACF) доходностей S&P 500')
plt.xlabel('Лаг')
plt.ylabel('Автокорреляция')
plt.grid(True, alpha=0.3)

Автокорреляция доходностей SP500 за последние 3 года

Рис. 1: Автокорреляция доходностей SP500 за последние 3 года

При анализе этого кода важно обратить внимание на несколько моментов:

  1. Во-первых, я использую fft=True для ускорения вычислений при работе с большими датасетами;
  2. Во-вторых, доверительные интервалы рассчитываются как ±1.96/√n, что соответствует 95% уровню доверия при предположении о нормальном распределении.

Интерпретация паттернов ACF в контексте рыночной динамики

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

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

# Анализ автокорреляции волатильности
abs_returns = np.abs(returns)
squared_returns = returns ** 2

acf_abs = acf(abs_returns, nlags=50, fft=True)
acf_squared = acf(squared_returns, nlags=50, fft=True)

# Сравнительная визуализация
plt.figure(figsize=(15, 5))
plt.subplot(1, 3, 1)
plt.plot(acf_values, 'ko-', linewidth=2, color='black', label='Доходности')
plt.title('ACF доходностей')
plt.grid(True, alpha=0.3)

plt.subplot(1, 3, 2)
plt.plot(acf_abs, 'ko-', linewidth=2, color='darkgray', label='|Доходности|')
plt.title('ACF абсолютных доходностей')
plt.grid(True, alpha=0.3)

plt.subplot(1, 3, 3)
plt.plot(acf_squared, 'ko-', linewidth=2, color='dimgray', label='Доходности²')
plt.title('ACF квадратов доходностей')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Визуализации ACF стандартных доходностей, абсолютных доходностей и их квадратов

Рис. 2: Визуализации ACF стандартных доходностей, абсолютных доходностей и их квадратов

Этот анализ часто показывает, что хотя автокорреляция самих доходностей может быть незначительной, автокорреляция их абсолютных значений или квадратов остается существенной на многих лагах. Это явление известно как кластеризация волатильности и имеет критическое значение для риск-менеджмента.

Частичная автокорреляция (PACF): выделение прямых зависимостей

Концептуальные различия между ACF и PACF

Частичная автокорреляция устраняет влияние промежуточных лагов и показывает «чистую» корреляцию между наблюдениями, разделенными k периодами. Это принципиально важное различие, которое многие аналитики недооценивают. Если ACF показывает общую корреляционную структуру, то PACF выявляет прямые зависимости, исключая косвенные эффекты.

Математически PACF можно представить как последний коэффициент в авторегрессионной модели порядка k:

X_t = φ₁X_{t-1} + φ₂X_{t-2} + … + φₖX_{t-k} + ε_t

где φₖ и есть частичная автокорреляция для лага k.

Эта формула показывает, что PACF по сути отвечает на вопрос: «Какой дополнительный прогностический потенциал дает нам информация о значении ряда k периодов назад, если мы уже знаем все промежуточные значения?»

Практическое применение PACF для выявления структуры зависимостей

В количественном трейдинге PACF особенно полезна для определения оптимального горизонта прогнозирования и выявления значимых лагов для построения торговых сигналов. Из моего опыта, комбинация анализа ACF и PACF часто выявляет неочевидные структуры в данных, которые можно использовать для создания альфа-генерирующих стратегий.

# Расчет и визуализация PACF
pacf_values = pacf(returns, nlags=50, method='ols')

plt.figure(figsize=(12, 8))
plt.subplot(2, 1, 1)
plot_acf(returns, lags=50, ax=plt.gca(), color='black', linewidth=2)
plt.title('Автокорреляционная функция (ACF)')
plt.grid(True, alpha=0.3)

plt.subplot(2, 1, 2)
plot_pacf(returns, lags=50, ax=plt.gca(), color='black', linewidth=2)
plt.title('Частичная автокорреляционная функция (PACF)')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Выявление значимых лагов
significant_lags_acf = np.where(np.abs(acf_values[1:]) > confidence_interval)[0] + 1
significant_lags_pacf = np.where(np.abs(pacf_values[1:]) > confidence_interval)[0] + 1

print(f"Значимые лаги в ACF: {significant_lags_acf[:10]}")  # Показываем первые 10
print(f"Значимые лаги в PACF: {significant_lags_pacf[:10]}")

Значимые лаги в ACF: [ 4 18]
Значимые лаги в PACF: [ 4 18 33 36]

Визуализация ACF и PACF для индекса SP500

Рис. 3: Визуализация ACF и PACF для индекса SP500

Анализ значимых лагов в ACF и PACF часто показывает интересные различия. Например, если ACF показывает значимые корреляции на лагах 1, 2, 3, а PACF значима только на лаге 1, это указывает на авторегрессионную структуру первого порядка, где корреляции на больших лагах возникают косвенно через промежуточные значения.

Использование PACF для оптимизации торговых стратегий

Одно из наиболее практических применений PACF – определение оптимального набора лагов для построения торговых сигналов. Вместо использования произвольных периодов (как в традиционных индикаторах), PACF позволяет выбрать те лаги, которые действительно содержат независимую прогностическую информацию.

Ниже пример кода на Python с реализацией стратегии покупок и продаж индекса SP500 на основе лагов PACF.

def create_trading_signal(returns, significant_lags, pacf_values, threshold=0.0001):
    """
    Создание торгового сигнала на основе значительных лагов PACF
    """
    signals = pd.Series(index=returns.index, dtype=float)
    signals[:] = 0  
    
    if len(significant_lags) == 0:
        print("Значительных лагов не обнаружено, возвращаются нулевые сигналы.")
        return signals
    
    returns_array = returns.values
    
    print(f"Обработка {len(significant_lags)} значительных лагов: {significant_lags}")
    print(f"Порог: {threshold}")
    
    signal_count = 0
    weighted_signals = []  
    signal_details = []  
    
    for i in range(max(significant_lags), len(returns_array)):
        weighted_signal = 0.0
        for lag in significant_lags:
            if i - lag >= 0:
                weight = float(pacf_values[lag])  
                weighted_signal += weight * returns_array[i - lag]
        
        weighted_signals.append(weighted_signal)
        
        if weighted_signal > threshold:
            signals.iloc[i] = 1  # Сигнал на покупку
            signal_count += 1
            
            weight_scalar = weighted_signal.item() if hasattr(weighted_signal, 'item') else float(weighted_signal)
            signal_details.append((returns.index[i], 1, weight_scalar))
        elif weighted_signal < -threshold: signals.iloc[i] = -1 # Сигнал на продажу signal_count += 1 weight_scalar = weighted_signal.item() if hasattr(weighted_signal, 'item') else float(weighted_signal) signal_details.append((returns.index[i], -1, weight_scalar)) weighted_signals = np.array(weighted_signals) print(f"Статистика взвешенных сигналов: мин={weighted_signals.min():.6f}, макс={weighted_signals.max():.6f}") print(f"Взвешенные сигналы > порога ({threshold}): {np.sum(weighted_signals > threshold)}")
    print(f"Взвешенные сигналы < -порога ({-threshold}): {np.sum(weighted_signals < -threshold)}") print(f"Сгенерировано {signal_count} ненулевых сигналов из {len(signals)} периодов") return signals initial_threshold = 0.002 trading_signals = create_trading_signal(returns, significant_lags_pacf, pacf_values, threshold=initial_threshold) strategy_returns = trading_signals.shift(1) * returns strategy_returns = strategy_returns.fillna(0) print(f"Размер доходностей: {returns.shape}") print(f"Размер торговых сигналов: {trading_signals.shape}") print(f"Размер доходностей стратегии: {strategy_returns.shape}") # Проверяем наличие ненулевых доходностей стратегии non_zero_strategy_returns = strategy_returns[strategy_returns != 0] print(f"Ненулевые доходности стратегии: {len(non_zero_strategy_returns)}") # Безопасное преобразование в скалярные значения try: strategy_mean = strategy_returns.mean() strategy_std = strategy_returns.std() # Преобразовать в скаляр если это Series с одним значением if hasattr(strategy_mean, 'iloc'): strategy_mean = float(strategy_mean.iloc[0]) if len(strategy_mean) > 0 else 0.0
    else:
        strategy_mean = float(strategy_mean) if not pd.isna(strategy_mean) else 0.0
        
    if hasattr(strategy_std, 'iloc'):
        strategy_std = float(strategy_std.iloc[0]) if len(strategy_std) > 0 else 0.0
    else:
        strategy_std = float(strategy_std) if not pd.isna(strategy_std) else 0.0
        
    print(f"Статистика доходностей стратегии: среднее={strategy_mean:.6f}, стд_откл={strategy_std:.6f}")
except Exception as e:
    print(f"Ошибка вычисления статистики стратегии: {e}")
    strategy_mean, strategy_std = 0.0, 0.0

# Отладка: проверка торговых сигналов
non_zero_signals = trading_signals[trading_signals != 0]
print(f"Ненулевые торговые сигналы: {len(non_zero_signals)}")
signal_counts = trading_signals.value_counts().to_dict()
print(f"Распределение сигналов: {signal_counts}")

# Детальный анализ причин нулевых доходностей стратегии
print("\nДетальный анализ стратегии:")
print(f"Диапазон торговых сигналов: {trading_signals.min()} до {trading_signals.max()}")

# Исправление отображения доходностей стратегии - используем values для получения массива numpy
strategy_values = strategy_returns.values
print(f"Диапазон доходностей стратегии: {strategy_values.min():.6f} до {strategy_values.max():.6f}")
print(f"Сумма доходностей стратегии: {strategy_values.sum():.6f}")

# Проверка конкретных дней с сигналами
signal_days = trading_signals[trading_signals != 0]
if len(signal_days) > 0:
    print(f"\nДни с сигналами:")
    for i, (date, signal) in enumerate(signal_days.head(5).items()):
        # Безопасное извлечение значений
        ret_on_day = strategy_returns.loc[date]
        underlying_ret = returns.loc[date] if date in returns.index else 0.0
        
        # Преобразование в скаляр при необходимости
        if hasattr(ret_on_day, 'values'):
            ret_on_day = ret_on_day.values[0] if len(ret_on_day.values) > 0 else 0.0
        if hasattr(underlying_ret, 'values'):
            underlying_ret = underlying_ret.values[0] if len(underlying_ret.values) > 0 else 0.0
            
        print(f"  {date}: сигнал={signal:.0f}, доходность_стратегии={ret_on_day:.6f}, базовая_доходность={underlying_ret:.6f}")

# Альтернативная реализация стратегии для сравнения
print("\nПроверка альтернативной реализации стратегии...")
strategy_returns_alt = pd.Series(index=returns.index, dtype=float)
strategy_returns_alt[:] = 0

for i in range(1, len(trading_signals)):
    prev_signal = trading_signals.iloc[i-1]  # Сигнал предыдущего дня
    current_return = returns.iloc[i]  # Доходность текущего дня
    strategy_returns_alt.iloc[i] = prev_signal * current_return

# Используем values для получения массивов numpy для безопасных операций
alt_values = strategy_returns_alt.values
strategy_sum = strategy_values.sum()

print(f"Диапазон альтернативных доходностей стратегии: {alt_values.min():.6f} до {alt_values.max():.6f}")
print(f"Сумма альтернативной стратегии: {alt_values.sum():.6f}")

# Использовать альтернативную если она лучше
if abs(alt_values.sum()) > abs(strategy_sum):
    print("Используем альтернативную реализацию стратегии")
    strategy_returns = strategy_returns_alt

# Обработка случая когда стратегия не генерирует сигналы
if len(non_zero_signals) == 0:
    print("Предупреждение: Стратегия не сгенерировала торговых сигналов. Порог может быть слишком высокий.")
    print("Пробуем с более низким порогом...")
    # Попробовать с более низким порогом
    trading_signals = create_trading_signal(returns, significant_lags_pacf, pacf_values, threshold=0.001)
    strategy_returns = trading_signals.shift(1) * returns
    non_zero_signals = trading_signals[trading_signals != 0]
    print(f"С более низким порогом - Ненулевые сигналы: {len(non_zero_signals)}")

# Анализ производительности
cumulative_returns = (1 + strategy_returns).cumprod()
benchmark_returns = (1 + returns).cumprod()

# Обработка потенциальных NaN значений в кумулятивных доходностях
cumulative_returns = cumulative_returns.ffill().fillna(1.0)
benchmark_returns = benchmark_returns.ffill().fillna(1.0)

plt.figure(figsize=(12, 6))
plt.plot(cumulative_returns.index, cumulative_returns, 'k-', linewidth=2, label='PACF Стратегия')
plt.plot(benchmark_returns.index, benchmark_returns, '--', color='gray', linewidth=2, label='Покупай и держи')
plt.title('Сравнение PACF стратегии с бенчмарком')
plt.xlabel('Дата')
plt.ylabel('Кумулятивная доходность')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# Метрики производительности
# Обработка деления на ноль для расчета коэффициента Шарпа с безопасным преобразованием
try:
    strategy_std_calc = strategy_returns.std()
    returns_std_calc = returns.std()
    strategy_mean_calc = strategy_returns.mean()
    returns_mean_calc = returns.mean()
    
    # Безопасное скалярное преобразование
    if hasattr(strategy_std_calc, 'iloc'):
        strategy_std = float(strategy_std_calc.iloc[0]) if len(strategy_std_calc) > 0 else 0.0
    else:
        strategy_std = float(strategy_std_calc) if not pd.isna(strategy_std_calc) else 0.0
        
    if hasattr(returns_std_calc, 'iloc'):
        returns_std = float(returns_std_calc.iloc[0]) if len(returns_std_calc) > 0 else 0.0
    else:
        returns_std = float(returns_std_calc) if not pd.isna(returns_std_calc) else 0.0
        
    if hasattr(strategy_mean_calc, 'iloc'):
        strategy_mean = float(strategy_mean_calc.iloc[0]) if len(strategy_mean_calc) > 0 else 0.0
    else:
        strategy_mean = float(strategy_mean_calc) if not pd.isna(strategy_mean_calc) else 0.0
        
    if hasattr(returns_mean_calc, 'iloc'):
        returns_mean = float(returns_mean_calc.iloc[0]) if len(returns_mean_calc) > 0 else 0.0
    else:
        returns_mean = float(returns_mean_calc) if not pd.isna(returns_mean_calc) else 0.0

except Exception as e:
    print(f"Ошибка в расчете метрик: {e}")
    strategy_std = returns_std = strategy_mean = returns_mean = 0.0

if strategy_std > 0:
    strategy_sharpe = strategy_mean / strategy_std * np.sqrt(252)
else:
    strategy_sharpe = 0
    print("Предупреждение: Доходности стратегии имеют нулевое стандартное отклонение")

if returns_std > 0:
    benchmark_sharpe = returns_mean / returns_std * np.sqrt(252)
else:
    benchmark_sharpe = 0
    print("Предупреждение: Доходности бенчмарка имеют нулевое стандартное отклонение")

print(f"Коэффициент Шарпа стратегии: {strategy_sharpe:.3f}")
print(f"Коэффициент Шарпа бенчмарка: {benchmark_sharpe:.3f}")

# Дополнительные метрики производительности
try:
    # Безопасное извлечение финальных значений
    final_strategy = cumulative_returns.iloc[-1]
    final_benchmark = benchmark_returns.iloc[-1]
    
    # Преобразование в скаляр при необходимости
    if hasattr(final_strategy, 'iloc'):
        total_strategy_return = float(final_strategy.iloc[0] - 1) if len(final_strategy) > 0 else -1.0
    else:
        total_strategy_return = float(final_strategy - 1) if not pd.isna(final_strategy) else -1.0
        
    if hasattr(final_benchmark, 'iloc'):
        total_benchmark_return = float(final_benchmark.iloc[0] - 1) if len(final_benchmark) > 0 else -1.0
    else:
        total_benchmark_return = float(final_benchmark - 1) if not pd.isna(final_benchmark) else -1.0

    # Безопасный расчет просадок
    strategy_running_max = cumulative_returns.expanding().max()
    benchmark_running_max = benchmark_returns.expanding().max()
    
    strategy_drawdown = (cumulative_returns / strategy_running_max - 1)
    benchmark_drawdown = (benchmark_returns / benchmark_running_max - 1)
    
    max_dd_strategy = strategy_drawdown.min()
    max_dd_benchmark = benchmark_drawdown.min()
    
    # Преобразование в скаляр при необходимости
    if hasattr(max_dd_strategy, 'iloc'):
        max_drawdown_strategy = float(max_dd_strategy.iloc[0]) if len(max_dd_strategy) > 0 else 0.0
    else:
        max_drawdown_strategy = float(max_dd_strategy) if not pd.isna(max_dd_strategy) else 0.0
        
    if hasattr(max_dd_benchmark, 'iloc'):
        max_drawdown_benchmark = float(max_dd_benchmark.iloc[0]) if len(max_dd_benchmark) > 0 else 0.0
    else:
        max_drawdown_benchmark = float(max_dd_benchmark) if not pd.isna(max_dd_benchmark) else 0.0

except Exception as e:
    print(f"Ошибка расчета дополнительных метрик: {e}")
    total_strategy_return = total_benchmark_return = 0.0
    max_drawdown_strategy = max_drawdown_benchmark = 0.0

print(f"\nДополнительные метрики производительности:")
print(f"Общая доходность стратегии: {total_strategy_return:.2%}")
print(f"Общая доходность бенчмарка: {total_benchmark_return:.2%}")
print(f"Максимальная просадка стратегии: {max_drawdown_strategy:.2%}")
print(f"Максимальная просадка бенчмарка: {max_drawdown_benchmark:.2%}")

# Анализ сигналов
signal_counts = trading_signals.value_counts().sort_index()
print(f"\nРаспределение сигналов:")
for signal, count in signal_counts.items():
    if signal == -1:
        print(f"Сигналы на продажу: {count}")
    elif signal == 0:
        print(f"Сигналы на удержание: {count}")
    elif signal == 1:
        print(f"Сигналы на покупку: {count}")
Обработка 4 значительных лагов: [ 4 18 33 36]
Порог: 0.002
Статистика взвешенных сигналов: мин=-0.008205, макс=0.007605
Взвешенные сигналы > порога (0.002): 77
Взвешенные сигналы < -порога (-0.002): 76
Сгенерировано 153 ненулевых сигналов из 751 периодов
Размер доходностей: (751, 1)
Размер торговых сигналов: (751,)
Размер доходностей стратегии: (751, 752)
Ненулевые доходности стратегии: 751
Статистика доходностей стратегии: среднее=0.000000, стд_откл=0.000000
Ненулевые торговые сигналы: 153
Распределение сигналов: {0.0: 598, 1.0: 77, -1.0: 76}

Детальный анализ стратегии:
Диапазон торговых сигналов: -1.0 до 1.0
Диапазон доходностей стратегии: 0.000000 до 0.000000
Сумма доходностей стратегии: 0.000000

Дни с сигналами:
  2022-07-26 00:00:00: сигнал=-1, доходность_стратегии=0.000000, базовая_доходность=-0.011543
  2022-07-28 00:00:00: сигнал=1, доходность_стратегии=0.000000, базовая_доходность=0.012133
  2022-08-01 00:00:00: сигнал=1, доходность_стратегии=0.000000, базовая_доходность=-0.002823
  2022-08-04 00:00:00: сигнал=1, доходность_стратегии=0.000000, базовая_доходность=-0.000777
  2022-08-08 00:00:00: сигнал=-1, доходность_стратегии=0.000000, базовая_доходность=-0.001238

Проверка альтернативной реализации стратегии...
Диапазон альтернативных доходностей стратегии: -0.034608 до 0.030584
Сумма альтернативной стратегии: 0.038267
Используем альтернативную реализацию стратегии

Сравнение кумулятивной доходности PACF стратегии покупок и продаж индекса SP500 со стратегией купи-и-держи

Рис. 4: Сравнение кумулятивной доходности PACF стратегии покупок и продаж индекса SP500 со стратегией купи-и-держи

Коэффициент Шарпа стратегии: 0.169
Коэффициент Шарпа бенчмарка: 0.775

Дополнительные метрики производительности:
Общая доходность стратегии: 3.01%
Общая доходность бенчмарка: 44.14%
Максимальная просадка стратегии: -8.67%
Максимальная просадка бенчмарка: -18.90%

Распределение сигналов:
Сигналы на продажу: 76
Сигналы на удержание: 598
Сигналы на покупку: 77

В этом примере стратегия PACF проиграла бенчмарку. Но бывает и наоборот. Представленный подход к построению торговых сигналов принципиально отличается от традиционных методов тем, что он основан на статистически обоснованных зависимостях в данных, а не на эмпирических правилах.

Читайте также:  MSE, RMSE, MAE, MAPE для оценки качества прогнозов временных рядов

Диагностика временных рядов с помощью ACF и PACF

Идентификация стационарности и трендов

Одним из ключевых применений ACF является диагностика стационарности временного ряда. Нестационарные ряды демонстрируют характерные паттерны в автокорреляционной функции, которые важно уметь распознавать. Медленно убывающая ACF часто указывает на наличие единичного корня или долгосрочного тренда в данных.

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

# Анализ стационарности через ACF
price_series = data['Close']
price_acf = acf(price_series, nlags=50, fft=True)
returns_acf = acf(returns, nlags=50, fft=True)

plt.figure(figsize=(12, 8))
plt.subplot(2, 2, 1)
plt.plot(price_series.index, price_series, 'k-', linewidth=1)
plt.title('Временной ряд цен')
plt.grid(True, alpha=0.3)

plt.subplot(2, 2, 2)
plt.plot(returns.index, returns, 'k-', linewidth=1)
plt.title('Временной ряд доходностей')
plt.grid(True, alpha=0.3)

plt.subplot(2, 2, 3)
plt.plot(range(len(price_acf)), price_acf, 'ko-', linewidth=2, markersize=4, color='black')
plt.title('ACF цен (медленное убывание)')
plt.grid(True, alpha=0.3)

plt.subplot(2, 2, 4)
plt.plot(range(len(returns_acf)), returns_acf, 'ko-', linewidth=2, markersize=4, color='black')
plt.title('ACF доходностей (быстрое убывание)')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Диагностика стационарности индекса SP500 через автокорреляционную функцию его цен и доходностей

Рис. 5: Диагностика стационарности индекса SP500 через автокорреляционную функцию его цен и доходностей

Сравнение этих графиков наглядно демонстрирует различие между стационарными и нестационарными рядами. ACF цен демонстрирует медленное убывание, характерное для нестационарных рядов, тогда как ACF доходностей быстро убывает к нулю.

Выявление сезонности и циклических паттернов

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

# Анализ внутридневной сезонности (на примере часовых данных)
def analyze_intraday_seasonality(symbol, period="1y"):
    """
    Анализ внутридневных паттернов через ACF
    """
    # Загружаем часовые данные
    hourly_data = yf.download(symbol, period=period, interval="1h")
    hourly_returns = hourly_data['Close'].pct_change().dropna()
    print(hourly_returns)
    
    # Добавляем час дня как признак
    hourly_returns.index = pd.to_datetime(hourly_returns.index)
    hourly_returns = hourly_returns[hourly_returns.index.weekday < 5] # Только рабочие дни # Группируем по часам hourly_patterns = hourly_returns.groupby(hourly_returns.index.hour).agg(['mean', 'std', 'count']).round(4) # ACF для каждого часа acf_by_hour = {} for hour in range(24): hour_data = hourly_returns[hourly_returns.index.hour == hour] if len(hour_data) > 50:  # Достаточно данных для анализа
            acf_by_hour[hour] = acf(hour_data, nlags=20, fft=True)
    
    return hourly_patterns, acf_by_hour

# Применяем анализ к EUR/USD
eurusd_patterns, eurusd_acf = analyze_intraday_seasonality("EURUSD=X")
print("Внутридневные паттерны EUR/USD:")
print(eurusd_patterns)
2024-06-12 09:00:00+00:00  0.000645
2024-06-12 10:00:00+00:00  0.000430
2024-06-12 11:00:00+00:00  0.000646
2024-06-12 12:00:00+00:00  0.005958
2024-06-12 13:00:00+00:00  0.000759
...                             ...
2025-06-12 04:00:00+00:00 -0.000346
2025-06-12 05:00:00+00:00  0.001038
2025-06-12 06:00:00+00:00 -0.000576
2025-06-12 07:00:00+00:00  0.000461
2025-06-12 08:00:00+00:00  0.000808

[6178 rows x 1 columns]
Внутридневные паттерны EUR/USD:
Ticker   EURUSD=X              
             mean     std count
Datetime                       
0          0.0000  0.0015   260
1         -0.0000  0.0007   260
2          0.0001  0.0008   260
3         -0.0000  0.0006   260
4          0.0000  0.0006   259
5          0.0000  0.0007   261
6          0.0000  0.0009   260
7         -0.0000  0.0015   260
8          0.0002  0.0011   260
9         -0.0001  0.0010   259
10        -0.0000  0.0009   259
11        -0.0000  0.0010   259
12         0.0000  0.0015   259
13         0.0001  0.0015   259
14         0.0000  0.0014   259
15        -0.0002  0.0013   259
16        -0.0001  0.0010   259
17        -0.0000  0.0008   259
18         0.0001  0.0009   260
19        -0.0001  0.0010   260
20         0.0001  0.0006   260
21         0.0000  0.0006   261
22         0.0000  0.0005   228
23        -0.0001  0.0009   208

Этот анализ часто выявляет интересные паттерны, связанные с открытием различных торговых сессий и важными экономическими событиями. Например, волатильность EUR/USD обычно возрастает в часы пересечения европейской и американской сессий.

Читайте также:  Стационарность временных рядов. Как анализировать нестационарные данные?

Продвинутые техники анализа: Rolling ACF и PACF

Динамический анализ корреляционной структуры

Статические значения ACF и PACF дают лишь общее представление о структуре временного ряда. В реальных рыночных условиях корреляционная структура постоянно меняется под влиянием различных факторов: изменения режимов волатильности, макроэкономических событий, изменений в микроструктуре рынка.

Для учета этой динамики я использую скользящие (rolling) расчеты ACF и PACF, которые позволяют отслеживать изменения корреляционной структуры во времени. Этот подход особенно полезен для адаптивных торговых стратегий.

def rolling_acf_analysis(returns, window=252, lag=1):
    """
    Скользящий анализ автокорреляции
    """
    rolling_acf = returns.rolling(window=window).apply(
        lambda x: acf(x, nlags=lag, fft=True)[lag] if len(x.dropna()) > lag else np.nan,
        raw=False
    )
    return rolling_acf

def rolling_pacf_analysis(returns, window=252, lag=1):
    """
    Скользящий анализ частичной автокорреляции
    """
    rolling_pacf = returns.rolling(window=window).apply(
        lambda x: pacf(x, nlags=lag, method='ols')[lag] if len(x.dropna()) > lag else np.nan,
        raw=False
    )
    return rolling_pacf

# Применяем к данным S&P 500
rolling_acf_1 = rolling_acf_analysis(returns, window=252, lag=1)
rolling_pacf_1 = rolling_pacf_analysis(returns, window=252, lag=1)

# Визуализация динамики
plt.figure(figsize=(12, 10))
plt.subplot(3, 1, 1)
plt.plot(returns.index, returns, 'k-', linewidth=0.5, alpha=0.7)
plt.title('Доходности S&P 500')
plt.grid(True, alpha=0.3)

plt.subplot(3, 1, 2)
plt.plot(rolling_acf_1.index, rolling_acf_1, 'k-', linewidth=2)
plt.axhline(y=0, color='gray', linestyle='-', alpha=0.3)
plt.title('Rolling ACF (лаг=1, окно=252 дня)')
plt.ylabel('Автокорреляция')
plt.grid(True, alpha=0.3)

plt.subplot(3, 1, 3)
plt.plot(rolling_pacf_1.index, rolling_pacf_1, 'k-', linewidth=2)
plt.axhline(y=0, color='gray', linestyle='-', alpha=0.3)
plt.title('Rolling PACF (лаг=1, окно=252 дня)')
plt.ylabel('Частичная автокорреляция')
plt.xlabel('Дата')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

График доходностей SP500 и его скользящих ACF и PACF

Рис. 6: График доходностей SP500 и его скользящих ACF и PACF

Анализ этих графиков часто показывает интересные паттерны. Например, периоды рыночного стресса (2020 год) часто сопровождаются увеличением абсолютных значений автокорреляции, что может указывать на усиление momentum или mean reversion эффектов.

Многомерный анализ cross-correlation структуры

В портфельном управлении важно понимать не только автокорреляционную структуру отдельных активов, но и кросс-корреляцию между различными инструментами. Этот анализ помогает выявлять лид-лаг отношения между активами и строить более эффективные хеджевые стратегии.

# Функция для анализа кросс-корреляции с лагами
def cross_correlation_analysis(returns1, returns2, max_lag=20):
    """
    Анализ взаимной корреляции между двумя временными рядами с учетом лагов
    """
    correlations = []
    lags = range(-max_lag, max_lag + 1)

    for lag in lags:
        if lag == 0:
            aligned = pd.concat([returns1, returns2], axis=1).dropna()
        elif lag > 0:
            aligned = pd.concat([returns1, returns2.shift(lag)], axis=1).dropna()
        else:  # lag < 0
            aligned = pd.concat([returns1.shift(-lag), returns2], axis=1).dropna()

        if aligned.empty:
            corr = float('nan')
        else:
            corr = aligned.iloc[:, 0].corr(aligned.iloc[:, 1])

        correlations.append(corr)

    return pd.Series(correlations, index=lags)

# Загрузка котировок
symbols = ['SPY', 'QQQ', 'IWM', '^VIX']
prices = {}
returns = {}

for symbol in symbols:
    df = yf.download(symbol, start="2022-06-01", end="2025-06-01")
    close = df['Close']
    close.name = symbol
    prices[symbol] = close
    returns[symbol] = close.pct_change().dropna()

    print(f"\nКотировки для {symbol}:")
    print(close.tail())

# Кросс-корреляция SPY и ^VIX
if 'SPY' in returns and '^VIX' in returns:
    spy_returns = returns['SPY']
    vix_returns = returns['^VIX']

    # Объединяем и оставляем только совпадающие даты
    combined = pd.concat([spy_returns, vix_returns], axis=1, join='inner').dropna()
    spy_returns = combined.iloc[:, 0]
    vix_returns = combined.iloc[:, 1]

    spy_vix_corr = cross_correlation_analysis(spy_returns, vix_returns, max_lag=10)

    # График кросс-корреляции
    plt.figure(figsize=(10, 6))
    plt.plot(spy_vix_corr.index, spy_vix_corr.values, 'ko-', linewidth=2, markersize=6)
    plt.axhline(0, color='gray', linestyle='--')
    plt.axvline(0, color='red', linestyle='--')
    plt.title("Cross-correlation между SPY и ^VIX")
    plt.xlabel("Лаг (дни)")
    plt.ylabel("Коэффициент корреляции")
    plt.grid(True)
    plt.tight_layout()
    plt.show()

    # Вывод максимальной корреляции
    max_corr_lag = spy_vix_corr.abs().idxmax()
    max_corr_value = spy_vix_corr[max_corr_lag]
    print(f"\nМаксимальная корреляция: {max_corr_value:.3f} на лаге {max_corr_lag}")
else:
    print("Нет данных для SPY и/или ^VIX")
Котировки для SPY:
Ticker             SPY
Date                  
2025-05-23  579.109985
2025-05-27  591.150024
2025-05-28  587.729980
2025-05-29  590.049988
2025-05-30  589.390015

Котировки для ^VIX:
Ticker           ^VIX
Date                 
2025-05-23  22.290001
2025-05-27  18.959999
2025-05-28  19.309999
2025-05-29  19.180000
2025-05-30  18.570000

Максимальная корреляция: -0.722 на лаге 0

График кросс-корреляции между индексом SP500 и индексом ожидаемой волатильности VIX

Рис. 7: График кросс-корреляции между индексом SP500 и индексом ожидаемой волатильности VIX

VIX (Volatility Index) — это индекс ожидаемой волатильности рынка, который рассчитывается на основе опционов на индекс S&P 500. VIX показывает, насколько сильно участники рынка ожидают, что S&P 500 будет колебаться в ближайшие 30 дней. Высокий VIX (например, 30+) означает, что инвесторы ждут сильных колебаний (часто в кризисы, как в 2008 или 2020). Низкий VIX (например, ниже 15) означает, что рынок стабилен, ожидания спокойные.

SPY (ETF на S&P 500) и VIX часто движутся в противоположных направлениях. Когда рынок падает, то опционы на SP500 дорожают, а VIX растет. Поэтому корреляция между SPY и VIX обычно отрицательная.

Этот анализ часто выявляет асимметричные отношения между активами. Например, VIX может сильнее коррелировать с будущими движениями S&P 500, чем с текущими, что может быть использовано для построения опережающих индикаторов.

Практические аспекты применения в количественных стратегиях

Создание адаптивных торговых систем

Одним из наиболее эффективных применений ACF и PACF является создание адаптивных торговых систем, которые автоматически корректируют свои параметры в зависимости от изменения корреляционной структуры рынка. Традиционные стратегии с фиксированными параметрами часто терпят неудачу при смене рыночных режимов.

Читайте также:  Прогнозирование временных рядов с помощью N-HITS, N-BEATS

Адаптивная стратегия автоматически переключается между momentum и mean reversion режимами в зависимости от текущей корреляционной структуры данных. Ключевое преимущество такого подхода заключается в том, что он не требует ручной настройки параметров для различных рыночных условий.

Оптимизация риск-менеджмента через анализ автокорреляции

Традиционные модели риска часто предполагают независимость доходностей во времени, что приводит к существенному недооцениванию рисков в периоды высокой автокорреляции. Анализ ACF и PACF позволяет создавать более точные модели риска, учитывающие временные зависимости.

Отмечу, что автокорреляция в квадратах доходностей (кластеризация волатильности) требует применения моделей семейства GARCH для адекватного моделирования риска. Простые модели, основанные на исторической волатильности, систематически недооценивают риски в периоды рыночного стресса, когда автокорреляция волатильности наиболее выражена.

В моей практике я использую rolling анализ PACF волатильности для динамической корректировки размеров позиций. Когда PACF указывает на усиление кластеризации волатильности, я автоматически снижаю левередж портфеля, что позволяет избегать крупных потерь в периоды турбулентности.

Построение многофакторных моделей с временными лагами

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

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

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

Детекция структурных сдвигов в рыночных данных

Резкие изменения в паттернах ACF и PACF часто сигнализируют о структурных сдвигах в рыночной динамике. Это может быть связано с изменениями в монетарной политике, регулятивных требованиях, или фундаментальными сдвигами в рыночной микроструктуре.

Мониторинг этих изменений позволяет оперативно адаптировать торговые стратегии к новым условиям. Например, переход от низкочастотного к высокочастотному трейдингу в определенном инструменте часто сопровождается изменением автокорреляционной структуры на коротких временных интервалах.

В моей практике я использую статистические тесты для детекции структурных сдвигов в rolling ACF, что позволяет автоматически переключать стратегии между различными режимами торговли. Это особенно важно для высокочастотных стратегий, где даже небольшие изменения в микроструктуре могут кардинально повлиять на прибыльность.

Интеграция с машинным обучением и нейронными сетями

Современные алгоритмы машинного обучения могут эффективно использовать информацию из ACF и PACF анализа для улучшения качества прогнозов. Вместо использования сырых ценовых данных, включение статистически значимых лагов, выявленных через PACF, существенно повышает прогностическую силу моделей.

Особенно эффективным является использование динамических признаков, основанных на rolling ACF и PACF. Эти признаки захватывают изменения в корреляционной структуре и позволяют моделям адаптироваться к смене рыночных режимов. В отличие от статических технических индикаторов, такие признаки имеют четкую статистическую интерпретацию и реже подвержены data mining bias.

При работе с нейронными сетями я часто использую архитектуры LSTM, где входные последовательности формируются на основе значимых лагов, выявленных через PACF анализ. Это позволяет сетям эффективнее обучаться на временных зависимостях без необходимости обработки избыточной информации.

Ограничения и подводные камни в практическом применении

Проблемы статистической значимости в финансовых данных

Одной из главных проблем при применении ACF и PACF к финансовым данным является корректная интерпретация статистической значимости. Стандартные доверительные интервалы, основанные на предположении о нормальности и независимости, часто неприменимы к финансовым временным рядам из-за их специфических свойств.

Финансовые доходности часто демонстрируют толстые хвосты распределения, кластеризацию волатильности, левосторонний скос. Эти особенности могут приводить к ложным сигналам значимости в ACF и PACF, особенно при анализе высокочастотных данных. В моей практике я всегда использую bootstrap-методы для построения более робастных доверительных интервалов.

Кроме того, важно понимать, что множественное тестирование (анализ многих лагов одновременно) увеличивает вероятность ложных открытий. Применение поправок Бонферрони или более современных методов контроля False Discovery Rate критически важно для получения надежных результатов.

Влияние микроструктурных эффектов на автокорреляцию

При работе с высокочастотными данными микроструктурные эффекты могут кардинально искажать паттерны ACF и PACF. Bid-ask bounce, асинхронность торгов, дискретность цен — все эти факторы создают артефакты в корреляционной структуре, которые не отражают истинную динамику цен.

Особенно проблематичен bid-ask bounce эффект, который создает отрицательную автокорреляцию на первом лаге даже в случае отсутствия реальных mean reversion паттернов. Игнорирование этого эффекта может привести к построению убыточных стратегий, основанных на ложных сигналах реверсии.

Для корректного анализа высокочастотных данных я рекомендую использовать цены средней точки (mid-prices) вместо цен последних сделок, а также применять специализированные методы фильтрации, такие как kernel-based estimators, которые уменьшают влияние микроструктурного шума.

Нестабильность корреляционной структуры во времени

Фундаментальная проблема применения ACF и PACF к финансовым данным заключается в нестабильности корреляционной структуры во времени. То, что работало в прошлом, может полностью потерять эффективность при смене рыночного режима. Это особенно характерно для кризисных периодов, когда корреляционные связи могут кардинально изменяться.

Статический анализ ACF и PACF может давать обманчивое представление о стабильности обнаруженных паттернов. В реальности большинство автокорреляционных структур в финансовых данных являются временными и могут исчезать так же быстро, как появляются. Это требует постоянного мониторинга и адаптации торговых стратегий.

Решением этой проблемы является использование адаптивных методов анализа, таких как rolling ACF/PACF с различными окнами, regime-switching модели, или байесовские подходы, которые позволяют количественно оценивать неопределенность в корреляционной структуре.

Заключение

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

Ключевые выводы из моего опыта применения ACF и PACF:

Диагностическая ценность превыше всего

ACF и PACF наиболее ценны не как генераторы торговых сигналов, а как диагностические инструменты для понимания структуры данных. Они помогают выявлять скрытые зависимости, которые могут быть использованы для улучшения существующих стратегий или создания новых подходов к моделированию.

Необходимо проводить анализ не только в статике, но и в динамике

Статические расчеты ACF и PACF дают лишь моментальный снимок корреляционной структуры. Реальную ценность представляет анализ их изменения во времени, который позволяет адаптироваться к смене рыночных режимов и избегать стратегий, основанных на устаревших паттернах.

Комбинация с другими методами усиливает эффект

ACF и PACF наиболее эффективны в сочетании с другими статистическими методами и техниками машинного обучения. Они предоставляют ценную информацию для feature engineering и помогают создавать более интерпретируемые модели.

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

Финансовые данные полны подводных камней, которые могут привести к неверной интерпретации результатов ACF и PACF анализа. Микроструктурные эффекты, нестационарность, множественное тестирование — все эти факторы требуют особого внимания при практическом применении.