Skip to main content

Documentation Index

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

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

backtest-kit separates market data fetching from strategy logic through a named exchange schema. You register one or more schemas at startup, each identified by an exchangeName string. Adapters and strategies reference that name rather than importing exchange clients directly, which makes it straightforward to swap exchanges or use different configurations in backtest, paper, and live modes. This project ships with a single CCXT-based Binance spot schema that covers all three modes.

What addExchangeSchema() Does

addExchangeSchema() registers a named provider for four market-data operations: historical candles, live order books, price formatting, and quantity formatting. When the runner needs candle data for a symbol, it calls the getCandles implementation you registered under the matching exchangeName. The function is idempotent within a process — registering the same exchangeName twice overwrites the first registration.

Global Configuration

Before registering the schema, call setConfig to tune framework-level constants. The most important one for this project is CC_MAX_STOPLOSS_DISTANCE_PERCENT, which caps how far a stop-loss can be placed from the entry price. Setting it to 100 disables the cap and allows strategies to place stops at any distance:
import { setConfig } from "backtest-kit";

setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
});
The default value for CC_MAX_STOPLOSS_DISTANCE_PERCENT in backtest-kit is much lower than 100. If your strategy uses Position.moonbag() with a large stop, you will see silent rejections unless you raise this limit.

The Singleton Exchange Instance

Creating a CCXT exchange object is expensive: it allocates HTTP clients and, when loadMarkets() is called, fetches the full symbol catalogue from the exchange. Use singleshot from functools-kit to create the exchange exactly once and reuse the same instance across all calls:
import { singleshot } from "functools-kit";
import ccxt from "ccxt";

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({
    options: {
      defaultType: "spot",
      adjustForTimeDifference: true,
      recvWindow: 60000,
    },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});
The options used here are:
OptionEffect
defaultType: "spot"Targets the Binance spot market, not futures
adjustForTimeDifference: trueCompensates for clock drift between the host and Binance servers
recvWindow: 60000Extends the Binance receive window to 60 s, reducing timestamp-related rejections
enableRateLimit: trueActivates CCXT’s built-in request throttle to stay within API limits

The Four Required Functions

getCandles

Fetches OHLCV bars for a symbol. The function receives a since Date (start of the requested range) and a limit count. CCXT’s fetchOHLCV returns an array of [timestamp, open, high, low, close, volume] tuples; map them to backtest-kit’s named-field format:
getCandles: async (symbol, interval, since, limit) => {
  const exchange = await getExchange();
  const candles = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
  return candles.map(([timestamp, open, high, low, close, volume]) => ({
    timestamp, open, high, low, close, volume,
  }));
},

getOrderBook

Fetches the live order book. Because historical order book snapshots are not available through CCXT, this function must throw in backtest mode. The backtest boolean parameter signals which mode is active:
getOrderBook: async (symbol, depth, _from, _to, backtest) => {
  if (backtest) {
    throw new Error("Order book fetching is not supported in backtest mode.");
  }
  const exchange = await getExchange();
  const bookData = await exchange.fetchOrderBook(symbol, depth);
  return {
    symbol,
    asks: bookData.asks.map(([price, quantity]) => ({
      price:    String(price),
      quantity: String(quantity),
    })),
    bids: bookData.bids.map(([price, quantity]) => ({
      price:    String(price),
      quantity: String(quantity),
    })),
  };
},
Always guard getOrderBook with if (backtest) throw. If you omit the guard, the backtester will attempt live network calls for every tick where a strategy requests order book data, corrupting the replay with real-time prices.

formatPrice

Rounds a raw price to the market’s tick size. Use roundTicks from backtest-kit when the tick size is available from CCXT market metadata, and fall back to CCXT’s own priceToPrecision otherwise:
formatPrice: async (symbol, price) => {
  const exchange = await getExchange();
  const market = exchange.market(symbol);
  const tickSize = market.limits?.price?.min || market.precision?.price;
  if (tickSize !== undefined) return roundTicks(price, tickSize);
  return exchange.priceToPrecision(symbol, price);
},

