Перейти к основному содержимому
инжинирингбэктестингалгоритмы
Почему бэктесты лгут: симулировать выходы на основе индикаторов сложнее, чем кажется

Почему бэктесты лгут: симулировать выходы на основе индикаторов сложнее, чем кажется

Бэктест выдаёт число, а числа убедительны. В этом и вся проблема. Когда симуляция ошибается едва заметно, она не падает и ни о чём не предупреждает. Она вручает вам результат чуть лучше реального и позволяет действовать на его основе.

Симулировать фиксированный тейк-профит легко: цена либо дошла до вашего уровня, либо нет. А вот симуляция входа или выхода на основе индикатора уже сложна, потому что теперь симуляции нужно ответить на куда более коварный вопрос: что бот на самом деле знал в тот момент?

Вот три способа, которыми мы отвечали на этот вопрос неправильно, и чему научил каждый из них.

Главное

  • Классическая ошибка в том, чтобы прочитать индикатор старшего таймфрейма до того, как закрылся его бар. 4-часовой RSI, к которому обращаются на открытии бара, несёт в себе четыре часа ценового движения, которое ещё не произошло.
  • Исправление здесь арифметическое, а не эвристическое: значение с таймфрейма T можно законно использовать только после того, как T истёк, поэтому смещайте обращение назад на (таймфрейм триггера минус таймфрейм симуляции).
  • У живых движков та же проблема в другом обличье. Индикатор, рассчитанный на ещё формирующейся свече, перерисовывается, а значит, условие может быть истинным, а затем ложным на том же баре.
  • Симулятор, который молча пропускает то, что не может воспроизвести, оказывается более снисходительным, чем живой бот, и разрыв всегда играет в пользу бэктеста.
  • Честный ход в том, чтобы показать этот разрыв в результате, а не прятать его. Бэктест, который сообщает, где он лжёт, стоит дороже того, что выглядит безупречно.

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

Эту ошибку проще всего написать и труднее всего заметить.

Допустим, симуляция шагает вперёд по одной 5-минутной свече за раз, а в стратегии есть условие на 4-часовом RSI. На каждом 5-минутном шаге движку нужно значение этого RSI. Наивная реализация берёт 4-часовой бар, содержащий текущий момент, и читает его значение.

Это машина времени.

У 4-часового бара, который открылся в 12:00, нет финального значения RSI, пока он не закроется в 16:00. Если симуляция стоит на отметке 12:05 и читает этот бар, она читает число, рассчитанное из четырёх часов ценового движения, которое с точки зрения 12:05 ещё не произошло. Стратегия словно входит прямо перед движениями, которые она не могла предвидеть, и кривая капитала выглядит превосходно.

Исправление не является эвристикой. Оно вытекает из того, что мы записываем, когда значение действительно становится известным. Бар, который открывается в момент t на таймфрейме triggerMs, становится известен только в t + triggerMs. Симуляция принимает решение на закрытии текущей свечи, в candleOpenTime + simulationMs. Значит, значение законно использовать ровно тогда, когда:

t + triggerMs <= candleOpenTime + simulationMs
t             <= candleOpenTime - (triggerMs - simulationMs)

Это означает, что правильное обращение остаётся тем же, что у вас уже было, только выполненным против метки времени, смещённой назад на triggerMs - simulationMs.

Два свойства этого выражения стоит отметить, потому что именно они делают его безопасным для применения везде:

  • Условия на том же таймфрейме получают сдвиг ноль. 5-минутный индикатор, прочитанный на закрытии 5-минутной свечи, и есть его собственное значение на закрытии, которое всегда было корректным. Двигаются только по-настоящему старшие таймфреймы.
  • Сдвиг выводится, а не настраивается. Он берётся из самой свечи, а не из параметра, который кто-то должен не забыть задать.

Общий урок я применил бы к любой симуляции, а не только к торговле: когда вы не уверены, есть ли у вас заглядывание в будущее, запишите метку времени, в которую каждый вход становится известным, и сравните её с меткой времени, в которую вы его используете. Смещением называется как раз то, что остаётся, когда вы пропускаете этот шаг и полагаетесь на интуицию, а интуиция тут плоха, потому что «текущий 4-часовой бар» звучит совершенно разумно.

2. Та же ошибка в живом коде носит другой костюм

Можно подумать, что эта проблема относится к бэктестингу. Это не так. Живой движок сталкивается с ней тоже, и там она называется перерисовкой (repainting).

В любой момент самая свежая свеча ещё формируется. Индикатор, рассчитанный по окну, заканчивающемуся на этой свече, будет меняться по мере изменения свечи. Условие может быть истинным в 12:01, ложным в 12:03 и снова истинным в 12:04, всё в пределах одного бара, при этом рынок не делает ничего необычного. Если бот действует по первому из этих значений, он действует по значению, которого больше не существует.

По умолчанию мы отбрасываем ещё формирующуюся свечу и считаем только по подтверждённым барам, поэтому сигнал никогда не перерисовывается. Именно это и заставляет живую торговлю и бэктест сходиться, поскольку симуляция по построению работает с закрытыми барами.

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

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

3. То, что симулятор не может воспроизвести, опаснее того, что он считает неверно

Первые две проблемы относятся к ошибкам корректности. А эта уже проблема дизайна, и, на мой взгляд, именно её недооценивают сильнее всего.

Некоторые условия невозможно воспроизвести. Ярче всего это видно на внешнем сигнале через вебхук: не существует исторической записи того, что внешняя система отправила бы в 14:20 какого-то дня прошлого марта. Симуляция просто не может этого знать.

Так что же ей делать? Заманчивый ответ состоит в том, чтобы пропустить это условие и оценить остальные. Именно это делает результат целостным на вид. И именно здесь возникает вред, а механизм стоит расписать:

Если стратегия требует A AND B, а B невозможно симулировать, то пропуск B оставляет A решать в одиночку. Симулированный бот входит на условиях, где живой бот остался бы в стороне. Симуляция систематически более снисходительна, чем реальность.

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

Мы выбрали продолжать симуляцию, а не отказываться от неё, потому что частичный результат с известным разрывом всё же полезен. Но тогда разрыв должен быть виден в выводе, а не спрятан в логе. Результат несёт явное предупреждение на языке самого пользователя, с названием, которое сообщает, что произошло: «Часть Условий Входа Не Смоделирована». Есть парное предупреждение для выходов.

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

Что из этого вынести

Если вы строите что-либо, что воспроизводит историю ради получения числа:

  • Каждому входу нужна метка времени «когда становится известным». Если вы не можете её назвать, вы не можете исключить заглядывание в будущее.
  • Проверяйте направление своих ошибок, а не только их размер. Ошибка, которая всегда указывает в одну сторону, накапливается; та, что указывает случайно, усредняется. Однонаправленный оптимизм служит подписью заглядывания в будущее.
  • Заставьте симулятор и живую систему разделять логику принятия решений везде, где можете. Две реализации «истинно ли это условие» разойдутся, и вы узнаете об этом от пользователя, а не из теста.
  • Сообщайте о том, что вы не смогли сделать. Достоверность результата исходит из его заявленных ограничений, а не из его безупречности.

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


Читайте также

Создайте аккаунт

Примените знания на практике: откройте аккаунт Freya уже сегодня.

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