Pandas-бектесты завышают доходность на 30-70%. Это не баг, это by design. Попробовал разобрать на данных MOEX, сравнил с event-driven подходом и показал замеры latency для Tinkoff API. Плюс немного боли про то, когда Python уже не вывозит


Введение

Март 2019. Запускаю первую стратегию на реальные деньги. Momentum на пересечении скользящих средних – бектест в pandas показывал Sharpe 2.1. Депозит 200 тысяч рублей.

К июню осталось 142 тысячи. Минус 29%. Что-то пошло не так...

Полгода разбирался, что именно. Грешил на рынок, на невезение, на размер позиции. Оказалось – врал бектест. Не случайно, а систематически.

В прошлой статье я показывал статистику: 97% дейтрейдеров в минусе. Но одно дело знать, что большинство теряет. Другое – понять, почему твой конкретный бектест показывает прибыль, которой потом не будет.


Откуда берётся look-ahead bias

Что это такое

Look-ahead bias – когда используешь информацию, которой не было в момент принятия решения. Термин из работы Bailey, López de Prado, 2014.

Ошибка в одной строчке

Когда начал копать, нашёл вот это в своём коде:

returns = prices.pct_change().shift(-1)

shift(-1) сдвигает ряд назад. В ячейку с индексом t попадает значение из t+1. Мы кладём завтрашнюю доходность в сегодняшнюю строку.

При умножении strategy_returns = signal * returns неявно предполагаем покупку по цене закрытия того же бара. Но цену закрытия узнаёшь после закрытия. Торговать по ней нельзя.

Правильный вариант из López de Prado, глава 3:

# Сигнал вчера, исполнение сегодня
strategy_returns = signals.shift(1) * prices.pct_change()

Исправил. Sharpe упал с 2.1 до 1.4. Ближе к реальности, но всё ещё не 0.3. То есть дыра была не одна.

Survivor bias

Когда в 2019-м тестировал стратегию, взял список акций из индекса IMOEX на тот момент. Но часть компаний, которые были в индексе в 2015, к 2019 уже вылетели или обанкротились. В мой тест они не попали. Тестировал только на выживших.

По Shumway, 1997 это занижает оценку риска на десятки процентов годовых. Насколько применимо к MOEX честно говоря, не знаю. Но явно не ноль

Fill assumption

В бектесте считал, что покупаю по close. В реальности покупал по open следующего бара. Плюс проскальзывание. На волатильных бумагах разница легко 1-2%, в панические дни больше.

Подробнее: Interactive Brokers, 2025.


Методология

Что сравнивал

Vectorized (pandas). Операции над всем массивом сразу. Быстро. Но легко случайно заглянуть в будущее.

Event-driven. Последовательная обработка событий. Стратегия видит только то, что было доступно на момент текущего события. Медленнее, зато не обманешь сам себя.

Базовая структура по мотивам Ernest Chan, глава 7:

from heapq import heappush, heappop

def run(self):
    while self.events:
        timestamp, event_type, data = heappop(self.events)
        
        if event_type == 'BAR':
            # Стратегия видит только историю до текущего момента
            signal = self.strategy.on_bar(data)
            if signal:
                # Ордер попадёт на биржу через latency
                heappush(self.events, 
                    (timestamp + self.latency, 'ORDER', signal))
        
        elif event_type == 'ORDER':
            fill_price = data.price * (1 + self.slippage)
            # ...

Суть: данные будущих баров ещё не в очереди. Стратегия физически не может туда залезть.

Данные и параметры

  • Инструменты: SBER, GAZP, LKOH

  • Период: 2020-2024

  • Таймфрейм: часовые свечи

  • Slippage: 0.05% (эмпирически, по своим сделкам)

Про VectorBT

Год использовал VectorBT. Думал, раз в названии "vector", значит там что-то умное с параллельными вычислениями.

Потом нашёл комментарий автора:

"Although vectorbt has 'vector' in its name, it doesn't actually do any vectorized backtesting - it follows a sequential approach just like most other libraries."

