baykovpro

← Quant

RMP: как оценить бэктест против случайного максимума поиска

18 августа 2026 · 12 мин · бэктест, data-snooping, Deflated Sharpe, переподгонка

Суть идеи

Когда вы прогоняете скан из N стратегий и берёте лучшую, вы неизбежно получаете инфляцию результата — даже если ни одна стратегия не работает, случай подарит вам «победителя». Deflated Sharpe (DSR) пытается это учесть через поправку на множественное тестирование, но у него есть слепая зона: он предполагает определённую форму распределения результатов поиска и плохо работает, когда число испытаний велико, а распределение Sharpe нестандартно.

RMP (Random-Max Percentile) — альтернативный взгляд на ту же проблему. Для каждой строки в результатах скана он отвечает на вопрос: если бы все N стратегий были чистым шумом, какова вероятность, что лучший из N случайных результатов окажется ниже данной строки? RMP = 0.97 означает: даже «рекорд удачи» при данном размере поиска с вероятностью 97% не дотягивает до этой стратегии. Иными словами, это перцентиль наблюдаемого результата в распределении случайных максимумов.

Метрика не заменяет DSR, а закрывает его слепую зону — особенно когда N велико и максимум случайного поиска сам по себе становится внушительным числом. Авторы встраивают RMP как дополнительную колонку в экспорт результатов скана.

Применимость к крипте

Для нашего bt_engine это прямая методология, не требующая адаптации под специфику крипты. Мы регулярно прогоняем grid-поиск по параметрам: окно rolling-z для skew/RR, порог GEX gamma flip, лаг сигнала по STH-MVRV, комбинации макро-оверлеев. Каждый такой скан — это ровно та ситуация, для которой придуман RMP.

Особенно актуально для BTC/ETH опционных стратегий, где история коротка (2017–2025 для BTC, 2020+ для ETH с нормальной ликвидностью), а число независимых событий мало. Чем короче история и чем больше параметров перебирается — тем выше риск, что «лучшая» стратегия просто случайный максимум. RMP даёт этому риску численную оценку.

Тестируемо ли

Да, и реализация несложная. Алгоритм: (1) зафиксировать N — число испытаний в скане; (2) для каждого испытания получить Sharpe (или другую целевую метрику); (3) смоделировать распределение максимума из N случайных Sharpe (аналитически или через симуляцию); (4) для каждой строки результатов вычислить CDF этого распределения в точке наблюдаемого значения — это и есть RMP.

В нашем движке это добавляется как постпроцессинг после walk-forward: на каждом окне считаем RMP по результатам того же скана. Никаких дополнительных рядов данных не нужно — только результаты самого скана.

Сомнения

RMP корректно работает только если испытания в скане действительно независимы или хотя бы слабо коррелированы. Если вы перебираете параметры одной стратегии (например, окно от 10 до 50 дней с шагом 1), результаты соседних параметров сильно скоррелированы — эффективное N намного меньше номинального, и RMP будет завышен. Это та же проблема, что у DSR, просто в другой форме.

Кроме того, RMP не говорит ничего о том, почему стратегия работает. Высокий RMP при отсутствии внятного механизма — это просто красиво упакованная переподгонка.

Следующий шаг

Взять последний grid-поиск по параметрам skew/RR сигнала (25Δ BTC, rolling-z окно 5–60 дней, порог ±1σ..±2σ) — это легко даёт N > 100 испытаний — и добавить к результатам колонку RMP через симуляцию случайного максимума (10 000 итераций). Сравнить топ-5 стратегий по Sharpe с их RMP: если RMP < 0.90, результат статистически не отличим от удачи при данном размере поиска.

Результаты тестов

Дописано 2026-08-18 — прогнали «Следующий шаг» буквально. Метрика оказалась не тем, чем её описывает аннотация: собственное «Сомнение» статьи названо верно, но с обратным знаком, и правило RMP < 0.90 ошибается не в сторону оптимизма, а в сторону молчания. Починенная версия RMP численно совпадает с тем, что у нас уже стоит.

Спека выполнена как написана: сигнал — 25Δ skew BTC (btc_skew_25delta, тот же живой ряд, на котором работает s_skew_fade в bt_engine), rolling-z с окном L, вход при |z| выше порога θ. Оси зафиксированы до прогона: L ∈ {5,10,…,60} (12 значений) × θ ∈ {1.0,1.1,…,2.0} (11) × направление сделки {фейд, моментум} (2) — N = 264 испытания, как и требует спека («легко даёт N > 100»). Окно 2025-06-24…2026-08-18 (421 день), решение на закрытии дня t → позиция с t+1, издержки 6 б.п. на единицу оборота, Sharpe годовой нетто, симуляция случайного максимума — 10 000 итераций.

