Skip to main content

Documentation Index

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

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

An exchange adapter is the bridge between backtest-kit and a real (or simulated) market data source. It is registered once via addExchangeSchema and then used by every strategy that references the same exchangeName. The adapter must implement four async methods: getCandles, getOrderBook, formatPrice, and formatQuantity. This project ships a ccxt Binance spot adapter that is registered in three module files — one per running mode.

addExchangeSchema API

addExchangeSchema({
  exchangeName:    string,
  getCandles:      (symbol, interval, since, limit) => Promise<CandleData[]>,
  getOrderBook:    (symbol, depth, from, to, backtest) => Promise<OrderBook>,
  formatPrice:     (symbol, price)    => Promise<string>,
  formatQuantity:  (symbol, quantity) => Promise<string>,
})

exchangeName

A unique string identifier. Must match the ExchangeName enum value used in Backtest.background / Live.background and in warmCandles.

getCandles

Fetches OHLCV data from the exchange. Called by warmCandles to pre-populate the candle cache and by the live/paper runner on each tick.

getOrderBook

Fetches the current order book. Throws in backtest mode (no live book available for historical data). Implement custom logic here for order-book-based strategies.

formatPrice / formatQuantity

Round a raw price or quantity to the exchange’s tick/step size using roundTicks. Ensures orders are accepted without precision errors.

The ExchangeName Enum

All three module files register the same string "ccxt-exchange", which maps to ExchangeName.CCXT:
// src/enum/ExchangeName.ts
export enum ExchangeName {
  CCXT = "ccxt-exchange",
}
Add a new enum member whenever you register an additional exchange adapter (e.g. a different ccxt exchange, a custom WebSocket feed, or a simulated order book).

Module Files

The exchange adapter is registered in three separate files under modules/, one per running mode. All three are currently identical — this lets you diverge them later (e.g. use different API credentials, a mock order book, or a different ccxt exchange for paper mode) without touching shared code:
modules/
  backtest.module.ts   ← adapter for Backtest.background
  live.module.ts       ← adapter for Live.background (live mode)
  paper.module.ts      ← adapter for Live.background (paper mode)
Each module file is imported by the corresponding src/main/*.ts entry point before the strategy loop starts, so the schema is always registered before it is needed.

Complete ccxt Binance Adapter (modules/backtest.module.ts)

The following is the full, unmodified source of modules/backtest.module.ts. The other two module files are identical.
import { addExchangeSchema, addFrameSchema, roundTicks, setConfig } from "backtest-kit";
import { singleshot } from "functools-kit";
import ccxt from "ccxt";

// ── Global config ──────────────────────────────────────────────────────────
setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
});

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

// ── Schema registration ────────────────────────────────────────────────────
addExchangeSchema({
  exchangeName: "ccxt-exchange",

  getCandles: async (symbol, interval, since, limit) => {
    const exchange = await getExchange();
    const candles  = await exchange.fetchOHLCV(
      symbol,
      interval,
      since.getTime(),   // ccxt expects epoch ms
      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),
      })),
    };
  },

  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);
  },
});

Method Reference

getCandles(symbol, interval, since, limit)

Fetches historical OHLCV data starting at since (a Date object). Returns an array of CandleData:
interface CandleData {
  timestamp: number;   // epoch milliseconds (candle open time)
  open:      number;
  high:      number;
  low:       number;
  close:     number;
  volume:    number;
}
The ccxt adapter converts the Date to epoch milliseconds with since.getTime() because fetchOHLCV expects a numeric timestamp. Results are mapped from ccxt’s raw [timestamp, open, high, low, close, volume] tuples into plain objects.
warmCandles calls getCandles in paginated batches and writes each candle to the candle-items table via the Candle persist adapter. After warm-up, getClosePrice and getCandles in strategy code read from the database rather than hitting the exchange again.

getOrderBook(symbol, depth, from, to, backtest)

ParameterTypeDescription
symbolstringTrading pair, e.g. "TRXUSDT"
depthnumberNumber of price levels to fetch on each side
fromDateStart of the requested window (informational; exchange adapters may ignore)
toDateEnd of the requested window
backtestbooleantrue when called during a backtest replay
The backtest guard is critical: a live order book has no meaning during historical replay, so the default adapter throws immediately. If your strategy uses order-book data in backtests, replace this with a persisted book snapshot or a synthetic book constructed from the candle spread.
If you call any getOrderBook-dependent strategy helpers while running in backtest mode without overriding this method, the strategy loop will throw and the position will not be entered. Implement historical order book logic or guard your strategy code with the backtest flag before using order book data.

formatPrice(symbol, price) and formatQuantity(symbol, quantity)

Both helpers use roundTicks from backtest-kit to snap a raw floating-point value to the exchange’s minimum increment:
import { roundTicks } from "backtest-kit";

// For a market with tickSize = 0.0001:
roundTicks(0.12345678, 0.0001)  // → "0.1234"
The fallback path (exchange.priceToPrecision / exchange.amountToPrecision) handles markets where ccxt stores precision as a decimal count rather than an absolute step size.

The singleshot Pattern for Lazy Initialisation

getExchange is wrapped in singleshot from functools-kit. This is a memoised async factory: the first call creates the ccxt exchange instance and calls loadMarkets() (a network round-trip that fetches symbol metadata); every subsequent call returns the same resolved promise.
import { singleshot } from "functools-kit";

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({ /* ... */ });
  await exchange.loadMarkets();
  return exchange;
});
This avoids re-initialising the exchange on every candle tick while keeping the initialisation lazy (it does not run until the first getCandles or getOrderBook call).

