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 exposes the same simulation engine through two complementary execution models: a background (event-driven) model where Backtest.background() fires without blocking your process and emits results through event listeners, and an async iterator (pull-based) model where you for await over Backtest.run() and process each event inline. Both models share identical engine guarantees — they differ only in how you consume results, making it straightforward to switch between them as your workflow evolves.

Execution Models

Background Execution

Non-blocking. Best for production bots, multi-symbol fan-out, and monitoring dashboards. listenDoneBacktest triggers report generation when complete.

Async Iterator

Pull-based. Best for research scripts, testing pipelines, and LLM agent workflows where you want direct control over the event loop.

Background Execution Model

The background model runs the full historical replay without blocking the calling process. Register your listeners before calling Backtest.background() to ensure no events are missed.
import { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';

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

listenSignalBacktest((event) => console.log(event));

listenDoneBacktest(async (event) => {
  await Backtest.dump(event.strategyName);
});
listenDoneBacktest fires once per symbol when all timeframes in the frame have been replayed. Use it to generate and persist the Markdown report.

Async Iterator Model

The async iterator model yields each closed signal event as it is produced. Every yielded event has action === 'closed' and includes the full PNL and close-reason data for that signal.
for await (const event of Backtest.run('BTCUSDT', config)) {
  // event.action === 'closed' — only closed signal results are yielded
  console.log(event.action, event.pnl);
}
// After the loop completes (all timeframes processed), generate the report:
await Backtest.dump(config.strategyName);
for await supports early termination via break. The iterator is not reusable — create a new Backtest.run() call for each replay.

Registering Components

Before a backtest can run, you need to register three schemas: an exchange (data source), a frame (time window), and a strategy (signal logic). A risk schema is optional but strongly recommended.
1

Add a frame schema

A frame defines the historical window, the tick interval, and the candle resolution the engine replays.
import { addFrameSchema } from 'backtest-kit';

addFrameSchema({
  frameName: '1d-test',
  interval: '1m',          // tick resolution (1-minute candles)
  startDate: new Date('2025-12-01'),
  endDate:   new Date('2025-12-02'),
});
The engine iterates over every interval boundary between startDate and endDate, calling your strategy’s getSignal at the configured strategy interval.
2

Add an exchange schema

An exchange schema wraps any CCXT-compatible data source. The minimal implementation for Binance via CCXT looks like this:
import ccxt from 'ccxt';
import { addExchangeSchema } from 'backtest-kit';

addExchangeSchema({
  exchangeName: 'binance',
  getCandles: async (symbol, interval, since, limit) => {
    const exchange = new ccxt.binance();
    const ohlcv = await exchange.fetchOHLCV(
      symbol,
      interval,
      since.getTime(),
      limit
    );
    return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
      timestamp, open, high, low, close, volume,
    }));
  },
  formatPrice:    (symbol, price)    => price.toFixed(2),
  formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
The adapter contract requires that the first returned candle’s timestamp equals the aligned since value, and that exactly limit candles are returned in sequential order.
3

Add a risk schema

Risk validations run before any signal is accepted. They throw to reject a signal and silently pass to allow it.
import { addRiskSchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'demo',
  validations: [
    // TP at least 1%
    ({ pendingSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal;
      const tpDistance = position === 'long'
        ? ((priceTakeProfit - priceOpen) / priceOpen) * 100
        : ((priceOpen - priceTakeProfit) / priceOpen) * 100;
      if (tpDistance < 1)
        throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);
    },
    // R/R at least 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('Poor R/R ratio');
    },
  ],
});
4

Add a strategy schema

A strategy schema defines the interval at which getSignal is called, which risk profile to apply, and the signal-generation function itself.
import { addStrategySchema } from 'backtest-kit';

addStrategySchema({
  strategyName: 'llm-strategy',
  interval: '5m',
  riskName: 'demo',
  getSignal: async (symbol) => {
    // return { position, priceOpen, priceTakeProfit, priceStopLoss, minuteEstimatedTime, id }
    // return null to skip
  },
});

VWAP Pricing and Realistic Fills

