backtest-kit

🧿 Backtest Kit

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.

screenshot

Ask DeepWiki npm TypeScript Build

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
  • πŸš€ Production-Ready: Seamless switch between backtest/live modes; identical code across environments.
  • πŸ’Ύ Crash-Safe: Atomic persistence recovers states after crashes, preventing duplicates or losses.
  • βœ… Validation: Checks signals for TP/SL logic, risk/reward ratios, and portfolio limits.
  • πŸ”„ Efficient Execution: Streaming architecture for large datasets; VWAP pricing for realism.
  • πŸ€– AI Integration: LLM-powered strategy generation (Optimizer) with multi-timeframe analysis.
  • πŸ“Š Reports & Metrics: Auto Markdown reports with PNL, Sharpe Ratio, win rate, and more.
  • πŸ›‘οΈ Risk Management: Custom rules for position limits, time windows, and multi-strategy coordination.
  • πŸ”Œ Pluggable: Custom data sources (CCXT), persistence (file/Redis), and sizing calculators.
  • πŸ—ƒοΈ Transactional Live Orders: Broker adapter intercepts every trade mutation before internal state changes β€” exchange rejection rolls back the operation atomically.
  • πŸ§ͺ Tested: 350+ unit/integration tests for validation, recovery, and events.
  • πŸ”“ Self hosted: Zero dependency on third-party node_modules or platforms; run entirely in your own environment.

With the calculation of PnL

  • Market/Limit entries
  • TP/SL/OCO exits
  • Grid with auto-cancel on unmet conditions
  • Partial profit/loss levels
  • Trailing stop-loss
  • Breakeven protection
  • Stop limit entries (before OCO)
  • Dollar cost averaging
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));
  • Use listenRisk, listenError, listenPartialProfit/Loss for alerts.
  • Dump reports: 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.

These three functions work together to dynamically manage the position. To reduce position linearity, by default, each DCA entry is formatted as a fixed unit of $100. This can be changed. No mathematical knowledge is required.

Public API:

  • commitAverageBuy β€” adds a new DCA entry. For LONG, only accepted when current price is below a new low. Silently rejected otherwise. This prevents averaging up. Can be overridden using setConfig
  • commitPartialProfit β€” closes X% of the position at a profit. Locks in gains while keeping exposure.
  • commitPartialLoss β€” closes X% of the position at a loss. Cuts exposure before the stop-loss is hit.
The Math

Scenario: LONG entry @ 1000, 4 DCA attempts (1 rejected), 3 partials, closed at TP. totalInvested = $400 (4 Γ— $100, rejected attempt not counted).

Entries

  entry#1 @ 1000  β†’ 0.10000 coins
commitPartialProfit(30%) @ 1150 ← cnt=1
entry#2 @ 950 β†’ 0.10526 coins
entry#3 @ 880 β†’ 0.11364 coins
commitPartialLoss(20%) @ 860 ← cnt=3
entry#4 @ 920 β†’ 0.10870 coins
commitPartialProfit(40%) @ 1050 ← cnt=4
entry#5 @ 980 βœ— REJECTED (980 > ep3β‰ˆ929.92)
totalInvested = $400

Partial#1 β€” commitPartialProfit @ 1150, 30%, cnt=1

  effectivePrice = hm(1000) = 1000
costBasis = $100
partialDollarValue = 30% Γ— 100 = $30 β†’ weight = 30/400 = 0.075
pnl = (1150βˆ’1000)/1000 Γ— 100 = +15.00%
costBasis β†’ $70
coins sold: 0.03000 Γ— 1150 = $34.50
remaining: 0.07000

DCA after Partial#1

  entry#2 @ 950  (950 < ep1=1000 βœ“ accepted)
entry#3 @ 880 (880 < ep1=1000 βœ“ accepted)
coins: 0.07000 + 0.10526 + 0.11364 = 0.28890

Partial#2 β€” commitPartialLoss @ 860, 20%, cnt=3

  costBasis = 70 + 100 + 100 = $270
ep2 = 270 / 0.28890 β‰ˆ 934.58
partialDollarValue = 20% Γ— 270 = $54 β†’ weight = 54/400 = 0.135
pnl = (860βˆ’934.58)/934.58 Γ— 100 β‰ˆ βˆ’7.98%
costBasis β†’ $216
coins sold: 0.05778 Γ— 860 = $49.69
remaining: 0.23112

