Skip to main content

Documentation Index

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

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

The exchange schema tells backtest-kit how to fetch OHLCV candles, order book data, and aggregated trades, and how to format prices and quantities for a specific exchange. It is the bridge between backtest-kit’s internal tick engine and any ccxt-compatible exchange. In this monorepo the schema lives in content/apr_2026.strategy/modules/backtest.module.ts alongside the frame schema.

Global Configuration

Before registering the exchange schema, setConfig applies two global backtest-kit parameters:
import { setConfig } from "backtest-kit";

setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
  CC_BREAKEVEN_THRESHOLD: 0,
});
OptionValueEffect
CC_MAX_STOPLOSS_DISTANCE_PERCENT100Allows stop-loss levels up to 100% away from entry — effectively disables the distance guard
CC_BREAKEVEN_THRESHOLD0Disables the automatic breakeven move — the stop-loss stays at the level set by Position.moonbag()

The singleshot Exchange Factory

Creating a ccxt.binance instance and loading its market data is expensive. The singleshot utility from functools-kit wraps the factory so it runs exactly once and returns the cached instance on every subsequent call:
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 singleshot() pattern from functools-kit ensures the ccxt exchange is initialized exactly once and reused across all symbols. With 9 symbols running in parallel, this avoids 9 redundant loadMarkets() round-trips on start-up.

addExchangeSchema

import { addExchangeSchema, roundTicks } from "backtest-kit";

addExchangeSchema({
  exchangeName: "ccxt-exchange",
  getCandles: async (symbol, interval, since, limit) => { /* ... */ },
  getOrderBook: async (symbol, depth, _from, _to, backtest) => { /* ... */ },
  getAggregatedTrades: async (symbol, from, to) => { /* ... */ },
  formatPrice: async (symbol, price) => { /* ... */ },
  formatQuantity: async (symbol, quantity) => { /* ... */ },
});
exchangeName is the string key used to link this adapter to a frame schema and to Backtest.background(). It must match the value passed to cacheCandles() and Backtest.background({ exchangeName }).

Method Reference

Fetches OHLCV candles from the exchange and maps them to the shape backtest-kit expects.
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,
  }));
},
since is a Date — call .getTime() to convert to the Unix millisecond integer that ccxt.fetchOHLCV expects. The return type is an array of { timestamp, open, high, low, close, volume }.
Returns the current order book. The backtest flag is true during replay; historical order book snapshots are not available from ccxt, so this implementation throws in that mode.
getOrderBook: async (symbol, depth, _from, _to, backtest) => {
  if (backtest) {
    throw new Error(
      "Order book fetching is not supported in backtest mode for the default exchange schema. " +
      "Please implement it according to your needs.",
    );
  }
  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),
    })),
  };
},
getOrderBook throws in backtest mode — implement your own order book source (e.g. a pre-recorded Mongo snapshot) if your strategy logic depends on order book depth during historical replay.
Fetches Binance aggregated trade data via the publicGetAggTrades endpoint. Each record represents a batch of trades executed at the same price in the same direction.
getAggregatedTrades: async (symbol: string, from: Date, to: Date) => {
  const exchange = await getExchange();
  const response = await exchange.publicGetAggTrades({
    symbol,
    startTime: from.getTime(),
    endTime: to.getTime(),
  });
  return response.map((t: any) => ({
    id: String(t.a),
    price: parseFloat(t.p),
    qty: parseFloat(t.q),
    timestamp: t.T,
    isBuyerMaker: t.m,
  }));
},
FieldBinance keyDescription
idt.aAggregate trade ID
pricet.pExecution price
qtyt.qQuantity
timestampt.TTrade time (Unix ms)
isBuyerMakert.mtrue if the buyer was the market maker
Rounds a raw price to the exchange’s tick size using roundTicks() from backtest-kit. Falls back to ccxt’s built-in priceToPrecision if the market’s tick size is unavailable.
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);
},
roundTicks(price, tickSize) snaps price to the nearest multiple of tickSize, matching the exchange’s accepted precision.
Rounds a raw quantity to the exchange’s step size using roundTicks(). Falls back to ccxt’s amountToPrecision if the step size is absent.
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 Module File

The full backtest.module.ts registers both the exchange schema and the frame schema in a single file:
import { addExchangeSchema, addFrameSchema, roundTicks, setConfig } from "backtest-kit";
import { singleshot } from "functools-kit";
import ccxt from "ccxt";

setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
  CC_BREAKEVEN_THRESHOLD: 0,
});

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 for the default exchange schema. " +
        "Please implement it according to your needs.",
      );
    }
    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),
      })),
    };
  },
  getAggregatedTrades: async (symbol: string, from: Date, to: Date) => {
    const exchange = await getExchange();
    const response = await exchange.publicGetAggTrades({
      symbol,
      startTime: from.getTime(),
      endTime: to.getTime(),
    });
    return response.map((t: any) => ({
      id: String(t.a),
      price: parseFloat(t.p),
      qty: parseFloat(t.q),
      timestamp: t.T,
      isBuyerMaker: t.m,
    }));
  },
  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);
  },
});

addFrameSchema({
  frameName: "apr_2026_frame",
  interval: "1m",
  startDate: new Date("2026-04-01T00:00:00Z"),
  endDate: new Date("2026-04-27T00:00:00Z"),
});

Build docs developers (and LLMs) love