Сколько бэктестов убивают стратегию: эксперимент с BTC/ETH перпами
Суть идеи
Авторы из Rulyfi провели один и тот же поиск стратегий на 5.7-летней истории BTC и ETH perpetuals дважды — с единственным отличием: во втором прогоне убрали «выметенные» конфигурации двух параметров. Результат: одна и та же стратегия с побитово идентичными сделками получила Deflated Sharpe (DSR) 0.035 в первом прогоне (21.9M испытаний) и 0.919 во втором (14.8M испытаний). Стратегия не изменилась ни на йоту — изменился знаменатель поправки на множественное тестирование.
DSR — это классический Sharpe, скорректированный на число независимых испытаний в пространстве поиска. Чем шире пространство, тем жёстче штраф: при 21.9M попыток даже хорошо выглядящая кривая капитала оказывается статистически неотличима от случайного везения. Сократи пространство вдвое — и та же кривая «проходит» порог значимости.
Механизм прост и беспощаден: если ты перебрал достаточно комбинаций параметров, хотя бы одна из них случайно подстроится под шум в данных. DSR пытается это учесть, но сам DSR зависит от того, насколько честно ты сообщаешь о размере поиска. Занизи число — получишь ложное ощущение значимости.
Применимость к крипте
Эксперимент проведён прямо на BTC/ETH perps — переносить ничего не нужно. Это редкий случай, когда методологическая статья использует именно наш актив-класс. Выводы напрямую касаются любого grid-поиска по параметрам: скользящие окна для rolling-z, пороги gamma flip, уровни VRP-фильтра, длины look-back для SOPR или STH-MVRV. Каждый дополнительный параметр, каждый дополнительный диапазон значений — это множитель к числу испытаний и, соответственно, штраф к DSR.
Для нашего движка это означает: нужно логировать полное число протестированных конфигураций с самого начала, а не только «финалистов». Иначе DSR, посчитанный постфактум, будет оптимистично смещён.
Тестируемо ли
Да, и именно на нашем движке. Нужны: BTC/ETH OHLCV (perp или spot), любая параметризованная стратегия (например, momentum с rolling-z), два прогона grid-search с разным охватом пространства параметров. Считаем DSR для каждого прогона по формуле Бейли–Лопес де Прадо. Данные есть, формула открытая, Python-реализация существует. Единственное честное ограничение: нужно заранее зафиксировать и не редактировать список конфигураций — иначе воспроизводим именно ту ошибку, о которой статья предупреждает.
Сомнения
Статья сама по себе — демонстрация проблемы, а не её решение. DSR не защищает от data-snooping полностью: он работает только если ты честно сообщаешь о размере пространства поиска. На практике исследователи часто «забывают» посчитать предварительные эксперименты, ручные правки и отброшенные идеи — и DSR снова становится оптимистичным. Кроме того, 5.7 лет BTC/ETH — это несколько очень разных режимов (бычий 2020–2021, медвежий 2022, боковик 2023–2024); стратегия, выжившая в grid-search, могла просто поймать один из этих режимов. Walk-forward OOS здесь необходим, но в аннотации о нём не упоминается.
Следующий шаг
Взять нашу rolling-z momentum стратегию на BTC perp (параметры: окно look-back, порог входа, стоп), запустить два grid-search: широкий (все комбинации) и узкий (только «разумные» диапазоны), посчитать DSR для каждого прогона и сравнить — насколько DSR лучшей конфигурации меняется в зависимости от задекларированного размера пространства. Горизонт: 2020–2024, walk-forward split 70/30.
Результаты тестов
Дописано 2026-08-04 — прогнали «Следующий шаг» буквально, на BTC и ETH. Эффект статьи воспроизвёлся полностью, но его заявленная причина не проходит арифметику: число испытаний тут почти ни при чём.
Спека выполнена как написана: rolling-z momentum на дневных перпах (z = отклонение цены от своей скользящей средней в СКО), три оси параметров — окно look-back, порог входа, трейлинг-стоп — плюс ось «только лонг / лонг-шорт». Меню зафиксированы до прогона: широкое 11×9×6×2 = 1188 конфигураций, узкое («разумные диапазоны») 5×3×3×2 = 90, причём узкое — строго подмножество широкого, чтобы победитель был один и тот же с побитово одинаковыми сделками. Окно 2020-01-01…2026-07-31 (2404 дня), вход на следующий день после сигнала, издержки 6 б.п. на единицу оборота, funding перпа по фактическим 8-часовым событиям (drag лонга +11.9%/год на BTC, +14.1% на ETH), размер позиции фиксированный — никаких рамп плеча.
Порог DSR почти не чувствителен к размеру поиска
Deflated Sharpe — это вероятностный Sharpe, замеренный не от нуля, а от планки «ожидаемый максимум из N испытаний под нулевой гипотезой»:
sr0 = sd_SR · [(1−γ)·Φ⁻¹(1−1/N) + γ·Φ⁻¹(1−1/(N·e))]
Здесь два входа, и они устроены принципиально по-разному. Число испытаний N сидит внутри обратной функции нормального распределения, то есть входит примерно как √(2·lnN) — почти плоско. А sd_SR, разброс Sharpe по «толпе» испытаний, множитель — входит линейно.
Считаем множитель при sd_SR для двух чисел из статьи: N = 21.9M даёт 5.4462, N = 14.8M даёт 5.3760. Разница — 1.31%. При нашем разбросе меню это сдвиг порога на 0.0013 дневного Sharpe. А чтобы перевести DSR с 0.035 на 0.919 при длине истории ~2400 дней, порог должен сдвинуться на 0.0655 дневного Sharpe — в 52 раза больше.
То же самое на живой стратегии. Берём победителя нашего широкого поиска и применяем к нему в точности срез из статьи:
| Размер поиска | порог, годовых | DSR (BTC) | DSR (ETH) |
|---|---|---|---|
| 21.9M | 1.87 | 0.079 | 0.009 |
| 14.8M | 1.84 | 0.090 | 0.011 |
| изменение | +0.011 | +0.002 |
Заявлено +0.884. Чтобы получить такой размах одним лишь числом испытаний, поиск должен измениться в 1.9 миллиона раз (BTC: с 121 конфигурации до 234 миллионов) или в 98 тысяч раз (ETH) — а не в 1.48 раза, как в эксперименте.
Тот же размах — при полностью неизменном N
Раз не N, то что? Второй вход формулы, sd_SR. Он оценивается по разбросу наблюдаемых Sharpe внутри самого меню, то есть зависит от того, кого именно исследователь записал в соседи — а не от размера поиска.
Фиксируем задекларированное число испытаний ровно на 90 и меняем только состав этой девяностки. Стратегия, её сделки и её Sharpe не меняются ни на йоту:
| Состав «толпы» (N = 90 везде) | sd_SR, годовых | DSR (BTC) | DSR (ETH) |
|---|---|---|---|
| тесная кучка вокруг медианы | 0.01 / 0.02 | 1.000 | 0.998 |
| 90 случайных соседей | 0.33 / 0.37 | 0.947 | 0.658 |
| разношёрстные: 45 худших + 45 лучших | 0.72 / 0.76 | 0.114 | 0.010 |
Размах — 0.886 на BTC и 0.988 на ETH. То есть феномен статьи — «одна и та же стратегия получает почти нулевой и почти единичный DSR» — воспроизводится целиком, без единого изменения числа испытаний. Это не смягчает вывод статьи, а ужесточает его: манипулируемая ручка не та, на которую показывают, и она куда чувствительнее.
Отсюда же следует неприятное свойство: формула немонотонна по размеру поиска. Мы взяли широкое меню и сгустили сетку вокруг найденного плато — 192 дополнительные конфигурации в окрестности победителя, самый обычный ход исследователя «дай-ка посмотрю поближе». На ETH DSR от этого вырос: 0.395 → 0.405. Расширил поиск на две сотни испытаний — стало «значимее».
Что дают наши собственные два меню
Здесь N-канал уже работает — но потому, что меню различаются в 13 раз, а не в 1.48:
| BTC (L60, θ 0.5, стоп 4%, только-лонг) | ETH (L20, θ 1.5, без стопа, только-лонг) | |
|---|---|---|
| Sharpe стратегии | +1.38 | +1.07 |
| DSR при N = 1188 | 0.759 | 0.395 |
| DSR при N = 90 | 0.967 | 0.860 |
| вклад канала N | +0.175 | +0.307 |
| вклад канала sd_SR | +0.117 | +0.283 |
Прямой ответ на вопрос статьи «насколько DSR лучшей конфигурации меняется от задекларированного размера пространства»: на 0.21 (BTC) и 0.47 (ETH) при сокращении поиска в 13 раз — и примерно наполовину это вклад не размера, а состава.
Чем лечится: нуль вместо формулы
Формула требует угадать N независимых испытаний. Но испытания в сетке чудовищно перекрываются: средняя парная корреляция дневных доходностей по нашему меню +0.38 (BTC) и +0.34 (ETH), что даёт N_eff ≈ 2.6 и 2.9 из 1188. Любое число вида «21.9 миллиона испытаний» как счёт независимых попыток бессмысленно — не потому, что кто-то его занизил или завысил, а потому, что независимых попыток в параметрической сетке единицы.
Поэтому угадывать не надо: инфляцию от выбора можно измерить. Ротационный нуль — позиции и издержки каждой конфигурации сохраняются, доходности циклически сдвигаются общим для всех сдвигом (так сохраняется корреляция испытаний), максимум берётся по строке. Получаем распределение «лучшего из меню» при полном отсутствии связи сигнала с будущей ценой:
| BTC | ETH | |
|---|---|---|
| одиночное испытание под нулём | +0.10 | +0.13 |
| лучшее из 1188 под нулём | +1.05 | +1.09 |
| «даром» за счёт перебора | +0.95 | +0.96 |
| для сравнения, планка Bailey-LdP | 1.13 | 1.17 |
| лучшее из 90 под нулём | +0.78 | +0.85 |
| планка Bailey-LdP при N = 90 | 0.74 | 0.68 |
Аналитика попадает в цель по порядку величины, но её нельзя отмыть, а эмпирический порог — можно проверить на отмывание напрямую. Добавляем в меню 528 заведомо пустых конфигураций: DSR послушно падает (0.759 → 0.517), но при сгущении сетки, как показано выше, может и вырасти. Эмпирический порог за то же расширение сдвигается с 1.05 до 1.06 годовых — то есть расширение поиска эмпирическим нулём не отмывается ни в одну сторону.
И главный практический выход. Честный family-wise вердикт по самим победителям: доля сдвигов, где лучшее из меню под нулём дотягивается до наблюдаемого, — 0.109 на BTC и 0.184 на ETH для широкого меню. Оба победителя незначимы. Узкое меню даёт BTC 0.027 — но узкое меню и выбрано было постфактум, ровно в этом и состоит предупреждение статьи.
Честный OOS
Walk-forward 70/30 по спеке (IS до 2024-08-09, OOS после):
| IS-Sharpe | OOS-Sharpe | buy & hold OOS | |
|---|---|---|---|
| BTC, победитель IS | +1.75 | +0.25 | +0.24 |
| ETH, победитель IS | +1.37 | −0.16 | +0.08 |
BTC-победитель на честном отрезке в точности повторил пассивный лонг, ETH-победитель проиграл ему. Сомнение из аннотации — «стратегия, выжившая в grid-search, могла просто поймать режим» — подтверждено. Добавим сюда, что победитель хрупок и к задержке исполнения: вход на день позже обваливает BTC с +1.38 до +0.79, ETH с +1.07 до +0.80.
Плацебо и положительный контроль
Тест обязан не пропускать пустышку — иначе он не защищает ни от чего. Прогнали весь пайплайн на 20 независимых синтетических мирах с той же волатильностью и без всякого сигнала. «Sharpe шума» — лучший из 1188 конфигураций на случайных данных — в среднем +0.91 (BTC-вола) и +0.88 (ETH-вола), в лучшем мире +1.37; это и есть цена перебора, которую иначе принимают за находку. Ложных срабатываний: 0 из 20 у DSR при порогах 0.95 и 0.90, 0 из 20 у ротационного нуля при 5% (среднее p = 0.48 и 0.55 — нуль откалиброван).
Обратная проверка — тест обязан находить настоящий эдж. Взяли непобедившую, медианную по Sharpe конфигурацию (чтобы база не несла в себе результат отбора) и внедрили в неё известный истинный эдж. Оба теста уверенно находят его начиная с истинного ann-Sharpe 1.5 (реализованный ~2.0): DSR 0.993/0.965, p нуля 0.010/0.017. При истинном эдже 1.0 не проходит ни один — это честная цена поправки на перебор из 1188 конфигураций, а не слепота.
Вывод
Тезис статьи — «размер пространства поиска убивает результат» — верен по духу и промахивается по механике. Число испытаний входит в поправку логарифмически: чтобы им сдвинуть DSR на заявленную величину, поиск надо менять не в полтора раза, а в сотни тысяч раз. Реальная ручка — состав меню сравнения, через который DSR той же самой стратегии гоняется от 0.01 до 1.00 при неизменном N, и который к тому же ломает монотонность: расширение поиска может повысить «значимость». Практический вывод для собственного движка ровно противоположен процитированному в аннотации: логировать полное число протестированных конфигураций полезно для дисциплины, но само по себе оно поправку не спасает — спасает измерение максимума под нулём на фактическом меню, где ни угадывать N, ни оценивать разброс по соседям не требуется. У нас это уже так: в турнире bt_engine DSR служит сортировкой внутри одного прогона, ни один крон и ни одна публикация на сайте от его уровня не зависят, а решения о заводе линий в форвард принимаются по эмпирическому нулю.
Проверка: research_dsr_breadth.py (brain), меню и оси зафиксированы в шапке скрипта до прогона. Данные — Binance perp, дневные бары из собственного 5-минутного кэша, funding по фактическим событиям. Ключевые числа воспроизведены независимой реимплементацией verify_dsr_breadth.py с нуля: цены сверены со сторонним источником (максимальное расхождение 0), Sharpe победителя пересчитан своим стоп-циклом (расхождение 0), DSR — собственным обратным нормальным распределением и собственными моментами (расхождение < 1.2e-9), ротационный нуль пересчитан на другой сетке сдвигов. Лукахед-аудит: шок ×3 в последних 30 днях не меняет ни одной позиции до момента шока; повторный прогон побайтово детерминирован. Оговорка о нуле: наше меню однородно-трендовое и лонговое, а циклический сдвиг против постоянной позиции бессилен тождественно, поэтому равновзвешенное меню лежит на 98–100-м перцентиле своего нуля — нуль здесь консервативен, его легко бить, и то, что победители его всё равно не проходят, делает отрицательный вердикт только надёжнее. Оригинал Rulyfi на момент прогона недоступен (redirect Quantocracy пуст), поэтому проверялся механизм в формулировке этой аннотации; найденный канал sd_SR не столько противоречит их числам, сколько объясняет их: удаление «выметенных» конфигураций меняет именно разброс по меню.