Внутри обычная последовательная обработка с Numba JIT. Библиотека хорошая, но от проблем с моделированием исполнения не спасает

Справедливости ради стоит сказать, что автор этого и не обещал. Это я додумал сам


Результаты

Что получилось с Sharpe

Этап

Sharpe ratio

pandas с ошибкой shift(-1)

2.1

pandas после исправления

1.4

Event-driven, slippage 0.05%

0.85

Реальная торговля (2019)

~0.3*

*Оценка на коротком периоде, статистически так себе

Оценка завышения по типам стратегий

Называю это «pandas premium»:

Тип стратегии

Завышение

Momentum

30-50%

Mean reversion

50-70%

Breakout

25-40%

Простота методологии в том что сравнивается результат одной и та же логики в pandas и в event-driven

Это мой опыт на российском рынке на ликвидных бумагах. На американском или на неликвиде цифры могут быть другими. Не проверял.

Похожие оценки в работе Bailey, López de Prado, 2014. Backtest overfitting приводит к значительной деградации out-of-sample результатов.


Где заканчивается Python

Что реально тормозит

В прошлой статье упоминал колокацию MOEX. Где граница Python?

Проблема не только в GIL. По материалам доклада Optiver на CppCon 2024:

Garbage Collector. Для молодого поколения – микросекунды. Для старого 10-50 мс. По умолчанию срабатывает когда хочет. Можно отключить и дёргать gc.collect() вручную, но это дополнительная возня

Import time. import pandas занимает около 400 мс на моей машине. Просто import.

Object overhead. int в Python требует минимум 28 байт. В C++ только 4. При миллионах точек разница ощутимая.

Dynamic dispatch. Каждый вызов метода = поиск по словарю.

PEP 703 убирает GIL. Остальное никуда не денется.

Мои замеры latency

Tinkoff API (замеры через SDK, январь 2025):

Компонент

Задержка

WebSocket → Python dict

0.5-2 мс

Логика стратегии

3-8 мс

gRPC вызов

40-120 мс

Итого

50-150 мс

Не претендую на точность. Это прикидка на моём канале и моей машине.

DMA + колокация (по спецификациям MOEX):

Компонент

Задержка

C++ + FIX

5-15 мкс

Network

20-50 мкс

Итого

30-70 мкс

Три порядка разницы. Но колокация стоит миллионы в год.

Если roundtrip 100 мс, а я оптимизирую код с 10 мс до 0.1 мс, то выигрыш ~10%. Месяц работы ради 10%

Когда что использовать

По мотивам Aldridge, "High-Frequency Trading", с поправками на реальность MOEX:

Частота

Горизонт

Latency

Стек

Низкая

дни+

секунды

Python

Средняя

часы

сотни мс

Python + Numba

Высокая

минуты

единицы мс

C++ core

Сверхвысокая

секунды

< 1 мс

C++/Rust + колокация

Гибридная архитектура

Если нужна и скорость и удобство, то интересна комбинация из Sourav Ghosh, 2023. C++ для критичного пути, Python для остального. Общение через shared memory.

Для биндингов можно посмотреть nanobind. По бенчмаркам компилируется в 4 раза быстрее pybind11

Сам пока до гибридной архитектуры не дошёл. Мои стратегии достаточно медленные


Практические рекомендации

Workflow

Research: pandas + Jupyter. Абсолютным цифрам не верить. Только сравнивать варианты между собой.

Validation: Event-driven со slippage и комиссиями. Если Sharpe падает меньше чем на 30% относительно pandas, то где-то течёт look-ahead и надо искать баг.

Paper trading: 2-4 недели. Расхождение с validation до 20% это норма, а больше это уже проблема.

Production: Daily хватит и Python. Intraday стоит смотреть в сторону C++.

Три ошибки, на которых я обжёгся

«Sharpe 3 в бектесте. Запускаю!»

Запустил. Через месяц Sharpe был 0.4. Теперь любой результат из pandas делю на два.

