Стандартные статистические тесты опираются на строгие допущения. Они предполагают нормальное распределение данных, независимость наблюдений и линейные связи между переменными.
Финансовые данные этим требованиям обычно не соответствуют. Доходности активов имеют тяжелые хвосты распределений. Волатильность склонна группироваться во времени. Связи между финансовыми инструментами часто нелинейны и тоже меняются со временем.
Продвинутые статистические методы решают задачи, с которыми классические подходы не справляются:
- Тесты причинности (causality tests) помогают определить направление влияния между переменными. Это важно для построения прогнозных моделей;
- Метод бутстрэп (bootstrap) позволяет оценивать надежность результатов без жестких предположений о распределении данных;
- Непараметрические тесты подходят для любых распределений и менее чувствительны к выбросам.
Python предлагает готовые реализации этих методов. Они доступны в библиотеках scipy, statsmodels и arch. Понимание принципов их работы и ограничений дает преимущество при разработке торговых стратегий и анализе рыночных аномалий.
Причинность в статистическом анализе
Выявление причинно-следственных связей отличается от поиска корреляций. Корреляция между двумя временными рядами может возникать из-за общего скрытого фактора, случайного совпадения трендов или обратной причинности. Причинность требует доказательства направленного влияния одной переменной на другую.
Корреляция и причинность
Корреляция измеряет силу линейной связи между переменными, однако не указывает на направление влияния. Классический пример: продажи мороженого коррелируют с количеством утоплений, но причина обеих переменных — летняя жара.
В финансовых данных ложные корреляции появляются регулярно. Два актива могут двигаться вместе не потому, что один влияет на другой. Чаще всего у них есть общая причина. Это может быть макроэкономический фактор. Это могут быть сделки одного крупного фонда. Иногда синхронность возникает из-за одновременной работы торговых алгоритмов.
Стратегия, построенная на такой корреляции, ненадежна. Когда скрытый фактор меняется, связь исчезает. В этот момент стратегия начинает приносить убытки.
Причинность (каузальность) опирается на три условия:
- Причина должна предшествовать следствию во времени;
- Между переменными должна существовать статистическая связь;
- Не должно быть других правдоподобных объяснений этой связи.
Проверить выполнение этих условий с помощью простых методов нельзя. Для этого применяют специальные статистические тесты.
Тест Грейнджера
Тест Грейнджера проверяет простую идею. Улучшают ли прошлые значения переменной X прогноз переменной Y. Если прогноз становится точнее, говорят, что X «причиняет по Грейнджеру» Y.
Этот метод не доказывает настоящую причинность. Он показывает только предсказательную силу одной переменной для другой. Логика теста следующая:
- Сначала строят 1-ю модель для Y, которая использует только прошлые значения самой Y;
- Затем строят 2-ю модель. В нее добавляют прошлые значения X;
- После этого сравнивают качество двух моделей с помощью F-теста (F-test);
- Если 2-ая модель заметно лучше, делают вывод о причинности по Грейнджеру.
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import grangercausalitytests
import yfinance as yf
import matplotlib.pyplot as plt
# Загрузка данных двух связанных активов
ticker1 = yf.Ticker('TSM')
ticker2 = yf.Ticker('ASML')
data1 = ticker1.history(period='2y')['Close']
data2 = ticker2.history(period='2y')['Close']
# Расчет доходностей
returns1 = data1.pct_change().dropna()
returns2 = data2.pct_change().dropna()
# Объединение данных
df = pd.DataFrame({
'TSM': returns1,
'ASML': returns2
}).dropna()
# Тест Грейнджера: влияет ли ASML на TSM
max_lag = 5
print("Тест причинности: ASML -> TSM")
result = grangercausalitytests(df[['TSM', 'ASML']], maxlag=max_lag, verbose=True)
# Извлечение p-values для визуализации
p_values = []
for lag in range(1, max_lag + 1):
p_val = result[lag][0]['ssr_ftest'][1]
p_values.append(p_val)
# Визуализация результатов
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
# График доходностей
ax1.plot(df.index, df['TSM'], label='TSM', color='black', linewidth=0.8)
ax1.plot(df.index, df['ASML'], label='ASML', color='gray', linewidth=0.8)
ax1.set_ylabel('Доходность')
ax1.legend()
ax1.grid(True, alpha=0.3)
# P-values по лагам
ax2.bar(range(1, max_lag + 1), p_values, color='black', alpha=0.7)
ax2.axhline(y=0.05, color='red', linestyle='--', label='Уровень значимости 5%')
ax2.set_xlabel('Лаг (дни)')
ax2.set_ylabel('P-value')
ax2.set_xticks(range(1, max_lag + 1))
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Тест причинности: ASML -> TSM
Granger Causality
number of lags (no zero) 1
ssr based F test: F=1.0264 , p=0.3115 , df_denom=497, df_num=1
ssr based chi2 test: chi2=1.0326 , p=0.3095 , df=1
likelihood ratio test: chi2=1.0316 , p=0.3098 , df=1
parameter F test: F=1.0264 , p=0.3115 , df_denom=497, df_num=1
Granger Causality
number of lags (no zero) 2
ssr based F test: F=0.5528 , p=0.5757 , df_denom=494, df_num=2
ssr based chi2 test: chi2=1.1169 , p=0.5721 , df=2
likelihood ratio test: chi2=1.1156 , p=0.5725 , df=2
parameter F test: F=0.5528 , p=0.5757 , df_denom=494, df_num=2
Granger Causality
number of lags (no zero) 3
ssr based F test: F=0.9214 , p=0.4302 , df_denom=491, df_num=3
ssr based chi2 test: chi2=2.8037 , p=0.4229 , df=3
likelihood ratio test: chi2=2.7958 , p=0.4242 , df=3
parameter F test: F=0.9214 , p=0.4302 , df_denom=491, df_num=3
Granger Causality
number of lags (no zero) 4
ssr based F test: F=0.7270 , p=0.5738 , df_denom=488, df_num=4
ssr based chi2 test: chi2=2.9618 , p=0.5642 , df=4
likelihood ratio test: chi2=2.9530 , p=0.5657 , df=4
parameter F test: F=0.7270 , p=0.5738 , df_denom=488, df_num=4
Granger Causality
number of lags (no zero) 5
ssr based F test: F=0.6535 , p=0.6590 , df_denom=485, df_num=5
ssr based chi2 test: chi2=3.3415 , p=0.6475 , df=5
likelihood ratio test: chi2=3.3303 , p=0.6492 , df=5
parameter F test: F=0.6535 , p=0.6590 , df_denom=485, df_num=5
Код загружает котировки акций компаний Taiwan Semiconductor (TSM) и ASML Holding. Это связанные корпорации из полупроводниковой отрасли.
Затем код проверяет, помогают ли прошлые доходности ASML предсказывать будущие доходности TSM. Для этого используется тест Грейнджера (Granger causality test). Функция grangercausalitytests() выполняет проверку для лагов от 1 до 5 дней. Для каждого лага она выводит статистику F-теста и соответствующее p-value.
Если p-value меньше 0.05, результат считают значимым на уровне 5%. В этом случае говорят, что доходности ASML причиняют по Грейнджеру доходности TSM.
Код выводит результаты для всех лагов. И мы видим, что все значения существенно больше 0.05. Тест причинности по Грейнджеру не выявил влияния доходностей ASML на доходности TSM. Для лагов от 1 до 5 нулевая гипотеза не отвергается (p-value > 0.05). Прошлые доходности ASML не улучшают прогноз доходностей TSM.

