Через 6 недель после первого запуска бэктеста мы повторили его с теми же кодом, конфигурацией и диапазоном дат. Итоговый капитал оказался другим. Разница была не огромной, но достаточной, чтобы коэффициент Sharpe изменился с 1.31 на 1.08, а лучшим стал выглядеть другой набор параметров.
Стратегия не менялась. Изменились исторические данные. Поставщик исправил данные по сделкам и барам за несколько дней, а в первом запуске мы не сохранили точный снимок входных данных. Следующие несколько дней мы выясняли, был ли новый результат следствием исправления данных, ошибки в конвейере или исследовательского процесса, который невозможно воспроизвести.
Понедельник: повторный запуск дал другой результат
Первой зацепкой стал небольшой разрыв в ликвидной спотовой паре. В исходном баре максимум составлял 62,418. При повторном запуске он оказался равен 62,431. Разница казалась безобидной, пока мы не проследили за входом стратегии: порог находился между этими ценами. Из-за нескольких изменившихся баров сигналы стали другими, а через несколько дней это привело к другим позициям.
Мы сравнили дневное количество сделок и обнаружили, что расхождение начинается в один конкретный день. Затем сопоставили исходные строки, использованные для этого дня. В текущем ответе поставщика нашлось несколько исправленных сделок и пересчитанный агрегированный бар. К ответу, полученному при первом запросе, журнал изменений не прилагался.
Для данных это обычное дело. Биржи исправляют записи, а поставщики заполняют пробелы, корректируют ошибочные сделки и меняют логику агрегации. Запрос «данные за январь» в марте может вернуть другой ответ.
Вторник: мы нашли пропавшую квитанцию
В записи о запуске мы сохранили название поставщика, символ и диапазон дат. Но сам ответ и хеш содержимого для каждого раздела входных данных не сохранили. Мы могли доказать, что именно запрашивали, но не могли подтвердить, какие байты вернул поставщик.
Мы нашли самые старые кэшированные файлы и сравнили их с текущим ответом. Расхождение не было связано с поправкой на сплит или преобразованием часового пояса. Причиной стали исправленные записи о нескольких сделках, прошедшие через построитель баров. Это исключило одну из версий, но кэш оказался неполным, поэтому мы всё ещё не могли точно восстановить каждый затронутый бар.
| Что мы записали | На какой вопрос это помогло ответить | Чего не хватало |
|---|---|---|
| Поставщик, символ, диапазон дат | Какой запрос сделал запуск | Содержимое ответа |
| Версия конвейера | Какой код построил бары | Точные входные строки |
| Время запуска | Когда выполнялась задача | Какая версия данных поставщика была доступна на тот момент |
Мы считали, что «тот же запрос» означает «те же данные». Это допущение создавало впечатление, что эксперимент воспроизводим лучше, чем на самом деле.
Среда: исправление изменило картину
Мы повторно обработали затронутые даты с обеими версиями доступных данных. Исправленные бары убрали 2 входа и сдвинули один выход на 1 бар. На эти 3 сделки пришлась большая часть изменения Sharpe. Стратегия оказалась чувствительной к нескольким наблюдениям около порога срабатывания, хотя на графике весь ценовой ряд выглядел почти так же.
Это не означало, что первоначальный результат был недостоверным или что исправленный результат автоматически лучше. Это означало, что утверждение нужно было уточнить: прежний запуск описывал снимок данных, доступный нам тогда, а повторный — текущую историю поставщика. Мы не могли честно назвать один результат воспроизведением другого.
Четверг: мы изменили записи об исследовании
Теперь для новых исследовательских запусков мы сохраняем неизменяемые снимки входных данных. Для каждого раздела рассчитывается хеш содержимого, а в манифесте запуска фиксируются источник, время получения, версия схемы, политика корректировок и версия кода преобразования. Большие наборы данных мы храним в системе адресации по содержимому: одинаковые разделы можно использовать совместно, не перезаписывая предыдущие версии.
Если правила хранения или лицензия не позволяют сохранять исходные данные, мы сохраняем максимально полную квитанцию, разрешённую условиями: хеши, параметры запроса, идентификаторы версии источника и небольшую выборку разобранных строк для диагностики. По хешу нельзя восстановить данные, но можно понять, что два запуска использовали разные байты.
Для воспроизводимого бэктеста нужна идентификация входных данных, а не просто URL. Записывайте, что именно поступило, когда оно поступило и как превратилось в строки, которые использовала стратегия.
Пятница: от чего мы отказались бы в следующий раз
Мы бы не стали сравнивать только итоговые метрики. Изменившийся Sharpe показывает, что-то сдвинулось, но не указывает причину. Сначала проверьте количество строк и хеши разделов, затем найдите первую дату с различиями во входных данных. После этого сравните преобразованные строки, сигналы, заявки и исполнения именно в таком порядке.
Мы также не стали бы молча заменять старые данные исправленной историей. Исправления полезны, но прежние запуски должны оставаться привязанными к исходным входным данным. Новый набор данных должен запускать новый эксперимент с явной связью с прежним.
Теперь у нас простое практическое правило: если повторный запуск должен воспроизвести эксперимент, зафиксируйте снимок данных. Если он должен учитывать исправленную историю, обозначьте его как новый эксперимент. Нам понадобилось несколько дней, чтобы усвоить эту разницу, а в манифесте запуска для неё нужна всего одна строка.
← Все статьи


