Почему бэктестинг JavaScript ломается на реальных данных
Самодельный прогон стратегии выглядит убедительно до первой сверки с логом реальных сделок. Расхождение с реальными сделками возникает не из-за ошибки в формуле, а из-за допущений, которые в код просто не выписали.
Первое — модель исполнения заявок. Когда рыночная заявка срабатывает по цене закрытия бара, а лимитная — по касанию уровня, движок неявно отдаёт стратегии цену лучше, чем она получит в стакане. За кадром остаются проскальзывание, частичное исполнение и очередь заявок на одном уровне цены.
Второе — данные. Большие массивы истории в наивной реализации грузятся в память целиком и перебираются одним циклом. На нескольких годах минутных свечей это уже не помещается в отведённую память: прогон замедляется, потом зависает, а результаты между запусками становятся несопоставимы.
Третье — незамеченное подглядывание в будущее. Индикатор, посчитанный по всей серии сразу, к моменту сигнала уже знает дальнейшие котировки; то же происходит при пересчёте на изменившемся составе данных, на смене таймзоны и на пропусках в истории.
Поэтому бэктестинг JavaScript начинается не с цикла по свечам, а с описания модели рынка: правила исполнения, комиссии, ограничения ликвидности, порядок подгрузки истории. Разбираем, как это закрывается в движке проверки стратегий.
Что даёт собственный движок бэктестинга на JavaScript
Пока набор стратегий считается во внешнем сервисе или скриптом в один поток, цикл проверки гипотезы растягивается на часы. Движок на JavaScript/TypeScript живёт в том же рантайме, что и торговая логика, и раздаёт прогоны по ядрам процесса: набор из десятков конфигураций считается за минуты. Разница не в «оптимизации», а в том, что исследователь успевает проверить десять вариантов параметров, пока не потерял нить.
Повторный расчёт на истории опирается на кэш загруженных свечей и инкрементальный пересчёт: при правке одного правила заново считаются только затронутые участки и сделки. Полный прогон нужен лишь при смене набора данных или логики исполнения, а не при каждой итерации. Правка стратегии перестаёт быть дорогой операцией.
Воспроизводимость держится на фиксированном снапшоте рыночных данных, закреплённой модели комиссий и проскальзывания и сохранённом зерне генератора случайностей. Тот же прогон, повторённый через месяц или на другой машине, даёт совпадающую кривую доходности и совпадающий список сделок. Вместе с этим уходит класс отказов, когда стратегия выглядит рабочей на истории только из-за расхождения данных или незамеченной подгонки под прошлое.
Стоимость простоя расчётов обычно недооценивают: пока считается один вариант, очередь гипотез стоит, а решения принимаются по устаревшей картине. Когда прогон занимает минуты, спорные идеи отсеиваются на истории, а не на живом счёте.
Подходы к расчёту: событийный, векторный и потоковый
Выбор режима расчёта определяет, насколько поведение стратегии на истории совпадёт с её поведением в бою. Векторный расчёт обрабатывает весь ряд котировок одной операцией: он быстр и удобен для первичного отбора гипотез, но не воспроизводит порядок заявок внутри бара и проскальзывает мимо пересечений стопа и цели. Событийный расчёт прогоняет данные событие за событием — это тот же порядок, что и в режиме проверки стратегий на живом рынке.
Требования к историческим данным у режимов разные. Векторному нужны выровненные ряды одной частоты без пропусков, событийному — корректные метки времени и несинхронные источники, а потоковая обработка котировок ставит самые жёсткие условия: порядок и формат записей должны совпадать с биржевыми, включая разрывы и повторные публикации.
| Режим | Когда подходит | Ограничения |
|---|---|---|
| Векторный расчёт | Массовый перебор гипотез, индикаторные и портфельные стратегии на длинной истории | Не моделирует внутрибарные события, завышает результат у стратегий со стопами |
| Событийный расчёт | Заявки, частичные исполнения, работа по нескольким инструментам сразу | Считает медленнее, требует полных данных о заявках и комиссиях |
| Потоковая обработка котировок | Сверка с живым потоком и повторное проигрывание записанной сессии | Ошибка в данных проходит без предупреждения, нужен отдельный контур контроля |
На практике режимы не конкурируют: гипотезы отбираем векторным расчётом, финальных кандидатов прогоняем событийным и сверяем с потоковым проигрыванием. Тогда расхождение между историей и реальностью измеряется заранее, а не обнаруживается на счёте.
Этапы разработки движка проверки торговых стратегий
Порядок работ выстроен так, чтобы результат каждого этапа можно было проверить до перехода к следующему. Проверка стратегий на истории — это в первую очередь вопрос воспроизводимости: один и тот же набор данных и код должны давать одинаковый отчёт независимо от машины и времени запуска. Если этого нет, любые выводы о доходности стратегии ничего не стоят.
- Аудит расчётного контура. Разбираем, как результаты считаются сейчас: где теряется история сделок, учитываются ли комиссии и проскальзывание, есть ли заглядывание в будущие данные относительно момента входа.
- Проектирование схемы данных. Определяем формат хранения котировок и сделок, правила сведения разных таймфреймов, версионирование наборов, чтобы старый прогон можно было повторить через месяцы.
- Настройка окружения расчётов. Фиксируем источник времени, изолируем зависимости, добиваемся идентичного отчёта при запуске локально и на сервере.
- Реализация ядра. Пишем обработчик потока котировок, расчёт метрик и отчётность; ядро остаётся чистым, без привязки к конкретному источнику данных.
- Контрольные наборы данных. Готовим эталонные прогоны с заранее известным результатом: расхождение по числу сделок или итоговой кривой — блокирующий сигнал, а не повод продолжать.
- Передача движка команде. Отдаём конфигурации, описание схемы данных, регламент сопровождения и порядок добавления новых инструментов, проводим разбор с вашими инженерами.
На выходе команда получает не разовый расчёт, а инструмент, которому можно доверять при сравнении стратегий: расхождения в отчётах объясняются логикой стратегии, а не поведением самого движка.
Что остаётся в репозитории после сдачи движка
Отдаём не «код и созвон», а рабочий репозиторий, в котором ваша команда продолжает работу без нас. Ниже — что лежит в ветке на момент сдачи и зачем нужен каждый артефакт.
- Конфигурации окружения — отдельные наборы для локального, тестового и рабочего контура: источники котировок, лимиты параллельных прогонов, пути к хранилищу результатов. Секреты подтягиваются из переменных окружения и в репозиторий не попадают.
- Схема данных расчётов — сущности свечей, сделок, ордеров и прогонов с типами полей и связями. Новая метрика встраивается в неё без правки существующих таблиц.
- Политики доступа к данным — роли для сервисов и людей: чтение истории, запись результатов, доступ к торговым ключам. Ключи подключаются только на чтение.
- Регламент обновления истории — как догружаются пропущенные свечи, чем проверяются разрывы и дубли, в каком порядке идёт пересчёт. Неполный прогон помечается явно, а не выдаётся за результат.
- Документация движка — точки расширения с примерами на JavaScript и TypeScript: как добавить индикатор, тип ордера, модель комиссий и проскальзывания.
- Тесты и эталонные прогоны — фиксированный набор данных и ожидаемые метрики, по которым видно, что после правок результат не поехал.
- Инструкция по запуску и откату — как поднять движок с нуля и вернуться к предыдущей версии.
Разбор по репозиторию проводим в конце проекта: проходим с вашей командой по каждому артефакту и показываем, где что менять.
Кейс: расчёты ускорились, расхождения в отчётах исчезли
Заказчик — команда количественной торговли криптовалютами — считал стратегии набором разрозненных скриптов: историю тянули из разных источников, комиссии и проскальзывание учитывали вручную, а месячный отчёт сходился с биржевой выпиской только после правок в таблице. Один прогон по длинной истории занимал часы, поэтому гипотезы проверяли редко и крупными партиями.
Мы собрали единый движок на JavaScript и TypeScript: слой загрузки приводит котировки из разных источников к одной схеме, расчётный модуль воспроизводит порядок событий внутри свечи, а модель сделки учитывает комиссию, проскальзывание и частичные исполнения. Историю берём через CoinGecko и биржевые архивы, а результаты прогонов складываем в промежуточное хранилище — повторный расчёт по тем же данным не начинается с нуля.
Что изменилось на замерах
- время прогона по многолетней истории — с часов до минут на том же наборе данных;
- доля расхождений с реальными сделками — с десятой части сделок до единичных случаев;
- ручные правки отчётов — с десятков строк на отчёт до нуля.
Устойчивость расчётов закрыли отдельно: фиксированный набор периодов истории прогоняется перед каждым изменением, а итог сверяется с фактическими сделками по журналу исполнений. Если расхождение выше порога, сборка не проходит — отчёт с неверной доходностью нельзя выпустить случайно.
Чем отличается проверка на истории от демо-торговли?
Проверка на истории, то есть бэктестинг, прогоняет один и тот же поток котировок многократно, поэтому воспроизводимость прогонов здесь полная: одинаковые входные данные дают одинаковый набор сделок. Отсюда и контроль условий расчёта — комиссии, фандинг, проскальзывание, правила срабатывания лимитных заявок и момент выставления задаются явно и хранятся вместе с версией логики, так что правки стратегии сравниваются на одном и том же отрезке.
Обратная сторона — допущения модели исполнения. Если заявка исполняется по цене закрытия свечи, в расчёте нет очереди заявок, частичного исполнения и задержки сети, результат расходится с реальностью тем сильнее, чем плотнее торговля. Добавьте инструменты, выпавшие из выборки после делистинга, и пропуски в данных — и расчёт покажет устойчивость там, где её нет.
Демонстрационный режим торговли даёт другое: живой поток и настоящий контур исполнения. Здесь видны лимиты запросов к биржевому интерфейсу, отказы заявок, разрывы соединения, реальные спред и глубина. Но повторяемость нулевая — лента не повторяется, поэтому два прогона одной и той же логики несравнимы между собой.
Вывод практический: исторический расчёт отвечает, есть ли у логики преимущество при заданных допущениях, а демонстрационный режим — выдержит ли она соприкосновение с боевой инфраструктурой. Смешивать эти ответы нельзя: качество стратегии считают по истории, а из демо берут факты отказов и поведение при сбоях.
Технические сомнения перед стартом разработки
Сомнения перед стартом обычно одни и те же: расчёт слишком идеален, чтобы ему верить, движок не сойдётся с боевым кодом, прогон упрётся в ресурсы. Ниже — как каждое из них закрывается ещё на этапе разработки.
Насколько точны расчёты?
Свечной прогон усредняет движение внутри интервала — заявка может исполниться по цене, которой на рынке не было. Считаем на потоке событий в порядке поступления, а расхождение с детальным прогоном показываем в отчёте, а не оставляем на потом.
Как учитываются комиссии и проскальзывание?
Комиссия берётся по факту исполнения: отдельно для встречных и рыночных заявок. Проскальзывание зависит от объёма заявки против ликвидности — модель снимает несколько уровней с книги. Повторяем прогон в ухудшенных условиях: стратегия, которая держится только на идеальном исполнении, отсеивается до запуска.
Совместимо ли с текущим кодом?
Движок подключается как библиотека, ваша логика входов и выходов остаётся основной. Адаптер приводит данные и события к общему виду, а контрольный прогон сверяет сигналы, пока список расхождений не обнулится.
Выдержит ли расчётный контур нагрузку?
История режется на диапазоны и считается параллельно, данные кэшируются между запусками. Многолетний прогон по десяткам инструментов укладывается в минуты, и его можно ставить в ночное окно, не занимая мощности под другие задачи.
Как не подогнать стратегию под историю?
Выборка делится на участок настройки и участок проверки, который не участвует в подборе. Параметры прогоняются по разным режимам рынка, в отчёт идёт разброс результата: если прибыль держится на одном удачном наборе, это видно заранее.
Обсудим задачу и вернёмся с оценкой и планом
Чтобы оценка опиралась на ваши данные, а не на усреднённые допущения, для старта достаточно описать задачу и прислать то, что уже наработано. Дальше мы разбираем материалы, задаём уточняющие вопросы и возвращаемся с планом работ, разбитым по этапам, — с указанием, что делаем на каждом и что понадобится от вас.
- Постановка задачи: какие стратегии тестируем, на каких инструментах и таймфреймах, что считаем результатом одного прогона.
- Источники данных: откуда берём историю цен и сделок, в каком формате выгрузка, есть ли пропуски и как они помечены.
- Правила исполнения: комиссии, проскальзывание, лимиты объёма, плечо, логика отмены и частичного заполнения заявок.
- Метрики на выходе: какие показатели нужны и в каком виде их смотреть — таблица, график, выгрузка в файл.
- Ограничения окружения: где крутится движок, нужен ли параллельный прогон набора стратегий, важна ли работа в браузере.
- Эталонные расчёты: пример, которому вы доверяете, чтобы сверить с ним результат нашего движка.
- Приёмка: какие сценарии обязательны и что для вас означает корректный результат бэктестинга.
Порядок старта такой: короткий разговор по задаче с инженером, который будет её делать, затем оценка и план по этапам. Отчитываемся по каждому этапу в одном формате: что сделано, какими метриками это подтверждается, какие ограничения остались и что нужно от вас. Показываем результат на ваших данных и ваших стратегиях, а не на синтетическом примере.
Опишите задачу в свободной форме — вернёмся с оценкой и планом работ.