Рис. 1: Результаты теста Грейнджера для пары TSM-ASML. Верхняя панель показывает динамику дневных доходностей обоих активов. Нижняя панель отображает p-values F-теста для каждого лага. Значения ниже красной линии (0.05) указывают на статистически значимую причинность
К интерпретации результатов нужно подходить осторожно. Тест выявляет только линейные предсказательные связи. И он работает в рамках выбранного временного окна. Если режим рынка меняется, даже найденная причинность может исчезнуть.
Кроме того, причинность может быть двунаправленной. Переменная X может влиять на Y, а Y — на X. Поэтому тест нужно выполнять в обоих направлениях и учитывать ограничения метода.
Инструментальные переменные
Метод инструментальных переменных (IV) помогает решить проблему эндогенности — ситуации, когда объясняющая переменная X коррелирует с ошибкой модели. Эндогенность возникает из-за пропущенных переменных, одновременности или ошибок измерения.
Инструментальная переменная Z должна удовлетворять двум условиям:
- Релевантность: Z коррелирует с объясняющей переменной X;
- Экзогенность: Z не коррелирует с ошибкой модели.
Первое условие проверяется статистически, второе требует теоретического обоснования.
Процедура выполняется двухшаговых методом наименьших квадратов (2SLS):
- Регрессия X на инструмент Z;
- Регрессия Y на предсказанные значения X из первого шага.
Стандартные ошибки корректируются, чтобы учитывать двухступенчатую процедуру.
from statsmodels.sandbox.regression.gmm import IV2SLS
import numpy as np
# Генерация синтетических данных с эндогенностью
np.random.seed(42)
n = 500
# Скрытая переменная (общий фактор)
hidden_factor = np.random.normal(0, 1, n)
# Инструментальная переменная (экзогенная)
Z = np.random.normal(0, 1, n)
# Эндогенная переменная X зависит от инструмента и скрытого фактора
X = 0.7 * Z + 0.5 * hidden_factor + np.random.normal(0, 0.5, n)
# Y зависит от X и скрытого фактора (эндогенность)
Y = 2.0 * X + 0.8 * hidden_factor + np.random.normal(0, 1, n)
# OLS регрессия (смещенная из-за эндогенности)
from statsmodels.api import OLS, add_constant
X_const = add_constant(X)
ols_model = OLS(Y, X_const).fit()
print("OLS коэффициент:", ols_model.params[1])
# 2SLS с инструментальной переменной
Z_const = add_constant(Z)
X_const_for_iv = add_constant(X)
iv_model = IV2SLS(Y, X_const_for_iv, Z_const).fit()
print("2SLS коэффициент:", iv_model.params[1])
print("Истинный коэффициент: 2.0")
OLS коэффициент: 2.348773950539365
2SLS коэффициент: 1.8824340976018072
Истинный коэффициент: 2.0
Представленный пример кода генерирует синтетические данные с эндогенностью, где объясняющая переменная X зависит от скрытого фактора, влияющего на Y, и инструментальной переменной Z.
Сначала выполняется обычная линейная регрессия (OLS), которая дает смещенный коэффициент из-за эндогенности X. Затем применяется метод двухшаговых наименьших квадратов (2SLS) с инструментальной переменной Z, чтобы скорректировать смещение и получить более точную оценку влияния X на Y, сравнивая ее с известным истинным коэффициентом 2.0.
В биржевой торговле инструментальные переменные применяются для оценки влияния:
- Объема на цену (инструмент — время суток);
- Эффектов ликвидности (инструмент — индексные ребалансировки);
- Воздействия новостей на волатильность (инструмент — запланированные релизы).
Бутстрап методы
Бустрап (Bootstrap) — это метод статистического анализа, который строит выводы с помощью многократной повторной выборки из исходных данных. Вместо того чтобы делать предположения о распределении данных, бутстрэп создает эмпирическое распределение интересующей статистики, выбирая случайные наблюдения с возвращением.
Принцип работы бутстрэпа
Классический способ построения доверительных интервалов требует знания распределения статистики или использования приближений для больших выборок. Бутстрап позволяет обойти эти ограничения. Из исходной выборки размером n многократно (обычно 1000–10000 раз) извлекаются подвыборки того же размера с возвращением. Для каждой подвыборки вычисляется интересующая статистика.
Распределение этих значений аппроксимирует истинное распределение статистики. Перцентили полученного распределения дают доверительные интервалы. Метод подходит для любых статистик: среднего, медианы, коэффициентов регрессии, коэффициента Шарпа.
Ограничения бутстрэпа:
- нужна достаточно большая выборка;
- предполагается независимость наблюдений.
Для временных рядов применяются модификации, например блоковый бутстрэп (block bootstrap), который сохраняет автокорреляцию, передискретизируя блоки последовательных наблюдений.
Доверительные интервалы через bootstrap
Построение доверительного интервала для коэффициента Шарпа показывает, как бутстрэп применяется к финансовым данным. Коэффициент Шарпа обычно не имеет известного аналитического распределения, поэтому бутстрэп становится естественным и удобным методом для оценки его доверительных интервалов.
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
# Загрузка данных
ticker = yf.Ticker('INTC')
data = ticker.history(period='3y')['Close']
returns = data.pct_change().dropna()
# Функция для расчета Sharpe ratio
def sharpe_ratio(returns, risk_free_rate=0.02):
excess_returns = returns - risk_free_rate / 252
return np.sqrt(252) * excess_returns.mean() / excess_returns.std()
# Bootstrap для Sharpe ratio
n_bootstrap = 10000
bootstrap_sharpe = []
np.random.seed(42)
for _ in range(n_bootstrap):
# Выборка с возвращением
sample = returns.sample(n=len(returns), replace=True)
bootstrap_sharpe.append(sharpe_ratio(sample))
bootstrap_sharpe = np.array(bootstrap_sharpe)
# Расчет доверительного интервала (95%)
ci_lower = np.percentile(bootstrap_sharpe, 2.5)
ci_upper = np.percentile(bootstrap_sharpe, 97.5)
observed_sharpe = sharpe_ratio(returns)
print(f"Наблюдаемый Sharpe: {observed_sharpe:.3f}")
print(f"95% доверительный интервал: [{ci_lower:.3f}, {ci_upper:.3f}]")
# Визуализация
fig, ax = plt.subplots(figsize=(12, 6))
ax.hist(bootstrap_sharpe, bins=50, color='black', alpha=0.7, edgecolor='black')
ax.axvline(observed_sharpe, color='red', linestyle='--', linewidth=2, label='Наблюдаемое значение')
ax.axvline(ci_lower, color='blue', linestyle='--', linewidth=1.5, label='95% CI')
ax.axvline(ci_upper, color='blue', linestyle='--', linewidth=1.5)
ax.set_xlabel('Sharpe Ratio')
ax.set_ylabel('Частота')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Код загружает исторические доходности акций Intel, затем генерирует 10000 бутстрэп-выборок с возвращением. Для каждой выборки вычисляется коэффициент Шарпа. На основе распределения этих коэффициентов строится эмпирический 95% доверительный интервал (перцентили 2.5% и 97.5%). Если этот интервал не включает ноль, наблюдаемый коэффициент Шарпа считается статистически значимым.