DCA after Partial#2

  entry#4 @ 920  (920 < ep2=934.58 βœ“ accepted)
coins: 0.23112 + 0.10870 = 0.33982

Partial#3 β€” commitPartialProfit @ 1050, 40%, cnt=4

  costBasis = 216 + 100 = $316
ep3 = 316 / 0.33982 β‰ˆ 929.92
partialDollarValue = 40% Γ— 316 = $126.4 β†’ weight = 126.4/400 = 0.316
pnl = (1050βˆ’929.92)/929.92 Γ— 100 β‰ˆ +12.91%
costBasis β†’ $189.6
coins sold: 0.13593 Γ— 1050 = $142.72
remaining: 0.20389

DCA after Partial#3 β€” rejected

  entry#5 @ 980  (980 > ep3β‰ˆ929.92 βœ— REJECTED)

Close at TP @ 1200

  ep_final = ep3 β‰ˆ 929.92  (no new entries)
coins: 0.20389

remainingDollarValue = 400 βˆ’ 30 βˆ’ 54 βˆ’ 126.4 = $189.6
weight = 189.6/400 = 0.474
pnl = (1200βˆ’929.92)/929.92 Γ— 100 β‰ˆ +29.04%
coins sold: 0.20389 Γ— 1200 = $244.67

Result (toProfitLossDto)

  0.075 Γ— (+15.00) = +1.125
0.135 Γ— (βˆ’7.98) = βˆ’1.077
0.316 Γ— (+12.91) = +4.080
0.474 Γ— (+29.04) = +13.765
─────────────────────────────
β‰ˆ +17.89%

Cross-check (coins):
34.50 + 49.69 + 142.72 + 244.67 = $471.58
(471.58 βˆ’ 400) / 400 Γ— 100 = +17.90% βœ“

priceOpen is the harmonic mean of all accepted DCA entries. After each partial close (commitPartialProfit or commitPartialLoss), the remaining cost basis is carried forward into the harmonic mean calculation for subsequent entries β€” so priceOpen shifts after every partial, which in turn changes whether the next commitAverageBuy call will be accepted.

Broker.useBrokerAdapter connects a live exchange (ccxt, Binance, etc.) to the framework with transaction safety. Every commit method fires before the internal position state mutates. If the exchange rejects the order, the fill times out, or the network fails, the adapter throws, the mutation is skipped, and backtest-kit retries automatically on the next tick.

The code

Spot

import ccxt from "ccxt";
import { singleshot, sleep } from "functools-kit";
import {
Broker,
IBroker,
BrokerSignalOpenPayload,
BrokerSignalClosePayload,
BrokerPartialProfitPayload,
BrokerPartialLossPayload,
BrokerTrailingStopPayload,
BrokerTrailingTakePayload,
BrokerBreakevenPayload,
BrokerAverageBuyPayload,
} from "backtest-kit";

const FILL_POLL_INTERVAL_MS = 10_000;
const FILL_POLL_ATTEMPTS = 10;

const getSpotExchange = singleshot(async () => {
const exchange = new ccxt.binance({
apiKey: process.env.BINANCE_API_KEY,
secret: process.env.BINANCE_API_SECRET,
options: {
defaultType: "spot",
adjustForTimeDifference: true,
recvWindow: 60000,
},
enableRateLimit: true,
});
await exchange.loadMarkets();
return exchange;
});

/**
* Place a limit order and poll until filled (status === "closed").
* On timeout: cancel the order, sell any partial fill via market to rollback cleanly,
* then throw β€” backtest-kit will retry the commit against a clean exchange state.
*/
async function createLimitOrderAndWait(
exchange: ccxt.binance,
symbol: string,
side: "buy" | "sell",
qty: number,
price: number
): Promise<void> {
const order = await exchange.createOrder(symbol, "limit", side, qty, price);

for (let i = 0; i !== FILL_POLL_ATTEMPTS; i++) {
await sleep(FILL_POLL_INTERVAL_MS);
const status = await exchange.fetchOrder(order.id, symbol);
if (status.status === "closed") {
return;
}
}

// Cancel the remaining open portion
await exchange.cancelOrder(order.id, symbol);

// Check how much was partially filled
const final = await exchange.fetchOrder(order.id, symbol);
const filledQty = final.filled ?? 0;

if (filledQty > 0) {
// Sell partial fill via market to restore clean exchange state before backtest-kit retries
const rollbackSide = side === "buy" ? "sell" : "buy";
await exchange.createOrder(symbol, "market", rollbackSide, filledQty);
}

throw new Error(`Limit order ${order.id} [${side} ${qty} ${symbol} @ ${price}] not filled in time β€” partial fill rolled back, backtest-kit will retry`);
}

