Strasmore Research
Аналитика Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-08

Как сделать воспроизводимый бэктест на Python без API

Создайте воспроизводимый бэктест на Python без использования API ключей. Используйте фиксированные версии библиотек и детерминированные данные для анализа кривой доходности.

Воспроизводимое тестирование стратегий

Воспроизводимый бэктест на Python — это такой тест, который сторонний наблюдатель может запустить на «чистой» машине и получить в точности ваши результаты. Для этого не должны требоваться учетные записи или API-ключи. Большинство руководств проваливают этот тест уже на первой строке, где загрузка данных в реальном времени предоставляет следующему читателю историю цен, которая немного отличается от той, что была у автора. В этом обзоре мы фиксируем один движок с открытым исходным кодом, quantjourney-bt версии 0.12.4, запускаем его встроенный пример без каких-либо учетных данных, а затем используем реальные рыночные данные, чтобы показать, чего даже один «чистый» запуск все еще не может вам сказать.

Что делает бэктест воспроизводимым?

Воспроизводимость в данном контексте имеет узкое, проверяемое значение: другой человек запускает одну команду на «чистой» машине и получает ваши цифры вплоть до последнего десятичного знака. Этому препятствуют два обычных фактора.

Первый — это код. Библиотека версии 0.x не дает никаких гарантий совместимости между минорными релизами. Переименование функции, изменение параметров по умолчанию, перестановка столбцов — и ваш скрипт продолжает работать, но молча выдает другие результаты.

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

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

Фиксация версии установки: pip install quantjourney-bt==0.12.4

quantjourney-bt — это бэктестер QuantJourney, выпущенный под лицензией Apache License 2.0 и требующий Python версии 3.11 или новее. Версия 0.12.4 была опубликована двадцать первого июля две тысячи двадцать шестого года; именно на эту версию ссылаются все приведенные ниже команды, согласно документации от августа две тысячи двадцать шестого года.

Работайте в изолированной среде. python3 -m venv .venv создает её, source .venv/bin/activate активирует, а python -m pip install -U pip обновляет установщик внутри неё. Затем используйте точную версию: pip install quantjourney-bt==0.12.4.

В документации проекта указана форма без фиксации версии: pip install quantjourney-bt. Часть ==0.12.4 остается на ваше усмотрение, и на этапе 0.x она оправдывает свое наличие. Зафиксируйте версию там, где её сможет найти следующий специалист: pip freeze > requirements.txt сохраняет все разрешенные зависимости, включая те, которые вы не указывали явно.

Существуют два дополнительных расширения. pip install "quantjourney-bt[wf]" добавляет Optuna для примеров с walk-forward анализом и оптимизацией. pip install "quantjourney-bt[data]" добавляет yfinance в качестве резервного источника данных для бенчмарков.

Лицензия Apache-2.0 является разрешительной. Вы можете использовать и изменять код в коммерческих целях, при этом необходимо сохранять файлы лицензии и уведомлений при любом распространении, а участники проекта явно предоставляют патентные права.

Как запустить пример SMA из комплекта без API-ключа

В репозитории содержится скрипт запуска и пятьдесят готовых стратегий. Они разделены на стратегии, основанные на весах, и стратегии, основанные на ордерах; среди них также есть пять рабочих процессов с оптимизацией на скользящем окне (walk-forward). ./strategy.sh --list выводит каталог. ./strategy.sh example_weights_01_sma_daily --check импортирует одну стратегию и не обращается к данным, что является самым быстрым способом подтвердить корректность установки.

Демонстрационный запуск выполняется одной строкой: ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample

Флаг --sample-data — это ключевой элемент. Проект описывает используемый набор данных следующим образом:

Пример набора данных намеренно сделан небольшим и воспроизводимым. Он полезен для проверки установки, создания отчетов и ознакомления с работой движка без создания учетной записи.
Источник: quantjourney-bt README, версия 0.12.4, прочитано 6 августа 2026 года.

Результатом запуска является создание директории, а не вывод в консоль: summary.txt и summary.json, metrics.csv, equity_curve.csv рядом с equity_curve.png, dashboard.html, папка plots/ и run_metadata.json, где записаны параметры запуска. Последний файл — это то, что большинство пользователей игнорируют, хотя именно он позволяет провести аудит результата спустя год.

