Методика
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). - Только обыкновенные / привилегированные акции. История цен собирается юнионом по всем торговым бордам MOEX, на которых бумага листингована: главный пост-2014 режим TQBR + legacy MICEX-бордам EQBR/EQNE/EQNL/EQBS/SMAL/… до перехода на T+2 (2013). На overlap-окнах побеждает primary-board (TQBR). Облигации, ОФЗ, ETF — не участвуют.
Делистинг обрабатывается естественно: данные тикера заканчиваются → он автоматически выпадает из универса в момент исчезновения котировок. Никакого ретроспективного составления списка «успешных» бумаг.
Сигнал
Дефолтные страницы сайта и квартильные списки построены по формуле curve_fit:
score = (0.9 · r(12-1) + 0.1 · r(6-1)) / σ(12)
r(12-1)— total return за период с месяца t-12 по t-1 (исключая последний месяц — это skip-month, гасит краткосрочный реверс).r(6-1)— аналогично за t-6 до t-1.σ(12)— стандартное отклонение (sample, ddof=1) месячных log-доходностей за окно [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 это публичная информация после клиринга.
- На странице Q history имена внутри Q1-Q4 идут по убыванию score (тай-брейк — тикер по алфавиту), номер строки = ранг по q-фактору. Сам score не показывается: порядок массива и есть авторитетный ранг (полная точность, как в ранжировании квартилей). Кнопка «по имени» лишь переставляет строки по алфавиту, ранг при этом сохраняется. Срез «top-K» в блоке изменений берёт первые K имён этого порядка.
Ожидающие включения (новые акции)
Свежие IPO с историей <12 месяцев не попадают в Q1-Q4 — для score нужно 13 закрытий. Чтобы они не выпадали из поля зрения, на странице Q history под четырьмя квартилями есть справочный блок «ожидающие включения». Он не влияет на 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.
Учёт издержек
| Параметр | Значение |
|---|---|
| Налог на дивиденды (резидент РФ) | 13% |
| Комиссия брокера на сторону | 0.05% |
| Ребаланс | ежемесячный |
Комиссия применяется к торговому обороту между составами Q-портфелей соседних месяцев (вход + выход). Дивиденды реинвестируются после удержания налога.
Бенчмарк
MCFTRR — индекс полной доходности MOEX после налога на дивиденды. Совпадает с базой нашего расчёта, прямое сравнение корректное.
Источники данных
| Класс | Источники |
|---|---|
| Цены (raw, daily) | MOEX ISS |
| Дивиденды | MOEX ISS (primary) + dohod.ru + Yahoo Finance + Tinkoff (backfill pre-2014, дополняющие транши) |
| Сплиты / bonus issues | MOEX ISS + manual override |
| Бенчмарк (MCFTRR) | MOEX ISS |
| Корпоративные ребрендинги | MOEX ISS changeover + manual |
ISS — единственный first-class источник; остальные подключены каскадом для покрытия известных пропусков (pre-2014 dividend cutoff, отдельные транши совместных выплат). Cross-source расхождения адъюдицируются вручную и логируются в репозитории.
Корпоративные действия
- Сплиты: raw-цены без on-write adjustment. Detector флагает
|return| > 30%без объяснения дивидендом или записанным сплитом — fail-loud. - Дивиденды: multi-source cascade; при overlap побеждает источник с более высоким priority. Конфликты разрешаются adjudication-файлом с actions
replace/drop/augment/ignore. - Ребрендинги — короткий разрыв (SECID-rename): auto-seed из ISS changeover. История сшивается. Примеры: MTSI→MTSS, EPLN→SFIN, EONR→UPRO, TCSG→T.
- Редомициляции — длинный разрыв: FIVE→X5, YNDX→YDEX, HHRU→HEAD, MAIL→VKCO и др. Не сшиваются ни по ценам, ни по дивидендам — это разные инструменты. Тикер появляется в универсе через 12+ месяцев после первого листинга на MOEX. Pre-redomicile дивиденды у предшественника не учитываются. Cross-check против hand-compiled CSV (где предшественник и преемник склеены) ожидаемо показывает «Σours < Σlegacy» — by design, not bug.
Покрытие исторических данных
- 2014+: TQBR (T+2), полное покрытие, дневные сделки.
- 2010-2013: преимущественно legacy boards (EQBR/EQNE/EQNL/EQBS/SMAL). Кандидатов с полной историей в этом окне 240-260.
- 2001-2009: на отдельных blue-chip'ах (GMKN с 2001-10, LKOH с 2002-02) есть данные, но универс слишком тонкий — backtest не публикуется до 2010-01.
- Pre-2014 prices split-adjusted через ISS (raw close с поправкой на согласованные сплиты). Mfd.ru как параллельный источник в pipeline не используется (был перенесён в
mfd_backfill/для архива и форензики недостающих сплитов).
Реализация
Стек, конфигурация и инструкции по запуску — в README репозитория. Единственный источник правды по налогам, комиссиям и коэффициентам формулы — src/config.py.