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 a registry pattern: before any backtest or live run can start, you call four registration functions to declare how data is fetched, what signals look like, which date range to replay, and what risk constraints to enforce. Registrations are stored in named ToolRegistry instances and looked up automatically when the engine needs them. All four schemas are required for backtesting; live mode does not need a frame schema.

Exchange Schema

The exchange schema connects Backtest Kit to a candle data source — typically a CCXT exchange in backtest mode and an exchange with API keys in live mode.

Interface

interface IExchangeSchema {
  /** Unique exchange identifier */
  exchangeName: ExchangeName;
  /** Optional developer note */
  note?: string;
  /** Fetch OHLCV candles from any data source */
  getCandles: (
    symbol: string,
    interval: CandleInterval,
    since: Date,
    limit: number,
    backtest: boolean
  ) => Promise<IPublicCandleData[]>;
  /**
   * Format price to exchange precision (optional).
   * Defaults to 2 decimal places if omitted.
   */
  formatPrice?: (symbol: string, price: number, backtest: boolean) => Promise<string>;
  /**
   * Format quantity to exchange precision (optional).
   * Defaults to 8 decimal places if omitted.
   */
  formatQuantity?: (symbol: string, quantity: number, backtest: boolean) => Promise<string>;
  /** Fetch order book snapshot (optional) */
  getOrderBook?: (
    symbol: string, depth: number, from: Date, to: Date, backtest: boolean
  ) => Promise<IOrderBookData>;
  /** Fetch aggregated trades (optional) */
  getAggregatedTrades?: (
    symbol: string, from: Date, to: Date, backtest: boolean
  ) => Promise<IAggregatedTradeData[]>;
  /** Optional lifecycle callbacks */
  callbacks?: Partial<IExchangeCallbacks>;
}
CandleInterval is: "1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "1d". If formatPrice or formatQuantity are omitted, Backtest Kit defaults to Bitcoin precision on Binance (2 and 8 decimal places, respectively).

Registration

import ccxt from 'ccxt';
import { addExchangeSchema } from 'backtest-kit';

addExchangeSchema({
  exchangeName: 'binance',
  getCandles: async (symbol, interval, since, limit, backtest) => {
    const exchange = new ccxt.binance({
      // In live mode only — read from environment
      ...(backtest ? {} : {
        apiKey: process.env.BINANCE_API_KEY,
        secret: process.env.BINANCE_API_SECRET,
      }),
    });
    const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
    return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
      timestamp, open, high, low, close, volume,
    }));
  },
  formatPrice:    async (symbol, price, backtest)    => price.toFixed(2),
  formatQuantity: async (symbol, quantity, backtest) => quantity.toFixed(8),
});

Frame Schema

Frame schemas define the backtest date range and tick interval. The engine generates a Date[] array from startDate to endDate spaced by interval and iterates through it step by step.

Interface

interface IFrameSchema {
  /** Unique frame identifier */
  frameName: FrameName;
  /** Optional developer note */
  note?: string;
  /** Tick spacing for the generated timestamp array */
  interval: FrameInterval;
  /** Start of backtest period (inclusive) */
  startDate: Date;
  /** End of backtest period (inclusive) */
  endDate: Date;
  /** Optional lifecycle callbacks */
  callbacks?: Partial<IFrameCallbacks>;
}
FrameInterval supports: "1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h" | "12h" | "1d".

Registration

import { addFrameSchema } from 'backtest-kit';

addFrameSchema({
  frameName: '1d-test',
  interval: '1m',
  startDate: new Date('2025-12-01'),
  endDate:   new Date('2025-12-02'),
  callbacks: {
    onTimeframe: (timeframe, startDate, endDate, interval) => {
      console.log(`Generated ${timeframe.length} timestamps for ${interval} interval`);
    },
  },
});
Use a short endDate - startDate window (one or two days at 1m) for development. Switch to weeks or months for production performance testing.

Risk Schema

Risk schemas define a portfolio-level guard that runs before any signal is accepted. The validations array holds functions that either return nothing (pass) or throw an error (reject).

Interface

interface IRiskSchema {
  /** Unique risk profile identifier */
  riskName: RiskName;
  /** Optional developer note */
  note?: string;
  /** Custom validation functions */
  validations: (IRiskValidation | IRiskValidationFn)[];
  /** Optional callbacks for rejected/allowed signals */
  callbacks?: Partial<IRiskCallbacks>;
}

interface IRiskValidationFn {
  (payload: IRiskValidationPayload): RiskRejection | Promise<RiskRejection>;
}

interface IRiskValidationPayload extends IRiskCheckArgs {
  currentSignal: IRiskSignalRow;
  activePositionCount: number;
  activePositions: IRiskActivePosition[];
}

Registration

import { addRiskSchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'demo',
  validations: [
    // TP must be at least 1% from entry
    ({ currentSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, position } = currentSignal;
      const tpDistance = position === 'long'
        ? ((priceTakeProfit - priceOpen) / priceOpen) * 100
        : ((priceOpen - priceTakeProfit) / priceOpen) * 100;
      if (tpDistance < 1) throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);
    },
    // Reward-to-risk must be at least 2:1
    ({ currentSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = currentSignal;
      const reward = position === 'long' ? priceTakeProfit - priceOpen : priceOpen - priceTakeProfit;
      const risk   = position === 'long' ? priceOpen - priceStopLoss : priceStopLoss - priceOpen;
      if (reward / risk < 2) throw new Error('Poor R/R ratio');
    },
  ],
  callbacks: {
    onRejected: (symbol, params) => console.warn(`[RISK] Signal rejected for ${symbol}`),
    onAllowed:  (symbol, params) => {},
  },
});
See the Risk Management guide for cross-strategy position tracking and time-window restrictions.

Strategy Schema

The strategy schema contains getSignal — the signal generation function that runs on every tick. Return an ISignalDto to open or schedule a position, or null to skip.

Interface

interface IStrategySchema {
  /** Unique strategy identifier */
  strategyName: StrategyName;
  /** Optional developer note */
  note?: string;
  /**
   * Minimum interval between getSignal calls.
   * SignalInterval: "1m" | "3m" | "5m" | "15m" | "30m" | "1h"
   */
  interval?: SignalInterval;
  /**
   * Signal generation function.
   * Return ISignalDto to open/schedule a position.
   * Return null to skip this tick.
   */
  getSignal: (
    symbol: string,
    when: Date,
    currentPrice: number
  ) => Promise<ISignalDto | null>;
  /** Optional lifecycle callbacks */
  callbacks?: Partial<IStrategyCallbacks>;
  /** Risk profile to apply (single) */
  riskName?: RiskName;
  /** Risk profiles to apply (multiple) */
  riskList?: RiskName[];
  /** Action handlers to attach */
  actions?: ActionName[];
}

Registration

import { v4 as uuid } from 'uuid';
import { addStrategySchema, getCandles } from 'backtest-kit';

addStrategySchema({
  strategyName: 'my-strategy',
  interval: '5m',
  riskName: 'demo',
  getSignal: async (symbol, when, currentPrice) => {
    const candles1h  = await getCandles(symbol, '1h', 24);
    const candles15m = await getCandles(symbol, '15m', 48);

    const lastClose1h  = candles1h.at(-1)!.close;
    const lastClose15m = candles15m.at(-1)!.close;

    // Simple momentum: only long when 15m close is above 1h close
    if (lastClose15m <= lastClose1h) return null;

    return {
      id: uuid(),
      position: 'long',
      priceTakeProfit: currentPrice * 1.03,
      priceStopLoss:   currentPrice * 0.985,
      minuteEstimatedTime: 120,
    };
  },
  callbacks: {
    onOpen:  (symbol, signal, price, backtest) => console.log(`Opened at ${price}`),
    onClose: (symbol, signal, price, backtest) => console.log(`Closed at ${price}`),
  },
});
If you provide priceOpen in the returned ISignalDto, the signal becomes a scheduled (limit order) signal and waits for price to reach that level before activating. Omit priceOpen for an immediate market entry at current VWAP.

Sizing Schema (Brief)

addSizingSchema registers a position sizing calculator. Three methods are supported: "fixed-percentage", "kelly-criterion", and "atr-based". Attach a sizing schema to a strategy to dynamically compute position size from account balance.
import { addSizingSchema } from 'backtest-kit';

addSizingSchema({
  sizingName: 'conservative',
  method: 'fixed-percentage',
  riskPercentage: 1,           // risk 1% of account per trade
  maxPositionPercentage: 10,   // cap at 10% of account
});

Action Schema (Brief)

addActionSchema attaches event handlers to strategies for notifications, state management, or analytics. Action classes receive every signal lifecycle event (opened, closed, breakeven, partial profit, etc.).
import { addActionSchema } from 'backtest-kit';

addActionSchema({
  actionName: 'telegram-notifier',
  handler: {
    signal: async (event) => {
      if (event.action === 'opened') {
        // await telegram.send(`New ${event.signal.position} signal`);
      }
    },
  },
});

Overriding Schemas

All schemas support partial overrides after registration using override*Schema functions:
import { overrideRiskSchema, overrideFrameSchema } from 'backtest-kit';

// Tighten the R/R floor without re-registering everything
overrideRiskSchema({
  riskName: 'demo',
  validations: [/* new validations */],
});

// Extend the backtest window
overrideFrameSchema({
  frameName: '1d-test',
  endDate: new Date('2026-03-01'),
});

Build docs developers (and LLMs) love