«Перепишу на C++»

Переписал кусок. Было 50 мс, стало 0.5 мс. Гордый, замерил общий roundtrip. Был 150 мс, стал 100.5 мс. Месяц работы ради 33%. На daily-стратегии это технически интересно, но фактически бесполезно

«Наносекунды важны»

Начитался про HFT, полез оптимизировать аллокации. Потом подумал, что торгую раз в день, roundtrip 100 мс, позицию держу неделями. Наносекунды пока не для меня


Заключение

Vectorized-бектесты завышают доходность на 30-70%. Look-ahead bias и кривое моделирование исполнения. Event-driven это лечит, но работает медленнее.

Python или C++ зависит от частоты. Для daily через Tinkoff API переписывание на плюсы ничего не даст. Для intraday на минутках уже имеет смысл подумать.


Источники

  1. López de Prado M. Advances in Financial Machine Learning. Wiley, 2018

  2. Bailey D.H., López de Prado M. The Probability of Backtest Overfitting. 2014

  3. Bailey D.H., López de Prado M. The Deflated Sharpe Ratio. Journal of Portfolio Management, 2014

  4. Shumway T. The Delisting Bias in CRSP Data. Journal of Finance, 1997

  5. Chan E. Quantitative Trading. 2nd ed. Wiley, 2017

  6. Aldridge I. High-Frequency Trading. 2nd ed. Wiley, 2013

  7. Ghosh S. Building Low Latency Applications with C++. Packt, 2023

  8. PEP 703 — Making the Global Interpreter Lock Optional

  9. VectorBT — комментарий автора

  10. Колокация MOEX

  11. Tinkoff Invest API

  12. nanobind


Материал не является инвестиционной рекомендацией. Торговля на бирже связана с риском потери капитала.


C++-разработчик. Инвестирую больше десяти лет. Пишу о пересечении кода и капитала.

Комментарии (4)


  1. sic
    15.01.2026 07:49

    Отличная статья!

    Немного подушню, на тему того, что эти 97% дейтрейдеров в минусе - это все-таки местечковая история, полученная на базе двух исследований, на биржах с по современным меркам конскими комиссиями, но к статье это почти никак не относится.

    А вот с остальными цифрами из статьи согласен почти полностью. Очень похожие и у себя наблюдал. Тейки и подходы из статьи, - по сути все полностью справедливо. Даже с, казалось бы, спорным

    Переписал кусок. Было 50 мс, стало 0.5 мс. Гордый, замерил общий roundtrip. Был 150 мс, стал 100.5 мс. Месяц работы ради 33%. На daily-стратегии это технически интересно, но фактически бесполезно

    По опыту абсолютно согласен.


    1. aleontyev Автор
      15.01.2026 07:49

      Про 97% - справедливое уточнение. Тайвань и Бразилия это другие комиссии. В первой статье я это оговаривал, тут упростил. Но порядок величин, думаю, всё равно примерно такой - большинство в минусе


  1. Adgh
    15.01.2026 07:49

    Почему просто не взять готовые решения для бэктестов типа backtrader, Backtesting.py, не изобретать велосипед и не наступать на одни и те же грабли каждый раз (это про look-ahead bias). Тот же TA-lib для расчётов индикаторов? Зачем гнаться за скоростью, если HFT не является целью? Сомневаюсь, что на часовых свечах при ограниченном наборе инструментов подобные оптимизации имеют большое значение


    1. aleontyev Автор
      15.01.2026 07:49

      backtrader и Backtesting.py не решают проблему. Они тоже по умолчанию исполняют по close того же бара. Look-ahead bias никуда не девается, а просто прячется внутри библиотеки
      TA-lib для индикаторов сам использую. Но индикаторы это 5% задачи, остальное моделирование исполнения
      Про скорость согласен. В статье как раз об этом и говорю. Для daily-стратегий гнаться за миллисекундами бессмысленно. Но event-driven архитектура нужна не ради скорости, а чтобы физически не дать себе заглянуть в будущее.