Моя модель читала завтрашнюю газету

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

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

Подозрительно хороший ученик

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

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

Где просочилось будущее

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

Именно этот единственный выбор и есть то место, куда просочилось будущее. Когда вы вычисляете статистику по всей истории, а затем применяете её к точке в прошлом, каждое «нормализованное» значение в тот прошлый момент тихо кодирует информацию о данных, которые ещё не произошли. Модели, смотрящей на любой заданный момент, неявно сообщалось нечто о форме всей временной линии — включая её будущее. Она никогда не видела завтрашних цен напрямую. Ей это было и не нужно. Она дышала воздухом, в который было подмешано завтра.

Почему look-ahead так легко построить

Опасность look-ahead bias не в том, что его трудно создать. А в том, что он проскальзывает через в остальном обыденную предобработку. Самый естественный, читаемый способ написать код — обучить ваш масштабировщик на данных, а затем преобразовать данные — уже неверен для временных рядов, и ничто не возражает. Исключение не выбрасывается. Тест не загорается красным. Цифры выглядят совершенно разумными. Единственный симптом — что ваша модель лучше, чем должна быть, а это единственный симптом, который вы менее всего мотивированы расследовать.

В этом и есть ловушка утечки: она не выглядит как баг. Она выглядит как успех.

Признак: блестит здесь, мёртв там

Есть один показательный признак, хотя сам по себе он не доказательство. Модель, испорченная look-ahead, как правило, блестяща на данных, под которые её настраивали, и разваливается в тот же миг, когда встречает данные, в которые не может подсмотреть. Большой разрыв между внутривыборочным триумфом и вневыборочным крахом — предупреждающий знак утечки, хотя он может происходить и из переобучения, смены режима или недооценённых издержек. В моём случае виновницей была утечка.

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

Исправление: никогда не используй то, чего ты ещё не мог знать

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

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

Утечка — это семейство, а не единственный баг

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

Я перестал думать о look-ahead как о баге, который надо исправить, и начал относиться к нему как к свойству, которое надо проверять, — к чему-то, что я теперь намеренно выискиваю, исходя из предположения, что если результат выглядит слишком чисто, то будущее куда-то просачивается там, где я ещё не проверял.

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

Под всем этим лежит неудобная правда о бэктестах: они не могут отличить умение от послезнания. Бэктест выплачивает ровно одно и то же модели, которая подлинно предвосхищает рынок, и модели, которая тихо читает ключ к ответам. Обе производят красивую кривую. Строго прямые, невиденные данные — более сильный тест, который начинает их разделять, — хотя даже он сам по себе не может доказать, что конвейер свободен от утечек.

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

Модель, читающая завтрашнюю газету, всегда будет выглядеть как гений. Гений — ваш, а не её, и живёт он целиком в утечке, которую вы забыли закрыть.

— Никаких сигналов, никакой доходности, не инвестиционный совет.