Broker.useBrokerAdapter(
class implements IBroker {

async waitForInit(): Promise<void> {
await getSpotExchange();
}

async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
const { symbol, cost, priceOpen, priceTakeProfit, priceStopLoss, position } = payload;

// Spot does not support short selling β€” reject immediately so backtest-kit skips the mutation
if (position === "short") {
throw new Error(`SpotBrokerAdapter: short position is not supported on spot (symbol=${symbol})`);
}

const exchange = await getSpotExchange();

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / priceOpen));
const openPrice = parseFloat(exchange.priceToPrecision(symbol, priceOpen));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));

// Entry: limit buy, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, "buy", qty, openPrice);

// Take-profit: resting limit sell, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "limit", "sell", qty, tpPrice);

// Stop-loss: resting stop-limit sell, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "stop_loss_limit", "sell", qty, slPrice, { stopPrice: slPrice });
}

async onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void> {
const { symbol, currentPrice } = payload;
const exchange = await getSpotExchange();

// Cancel all resting SL/TP orders β€” throws on exchange error, backtest-kit retries
const orders = await exchange.fetchOpenOrders(symbol);
for (const order of orders) {
await exchange.cancelOrder(order.id, symbol);
}

const balance = await exchange.fetchBalance();
const base = symbol.replace(/USDT|BUSD|BTC|ETH$/, "");
const qty = parseFloat(String(balance?.free?.[base] ?? 0));

if (qty > 0) {
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
// Close: limit sell, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, "sell", qty, closePrice);
}
}

async onPartialProfitCommit(payload: BrokerPartialProfitPayload): Promise<void> {
const { symbol, cost, currentPrice } = payload;
const exchange = await getSpotExchange();

// Cancel all resting orders before partial close β€” throws on exchange error, backtest-kit retries
const orders = await exchange.fetchOpenOrders(symbol);
for (const order of orders) {
await exchange.cancelOrder(order.id, symbol);
}

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / currentPrice));
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));

// Partial close: limit sell, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, "sell", qty, closePrice);
}

async onPartialLossCommit(payload: BrokerPartialLossPayload): Promise<void> {
const { symbol, cost, currentPrice } = payload;
const exchange = await getSpotExchange();

// Cancel all resting orders before partial close β€” throws on exchange error, backtest-kit retries
const orders = await exchange.fetchOpenOrders(symbol);
for (const order of orders) {
await exchange.cancelOrder(order.id, symbol);
}

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / currentPrice));
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));

// Partial close: limit sell, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, "sell", qty, closePrice);
}

async onTrailingStopCommit(payload: BrokerTrailingStopPayload): Promise<void> {
const { symbol, newStopLossPrice } = payload;
const exchange = await getSpotExchange();

// Cancel existing SL order only β€” TP stays untouched
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) => ["stop_loss_limit", "stop", "STOP_LOSS_LIMIT"].includes(o.type ?? "")) ?? null;
if (slOrder) {
await exchange.cancelOrder(slOrder.id, symbol);
}

const balance = await exchange.fetchBalance();
const base = symbol.replace(/USDT|BUSD|BTC|ETH$/, "");
const qty = parseFloat(String(balance?.free?.[base] ?? 0));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));

// Updated SL: resting stop-limit sell, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "stop_loss_limit", "sell", qty, slPrice, { stopPrice: slPrice });
}

async onTrailingTakeCommit(payload: BrokerTrailingTakePayload): Promise<void> {
const { symbol, newTakeProfitPrice } = payload;
const exchange = await getSpotExchange();

// Cancel existing TP order only β€” SL stays untouched
const orders = await exchange.fetchOpenOrders(symbol);
const tpOrder = orders.find((o) => ["limit", "LIMIT"].includes(o.type ?? "")) ?? null;
if (tpOrder) {
await exchange.cancelOrder(tpOrder.id, symbol);
}

const balance = await exchange.fetchBalance();
const base = symbol.replace(/USDT|BUSD|BTC|ETH$/, "");
const qty = parseFloat(String(balance?.free?.[base] ?? 0));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, newTakeProfitPrice));

