Package Exports
- backtest-kit
Readme
🧿 Backtest Kit
A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.
Features
- 🚀 Production-Ready Architecture - Backtest/live mode, robust error recovery
- 💾 Crash-Safe Persistence - Atomic file writes with automatic state recovery
- ✅ Signal Validation - Comprehensive validation prevents invalid trades
- 🔄 Async Generators - Memory-efficient streaming for backtest and live execution
- 📊 VWAP Pricing - Volume-weighted average price from last 5 1m candles
- 🎯 Signal Lifecycle - Type-safe state machine (idle → opened → active → closed)
- 📈 Accurate PNL - Calculation with fees (0.1%) and slippage (0.1%)
- 🧠 Interval Throttling - Prevents signal spam at strategy level
- ⚡ Memory Optimized - Prototype methods + memoization + streaming
- 🔌 Flexible Architecture - Plug your own exchanges and strategies
- 📝 Markdown Reports - Auto-generated trading reports with statistics (win rate, avg PNL, Sharpe Ratio, Standard Deviation, Certainty Ratio, Expected Yearly Returns, Risk-Adjusted Returns)
- 📊 Performance Profiling - Built-in performance tracking with aggregated statistics (avg, min, max, stdDev, P95, P99) for bottleneck analysis
- 🛑 Graceful Shutdown - Live.background() waits for open positions to close before stopping
- 💉 Strategy Dependency Injection - addStrategy() enables DI pattern for trading strategies
- 🔍 Schema Reflection API - listExchanges(), listStrategies(), listFrames() for runtime introspection
- 🧪 Comprehensive Test Coverage - 61 unit tests covering validation, PNL, callbacks, reports, performance tracking, and event system
- 💾 Zero Data Download - Unlike Freqtrade, no need to download gigabytes of historical data - plug any data source (CCXT, database, API)
- 🔒 Safe Math & Robustness - All metrics protected against NaN/Infinity with unsafe numeric checks, returns N/A for invalid calculations
Installation
npm install backtest-kitQuick Start
1. Register Exchange Data Source
import { addExchange } from "backtest-kit";
import ccxt from "ccxt"; // Example using CCXT library
addExchange({
exchangeName: "binance",
// Fetch historical candles
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,
}));
},
// Format price according to exchange rules (e.g., 2 decimals for BTC)
formatPrice: async (symbol, price) => {
const exchange = new ccxt.binance();
const market = exchange.market(symbol);
return exchange.priceToPrecision(symbol, price);
},
// Format quantity according to exchange rules (e.g., 8 decimals)
formatQuantity: async (symbol, quantity) => {
const exchange = new ccxt.binance();
return exchange.amountToPrecision(symbol, quantity);
},
});Alternative: Database implementation
import { addExchange } from "backtest-kit";
import { db } from "./database"; // Your database client
addExchange({
exchangeName: "binance-db",
getCandles: async (symbol, interval, since, limit) => {
// Fetch from database for faster backtesting
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),
});2. Register Trading Strategy
import { addStrategy } from "backtest-kit";
addStrategy({
strategyName: "my-strategy",
interval: "5m", // Throttling: signals generated max once per 5 minutes
getSignal: async (symbol) => {
// Your signal generation logic
// Validation happens automatically (prices, TP/SL logic)
return {
position: "long",
note: "BTC breakout",
priceOpen: 50000,
priceTakeProfit: 51000, // Must be > priceOpen for long
priceStopLoss: 49000, // Must be < priceOpen for long
minuteEstimatedTime: 60, // Signal duration in minutes
};
},
callbacks: {
onOpen: (symbol, signal, currentPrice, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
},
onClose: (symbol, signal, priceClose, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
},
},
});3. Add Timeframe Generator
import { addFrame } from "backtest-kit";
addFrame({
frameName: "1d-backtest",
interval: "1m",
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} timeframes from ${startDate} to ${endDate}`);
},
},
});4. Run Backtest
import { Backtest, listenSignalBacktest, listenError, listenDone } from "backtest-kit";
// Run backtest in background
const stopBacktest = Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Listen to closed signals
listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log("PNL:", event.pnl.pnlPercentage);
}
});
// Listen to errors
listenError((error) => {
console.error("Error:", error.message);
});
// Listen to completion
listenDone((event) => {
if (event.backtest) {
console.log("Backtest completed:", event.symbol);
// Generate and save report
Backtest.dump(event.strategyName); // ./logs/backtest/my-strategy.md
}
});5. Run Live Trading (Crash-Safe)
import { Live, listenSignalLive, listenError, listenDone } from "backtest-kit";
// Run live trading in background (infinite loop, crash-safe)
const stop = Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});
// Listen to all signal events
listenSignalLive((event) => {
if (event.action === "opened") {
console.log("Signal opened:", event.signal.id);
}
if (event.action === "closed") {
console.log("Signal closed:", {
reason: event.closeReason,
pnl: event.pnl.pnlPercentage,
});
// Auto-save report
Live.dump(event.strategyName);
}
});
// Listen to errors
listenError((error) => {
console.error("Error:", error.message);
});
// Listen to completion
listenDone((event) => {
if (!event.backtest) {
console.log("Live trading stopped:", event.symbol);
}
});
// Stop when needed: stop();Crash Recovery: If process crashes, restart with same code - state automatically recovered from disk (no duplicate signals).
6. Alternative: Async Generators (Optional)
For manual control over execution flow:
import { Backtest, Live } from "backtest-kit";
// Manual backtest iteration
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
})) {
console.log("PNL:", result.pnl.pnlPercentage);
if (result.pnl.pnlPercentage < -5) break; // Early termination
}
// Manual live iteration (infinite loop)
for await (const result of Live.run("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
})) {
if (result.action === "closed") {
console.log("PNL:", result.pnl.pnlPercentage);
}
}7. Schema Reflection API (Optional)
Retrieve registered schemas at runtime for debugging, documentation, or building dynamic UIs:
import {
addExchange,
addStrategy,
addFrame,
listExchanges,
listStrategies,
listFrames
} from "backtest-kit";
// Register schemas with notes
addExchange({
exchangeName: "binance",
note: "Binance cryptocurrency exchange with database backend",
getCandles: async (symbol, interval, since, limit) => [...],
formatPrice: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
});
addStrategy({
strategyName: "sma-crossover",
note: "Simple moving average crossover strategy (50/200)",
interval: "5m",
getSignal: async (symbol) => ({...}),
});
addFrame({
frameName: "january-2024",
note: "Full month backtest for January 2024",
interval: "1m",
startDate: new Date("2024-01-01"),
endDate: new Date("2024-02-01"),
});
// List all registered schemas
const exchanges = await listExchanges();
console.log("Available exchanges:", exchanges.map(e => ({
name: e.exchangeName,
note: e.note
})));
// Output: [{ name: "binance", note: "Binance cryptocurrency exchange..." }]
const strategies = await listStrategies();
console.log("Available strategies:", strategies.map(s => ({
name: s.strategyName,
note: s.note,
interval: s.interval
})));
// Output: [{ name: "sma-crossover", note: "Simple moving average...", interval: "5m" }]
const frames = await listFrames();
console.log("Available frames:", frames.map(f => ({
name: f.frameName,
note: f.note,
period: `${f.startDate.toISOString()} - ${f.endDate.toISOString()}`
})));
// Output: [{ name: "january-2024", note: "Full month backtest...", period: "2024-01-01..." }]Use cases:
- Generate documentation automatically from registered schemas
- Build admin dashboards showing available strategies and exchanges
- Create CLI tools with auto-completion based on registered schemas
- Validate configuration files against registered schemas
Architecture Overview
The framework follows clean architecture with:
- Client Layer - Pure business logic without DI (ClientStrategy, ClientExchange, ClientFrame)
- Service Layer - DI-based services organized by responsibility
- Schema Services - Registry pattern for configuration
- Connection Services - Memoized client instance creators
- Global Services - Context wrappers for public API
- Logic Services - Async generator orchestration (backtest/live)
- Persistence Layer - Crash-safe atomic file writes with
PersistSignalAdaper
See ARCHITECTURE.md for detailed documentation.
Signal Validation
All signals are validated automatically before execution:
// ✅ Valid long signal
{
position: "long",
priceOpen: 50000,
priceTakeProfit: 51000, // ✅ 51000 > 50000
priceStopLoss: 49000, // ✅ 49000 < 50000
minuteEstimatedTime: 60, // ✅ positive
}
// ❌ Invalid long signal - throws error
{
position: "long",
priceOpen: 50000,
priceTakeProfit: 49000, // ❌ 49000 < 50000 (must be higher for long)
priceStopLoss: 51000, // ❌ 51000 > 50000 (must be lower for long)
}
// ✅ Valid short signal
{
position: "short",
priceOpen: 50000,
priceTakeProfit: 49000, // ✅ 49000 < 50000 (profit goes down for short)
priceStopLoss: 51000, // ✅ 51000 > 50000 (stop loss goes up for short)
}Validation errors include detailed messages for debugging.
Custom Persistence Adapter
By default, signals are persisted to disk using atomic file writes (./logs/data/signal/). You can override the persistence layer with a custom adapter (e.g., Redis, MongoDB):
import { PersistBase, PersistSignalAdaper, ISignalData, EntityId } from "backtest-kit";
import Redis from "ioredis";
// Create custom Redis adapter
class RedisPersist extends PersistBase {
private redis = new Redis({
host: "localhost",
port: 6379,
});
async waitForInit(initial: boolean): Promise<void> {
// Initialize Redis connection if needed
await this.redis.ping();
}
async readValue(entityId: EntityId): Promise<ISignalData> {
const key = `${this.entityName}:${entityId}`;
const data = await this.redis.get(key);
if (!data) {
throw new Error(`Entity ${this.entityName}:${entityId} not found`);
}
return JSON.parse(data);
}
async hasValue(entityId: EntityId): Promise<boolean> {
const key = `${this.entityName}:${entityId}`;
const exists = await this.redis.exists(key);
return exists === 1;
}
async writeValue(entityId: EntityId, entity: ISignalData): Promise<void> {
const key = `${this.entityName}:${entityId}`;
await this.redis.set(key, JSON.stringify(entity));
}
}
// Register custom adapter
PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
// Now all signal persistence uses Redis
Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});Key methods to implement:
waitForInit(initial)- Initialize storage connectionreadValue(entityId)- Read entity from storagehasValue(entityId)- Check if entity existswriteValue(entityId, entity)- Write entity to storage
The adapter is registered globally and applies to all strategies.
Interval Throttling
Prevent signal spam with automatic throttling:
addStrategy({
strategyName: "my-strategy",
interval: "5m", // Signals generated max once per 5 minutes
getSignal: async (symbol) => {
// This function will be called max once per 5 minutes
// Even if tick() is called every second
return signal;
},
});Supported intervals: "1m", "3m", "5m", "15m", "30m", "1h"
Markdown Reports
Generate detailed trading reports with statistics:
Backtest Reports
import { Backtest } from "backtest-kit";
// Run backtest
const stopBacktest = Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Get raw statistical data (Controller)
const stats = await Backtest.getData("my-strategy");
console.log(stats);
// Returns:
// {
// signalList: [...], // All closed signals
// totalSignals: 10,
// winCount: 7,
// lossCount: 3,
// winRate: 70.0, // Percentage (higher is better)
// avgPnl: 1.23, // Average PNL % (higher is better)
// totalPnl: 12.30, // Total PNL % (higher is better)
// stdDev: 2.45, // Standard deviation (lower is better)
// sharpeRatio: 0.50, // Risk-adjusted return (higher is better)
// annualizedSharpeRatio: 9.55, // Sharpe × √365 (higher is better)
// certaintyRatio: 1.75, // avgWin / |avgLoss| (higher is better)
// expectedYearlyReturns: 156 // Estimated yearly trades (higher is better)
// }
// Generate markdown report (View)
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");getData() returns BacktestStatistics:
signalList- Array of all closed signalstotalSignals- Total number of closed signalswinCount/lossCount- Number of winning/losing tradeswinRate- Win percentage (higher is better)avgPnl- Average PNL percentage (higher is better)totalPnl- Total PNL percentage (higher is better)stdDev- Standard deviation / volatility (lower is better)sharpeRatio- Risk-adjusted return (higher is better)annualizedSharpeRatio- Sharpe Ratio × √365 (higher is better)certaintyRatio- avgWin / |avgLoss| (higher is better)expectedYearlyReturns- Estimated number of trades per year (higher is better)
getReport() includes:
- All metrics from getData() formatted as markdown
- All signal details (prices, TP/SL, PNL, duration, close reason)
- Timestamps for each signal
- "Higher is better" / "Lower is better" annotations
Live Trading Reports
import { Live } from "backtest-kit";
// Get raw statistical data (Controller)
const stats = await Live.getData("my-strategy");
console.log(stats);
// Returns:
// {
// eventList: [...], // All events (idle, opened, active, closed)
// totalEvents: 15,
// totalClosed: 5,
// winCount: 3,
// lossCount: 2,
// winRate: 60.0, // Percentage (higher is better)
// avgPnl: 1.23, // Average PNL % (higher is better)
// totalPnl: 6.15, // Total PNL % (higher is better)
// stdDev: 1.85, // Standard deviation (lower is better)
// sharpeRatio: 0.66, // Risk-adjusted return (higher is better)
// annualizedSharpeRatio: 12.61,// Sharpe × √365 (higher is better)
// certaintyRatio: 2.10, // avgWin / |avgLoss| (higher is better)
// expectedYearlyReturns: 365 // Estimated yearly trades (higher is better)
// }
// Generate markdown report (View)
const markdown = await Live.getReport("my-strategy");
// Save to disk (default: ./logs/live/my-strategy.md)
await Live.dump("my-strategy");getData() returns LiveStatistics:
eventList- Array of all events (idle, opened, active, closed)totalEvents- Total number of eventstotalClosed- Total number of closed signalswinCount/lossCount- Number of winning/losing tradeswinRate- Win percentage (higher is better)avgPnl- Average PNL percentage (higher is better)totalPnl- Total PNL percentage (higher is better)stdDev- Standard deviation / volatility (lower is better)sharpeRatio- Risk-adjusted return (higher is better)annualizedSharpeRatio- Sharpe Ratio × √365 (higher is better)certaintyRatio- avgWin / |avgLoss| (higher is better)expectedYearlyReturns- Estimated number of trades per year (higher is better)
getReport() includes:
- All metrics from getData() formatted as markdown
- Signal-by-signal details with current state
- "Higher is better" / "Lower is better" annotations
Report example:
# Live Trading Report: my-strategy
Total events: 15
Closed signals: 5
Win rate: 60.00% (3W / 2L) (higher is better)
Average PNL: +1.23% (higher is better)
Total PNL: +6.15% (higher is better)
Standard Deviation: 1.85% (lower is better)
Sharpe Ratio: 0.66 (higher is better)
Annualized Sharpe Ratio: 12.61 (higher is better)
Certainty Ratio: 2.10 (higher is better)
Expected Yearly Returns: 365 trades (higher is better)
| Timestamp | Action | Symbol | Signal ID | Position | ... | PNL (net) | Close Reason |
|-----------|--------|--------|-----------|----------|-----|-----------|--------------|
| ... | CLOSED | BTCUSD | abc-123 | LONG | ... | +2.45% | take_profit |Event Listeners
Subscribe to signal events with filtering support. Useful for running strategies in background while reacting to specific events.
Background Execution with Event Listeners
import { Backtest, listenSignalBacktest } from "backtest-kit";
// Run backtest in background (doesn't yield results)
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Listen to all backtest events
const unsubscribe = listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log("Signal closed:", {
pnl: event.pnl.pnlPercentage,
reason: event.closeReason
});
}
});
// Stop listening when done
// unsubscribe();Listen Once with Filter
import { Backtest, listenSignalBacktestOnce } from "backtest-kit";
// Run backtest in background
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Wait for first take profit event
listenSignalBacktestOnce(
(event) => event.action === "closed" && event.closeReason === "take_profit",
(event) => {
console.log("First take profit hit!", event.pnl.pnlPercentage);
// Automatically unsubscribes after first match
}
);Live Trading with Event Listeners
import { Live, listenSignalLive, listenSignalLiveOnce } from "backtest-kit";
// Run live trading in background (infinite loop)
const cancel = Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});
// Listen to all live events
listenSignalLive((event) => {
if (event.action === "opened") {
console.log("Signal opened:", event.signal.id);
}
if (event.action === "closed") {
console.log("Signal closed:", event.pnl.pnlPercentage);
}
});
// React to first stop loss once
listenSignalLiveOnce(
(event) => event.action === "closed" && event.closeReason === "stop_loss",
(event) => {
console.error("Stop loss hit!", event.pnl.pnlPercentage);
// Send alert, dump report, etc.
}
);
// Stop live trading after some condition
// cancel();Listen to All Signals (Backtest + Live)
import { listenSignal, listenSignalOnce, Backtest, Live } from "backtest-kit";
// Listen to both backtest and live events
listenSignal((event) => {
console.log("Event:", event.action, event.strategyName);
});
// Wait for first loss from any source
listenSignalOnce(
(event) => event.action === "closed" && event.pnl.pnlPercentage < 0,
(event) => {
console.log("First loss detected:", event.pnl.pnlPercentage);
}
);
// Run both modes
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});Available event listeners:
listenSignal(callback)- Subscribe to all signal events (backtest + live)listenSignalOnce(filter, callback)- Subscribe once with filter predicatelistenSignalBacktest(callback)- Subscribe to backtest signals onlylistenSignalBacktestOnce(filter, callback)- Subscribe to backtest signals oncelistenSignalLive(callback)- Subscribe to live signals onlylistenSignalLiveOnce(filter, callback)- Subscribe to live signals oncelistenPerformance(callback)- Subscribe to performance metrics (backtest + live)listenProgress(callback)- Subscribe to backtest progress eventslistenError(callback)- Subscribe to background execution errorslistenDone(callback)- Subscribe to background completion eventslistenDoneOnce(filter, callback)- Subscribe to background completion once
All listeners return an unsubscribe function. All callbacks are processed sequentially using queued async execution.
Listen to Background Completion
import { listenDone, listenDoneOnce, Backtest, Live } from "backtest-kit";
// Listen to all completion events
listenDone((event) => {
console.log("Execution completed:", {
mode: event.backtest ? "backtest" : "live",
symbol: event.symbol,
strategy: event.strategyName,
exchange: event.exchangeName,
});
// Auto-generate report on completion
if (event.backtest) {
Backtest.dump(event.strategyName);
} else {
Live.dump(event.strategyName);
}
});
// Wait for specific backtest to complete
listenDoneOnce(
(event) => event.backtest && event.symbol === "BTCUSDT",
(event) => {
console.log("BTCUSDT backtest finished");
// Start next backtest or live trading
Live.background(event.symbol, {
strategyName: event.strategyName,
exchangeName: event.exchangeName,
});
}
);
// Run backtests
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});API Reference
High-Level Functions
Schema Registration
// Register exchange
addExchange(exchangeSchema: IExchangeSchema): void
// Register strategy
addStrategy(strategySchema: IStrategySchema): void
// Register timeframe generator
addFrame(frameSchema: IFrameSchema): voidExchange Data
// Get historical candles
const candles = await getCandles("BTCUSDT", "1h", 5);
// Returns: [
// { timestamp: 1704067200000, open: 42150.5, high: 42380.2, low: 42100.0, close: 42250.8, volume: 125.43 },
// { timestamp: 1704070800000, open: 42250.8, high: 42500.0, low: 42200.0, close: 42450.3, volume: 98.76 },
// { timestamp: 1704074400000, open: 42450.3, high: 42600.0, low: 42400.0, close: 42580.5, volume: 110.22 },
// { timestamp: 1704078000000, open: 42580.5, high: 42700.0, low: 42550.0, close: 42650.0, volume: 95.18 },
// { timestamp: 1704081600000, open: 42650.0, high: 42750.0, low: 42600.0, close: 42720.0, volume: 102.35 }
// ]
// Get VWAP from last 5 1m candles
const vwap = await getAveragePrice("BTCUSDT");
// Returns: 42685.34
// Get current date in execution context
const date = await getDate();
// Returns: 2024-01-01T12:00:00.000Z (in backtest mode, returns frame's current timestamp)
// Returns: 2024-01-15T10:30:45.123Z (in live mode, returns current wall clock time)
// Get current mode
const mode = await getMode();
// Returns: "backtest" or "live"
// Format price/quantity for exchange
const price = await formatPrice("BTCUSDT", 42685.3456789);
// Returns: "42685.35" (formatted to exchange precision)
const quantity = await formatQuantity("BTCUSDT", 0.123456789);
// Returns: "0.12345" (formatted to exchange precision)Service APIs
Backtest API
import { Backtest, BacktestStatistics } from "backtest-kit";
// Stream backtest results
Backtest.run(
symbol: string,
context: {
strategyName: string;
exchangeName: string;
frameName: string;
}
): AsyncIterableIterator<IStrategyTickResultClosed>
// Run in background without yielding results
Backtest.background(
symbol: string,
context: { strategyName, exchangeName, frameName }
): Promise<() => void> // Returns cancellation function
// Get raw statistical data (Controller)
Backtest.getData(strategyName: string): Promise<BacktestStatistics>
// Generate markdown report (View)
Backtest.getReport(strategyName: string): Promise<string>
// Save report to disk
Backtest.dump(strategyName: string, path?: string): Promise<void>Live Trading API
import { Live, LiveStatistics } from "backtest-kit";
// Stream live results (infinite)
Live.run(
symbol: string,
context: {
strategyName: string;
exchangeName: string;
}
): AsyncIterableIterator<IStrategyTickResult>
// Run in background without yielding results
Live.background(
symbol: string,
context: { strategyName, exchangeName }
): Promise<() => void> // Returns cancellation function
// Get raw statistical data (Controller)
Live.getData(strategyName: string): Promise<LiveStatistics>
// Generate markdown report (View)
Live.getReport(strategyName: string): Promise<string>
// Save report to disk
Live.dump(strategyName: string, path?: string): Promise<void>Performance Profiling API
import { Performance, PerformanceStatistics, listenPerformance } from "backtest-kit";
// Get raw performance statistics (Controller)
Performance.getData(strategyName: string): Promise<PerformanceStatistics>
// Generate markdown report with bottleneck analysis (View)
Performance.getReport(strategyName: string): Promise<string>
// Save performance report to disk (default: ./logs/performance)
Performance.dump(strategyName: string, path?: string): Promise<void>
// Clear accumulated performance data
Performance.clear(strategyName?: string): Promise<void>
// Listen to real-time performance events
listenPerformance((event) => {
console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
console.log(`Strategy: ${event.strategyName} @ ${event.exchangeName}`);
console.log(`Symbol: ${event.symbol}, Backtest: ${event.backtest}`);
});Type Definitions
Statistics Types
// Backtest statistics (exported from "backtest-kit")
interface BacktestStatistics {
signalList: IStrategyTickResultClosed[]; // All closed signals
totalSignals: number;
winCount: number;
lossCount: number;
winRate: number | null; // Win percentage (higher is better)
avgPnl: number | null; // Average PNL % (higher is better)
totalPnl: number | null; // Total PNL % (higher is better)
stdDev: number | null; // Standard deviation (lower is better)
sharpeRatio: number | null; // Risk-adjusted return (higher is better)
annualizedSharpeRatio: number | null; // Sharpe × √365 (higher is better)
certaintyRatio: number | null; // avgWin / |avgLoss| (higher is better)
expectedYearlyReturns: number | null; // Estimated yearly trades (higher is better)
}
// Live statistics (exported from "backtest-kit")
interface LiveStatistics {
eventList: TickEvent[]; // All events (idle, opened, active, closed)
totalEvents: number;
totalClosed: number;
winCount: number;
lossCount: number;
winRate: number | null; // Win percentage (higher is better)
avgPnl: number | null; // Average PNL % (higher is better)
totalPnl: number | null; // Total PNL % (higher is better)
stdDev: number | null; // Standard deviation (lower is better)
sharpeRatio: number | null; // Risk-adjusted return (higher is better)
annualizedSharpeRatio: number | null; // Sharpe × √365 (higher is better)
certaintyRatio: number | null; // avgWin / |avgLoss| (higher is better)
expectedYearlyReturns: number | null; // Estimated yearly trades (higher is better)
}
// Performance statistics (exported from "backtest-kit")
interface PerformanceStatistics {
strategyName: string; // Strategy name
totalEvents: number; // Total number of performance events
totalDuration: number; // Total execution time (ms)
metricStats: Record<string, { // Statistics by metric type
metricType: PerformanceMetricType; // backtest_total | backtest_timeframe | backtest_signal | live_tick
count: number; // Number of samples
totalDuration: number; // Total duration (ms)
avgDuration: number; // Average duration (ms)
minDuration: number; // Minimum duration (ms)
maxDuration: number; // Maximum duration (ms)
stdDev: number; // Standard deviation (ms)
median: number; // Median duration (ms)
p95: number; // 95th percentile (ms)
p99: number; // 99th percentile (ms)
}>;
events: PerformanceContract[]; // All raw performance events
}
// Performance event (exported from "backtest-kit")
interface PerformanceContract {
timestamp: number; // When metric was recorded (epoch ms)
metricType: PerformanceMetricType; // Type of operation measured
duration: number; // Operation duration (ms)
strategyName: string; // Strategy name
exchangeName: string; // Exchange name
symbol: string; // Trading symbol
backtest: boolean; // true = backtest, false = live
}
// Performance metric types (exported from "backtest-kit")
type PerformanceMetricType =
| "backtest_total" // Total backtest duration
| "backtest_timeframe" // Single timeframe processing
| "backtest_signal" // Signal processing (tick + getNextCandles + backtest)
| "live_tick"; // Single live tick durationSignal Data
interface ISignalRow {
id: string; // UUID v4 auto-generated
position: "long" | "short";
note?: string;
priceOpen: number;
priceTakeProfit: number;
priceStopLoss: number;
minuteEstimatedTime: number;
exchangeName: string;
strategyName: string;
timestamp: number; // Signal creation timestamp
symbol: string; // Trading pair (e.g., "BTCUSDT")
}Tick Results (Discriminated Union)
type IStrategyTickResult =
| {
action: "idle";
signal: null;
strategyName: string;
exchangeName: string;
currentPrice: number;
}
| {
action: "opened";
signal: ISignalRow;
strategyName: string;
exchangeName: string;
currentPrice: number;
}
| {
action: "active";
signal: ISignalRow;
currentPrice: number;
strategyName: string;
exchangeName: string;
}
| {
action: "closed";
signal: ISignalRow;
currentPrice: number;
closeReason: "take_profit" | "stop_loss" | "time_expired";
closeTimestamp: number;
pnl: {
pnlPercentage: number;
priceOpen: number; // Entry price adjusted with slippage and fees
priceClose: number; // Exit price adjusted with slippage and fees
};
strategyName: string;
exchangeName: string;
};PNL Calculation
// Constants
PERCENT_SLIPPAGE = 0.1% // 0.001
PERCENT_FEE = 0.1% // 0.001
// LONG position
priceOpenWithCosts = priceOpen * (1 + slippage + fee)
priceCloseWithCosts = priceClose * (1 - slippage - fee)
pnl% = (priceCloseWithCosts - priceOpenWithCosts) / priceOpenWithCosts * 100
// SHORT position
priceOpenWithCosts = priceOpen * (1 - slippage + fee)
priceCloseWithCosts = priceClose * (1 + slippage + fee)
pnl% = (priceOpenWithCosts - priceCloseWithCosts) / priceOpenWithCosts * 100Production Readiness
✅ Production-Ready Features
- Crash-Safe Persistence - Atomic file writes with automatic recovery
- Signal Validation - Comprehensive validation prevents invalid trades
- Type Safety - Discriminated unions eliminate runtime type errors
- Memory Efficiency - Prototype methods + async generators + memoization
- Interval Throttling - Prevents signal spam
- Live Trading Ready - Full implementation with real-time progression
- Error Recovery - Stateless process with disk-based state
Advanced Examples
Multi-Symbol Live Trading
import { Live } from "backtest-kit";
const symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT"];
// Run all symbols in parallel
await Promise.all(
symbols.map(async (symbol) => {
for await (const result of Live.run(symbol, {
strategyName: "my-strategy",
exchangeName: "binance"
})) {
console.log(`[${symbol}]`, result.action);
// Generate reports periodically
if (result.action === "closed") {
await Live.dump("my-strategy");
}
}
})
);Backtest Progress Listener
import { listenProgress, Backtest } from "backtest-kit";
listenProgress((event) => {
console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
});
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});Performance Profiling
import { Performance, listenPerformance, Backtest } from "backtest-kit";
// Listen to real-time performance metrics
listenPerformance((event) => {
console.log(`[${event.metricType}] ${event.duration.toFixed(2)}ms`);
console.log(` Strategy: ${event.strategyName}`);
console.log(` Symbol: ${event.symbol}, Backtest: ${event.backtest}`);
});
// Run backtest
await Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Get aggregated performance statistics
const perfStats = await Performance.getData("my-strategy");
console.log("Performance Statistics:");
console.log(` Total events: ${perfStats.totalEvents}`);
console.log(` Total duration: ${perfStats.totalDuration.toFixed(2)}ms`);
console.log(` Metrics tracked: ${Object.keys(perfStats.metricStats).join(", ")}`);
// Analyze bottlenecks
for (const [type, stats] of Object.entries(perfStats.metricStats)) {
console.log(`\n${type}:`);
console.log(` Count: ${stats.count}`);
console.log(` Average: ${stats.avgDuration.toFixed(2)}ms`);
console.log(` Min/Max: ${stats.minDuration.toFixed(2)}ms / ${stats.maxDuration.toFixed(2)}ms`);
console.log(` P95/P99: ${stats.p95.toFixed(2)}ms / ${stats.p99.toFixed(2)}ms`);
console.log(` Std Dev: ${stats.stdDev.toFixed(2)}ms`);
}
// Generate and save performance report
const markdown = await Performance.getReport("my-strategy");
await Performance.dump("my-strategy"); // Saves to ./logs/performance/my-strategy.mdPerformance Report Example:
# Performance Report: my-strategy
**Total events:** 1440
**Total execution time:** 12345.67ms
**Number of metric types:** 3
## Time Distribution
- **backtest_timeframe**: 65.4% (8074.32ms total)
- **backtest_signal**: 28.3% (3493.85ms total)
- **backtest_total**: 6.3% (777.50ms total)
## Detailed Metrics
| Metric Type | Count | Total (ms) | Avg (ms) | Min (ms) | Max (ms) | Std Dev (ms) | Median (ms) | P95 (ms) | P99 (ms) |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| backtest_timeframe | 1440 | 8074.32 | 5.61 | 2.10 | 12.45 | 1.85 | 5.20 | 8.90 | 10.50 |
| backtest_signal | 45 | 3493.85 | 77.64 | 45.20 | 125.80 | 18.32 | 75.10 | 110.20 | 120.15 |
| backtest_total | 1 | 777.50 | 777.50 | 777.50 | 777.50 | 0.00 | 777.50 | 777.50 | 777.50 |
**Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times.Early Termination
Using async generator with break:
import { Backtest } from "backtest-kit";
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
})) {
if (result.closeReason === "stop_loss") {
console.log("Stop loss hit - terminating backtest");
// Save final report before exit
await Backtest.dump("my-strategy");
break; // Generator stops immediately
}
}Using background mode with stop() function:
import { Backtest, Live, listenSignalLiveOnce } from "backtest-kit";
// Backtest.background returns a stop function
const stopBacktest = await Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Stop backtest after some condition
setTimeout(() => {
console.log("Stopping backtest...");
stopBacktest(); // Stops the background execution
}, 5000);
// Live.background also returns a stop function
const stopLive = Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});
// Stop live trading after detecting stop loss
listenSignalLiveOnce(
(event) => event.action === "closed" && event.closeReason === "stop_loss",
(event) => {
console.log("Stop loss detected - stopping live trading");
stopLive(); // Stops the infinite loop
}
);Use Cases
- Algorithmic Trading - Backtest and deploy strategies with crash recovery
- Strategy Research - Test hypotheses on historical data
- Signal Generation - Use with ML models or technical indicators
- Portfolio Management - Track multiple strategies across symbols
- Educational Projects - Learn trading system architecture
Contributing
Pull requests are welcome. For major changes, please open an issue first.
License
MIT