Рис. 2: Бутстрап-распределение коэффициента Шарпа для акций Intel (INTC). Гистограмма показывает 10000 значений, полученных передискретизацией. Красная линия — наблюдаемое значение, синие линии — границы 95% доверительного интервала. Ширина интервала характеризует неопределенность оценки
Метод бустрапа позволяет оценить стабильность любой метрики портфеля: максимальную просадку, VaR, коэффициент Сортино. Чем уже доверительный интервал, тем надежнее оценка. Широкий интервал сигнализирует о высокой вариативности метрики и требует осторожности в интерпретации.
Статистическая значимость стратегий
Бэктест торговой стратегии может показать положительную доходность, но важно понять, насколько она отличается от случайного результата. Бутстрэп помогает это проверить с помощью перестановочных тестов (permutation tests).
Идея проста: если стратегия не имеет предсказательной силы, порядок сделок не должен влиять на итоговую прибыль. Доходности сделок многократно перемешивают, для каждой перестановки вычисляют суммарную прибыль. Затем распределение этих прибылей при случайном порядке сравнивают с наблюдаемой прибылью. Это позволяет оценить, насколько результат стратегии статистически значим.
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
# Параметры стратегии
n_trades = 200 # Общее количество сделок в симуляции стратегии
win_prob = 0.55 # Вероятность того, что отдельная сделка окажется прибыльной (55%)
win_mean = 0.015 # Средняя доходность выигрышной сделки (1.5%)
win_std = 0.02 # Стандартное отклонение доходности выигрышной сделки (отражает волатильность)
lose_mean = -0.012 # Средняя доходность проигрышной сделки (-1.2%)
lose_std = 0.018 # Стандартное отклонение доходности проигрышной сделки
# Генерация исходных сделок
wins_mask = np.random.random(n_trades) < win_prob trades = np.zeros(n_trades) trades[wins_mask] = np.random.normal(win_mean, win_std, wins_mask.sum()) trades[~wins_mask] = np.random.normal(lose_mean, lose_std, (~wins_mask).sum()) observed_return = trades.sum() print(f"Наблюдаемая доходность: {observed_return:.4f}") # Перестановочный тест n_permutations = 10000 permuted_returns = [] n_wins = wins_mask.sum() for _ in range(n_permutations): # Генерация случайных наборов выигрышей с тем же количеством permuted = np.zeros(n_trades) permuted[:n_wins] = np.random.normal(win_mean, win_std, n_wins) permuted[n_wins:] = np.random.normal(lose_mean, lose_std, n_trades - n_wins) np.random.shuffle(permuted) permuted_returns.append(permuted.sum()) permuted_returns = np.array(permuted_returns) # P-value: доля перестановок с доходностью >= наблюдаемой
p_value = (permuted_returns >= observed_return).sum() / n_permutations
print(f"P-value: {p_value:.4f}")
# Визуализация
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14,5))
# Кумулятивная доходность
ax1.plot(np.cumsum(trades), color='black', linewidth=1.5)
ax1.axhline(0, color='gray', linestyle='--', alpha=0.5)
ax1.set_xlabel("Номер сделки")
ax1.set_ylabel("Кумулятивная доходность")
ax1.grid(True, alpha=0.3)
# Гистограмма перестановочного теста
ax2.hist(permuted_returns, bins=50, color='black', alpha=0.7, edgecolor='black')
ax2.axvline(observed_return, color='red', linestyle='--', linewidth=2,
label=f'Наблюдаемая (p={p_value:.4f})')
ax2.set_xlabel("Суммарная доходность")
ax2.set_ylabel("Частота")
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Наблюдаемая доходность: 0.9891
P-value: 0.1686