// Updated TP: resting limit sell, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "limit", "sell", qty, tpPrice);
}

async onBreakevenCommit(payload: BrokerBreakevenPayload): Promise<void> {
const { symbol, newStopLossPrice } = payload;
const exchange = await getSpotExchange();

// Cancel existing SL order only β€” TP stays untouched
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) => ["stop_loss_limit", "stop", "STOP_LOSS_LIMIT"].includes(o.type ?? "")) ?? null;
if (slOrder) {
await exchange.cancelOrder(slOrder.id, symbol);
}

const balance = await exchange.fetchBalance();
const base = symbol.replace(/USDT|BUSD|BTC|ETH$/, "");
const qty = parseFloat(String(balance?.free?.[base] ?? 0));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));

// Breakeven SL: resting stop-limit sell at entry price, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "stop_loss_limit", "sell", qty, slPrice, { stopPrice: slPrice });
}

async onAverageBuyCommit(payload: BrokerAverageBuyPayload): Promise<void> {
const { symbol, currentPrice, cost } = payload;
const exchange = await getSpotExchange();

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / currentPrice));
const entryPrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));

// DCA entry: limit buy, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, "buy", qty, entryPrice);
}
}
);

Broker.enable();

Futures

import ccxt from "ccxt";
import { singleshot, sleep } from "functools-kit";
import {
Broker,
IBroker,
BrokerSignalOpenPayload,
BrokerSignalClosePayload,
BrokerPartialProfitPayload,
BrokerPartialLossPayload,
BrokerTrailingStopPayload,
BrokerTrailingTakePayload,
BrokerBreakevenPayload,
BrokerAverageBuyPayload,
} from "backtest-kit";

const FILL_POLL_INTERVAL_MS = 10_000;
const FILL_POLL_ATTEMPTS = 10;

const getFuturesExchange = singleshot(async () => {
const exchange = new ccxt.binance({
apiKey: process.env.BINANCE_API_KEY,
secret: process.env.BINANCE_API_SECRET,
options: {
defaultType: "future",
adjustForTimeDifference: true,
recvWindow: 60000,
},
enableRateLimit: true,
});
await exchange.loadMarkets();
return exchange;
});

/**
* Place a limit order and poll until filled (status === "closed").
* On timeout: cancel the order, sell any partial fill via market to rollback cleanly,
* then throw β€” backtest-kit will retry the commit against a clean exchange state.
*/
async function createLimitOrderAndWait(
exchange: ccxt.binance,
symbol: string,
side: "buy" | "sell",
qty: number,
price: number
): Promise<void> {
const order = await exchange.createOrder(symbol, "limit", side, qty, price);

for (let i = 0; i !== FILL_POLL_ATTEMPTS; i++) {
await sleep(FILL_POLL_INTERVAL_MS);
const status = await exchange.fetchOrder(order.id, symbol);
if (status.status === "closed") {
return;
}
}

// Cancel the remaining open portion
await exchange.cancelOrder(order.id, symbol);

// Check how much was partially filled
const final = await exchange.fetchOrder(order.id, symbol);
const filledQty = final.filled ?? 0;

if (filledQty > 0) {
// Close partial fill via market to restore clean exchange state before backtest-kit retries
const rollbackSide = side === "buy" ? "sell" : "buy";
await exchange.createOrder(symbol, "market", rollbackSide, filledQty, undefined, { reduceOnly: true });
}

throw new Error(`Limit order ${order.id} [${side} ${qty} ${symbol} @ ${price}] not filled in time β€” partial fill rolled back, backtest-kit will retry`);
}

Broker.useBrokerAdapter(
class implements IBroker {

async waitForInit(): Promise<void> {
await getFuturesExchange();
}

async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
const { symbol, cost, priceOpen, priceTakeProfit, priceStopLoss, position } = payload;
const exchange = await getFuturesExchange();

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / priceOpen));
const openPrice = parseFloat(exchange.priceToPrecision(symbol, priceOpen));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));

const entrySide = position === "long" ? "buy" : "sell";
const exitSide = position === "long" ? "sell" : "buy";

// Entry: limit order, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, entrySide, qty, openPrice);

