Разработка высокопроизводительного движка бэктестинга на Python

Прогон исторических данных растягивается на часы, и проверка гипотез тормозит всё направление. Мы разрабатываем расчётное ядро бэктестинга на Python под ключ: перерабатываем архитектуру расчёта и снимаем узкие места. Время прогона сокращается в разы, а результаты воспроизводятся на одних и тех же данных.
Часто задаваемые вопросы

Почему бэктестинг на Python упирается в скорость

На тестовом отрезке в неделю торгов прогон укладывается в минуты, на нескольких годах истории — уже в часы. Бэктестинг на Python упирается при этом не в объём данных как таковой: каждая перегруппировка, скользящее окно и срез создают копии фрейма, а поэлементный цикл по барам добавляет накладные расходы на каждый вызов. Узкое место расчёта почти всегда оказывается не в математике стратегии, а в перекладывании данных между структурами.

Второй предел — память. Вся история загружена одним фреймом, к ней присоединяются сигналы и результаты сделок, на каждый бар пишутся метрики — потребление растёт до предела машины, и прогон падает посреди теста. Помогает не добавление памяти, а потоковая обработка окнами и явные числовые типы вместо объектов. Оптимизировать вслепую бессмысленно: замер производительности показывает, какой участок съедает основное время, и только после него правки дают кратный эффект, а не переписывание ради переписывания.

Воспроизводимость результатов — проблема менее очевидная. Порядок операций с плавающей точкой, нестабильная сортировка, незафиксированные генераторы случайных чисел и разный порядок обхода приводят к тому, что повторный запуск на тех же данных выдаёт другие цифры. Сравнение двух версий стратегии теряет смысл: непонятно, это улучшение или шум округлений.

Медленный прогон и расхождение цифр — не свойства языка, а следствие устройства конвейера расчёта: где живут данные, кто владеет копиями, что и в каком порядке считается. Ниже разбираем, как закрывается каждое из этих мест — от замеров до фиксации окружения прогона.

Что даёт переработка движка бэктестинга на Python

Скорость прогона на исторических данных

Переработка движка начинается с профилирования: находим, где расчёт упирается в интерпретатор, а где — в лишнее копирование данных. Векторизуем вычисление индикаторов, убираем повторные проходы по котировкам и переводим тяжёлые участки в компилируемый код Python. Скорость прогона на многолетней истории вырастает в разы: то, что считалось десятками минут, укладывается в минуты, и вся сетка параметров проверяется за один запуск вместо ночи ожидания.

Устойчивость расчёта

Второй эффект — предсказуемость результата. Пиковая нагрузка на память перестаёт зависеть от длины истории: данные читаются порциями, промежуточные ряды освобождаются сразу. Уходит целый класс отказов — падения по памяти и «тихие» расхождения между одинаковыми прогонами. На одинаковых входных данных получается одна и та же кривая, поэтому расхождение в отчётах становится сигналом о логике стратегии, а не о движке.

Освободившееся время команда тратит на гипотезы, а не на ожидание и разбор падений. Отчёт по каждому прогону воспроизводится, откат к прошлой конфигурации занимает один запуск — проверка идей идёт непрерывно, без ручных обходов и пересборки окружения.

Варианты организации расчётов: сравнение режимов

Режим расчёта определяет не только скорость, но и то, какие проверки вообще воспроизводимы. Событийный прогон ведёт историю в том же порядке, в котором работает торговая логика, векторный считает массив целиком за один проход, распределённый разносит независимые сценарии по узлам. Выбор режима диктуют число сценариев и требования к точности модели исполнения — ниже альтернативы и то, чем за каждую платят.

Вариант Когда подходит Ограничения
Событийный прогон Логика зависит от потока заявок, частичных исполнений и срабатывания стопов внутри бара Один полный проход по истории на каждый сценарий, дорого при переборе параметров
Векторный расчёт Массовая проверка сигналов и индикаторов на широкой истории Не воспроизводит очередь заявок, проскальзывание и приоритет ордеров
Распределённые вычисления на нескольких узлах Сотни независимых наборов параметров и прогонов по разным инструментам Нужны общий кэш рыночных данных, сериализация состояния и контроль, что результат на кластере совпадает с одиночным запуском
Инкрементальный пересчёт Изменился последний участок данных или один блок правил Требует карты зависимостей: часть результатов придётся считать заново целиком

Сочетание режимов в одном движке