Рис. 3: Визуализация результатов перестановочного теста. Гистограмма показывает распределение суммарной доходности 10 000 случайных перестановок сделок стратегии. Красная линия — наблюдаемая доходность, а p-value отражает, насколько она отличается от случайного ожидания
Перестановочные тесты можно применять к любым показателям: коэффициенту Шарпа, максимальной просадке, доле выигрышных сделок (win rate). Метод не требует предположений о распределении доходностей и работает даже с небольшими выборками. Главное ограничение: наблюдения должны быть взаимозаменяемыми, то есть отсутствовать тренды и автокорреляция.
Непараметрические тесты
Параметрические тесты (t-test, ANOVA) предполагают нормальность распределений и равенство дисперсий. Финансовые данные часто демонстрируют асимметрию, эксцесс и гетероскедастичность. Непараметрические методы работают с рангами наблюдений вместо их абсолютных значений, что обеспечивает робастность к выбросам и не требует предположений о форме распределения.
Когда параметрика не работает
T-test теряет мощность при отклонениях от нормальности: тяжелые хвосты увеличивают вероятность ошибок I и II рода, асимметрия смещает оценки. Малые выборки усиливают эти проблемы, так как центральная предельная теорема не обеспечивает приближение к нормальности.
Типичные ситуации в алгоритмической торговле: сравнение доходностей стратегий с разными профилями риска, анализ распределения времени удержания позиций, тестирование различий в проскальзывании между брокерами. Во всех случаях данные часто нарушают предположения параметрических тестов.
Непараметрические альтернативы используют порядковую информацию. Вместо сравнения средних значений тесты работают с рангами или знаками, что делает их устойчивыми к экстремальным значениям и монотонным преобразованиям данных.
Тест Манна-Уитни
Тест Манна-Уитни (Mann-Whitney U test) проверяет гипотезу о равенстве распределений двух независимых выборок. Метод ранжирует все наблюдения из обеих групп и сравнивает суммы рангов. Если группы имеют одинаковое распределение, суммы рангов будут близки с учетом размеров выборок.
import numpy as np
from scipy.stats import mannwhitneyu, ttest_ind
import matplotlib.pyplot as plt
# Генерация данных: две стратегии с разными распределениями
np.random.seed(42)
# Стратегия A: нормальное распределение
strategy_a = np.random.normal(0.0008, 0.015, 150)
# Стратегия B: распределение с тяжелыми хвостами (t-распределение)
strategy_b = np.random.standard_t(df=3, size=150) * 0.015 + 0.0012
# Mann-Whitney U test
stat_mw, p_value_mw = mannwhitneyu(strategy_a, strategy_b, alternative='two-sided')
print(f"Mann-Whitney U статистика: {stat_mw:.2f}, p-value: {p_value_mw:.4f}")
# T-test для сравнения
stat_t, p_value_t = ttest_ind(strategy_a, strategy_b)
print(f"T-test статистика: {stat_t:.2f}, p-value: {p_value_t:.4f}")
# Визуализация
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))
# Гистограммы распределений
ax1.hist(strategy_a, bins=30, alpha=0.7, color='black', edgecolor='black', label='Стратегия A')
ax1.hist(strategy_b, bins=30, alpha=0.5, color='gray', edgecolor='black', label='Стратегия B')
ax1.set_xlabel('Дневная доходность')
ax1.set_ylabel('Частота')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Box plots
ax2.boxplot([strategy_a, strategy_b], labels=['A', 'B'])
ax2.set_ylabel('Дневная доходность')
ax2.grid(True, alpha=0.3)
# Q-Q plot для проверки нормальности
from scipy import stats
stats.probplot(strategy_b, dist="norm", plot=ax3)
ax3.set_title('Q-Q plot: Стратегия B')
ax3.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Mann-Whitney U статистика: 10424.00, p-value: 0.2718
T-test статистика: -0.78, p-value: 0.4373