// Take-profit: resting limit on exit side, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "limit", exitSide, qty, tpPrice, { reduceOnly: true });

// Stop-loss: resting stop-market on exit side, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "stop_market", exitSide, qty, undefined, { stopPrice: slPrice, reduceOnly: true });
}

async onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void> {
const { symbol, position, currentPrice } = payload;
const exchange = await getFuturesExchange();

// Cancel all resting SL/TP orders β€” throws on exchange error, backtest-kit retries
const orders = await exchange.fetchOpenOrders(symbol);
for (const order of orders) {
await exchange.cancelOrder(order.id, symbol);
}

const positions = await exchange.fetchPositions([symbol]);
const pos = positions.find((p) => p.symbol === symbol);
const qty = Math.abs(parseFloat(String(pos?.contracts ?? 0)));
const exitSide = position === "long" ? "sell" : "buy";

if (qty > 0) {
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
// Close: limit order on exit side, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, exitSide, qty, closePrice);
}
}

async onPartialProfitCommit(payload: BrokerPartialProfitPayload): Promise<void> {
const { symbol, cost, currentPrice, position } = payload;
const exchange = await getFuturesExchange();

// Cancel all resting orders before partial close β€” throws on exchange error, backtest-kit retries
const orders = await exchange.fetchOpenOrders(symbol);
for (const order of orders) {
await exchange.cancelOrder(order.id, symbol);
}

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / currentPrice));
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const exitSide = position === "long" ? "sell" : "buy";

// Partial close: limit order on exit side, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, exitSide, qty, closePrice);
}

async onPartialLossCommit(payload: BrokerPartialLossPayload): Promise<void> {
const { symbol, cost, currentPrice, position } = payload;
const exchange = await getFuturesExchange();

// Cancel all resting orders before partial close β€” throws on exchange error, backtest-kit retries
const orders = await exchange.fetchOpenOrders(symbol);
for (const order of orders) {
await exchange.cancelOrder(order.id, symbol);
}

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / currentPrice));
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const exitSide = position === "long" ? "sell" : "buy";

// Partial close: limit order on exit side, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, exitSide, qty, closePrice);
}

async onTrailingStopCommit(payload: BrokerTrailingStopPayload): Promise<void> {
const { symbol, newStopLossPrice, position } = payload;
const exchange = await getFuturesExchange();

// Cancel existing SL order only β€” TP stays untouched
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) => ["stop_market", "stop", "STOP_MARKET"].includes(o.type ?? "")) ?? null;
if (slOrder) {
await exchange.cancelOrder(slOrder.id, symbol);
}

const positions = await exchange.fetchPositions([symbol]);
const pos = positions.find((p) => p.symbol === symbol);
const qty = Math.abs(parseFloat(String(pos?.contracts ?? 0)));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));
const exitSide = position === "long" ? "sell" : "buy";

// Updated SL: resting stop-market on exit side, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "stop_market", exitSide, qty, undefined, { stopPrice: slPrice, reduceOnly: true });
}

async onTrailingTakeCommit(payload: BrokerTrailingTakePayload): Promise<void> {
const { symbol, newTakeProfitPrice, position } = payload;
const exchange = await getFuturesExchange();

// Cancel existing TP order only β€” SL stays untouched
const orders = await exchange.fetchOpenOrders(symbol);
const tpOrder = orders.find((o) => ["limit", "LIMIT"].includes(o.type ?? "")) ?? null;
if (tpOrder) {
await exchange.cancelOrder(tpOrder.id, symbol);
}

const positions = await exchange.fetchPositions([symbol]);
const pos = positions.find((p) => p.symbol === symbol);
const qty = Math.abs(parseFloat(String(pos?.contracts ?? 0)));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, newTakeProfitPrice));
const exitSide = position === "long" ? "sell" : "buy";

// Updated TP: resting limit on exit side, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "limit", exitSide, qty, tpPrice, { reduceOnly: true });
}

async onBreakevenCommit(payload: BrokerBreakevenPayload): Promise<void> {
const { symbol, newStopLossPrice, position } = payload;
const exchange = await getFuturesExchange();

// Cancel existing SL order only β€” TP stays untouched
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) => ["stop_market", "stop", "STOP_MARKET"].includes(o.type ?? "")) ?? null;
if (slOrder) {
await exchange.cancelOrder(slOrder.id, symbol);
}

