Как построить доверительные интервалы для бэктеста
Одна кривая доходности является лишь единичной выборкой. Доверительный интервал коэффициента Шарпа часто включает ноль. Узнайте, как правильно строить и интерпретировать эти данные.
0.88
Доверительный интервал бэктеста отвечает на один вопрос: какая часть кривой доходности обусловлена самой стратегией, а какая — конкретным историческим периодом, на котором она тестировалась. Метод бутстрэппинга (bootstrapping) позволяет построить такой интервал путем многократной перевыборки ряда доходностей, пересчета статистики для каждой итерации и определения перцентилей полученного распределения. Коэффициент Шарпа, равный одной целой и четырем десятым, измеренный на годовом интервале по дневным данным, имеет девяностопятипроцентный доверительный интервал, который включает в себя ноль. Приведенные ниже графики объясняют причины этого явления.
Анализ статистической значимости результатов
Анализ статистической значимости результатов
Почему одна кривая доходности — это лишь одна выборка
Бэктест выдает по одному значению на каждый показатель: один коэффициент Шарпа, одну годовую доходность, одну максимальную просадку, один процент прибыльных сделок. Ни один из этих показателей не является истинной ценностью стратегии. Каждый из них — это оценка, построенная на конечном отрезке торговых дней, и другой временной интервал той же длительности показал бы иные результаты.
Приведенная ниже панель полностью исключает стратегию из уравнения. Она оценивает простейшую позицию: удержание SPY в течение одного календарного года на основе доходности от закрытия до закрытия.
Точный SQL-код для каждого числа
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2011-12-01'
AND date < '2026-01-01'
)
SELECT
toString(toYear(date)) AS year,
count() AS obs_count,
round(avg(ret) * 252 * 100, 2) AS ann_return_pct,
round(stddevSamp(ret) * sqrt(252) * 100, 2) AS ann_vol_pct,
round(avg(ret) / stddevSamp(ret) * sqrt(252), 2) AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
AND ret IS NOT NULL
GROUP BY year
ORDER BY yearКаждая строка охватывает около 250 сессий, что соответствует стандартному торговому году. Условия позиции не менялись ни в одном из случаев. В 2012 годовой коэффициент Шарпа составил 1.06; в 2025 он составил 0.88, при этом на графике отображено 14 лет. Один год удержания широкого индекса меняет итоговый показатель на величину, которую большинство читателей сочли бы значимой, а не просто рыночным шумом, и бэктест стратегии такой же длительности наследует как минимум такую же погрешность. Конвенция аннуализации, лежащая в основе этого столбца, описана в нашем руководстве по коэффициенту Шарпа, а механика доходности за период — в статье как измеряется месячная доходность.
Насколько широк доверительный интервал для коэффициента Шарпа в бэктесте?
Стандартная ошибка оценки коэффициента Шарпа примерно обратно пропорциональна квадратному корню из количества наблюдений. Вместо того чтобы полагаться на эту формулу, измерьте разброс напрямую. Разделите двадцатилетний период торгов на непересекающиеся окна фиксированной длины, вычислите годовой коэффициент Шарпа внутри каждого окна и оцените разброс полученных значений.
Точный SQL-код для каждого числа
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
blocks AS
(
SELECT
w,
intDiv(i, w) AS blk,
count() AS n,
avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252) AS sharpe
FROM indexed
CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
GROUP BY w, blk
HAVING n = w
)
SELECT
concat(toString(w), ' sessions') AS horizon,
count() AS block_count,
round(quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_p05,
round(quantileDeterministic(0.50)(sharpe, blk), 2) AS sharpe_p50,
round(quantileDeterministic(0.95)(sharpe, blk), 2) AS sharpe_p95,
round(quantileDeterministic(0.95)(sharpe, blk)
- quantileDeterministic(0.05)(sharpe, blk), 2) AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY wПри 21 sessions на окно, диапазон от пятого до девяносто пятого перцентиля измеренного коэффициента Шарпа составляет от -3.37 до 6.97, что дает разброс в 10.34 пунктов коэффициента Шарпа по 238 окнам. Увеличьте каждое окно до 504 sessions, и разброс сократится до 1.67 пунктов, измеренных теперь всего по 8 окнам. Происходят два процесса одновременно. Оценка становится точнее с увеличением размера выборки, но количество независимых периодов, которые может предоставить история, сокращается. Это противоречие и является сутью проблемы.
Как рассчитать доверительный интервал бэктеста методом бутстрепа
Процедура достаточно коротка, чтобы привести её полностью.
- Возьмите ряд реализованной доходности стратегии, по одному значению на каждый период, всего N значений.
- Случайным образом выберите из этого списка N значений с возвращением. Некоторые попадутся дважды, некоторые не попадутся вовсе.
- Пересчитайте статистический показатель для полученной выборки.
- Повторите процедуру несколько тысяч раз, сохраняя каждое значение.
- Отсортируйте сохраненные значения и определите 2,5-й и 97,5-й процентили для получения девяностопятипроцентного интервала.
Перед выполнением шага два задайте начальное значение (seed) генератора случайных чисел и укажите его рядом с опубликованным интервалом. Бутстреп — это метод Монте-Карло: два запуска без фиксации seed дадут разные результаты в последних знаках, и проверяющий, который не может воспроизвести ваш интервал, не имеет возможности его подтвердить. Это та же дисциплина, что описана в воспроизводимой настройке бэктеста.
Средняя доходность и коэффициент Шарпа корректно проходят через шаг два, так как оба показателя считывают список доходностей как набор данных. Максимальная просадка — нет. Просадка учитывает последовательность событий. Перевыборка, меняющая порядок доходностей, дает максимальную просадку, которую не могла бы создать ни одна последовательность реальных сделок. Тем не менее, применение бутстрепа оправдано, если результат четко обозначен: это распределение просадок по перетасованным историям, а не прогноз следующей просадки. Определение приведено в максимальной просадке.
Почему IID-бутстреп некорректен для оценки рыночной доходности
Второй этап предполагает, что каждая доходность является независимой и одинаково распределенной — это допущение IID. Дневная доходность нарушает это условие, что меняет ширину интервала. Знаковые доходности обладают лишь слабой однодневной памятью. Однако их абсолютные величины склонны к кластеризации: за крупными движениями следуют крупные, а спокойные дни идут сериями.
Точный SQL-код для каждого числа
WITH daily AS
(
SELECT
ticker,
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
AND date >= '2015-11-01'
AND date < '2026-01-01'
),
lagged AS
(
SELECT
ticker,
date,
ret,
lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
FROM daily
WHERE ret IS NOT NULL
AND abs(ret) < 0.35
)
SELECT
ticker,
count() AS obs_count,
round(corr(ret, ret_prev), 3) AS return_autocorr,
round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESCДля SPY автокорреляция первого порядка абсолютной дневной доходности, измеренная 0.366 за 2514 сессий, составляет -0.133 по сравнению с показателем для знаковых доходностей за те же дни. Сравните два столбца для любого инструмента на графике. Перемешивание ряда доходностей разрушает эту кластеризацию, и IID-бутстреп, запущенный на этих данных, показывает более узкий интервал, чем тот, который подтверждается выборкой. Ошибка проявляется в наименее желательном ключе: она приукрашивает результаты стратегии.
Скользящий блочный бутстреп и выбор длины блока
Решение заключается в повторной выборке непрерывных блоков вместо отдельных доходностей. Выберите длину блока L, извлекайте блоки из L последовательных доходностей случайным образом с возвращением и соединяйте их друг с другом, пока синтетический ряд не достигнет длины N. Зависимость внутри блока сохраняется: эти доходности остаются в исходном порядке. Искусственными являются только точки соединения между блоками.
Длина блока — это компромисс между двумя видами ошибок, и ни один параметр не позволяет избежать обеих. Короткие блоки ведут себя как IID-бутстреп и занижают интервал, что создает смещение. Длинные блоки сохраняют больше зависимостей, но оставляют меньше отдельных блоков для выборки, поэтому каждая повторная выборка повторяет крупные фрагменты одной и той же истории, а сам интервал становится шумным, что увеличивает дисперсию. Опубликованные эмпирические правила масштабируют длину блока как N в степени одна треть. Рассматривайте их как отправные точки.
Более простой диагностический метод — проверить, как дисперсия масштабируется с горизонтом. При независимости дисперсия суммы доходностей за k дней равна k, умноженному на однодневную дисперсию, а отношение этих величин равно единице.
Точный SQL-код для каждого числа
WITH daily AS
(
SELECT
date,
toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2005-12-01'
AND date < '2026-01-01'
),
indexed AS
(
SELECT
ret,
row_number() OVER (ORDER BY date) AS i
FROM daily
WHERE date >= '2006-01-01'
AND ret IS NOT NULL
),
base AS
(
SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
SELECT
k,
intDiv(i, k) AS blk,
count() AS n,
sum(ret) AS block_ret
FROM indexed
CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
GROUP BY k, blk
HAVING n = k
)
SELECT
concat(toString(k), ' sessions') AS block_length,
count() AS block_count,
round(varSamp(block_ret) / (k * any(var_1d)), 3) AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY kНа 2 sessions это отношение составило 0.844; на 63 sessions оно составило 0.584, рассчитанное по 78 непересекающимся блокам. Значения, близкие к единице, означают, что сумма масштабируется так же, как при независимых выборках на данном горизонте. Значения, отличные от единицы, указывают на горизонты, где зависимость все еще проявляется, и это тот диапазон, который должна охватывать длина блока. Выберите кратчайший блок, который перекрывает этот диапазон, а затем оцените, до какого значения сократилось количество блоков.
Бакетная перевыборка и количество наблюдений рядом с каждым интервалом
Перевыборка внутри бакетов — по режимам волатильности или календарным месяцам — сохраняет условия, которые делают вопрос содержательным. Если утверждение состоит в том, что стратегия обеспечивает свой коэффициент Шарпа в периоды высокой волатильности, то интервал, построенный исключительно на данных дней с высокой волатильностью, является тем инструментом, который проверяет это утверждение. Цена этого — арифметическая погрешность. Разделение двухсот пятидесяти наблюдений на четыре бакета оставляет примерно шестьдесят наблюдений на каждый, а бутстреп по шестидесяти наблюдениям дает интервал примерно в два раза шире, чем версия по всей выборке.
Поэтому указывайте количество наблюдений в бакете рядом с каждым интервалом, каждый раз. Столбец block_count на панелях выше — это наглядное воплощение данной привычки: пятое процентильное значение, полученное из девяти окон, является иным объектом, нежели значение, полученное из двухсот, даже если оба они отображаются с двумя знаками после запятой.
Что не может исправить бутстреп
Бутстреп количественно оценивает только один аспект: шум выборки в статистике, рассчитанной на основе имеющихся данных. Он не дает ответа на вопрос, были ли эти данные достижимы в реальности.
Бэктест, содержащий ошибку заглядывания в будущее (look-ahead bias), формирует ряд доходности, который невозможно было реализовать в торговле, а бутстреп этого ряда лишь создает узкий, «уверенный» доверительный интервал вокруг вымысла. Вселенная активов, сформированная из текущих участников индекса, несет в себе ошибку выжившего (survivorship bias), и каждая повторная выборка из этой вселенной наследует данную проблему. Третий пробел — это эффект селекции: если запустить двести вариантов стратегии и оставить лучший, бутстреп-интервал опишет шум выборки только для этого одного варианта, игнорируя те сто девяносто девять попыток, которые были сделаны для его выбора. Честные доверительные интервалы полезны. Но они не заменяют данные вне выборки (out-of-sample).
Примечания к данным и методологии
- Доходность рассчитывается как изменение цены от закрытия до закрытия по дневным барам. Дивиденды исключены, что занижает уровень каждой доходности и коэффициента Шарпа примерно на величину дивидендной доходности. Дисперсия, рассматриваемая в этой статье, остается практически неизменной.
- Окна и блоки не перекрываются, поэтому каждая измеренная статистика использует непересекающиеся фрагменты истории. Перекрывающиеся окна искусственно завысили бы количество наблюдений.
- Квантили рассчитываются с помощью детерминированного оценщика, поэтому повторный запуск панели возвращает тот же процентиль, а не новое приближение.
- Панель автокорреляции использует шесть крупных компаний, у которых не было дробления акций внутри окна, и исключает любые дни с изменением цены более тридцати пяти процентов, что позволяет избежать влияния артефактов корректировки цен на корреляцию.
Часто задаваемые вопросы
Что бутстреп-доверительный интервал говорит о бэктесте?
Он показывает диапазон значений, которые статистический показатель мог бы принять при повторной выборке того же процесса на том же количестве периодов. Если девяностопятипроцентный интервал включает ноль, это означает, что выборка слишком мала для того, чтобы отличить стратегию от отсутствия рыночного преимущества.
Сколько бутстреп-ресемплов достаточно?
Для девяностопятипроцентного интервала нескольких тысяч ресемплов обычно достаточно для стабильности, а десять тысяч — общепринятый стандарт, не требующий больших вычислительных затрат. Для оценки квантилей «хвостов» распределения требуется больше: первый процентиль из тысячи выборок определяется всего по десяти значениям.
Какую длину блока следует использовать в бутстрепе с подвижным блоком?
Универсально верной длины не существует. Эмпирические правила предлагают масштабировать её пропорционально кубическому корню из размера выборки. Практический метод проверки — горизонт, на котором дисперсия перестает масштабироваться линейно, что измеряется панелью коэффициента дисперсии выше. Указывайте использованную длину блока рядом с интервалом.
Можно ли применять бутстреп к максимальной просадке?
Да, с оговоркой относительно порядка данных. Просадка зависит от последовательности доходностей, поэтому ресемпл, созданный путём перемешивания, показывает просадку переупорядоченной истории. Бутстреп с блоками сохраняет короткие последовательности нетронутыми и является более подходящим инструментом для статистики путей.
Исправляет ли бутстреп переобучение?
Нет. Он измеряет шум выборки внутри одного ряда доходностей. Ошибка заглядывания в будущее и эффект селекции при тестировании множества вариантов находятся вне зоны его видимости.
Каждая панель здесь содержит SQL-запрос, с помощью которого она была сформирована. Измените тикер или длину окна и запустите его самостоятельно в терминале Strasmore.