Рис. 4: Сравнение распределений доходностей двух стратегий. Левая панель — гистограммы обеих стратегий, демонстрирующие разные формы распределений. Центральная панель — график боксплотов для визуализации медиан и квартилей. Правая панель — график Q-Q plot стратегии B, показывающий отклонение от нормальности в хвостах
Интерпретация U-статистики: низкое p-value (< 0.05) отвергает гипотезу о равенстве распределений. Тест не указывает характер различий (сдвиг медианы, изменение дисперсии, форма распределения), только их наличие. Для конкретизации используются описательные статистики и визуализация.
Тест Краскела-Уоллиса
Тест Краскела-Уоллиса расширяет подход Манна-Уитни на случай трех и более независимых групп. Это непараметрический аналог однофакторного ANOVA. Применяется для сравнения нескольких стратегий, режимов рынка или временных периодов.
import numpy as np
from scipy.stats import kruskal
import matplotlib.pyplot as plt
# Генерация данных: четыре стратегии с разными характеристиками
np.random.seed(42)
strategy_1 = np.random.normal(0.0005, 0.012, 100)
strategy_2 = np.random.normal(0.0010, 0.015, 100)
strategy_3 = np.random.exponential(0.008, 100) - 0.004
strategy_4 = np.random.lognormal(-0.002, 0.02, 100) - 0.05
# Kruskal-Wallis test
stat, p_value = kruskal(strategy_1, strategy_2, strategy_3, strategy_4)
print(f"Kruskal-Wallis H статистика: {stat:.2f}, p-value: {p_value:.4f}")
# Попарные сравнения (post-hoc) с Mann-Whitney
from scipy.stats import mannwhitneyu
strategies = [strategy_1, strategy_2, strategy_3, strategy_4]
strategy_names = ['S1', 'S2', 'S3', 'S4']
print("\nПопарные сравнения (Mann-Whitney):")
for i in range(len(strategies)):
for j in range(i+1, len(strategies)):
_, p = mannwhitneyu(strategies[i], strategies[j])
print(f"{strategy_names[i]} vs {strategy_names[j]}: p = {p:.4f}")
# Визуализация
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Violin plots
parts = ax1.violinplot([strategy_1, strategy_2, strategy_3, strategy_4],
positions=[1, 2, 3, 4], showmeans=True)
ax1.set_xticks([1, 2, 3, 4])
ax1.set_xticklabels(['S1', 'S2', 'S3', 'S4'])
ax1.set_ylabel('Дневная доходность')
ax1.grid(True, alpha=0.3, axis='y')
# Средние и медианы
means = [np.mean(s) for s in strategies]
medians = [np.median(s) for s in strategies]
x = np.arange(1, 5)
ax2.scatter(x, means, color='black', s=100, label='Среднее', zorder=3)
ax2.scatter(x, medians, color='gray', s=100, label='Медиана', zorder=3)
ax2.plot(x, means, color='black', alpha=0.3)
ax2.plot(x, medians, color='gray', alpha=0.3)
ax2.set_xticks(x)
ax2.set_xticklabels(['S1', 'S2', 'S3', 'S4'])
ax2.set_ylabel('Доходность')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Kruskal-Wallis H статистика: 227.58, p-value: 0.0000
Попарные сравнения (Mann-Whitney):
S1 vs S2: p = 0.3182
S1 vs S3: p = 0.0077
S1 vs S4: p = 0.0000
S2 vs S3: p = 0.4703
S2 vs S4: p = 0.0000
S3 vs S4: p = 0.0000

