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.

Backtest Kit uses Node.js AsyncLocalStorage (via ExecutionContextService) to implicitly carry the current virtual timestamp — when — into every candle request. You never pass a timestamp yourself; the framework reads it from the async execution context and aligns it down to the nearest interval boundary before calculating the window to fetch. The same code path works unchanged in both backtest mode (where when comes from the timeframe iterator) and live mode (where when is new Date()), making look-ahead bias structurally impossible.

getCandles — historical window ending before when

const candles = await getCandles(symbol, interval, limit);
Returns exactly limit closed candles ending before the current aligned time. The pending (incomplete) candle at alignedWhen is always excluded. Timestamp math:
alignedWhen = Math.floor(when / stepMs) * stepMs
since       = alignedWhen - limit * stepMs

Range: [since, alignedWhen)   ← half-open, alignedWhen exclusive
For example, calling getCandles("BTCUSDT", "15m", 4) at when = 00:12:00:
alignedWhen = 00:00:00   (floor to 15m boundary)
since       = 23:00:00   (go back 4 × 15m = 60m)

Returned candles: 23:00, 23:15, 23:30, 23:45
The candle at 00:00:00 is NOT returned as an additional result — it is alignedWhen itself, which is exclusive. The last returned candle has timestamp = 23:45:00.

getNextCandles — forward window (backtest only)

const candles = await getNextCandles(symbol, interval, limit);
Returns limit candles starting from alignedWhen, going forward. Used internally by BacktestLogicPrivateService to simulate how a signal plays out after entry.
since = alignedWhen   (inclusive)
Range: [alignedWhen, alignedWhen + limit * stepMs)
getNextCandles throws in live mode to prevent look-ahead bias. It is only valid inside a backtest execution context.

getRawCandles — flexible combinations

const candles = await getRawCandles(symbol, interval, limit?, sDate?, eDate?);
Supports five parameter combinations. All combinations respect the execution context and validate eDate <= when to prevent look-ahead:
ParametersBehaviour
(limit)since = alignedWhen - limit * stepMs, range [since, alignedWhen)
(limit, sDate)since = align(sDate), fetch limit forward from there
(limit, undefined, eDate)since = align(eDate) - limit * stepMs, range [since, eDate)
(undefined, sDate, eDate)since = align(sDate), limit calculated from range
(limit, sDate, eDate)since = align(sDate), returns limit candles

Alignment in detail — the 15-minute example

Every request first aligns the reference timestamp down to the nearest interval boundary:
// 15-minute interval, when = 1704067920000 (00:12:00 UTC)
stepMs      = 15 * 60000               // 900 000 ms
alignedWhen = Math.floor(1704067920000 / 900000) * 900000
            = 1704067200000            // 00:00:00 UTC  ← 00:12 is between 00:00 and 00:15 boundaries

since       = 1704067200000 - 4 * 900000
            = 1704063600000            // 23:00:00 previous day

// Candles returned:
// [0] timestamp = 1704063600000  (23:00)
// [1] timestamp = 1704064500000  (23:15)
// [2] timestamp = 1704065400000  (23:30)
// [3] timestamp = 1704066300000  (23:45)
alignedWhen (00:00:00) is exclusive — the candle covering [00:00, 00:15) is still open at when = 00:12:00 and is not returned.
The timestamp field on every candle is its openTime, not closeTime. A candle with timestamp = 00:00:00 and a 15-minute step covers the period [00:00, 00:15) — it closes at 00:15.

Supported intervals

IntervalStep
"1m"60 000 ms
"3m"180 000 ms
"5m"300 000 ms
"15m"900 000 ms
"30m"1 800 000 ms
"1h"3 600 000 ms
"2h"7 200 000 ms
"4h"14 400 000 ms
"6h"21 600 000 ms
"8h"28 800 000 ms
"12h"43 200 000 ms
"1d"86 400 000 ms

Persistent cache

Backtest Kit maintains a per-symbol/per-interval/per-exchange on-disk candle cache (via PersistCandleAdapter). On every getCandles or getRawCandles call:
  1. The cache lookup computes expected timestamps: since + i * stepMs for i = 0..limit-1
  2. If all expected timestamps are present → return cached candles immediately (no API call)
  3. If any timestamp is missing → fetch from the exchange, write to cache, return fresh data
Cache entries are keyed by their openTime. Incomplete candles (where closeTime > now) are never written to cache, ensuring cache entries are always final OHLCV data.

getOrderBook — aligned time window

Order book fetching uses the same AsyncLocalStorage context but aligns to a configurable offset window rather than a candle interval:
offsetMinutes = CC_ORDER_BOOK_TIME_OFFSET_MINUTES   (default: 10)
depth         = CC_ORDER_BOOK_MAX_DEPTH_LEVELS       (default: 20)
offsetMs      = offsetMinutes * 60000

alignedTo = Math.floor(when / offsetMs) * offsetMs
to        = alignedTo
from      = alignedTo - offsetMs
Example with 10-minute offset, when = 00:12:00 UTC:
alignedTo = 00:10:00 UTC
to        = 00:10:00 UTC
from      = 00:00:00 UTC
The exchange adapter receives (symbol, depth, from, to, backtest) where depth defaults to CC_ORDER_BOOK_MAX_DEPTH_LEVELS. In live mode the from/to range is typically ignored and the adapter fetches the current order book snapshot.
Most exchanges (e.g. Binance GET /api/v3/depth) only expose the current order book with no historical query support. For backtesting you must supply your own snapshot storage and implement the getOrderBook function in your exchange schema to query it.

getAggregatedTrades — 1-minute aligned pagination

Aggregated trades always align to down to the 1-minute boundary to prevent future trades leaking in:
alignedTo = Math.floor(when / 60000) * 60000
windowMs  = CC_AGGREGATED_TRADES_MAX_MINUTES * 60000 − 60000
to        = alignedTo
from      = alignedTo − windowMs
Without limit, one full window is returned. With limit, the function paginates backwards in CC_AGGREGATED_TRADES_MAX_MINUTES chunks until at least limit trades are collected, then slices to the most recent limit. Example — 200 trades with 60-minute window, when = 00:12:00 UTC:
alignedTo = 00:12:00 UTC
windowMs  = 59 × 60000 = 59 min

Window 1: from = 23:13:00, to = 00:12:00 → 120 trades  (not enough)
Window 2: from = 22:14:00, to = 23:13:00 → 100 more → 220 total

result = last 200 of 220 (most recent)

Timezone warning

All alignment uses UTC (Unix epoch). For intervals like 4h, boundaries are 00:00, 04:00, 08:00, 12:00, 16:00, 20:00 UTC. If your local timezone offset is not a multiple of the interval step, timestamps in logs may appear uneven in local time but are perfectly aligned in UTC.
Local (UTC+5)               UTC
13:00 Sat Sep 20  →  08:00 Sat Sep 20  ✓ 4h boundary
17:00 Sat Sep 20  →  12:00 Sat Sep 20  ✓ 4h boundary
21:00 Sat Sep 20  →  16:00 Sat Sep 20  ✓ 4h boundary
05:00 Sun Sep 21  →  00:00 Sun Sep 21  ✓ 4h boundary
Use toUTCString() or toISOString() when logging timestamps to always see the actual aligned UTC time.

Build docs developers (and LLMs) love