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 uses Node.js AsyncLocalStorage to maintain a temporal execution context throughout the entire async call stack. Every function that runs inside a strategy tick — regardless of how deeply nested — automatically knows the current simulation timestamp, the active symbol, and whether the engine is in backtest or live mode. No parameter threading is required. The same strategy code runs identically in both environments.

What the Execution Context Contains

The execution context is a small, immutable record injected at the start of every tick:
interface IExecutionContext {
  symbol: string;   // Trading pair being processed (e.g. "BTCUSDT")
  when: Date;       // Current simulation time (backtest) or real wall-clock time (live)
  backtest: boolean; // true in backtest mode, false in live mode
}

symbol

The trading pair currently being processed. When running multiple symbols in parallel, each async execution chain receives its own isolated context — there is no shared mutable state between symbols.

when

The authoritative timestamp for the current tick. In backtest mode this is the simulation time from the historical frame. In live mode it is the wall-clock new Date() captured at the start of the tick. All candle and data queries are resolved relative to this value.

backtest

A boolean flag that lets library internals and strategy helpers distinguish execution mode. Risk functions, persistence adapters, and candle fetchers all branch on this value to apply the correct behavior.

The Look-Ahead Bias Problem

Look-ahead bias is one of the most common and difficult-to-detect errors in backtesting systems. It occurs when a strategy accidentally uses data that would not have been available at the time of the decision — for example, calling new Date() to determine “now” while simulating a trade from January. In most frameworks, developers are expected to pass a timestamp parameter through every function call. This is error-prone: a single missed pass, a helper that ignores the parameter, or a third-party utility that calls Date.now() internally is enough to contaminate results. Backtest Kit eliminates this failure mode at the infrastructure level. AsyncLocalStorage makes the when timestamp implicit. Every call to getCandles, getAveragePrice, getOrderBook, or any other time-sensitive API reads when from the context automatically — the developer never has a parameter to forget.

Automatic Temporal Resolution

The same getCandles call produces correct results in both modes without any code change:
// This works identically in backtest and live
const candles = await getCandles(symbol, '1h', 24);
// In backtest: returns 24 candles ending at simulation time
// In live: returns 24 candles ending at current real time
Internally, getCandles reads when from AsyncLocalStorage, aligns it to the requested interval boundary, computes since = alignedWhen - limit * stepMs, and fetches exactly limit closed candles. The caller never touches a timestamp.

Candle Timestamp Alignment

All candle queries align the when timestamp down to the nearest interval boundary before computing the fetch range. This prevents partial candles from contaminating indicator calculations.
when     = 00:17:00  (current tick time)
interval = 15m
stepMs   = 900000ms

alignedWhen = Math.floor(when / stepMs) * stepMs
            = 00:15:00

since = alignedWhen - limit * stepMs
The candle that opens at alignedWhen (00:15:00 in this example) is excluded. At tick time 00:17:00, the 00:15:00 candle covers the period [00:15, 00:30) and is still open — its high, low, and close values are incomplete. Including it would mean the strategy sees partial OHLCV data that doesn’t reflect final price action for that period. Only fully closed candles are returned. This rule is enforced identically for getCandles, getRawCandles, and all cache reads and writes.
Never use new Date() directly inside a strategy or any helper it calls. In backtest mode, new Date() returns the real wall-clock time — not the simulation timestamp — which will silently introduce look-ahead bias into every indicator and signal you compute. Always use the when value from the execution context, which is available via any framework data-access function.

Multi-Timeframe Synchronization

Because all intervals are aligned relative to the same when value, multi-timeframe queries are automatically synchronized. Requesting 1h, 15m, and 5m candles in the same tick produces three arrays that are all anchored to the same logical point in time, with no off-by-one errors between timeframes.
addStrategySchema({
  strategyName: 'multi-tf-strategy',
  interval: '5m',
  riskName: 'demo',
  getSignal: async (symbol) => {
    // All three queries are time-synchronized automatically
    const candles1h  = await getCandles(symbol, '1h', 24);
    const candles15m = await getCandles(symbol, '15m', 48);
    const candles5m  = await getCandles(symbol, '5m', 60);

    // candles1h[-1].timestamp  ≤ when (aligned to 1h boundary)
    // candles15m[-1].timestamp ≤ when (aligned to 15m boundary)
    // candles5m[-1].timestamp  ≤ when (aligned to 5m boundary)
    // No look-ahead, no misalignment between timeframes
  },
});
This synchronization extends to every data source the framework provides — order book snapshots, aggregated trades, and VWAP calculations all use the same when-aligned temporal reference.

Context Propagation in Parallel Backtests

When running multiple symbols concurrently with Backtest.background, each symbol operates in its own isolated AsyncLocalStorage context. The engine wraps each tick in ExecutionContextService.runInContext, scoping { symbol, when, backtest } to that specific async execution chain. Parallel symbol backtests cannot read or overwrite each other’s context.
// These run in parallel — each has its own isolated execution context
Backtest.background('BTCUSDT', config);
Backtest.background('ETHUSDT', config);
Backtest.background('SOLUSDT', config);

// Inside getSignal for BTCUSDT, symbol === 'BTCUSDT' and when === BTCUSDT simulation time
// Inside getSignal for ETHUSDT, symbol === 'ETHUSDT' and when === ETHUSDT simulation time
// No cross-contamination is possible
The nested context hierarchy — ExecutionContextService wrapping MethodContextService — ensures that schema lookups (strategy name, exchange name, frame name) are also scoped correctly per execution chain.

Build docs developers (and LLMs) love