Режимы редко выбирают раз и навсегда. Обычно векторный проход отсеивает заведомо слабые комбинации на всей истории, событийный проверяет отобранные с точной моделью исполнения, а инкрементальный пересчёт держит результат актуальным при поступлении новых данных. Распределение подключается слоем выше — на уровне постановки задач, без правок логики стратегии: один и тот же набор правил даёт идентичный отчёт и на локальной машине, и на кластере.

Этапы переработки: от аудита расчёта до передачи

Переработка расчётного ядра идёт итерациями: сначала измеряем, потом меняем, потом сверяем на контрольных прогонах. После каждого этапа остаётся артефакт, по которому видно, можно ли двигаться дальше.

  1. Аудит движка и замер узких мест. Профилируем расчёт на вашей истории: где время уходит в ожидание, где данные считаются повторно, где итог зависит от порядка обхода.
  2. Схема расчёта. Фиксируем поток данных: источники котировок, правила очистки, момент решения, учёт комиссий и проскальзывания. Схема становится контрактом для приёмки.
  3. Переработка ядра. Убираем повторные вычисления и лишние копии, разделяем подготовку данных и прогон, тяжёлые участки переводим на векторизацию. Python остаётся языком описания стратегий — меняется внутренность движка.
  4. Проверка на истории. Прогоняем новый движок на тех же данных, что и старый, и сверяем метрики сделка за сделкой. Расхождения разбираем до причины, а не до «примерно похоже».
  5. Приёмка результатов. Вы запускаете свои сценарии, мы показываем совпадение и разбор расхождений по схеме расчёта, а не по ощущениям.
  6. Передача и обучение. Отдаём код, конфигурации и регламент прогона, разбираем с вашей командой, как добавлять стратегии и данные без нас.

Параллельная работа с вашей инфраструктурой

Пока идёт переработка, часть шагов закрываем на вашей стороне: подключаемся к вашему хранилищу данных и запускаем прогоны в вашем окружении. Так результат виден на реальных данных до финальной передачи, а не после неё.

Комплект поставки: расчётное ядро, схемы, регламенты

Комплект передаётся не архивом «как получилось», а набором артефактов, по которым ваша команда запускает движок, воспроизводит расчёт и дорабатывает его без нас. Ниже — что именно уходит заказчику.

  • Исходный код движка — модули расчёта позиций и метрик, тесты, примеры прогонов на исторических данных.
  • Схемы расчёта — граф вызовов, порядок обработки свечей и тиков, точки учёта комиссий, проскальзывания и задержек исполнения.
  • Схемы потоков данных — путь от источника котировок до витрины результатов: форматы файлов, разбиение на партии, куда пишутся метрики.
  • Конфигурации окружений — параметры запуска, наборы данных, профили для одиночного и распределённого прогона.
  • Политики доступа и роли — кто запускает расчёт, кто меняет параметры стратегий, кто читает результаты; секреты хранятся вне репозитория.
  • Регламент запуска и восстановления — старт прогона, продолжение с контрольной точки, откат к предыдущей версии ядра.
  • Документация и инструкции для команды — описание интерфейсов, справочник параметров, порядок добавления новой стратегии.

Как проходит приёмка

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

Кейс: как сократили время прогона исторических данных

Клиент пришёл с движком бэктестинга на Python, который работал, пока история укладывалась в один торговый год. Полный прогон по пятилетнему окну занимал около четырнадцати часов и часто не доживал до конца: расчёт падал на длинных рядах, а два запуска одной стратегии давали разный результат на третьем знаке. Сравнивать гипотезы приходилось вручную, а решениям по таким цифрам никто не доверял.

Замеры по участкам кода показали: большую часть времени съедали построчные циклы по барам и пересчёт индикаторов на каждом шаге, а нестабильность давали плавающие порядки сложения и протекающее состояние между запусками.

Что именно переработали

Логику расчёта перевели на векторные операции над массивами: индикаторы считаются один раз по всей истории, а не заново на каждом баре. Тяжёлые участки вынесли в отдельные процессы, промежуточные результаты сохраняются, поэтому повторный прогон не считает готовое заново. Зафиксировали порядок агрегации и изолировали состояние: каждый запуск стартует с чистой среды, и результат воспроизводится бит в бит.

После переработки полный прогон по тому же окну укладывается в сорок минут, разброс между запусками — ноль, а на длинной истории за квартал наблюдений не было ни одного падения из нескольких сотен запусков. Ограничения проговариваем сразу: на тиковой истории за несколько лет расчёт всё ещё идёт часами, такие задачи ставим в ночное окно, а число параллельных прогонов ограничено памятью — перебор параметров идёт очередями, а не всем полем сразу.

Чем отличается векторный расчёт от событийного?