Backtest Kit uses VWAP (Volume Weighted Average Price) computed from the last 5 one-minute candles for all entry and exit decisions, rather than the raw candle close price. This models the realistic slippage of a market order that fills across multiple price levels.
typicalPrice = (high + low + close) / 3
vwap         = Σ(typicalPrice × volume) / Σ(volume)   // over last 5 × 1m candles
The number of candles used for the VWAP window is configurable:
import { setConfig } from 'backtest-kit';

setConfig({
  CC_AVG_PRICE_CANDLES_COUNT: 5,   // default: 5 one-minute candles
  CC_PERCENT_SLIPPAGE: 0.1,        // % slippage applied on top of VWAP
  CC_PERCENT_FEE: 0.1,             // % fee applied on open and close
});
OHLC path-aware exit: Backtest Kit does not close positions close-to-close. When a signal is active, the engine fetches the actual one-minute candle sequence for minuteEstimatedTime minutes and replays the OHLC path. A TP or SL is triggered at the exact candle where the VWAP crosses the level — the same way a live exchange would behave. This eliminates the look-ahead bias introduced by close-to-close exit models.

Report Generation

When a backtest completes, use the Backtest facade to produce and persist reports.
// Save a Markdown report to disk (default: ./dump/backtest/)
await Backtest.dump('llm-strategy');

// Get the Markdown report as a string
const markdown = await Backtest.getReport('llm-strategy');

// Get the raw statistics object for programmatic access
const stats = await Backtest.getData('llm-strategy');

// Clear accumulated data for a fresh run
await Backtest.clear('llm-strategy');

Performance Metrics

MetricDescription
Total PNLCumulative profit/loss across all closed signals
Win ratePercentage of trades that closed in profit
Average PNLMean PNL per closed trade
Sharpe RatioRisk-adjusted return: mean(PNL) / stdDev(PNL)
Annualized SharpeSharpe × √365
Sortino RatioDownside-risk-adjusted return (penalises only losses)
Pooled SharpeCross-symbol portfolio Sharpe for multi-symbol runs
Calmar RatioAnnualised return divided by maximum drawdown
Recovery FactorTotal PNL divided by maximum drawdown
Max DrawdownLargest peak-to-trough loss in the equity curve
ExpectancyExpected value per trade in %
Profit FactorGross profit divided by gross loss
Certainty Ratio`avgWin /avgLoss` — quality of the win/loss distribution
Standard DeviationVolatility of per-trade PNL
PNL is calculated after applying both slippage and fees to every simulated fill:
// LONG
priceOpenWithCosts  = priceOpen  × (1 + slippage + fee)
priceCloseWithCosts = priceClose × (1 - slippage - fee)
pnl%  = (priceCloseWithCosts - priceOpenWithCosts) / priceOpenWithCosts × 100

// SHORT
priceOpenWithCosts  = priceOpen  × (1 - slippage + fee)
priceCloseWithCosts = priceClose × (1 + slippage + fee)
pnl%  = (priceOpenWithCosts - priceCloseWithCosts) / priceOpenWithCosts × 100

Listening to Progress

Use listenProgress to track how far through the timeframe the replay has advanced — useful for long multi-day runs.
import { listenProgress, listenDoneBacktest } from 'backtest-kit';

listenProgress(({ symbol, percent, processed, total }) => {
  console.log(`[${symbol}] ${percent.toFixed(2)}% — ${processed}/${total}`);
});

listenDoneBacktest(async ({ symbol, strategyName }) => {
  console.log(`Backtest completed: ${symbol}`);
  await Backtest.dump(strategyName);
});

Global Configuration Reference

import { setConfig, setLogger } from 'backtest-kit';

setLogger({
  log:   console.log,
  debug: console.debug,
  info:  console.info,
  warn:  console.warn,
});

setConfig({
  CC_PERCENT_SLIPPAGE:         0.1,   // % slippage per fill
  CC_PERCENT_FEE:              0.1,   // % fee per fill
  CC_SCHEDULE_AWAIT_MINUTES:   120,   // pending signal timeout (minutes)
  CC_AVG_PRICE_CANDLES_COUNT:  5,     // VWAP window (1m candles)
});

Build docs developers (and LLMs) love