setConfig — Global Strategy Configuration

setConfig sets global backtest-kit configuration values. It is called at module load time, before the schema is registered:
setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
});
CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100 removes the default cap on stop-loss distance (which is 5% by default), allowing Position.moonbag to place stops up to 100% away from entry. Adjust this value to match your risk model.

Registering the Adapter for All Three Modes

To ensure the adapter is available regardless of which mode is started, import all three module files from your entry points. The existing structure already does this: src/main/backtest.ts imports from modules/backtest.module.ts, src/main/live.ts from modules/live.module.ts, and src/main/paper.ts from modules/paper.module.ts. If you want a single adapter registration shared across all modes, extract it into a shared module and import it from each src/main/*.ts:
// modules/shared.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 () => { /* ... */ });

addExchangeSchema({
  exchangeName: "ccxt-exchange",
  // ... same implementation
});
// src/main/backtest.ts
import "../../modules/shared.module";
// rest of backtest entry point
Create a new module file, add a new ExchangeName enum member, and call addExchangeSchema with the new name:
// modules/okx.module.ts
import { addExchangeSchema, roundTicks } from "backtest-kit";
import { singleshot } from "functools-kit";
import ccxt from "ccxt";

const getOkx = singleshot(async () => {
  const exchange = new ccxt.okx({ enableRateLimit: true });
  await exchange.loadMarkets();
  return exchange;
});

addExchangeSchema({
  exchangeName: "okx-exchange",   // matches ExchangeName.OKX
  getCandles: async (symbol, interval, since, limit) => {
    const exchange = await getOkx();
    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 not supported in backtest");
    const exchange = await getOkx();
    const book     = await exchange.fetchOrderBook(symbol, depth);
    return {
      symbol,
      asks: book.asks.map(([p, q]) => ({ price: String(p), quantity: String(q) })),
      bids: book.bids.map(([p, q]) => ({ price: String(p), quantity: String(q) })),
    };
  },
  formatPrice:    async (symbol, price)    => { /* roundTicks logic */ return String(price); },
  formatQuantity: async (symbol, quantity) => { /* roundTicks logic */ return String(quantity); },
});
Then add ExchangeName.OKX = "okx-exchange" to the enum and reference it in your Backtest.background or Live.background call.

Build docs developers (and LLMs) love