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.

This guide takes you from zero to a running backtest in a single sitting. You will install the package, configure logging, register an exchange data source, define risk rules, attach a time frame, write a signal generator, and run your first backtest against historical candle data. At the end you will see how to switch to live trading by changing a single class name.
1
Install
2
The fastest path is the CLI scaffolder, which generates a project with all boilerplate pre-wired:
3
CLI Init (recommended)
npx @backtest-kit/cli --init --output backtest-kit-project
cd backtest-kit-project
npm install
npm start
Sidekick (full control)
npx -y @backtest-kit/sidekick my-trading-bot
cd my-trading-bot
npm start
Manual Install
npm install backtest-kit ccxt ollama uuid
4
The CLI init scaffold keeps all boilerplate inside @backtest-kit/cli. The sidekick scaffold puts every wiring file directly in your project for full visibility and control.
5
Configure Logging and Global Settings
6
Call setLogger to forward internal framework messages to your console. Call setConfig to override global defaults like slippage, fees, and pending signal timeout.
7
import { setLogger, setConfig } from 'backtest-kit';

setLogger({
  log: console.log,
  debug: console.debug,
  info: console.info,
  warn: console.warn,
});

// Optional — override defaults
setConfig({
  CC_PERCENT_SLIPPAGE: 0.1,  // % slippage per transaction
  CC_PERCENT_FEE: 0.1,       // % fee per transaction
  CC_SCHEDULE_AWAIT_MINUTES: 120,  // pending signal timeout (minutes)
});
8
Register Exchange Schema
9
The exchange schema tells Backtest Kit how to fetch candle data and how to format prices and quantities for a specific market. The example below uses CCXT to connect to Binance.
10
import ccxt from 'ccxt';
import { addExchangeSchema } from 'backtest-kit';

addExchangeSchema({
  exchangeName: 'binance',
  getCandles: async (symbol, interval, since, limit, backtest) => {
    const exchange = new ccxt.binance();
    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) => price.toFixed(2),
  formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
});
11
For live trading, initialise the CCXT exchange with apiKey and secret from environment variables. The same addExchangeSchema call works for both modes — just pass the credentials only when backtest === false.
12
Register Risk Schema
13
Risk validations run before any signal is accepted. Each validation function receives the pending signal and the current portfolio state. Throw an error to reject the signal.
14
import { addRiskSchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'demo',
  validations: [
    // Take-profit 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 must be at least 2× the risk
    ({ 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');
      }
    },
  ],
});
15
Register Frame Schema
16
A frame schema defines the date range and tick interval for a backtest run. Choose interval: '1m' for minute-by-minute precision. The engine generates a Date[] array from startDate to endDate and iterates through it.
17
import { addFrameSchema } from 'backtest-kit';

addFrameSchema({
  frameName: '1d-test',
  interval: '1m',
  startDate: new Date('2025-12-01'),
  endDate: new Date('2025-12-02'),
});
18
Register Strategy Schema
19
The strategy schema contains getSignal — the heart of your trading logic. Return an ISignalDto to open a position, or null to skip this tick. The interval throttles how often getSignal is called.
20
import { v4 as uuid } from 'uuid';
import { addStrategySchema, getCandles } from 'backtest-kit';

addStrategySchema({
  strategyName: 'my-strategy',
  interval: '5m',       // getSignal called at most every 5 minutes
  riskName: 'demo',     // risk validations applied before opening
  getSignal: async (symbol, when, currentPrice) => {
    // Fetch multi-timeframe candles (look-ahead bias is impossible —
    // AsyncLocalStorage enforces the current virtual time)
    const candles1h  = await getCandles(symbol, '1h', 24);
    const candles15m = await getCandles(symbol, '15m', 48);

    // Your signal logic here — LLM, indicators, pattern matching…
    const shouldLong = candles15m.at(-1)!.close > candles1h.at(-1)!.close;

    if (!shouldLong) return null;

    return {
      id: uuid(),
      position: 'long',
      priceTakeProfit: currentPrice * 1.03,  // +3%
      priceStopLoss:   currentPrice * 0.985, // -1.5%
      minuteEstimatedTime: 120,              // expire after 2 hours
    };
  },
});
21
Run Backtest
22
Backtest.background consumes the generator internally and fires event listeners. listenDoneBacktest fires when all timeframes have been processed; call Backtest.dump inside it to write the Markdown report.
23
import { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';

Backtest.background('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
  frameName:    '1d-test',
});

listenSignalBacktest((event) => {
  if (event.action === 'closed') {
    console.log(`PNL: ${event.pnl.pnlPercentage.toFixed(2)}% — ${event.closeReason}`);
  }
});

listenDoneBacktest(async (event) => {
  // Saves report to ./dump/backtest/
  await Backtest.dump(event.symbol, {
    strategyName: event.strategyName,
    exchangeName: event.exchangeName,
    frameName:    event.frameName,
  });
});
24
Alternatively, use Backtest.run to iterate results directly with for await:
25
for await (const result of Backtest.run('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
  frameName:    '1d-test',
})) {
  if (result.action === 'closed') {
    console.log(result.pnl.pnlPercentage);
  }
}
26
Switch to Live Trading
27
Replace Backtest.background with Live.background. The frameName field is no longer needed because live mode drives time from the real clock. Every other registration — exchange, risk, strategy — remains unchanged.
28
import { Live, listenSignalLive } from 'backtest-kit';

const stop = Live.background('BTCUSDT', {
  strategyName: 'my-strategy',
  exchangeName: 'binance',
});

listenSignalLive((event) => console.log(event.action, event.signal?.id));

// Call stop() to gracefully halt new signals while existing positions wind down
process.on('SIGINT', () => stop());
For a full production-ready example including LLM forecasting, Telegram alerts, and a documented February 2026 backtest (+16.99% during a -16.4% month), see the reference implementation on GitHub.

Build docs developers (and LLMs) love