13.8 C
Sevastopol
18 сентября, 2026
СЕВОБОРОНА
Технологии

Полноценный trading engine для экосистемы NodeJS

Полноценный trading engine для экосистемы NodeJS

Последние пять лет экосистема Node.js стабилизировалась. Ничего принципиально нового в ней не появляется: мы обсуждаем, чем Fastify лучше Express, выбираем между Prisma и Drizzle, спорим про ESM против CommonJS и меняем один бандлер на другой, который делает то же самое, но быстрее. Ниши поделены, инструменты зрелые, хайп ушёл в соседние рантаймы. Это хорошая новость для продакшена и скучная — для инженера.

Тем интереснее, когда в экосистеме открывается целая область применения.

Backtest как один из режимов исполнения

Библиотека представляет собой набор инструментов для алгоритмической торговли, который расширяет сферу применения Node.js туда, где исторически безраздельно правил Python: Backtrader, VectorBT, Freqtrade. Одна и та же торговая стратегия работает как в live, так и backtest без изменений

Полноценный trading engine для экосистемы NodeJS

Дашборд backtest-kit

Разница принципиальная. Бектестер отвечает на вопрос: «Что было бы, если прогнать стратегию по истории?» Архитектура backtest-kit отвечает на вопрос шире: «Как стратегия существует и исполняется внутри торговой системы — исторически и в реальном времени

Вот целая стратегия — три регистрации и запуск. Ни бутстрапа, ни DI-контейнера, ни цикла по свечам:

import ccxt from «ccxt»; import { addExchangeSchema, addFrameSchema, addStrategySchema, Position, Backtest, listenSignalBacktest, } from «backtest-kit»; // Откуда брать свечи addExchangeSchema({ exchangeName: «binance», getCandles: async (symbol, interval, since, limit) => { const ohlcv = await new ccxt.binance().fetchOHLCV(symbol, interval, since.getTime(), limit); return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({ timestamp, open, high, low, close, volume })); }, }); // Какой период истории проигрывать addFrameSchema({ frameName: «feb-2026», interval: «1m», startDate: new Date(«2026-02-01»), endDate: new Date(«2026-02-28»), }); // Что делать: чистая функция «состояние рынка -> сигнал или null» addStrategySchema({ strategyName: «my-strategy», interval: «15m», getSignal: async (symbol, when, currentPrice) => ({ position: «long», …Position.bracket({ position: «long», currentPrice, percentTakeProfit: 2, percentStopLoss: 1 }), minuteEstimatedTime: 60 * 24, cost: 100, }), }); Backtest.background(«BTCUSDT», { strategyName: «my-strategy», exchangeName: «binance», frameName: «feb-2026», }); listenSignalBacktest(console.log);

Полноценный trading engine для экосистемы NodeJS

Ниже — разбор, почему это именно engine, по пунктам.

Backtest и Live используют одну модель исполнения

Это не отдельный симулятор, прикрученный сбоку к стратегии. В системе один execution flow: как для backtest, так и для live режима. В режиме backtest этот flow воспроизводится на исторических данных, в live — работает с текущим рынком. Функция getSignal, которую вы гоняли по истории, — байт в байт та же функция, что торгует вживую. Меняется только источник часов:

// Backtest — часы двигает исторический фрейм Backtest.background(«BTCUSDT», { strategyName, exchangeName, frameName }); // Live — часы двигает wall-clock; файл стратегии не изменился ни на строчку Live.background(«BTCUSDT», { strategyName, exchangeName }); // Paper — живые цены, ни одного реального ордера, тот же код-путь

Полноценный trading engine для экосистемы NodeJS

Тесты фреймворка (1030+ юнит- и интеграционных) прямо защищают это свойство: валидация сигналов, немедленная и отложенная активация, проверки TP/SL/timeout идентичны в обоих режимах, и если пути когда-нибудь разойдутся — тест упадёт раньше, чем ваш депозит. Практическое следствие: исчезает классическая болезнь «в ноутбуке стратегия была прибыльной, а в проде переписали — и она другая». Переписывать нечего.

Engine управляет жизненным циклом сделки, а не просто считает PnL

