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.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.
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.Call
setLogger to forward internal framework messages to your console. Call setConfig to override global defaults like slippage, fees, and pending signal timeout.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)
});
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.
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),
});
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.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.
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');
}
},
],
});
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.import { addFrameSchema } from 'backtest-kit';
addFrameSchema({
frameName: '1d-test',
interval: '1m',
startDate: new Date('2025-12-01'),
endDate: new Date('2025-12-02'),
});
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.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
};
},
});
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.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,
});
});
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);
}
}
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.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.