Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt

Use this file to discover all available pages before exploring further.

Backtest Kit is designed to run many symbols concurrently in a single Node.js process. All Backtest.background() contexts share one event loop, one MongoDB connection pool, and one Redis pool. There is no inter-process communication, no subprocess fork overhead, and no per-symbol OS thread. The cooperative event loop and Redis O(1) lookup cache allow parallel replays to advance nearly as fast as individual ones — with measured throughput of ~6,300× real-time aggregate speed across 9 symbols on commodity developer hardware.

Measured Performance

The numbers below were captured from backtest-monorepo-parallel running the apr_2026 DCA ladder strategy across 9 symbols on a mid-range laptop (HP Victus 15, i5-13420H, 16 GB DDR4-3200, NVMe SSD, Mongo + Redis on localhost).
MetricValue
Symbols in parallel9 (BTC, POL, ZEC, HYPE, XAUT, DOGE, SOL, PENGU, HBAR)
Historical window27 days of 1m candles per symbol
Wall-clock duration~2.9 seconds for a 34-minute slice
Per-symbol replay speed~703× real-time
Aggregate replay speed~6,326× real-time (9 × 703)
Event throughput~103 events/sec (one Node process)
Total candle ticks~350,000 (38,880 × 9 symbols)

Why it’s fast

  1. Single-process concurrency. All 9 Backtest.background() contexts share one event loop, one Mongo pool, and one Redis pool. No IPC, no fork overhead.
  2. Redis O(1) cache. Every findByContext(...) is a Redis GET before falling back to Mongo. Cold miss fills Redis; all subsequent ticks hit the cache directly.
  3. Atomic upserts. Every write is a single findOneAndUpdate with upsert: true — no read-modify-write, no application-side locks, no E11000 retry loop under concurrent writes.
  4. Pre-warmed candle cache. The --cache flag pre-fetches all OHLCV data into Mongo before the runners start, so the inner loop never blocks on CCXT HTTP.
  5. JIT-friendly hot path. The per-tick body is synchronous arithmetic plus a few awaited helpers — V8 inlines aggressively and the tight loop reaches full JIT speed quickly.

Fan-Out: Running Multiple Symbols

Call Backtest.background() in a loop. Each call returns independently; all symbols advance in parallel through the shared event loop.
import { Backtest, listenDoneBacktest } from 'backtest-kit';

const symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'];

for (const symbol of symbols) {
  Backtest.background(symbol, {
    strategyName: 'my-strategy',
    exchangeName: 'binance',
    frameName:    'feb-2026',
  });
}

listenDoneBacktest(async (event) => {
  console.log(`Completed: ${event.symbol}`);
  await Backtest.dump(event.strategyName);
});
listenDoneBacktest fires once per symbol as each replay completes. Use it to generate per-symbol reports independently without waiting for the whole fan-out to finish.

State Isolation

Despite sharing all infrastructure, every symbol’s position state is completely independent. The framework keys every persistence operation on (symbol, strategyName, exchangeName) — a signal open for BTCUSDT cannot interfere with ETHUSDT’s state in any way.
// These run in the same Node process and event loop,
// but their position state is fully isolated.
Backtest.background('BTCUSDT', config);
Backtest.background('ETHUSDT', config);
Backtest.background('SOLUSDT', config);

Cron Coordination Across Parallel Backtests

The built-in Cron scheduler is aware of parallel replays. A global job (no symbols argument) fires exactly once per virtual boundary across all running symbols — the first symbol to reach the boundary opens the slot; all others await the same promise and release together. This prevents double-fires of expensive operations like news fetches or model warm-ups.
import { Cron, Backtest } from 'backtest-kit';

// Global hourly job — fires ONCE per virtual hour, not once per symbol
Cron.register({
  name:    'fetch-news',
  interval: '1h',
  handler: async ({ symbol, when }) => {
    await fetchNewsFromAPI(when);
  },
});

