This document describes the public API for historical backtesting operations provided by the Backtest singleton. The Backtest API enables testing trading strategies against historical market data with statistical analysis and markdown report generation.
For live trading operations, see Live Trading API. For strategy comparison across multiple configurations, see Walker API. For component registration, see Component Registration Functions.
The Backtest API is implemented as a singleton instance of BacktestUtils exported from src/classes/Backtest.ts:231. It provides a simplified interface to the underlying backtest execution engine, with five primary methods:
| Method | Purpose | Execution Mode |
|---|---|---|
run() |
Execute backtest and yield results | Foreground AsyncGenerator |
background() |
Execute backtest silently | Background with cancellation |
getData() |
Retrieve statistics for completed backtest | Post-execution analysis |
getReport() |
Generate markdown report | Post-execution analysis |
dump() |
Save report to filesystem | Post-execution persistence |
The Backtest API operates on historical data defined by Frame schemas (see Frame Schemas) and evaluates Strategy schemas (see Strategy Schemas) sequentially through each timeframe.
The Backtest API uses a two-tier architecture with instance management:
Tier 1: BacktestUtils (Singleton Facade)
The Backtest singleton src/classes/Backtest.ts:586 is an instance of BacktestUtils that manages multiple BacktestInstance objects through memoization src/classes/Backtest.ts:359-364.
Tier 2: BacktestInstance (Per Symbol-Strategy)
Each unique symbol:strategyName combination gets its own BacktestInstance src/classes/Backtest.ts:73-333 with isolated state and execution context.
Key Flow Characteristics:
BacktestInstance per symbol:strategyName pairBacktestMarkdownService accumulates signals via signalBacktestEmitterExecutes historical backtest and yields closed signals as an AsyncGenerator.
Signature:
run(
symbol: string,
context: {
strategyName: string;
exchangeName: string;
frameName: string;
}
): AsyncGenerator<IStrategyBacktestResult, void, unknown>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol (e.g., "BTCUSDT") |
context.strategyName |
string |
Registered strategy name from addStrategy() |
context.exchangeName |
string |
Registered exchange name from addExchange() |
context.frameName |
string |
Registered frame name from addFrame() |
Return Type: AsyncGenerator<IStrategyBacktestResult>
The generator yields IStrategyBacktestResult objects for each closed signal. The result is a discriminated union with action: "closed" and includes:
interface IStrategyBacktestResult {
action: "closed";
signal: ISignalRow;
pnl: IStrategyPnL;
backtest: true;
}
Execution Flow:
Example Usage:
import { Backtest } from "backtest-kit";
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
})) {
console.log(`Signal ID: ${result.signal.id}`);
console.log(`PNL: ${result.pnl.pnlPercentage}%`);
console.log(`Close Reason: ${result.signal.closeReason}`);
}
Implementation Details:
backtestMarkdownService and scheduleMarkdownService for the strategy src/classes/Backtest.ts:52-54strategyGlobalService.clear() src/classes/Backtest.ts:57-58riskName src/classes/Backtest.ts:60-63backtestCommandService.run() src/classes/Backtest.ts:65Executes backtest silently in the background, consuming all results internally. Returns a cancellation function.
Signature:
background(
symbol: string,
context: {
strategyName: string;
exchangeName: string;
frameName: string;
}
): () => void
Parameters: Same as run() method.
Return Type: () => void - Cancellation closure
The returned function can be called to stop the backtest early. When cancelled, the backtest will:
strategyCoreService.stop() to prevent new signals_isStopped flag to break the iteration loopdoneBacktestSubject event if not already doneCancellation Flow:
Example Usage:
import { Backtest, listenDoneBacktest } from "backtest-kit";
// Start backtest in background
const cancel = Backtest.background("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
});
// Listen for completion
listenDoneBacktest((event) => {
console.log(`Backtest completed for ${event.strategyName}`);
});
// Cancel after 30 seconds
setTimeout(() => {
console.log("Cancelling backtest...");
cancel();
}, 30000);
Implementation Details:
BacktestUtils.background()BacktestInstance.background() src/classes/Backtest.ts:435task with singlerun wrapperINSTANCE_TASK_FNexitEmitter src/classes/Backtest.ts:210-212_isStopped flag checked by INSTANCE_TASK_FN src/classes/Backtest.ts:38-42Retrieves statistical analysis data from completed backtest signals for a symbol-strategy pair.
Signature:
getData(
symbol: string,
strategyName: StrategyName
): Promise<BacktestStatistics>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol |
strategyName |
StrategyName |
Strategy name (branded string type) |
Return Type: Promise<BacktestStatistics>
The BacktestStatistics interface contains comprehensive performance metrics:
interface BacktestStatistics {
// Core Metrics
sharpeRatio: number | null;
avgPnl: number | null;
totalPnl: number | null;
winRate: number | null;
// Risk-Adjusted Metrics
certaintyRatio: number | null;
annualizedSharpeRatio: number | null;
expectedYearlyReturns: number | null;
// Trade Counts
totalTrades: number;
winningTrades: number;
losingTrades: number;
// Signal List
events: IStrategyBacktestResult[];
// Safety Flags
isUnsafe: boolean; // true if any metric is NaN/Infinity
}
Calculation Flow:
Example Usage:
import { Backtest } from "backtest-kit";
// Run backtest first
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
})) {
// Consume results...
}
// Get statistics
const stats = await Backtest.getData("BTCUSDT", "rsi-strategy");
console.log(`Sharpe Ratio: ${stats.sharpeRatio}`);
console.log(`Win Rate: ${stats.winRate}%`);
console.log(`Total PNL: ${stats.totalPnl}`);
console.log(`Total Trades: ${stats.totalTrades}`);
console.log(`Certainty Ratio: ${stats.certaintyRatio}`);
Implementation Details:
backtestMarkdownService.getData() src/classes/Backtest.ts:162signalBacktestEmitter eventsnull for metrics when insufficient dataGenerates human-readable markdown report from completed backtest signals.
Signature:
getReport(
symbol: string,
strategyName: StrategyName
): Promise<string>
Parameters: Same as getData() method.
Return Type: Promise<string> - Markdown formatted report
Report Structure:
The generated markdown report includes the following sections:
Example Output Structure:
# Backtest Report: rsi-strategy (BTCUSDT)
Generated: 2024-01-15 10:30:00
## Summary Statistics
| Metric | Value |
|--------|-------|
| Total Trades | 42 |
| Win Rate | 65.5% |
| Sharpe Ratio | 1.85 |
| Total PNL | 12.3% |
| Average PNL | 0.29% |
| Certainty Ratio | 11.98 |
## Closed Signals
| ID | Open Time | Close Time | Side | Entry | Exit | PNL | Reason |
|----|-----------|------------|------|-------|------|-----|--------|
| sig_001 | 2024-01-01 00:00 | 2024-01-01 06:00 | LONG | 42000 | 42800 | +1.9% | TP |
| sig_002 | 2024-01-02 00:00 | 2024-01-02 04:00 | SHORT | 42500 | 42200 | +0.7% | TP |
...
Example Usage:
import { Backtest } from "backtest-kit";
import fs from "fs/promises";
// Run backtest
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
})) {
// Consume results...
}
// Generate and save report
const markdown = await Backtest.getReport("BTCUSDT", "rsi-strategy");
console.log(markdown);
// Or save to file
await fs.writeFile("./backtest-report.md", markdown);
Implementation Details:
backtestMarkdownService.getReport() src/classes/Backtest.ts:183Stops the strategy from generating new signals while allowing active signal to complete naturally.
Signature:
stop(
symbol: string,
strategyName: StrategyName
): Promise<void>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol |
strategyName |
StrategyName |
Strategy name to stop |
Behavior:
strategyCoreService.stop() src/classes/Backtest.ts:257getSignal() from being called on subsequent ticksStop Flow:
Example Usage:
import { Backtest } from "backtest-kit";
// Start backtest
const cancel = Backtest.background("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
});
// Stop after some condition
setTimeout(async () => {
await Backtest.stop("BTCUSDT", "rsi-strategy");
console.log("Strategy stopped, active signal will close naturally");
}, 30000);
Implementation Details:
BacktestUtils.stop()BacktestInstance.stop() src/classes/Backtest.ts:464strategyCoreService.stop()backtest: true flag to indicate backtest mode src/classes/Backtest.ts:257Saves backtest report to filesystem with automatic path resolution.
Signature:
dump(
symbol: string,
strategyName: StrategyName,
path?: string
): Promise<void>
Parameters:
| Parameter | Type | Description |
|---|---|---|
symbol |
string |
Trading pair symbol |
strategyName |
StrategyName |
Strategy name for filename generation |
path |
string (optional) |
Directory path (default: "./dump/backtest") |
File Naming Convention:
{path}/{strategyName}.md
Default Path Resolution:
Example Usage:
import { Backtest } from "backtest-kit";
// Run backtest
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
})) {
// Consume results...
}
// Save to default path: ./dump/backtest/rsi-strategy.md
await Backtest.dump("BTCUSDT", "rsi-strategy");
// Save to custom path: ./reports/2024/rsi-strategy.md
await Backtest.dump("BTCUSDT", "rsi-strategy", "./reports/2024");
Implementation Details:
BacktestUtils.dump()BacktestInstance.dump() src/classes/Backtest.ts:546backtestMarkdownService.dump() src/classes/Backtest.ts:331Lists all active backtest instances with their current execution status.
Signature:
list(): Promise<Array<{
id: string;
symbol: string;
strategyName: string;
status: "ready" | "pending" | "fulfilled" | "rejected";
}>>
Parameters: None
Return Type: Promise<StatusArray>
Returns array of status objects with:
| Field | Type | Description |
|---|---|---|
id |
string |
Random instance identifier generated by randomString() |
symbol |
string |
Trading pair symbol |
strategyName |
string |
Strategy name |
status |
string |
Task execution status from singlerun wrapper |
Status Values:
"ready": Instance created but task not started"pending": Task currently executing"fulfilled": Task completed successfully"rejected": Task failed with errorExample Usage:
import { Backtest } from "backtest-kit";
// Start multiple backtests
Backtest.background("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
});
Backtest.background("ETHUSDT", {
strategyName: "macd-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
});
// Check status
const statusList = await Backtest.list();
statusList.forEach(status => {
console.log(`${status.symbol} - ${status.strategyName}: ${status.status}`);
});
// Output:
// BTCUSDT - rsi-strategy: pending
// ETHUSDT - macd-strategy: pending
Implementation Details:
_getInstance.values() src/classes/Backtest.ts:563getStatus() on each instance src/classes/Backtest.ts:564getStatus() at src/classes/Backtest.ts:131-139 returns task.getStatus() from singlerunid generated at src/classes/Backtest.ts:75 using randomString()The Backtest API emits events throughout execution for monitoring and callbacks:
Available Event Listeners:
| Listener Function | Event Type | Description |
|---|---|---|
listenSignalBacktest() |
Signal closed | All closed backtest signals |
listenBacktestProgress() |
Progress update | Timeframe completion percentage |
listenDoneBacktest() |
Completion | Backtest execution finished |
listenPerformance() |
Performance metrics | Timing and bottleneck data |
Example with Event Listeners:
import {
Backtest,
listenSignalBacktest,
listenBacktestProgress,
listenDoneBacktest
} from "backtest-kit";
// Monitor progress
listenBacktestProgress((progress) => {
console.log(`Progress: ${progress.current}/${progress.total} timeframes`);
console.log(`Percentage: ${progress.percentage}%`);
});
// Monitor closed signals
listenSignalBacktest((result) => {
console.log(`Signal closed: ${result.signal.id}`);
console.log(`PNL: ${result.pnl.pnlPercentage}%`);
});
// Monitor completion
listenDoneBacktest((event) => {
console.log(`Backtest completed: ${event.strategyName}`);
});
// Run backtest
await Backtest.background("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
});
The Backtest API operates under several constraints inherited from the underlying execution engine:
All components must be registered before calling run() or background():
import { addStrategy, addExchange, addFrame, Backtest } from "backtest-kit";
// 1. Register strategy
addStrategy({
strategyName: "rsi-strategy",
interval: "1h",
getSignal: async (context) => {
// Strategy logic...
}
});
// 2. Register exchange
addExchange({
exchangeName: "binance",
getCandles: async (params) => {
// Fetch candles...
}
});
// 3. Register frame
addFrame({
frameName: "2024-backtest",
interval: "1h",
startDate: new Date("2024-01-01"),
endDate: new Date("2024-12-31")
});
// 4. Now run backtest
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
})) {
// Process results...
}
Each backtest execution is isolated by symbol-strategy pair. The memoization key used internally is ${symbol}:${strategyName} src/lib/services/connection/StrategyConnectionService.ts:79. This means:
The frame interval must be compatible with the strategy interval:
| Strategy Interval | Compatible Frame Intervals |
|---|---|
1m |
1m |
5m |
1m, 5m |
15m |
1m, 5m, 15m |
1h |
1m, 5m, 15m, 1h |
1d |
Any interval |
Misalignment will cause validation errors at runtime src/classes/Backtest.ts:60-63.
The Backtest API includes several optimizations for efficient historical simulation:
When a signal opens, the backtest engine skips all intermediate timeframes until the signal closes. This is implemented in BacktestLogicPrivateService and dramatically reduces computation time:
Without skip-ahead: Process every timeframe
Timeframes: T1 T2 T3 T4 T5 T6 T7 T8 T9 T10
Processing: ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
With skip-ahead: Skip during active signal
Timeframes: T1 T2 T3 T4 T5 T6 T7 T8 T9 T10
Processing: ✓ ✓ ⊗ ⊗ ⊗ ⊗ ⊗ ✓ ✓ ✓
^-Signal Opens ^-Signal Closes
This optimization is transparent to the user and does not affect result accuracy.
Strategy instances are memoized by ${symbol}:${strategyName} to avoid redundant instantiation src/lib/services/connection/StrategyConnectionService.ts:78-98. The cache persists across multiple run() calls unless explicitly cleared.
To clear the cache:
import backtest from "backtest-kit/lib";
// Clear specific strategy
await backtest.strategyGlobalService.clear({
symbol: "BTCUSDT",
strategyName: "rsi-strategy"
});
// Or clear all strategies
await backtest.strategyGlobalService.clear();
The BacktestMarkdownService has a configurable maximum event count (default: no limit). For very long backtests with thousands of signals, memory usage can grow. Consider using background() without collecting statistics for memory-constrained environments.
import {
addStrategy,
addExchange,
addFrame,
Backtest,
listenSignalBacktest,
listenBacktestProgress,
listenDoneBacktest,
} from "backtest-kit";
// 1. Register components
addStrategy({
strategyName: "rsi-strategy",
interval: "1h",
getSignal: async (context) => {
const rsi = await context.getRSI(14);
if (rsi < 30) {
return {
side: "LONG",
priceOpen: context.currentPrice,
takeProfit: context.currentPrice * 1.02,
stopLoss: context.currentPrice * 0.98,
};
}
return null;
},
});
addExchange({
exchangeName: "binance",
getCandles: async (params) => {
// Fetch from database or API
return candles;
},
});
addFrame({
frameName: "2024-q1-backtest",
interval: "1h",
startDate: new Date("2024-01-01"),
endDate: new Date("2024-03-31"),
});
// 2. Setup event listeners
listenBacktestProgress((progress) => {
console.log(`${progress.percentage}% complete`);
});
listenSignalBacktest((result) => {
console.log(`Signal: ${result.signal.side} | PNL: ${result.pnl.pnlPercentage}%`);
});
listenDoneBacktest((event) => {
console.log(`Backtest completed: ${event.strategyName}`);
});
// 3. Run backtest and consume results
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "rsi-strategy",
exchangeName: "binance",
frameName: "2024-q1-backtest",
})) {
console.log(`Closed at ${result.signal.closeTimestamp}`);
}
// 4. Get statistics
const stats = await Backtest.getData("BTCUSDT", "rsi-strategy");
console.log(`Sharpe Ratio: ${stats.sharpeRatio}`);
console.log(`Win Rate: ${stats.winRate}%`);
console.log(`Total Trades: ${stats.totalTrades}`);
// 5. Generate report
const markdown = await Backtest.getReport("BTCUSDT", "rsi-strategy");
console.log(markdown);
// 6. Save to disk
await Backtest.dump("rsi-strategy", "./reports");