A TypeScript framework for backtesting and live trading strategies on multi-asset, crypto, forex or DEX (peer-to-peer marketplace), spot, futures with crash-safe persistence, signal validation, and AI optimization.

Build reliable trading systems: backtest on historical data, deploy live bots with recovery, and optimize strategies using LLMs like Ollama.
π API Reference | π Quick Start | π° Article
Create a production-ready trading bot in seconds:
# Create project with npx (recommended)
npx -y @backtest-kit/sidekick my-trading-bot
cd my-trading-bot
npm start
Want to see the code? π Demo app π
npm install backtest-kit ccxt ollama uuid
import { setLogger, setConfig } from 'backtest-kit';
// Enable logging
setLogger({
log: console.log,
debug: console.debug,
info: console.info,
warn: console.warn,
});
// Global config (optional)
setConfig({
CC_PERCENT_SLIPPAGE: 0.1, // % slippage
CC_PERCENT_FEE: 0.1, // % fee
CC_SCHEDULE_AWAIT_MINUTES: 120, // Pending signal timeout
});
import ccxt from 'ccxt';
import { addExchangeSchema, addStrategySchema, addFrameSchema, addRiskSchema } from 'backtest-kit';
// Exchange (data source)
addExchangeSchema({
exchangeName: 'binance',
getCandles: async (symbol, interval, since, limit) => {
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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
// Risk profile
addRiskSchema({
riskName: 'demo',
validations: [
// TP at least 1%
({ pendingSignal, currentPrice }) => {
const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal;
const tpDistance = position === 'long' ? ((priceTakeProfit - priceOpen) / priceOpen) * 100 : ((priceOpen - priceTakeProfit) / priceOpen) * 100;
if (tpDistance < 1) throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);
},
// R/R at least 2:1
({ pendingSignal, currentPrice }) => {
const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal;
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');
},
],
});
// Time frame
addFrameSchema({
frameName: '1d-test',
interval: '1m',
startDate: new Date('2025-12-01'),
endDate: new Date('2025-12-02'),
});
import { v4 as uuid } from 'uuid';
import { addStrategySchema, dumpSignalData, getCandles } from 'backtest-kit';
import { json } from './utils/json.mjs'; // LLM wrapper
import { getMessages } from './utils/messages.mjs'; // Market data prep
addStrategySchema({
strategyName: 'llm-strategy',
interval: '5m',
riskName: 'demo',
getSignal: async (symbol) => {
const candles1h = await getCandles(symbol, "1h", 24);
const candles15m = await getCandles(symbol, "15m", 48);
const candles5m = await getCandles(symbol, "5m", 60);
const candles1m = await getCandles(symbol, "1m", 60);
const messages = await getMessages(symbol, {
candles1h,
candles15m,
candles5m,
candles1m,
}); // Calculate indicators / Fetch news
const resultId = uuid();
const signal = await json(messages); // LLM generates signal
await dumpSignalData(resultId, messages, signal); // Log
return { ...signal, id: resultId };
},
});
import { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';
Backtest.background('BTCUSDT', {
strategyName: 'llm-strategy',
exchangeName: 'binance',
frameName: '1d-test',
});
listenSignalBacktest((event) => console.log(event));
listenDoneBacktest(async (event) => {
await Backtest.dump(event.symbol, event.strategyName); // Generate report
});
import { Live, listenSignalLive } from 'backtest-kit';
Live.background('BTCUSDT', {
strategyName: 'llm-strategy',
exchangeName: 'binance', // Use API keys in .env
});
listenSignalLive((event) => console.log(event));
listenRisk, listenError, listenPartialProfit/Loss for alerts.Backtest.dump(), Live.dump().Customize via setConfig():
CC_SCHEDULE_AWAIT_MINUTES: Pending timeout (default: 120).CC_AVG_PRICE_CANDLES_COUNT: VWAP candles (default: 5).Backtest Kit is not a data-processing library - it is a time execution engine. Think of the engine as an async stream of time, where your strategy is evaluated step by step.
backtest-kit uses Node.js AsyncLocalStorage to automatically provide
temporal time context to your strategies.
For a candle with:
- `timestamp` = candle open time (openTime)
- `stepMs` = interval duration (e.g., 60000ms for "1m")
- Candle close time = `timestamp + stepMs`
**Alignment:** All timestamps are aligned down to interval boundary.
For example, for 15m interval: 00:17 β 00:15, 00:44 β 00:30
**Adapter contract:**
- First candle.timestamp must equal aligned `since`
- Adapter must return exactly `limit` candles
- Sequential timestamps: `since + i * stepMs` for i = 0..limit-1
**How `since` is calculated from `when`:**
- `when` = current execution context time (from AsyncLocalStorage)
- `alignedWhen` = `Math.floor(when / stepMs) * stepMs` (aligned down to interval boundary)
- `since` = `alignedWhen - limit * stepMs` (go back `limit` candles from aligned when)
**Boundary semantics (inclusive/exclusive):**
- `since` is always **inclusive** β first candle has `timestamp === since`
- Exactly `limit` candles are returned
- Last candle has `timestamp === since + (limit - 1) * stepMs` β **inclusive**
- For `getCandles`: `alignedWhen` is **exclusive** β candle at that timestamp is NOT included (it's a pending/incomplete candle)
- For `getRawCandles`: `eDate` is **exclusive** β candle at that timestamp is NOT included (it's a pending/incomplete candle)
- For `getNextCandles`: `alignedWhen` is **inclusive** β first candle starts at `alignedWhen` (it's the current candle for backtest, already closed in historical data)
- `getCandles(symbol, interval, limit)` - Returns exactly `limit` candles
- Aligns `when` down to interval boundary
- Calculates `since = alignedWhen - limit * stepMs`
- **since β inclusive**, first candle.timestamp === since
- **alignedWhen β exclusive**, candle at alignedWhen is NOT returned
- Range: `[since, alignedWhen)` β half-open interval
- Example: `getCandles("BTCUSDT", "1m", 100)` returns 100 candles ending before aligned when
- `getNextCandles(symbol, interval, limit)` - Returns exactly `limit` candles (backtest only)
- Aligns `when` down to interval boundary
- `since = alignedWhen` (starts from aligned when, going forward)
- **since β inclusive**, first candle.timestamp === since
- Range: `[alignedWhen, alignedWhen + limit * stepMs)` β half-open interval
- Throws error in live mode to prevent look-ahead bias
- Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles starting from aligned when
- `getRawCandles(symbol, interval, limit?, sDate?, eDate?)` - Flexible parameter combinations:
- `(limit)` - since = alignedWhen - limit * stepMs, range `[since, alignedWhen)`
- `(limit, sDate)` - since = align(sDate), returns `limit` candles forward, range `[since, since + limit * stepMs)`
- `(limit, undefined, eDate)` - since = align(eDate) - limit * stepMs, **eDate β exclusive**, range `[since, eDate)`
- `(undefined, sDate, eDate)` - since = align(sDate), limit calculated from range, **sDate β inclusive, eDate β exclusive**, range `[sDate, eDate)`
- `(limit, sDate, eDate)` - since = align(sDate), returns `limit` candles, **sDate β inclusive**
- All combinations respect look-ahead bias protection (eDate/endTime <= when)
**Persistent Cache:**
- Cache lookup calculates expected timestamps: `since + i * stepMs` for i = 0..limit-1
- Returns all candles if found, null if any missing (cache miss)
- Cache and runtime use identical timestamp calculation logic
According to this timestamp of a candle in backtest-kit is exactly the openTime, not closeTime
Key principles:
sincelimit candlessince + i * stepMsWhy align timestamps to interval boundaries?
Because candle APIs return data starting from exact interval boundaries:
// 15-minute interval example:
when = 1704067920000 // 00:12:00
step = 15 // 15 minutes
stepMs = 15 * 60000 // 900000ms
// Alignment: round down to nearest interval boundary
alignedWhen = Math.floor(when / stepMs) * stepMs
// = Math.floor(1704067920000 / 900000) * 900000
// = 1704067200000 (00:00:00)
// Calculate since for 4 candles backwards:
since = alignedWhen - 4 * stepMs
// = 1704067200000 - 4 * 900000
// = 1704063600000 (23:00:00 previous day)
// Expected candles:
// [0] timestamp = 1704063600000 (23:00)
// [1] timestamp = 1704064500000 (23:15)
// [2] timestamp = 1704065400000 (23:30)
// [3] timestamp = 1704066300000 (23:45)
Pending candle exclusion: The candle at 00:00:00 (alignedWhen) is NOT included in the result. At when=00:12:00, this candle covers the period [00:00, 00:15) and is still open (pending). Pending candles have incomplete OHLCV data that would distort technical indicators. Only fully closed candles are returned.
Validation is applied consistently across:
getCandles() - validates first timestamp and countgetNextCandles() - validates first timestamp and countgetRawCandles() - validates first timestamp and countResult: Deterministic candle retrieval with exact timestamp matching.
getCandles() always returns data UP TO the current backtest timestamp using async_hooksBacktest Kit exposes the same runtime in two equivalent forms. Both approaches use the same engine and guarantees - only the consumption model differs.
Suitable for production bots, monitoring, and long-running processes.
Backtest.background('BTCUSDT', config);
listenSignalBacktest(event => { /* handle signals */ });
listenDoneBacktest(event => { /* finalize / dump report */ });
Suitable for research, scripting, testing, and LLM agents.
for await (const event of Backtest.run('BTCUSDT', config)) {
// signal | trade | progress | done
}
Open-source QuantConnect/MetaTrader without the vendor lock-in
Unlike cloud-based platforms, backtest-kit runs entirely in your environment. You own the entire stack from data ingestion to live execution. In addition to Ollama, you can use neural-trader in getSignal function or any other third party library
The backtest-kit ecosystem extends beyond the core library, offering complementary packages and tools to enhance your trading system development experience:
Explore on NPM π
The @backtest-kit/pinets package lets you run TradingView Pine Script strategies directly in Node.js. Port your existing Pine Script indicators to backtest-kit with zero rewrite using the PineTS runtime.
.pine files or pass code strings directlyplot() outputs to structured signalsPerfect for traders who already have working TradingView strategies. Instead of rewriting your Pine Script logic in JavaScript, simply copy your .pine file and use getSignal() to extract trading signals. Works seamlessly with backtest-kit's temporal context - no look-ahead bias possible.
npm install @backtest-kit/pinets pinets backtest-kit
Explore on NPM π
The @backtest-kit/ui package is a full-stack UI framework for visualizing cryptocurrency trading signals, backtests, and real-time market data. Combines a Node.js backend server with a React dashboard - all in one package.
Perfect for monitoring your trading bots in production. Instead of building custom dashboards, @backtest-kit/ui provides a complete visualization layer out of the box. Each signal view includes detailed information forms, multi-timeframe candlestick charts, and JSON export for all data.
npm install @backtest-kit/ui backtest-kit ccxt
Explore on NPM π€
The @backtest-kit/ollama package is a multi-provider LLM inference library that supports 10+ providers including OpenAI, Claude, DeepSeek, Grok, Mistral, Perplexity, Cohere, Alibaba, Hugging Face, and Ollama with unified API and automatic token rotation.
Ideal for building multi-provider LLM strategies with fallback chains and ensemble predictions. The package returns structured trading signals with validated TP/SL levels, making it perfect for use in getSignal functions. Supports both backtest and live trading modes.
npm install @backtest-kit/ollama agent-swarm-kit backtest-kit
Explore on NPM π
The @backtest-kit/signals package is a technical analysis and trading signal generation library designed for AI-powered trading systems. It computes 50+ indicators across 4 timeframes and generates markdown reports optimized for LLM consumption.
Perfect for injecting comprehensive market context into your LLM-powered strategies. Instead of manually calculating indicators, @backtest-kit/signals provides a single function call that adds all technical analysis to your message context. Works seamlessly with getSignal function in backtest-kit strategies.
npm install @backtest-kit/signals backtest-kit
Explore on NPM π§Ώ
The @backtest-kit/sidekick package is the easiest way to create a new Backtest Kit trading bot project. Like create-react-app, but for algorithmic trading.
The fastest way to bootstrap a new trading bot project. Instead of manually setting up dependencies, configurations, and boilerplate code, simply run one command and get a working project with LLM-powered strategy, multi-timeframe technical analysis, and risk management validation.
npx -y @backtest-kit/sidekick my-trading-bot
cd my-trading-bot
npm start
For language models: Read extended description in ./LLMs.md
350+ tests cover validation, recovery, reports, and events.
Fork/PR on GitHub.
MIT Β© tripolskypetr