Одна развилка в спеке требует решения: «смоделировать распределение максимума из N случайных Sharpe» не говорит, какова дисперсия одного случайного Sharpe, а от неё зависит всё. Поэтому считаем три прочтения сразу, а не выбираем одно:

Скан из «Следующего шага» пуст

# окно L порог θ напр. Sharpe RMP-A RMP-B RMP-C pMax
1 5 1.3 моментум 1.70 0.000 0.613 0.452 0.35
2 5 1.2 моментум 1.62 0.000 0.465 0.385 0.45
3 5 1.4 моментум 1.38 0.000 0.092 0.188 0.60
4 5 1.1 моментум 1.16 0.000 0.002 0.055 0.78
5 15 1.9 моментум 1.16 0.000 0.002 0.055 0.96

Все три прочтения и наш собственный эмпирический нуль согласны: ни одна строка не дотягивает до 0.90, вердикт по правилу статьи — «не отличим от удачи». То же на длинной истории, где та же решётка навешена на rolling-z самой цены (2019-03-29…2026-08-18, 2700 дней): лучший Sharpe 0.86 при RMP-A 0.074, RMP-B 0.000, RMP-C 0.855, pMax 0.245.

Полезно понимать, что именно нашёл поиск. Победитель — не «скью-эдж», а однодневный разворот: 25Δ skew движется вместе со спотом почти механически (одновременная corr(Δskew, ret) = +0.53), поэтому конфигурация с коротким окном просто встаёт против дня, который уже случился. В дни её лонга средняя доходность того же дня −1.63%, а следующего +0.50%.

Заодно выяснилось, что номинальное N завышено даже как счёт конфигураций: 6 из 264 испытаний не могут торговать в принципе. При L = 5 сэмплный z-score ограничен сверху величиной (n−1)/√n = 4/√5 = 1.789, так что все конфигурации с θ ≥ 1.8 и окном 5 не срабатывают ни при каких данных. Они честно вошли в N и подняли планку остальным.

«Сомнение» статьи верно по адресу и обратно по знаку

Аннотация предупреждает: соседние параметры сильно скоррелированы, эффективное N много меньше номинального — «и RMP будет завышен». Первая половина фразы верна, вторая противоположна арифметике. Меньшее N_eff сдвигает распределение случайного максимума влево, значит CDF в точке наблюдения становится больше; подстановка завышенного номинального N двигает эталон вправо и делает RMP заниженным. Метрика ошибается в сторону чрезмерной строгости, а не оптимизма.

Насколько сильно — измеримо. Решаем E[max_N] = E[max] фактического ротационного нуля:

номинальное N E[max] под нулём N_eff
25Δ skew, 421 день 264 1.78 21
rolling-z цены, 2700 дней 264 0.65 15

Медианная |ρ| между испытаниями 0.77, три четверти дисперсии меню сидят в одной главной компоненте. Скан из 264 конфигураций — это примерно два десятка независимых попыток, а эталон статьи строится так, будто их 264: 2.66 против фактических 1.78.

Калибровка: правило почти не стреляет

Если данные заведомо без эджа, победитель скана есть в точности максимум из N нулевых испытаний — значит его RMP обязан быть равномерным на [0,1], а доля срабатываний правила RMP ≥ 0.90 обязана равняться 10%. Три независимых генератора нуля (круговой сдвиг, стационарный блочный бутстрап, чистый шум той же волатильности), по 300 реплик каждый, полный скан из 264 конфигураций в каждой реплике:

генератор нуля RMP-A RMP-B RMP-C наш pMax<0.10
круговой сдвиг 0.0% 0.7% 8.0% 10.3%
блочный бутстрап 0.0% 0.7% 9.0% 12.0%
чистый шум 1.7% 1.7% 8.3% 12.7%
эталон 10% 10% 10% 10%

На длинной истории картина та же: RMP-A 0.7–1.0%, RMP-B 0.0–1.0%, RMP-C 8.0–12.7%. Медиана RMP-C под нулём — 0.50, ровно как положено равномерной величине; медиана RMP-B — 0.003.

Мощность: чем сильнее эдж, тем выше планка

Обратная проверка — тест обязан находить настоящий эдж. Внедряем известный истинный эдж в конфигурацию, выбранную до прогона (L=30, θ=1.5, фейд), и подбираем снос так, чтобы фактический годовой Sharpe носителя совпал с целью:

факт. ann-SR RMP-A RMP-B RMP-C наш pMax<0.10
0.58 3.5% 0.5% 16.5% 19.5%
1.08 6.0% 0.0% 27.5% 28.0%
1.57 13.0% 1.0% 48.0% 47.5%
2.07 23.0% 0.0% 66.5% 64.0%

