Вот одна строка из дневного файла по акциям. NVDA, пятница, 7 июня 2024 года — последняя сессия перед вступлением в силу сплита 10 к 1.
| Колонка | Значение |
|---|---|
| дата | 2024-06-07 |
| закрытие | 1208.88 |
| объём | 42,800,000 (округлённо) |
| коэффициент сплита | 1.0 |
| дивиденд | 0.00 |
| закрытие с поправкой | 120.8781 |
Я намеренно не показал здесь колонки OHLC. К ним мы вернёмся ближе к концу: именно они незаметно всё ломают.
Всё интересное в этой строке — в разнице между второй и шестой колонками. 7 июня на ленте было 1208.88. По такой цене больше никто не совершит сделку, потому что акции, к которым она относилась, больше не существуют в прежнем количестве. Значение 120.8781 — это реконструкция, и оно не сводится к простому делению 1208.88 на 10. Разберём всё по колонкам.
дата: 2024-06-07 и ещё 2 даты, которые важнее
О сплите объявили 22 мая 2024 года вместе с финансовыми результатами. Дата
фиксации реестра — 6 июня. Торги с учётом сплита начались в понедельник, 10
июня. Три даты, а колонка split_factor у вашего поставщика данных
привязана ровно к одной из них — обычно к первой сессии после сплита. Ошибка
на 1 день — самый распространённый баг в обработке корпоративных действий,
который я встречаю, и в сводных показателях его не заметить: кривая капитала
получает разрыв вниз на 90% или вверх на 900% за один день. Sharpe,
рассчитанный за 2,000 дней, почти этого не покажет, а максимальная просадка
или правило стоп-лосса — покажет обязательно.
С датой дивиденда тоже есть проблема, и в 2024 году правила изменились для всех. Дата фиксации реестра для квартального дивиденда NVDA в размере $0.01 после сплита пришлась на 11 июня. 28 мая 2024 года в США перешли на расчёты T+1, поэтому теперь дата без дивиденда совпадает с датой фиксации реестра. Любой загрузчик, который вычисляет дату без дивиденда как «дата фиксации реестра минус 1 рабочий день» — так было правильно десятилетиями, — с июня 2024 года начал незаметно проставлять поправку на дивиденд на одну сессию раньше. Никаких ошибок не возникает. Просто коэффициент попадает не в ту строку.
Сбой в коде обработки корпоративных действий — это никогда не исключение. Это значение, сдвинутое на день и выглядящее совершенно правдоподобно.
закрытие: 1208.88 — цена, которой нет в файле
Это исходная цена сделки, и в большинстве исследовательских пайплайнов она
существует только в виде колонки, на которую никто не смотрит. Сигналы
рассчитываются по adj_close. А любое правило, в котором
используется цена акции в долларах, теперь опирается на значение, которого не
видел ни один участник рынка.
Взлёт NVDA к $1,000 весной 2024 года был реальным событием с реальными потоками ордеров: круглые числа притягивали рынок, опционы концентрировались на страйках 950 и 1000, а каждый новый подход к отметке нёс риск новостей. В скорректированных данных весь этот эпизод происходит около $100. Если вы тестируете реакцию на психологические уровни, логику закрытия гэпа возле круглых отметок или признак близости к страйку, то проверяете его на ценовом ряду, масштаб которого изменило событие, ещё не произошедшее в тот момент.
То же касается фильтров инвестиционной вселенной. «Исключать акции дешевле $5» — вполне обоснованный способ отсеять бумаги с типичными проблемами микроструктуры: широкими относительными спредами, ограничениями по маржинальной торговле и особенностями режима шага цены. Примените этот фильтр к скорректированной истории — и исключите компании за периоды, когда они торговались по $30, а обратный сплит провели позже. Условие «цена выше $500» незаметно исключит NVDA из каждого года до июня 2024-го. Ценовые фильтры нужно применять к исходным ценам сделок. Точка.
коэффициент сплита: 1.0 — и 0.1 в следующей строке
Коэффициент — это накопленное произведение, которое рассчитывается в обратном направлении от сегодняшнего дня. В каждой строке до 10 июня 2024 года учитывается коэффициент 0.1 от этого сплита, умноженный на 0.25 от сплита 4 к 1 в 2021 году и на все предыдущие коэффициенты. История NVDA до 2021 года масштабирована на 0.025, поэтому свеча, которая в 2019 году закрылась бы по $200, теперь отображается как $5.00.
Стоит помнить о двух последствиях. Во-первых, скорректированная история меняется при каждом новом корпоративном действии. Бэктест, который вы запустили в мае 2024 года, и тот, который запускаете сегодня, выдадут разные цифры на одном и том же коде и за те же даты. Если вы кэшировали признаки по тикеру и дате без отметки версии данных, теперь в кэше смешаны 2 масштаба. Именно поэтому мы включаем эпоху корректировок в ключ признака; на то, чтобы это понять, у нас ушёл один очень запутанный вечер.
Во-вторых, при масштабе 0.025 акция, торговавшаяся с шагом $0.01, теперь меняется с шагом $0.00025. Логика округления до шага цены, проверки «это новый максимум?» и сравнения значений с плавающей точкой — всё это теперь работает с точностью намного выше той, которую реально обеспечивал рынок.
дивиденд: 0.01 — величина, из-за которой цены теряют шаг в цент
Один цент. Кажется, мелочь, но именно поэтому значение
adj_close равно 120.8781, а не аккуратным 120.888.
10 июня цена закрытия составила 121.79. Коэффициент поправки на дивиденд равен (121.79 − 0.01) / 121.79 = 0.99991789. Он умножает все цены до 11 июня, поэтому скорректированная с учётом сплита цена 120.888 становится 120.878074. Разница небольшая, но постоянная; этот коэффициент применяется к каждой из более чем 60 дат без дивиденда в истории NVDA. Важен накопительный эффект: к 2015 году вы смотрите на цену, умноженную на десятки не вполне рациональных коэффициентов. Ни одна скорректированная историческая цена не попадает точно на шаг в цент.
А значит, каждое ==сравнение с круглым уровнем даёт ложный
результат, любую проверку «касались ли мы лимитной цены» нужно выполнять с
допуском, а восстановленные OHLC после наивного округления до 4 знаков уже не
соответствуют условию low ≤ open ≤ high. Кроме того,
adj_close представляет собой ряд полной доходности, а
close — ряд ценовой доходности. Если считать доходность по
одному, а просадки по другому, вы смешаете 2 разных определения произошедшего.
объём: 42,800,000 — делитель, который применяют непоследовательно
Чтобы сопоставить объём акций с сессиями после сплита, его нужно умножить на 10. Одни поставщики данных так и делают, другие — нет. Некоторые корректируют OHLC, оставляя исходный объём. Это худший из трёх вариантов: файл кажется внутренне согласованным, пока вы не перемножите цену на объём.
Надёжнее вообще перестать использовать объём акций как признак и перейти на денежный объём: по своей природе он не зависит от сплитов. Для фильтра ликвидности по среднему денежному объёму за 20 дней не нужна никакая логика корректировок, и корпоративные действия его не ломают. Устойчивость признаков лучше корректного кода корректировок: за корректностью такого кода нужно постоянно следить.
3 вида файлов поставщиков данных — 3 варианта поломки.
| Что передают | Что ломается |
|---|---|
| Только цена закрытия с поправкой, исходные OHLC | Гэпы и внутридневная логика рассчитываются для двух разных ценовых масштабов |
| OHLC с поправкой, исходный объём | Денежный объём искажается в соответствии с коэффициентом сплита; фильтры по ADV срабатывают неправильно |
| Исходные OHLCV и колонки коэффициентов | Ничего, если применить коэффициенты правильно. Всё, если применить их к неправильной строке |
Чего в файле нет: adj_open, adj_high, adj_low
Теперь вернём OHLC. Если поток данных корректирует только цену закрытия, а
стратегия рассчитывает ночной гэп как open / prev_adj_close − 1,
то 10 июня 2024 года она вычислит 120.37 / 120.8781 − 1 — вполне нормальное
значение, около −0.4%. Запустите тот же код для 7 июня 2024 года — и он
возьмёт цену открытия из исходного файла (примерно 1207), а цену закрытия
скорректирует на 0.025 с учётом более ранних сплитов. Получится гэп в
несколько тысяч процентов.
Любая смесь исходной и скорректированной колонок — это вычисления в двух разных системах координат. Теперь я проверяю такие смешанные обращения к данным на уровне загрузчика, а не оставляю исследователю задачу помнить о них: исследователи не будут помнить, в том числе и я.
Чего не покажет ни одна колонка: сколько акций вы действительно купили бы
Именно здесь модель издержек в бэктесте теряет деньги — и именно поэтому я вообще пишу о сплите акций.
В США комиссию за торговлю акциями часто взимают за каждую акцию. Допустим, это $0.005 при минимальной комиссии $1.00 за ордер — типичный фиксированный тариф розничного брокера. Теперь протестируем позицию в NVDA на $400,000 за год до сплита, когда исходная цена была около $400, а скорректированная отображается как $40.
| Количество акций | Комиссия | |
|---|---|---|
| Как было на самом деле (исходная цена $400) | 1,000 | $5.00 |
| Что видит бэктест (скорректированная цена $40) | 10,000 | $50.00 |
Экспозиция та же, сделка та же, а смоделированные издержки выше в 10 раз. Это $45 выдуманных расходов на одну сторону сделки, $90 за полный цикл, а стратегия с ежедневным оборотом набирает около $22,000 в год расходов, которых никогда не было. Для портфеля на $400k это 5.6 процентного пункта годовой доходности, целиком созданного делителем. В данном случае стратегия выглядит хуже, чем была на самом деле, и с этим можно жить.
Обратные сплиты дают противоположный эффект, и это опасное направление. При обратном сплите 1 к 10 скорректированная история показывает цены в 10 раз выше, а число акций — в 10 раз меньше фактического. Модель издержек незаметно занижает комиссию за акцию в 10 раз. Обратные сплиты часто встречаются среди компаний малой капитализации с ценой ниже $5 и широкими спредами, где комиссии за акцию составляют крупнейшую статью расходов. Бэктест приукрашивает именно те бумаги, которые должен наказывать.
Решение простое и системное: рассчитывайте размер позиции в долларах, переводите его в число акций по исходной цене на эту дату и взимайте комиссию за каждую акцию с этого количества. Сохраните исходную колонку цены в загрузчике навсегда — не как артефакт для аудита, а как действующий входной параметр движка исполнения.
- Сигналы и доходность: скорректированный ряд.
- Число акций, комиссии за акцию, округление до шага цены и ценовые фильтры: исходный ряд.
- Ликвидность и ёмкость: денежный объём, которому не нужны ни те, ни другие.
6 колонок, 1 строка и вполне обычное корпоративное действие, о котором объявили за 3 недели и которое никого не удивило. Рынок учёл его за утро. Точно восстановить это событие в исследовательском пайплайне гораздо сложнее, а признак того, что пайплайн с задачей не справился, почти никогда не выглядит как сообщение об ошибке.
← Все статьи