Оценивайте полученные метрики объективно. Входящий в комплект набор данных невелик и носит иллюстративный характер, поэтому коэффициент Шарпа и максимальная просадка, указанные в summary.txt, описывают лишь тестовый файл. Они не являются доказательством эффективности стратегии, и рассматривать их как результат — первая ошибка, которую можно совершить.

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

О чем не говорит одна кривая доходности на обучающей выборке

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

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

ЗапросПересечение скользящих средних 20/50 по SPY: ежегодная доходность в сравнении с удержанием
Точный SQL-код для каждого числа
WITH daily AS
(
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2016-01-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY d
),
averaged AS
(
    SELECT
        d,
        px,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        d,
        px,
        if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    toYear(d)                                                                   AS year,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
    round((exp(sum(log(px / px_prior))) - 1) * 100, 1)                          AS hold_pct,
    countIf(long_today != long_prior)                                           AS crossover_count
FROM positioned
WHERE px_prior > 0
  AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
Run this yourself

Одно неизменное правило, измеренное 9 отдельных раз. Сначала изучите два столбца с процентными значениями. В 2017 правило завершило год с результатом 16% против 19.4% при простом удержании SPY за тот же период. В 2025 в этих же двух столбцах указаны 10.4% и 16.4%. Код в обеих строках идентичен. Сдвинулось только временное окно.

Столбец с количеством пересечений показывает, насколько скудной становится доказательная база. 4 изменений позиции за 2025 сессий означает, что кривая доходности за целый год опирается лишь на несколько решений, что является слишком малой выборкой для того, чтобы считать результат статистически значимым.

Работает ли это правило так же на других активах?

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

ЗапросПрименение правила 20/50 к пяти ликвидным активам, 2021–2025 гг.
Точный SQL-код для каждого числа
WITH daily AS
(
    SELECT
        ticker,
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
      AND window_start >= '2020-07-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY ticker, d
),
averaged AS
(
    SELECT
        ticker,
        d,
        px,
        avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (PARTITION BY ticker ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        ticker,
        d,
        px,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    ticker                                                                      AS symbol,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
    round((exp(sum(log(px / px_prior))) - 1) * 100, 1)                          AS hold_pct,
    round(avg(long_prior) * 100, 0)                                             AS days_long_pct
FROM positioned
WHERE px_prior > 0
  AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESC
Run this yourself

QQQ находится в верхней части таблицы с показателем 40.2%, а нижняя строка, KO, демонстрирует 3.8%. Столбец days_long_pct показывает, какую долю времени каждая версия находилась в позиции, для верхней строки это 67%. Один набор параметров, пять инструментов и разброс результатов настолько велик, что выбор победителя постфактум ничего не говорит о сделке, которую вы еще не совершили.

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

Где возникает ошибка заглядывания в будущее при тестировании весов

Механизм на основе весов преобразует сигнал в целевые веса, моделирует исполнение сделок по этим весам, а затем восстанавливает стоимость портфеля на основе полученных позиций. Ошибка скрывается в моменте объединения сигнала и веса. Если сегодняшний вес рассчитывается на основе сегодняшней цены закрытия, а затем приносит доходность за тот же день, бэктест использует информацию, которой не существовало на момент предполагаемой отправки ордера. Это и есть ошибка заглядывания в будущее, и система не выдает по ней предупреждений. Она просто делает все показатели более привлекательными.

На панели ниже представлены обе версии одного правила, протестированные на одной и той же истории SPY.

ЗапросПравило 20/50 по SPY: сравнение сигналов предыдущей и текущей сессий по годам
Точный SQL-код для каждого числа
WITH daily AS
(
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        toFloat64(argMax(close, window_start))               AS px
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2016-01-01 00:00:00'
      AND window_start <  '2026-01-01 05:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
    GROUP BY d
),
averaged AS
(
    SELECT
        d,
        px,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
        avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
        row_number() OVER (ORDER BY d)                                      AS session_no
    FROM daily
),
positioned AS
(
    SELECT
        d,
        px,
        if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
        lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
        lagInFrame(px, 1)
            OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
    FROM averaged
)
SELECT
    toYear(d)                                                                   AS year,
    round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
    round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
    round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
              - exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
  AND toYear(d) >= 2017
GROUP BY year
ORDER BY year
Run this yourself

В 2017 версия на основе данных предыдущей сессии показала 16%, тогда как версия на основе данных текущей сессии — 17.1%, что составляет разницу в 1.1 процентных пункта. В 2025 разрыв между ними составил 1.6 процентных пункта. Только один из этих столбцов может быть получен машиной, которая заранее не знала цену закрытия сессии, и разница между ними — это чистая бухгалтерская манипуляция, за которой не стоит ни идеи, ни мастерства, ни реальной сделки.

Данный движок четко обозначает свою позицию по вопросу тайминга, что стоит дороже любого обещания:

При исполнении сделок по цене открытия, проскальзывание, чувствительное к диапазону, учитывает только предыдущий завершенный бар, а прогноз объема торгов строится на основе запаздывающих наблюдений; движок не использует данные о максимуме, минимуме, цене закрытия или полном дневном объеме текущего дня.
Источник: quantjourney-bt README, версия 0.12.4, прочитано 6 августа 2026 года.

Задокументированное допущение можно проверить по исходному коду, который вы уже установили. Недокументированное — это лишь догадка.

Почему существует дополнительный модуль walk-forward

Примеры walk-forward, от WF01 до WF05, поставляются с дополнительным модулем [wf] и его зависимостью Optuna. Метод walk-forward подбирает параметры на одном историческом срезе, оценивает их на следующем, а затем сдвигает пару окон вперед и повторяет процесс. Скользящие и расширяющиеся варианты различаются тем, отбрасывает ли окно подбора самые старые данные по мере продвижения. Еще один пример добавляет очистку (purge) и эмбарго на каждой границе, исключая наблюдения, ближайшие к стыку, чтобы данные из обучающего среза не проникали в срез, на котором проводится оценка.

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

Часто задаваемые вопросы

Можно ли провести бэктест стратегии без API-ключа?

Да. В состав quantjourney-bt входит набор демонстрационных данных, доступный через флаг --sample-data, поэтому примеры стратегий можно запускать без учетной записи и учетных данных. Этот набор данных невелик и носит иллюстративный характер, поэтому рассматривайте такой запуск как проверку установки и работоспособности конвейера, а не как доказательство эффективности стратегии.

Зачем фиксировать версию Python-пакета для бэктестинга?

Пакеты версии 0.x не гарантируют совместимость между минорными релизами, а изменение настроек по умолчанию или переименование метрики может произойти без предупреждения. Фиксация версии с помощью pip install quantjourney-bt==0.12.4 и сохранение окружения в файле зависимостей гарантируют, что результат, полученный сегодня, можно будет воспроизвести через год на том же движке.

Под какой лицензией выпущен quantjourney-bt?

Apache License 2.0. Она разрешает коммерческое использование и модификацию, требует сохранения файлов лицензии и уведомлений при распространении, а также включает явное предоставление патентных прав от участников разработки. Версия 0.12.4 была опубликована двадцать первого июля две тысячи двадцать шестого года и требует Python версии 3.11 или новее.

Означает ли сильный результат бэктеста, что стратегия работает?

Нет. Бэктест — это одно измерение за один период на одном наборе активов. Приведенные выше панели показывают, как одно и то же неизменное правило дает совершенно разные годовые показатели для одного тикера и разные результаты для пяти различных инструментов. Именно этот разрыв призваны выявить форвардное тестирование и проверка на данных вне выборки.

Как были рассчитаны приведенные выше панели

Дневные цены закрытия — это последний принт регулярной торговой сессии для каждой даты, зафиксированный по нью-йоркскому времени в интервале с девяти тридцати утра до четырех часов вечера. Это позволяет корректно учитывать укороченные торговые дни без жесткого кодирования длительности сессии. Быстрая скользящая средняя охватывает двадцать сессий, медленная — пятьдесят; обе являются простыми скользящими средними. Первые сорок девять сессий каждого ряда считаются периодом прогрева, в течение которого позиции не открываются. Годовые показатели суммируют движение цены от закрытия до закрытия для сессий, когда правило находилось в длинной позиции, а столбец «удержание» суммирует все сессии года для сравнения. Пять инструментов в кросс-секции были выбраны из-за непрерывной истории котировок без дробления акций внутри периода, поэтому ряды цен закрытия не требуют корректировки. Временные окна зафиксированы в прошлом, поэтому эти панели показывают одинаковые цифры при каждом пересчете.


Каждая панель здесь содержит точный SQL-запрос, на основе которого она построена. Это делает цифры на странице такими же воспроизводимыми, как и зафиксированная версия установки. Чтобы оценить правило на вашем собственном временном интервале до написания кода бэктеста, задайте вопрос на обычном английском языке в терминале Strasmore.

#backtesting#python#open source#reproducibility#quantjourney