Векторный расчёт применяет правило сразу ко всему ряду цен: индикатор и сигнал считаются массивами, без цикла по времени. Историю в несколько лет такой прогон проходит за секунды, поэтому на нём перебирают десятки тысяч комбинаций параметров и отсеивают заведомо слабые. Ограничение одно, но жёсткое: логика должна быть безусловной, без опоры на состояние позиции.

Событийный прогон идёт свеча за свечой и на каждой восстанавливает состояние: открытые позиции, остаток средств, выставленные заявки. Только так считаются стопы и тейки внутри свечи, частичное исполнение, перенос позиции через ночь, комиссия и проскальзывание, зависящее от объёма. Векторно это не выражается: итог зависит от того, что было раньше, и порядок событий меняет результат.

Разница в точности расчёта проявляется там, где решение зависит от текущего счёта. Вектор покажет вход и выход по цене закрытия, а событийный прогон — что часть заявок не исполнилась, а стоп сработал по худшей цене. Первый ответ годится, чтобы отобрать идеи; второй — чтобы решить, можно ли доверять стратегии.

Отсюда сочетание подходов в одном движке: векторный расчёт как фильтр по широкой сетке параметров, событийный прогон — для финалистов и проверки на устойчивость к издержкам. Оба режима должны читать одни данные и применять одну логику, иначе результаты разойдутся и будет непонятно, какому верить. Практическая проверка — прогнать одну стратегию двумя способами: расхождение в пределах издержек подтверждает корректность модели исполнения, заметное расхождение указывает на ошибку в ней.

Частые технические вопросы по расчётному ядру и данным

Перед первым прогоном всплывают одни и те же сомнения: переписывать ли всё заново, совпадут ли расчёты с рынком, повторятся ли результаты. Ниже — пять вопросов с технической встречи и то, как мы их закрываем.

Придётся ли переписывать существующий код?

Нет. Ядро расчёта отделяем от загрузки данных и описания стратегий стабильным интерфейсом: текущие правила переносим, заменяем только узкие места — пересчёт индикаторов и обход истории. Сами стратегии остаются в привычном виде.

Насколько расчёт совпадёт с реальными условиями?

В модель издержек закладываем комиссию, проскальзывание по стакану, задержку исполнения и частичное заполнение заявок. Без этого бэктестинг на Python показывает доходность, которой на рынке не было.

Как обеспечить воспроизводимость прогонов?

Прогон фиксируется целиком: версия набора данных, параметры стратегии, начальное состояние генератора случайных чисел, настройки издержек. Повтор через месяц даёт тот же результат — это база для сравнения гипотез.

Что делать с неполной историей?

Пропуски и рассинхрон таймфреймов ловим до расчёта: сверка со вторым источником, восстановление баров, явная пометка периодов с низким покрытием. Спорные участки помечаем, а не выкидываем молча.

Выдержит ли ядро рост числа инструментов?

Расчёт разбивается на независимые задачи и идёт параллельно по ядрам и машинам, общие индикаторы кэшируются. Добавление сотни инструментов увеличивает время прогона линейно, а не обрушивает его.

Что нужно для старта: обсудим задачу и вернёмся с планом

Точная оценка движка бэктестинга держится не на догадках, а на вводных. Пока не видно, какие сценарии прогоняются и на каком объёме истории, любые сроки будут приблизительными — поэтому старт всегда один: короткое обсуждение задачи, после которого мы возвращаемся с планом внедрения и разбивкой по этапам.

Чтобы посчитать трудозатраты, пришлите:

  • описание сценариев прогона — какие стратегии проверяются, как часто меняются параметры, нужен ли перебор комбинаций;
  • объём и формат истории — число инструментов, глубина рядов, источник данных и их качество: пропуски, разрывы, корпоративные события;
  • текущую схему расчёта — как считаете сейчас: самописные скрипты, готовую библиотеку или таблицы, и что именно перестало устраивать;
  • требования к времени полного прогона — какая длительность прохода считается рабочей и на какой конфигурации вы её ждёте;
  • ограничения по инфраструктуре — где будет выполняться расчёт: локальные машины, свои серверы или облако, есть ли требования к изоляции данных;
  • формат результатов — отчёты, метрики, выгрузки, нужна ли визуализация и интерфейс для смежных систем.

По этим вводным мы собираем оценку работ, предлагаем архитектуру расчёта и называем сроки. План внедрения разбит на этапы так, чтобы результат каждого можно было проверить на ваших данных, а не на демонстрационном примере.

Направления блокчейн-разработки

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    815
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139