Методика
Backtest квартильной momentum-стратегии на российских акциях. Основано на методике t.me/kpd_investments.
Универс
Survivorship-free, пересчитывается каждый месяц.
Тикер попадает в универс месяца t, если выполнены все условия:
- ≥ 13 непрерывных месячных закрытий, оканчивающихся месяцем t (нужно для формулы — 11 точек на r(12-1) с skip-month плюс текущая для σ(12)).
- Из прошедших по истории берутся 100 самых ликвидных по медиане месячного оборота за окно [t-11..t] (
UNIVERSE_TOP_N_LIQUID). Это относительный отбор: число имён стабильно по годам, а эффективный порог ликвидности сам подстраивается под масштаб рынка (медиана оборота 100-й бумаги — ~9 млн ₽/мес в 2013, ~500 млн в 2026). - Только обыкновенные / привилегированные акции и депозитарные расписки, которые торговались на TQBR в рублях. История цен собирается юнионом по всем торговым бордам MOEX, на которых бумага листингована: главный пост-2014 режим TQBR + legacy MICEX-борды EQBR/EQNE/EQNL/EQBS/… до перехода на T+2 (2013). На пересечениях побеждает primary-board (TQBR), затем обычные борды, и последним — борд неполных лотов SMAL: цена нескольких акций в тонком стакане не годится как закрытие бумаги, рядом с которой торгуются миллиарды. SMAL остаётся источником только для дней, когда больше не торговал никто. Облигации, ОФЗ, ETF — не участвуют.
Делистинг обрабатывается естественно: данные тикера заканчиваются → он автоматически выпадает из универса в момент исчезновения котировок. Никакого ретроспективного составления списка «успешных» бумаг.
Сигнал
Дефолтные страницы сайта и квартильные списки построены по формуле curve_fit:
score = (0.9 · r(12-1) + 0.1 · r(6-1)) / σ(12)
r(12-1)— среднегеометрическая месячная доходность за окно [t-11..t-1] (11 точек; последний месяц исключён — это skip-month, гасит краткосрочный реверс).r(6-1)— аналогично за окно [t-5..t-1] (5 точек).σ(12)— стандартное отклонение (sample, ddof=1) месячных доходностей за окно [t-11..t]. Доходности простые, не логарифмические.- Веса 0.9 / 0.1 и длина окна 12 — фиксированные в
config.py.
В pipeline также реализована формула simple = r(12-1) / σ(12) (чистый 12-месячный импульс, без добавки 6-месячного). Дефолтные страницы и состав Q1-Q4 остаются на curve_fit; для сравнения на странице Experiments рядом показаны simple, curve_fit и веер промежуточных весов (a · r(12-1) + (1−a) · r(6-1)) / σ(12), a = 1.0…0.0. Веер демонстрирует, что выбор весов не критичен — на верхнем квартиле любые коэффициенты дают близкие кривые, обгоняющие рынок. Количественное сравнение simple vs curve_fit — внутренний research-документ, на сайт не выносится.
Почему такая формула
Сигнал стоит на классической литературе по momentum.
- Momentum. «Вчерашние победители» обгоняют «аутсайдеров» на горизонте 3–12 месяцев — устойчивая аномалия (Jegadeesh & Titman, 1993; как фактор — Carhart, 1997).
- skip-month. Последний месяц исключаем из доходности, потому что на горизонте ~1 месяца работает обратный эффект — краткосрочный разворот (Jegadeesh, 1990), который засоряет сигнал.
- Деление на
σ. Нормировка на собственную волатильность = ранжирование по доходности на единицу риска (Sharpe-подобный сигнал). При этом вσпоследний месяц входит (предпоследний торговый день), хотя из доходности он исключён. - Веса 0.9 / 0.1. Академического обоснования нет — это эмпирический подбор (curve-fit, максимизирует накопленную разницу Q1−Q4; устойчив в 95 из 100 скользящих 5-летних окон). Чувствительность к весам видна на странице Experiments.
Мы воспроизводим сигнал на собственных данных MOEX; вселенную задаём своим правилом (top-100 по ликвидности).
Квартили и ребаланс
- Универс месяца t ранжируется по score, разбивается на 4 равные группы. Q1 — топ (самый высокий momentum), Q4 — дно.
- Внутри квартиля веса равные.
- Ребаланс ежемесячный: signal-and-execute на одном close (последний торговый день месяца t).
- Сигнал считается из месячных закрытий [t-12..t]; портфель формируется по этим же close-ценам; первая holdings-доходность — close-t → close-(t+1).
- Look-ahead отсутствует — close-t это публичная информация после клиринга.
- На странице Quartile composition имена внутри Q1-Q4 идут по убыванию score (тай-брейк — тикер по алфавиту), номер строки = ранг по q-фактору. Сам score не показывается: порядок массива и есть авторитетный ранг (полная точность, как в ранжировании квартилей). Кнопка «по имени» лишь переставляет строки по алфавиту, ранг при этом сохраняется. Срез «top-K» в блоке изменений берёт первые K имён этого порядка.
Ожидающие включения (новые акции)
Свежие IPO с историей <12 месяцев не попадают в Q1-Q4 — для score нужно 13 закрытий. Чтобы они не выпадали из поля зрения, на странице Quartile composition под четырьмя квартилями есть справочный блок «ожидающие включения». Он не влияет на backtest, графики и составы Q1-Q4 — это только информация.
Тикер попадает в блок за месяц t, если выполнены оба условия:
- <13 закрытий подряд, оканчивающихся месяцем t (q ещё не считается — иначе он уже в одной из четырёх колонок). Гэп обнуляет счётчик: бумага после паузы считается заново молодой — та же логика, что в универсе;
- медианный месячный оборот за месяцы текущей непрерывной серии выше порога ликвидности универса — оборота наименее ликвидного (100-го) имени за тот же месяц. Этот порог показан в шапке блока за каждый месяц.
Что показываем, по возрасту с листинга:
- <6 месяцев — только факт: «вышла на биржу N месяцев назад». Данных слишком мало для оценки.
- ≥6 месяцев — оценка квартиля: «…входила бы в qN». Score кандидата —
r_L / σ_Lпо окну текущей серии. Определение per-month то же, что в проде (r — геометрическое среднее доходностей со skip-month, σ — месячное СКО), поэтому масштабирование по горизонту не требуется. Результат сравнивается с границами квартилейcurve_fitтого же месяца.
Оценка грубая по построению: короткое окно (6-11 точек) даёт шумную σ, а score кандидата считается в simple-форме при сравнении с curve_fit-границами. Это ориентир, а не часть методики ранжирования.
Конвенция периода
Период с меткой t на сайте означает: «состав Q1-Q4 по итогам месяца t, посчитан на close последнего торгового дня t». NAV-значение для периода t — реализованная доходность от close-(t-1) до close-t.
Из этого следует:
- Последний торговый день апреля (например, 2026-04-30) → попадает в период 2026-04.
- Период публикуется только после того, как месяц завершён. Признак завершённости берётся из самих данных: пока в котировках нет торговых дней позже конца месяца M, периода M на сайте нет — последняя строка остаётся за M−1.
- Последняя историческая строка одновременно служит actionable-инструкцией: «состав на close M−1» = портфель, который держится в течение месяца M. Отдельной «Live»-секции нет — последняя строка и есть live signal.
- Первая строка составов на месяц раньше начала бэктеста: это портфель, сформированный на close до окна, чтобы первый месяц окна заработали и стратегия, и бенчмарк. Без него сравнение шло бы по разному числу месяцев.
Учёт издержек
| Параметр | Значение |
|---|---|
| Налог на дивиденды (резидент РФ) | 13% |
| Комиссия брокера на сторону | 0.05% |
| Ребаланс | ежемесячный |
Комиссия применяется к торговому обороту между составами Q-портфелей соседних месяцев (вход + выход). Дивиденды реинвестируются после удержания налога.
Бенчмарк
MCFTRR — индекс полной доходности MOEX после налога на дивиденды: налоговая база у нас общая. Схема весов — нет. MCFTRR взвешен по free-float капитализации с ограничением на эмитента, Q-портфели равновзвешены по ~25 именам. Поэтому разрыв Q1−MCFTRR — это моментум плюс эффект равного веса и меньшей капитализации; разделить их можно только cap-weighted версией Q1.
Ограничения
- Налог на прирост капитала не учтён. Стратегия (Q1) оборачивает около 40% портфеля в месяц, средняя позиция живёт порядка пяти месяцев — льгота на долгосрочное владение неприменима. При годовом сальдировании по ставке 13% это стоит порядка 1.5–2 п.п. CAGR. MCFTRR прибыль не реализует, поэтому по этой статье сравнение асимметрично в пользу бенчмарка; на ИИС эффект исчезает.
- Спред и проскальзывание не смоделированы. Расчёт ведётся по цене закрытия в предположении, что сделка любого размера проходит по ней.
- Часть разрыва с бенчмарком — не моментум. Q-портфели равновзвешены, MCFTRR капвзвешен: вклад равного веса и меньшей капитализации в отдельную величину не выделен.
- Делистнутая бумага даёт 0% за терминальный месяц. Для денежных выкупов это близко к истине, для банкротств завышает; смещение оседает в нижних квартилях и сужает спред Q1−Q4.
- Выплаты не в рублях отбрасываются. В месяцы таких выплат total return бумаги занижен — смещение одностороннее. Подробности в «Корпоративных действиях».
- Результат перебора вариантов не значим. Линия «Q1 top15» на главной и вееры на странице Experiments — это перебор параметров на одной и той же истории. Превосходство вариантов над базовой стратегией проверялось block-bootstrap с поправкой на множественные сравнения: ни один вариант поправку не пережил. K=15 выбран как продуктовое решение, а не как лучший вариант в выборке.
Источники данных
| Класс | Источники |
|---|---|
| Цены (raw, daily) | MOEX ISS |
| Даты закрытия реестров | MOEX (экспорт register-closing-dates) |
| Суммы дивидендов | MOEX ISS (до 2025-10) + dohod.ru + smart-lab.ru + Yahoo Finance + Tinkoff |
| Сплиты / bonus issues | MOEX ISS + manual override |
| Бенчмарк (MCFTRR) | MOEX ISS |
| Корпоративные ребрендинги | MOEX ISS changeover + manual |
ISS - first-class для цен, индексов и сплитов. Дивиденды собираются каскадом внешних источников, а полнота проверяется против собственного экспорта MOEX по датам закрытия реестров. Cross-source расхождения адъюдицируются вручную и логируются в репозитории.
Корпоративные действия
- Сплиты: raw-цены без on-write adjustment. Detector флагает
|return| > 30%без объяснения дивидендом или записанным сплитом — fail-loud. - Дивиденды: multi-source cascade; при overlap побеждает источник с более высоким priority. Конфликты разрешаются adjudication-файлом с actions
replace/drop/augment/ignore. - Делистнутая бумага в портфеле: за терминальный месяц позиция даёт 0% и выпадает из состава, историю не переписываем — анти-survivorship конвенция. Смещение — в «Ограничениях».
- Дата отсечки выводится, а не берётся из данных: источники хранят дату закрытия
реестра, а цена падает раньше — на два торговых дня до 2023-07-31 и на один после
(
skill_fill_yahooхранит саму дату отсечки). Дивиденд относится к цене дня перед отсечкой и зачитывается в её месяц. Выплаты, чья отсечка попадает в разрыв торгов, отбрасываются с WARN — там нет цены, против которой выплата измерялась. - Дивиденды не в рублях — не учитываются: конвертация валют вне скоупа, записи с
currency ≠ RUBотбрасываются. Реально теряются только выплаты POLY за 2015–2016: у остальных валютных записей есть рублёвый двойник либо они лежат до начала торгов бумаги. В месяцы таких выплат total return бумаги занижен — смещение одностороннее. - Ребрендинги — короткий разрыв (SECID-rename): auto-seed из ISS changeover. История сшивается. Примеры: MTSI→MTSS, EPLN→SFIN, EONR→UPRO, TCSG→T, MAIL→VKCO. В универс попадает только одна нога: предшественник до месяца переименования, преемник после — иначе один эмитент занял бы два слота из ста и получил двойной вес в квартиле.
- Два листинга одной бумаги без ребра (RUAL и расписка RUALR): в месяц, где проходят оба, в универсе остаётся более ликвидный.
- Редомициляции — длинный разрыв: FIVE→X5, YNDX→YDEX, HHRU→HEAD и др. Не сшиваются ни по ценам, ни по дивидендам — это разные инструменты. Тикер появляется в универсе через 12+ месяцев после первого листинга на MOEX. Выплаты до редомициляции принадлежат предшественнику и входят в его ряд. Cross-check против hand-compiled CSV (где предшественник и преемник склеены) ожидаемо показывает «Σours < Σlegacy» — by design, not bug.
Покрытие исторических данных
- 2014+: TQBR (T+2), полное покрытие, дневные сделки.
- 2010-2013: преимущественно legacy boards (EQBR/EQNE/EQNL/EQBS). Кандидатов с полной историей в этом окне 240-260.
- 2001-2009: на отдельных blue-chip'ах (GMKN с 2001-10, LKOH с 2002-02) есть данные, но универс слишком тонкий. Публикуемый бэктест начинается с 2013-01 (
ANALYSIS_START_DATE); данные 2010-2012 нужны только как разгон для 13-месячного окна. - Pre-2014 prices split-adjusted через ISS (raw close с поправкой на согласованные сплиты). Mfd.ru как параллельный источник в pipeline не используется (был перенесён в
mfd_backfill/для архива и форензики недостающих сплитов).
Реализация
Стек, конфигурация и инструкции по запуску — в README репозитория. Единственный источник правды по налогам, комиссиям и коэффициентам формулы — src/config.py.