Рис. 5: Сравнение четырех торговых стратегий методом Краскела-Уоллиса. Левая панель — violin plots, показывающие полное распределение доходностей каждой стратегии. Правая панель — средние и медианы с соединительными линиями для визуализации различий между стратегиями
Код сравнивает четыре торговые стратегии с разными распределениями доходностей: нормальным, экспоненциальным и логнормальным.
Сначала применяется тест Краскела—Уоллиса. Он проверяет общую гипотезу о том, что все распределения одинаковы. Если нулевая гипотеза отклоняется, выполняются попарные сравнения стратегий. Для этого используется тест Манна—Уитни. Он помогает понять, какие именно стратегии отличаются друг от друга.
Такой последующий анализ требует поправки на множественные проверки. Обычно применяют поправку Бонферрони (Bonferroni) или Холма (Holm). При сравнении 4-х стратегий получается 6 парных тестов. Поэтому уровень значимости корректируется: 0.05 / 6 ≈ 0.0083. Это снижает вероятность ложных выводов.
Тест знаковых рангов Вилкоксона
Тест Вилкоксона применяется для парных выборок. Его используют, когда наблюдения связаны между собой, например при измерениях «до и после» или при сравнении двух стратегий на одних и тех же временных периодах. Это непараметрический аналог парного t-теста (paired t-test).
Логика теста проста. Сначала считают разности между парными наблюдениями. Затем берут их абсолютные значения и присваивают им ранги. После этого отдельно суммируют ранги для положительных и отрицательных разностей. Если нулевая гипотеза верна и медиана разностей равна нулю, эти суммы должны быть близки друг к другу.
import numpy as np
from scipy.stats import wilcoxon
import matplotlib.pyplot as plt
# Генерация парных данных: доходности двух стратегий на одних периодах
np.random.seed(42)
n_periods = 120
# Базовая доходность рынка
market_returns = np.random.normal(0.0006, 0.015, n_periods)
# Стратегия A: небольшое улучшение над рынком
strategy_a = market_returns + np.random.normal(0.0003, 0.008, n_periods)
# Стратегия B: другой подход с иным профилем
strategy_b = market_returns + np.random.normal(0.0005, 0.010, n_periods)
# Разности доходностей
differences = strategy_b - strategy_a
# Wilcoxon signed-rank test
stat, p_value = wilcoxon(differences)
print(f"Wilcoxon статистика: {stat:.2f}, p-value: {p_value:.4f}")
print(f"Медиана разностей: {np.median(differences):.6f}")
# Парный t-test для сравнения
from scipy.stats import ttest_rel
t_stat, t_p_value = ttest_rel(strategy_a, strategy_b)
print(f"Парный t-test p-value: {t_p_value:.4f}")
# Визуализация
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))
# Временной ряд разностей
ax1.plot(differences, color='black', linewidth=1, alpha=0.7)
ax1.axhline(y=0, color='red', linestyle='--', alpha=0.5)
ax1.axhline(y=np.median(differences), color='blue', linestyle='--',
label=f'Медиана: {np.median(differences):.6f}')
ax1.set_xlabel('Период')
ax1.set_ylabel('Разность доходностей (B - A)')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Гистограмма разностей
ax2.hist(differences, bins=25, color='black', alpha=0.7, edgecolor='black')
ax2.axvline(x=0, color='red', linestyle='--', linewidth=2, label='Ноль')
ax2.axvline(x=np.median(differences), color='blue', linestyle='--',
linewidth=2, label='Медиана')
ax2.set_xlabel('Разность доходностей')
ax2.set_ylabel('Частота')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Scatter plot: A vs B
ax3.scatter(strategy_a, strategy_b, color='black', alpha=0.5, s=30)
ax3.plot([-0.04, 0.04], [-0.04, 0.04], 'r--', label='Равенство')
ax3.set_xlabel('Стратегия A')
ax3.set_ylabel('Стратегия B')
ax3.legend()
ax3.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Wilcoxon статистика: 3526.00, p-value: 0.7853
Медиана разностей: -0.000802
Парный t-test p-value: 0.7756
Код сравнивает две стратегии, тестируемые на одинаковых временных периодах. Парная структура данных устраняет влияние общих факторов (рыночная доходность, волатильность). Тест Вилкоксона работает с рангами разностей, обеспечивая хорошую устойчивость к выбросам.