const positions = await exchange.fetchPositions([symbol]);
const pos = positions.find((p) => p.symbol === symbol);
const qty = Math.abs(parseFloat(String(pos?.contracts ?? 0)));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));
const exitSide = position === "long" ? "sell" : "buy";

// Breakeven SL: resting stop-market at entry price on exit side, accepted synchronously by Binance REST
await exchange.createOrder(symbol, "stop_market", exitSide, qty, undefined, { stopPrice: slPrice, reduceOnly: true });
}

async onAverageBuyCommit(payload: BrokerAverageBuyPayload): Promise<void> {
const { symbol, currentPrice, cost, position } = payload;
const exchange = await getFuturesExchange();

const qty = parseFloat(exchange.amountToPrecision(symbol, cost / currentPrice));
const entryPrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const entrySide = position === "long" ? "buy" : "sell";

// DCA entry: limit order, waits for fill β€” partial fill rolled back on timeout, backtest-kit retries
await createLimitOrderAndWait(exchange, symbol, entrySide, qty, entryPrice);
}
}
);

Broker.enable();

Signal open/close events are routed automatically via an internal event bus once Broker.enable() is called. No manual wiring needed. All other operations (partialProfit, trailingStop, breakeven, averageBuy) are intercepted explicitly before the corresponding state mutation.

backtest-kit uses Node.js AsyncLocalStorage to automatically provide temporal time context to your strategies.

The Math

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:

  • All timestamps are aligned down to interval boundary
  • First candle.timestamp must equal aligned since
  • Adapter must return exactly limit candles
  • Sequential timestamps: since + i * stepMs

Order book fetching uses the same temporal alignment as candles, but with a configurable time offset window instead of candle intervals.

The Math
**Time range calculation:**
- `when` = current execution context time (from AsyncLocalStorage)
- `offsetMinutes` = `CC_ORDER_BOOK_TIME_OFFSET_MINUTES` (configurable)
- `alignedTo` = `Math.floor(when / (offsetMinutes * 60000)) * (offsetMinutes * 60000)`
- `to` = `alignedTo` (aligned down to offset boundary)
- `from` = `alignedTo - offsetMinutes * 60000`

**Adapter contract:**
- `getOrderBook(symbol, depth, from, to, backtest)` is called on the exchange schema
- `depth` defaults to `CC_ORDER_BOOK_MAX_DEPTH_LEVELS`
- The `from`/`to` range represents a time window of exactly `offsetMinutes` duration
- Schema implementation may use the time range (backtest) or ignore it (live trading)

**Example with CC_ORDER_BOOK_TIME_OFFSET_MINUTES = 10:**
```
when = 1704067920000       // 2024-01-01 00:12:00 UTC
offsetMinutes = 10
offsetMs = 10 * 60000      // 600000ms

alignedTo = Math.floor(1704067920000 / 600000) * 600000
          = 1704067800000  // 2024-01-01 00:10:00 UTC

to   = 1704067800000       // 00:10:00 UTC
from = 1704067200000       // 00:00:00 UTC
```

Unlike candles, most exchanges (e.g. Binance GET /api/v3/depth) only expose the current order book with no historical query support β€” for backtest you must provide your own snapshot storage.

Key principles:

  • Time range is aligned down to CC_ORDER_BOOK_TIME_OFFSET_MINUTES boundary
  • to = aligned timestamp, from = to - offsetMinutes * 60000
  • depth defaults to CC_ORDER_BOOK_MAX_DEPTH_LEVELS
  • Adapter receives (symbol, depth, from, to, backtest) β€” may ignore from/to in live mode

Aggregated trades fetching uses the same look-ahead bias protection as candles - to is always aligned down to the nearest minute boundary so future trades are never visible to the strategy.

Key principles:

  • to is always aligned down to the 1-minute boundary β€” prevents look-ahead bias
  • Without limit: returns one full window (CC_AGGREGATED_TRADES_MAX_MINUTES)
  • With limit: paginates backwards until collected, then slices to most recent limit
  • Adapter receives (symbol, from, to, backtest) β€” may ignore from/to in live mode
The Math

Time range calculation:

  • when = current execution context time (from AsyncLocalStorage)
  • alignedTo = Math.floor(when / 60000) * 60000 (aligned down to 1-minute boundary)
  • windowMs = CC_AGGREGATED_TRADES_MAX_MINUTES * 60000 βˆ’ 60000
  • to = alignedTo, from = alignedTo βˆ’ windowMs