formatQuantity

Mirrors formatPrice but for order quantities. Uses the market’s step size (minimum lot increment) rather than the tick size:
formatQuantity: async (symbol, quantity) => {
  const exchange = await getExchange();
  const market = exchange.market(symbol);
  const stepSize = market.limits?.amount?.min || market.precision?.amount;
  if (stepSize !== undefined) return roundTicks(quantity, stepSize);
  return exchange.amountToPrecision(symbol, quantity);
},

Complete Schema Registration

Putting all four functions together in modules/backtest.module.ts:
import { addExchangeSchema, roundTicks, setConfig } from "backtest-kit";
import { singleshot } from "functools-kit";
import ccxt from "ccxt";

setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
});

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({
    options: {
      defaultType: "spot",
      adjustForTimeDifference: true,
      recvWindow: 60000,
    },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});

addExchangeSchema({
  exchangeName: "ccxt-exchange",
  getCandles: async (symbol, interval, since, limit) => {
    const exchange = await getExchange();
    const candles = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
    return candles.map(([timestamp, open, high, low, close, volume]) => ({
      timestamp, open, high, low, close, volume,
    }));
  },
  getOrderBook: async (symbol, depth, _from, _to, backtest) => {
    if (backtest) {
      throw new Error("Order book fetching is not supported in backtest mode.");
    }
    const exchange = await getExchange();
    const bookData = await exchange.fetchOrderBook(symbol, depth);
    return {
      symbol,
      asks: bookData.asks.map(([price, quantity]) => ({
        price: String(price), quantity: String(quantity),
      })),
      bids: bookData.bids.map(([price, quantity]) => ({
        price: String(price), quantity: String(quantity),
      })),
    };
  },
  formatPrice: async (symbol, price) => {
    const exchange = await getExchange();
    const market = exchange.market(symbol);
    const tickSize = market.limits?.price?.min || market.precision?.price;
    if (tickSize !== undefined) return roundTicks(price, tickSize);
    return exchange.priceToPrecision(symbol, price);
  },
  formatQuantity: async (symbol, quantity) => {
    const exchange = await getExchange();
    const market = exchange.market(symbol);
    const stepSize = market.limits?.amount?.min || market.precision?.amount;
    if (stepSize !== undefined) return roundTicks(quantity, stepSize);
    return exchange.amountToPrecision(symbol, quantity);
  },
});
The ExchangeName enum ties the string literal to a compile-time constant so strategies and adapters never hard-code the exchange name:
// src/enum/ExchangeName.ts
export enum ExchangeName {
    CCXT = "ccxt-exchange",
}

The Three Module Files

The project contains three parallel module files for the three run modes:
modules/
  backtest.module.ts
  live.module.ts
  paper.module.ts
In this project all three are identical: they register the same ccxt.binance spot schema with the same options. This is intentional — the Binance spot API is the correct data source for all three modes and no per-mode configuration differences are needed. The separation exists so you can diverge them later without restructuring the project. Common reasons to differentiate them include:

Backtest

Use a local OHLCV cache or a different data vendor to avoid hitting Binance rate limits during long historical replays.

Paper

Connect to Binance testnet instead of production to validate order placement logic without real funds.

Live

Add API keys, switch to a co-located endpoint, or enable futures/margin types that are inappropriate for backtesting.

Using a Different Exchange

To switch from Binance to any other CCXT-supported exchange, replace ccxt.binance with the appropriate class and adjust the constructor options. The rest of the schema — getCandles, getOrderBook, formatPrice, formatQuantity — is exchange-agnostic because CCXT normalises the interface:
// Example: switch to Bybit
import ccxt from "ccxt";

const getExchange = singleshot(async () => {
  const exchange = new ccxt.bybit({
    options: { defaultType: "spot" },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});
If you change the exchange, also update ExchangeName in src/enum/ExchangeName.ts and the exchangeName field passed to addExchangeSchema() so every adapter and strategy that references ExchangeName.CCXT continues to compile.

Build docs developers (and LLMs) love