Открываем длинную позицию по 84,120. Стоп на 84,036, тейк-профит на 84,271. Появляется свеча следующей минуты: открытие 84,118, максимум 84,290, минимум 84,010, закрытие 84,240.
Оба уровня внутри этой свечи. Цена коснулась и стопа, и тейк-профита, а четыре значения OHLC ровным счётом ничего не говорят о том, что произошло раньше. Бэктест всё равно выдал результат. Где-то в цикле нашлась строка, которая решила вопрос — скорее всего, та самая строка, которую при написании кода вы не считали допущением модели.
Это крупнейший источник фиктивной доходности в краткосрочных стратегиях из всех, что я встречал: он важнее комиссий и проскальзывания, потому что не выглядит как допущение. Он выглядит как техническая деталь.
Ошибка №1: позволить цепочке if принять решение
Обычно код выглядит так:
if bar.high >= target:
exit(target, "tp")
elif bar.low <= stop:
exit(stop, "sl")
Никто не решал: «сначала срабатывает тейк-профит». Проверку тейк-профита просто написали первой — это основной сценарий, о котором вы думали. Поменяйте ветки местами — и кривая капитала изменится. Уже одно это должно показать, что P&L стратегии отчасти зависит от вашего редактора кода.
Я взял намеренно заурядного скальпера на возврате к среднему для бессрочного контракта BTCUSDT: 3 месяца 1-минутных свечей, 4,812 сделок, стоп 0.10% и тейк-профит 0.18% от цены входа. Сигналы те же, комиссии те же — изменилось только правило разрешения неоднозначности.
Восьмая часть сделок определяет весь результат. Такова арифметика узкой пары стопа и тейк-профита: неоднозначными оказываются сделки, где цена сходила в обе стороны, а это как раз большинство самых интересных случаев. В зависимости от подброшенной монеты каждая такая сделка даёт разницу в полном расстоянии от стопа до тейк-профита. 12.6% сделок × 0.28% диапазона — это 3.5% валового оборота на единицу выборки, что намного больше реального преимущества стратегии.
Эта доля зависит от размера свечи относительно расстояния между уровнями и быстро растёт, когда свечи становятся длиннее. Те же стоп и тейк-профит, те же сигналы — меняется только таймфрейм:
| Интервал свечи | Сделки, в которых оба уровня внутри одной свечи | Sharpe (сначала тейк-профит) |
|---|---|---|
| 1s | 0.3% | 0.91 |
| 1m | 12.6% | 2.31 |
| 5m | 34% | 3.60 |
| 15m | 49% | 4.42 |
| 1h | 71% | 5.88 |
Посмотрите, о чём на самом деле говорит эта таблица. На более крупных свечах бэктест показал лучшие результаты. Исследователи обычно считают часовые свечи более консервативным вариантом: меньше шума, меньше подгонки под микроструктуру. Но если правило обработки внутри свечи работает в вашу пользу, крупная свеча — просто большая коробка, внутри которой можно предположить, что вам повезло. На свечах 1h чисто условными оказываются 7 сделок из 10. Такой бэктест проверяет не стратегию, а порядок двух операторов `if`, повторённых 3,400 раз.
Ошибка №2: считать, что стоп исполнился по цене стопа
Допустим, порядок проверки вы исправили. Сначала срабатывает стоп, вы фиксируете убыток ровно 0.10% плюс taker-комиссия и считаете, что всё учли. Но здесь по-прежнему неверны две вещи.
Во-первых, стоп — это триггер, а не исполнение. На Binance USDⓈ-M ордер STOP_MARKET при выполнении условия срабатывания превращается в рыночный ордер, который забирает доступные заявки из стакана. В спокойную минуту проскальзывание составляет тик или два. А в ту минуту, когда стоп действительно сработал — со свечой, у которой тело в 40 пунктов и под ней каскад ликвидаций, — стакан тонок именно с той стороны, на которую вы выходите. В моей выборке сопоставление срабатываний стопов с лентой тиков показало: медианное исполнение оказалось на 1.4 б.п. хуже триггерной цены, а 95-й процентиль — на 11 б.п. При стопе в 10 б.п. хвост распределения добавляет ещё десятую часть риска, который, как вам казалось, был задан заранее.
Во-вторых, есть более тонкий нюанс, характерный для бессрочных контрактов: какая цена запускает стоп. По умолчанию Binance использует для стоп-ордеров mark price. Она рассчитывается по индексу с учётом сглаженного базиса, а не по последней сделке на этой площадке. Ваш ряд OHLC построен по последним ценам. Это разные ряды, и сильнее всего они расходятся как раз во время событий, которые запускают стопы.
| Последняя цена (ваши klines) | Mark price (триггер по умолчанию) | |
|---|---|---|
| Источник | сделки на этой площадке | индекс нескольких площадок + базис |
| Поведение теней | полный размах | сильно сглажены |
| Типичное расхождение | 1–3 б.п. в спокойное время, 20–35 б.п. в минуту каскада ликвидаций | |
| Последствие для бэктеста | стопы сработали, хотя не должны были, и наоборот | |
Поэтому тень в 25 б.п. на графике последних цен выбьет вас по бэктесту, хотя mark price в реальности не приблизилась к триггеру и на 10 б.п. Или наоборот — в день, когда сдвинулся индекс, а ваша площадка отставала. Если задать workingType как CONTRACT_PRICE, поведение в реальной торговле будет соответствовать вашим данным. Для исследователя это обычно правильный выбор: чтобы честно моделировать триггер по mark price, придётся провести второй ряд через весь движок исполнения.
Лучше всего мне запомнился такой случай: кто-то из нашей команды «улучшил» стратегию, передвинув тейк-профит с 0.18% на 0.21%. Sharpe вырос с 2.3 до 3.1. Никакого нового преимущества не появилось. Тейк-профит просто оказался за пределами основной массы распределения 1-минутных теней, поэтому меньше сделок попадало в неоднозначную категорию, где код незаметно присваивал им выигрыш. Оптимизировали правило разрешения неоднозначности.
Ошибка №3: всегда выбирать худший вариант и считать это консервативным подходом
Первое, что приходит в голову, — пессимистичный сценарий. Если достигнуты оба уровня, считаем, что сработал стоп. Готово, оптимизм исключён, можно запускать.
Раньше я тоже так делал. Это лучше альтернативы, но всё равно неправильно — по двум причинам.
Так можно отбросить вполне рабочие стратегии. Для этой стратегии пессимистичная обработка 12.6% сделок отняла 2.1 пункта Sharpe относительно результата 0.94 с разрешением по тикам. Если реальное значение — 0.94, а ваше правило показывает 0.18, вы откажетесь от идеи и возьмётесь за что-нибудь похуже. Подход, который ошибается на 2 пункта Sharpe, — это не консерватизм, а шум с моральным оправданием.
Хуже того, такой подход искажает оптимизацию. Если передать перебору параметров пессимистичное правило разрешения неоднозначности, оптимизатор научится избегать таких ситуаций, потому что они теперь означают чистый штраф. Он будет выбирать широкие стопы и близкие тейк-профиты или более длинные свечи, на которых оба уровня редко оказываются внутри одной свечи. В итоге параметры подбираются под ваше правило исполнения, а не под рынок. Это та же ошибка, что и при благоприятном правиле, только с противоположным знаком, и в отчёте её так же не видно.
Правило, с которого мы начинаем: если stop_distance + target_distance меньше 75-го процентиля диапазона свечей вашего таймфрейма, допущение о движении внутри свечи влияет на P&L сильнее, чем сигнал. Посчитайте оба значения. Это занимает 4 строки кода и завершало больше обсуждений стратегий, чем любая другая отдельная проверка.
Что работает на практике
Движение цены внутри свечи — это данные. Получите их или оцените границы ошибки, если данных нет.
- Разрешайте неоднозначность по самому детальному из доступных рядов. Данные Binance aggTrades за нужные минуты состоят из нескольких сотен строк и дают однозначный ответ: какой уровень достигнут первым и по какой цене прошло исполнение при движении через уровень. Не нужно загружать тиковые данные для всего бэктеста — только для неоднозначных свечей. В моей выборке таких минут было 606 из 129,600. Это небольшая загрузка данных, а не инфраструктурный проект.
- Если тиковых данных нет, для разрешения неоднозначности спуститесь на 1 или 2 таймфрейма ниже. Сигналы формируются на 15m, выходы определяются по свечам 1s или 1m. Доля неоднозначных случаев падает с 49% до долей процента, и остаток уже достаточно мал, чтобы им можно было пренебречь без самообмана.
- Всегда показывайте диапазон. Запускайте каждый бэктест дважды — с оптимистичным и пессимистичным разрешением — и выводите оба значения Sharpe рядом с результатом по фактическим данным. Разница между ними показывает неопределённость из-за движения внутри свечи. Её нужно указывать в отчёте рядом с доверительным интервалом для самого Sharpe. Если диапазон составляет 0.2–2.3, ни один вывод внутри него нельзя считать достоверным.
- Отслеживайте долю неоднозначных случаев как отдельную ключевую метрику. У нас она отображается вверху карточки каждой стратегии, рядом с числом сделок и оборотом. Если доля превышает примерно 5%, проверяется логика выхода, а не логика входа.
- Моделируйте триггер отдельно от исполнения. Используйте для триггера тот ряд цен, который действительно использует площадка; цену исполнения рассчитывайте как триггерную цену плюс проскальзывание, смоделированное по данным ленты, а не приравнивайте к триггерной цене.
С акциями та же проблема, только выглядит она иначе. Если акция открывается после ночного гэпа на 58.40, хотя стоп установлен на 62.00, исполнение произойдёт не по 62.00, а ниже цены открытия. А бэктест по дневным свечам, который при гэпах через стоп учитывает проскальзывание −$0.00, с готовностью покажет, будто защитный стоп улучшил просадку. Это не так. Просто проверка не затронула дни, когда это действительно важно. То же происходит при приостановках торгов: стоп фактически исполнится на аукционе возобновления торгов, по цене, которой может не быть даже в минимуме свечи.
Ничего экзотического здесь нет. Нужно лишь понимать, что свеча — это сводка, а стратегия со стопом и тейк-профитом зависит от порядка событий, который в этой сводке потерян. Когда движок бумажной торговли наконец запускает стратегию на живом потоке данных, у потока есть однозначный ответ насчёт этого порядка. Ему никогда не было важно, какую ветку оператора if вы написали первой.
← Все статьи