Сигнал в системе — не строка в итоговой таблице, а конечный автомат. У каждого состояния — только те поля, которые в нём осмысленны:

listenSignal((event) => { switch (event.action) { case «idle»: /* сигнала нет — есть только цена и контекст */ break; case «scheduled»: /* лимитка ждёт цену — есть priceOpen, scheduledAt */ break; case «opened»: /* только что вошли — есть entry, но нет closeReason */ break; case «active»: /* позиция живёт — pnl, peakProfit, maxDrawdown */ break; case «closed»: /* вышли — closeReason и финальный pnl; live-полей нет */ break; } });

Полноценный trading engine для экосистемы NodeJS

Прочитать «живой PnL» у закрытой позиции — это не баг, который ловят в QA, это строка, которая не скомпилируется. Система моделирует именно trade lifecycle — entry, partial exit, DCA-докупки, активация, отмена, длительность — а не арифметику «цена входа минус цена выхода».

Полноценный trading engine для экосистемы NodeJS

Портфель закрытых сигналов

Stateful execution и восстановление состояния

Live-engine сохраняет состояние pending signal и persisted signal state атомарно (запись во временный файл + rename) и после перезапуска продолжает с последней консистентной точки. Убили процесс во время выставления ордера — внутреннее состояние не изменилось, повтор на следующем тике. Пропала сеть — ретрай. Отключили питание во время записи — восстановление с последней атомарной записи:

listenSignalLive(async (event) => { if (event.action === «closed») { await Live.dump(event.symbol, event.strategyName); // атомарный снапшот на диск await Partial.dump(event.symbol, event.strategyName); } if (event.action === «scheduled» || event.action === «cancelled») { await Schedule.dump(event.symbol, event.strategyName); } });

Полноценный trading engine для экосистемы NodeJS

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

Persistence — часть runtime-архитектуры

Персистентность здесь — не «сохранить результаты теста в CSV». Это пятнадцать с лишним отдельных контрактов IPersist*Instance: свечи, сигналы, расписания, риск-состояние, partial-закрытия, breakeven, интервалы, measures, логи, память LLM-агента, сессии. Каждый заменяется собственным адаптером, а переезд с файлов на продакшен-бэкенд — один вызов:

// config/setup.config.ts — грузится один раз до первого обращения к персистентности import { setup } from «@backtest-kit/mongo»; // или @backtest-kit/pg, @backtest-kit/minio setup(); // все 16 контрактов переехали на MongoDB + Redis O(1)-кэш. // В коде стратегии не изменилось ничего.

Полноценный trading engine для экосистемы NodeJS

@backtest-kit/pg (PostgreSQL + Pgpool-II) даёт ~4× ускорение чтений за счёт read-реплик, @backtest-kit/minio — S3 как source of truth с Redis-индексом времени. Выбор бэкенда — вопрос инфраструктуры, не trading-логики.

Настоящий event-driven execution layer

Engine работает через события и типизированные контракты, а не через одну функцию runBacktest(). Есть streaming progress, completion events, validation notifications и целое семейство lifecycle-событий с Once и PerSignal вариантами. Это программная модель React для трейдинга: getSignal — чистая render-функция, listen* — слой эффектов, а цикл принадлежит движку. Поведение собирается добавлением независимых обработчиков, а не редактированием цикла:

// Эффект №1: трейлинг-тейк — закрыть при откате 1% от пикового профита listenActivePing(async ({ symbol }) => { if (await getPositionPnlPercent(symbol) < 0) return; if (await getPositionHighestProfitDistancePnlPercentage(symbol) < 1.0) return; await commitClosePending(symbol, { id: «trailing», note: «# Закрыто по trailing take» }); }); // Эффект №2: peak staleness — пик был давно, движения нет, выходим listenActivePing(async ({ symbol }) => { if (await getPositionHighestPnlPercentage(symbol) < 1.0) return; if (await getPositionHighestProfitMinutes(symbol) < 240) return; await commitClosePending(symbol, { id: «staleness», note: «# Закрыто по peak staleness» }); }); // Эффект №3: единая точка для всех ошибок listenError((error) => Log.debug(«error», { message: getErrorMessage(error) }));

Полноценный trading engine для экосистемы NodeJS

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

Полноценный trading engine для экосистемы NodeJS

Лента событий жизненного цикла

Отдельный runtime для Live

Live execution — это бесконечный async-цикл с троттлингом сигналов по интервалу, persisted state, отложенными сигналами и graceful shutdown. Live.background() при остановке не убивает процесс — он ждёт, пока открытая позиция дойдёт до состояния closed, и только потом отдаёт done event. Деплой никогда не обрывает живую сделку посередине. Это характеристики production runtime, а не аналитического инструмента.

Backtest — режим исполнения engine, а не его суть

Исторические данные просто становятся источником market events. Механизм стратегии, execution context, валидация, риск-слой — всё остаётся тем же. Поэтому кодовая база не распадается на «backtest implementation» и «production implementation» с их неизбежным дрейфом.

Полноценный execution context

Стратегия работает не с голыми свечами, переданными массивом. Через AsyncLocalStorage сквозь всю цепочку await’ов (включая Promise.all) течёт ambient-контекст: какая стратегия, какая биржа, какой символ, и главное — «сейчас». У getCandles нет параметра timestamp, который можно забыть:

getSignal: async (symbol) => { // Ни одного таймстемпа. Контекст течёт даже сквозь Promise.all — // все четыре таймфрейма автоматически приколочены к одному тику. const [c1h, c15m, c5m, c1m] = await Promise.all([ getCandles(symbol, «1h», 24), getCandles(symbol, «15m», 48), getCandles(symbol, «5m», 60), getCandles(symbol, «1m», 60), ]); // Свечу из будущего получить нельзя: движок отдаёт только [since, now), // а ещё не закрытая текущая свеча исключена — её полусырой OHLC // не отравит индикаторы. };

Полноценный trading engine для экосистемы NodeJS

Look-ahead bias устранён не дисциплиной, а отсутствием поверхности для ошибки.

Управление ордерами и позицией на уровне домена

Partial exits, DCA, average buy, отложенная активация по цене, trailing stop/take, breakeven, profit-lock — система описывает не «купили -> продали», а реальные механики управления позицией. Вот полная DCA-лестница из боевого примера (+67.85% за апрель 2026): открыться, докупать на просадках до 10 ступеней, закрыться по blended-цели — около тридцати строк, вся опасная математика внутри движка:

// render: открыть LONG без фиксированного TP, hard stop 25% addStrategySchema({ strategyName: «apr_2026_strategy», getSignal: async (symbol, when, currentPrice) => ({ position: «long», …Position.moonbag({ position: «long», currentPrice, percentStopLoss: 25 }), minuteEstimatedTime: Infinity, cost: 100, }), }); // эффект: лестница — докупить $100 на просадке вне ±1–5% коридора от входов listenActivePing(async ({ symbol, currentPrice }) => { if ((await getPositionEntries(symbol)).length >= 10) return; if (await getPositionEntryOverlap(symbol, currentPrice, { upperPercent: 5, lowerPercent: 1 })) return; await commitAverageBuy(symbol, 100); // докупка ВЫШЕ эффективной цены будет отклонена движком }); // эффект: выход, когда blended PnL всей лестницы дошёл до +3% listenActivePing(async ({ symbol }) => { if (await getPositionPnlPercent(symbol) < 3) return; await commitClosePending(symbol, { id: «target», note: «# Закрыто по target pnl» }); });

Полноценный trading engine для экосистемы NodeJS

Ключевая строка — комментарий к commitAverageBuy: усреднение вверх структурно запрещено. Эффективная цена считается как cost-weighted harmonic mean (корректно для входов фиксированной суммой), каждый partial снапшотит свой cost basis, и итоговый PnL сходится при перекрёстной проверке двумя независимыми способами.

Полноценный trading engine для экосистемы NodeJS

Детали позиции и manual control

Risk layer как часть архитектуры

Риск-профили регистрируются и валидируются отдельным сервисом, причём на уровне портфеля: ClientRisk видит все открытые позиции всех стратегий сразу, а checkSignalAndReserve атомарно резервирует слот между «проверили» и «ордер ушёл»:

addRiskSchema({ riskName: «demo», validations: [ // TP не ближе 1% — иначе комиссии съедят сделку ({ pendingSignal, currentPrice }) => { const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal; const tp = position === «long» ? ((priceTakeProfit — priceOpen) / priceOpen) * 100 : ((priceOpen — priceTakeProfit) / priceOpen) * 100; if (tp < 1) throw new Error(`TP слишком близко: ${tp.toFixed(2)}%`); }, // Risk/Reward не хуже 2:1 ({ pendingSignal, currentPrice }) => { const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal; const reward = position === «long» ? priceTakeProfit — priceOpen : priceOpen — priceTakeProfit; const risk = position === «long» ? priceOpen — priceStopLoss : priceStopLoss — priceOpen; if (reward / risk < 2) throw new Error(«Плохой R/R»); }, ], }); // Каждое отклонение — событие, а не молчаливый пропуск listenRisk(async (event) => { await Risk.dump(event.symbol, event.strategyName); });

Полноценный trading engine для экосистемы NodeJS

Десять стратегий, каждая «рискует 10%», больше не означают счёт под 100% экспозиции. Risk management — часть engine architecture, а не внешний скрипт вокруг прогона.

Cron — инфраструктура, а не if в цикле

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

Cron.register({ name: «tg-parser», interval: «1h», // глобально, раз в час handler: async ({ when }) => { await parseTelegramSignals(when); } }); Cron.register({ name: «funding», interval: «1h», // fan-out по символам symbols: [«BTCUSDT», «ETHUSDT»], handler: async ({ symbol, when }) => { await fetchFundingRate(symbol, when); } }); Cron.enable(); // один вызов — каждый тик движка дальше форвардится сам

Полноценный trading engine для экосистемы NodeJS

В live тот же API крутит реальный re-polling; параллельные бектесты на одной границе координируются mutex-семантикой — одна граница никогда не сработает дважды.

Market-data infrastructure

Свечи не грузятся одним массивом для расчёта индикатора. Есть candle persistence (каждая свеча — иммутабельная запись, first write wins), прогрев кэша, проверка полноты данных, дедупликация запросов (девять стратегий, запросивших одну свечу BTCUSDT 1m, порождают один запрос к бирже, а не девять) и TimeMetaService, отслеживающий актуальность market data между тиками:

for (const symbol of [«BTCUSDT», «ETHUSDT», «SOLUSDT», «BNBUSDT», «XRPUSDT»]) { await warmCandles({ exchangeName: «binance», interval: «1m», symbol, from: new Date(«2026-02-01T00:00:00Z»), to: new Date(«2026-02-28T23:59:59Z») }); Backtest.background(symbol, { strategyName, exchangeName: «binance», frameName: «feb-2026» }); } // Пять символов параллельно в одном процессе Node — без форков и IPC

Полноценный trading engine для экосистемы NodeJS

Результат на обычном ноутбуке: девять символов параллельно, ~703× реального времени на символ, ~6300× суммарно.

Research/optimization layer — поверх engine, а не вместо него

Walker (A/B-сравнение стратегий на одной истории с ранжированным отчётом) и Sweep (grid-перебор параметров по фиду краудсорсных торговых идей с грейдингом авторов) — не сам engine. Они его потребители:

# Прогнать три варианта стратегии по одной истории и получить ранжированный отчёт npx @backtest-kit/cli —walker —symbol BTCUSDT —markdown —output feb_comparison ./content/feb_v1.strategy.ts ./content/feb_v2.strategy.ts ./content/feb_v3.strategy.ts # -> ./dump/feb_comparison.md

Полноценный trading engine для экосистемы NodeJS

Research-инфраструктура построена над execution model — ровно так, как это устроено во взрослых торговых системах.

Полноценный trading engine для экосистемы NodeJS

Heatmap производительности по символам

Schema/registry architecture

Strategy, Exchange, Frame, Risk, Sizing, Walker, Sweep, Action, MCP — всё регистрируется через schema-сервисы с shallow-валидацией и возможностью частичного override*. Engine — платформа, на которой живут разные стратегии и execution-модули. Биржа подключается одной схемой — хоть CCXT/Binance, хоть MongoDB с распарсенными сделками региональной биржи, которой нет на TradingView:

addExchangeSchema({ exchangeName: «mongo-exchange», // Узбекская фондовая биржа, Монголия, Бангладеш — что угодно getCandles: async (symbol, interval, since, limit) => CandleModel.find({ symbol, interval, timestamp: { $gte: since.getTime() } }) .sort({ timestamp: 1 }).limit(limit).lean(), });

Полноценный trading engine для экосистемы NodeJS

Graceful shutdown

При остановке live runtime система не убивает процесс: дожидается завершения последнего действия, доводит позицию до терминального состояния и только после этого генерирует done event. Первый Ctrl+C в CLI останавливает все активные прогоны через штатный stop, второй — force-quit. Мелкая деталь, по которой безошибочно отличаешь production runtime от аналитической тулзы.

Streaming execution

Async-генераторы здесь — не украшение API. Один engine, два способа потребления — по задаче, а не по возможностям:

// Event-driven — для production-ботов и мониторинга Backtest.background(«BTCUSDT», config); listenSignalBacktest((e) => { /* … */ }); // Async-итератор — для research, скриптов и LLM-агентов for await (const event of Backtest.run(«BTCUSDT», config)) { // signal | progress | done — по мере работы движка, без аккумуляции в памяти, // с возможностью break — ранней остановки прогона }

Полноценный trading engine для экосистемы NodeJS

Именно на этом построен веб-дашборд @backtest-kit/ui: живые графики с оверлеями сигналов, KPI-борд, heatmap по портфелю, лента нотификаций и ручное управление позицией — кнопки Manual Control дёргают те же broker-хуки, что и стратегия.

Полноценный trading engine для экосистемы NodeJS

KPI-дашборд

Инфраструктура заменяется без переписывания trading logic

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

Broker.useBrokerAdapter(class implements Partial<IBroker> { async onOrderOpenCommit({ symbol, cost, priceOpen, priceTakeProfit, priceStopLoss }) { const ex = await getExchange(); const qty = truncateQty(ex, symbol, cost / priceOpen); await createLimitOrderAndWait(ex, symbol, «buy», qty, priceOpen); // не заполнился -> throw -> ретрай await ex.createOrder(symbol, «limit», «sell», qty, priceTakeProfit); // защита сразу после входа await createStopLossOrder(ex, symbol, qty, priceStopLoss); } // onOrderCloseCommit · onPartialProfitCommit · onTrailingStopCommit // onBreakevenCommit · onAverageBuyCommit — все хуки опциональны }); Broker.enable(); // в backtest-режиме адаптер не вызывается вообще

Полноценный trading engine для экосистемы NodeJS

Намерение адаптера декларируется тремя типизированными ошибками: OrderTransientError («временно, повтори»), OrderRejectedError («биржа отказала окончательно, повторять бессмысленно»), OrderDeletedError («ордера больше нет — позицию закрыть, лимитку отменить»). Счётчики повторов ограничены и переживают краш процесса.

Попробовать

# Проект с примером стратегии npx @backtest-kit/cli —init —output my-trading-bot cd my-trading-bot && npm install && npm start # Или «eject» — вся обвязка редактируемым кодом в вашем репозитории npx -y @backtest-kit/sidekick my-trading-bot

Полноценный trading engine для экосистемы NodeJS

Всё под MIT, ядро не имеет жёстких зависимостей от дополнительных пакетов, персистентность — обычные файлы или ваша собственная БД. В репозитории — девять production-quality примеров с записанными цифрами: от нейросети на TensorFlow и Python-индикатора через WASM до инверсии сигналов телеграм-канала (Sharpe 1.14) и DCA-лестниц — каждый с честным описанием рисков и трейд-логом.

Ссылки:

  • GitHub: https://github.com/tripolskypetr/backtest-kit

  • npm: https://www.npmjs.com/package/backtest-kit

  • Документация: https://backtest-kit.github.io

  • Примеры стратегий: https://github.com/tripolskypetr/backtest-kit/tree/master/example

Источник