Skip to main content

Documentation Index

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

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

A backtest replays your strategy against historical candle data by iterating through a generated timestamp array and calling your getSignal function at each tick. When a signal opens, the engine fetches the next minuteEstimatedTime candles and runs a fast VWAP simulation to find when TP, SL, or time expiry triggers. The closed result — with full PNL, peak profit, and max drawdown — is then yielded and the engine advances past the signal’s close timestamp.

Two Execution Modes

Backtest Kit exposes the same backtest runtime in two forms. Both produce identical results; only the consumption model differs.

Event-Driven: Backtest.background

Backtest.background fires and forgets — it starts the backtest in the background and routes events to globally registered listeners. Use it in production bots and monitoring scripts where you don’t want to block a for await loop.
import { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';

Backtest.background('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
  frameName:    '1d-test',
});

listenSignalBacktest((event) => {
  if (event.action === 'closed') {
    console.log(`${event.closeReason}: ${event.pnl.pnlPercentage.toFixed(2)}%`);
  }
});

listenDoneBacktest(async (event) => {
  console.log(`Backtest complete for ${event.symbol}`);
  await Backtest.dump(event.symbol, {
    strategyName: event.strategyName,
    exchangeName: event.exchangeName,
    frameName:    event.frameName,
  });
});
Backtest.background returns a cancellation function. Call it to stop the engine at the next safe point (idle state or after the current signal closes):
const stop = Backtest.background('BTCUSDT', context);
// ...later
stop();

Async Iterator: Backtest.run

Backtest.run returns an AsyncIterableIterator. Use it for research scripts, unit tests, and LLM agent loops where you want explicit control over each result.
for await (const result of Backtest.run('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
  frameName:    '1d-test',
})) {
  if (result.action === 'closed') {
    console.log(result.pnl.pnlPercentage, result.closeReason);
    // Break early to stop the backtest before exhausting all timeframes
    if (result.pnl.pnlPercentage < -20) break;
  }
}
Use break to terminate the generator early. The engine cleans up resources in the generator’s finally block, so no memory leaks occur.

Multi-Symbol Parallel Backtesting

Run multiple symbols in parallel by calling Backtest.background (or Backtest.run) for each symbol. All runs share the same registered schemas and event listeners. The engine uses Lookup.isParallel to coordinate cooperative event-loop yielding between parallel candle fetches.
import { Backtest, listenDoneBacktest } from 'backtest-kit';

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

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

listenDoneBacktest(async (event) => {
  console.log(`${event.symbol} done`);
  await Backtest.dump(event.symbol, {
    strategyName: event.strategyName,
    exchangeName: event.exchangeName,
    frameName:    event.frameName,
  });
});
For true parallelism at scale (9+ symbols), see the backtest-monorepo-parallel community template, which achieves approximately 6300× real-time aggregate speed on a commodity laptop.

Generating Reports

After signals accumulate (either via listenSignalBacktest or by completing a for await loop), generate and save reports using the Backtest facade.

Markdown Report to Disk

// Save to ./dump/backtest/{symbol}_{strategyName}_{exchangeName}_{frameName}_backtest-{timestamp}.md
await Backtest.dump('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
  frameName:    '1d-test',
});

// Or save to a custom directory
await Backtest.dump('BTCUSDT', context, './reports/backtests');

In-Memory Statistics

const stats = await Backtest.getData('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
  frameName:    '1d-test',
});

console.log(`Total signals:     ${stats.totalSignals}`);
console.log(`Win rate:          ${stats.winRate?.toFixed(1)}%`);
console.log(`Sharpe Ratio:      ${stats.sharpeRatio?.toFixed(3)}`);
console.log(`Sortino Ratio:     ${stats.sortinoRatio?.toFixed(3)}`);
console.log(`Max Drawdown:      ${stats.avgFallPnl?.toFixed(2)}%`);
console.log(`Expected Yearly:   ${stats.expectedYearlyReturns?.toFixed(1)}%`);
The BacktestStatisticsModel includes: totalSignals, winCount, lossCount, winRate, avgPnl, totalPnl, stdDev, sharpeRatio, annualizedSharpeRatio, certaintyRatio, expectedYearlyReturns, avgPeakPnl, avgFallPnl, sortinoRatio, calmarRatio, and recoveryFactor.

Raw Markdown String

const markdown = await Backtest.getReport('BTCUSDT', context);
// Write to file, send to Telegram, or display in a dashboard
await fs.writeFile('./report.md', markdown);

Performance Tracking

Every backtest run emits timing metrics via performanceEmitter. Subscribe to identify bottlenecks:
import { listenPerformance } from 'backtest-kit';

listenPerformance((event) => {
  console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
});
Metric types: backtest_total, backtest_timeframe, backtest_signal. Use Performance.dump(symbol, context) to save a detailed Markdown performance report.

Candle Caching

Fetching candles over the network for every tick is slow. Use cacheCandles or warmCandles to pre-populate the local cache before running:
import { cacheCandles, Backtest } from 'backtest-kit';

// Download and cache all 1-minute candles for the backtest window
await cacheCandles({
  symbol:       'BTCUSDT',
  exchangeName: 'binance',
  interval:     '1m',
  from:         new Date('2025-12-01'),
  to:           new Date('2025-12-02'),
});

// Now the backtest reads from disk — no network calls
Backtest.background('BTCUSDT', context);
cacheCandles runs a check-then-warm pipeline with one retry: it validates the cache first and, on a miss, downloads the missing data and re-validates.
Candle timestamps use Unix epoch in UTC. All interval boundaries are aligned downward (e.g. for 4h, boundaries are 00:00, 04:00, 08:00 UTC). If your local timezone offset is not a multiple of the interval, logged timestamps may appear uneven — use toUTCString() or toISOString() for display.

Build docs developers (and LLMs) love