Without limit: fetches a single window and returns it as-is.

With limit: paginates backwards in CC_AGGREGATED_TRADES_MAX_MINUTES chunks until at least limit trades are collected, then slices to the most recent limit trades.

Example with CC_AGGREGATED_TRADES_MAX_MINUTES = 60, limit = 200:

when       = 1704067920000   // 2024-01-01 00:12:00 UTC
alignedTo = 1704067800000 // 2024-01-01 00:12:00 β†’ aligned to 00:12:00
windowMs = 59 * 60000 // 3540000ms = 59 minutes

Window 1: from = 00:12:00 βˆ’ 59m = 23:13:00
to = 00:12:00
β†’ got 120 trades β€” not enough

Window 2: from = 23:13:00 βˆ’ 59m = 22:14:00
to = 23:13:00
β†’ got 100 more β†’ total 220 trades

result = last 200 of 220 (most recent)

Adapter contract:

  • getAggregatedTrades(symbol, from, to, backtest) is called on the exchange schema
  • from/to are Date objects
  • Schema implementation may use the time range (backtest) or ignore it (live trading)

Compatible with: garch for volatility modelling and volume-anomaly for detecting abnormal trade volume β€” both accept the same from/to time range format that getAggregatedTrades produces.

Why 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 count
  • βœ… getNextCandles() - validates first timestamp and count
  • βœ… getRawCandles() - validates first timestamp and count
  • βœ… Cache read - calculates exact expected timestamps
  • βœ… Cache write - stores validated candles

Result: Deterministic candle retrieval with exact timestamp matching.

All candle timestamp alignment uses UTC (Unix epoch). For intervals like 4h, boundaries are 00:00, 04:00, 08:00, 12:00, 16:00, 20:00 UTC. If your local timezone offset is not a multiple of the interval, the since timestamps will look "uneven" in local time.

For example, in UTC+5 the same 4h candle request logs as:

since: Sat Sep 20 2025 13:00:00 GMT+0500  ← looks uneven (13:00)
since: Sat Sep 20 2025 17:00:00 GMT+0500 ← looks uneven (17:00)
since: Sat Sep 20 2025 21:00:00 GMT+0500 ← looks uneven (21:00)
since: Sun Sep 21 2025 05:00:00 GMT+0500 ← looks uneven (05:00)

But in UTC these are perfectly aligned 4h boundaries:

since: Sat, 20 Sep 2025 08:00:00 GMT  ← 08:00 UTC βœ“
since: Sat, 20 Sep 2025 12:00:00 GMT ← 12:00 UTC βœ“
since: Sat, 20 Sep 2025 16:00:00 GMT ← 16:00 UTC βœ“
since: Sun, 21 Sep 2025 00:00:00 GMT ← 00:00 UTC βœ“

Use toUTCString() or toISOString() in callbacks to see the actual aligned UTC times.

  • getCandles() always returns data UP TO the current backtest timestamp using async_hooks
  • Multi-timeframe data is automatically synchronized
  • Impossible to introduce look-ahead bias - all time boundaries are enforced
  • Same code works in both backtest and live modes
  • Boundary semantics prevent edge cases in signal generation

Backtest 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

  • No C#/C++ required - pure TypeScript/JavaScript
  • Self-hosted - your code, your data, your infrastructure
  • No platform fees or hidden costs
  • Full control over execution and data sources
  • GUI for visualization and monitoring

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/cli package is a zero-boilerplate CLI runner for backtest-kit strategies. Point it at your strategy file and run backtests, paper trading, or live bots β€” no infrastructure code required.

  • πŸš€ Zero Config: Run a backtest with one command β€” no setup code needed
  • πŸ”„ Three Modes: --backtest, --paper, --live with graceful SIGINT shutdown
  • πŸ’Ύ Auto Cache: Warms OHLCV candle cache for all intervals before the backtest starts
  • 🌐 Web Dashboard: Launch @backtest-kit/ui with a single --ui flag
  • πŸ“¬ Telegram Alerts: Formatted trade notifications with price charts via --telegram
  • πŸ—‚οΈ Monorepo Ready: Each strategy's dump/, modules/, and template/ are automatically isolated by entry point directory

