Purpose: This guide provides a minimal working example to get you started with backtest-kit. You will learn how to register components (exchange, strategy, frame), run a backtest, and listen to events. After completing this guide, you will have a functioning backtest that generates trading signals and calculates performance metrics.
Scope: This guide covers only the essential steps to run your first backtest. For detailed explanations of execution modes, see Execution Modes. For signal lifecycle details, see Signal Lifecycle Overview. For component registration details, see Component Registration.
Here's a complete working example that demonstrates the core workflow:
import {
addExchange,
addStrategy,
addFrame,
Backtest,
listenSignalBacktest,
listenDoneBacktest,
getAveragePrice,
} from "backtest-kit";
import ccxt from "ccxt";
// 1. Register exchange data source
addExchange({
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: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
});
// 2. Register trading strategy
addStrategy({
strategyName: "simple-breakout",
interval: "5m",
getSignal: async (symbol) => {
const price = await getAveragePrice(symbol);
return {
position: "long",
note: "Breakout signal",
priceTakeProfit: price * 1.02, // 2% profit target
priceStopLoss: price * 0.98, // 2% stop loss
minuteEstimatedTime: 60,
};
},
});
// 3. Register timeframe for backtesting
addFrame({
frameName: "1d-backtest",
interval: "1m",
startDate: new Date("2024-01-01T00:00:00Z"),
endDate: new Date("2024-01-02T00:00:00Z"),
});
// 4. Listen to events
listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log(`Signal closed: ${event.pnl.pnlPercentage}%`);
}
});
listenDoneBacktest((event) => {
console.log("Backtest completed:", event.symbol);
Backtest.dump(event.strategyName);
});
// 5. Run backtest in background
Backtest.background("BTCUSDT", {
strategyName: "simple-breakout",
exchangeName: "binance",
frameName: "1d-backtest",
});
The framework uses a registration-first architecture. You must register all components before execution using the add* functions:
| Function | Purpose | Key Parameters | File Reference |
|---|---|---|---|
addExchange() |
Register data source (CCXT, database, API) | exchangeName, getCandles, formatPrice, formatQuantity |
src/function/add.ts:99-111 |
addStrategy() |
Register trading logic | strategyName, interval, getSignal, callbacks |
src/function/add.ts:50-62 |
addFrame() |
Register backtest timeframe | frameName, interval, startDate, endDate |
src/function/add.ts:143-149 |
addRisk() |
Register risk management rules | riskName, validations, callbacks |
src/function/add.ts:329-341 |
addWalker() |
Register strategy comparison | walkerName, strategies, metric |
src/function/add.ts:188-200 |
The exchange provides candle data and price/quantity formatting. You can use CCXT for live data or a database for faster backtesting:
// Option 1: CCXT (live or historical)
import ccxt from "ccxt";
addExchange({
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: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
});
// Option 2: Database (faster backtesting)
addExchange({
exchangeName: "binance-db",
getCandles: async (symbol, interval, since, limit) => {
return await db.query(`
SELECT timestamp, open, high, low, close, volume
FROM candles
WHERE symbol = $1 AND interval = $2 AND timestamp >= $3
ORDER BY timestamp ASC LIMIT $4
`, [symbol, interval, since, limit]);
},
formatPrice: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
});
Key Concepts:
getCandles() must return array of ICandleData objects with timestamp, open, high, low, close, volumeformatPrice() and formatQuantity() apply exchange-specific precision rulesexchangeName by ExchangeConnectionServiceThe strategy defines signal generation logic. The getSignal() function returns trade parameters or null:
addStrategy({
strategyName: "my-strategy",
interval: "5m", // Minimum time between getSignal() calls
getSignal: async (symbol) => {
const price = await getAveragePrice(symbol);
// Return null for no signal
if (someCondition) return null;
// Return signal for immediate entry (market order)
return {
position: "long",
note: "Breakout signal",
priceTakeProfit: price * 1.02,
priceStopLoss: price * 0.98,
minuteEstimatedTime: 60,
};
// Or return signal with priceOpen for delayed entry (limit order)
return {
position: "long",
note: "Limit order",
priceOpen: price * 0.99, // Wait for 1% pullback
priceTakeProfit: price * 1.02,
priceStopLoss: price * 0.98,
minuteEstimatedTime: 60,
};
},
callbacks: {
onOpen: (symbol, signal, currentPrice, backtest) => {
console.log(`Signal opened: ${signal.id}`);
},
onClose: (symbol, signal, priceClose, backtest) => {
console.log(`Signal closed: ${priceClose}`);
},
},
});
Signal Validation Rules (automatic):
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT, CC_MAX_STOPLOSS_DISTANCE_PERCENTCC_MAX_SIGNAL_LIFETIME_MINUTESThe frame defines the backtest period and timestamp generation interval:
addFrame({
frameName: "1d-backtest",
interval: "1m", // Generate timestamps every 1 minute
startDate: new Date("2024-01-01T00:00:00Z"),
endDate: new Date("2024-01-02T00:00:00Z"),
callbacks: {
onTimeframe: (timeframe, startDate, endDate, interval) => {
console.log(`Generated ${timeframe.length} timestamps`);
},
},
});
Supported Intervals: "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d"
The framework provides two execution modes:
Runs backtest asynchronously with event-based output:
// Start backtest in background
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest",
});
// Listen to signals
listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log(`PNL: ${event.pnl.pnlPercentage}%`);
}
});
// Listen to completion
listenDoneBacktest((event) => {
console.log("Backtest completed:", event.symbol);
Backtest.dump(event.strategyName); // Save markdown report
});
Use async generator for manual iteration and early termination:
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest",
})) {
console.log(`PNL: ${result.pnl.pnlPercentage}%`);
// Early termination example
if (result.pnl.pnlPercentage < -5) {
console.log("Stop loss threshold reached");
break;
}
}
Signals transition through states during execution:
State Transitions:
| From State | To State | Trigger | Event Emitted |
|---|---|---|---|
idle |
scheduled |
getSignal() returns signal with priceOpen |
onSchedule() |
idle |
opened |
getSignal() returns signal without priceOpen |
onOpen() |
scheduled |
opened |
Price reaches priceOpen |
onOpen() |
scheduled |
cancelled |
Timeout or stop loss hit before entry | onCancel() |
opened |
active |
Position being monitored | onActive() |
active |
closed |
TP/SL hit or time expired | onClose() |
The framework emits events through RxJS-style Subject emitters:
import {
listenSignalBacktest,
listenDoneBacktest,
listenError,
} from "backtest-kit";
// Listen to all backtest signals
listenSignalBacktest((event) => {
switch (event.action) {
case "opened":
console.log(`Signal opened: ${event.signal.id}`);
break;
case "active":
console.log(`Signal active: ${event.signal.id}`);
break;
case "closed":
console.log(`Signal closed: ${event.closeReason}, PNL: ${event.pnl.pnlPercentage}%`);
break;
}
});
// Listen to backtest completion
listenDoneBacktest((event) => {
console.log("Backtest completed:", {
symbol: event.symbol,
strategyName: event.strategyName,
timestamp: event.timestamp,
});
});
// Listen to errors
listenError((error) => {
console.error("Error:", error);
});
Available Event Listeners:
| Function | Purpose | Event Type |
|---|---|---|
listenSignal() |
All signals (backtest + live) | IStrategyTickResult |
listenSignalBacktest() |
Backtest signals only | IStrategyTickResult |
listenSignalLive() |
Live signals only | IStrategyTickResult |
listenDoneBacktest() |
Backtest completion | DoneContract |
listenDoneLive() |
Live completion | DoneContract |
listenError() |
All errors | Error |
After backtest completion, generate and save markdown reports:
// Get statistics
const stats = await Backtest.getData("my-strategy");
console.log("Win rate:", stats.winRate);
console.log("Sharpe ratio:", stats.sharpeRatio);
console.log("Total PNL:", stats.totalPnl);
// Generate markdown report
const markdown = await Backtest.getReport("my-strategy");
console.log(markdown);
// Save to disk (default: ./logs/backtest/my-strategy.md)
await Backtest.dump("my-strategy");
// Save to custom path
await Backtest.dump("my-strategy", "./custom/path");
Report Contents:
The framework uses di-scoped for implicit context propagation. You don't need to pass context parameters explicitly:
// Inside strategy.getSignal()
const price = await getAveragePrice(symbol); // No context params needed!
// Framework automatically provides:
// - executionContext: { symbol, when: timestamp, backtest: true/false }
// - methodContext: { strategyName, exchangeName, frameName }
How It Works:
Benefits:
For details, see Context Propagation.
Now that you have a working backtest, explore these topics:
Complete Example Projects: See README.md:84-257 for full working examples including CCXT integration, database sources, and advanced features.