Рис. 6: Анализ парных различий между стратегиями методом Вилкоксона. Левая панель — временной ряд разностей доходностей с медианой. Центральная панель — распределение разностей. Правая панель — диаграмма рассеяния доходностей обеих стратегий, точки выше красной линии соответствуют периодам, когда стратегия B показывает лучший результат
Метод применяют для сравнения версий стратегии после изменений, оценки влияния новых параметров и анализа эффективности в разных рыночных режимах. Парная структура данных повышает статистическую мощность, так как устраняет вариации между периодами.
Практический пример комбинирования методов
В реальных аналитических задачах одного метода бывает недостаточно. В таких случаях используют несколько подходов одновременно.
Например, при оценке торговой стратегии на основе импульса (momentum) сначала проверяют причинность между индикатором и доходностью. Затем оценивают статистическую значимость результатов с помощью бутстрэпа. После этого стратегию сравнивают с бенчмарком, используя непараметрические тесты.
import numpy as np
import pandas as pd
import yfinance as yf
from scipy.stats import mannwhitneyu
from statsmodels.tsa.stattools import grangercausalitytests
import matplotlib.pyplot as plt
# Загрузка данных
ticker = yf.Ticker('GM')
data = ticker.history(period='3y')
df = pd.DataFrame({
'Close': data['Close'],
'Volume': data['Volume']
})
# Расчет доходностей и momentum индикатора
df['Returns'] = df['Close'].pct_change()
df['Momentum'] = df['Close'].pct_change(periods=20) # 20-дневный momentum
df = df.dropna()
# 1. Причинность Грейнджера: влияет ли momentum на будущие доходности
causal_df = df[['Returns', 'Momentum']].copy()
print("=== Тест причинности Грейнджера ===")
granger_result = grangercausalitytests(causal_df[['Returns', 'Momentum']],
maxlag=5, verbose=False)
# Извлечение минимального p-value
min_p_value = min([granger_result[lag][0]['ssr_ftest'][1] for lag in range(1, 6)])
print(f"Минимальный p-value по лагам: {min_p_value:.4f}")
# 2. Построение простой momentum стратегии
df['Signal'] = np.where(df['Momentum'] > 0, 1, -1) # Long если momentum > 0
df['Strategy_Returns'] = df['Signal'].shift(1) * df['Returns']
df = df.dropna()
# 3. Bootstrap для оценки Sharpe ratio стратегии
def sharpe(returns):
return np.sqrt(252) * returns.mean() / returns.std()
n_bootstrap = 5000
bootstrap_sharpe = []
strategy_returns = df['Strategy_Returns'].values
np.random.seed(42)
for _ in range(n_bootstrap):
sample = np.random.choice(strategy_returns, size=len(strategy_returns), replace=True)
bootstrap_sharpe.append(sharpe(sample))
ci_lower = np.percentile(bootstrap_sharpe, 2.5)
ci_upper = np.percentile(bootstrap_sharpe, 97.5)
observed_sharpe = sharpe(strategy_returns)
print(f"\n=== Bootstrap анализ Sharpe Ratio ===")
print(f"Наблюдаемый Sharpe: {observed_sharpe:.3f}")
print(f"95% CI: [{ci_lower:.3f}, {ci_upper:.3f}]")
# 4. Сравнение с buy-and-hold через Mann-Whitney
bnh_returns = df['Returns'].values
mw_stat, mw_p = mannwhitneyu(strategy_returns, bnh_returns, alternative='two-sided')
print(f"\n=== Mann-Whitney: Стратегия vs Buy-and-Hold ===")
print(f"U-статистика: {mw_stat:.2f}, p-value: {mw_p:.4f}")
# Визуализация
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 10))
# Кумулятивные доходности
cum_strategy = (1 + df['Strategy_Returns']).cumprod()
cum_bnh = (1 + df['Returns']).cumprod()
ax1.plot(df.index, cum_strategy, label='Momentum Strategy', color='black', linewidth=1.5)
ax1.plot(df.index, cum_bnh, label='Buy and Hold', color='gray', linewidth=1.5)
ax1.set_ylabel('Кумулятивная доходность')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Bootstrap распределение Sharpe
ax2.hist(bootstrap_sharpe, bins=50, color='black', alpha=0.7, edgecolor='black')
ax2.axvline(observed_sharpe, color='red', linestyle='--', linewidth=2, label='Наблюдаемое')
ax2.axvline(ci_lower, color='blue', linestyle='--', linewidth=1.5)
ax2.axvline(ci_upper, color='blue', linestyle='--', linewidth=1.5)
ax2.set_xlabel('Sharpe Ratio')
ax2.set_ylabel('Частота')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Сравнение распределений доходностей
ax3.hist(strategy_returns, bins=50, alpha=0.7, color='black',
edgecolor='black', label='Strategy')
ax3.hist(bnh_returns, bins=50, alpha=0.5, color='gray',
edgecolor='black', label='B&H')
ax3.set_xlabel('Дневная доходность')
ax3.set_ylabel('Частота')
ax3.legend()
ax3.grid(True, alpha=0.3)
# Rolling Sharpe ratio
rolling_window = 60
rolling_sharpe_strategy = df['Strategy_Returns'].rolling(rolling_window).apply(
lambda x: np.sqrt(252) * x.mean() / x.std()
)
rolling_sharpe_bnh = df['Returns'].rolling(rolling_window).apply(
lambda x: np.sqrt(252) * x.mean() / x.std()
)
ax4.plot(df.index, rolling_sharpe_strategy, label='Strategy', color='black', linewidth=1)
ax4.plot(df.index, rolling_sharpe_bnh, label='B&H', color='gray', linewidth=1)
ax4.axhline(y=0, color='red', linestyle='--', alpha=0.5)
ax4.set_ylabel('Rolling Sharpe (60 дней)')
ax4.legend()
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Дополнительная статистика
print(f"\n=== Итоговая статистика ===")
print(f"Средняя доходность стратегии: {strategy_returns.mean()*252:.2%} годовых")
print(f"Средняя доходность B&H: {bnh_returns.mean()*252:.2%} годовых")
print(f"Медиана доходности стратегии: {np.median(strategy_returns):.6f}")
print(f"Медиана доходности B&H: {np.median(bnh_returns):.6f}")