The fastest way to run any backtest-kit strategy from the command line. Instead of writing boilerplate for storage, notifications, candle caching, and signal logging, add one dependency and wire up your package.json scripts. Works equally well for a single-strategy project or a monorepo with dozens of strategies in separate subdirectories.

npx -y @backtest-kit/cli --init

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 Script v5/v6: Native TradingView syntax with 1:1 compatibility
  • 🎯 60+ Indicators: SMA, EMA, RSI, MACD, Bollinger Bands, ATR, Stochastic built-in
  • πŸ“ File or Code: Load .pine files or pass code strings directly
  • πŸ—ΊοΈ Plot Extraction: Flexible mapping from Pine plot() outputs to structured signals
  • ⚑ Cached Execution: Memoized file reads for repeated strategy runs

Perfect 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/graph package lets you compose backtest-kit computations as a typed directed acyclic graph (DAG). Define source nodes that fetch market data and output nodes that compute derived values β€” then resolve the whole graph in topological order with automatic parallelism.

  • πŸ”Œ DAG Execution: Nodes are resolved bottom-up in topological order with Promise.all parallelism
  • πŸ”’ Type-Safe Values: TypeScript infers the return type of every node through the graph via generics
  • 🧱 Two APIs: Low-level INode for runtime/storage, high-level sourceNode + outputNode builders for authoring
  • πŸ’Ύ DB-Ready Serialization: serialize / deserialize convert the graph to a flat IFlatNode[] list with id / nodeIds
  • 🌐 Context-Aware Fetch: sourceNode receives (symbol, when, exchangeName) from the execution context automatically

Perfect for multi-timeframe strategies where multiple Pine Script or indicator computations must be combined. Instead of manually chaining async calls, define each computation as a node and let the graph resolve dependencies in parallel. Adding a new filter or timeframe requires no changes to the existing wiring.

npm install @backtest-kit/graph 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.

  • πŸ“ˆ Interactive Charts: Candlestick visualization with Lightweight Charts (1m, 15m, 1h timeframes)
  • 🎯 Signal Tracking: View opened, closed, scheduled, and cancelled signals with full details
  • πŸ“Š Risk Analysis: Monitor risk rejections and position management
  • πŸ”” Notifications: Real-time notification system for all trading events
  • πŸ’Ή Trailing & Breakeven: Visualize trailing stop/take and breakeven events
  • 🎨 Material Design: Beautiful UI with MUI 5 and Mantine components

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.

  • πŸ”Œ 10+ LLM Providers: OpenAI, Claude, DeepSeek, Grok, Mistral, Perplexity, Cohere, Alibaba, Hugging Face, Ollama
  • πŸ”„ Token Rotation: Automatic API key rotation for Ollama (others throw clear errors)
  • 🎯 Structured Output: Enforced JSON schema for trading signals (position, price levels, risk notes)
  • πŸ”‘ Flexible Auth: Context-based API keys or environment variables
  • ⚑ Unified API: Single interface across all providers
  • πŸ“Š Trading-First: Built for backtest-kit with position sizing and risk management

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.

  • πŸ“ˆ Multi-Timeframe Analysis: 1m, 15m, 30m, 1h with synchronized indicator computation
  • 🎯 50+ Technical Indicators: RSI, MACD, Bollinger Bands, Stochastic, ADX, ATR, CCI, Fibonacci, Support/Resistance
  • πŸ“Š Order Book Analysis: Bid/ask depth, spread, liquidity imbalance, top 20 levels
  • πŸ€– AI-Ready Output: Markdown reports formatted for LLM context injection
  • ⚑ Performance Optimized: Intelligent caching with configurable TTL per timeframe

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.

  • πŸš€ Zero Config: Get started with one command - no setup required
  • πŸ“¦ Complete Template: Includes backtest strategy, risk management, and LLM integration
  • πŸ€– AI-Powered: Pre-configured with DeepSeek, Claude, and GPT-5 fallback chain
  • πŸ“Š Technical Analysis: Built-in 50+ indicators via @backtest-kit/signals
  • πŸ”‘ Environment Setup: Auto-generated .env with all API key placeholders
  • πŸ“ Best Practices: Production-ready code structure with examples

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

450+ tests cover validation, recovery, reports, and events.

Fork/PR on GitHub.

MIT Β© tripolskypetr