RMP-A проигрывает по обеим осям сразу: ложных срабатываний втрое меньше нормы и мощность вчетверо ниже — это не «строгий тест», а просто глухой.

RMP-B хуже: мощность нулевая при любом размере эджа, и механизм у этого поучительный. Шкала sd_SR оценивается по разбросу Sharpe внутри самого скана, а внедрённый эдж этот разброс раздувает — планка убегает от результата быстрее, чем результат до неё дотягивается:

факт. ann-SR носителя sd_SR эталон RMP-B эталон RMP-C
−0.19 0.89 2.55 1.79
0.53 1.00 2.86 1.79
1.02 1.16 3.32 1.79
1.51 1.38 3.93 1.80
2.00 1.63 4.64 1.80

Ротационный эталон стоит на месте, как и должен стоять эталон; эталон на разбросе меню растёт вместе с тем, что он призван измерять. Это тот же канал, из-за которого мы в августе перевели bt_engine с аналитического DSR на эмпирический нуль — только здесь он виден в чистом виде.

Починенная версия — это max-T, который уже стоит

Замена «N выдуманных нормалей» на максимум по фактическому меню чинит обе беды сразу: RMP-C откалиброван (8.0–12.7% при эталоне 10%) и по мощности совпадает с нашей pMax на совпадающем уровне значимости — 16.5/27.5/48.0/66.5 против 19.5/28.0/47.5/64.0.

Совпадение не случайное, а тождественное: RMP = 1 − p_FWER, где p_FWER — p-значение статистики максимума (max-T Уэстфолла–Янга). Проверено численно на всех 264 конфигурациях: максимальное расхождение 5.6e-17. Иными словами, RMP — не новая метрика, а перцентильная запись давно известного p-значения; вся её судьба решается тем, откуда взято распределение максимума, а этот вопрос статья оставляет на усмотрение читателя.

Вывод

Идея названа правильно: перцентиль в распределении случайного максимума — законный способ измерить цену перебора, и он действительно закрывает слепое пятно DSR. Но в том виде, в каком статья предлагает его считать, правило RMP < 0.90 не защищает ни от чего: на реальной параметрической решётке оно срабатывает вхолостую в 0–2% случаев вместо 10% и находит истинный эдж силы ann-SR 2.0 лишь в четверти прогонов, а в варианте со шкалой по разбросу меню — никогда. Причина ровно та, которую авторы записали в «Сомнения», но с обратным знаком: корреляция испытаний делает метрику не завышенной, а заниженной, и наш скан из 264 конфигураций стоит примерно 21 независимой попытки.

Практический итог для нашего движка: внедрять нечего. Колонка pMax в турнире bt_engine — это и есть RMP, только с честным распределением максимума и без необходимости угадывать N; связь между ними ровно RMP = 1 − pMax. Что прогон добавил сверх отрицательного вердикта — это независимая аттестация нашего собственного нуля на чужом стенде: три генератора, две длины истории, ложные срабатывания 8–12.7% при номинальных 10%.

Проверка: research_rmp.py (brain), оси и правила зафиксированы в шапке скрипта до прогона; результаты в research_rmp_result.json. Ключевые числа перепроверены независимой реимплементацией research_rmp_verify.py, пять тестов. Главный из них — различающий: в мире, где посылка статьи выполнена буквально (264 действительно независимых испытания на i.i.d. шуме), та же машинка даёт 11.0% ложных при эталоне 10%, то есть нули на нашей решётке порождены корреляцией испытаний, а не ошибкой реализации. Независимый код воспроизвёл калибровку решётки (0.5%/0.5% против 0.0%/0.7% в основном прогоне); векторный путь сверен с наивным построчным циклом (расхождение 6.7e-16); повторный прогон побайтово детерминирован; тождество RMP-C и 1 − p_FWER — 5.6e-17. Лукахед-аудит потребовал знако-независимого критерия: наивное «заглядывание вперёд обязано улучшать результат» неверно для контрарной стратегии — победитель встаёт против движения дня, и заглядывание ломает его с +1.70 до −4.34. Правильная проверка — куда уходит результат при снятии лишней задержки: (база−лаг) = +0.60 и (шок−лаг) = −5.44 имеют разные знаки, то есть база уходит от шока, а не тянется к нему. Оговорка о циркулярности: RMP-C построен на ротации, поэтому под ротационным генератором нуля он калиброван по построению — его честные цифры дают бутстрап и чистый шум, они и приведены наравне. Оригинал Rulyfi на момент прогона недоступен (redirect Quantocracy пуст), проверялся механизм в формулировке этой аннотации.

Источник: Rulyfi · оригинал на английском, разбор — авторский на русском.