Рис. 7: Комплексный анализ momentum стратегии. Верхний левый график — кумулятивные доходности стратегии и бенчмарка. Верхний правый — бутстрап-распределение коэффициента Шарпа с доверительным интервалом. Нижний левый — сравнение распределений дневных доходностей. Нижний правый — динамика коэффициента Шарпа с скользящем окне для оценки стабильности преимущества
=== Тест причинности Грейнджера ===
Минимальный p-value по лагам: 0.6994
=== Bootstrap анализ Sharpe Ratio ===
Наблюдаемый Sharpe: 0.773
95% CI: [-0.373, 1.980]
=== Mann-Whitney: Стратегия vs Buy-and-Hold ===
U-статистика: 268079.50, p-value: 0.8397
=== Итоговая статистика ===
Средняя доходность стратегии: 26.53% годовых
Средняя доходность B&H: 34.50% годовых
Медиана доходности стратегии: 0.000931
Медиана доходности B&H: 0.001667
Представленный выше комплексный анализ сочетает три метода:
- Тест Грейнджера (Granger test) проверяет, помогает ли импульсный индикатор (momentum) предсказывать будущие доходности;
- Бутстрэп оценивает надежность коэффициента Шарпа и строит для него доверительный интервал;
- Тест Манна-Уитни сравнивает распределения доходностей стратегии и подхода «купи и держи» (buy-and-hold) без предположений о нормальном распределении данных.
Интерпретация полученных результатов: тесты не подтвердили эффективность стратегии. Тест Грейнджера не выявил предсказательной силы, коэффициент Шарпа статистически незначим, различий с buy-and-hold не обнаружено. Несмотря на периоды опережения, по факту фактическая доходность за полный период у стратегии оказалась ниже чем у «купи-и-держи»: 26.5% против 34.5%.
Интерпретация должна опираться на согласованность результатов всех тестов.
Изолированная причинность по Грейнджеру без подтверждения в тесте Манна—Уитни указывает на ограниченную экономическую значимость эффекта. Узкий доверительный интервал бутстрэпа при низком уровне коэффициента Шарпа свидетельствует о устойчиво слабых риск-скорректированных характеристиках стратегии.
Практически это означает необходимость проверять причинность до построения моделей, оценивать устойчивость всех метрик с помощью бутстрэпа и применять непараметрические тесты при сравнении стратегий. Совместное использование методов снижает вероятность ложных выводов и повышает надежность аналитических решений.
Заключение
Продвинутые статистические методы расширяют возможности анализа и сравнения выборок за пределы стандартных тестов и повышают качество решений при разработке торговых стратегий и управлении рисками:
- Тест Грейнджера (Granger test) выявляет предсказательные связи между переменными;
- Бутстрэп (Bootstrap) оценивает надежность метрик без предположений о распределении;
- Непараметрические тесты обеспечивают корректное сравнение стратегий при произвольных распределениях данных.
Использование этих методов в комплексе позволяет получать надежные, статистически обоснованные выводы, снижать влияние случайных факторов и формировать стратегии, опирающиеся на проверенные закономерности рынка.