// Per-symbol fan-out — fires once per hour PER symbol
Cron.register({
  name:     'fetch-funding',
  interval: '1h',
  symbols:  ['BTCUSDT', 'ETHUSDT'],
  handler:  async ({ symbol, when }) => {
    await fetchFundingRate(symbol, when);
  },
});

// Wire Cron to the engine once at startup
Cron.enable();

for (const symbol of ['BTCUSDT', 'ETHUSDT', 'SOLUSDT']) {
  Backtest.background(symbol, { strategyName, exchangeName, frameName });
}

CLI Fan-Out with --entry

For power-user setups, the --entry flag activates a parallel runner mode in @backtest-kit/cli that reads a CC_SYMBOL_LIST environment variable and calls Backtest.background() for every symbol automatically:
# Run 9 symbols in parallel, warm candle cache, launch web UI
npm run start -- --backtest --entry --ui --cache ./content/apr_2026.strategy.ts
CC_SYMBOL_LIST=BTCUSDT,POLUSDT,ZECUSDT,HYPEUSDT,XAUTUSDT,DOGEUSDT,SOLUSDT,PENGUUSDT,HBARUSDT
Without --entry, the CLI runs a standard single-strategy backtest using the exchange and frame registered inside the strategy file.

A/B Testing Strategy Variants in Parallel

Running multiple strategy variants side by side is identical to running multiple symbols — use distinct strategyName values:
import { Backtest, listenDoneBacktest } from 'backtest-kit';

const symbols   = ['BTCUSDT', 'ETHUSDT'];
const variants  = ['strategy-v1', 'strategy-v2', 'strategy-v3'];

for (const symbol of symbols) {
  for (const strategyName of variants) {
    Backtest.background(symbol, { strategyName, exchangeName: 'binance', frameName: 'q1-2026' });
  }
}

listenDoneBacktest(async ({ symbol, strategyName }) => {
  await Backtest.dump(strategyName);
});
Each (symbol, strategyName) pair is fully isolated. Reports are written to ./dump/backtest/{strategyName}.md and can be compared programmatically with Backtest.getData(strategyName).

Portfolio Heatmap and Pooled Metrics

When multiple symbols complete, Backtest.getData() and Backtest.getReport() aggregate cross-symbol metrics:
MetricDescription
Pooled SharpeSharpe computed over the combined PNL distribution of all symbols
Sortino RatioDownside-risk-adjusted return across the portfolio
Calmar RatioAnnualised return divided by portfolio maximum drawdown
Recovery FactorTotal portfolio PNL divided by maximum drawdown
ExpectancyExpected value per trade averaged across all symbols
These cross-symbol metrics give you a portfolio-level view that single-symbol Sharpe ratios cannot reveal — a strategy that looks strong on BTCUSDT alone may have poor correlation properties when paired with altcoins.

Community: backtest-monorepo-parallel

The backtest-monorepo-parallel community project is a TypeScript monorepo template that runs 9 symbols in parallel with shared Mongo + Redis infrastructure and a self-enforcement runtime that exposes the workspace DI container to strategy files without imports, bundler hooks, or strategy-author changes.It is the reference implementation for:
  • Per-subsystem persistence mode switching (Mongo for live, memory for backtest)
  • DI container pattern for shared services between strategy files
  • --entry flag usage with CC_SYMBOL_LIST fan-out
  • Mode A (parallel runner) vs Mode B (single CLI) entry point separation
git clone https://github.com/backtest-kit/backtest-monorepo-parallel.git

Resource Guidance

RAM

Budget ~50–100 MB per symbol for candle cache and signal state. 9 symbols on 16 GB RAM leaves ample headroom for Mongo and Redis.

Mongo + Redis

Run both on the same machine as the Node process for lowest latency. Use docker-compose for a one-command local setup during development.

Candle pre-warming

Use --cache to pre-fetch all OHLCV data before the runners start. Without pre-warming, the first pass blocks on CCXT HTTP and is 10–50× slower.

Symbol count

9–15 symbols is a practical upper bound for a single Node process on commodity hardware. Above that, split into multiple processes with separate CC_SYMBOL_LIST slices.

Build docs developers (and LLMs) love