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.
Build sophisticated trading systems with confidence. Backtest Kit empowers you to develop, test, and deploy algorithmic trading strategies with enterprise-grade reliability—featuring atomic state persistence, comprehensive validation, and memory-efficient execution. Whether you're backtesting historical data or running live strategies, this framework provides the tools you need to trade with precision.
📚 API Reference | 🌟 Quick Start
✨ Why Choose Backtest Kit?
🚀 Production-Ready Architecture: Seamlessly switch between backtest and live modes with robust error recovery and graceful shutdown mechanisms. Your strategy code remains identical across environments.
💾 Crash-Safe Persistence: Atomic file writes with automatic state recovery ensure no duplicate signals or lost data—even after crashes. Resume execution exactly where you left off. 🔄
✅ Signal Validation: Comprehensive validation prevents invalid trades before execution. Catches price logic errors (TP/SL), throttles signal spam, and ensures data integrity. 🛡️
🔄 Async Generator Architecture: Memory-efficient streaming for backtest and live execution. Process years of historical data without loading everything into memory. ⚡
📊 VWAP Pricing: Volume-weighted average price from last 5 1-minute candles ensures realistic backtest results that match live execution. 📈
🎯 Type-Safe Signal Lifecycle: State machine with compile-time guarantees (idle → scheduled → opened → active → closed/cancelled). No runtime state confusion. 🔒
📈 Accurate PNL Calculation: Realistic profit/loss with configurable fees (0.1%) and slippage (0.1%). Track gross and net returns separately. 💰
⏰ Time-Travel Context: Async context propagation allows same strategy code to run in backtest (with historical time) and live (with real-time) without modifications. 🌐
📝 Auto-Generated Reports: Markdown reports with statistics (win rate, avg PNL, Sharpe Ratio, standard deviation, certainty ratio, expected yearly returns, risk-adjusted returns). 📊
📊 Revenue Profiling: Built-in performance tracking with aggregated statistics (avg, min, max, stdDev, P95, P99) for bottleneck analysis. ⚡
🏃 Strategy Comparison (Walker): Compare multiple strategies in parallel with automatic ranking and statistical analysis. Find your best performer. 🏆
🔥 Portfolio Heatmap: Multi-symbol performance analysis with extended metrics (Profit Factor, Expectancy, Win/Loss Streaks, Avg Win/Loss) sorted by Sharpe Ratio. 📉
💰 Position Sizing Calculator: Built-in position sizing methods (Fixed Percentage, Kelly Criterion, ATR-based) with risk management constraints. 💵
🛡️ Risk Management System: Portfolio-level risk controls with custom validation logic, concurrent position limits, and cross-strategy coordination. 🔐
💾 Zero Data Download: Unlike Freqtrade, no need to download gigabytes of historical data—plug any data source (CCXT, database, API). 🚀
🔌 Pluggable Persistence: Replace default file-based persistence with custom adapters (Redis, MongoDB, PostgreSQL) for distributed systems and high-performance scenarios. 💾
🔒 Safe Math & Robustness: All metrics protected against NaN/Infinity with unsafe numeric checks. Returns N/A for invalid calculations. ✨
🧪 Comprehensive Test Coverage: 123 unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, scheduled signals, and event system.
🎳 Supported Order Types
Backtest Kit supports multiple execution styles to match real trading behavior:
Market — instant execution using current VWAP
Limit — entry at a specified
priceOpenTake Profit (TP) — automatic exit at the target price
Stop Loss (SL) — protective exit at the stop level
OCO (TP + SL) — linked exits; one cancels the other
Grid — auto-cancel if price never reaches entry point or hits SL before activation
🆕 Extendable Order Types
Easy to add without modifying the core:
Stop / Stop-Limit — entry triggered by
triggerPriceTrailing Stop — dynamic SL based on market movement
Conditional Entry — enter only if price breaks a level (
above/below)Post-Only / Reduce-Only — exchange-level execution flags
🚀 Getting Started
Installation
Get up and running in seconds:
npm install backtest-kitQuick Example
Here's a taste of what backtest-kit can do—create a simple moving average crossover strategy with crash-safe persistence:
import {
addExchange,
addStrategy,
addFrame,
Backtest,
listenSignalBacktest,
listenError,
listenDoneBacktest
} 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: "sma-crossover",
interval: "5m", // Throttling: signals generated max once per 5 minutes
getSignal: async (symbol) => {
const price = await getAveragePrice(symbol);
return {
position: "long",
note: "BTC breakout",
priceOpen: price,
priceTakeProfit: price + 1_000, // Must be > priceOpen for long
priceStopLoss: price - 1_000, // Must be < priceOpen for long
minuteEstimatedTime: 60,
};
},
callbacks: {
onSchedule: (symbol, signal, currentPrice, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Scheduled signal created:`, signal.id);
},
onOpen: (symbol, signal, currentPrice, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
},
onActive: (symbol, signal, currentPrice, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Signal active:`, signal.id);
},
onClose: (symbol, signal, priceClose, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
},
onCancel: (symbol, signal, currentPrice, backtest) => {
console.log(`[${backtest ? "BT" : "LIVE"}] Scheduled signal cancelled:`, signal.id);
},
},
});
// 3. Add timeframe generator
addFrame({
frameName: "1d-backtest",
interval: "1m",
startDate: new Date("2024-01-01T00:00:00Z"),
endDate: new Date("2024-01-02T00:00:00Z"),
});
// 4. Run backtest in background
Backtest.background("BTCUSDT", {
strategyName: "sma-crossover",
exchangeName: "binance",
frameName: "1d-backtest"
});
// Listen to closed signals
listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log("PNL:", event.pnl.pnlPercentage);
}
});
// Listen to backtest completion
listenDoneBacktest((event) => {
console.log("Backtest completed:", event.symbol);
Backtest.dump(event.strategyName); // ./logs/backtest/sma-crossover.md
});The feature of this library is dependency inversion for component injection. Exchanges, strategies, frames, and risk profiles are lazy-loaded during runtime, so you can declare them in separate modules and connect them with string constants 🧩
export enum ExchangeName {
Binance = "binance",
Bybit = "bybit",
}
export enum StrategyName {
SMACrossover = "sma-crossover",
RSIStrategy = "rsi-strategy",
}
export enum FrameName {
OneDay = "1d-backtest",
OneWeek = "1w-backtest",
}
// ...
addStrategy({
strategyName: StrategyName.SMACrossover,
interval: "5m",
// ...
});
Backtest.background("BTCUSDT", {
strategyName: StrategyName.SMACrossover,
exchangeName: ExchangeName.Binance,
frameName: FrameName.OneDay
});🌟 Key Features
- 🤝 Mode Switching: Seamlessly switch between backtest and live modes with identical strategy code. 🔄
- 📜 Crash Recovery: Atomic persistence ensures state recovery after crashes—no duplicate signals. 🗂️
- 🛠️ Custom Validators: Define validation rules with strategy-level throttling and price logic checks. 🔧
- 🛡️ Signal Lifecycle: Type-safe state machine prevents invalid state transitions. 🚑
- 📦 Dependency Inversion: Lazy-load components at runtime for modular, scalable designs. 🧩
- 🔍 Schema Reflection: Runtime introspection with
listExchanges(),listStrategies(),listFrames(). 📊 - 🔬 Data Validation: Automatic detection and rejection of incomplete candles from Binance API with anomaly checks.
🎯 Use Cases
- 📈 Algorithmic Trading: Backtest and deploy systematic trading strategies with confidence. 💹
- 🤖 Strategy Development: Rapid prototyping with automatic validation and PNL tracking. 🛠️
- 📊 Performance Analysis: Compare strategies with Walker and analyze portfolios with Heatmap. 📉
- 💼 Portfolio Management: Multi-symbol trading with risk controls and position sizing. 🏦
📖 API Highlights
- 🛠️
addExchange: Define exchange data sources (CCXT, database, API). 📡 - 🤖
addStrategy: Create trading strategies with custom signals and callbacks. 💡 - 🌐
addFrame: Configure timeframes for backtesting. 📅 - 🔄
Backtest/Live: Run strategies in backtest or live mode (generator or background). ⚡ - 📅
Schedule: Track scheduled signals and cancellation rate for limit orders. 📊 - 🏃
Walker: Compare multiple strategies in parallel with ranking. 🏆 - 🔥
Heat: Portfolio-wide performance analysis across multiple symbols. 📊 - 💰
PositionSize: Calculate position sizes with Fixed %, Kelly Criterion, or ATR-based methods. 💵 - 🛡️
addRisk: Portfolio-level risk management with custom validation logic. 🔐 - 💾
PersistBase: Base class for custom persistence adapters (Redis, MongoDB, PostgreSQL). 🗄️ - 🔌
PersistSignalAdapter/PersistRiskAdapter: Register custom adapters for signal and risk persistence. 🔄
Check out the sections below for detailed examples! 📚
🛠 Advanced Features
1. Register Exchange Data Source
You can plug any data source: CCXT for live data or a database for faster backtesting:
import { addExchange } from "backtest-kit";
import ccxt from "ccxt";
// Option 1: CCXT (live or historical)
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)
import { db } from "./database";
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),
});2. Register Trading Strategy
Define your signal generation logic with automatic validation:
import { addStrategy } from "backtest-kit";
addStrategy({
strategyName: "my-strategy",
interval: "5m", // Throttling: signals generated max once per 5 minutes
getSignal: async (symbol) => {
const price = await getAveragePrice(symbol);
return {
position: "long",
note: "BTC breakout",
priceOpen: price,
priceTakeProfit: price + 1_000, // Must be > priceOpen for long
priceStopLoss: price - 1_000, // Must be < priceOpen for long
minuteEstimatedTime: 60,
};
},
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. Run Backtest
Run strategies in background mode (infinite loop) or manually iterate with async generators:
import { Backtest, listenSignalBacktest, listenDoneBacktest } from "backtest-kit";
// Option 1: Background mode (recommended)
const stopBacktest = Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log("PNL:", event.pnl.pnlPercentage);
}
});
listenDoneBacktest((event) => {
console.log("Backtest completed:", event.symbol);
Backtest.dump(event.strategyName); // ./logs/backtest/my-strategy.md
});
// Option 2: Manual iteration (for custom control)
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
}4. Run Live Trading (Crash-Safe)
Live mode automatically persists state to disk with atomic writes:
import { Live, listenSignalLive } from "backtest-kit";
// Run live trading in background (infinite loop, crash-safe)
const stop = Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});
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,
});
Live.dump(event.strategyName); // Auto-save report
}
});
// Stop when needed: stop();Crash Recovery: If process crashes, restart with same code—state automatically recovered from disk (no duplicate signals).
5. Strategy Comparison with Walker
Walker runs multiple strategies in parallel and ranks them by a selected metric:
import { addWalker, Walker, listenWalkerComplete } from "backtest-kit";
// Register walker schema
addWalker({
walkerName: "btc-walker",
exchangeName: "binance",
frameName: "1d-backtest",
strategies: ["strategy-a", "strategy-b", "strategy-c"],
metric: "sharpeRatio", // Metric to compare strategies
callbacks: {
onStrategyStart: (strategyName, symbol) => {
console.log(`Starting strategy: ${strategyName}`);
},
onStrategyComplete: (strategyName, symbol, stats) => {
console.log(`${strategyName} completed:`, stats.sharpeRatio);
},
onComplete: (results) => {
console.log("Best strategy:", results.bestStrategy);
console.log("Best metric:", results.bestMetric);
},
},
});
// Run walker in background
Walker.background("BTCUSDT", {
walkerName: "btc-walker"
});
// Listen to walker completion
listenWalkerComplete((results) => {
console.log("Walker completed:", results.bestStrategy);
Walker.dump("BTCUSDT", results.walkerName); // Save report
});
// Get raw comparison data
const results = await Walker.getData("BTCUSDT", "btc-walker");
console.log(results);
// Returns:
// {
// bestStrategy: "strategy-b",
// bestMetric: 1.85,
// strategies: [
// { strategyName: "strategy-a", stats: { sharpeRatio: 1.23, ... }, metric: 1.23 },
// { strategyName: "strategy-b", stats: { sharpeRatio: 1.85, ... }, metric: 1.85 },
// { strategyName: "strategy-c", stats: { sharpeRatio: 0.98, ... }, metric: 0.98 }
// ]
// }
// Generate markdown report
const markdown = await Walker.getReport("BTCUSDT", "btc-walker");
console.log(markdown);Available metrics for comparison:
sharpeRatio- Risk-adjusted return (default)winRate- Win percentageavgPnl- Average PNL percentagetotalPnl- Total PNL percentagecertaintyRatio- avgWin / |avgLoss|
6. Portfolio Heatmap
Heat provides portfolio-wide performance analysis across multiple symbols:
import { Heat, Backtest } from "backtest-kit";
// Run backtests for multiple symbols
for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"]) {
for await (const _ of Backtest.run(symbol, {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "2024-backtest"
})) {}
}
// Get raw heatmap data
const stats = await Heat.getData("my-strategy");
console.log(stats);
// Returns:
// {
// symbols: [
// {
// symbol: "BTCUSDT",
// totalPnl: 15.5, // Total profit/loss %
// sharpeRatio: 2.10, // Risk-adjusted return
// profitFactor: 2.50, // Wins / Losses ratio
// expectancy: 1.85, // Expected value per trade
// winRate: 72.3, // Win percentage
// avgWin: 2.45, // Average win %
// avgLoss: -0.95, // Average loss %
// maxDrawdown: -2.5, // Maximum drawdown %
// maxWinStreak: 5, // Consecutive wins
// maxLossStreak: 2, // Consecutive losses
// totalTrades: 45,
// winCount: 32,
// lossCount: 13,
// avgPnl: 0.34,
// stdDev: 1.62
// },
// // ... more symbols sorted by Sharpe Ratio
// ],
// totalSymbols: 4,
// portfolioTotalPnl: 45.3, // Portfolio-wide total PNL
// portfolioSharpeRatio: 1.85, // Portfolio-wide Sharpe
// portfolioTotalTrades: 120
// }
// Generate markdown report
const markdown = await Heat.getReport("my-strategy");
console.log(markdown);
// Save to disk (default: ./logs/heatmap/my-strategy.md)
await Heat.dump("my-strategy");Heatmap Report Example:
# Portfolio Heatmap: my-strategy
**Total Symbols:** 4 | **Portfolio PNL:** +45.30% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
| Symbol | Total PNL | Sharpe | PF | Expect | WR | Avg Win | Avg Loss | Max DD | W Streak | L Streak | Trades |
|--------|-----------|--------|-------|--------|-----|---------|----------|--------|----------|----------|--------|
| BTCUSDT | +15.50% | 2.10 | 2.50 | +1.85% | 72.3% | +2.45% | -0.95% | -2.50% | 5 | 2 | 45 |
| ETHUSDT | +12.30% | 1.85 | 2.15 | +1.45% | 68.5% | +2.10% | -1.05% | -3.10% | 4 | 2 | 38 |
| SOLUSDT | +10.20% | 1.65 | 1.95 | +1.20% | 65.2% | +1.95% | -1.15% | -4.20% | 3 | 3 | 25 |
| BNBUSDT | +7.30% | 1.40 | 1.75 | +0.95% | 62.5% | +1.75% | -1.20% | -3.80% | 3 | 2 | 12 |Column Descriptions:
- Total PNL - Total profit/loss percentage across all trades
- Sharpe - Risk-adjusted return (higher is better)
- PF - Profit Factor: sum of wins / sum of losses (>1.0 is profitable)
- Expect - Expectancy: expected value per trade
- WR - Win Rate: percentage of winning trades
- Avg Win - Average profit on winning trades
- Avg Loss - Average loss on losing trades
- Max DD - Maximum drawdown (largest peak-to-trough decline)
- W Streak - Maximum consecutive winning trades
- L Streak - Maximum consecutive losing trades
- Trades - Total number of trades for this symbol
7. Position Sizing Calculator
Position Sizing Calculator helps determine optimal position sizes based on risk management rules:
import { addSizing, PositionSize } from "backtest-kit";
// Fixed Percentage Risk - risk fixed % of account per trade
addSizing({
sizingName: "conservative",
note: "Conservative 2% risk per trade",
method: "fixed-percentage",
riskPercentage: 2, // Risk 2% of account per trade
maxPositionPercentage: 10, // Max 10% of account in single position (optional)
minPositionSize: 0.001, // Min 0.001 BTC position (optional)
maxPositionSize: 1.0, // Max 1.0 BTC position (optional)
});
// Kelly Criterion - optimal bet sizing based on edge
addSizing({
sizingName: "kelly-quarter",
note: "Kelly Criterion with 25% multiplier for safety",
method: "kelly-criterion",
kellyMultiplier: 0.25, // Use 25% of full Kelly (recommended for safety)
maxPositionPercentage: 15, // Cap position at 15% of account (optional)
minPositionSize: 0.001, // Min 0.001 BTC position (optional)
maxPositionSize: 2.0, // Max 2.0 BTC position (optional)
});
// ATR-based - volatility-adjusted position sizing
addSizing({
sizingName: "atr-dynamic",
note: "ATR-based sizing with 2x multiplier",
method: "atr-based",
riskPercentage: 2, // Risk 2% of account
atrMultiplier: 2, // Use 2x ATR as stop distance
maxPositionPercentage: 12, // Max 12% of account (optional)
minPositionSize: 0.001, // Min 0.001 BTC position (optional)
maxPositionSize: 1.5, // Max 1.5 BTC position (optional)
});
// Calculate position sizes
const quantity1 = await PositionSize.fixedPercentage(
"BTCUSDT",
10000, // Account balance: $10,000
50000, // Entry price: $50,000
49000, // Stop loss: $49,000
{ sizingName: "conservative" }
);
console.log(`Position size: ${quantity1} BTC`);
const quantity2 = await PositionSize.kellyCriterion(
"BTCUSDT",
10000, // Account balance: $10,000
50000, // Entry price: $50,000
0.55, // Win rate: 55%
1.5, // Win/loss ratio: 1.5
{ sizingName: "kelly-quarter" }
);
console.log(`Position size: ${quantity2} BTC`);
const quantity3 = await PositionSize.atrBased(
"BTCUSDT",
10000, // Account balance: $10,000
50000, // Entry price: $50,000
500, // ATR: $500
{ sizingName: "atr-dynamic" }
);
console.log(`Position size: ${quantity3} BTC`);When to Use Each Method:
Fixed Percentage - Simple risk management, consistent risk per trade
- Best for: Beginners, conservative strategies
- Risk: Fixed 1-2% per trade
Kelly Criterion - Optimal bet sizing based on win rate and win/loss ratio
- Best for: Strategies with known edge, statistical advantage
- Risk: Use fractional Kelly (0.25-0.5) to reduce volatility
ATR-based - Volatility-adjusted sizing, accounts for market conditions
- Best for: Swing trading, volatile markets
- Risk: Position size scales with volatility
8. Risk Management
Risk Management provides portfolio-level risk controls across strategies:
import { addRisk } from "backtest-kit";
// Simple concurrent position limit
addRisk({
riskName: "conservative",
note: "Conservative risk profile with max 3 concurrent positions",
validations: [
({ activePositionCount }) => {
if (activePositionCount >= 3) {
throw new Error("Maximum 3 concurrent positions allowed");
}
},
],
callbacks: {
onRejected: (symbol, params) => {
console.warn(`Signal rejected for ${symbol}:`, params);
},
onAllowed: (symbol, params) => {
console.log(`Signal allowed for ${symbol}`);
},
},
});
// Symbol-based filtering
addRisk({
riskName: "no-meme-coins",
note: "Block meme coins from trading",
validations: [
({ symbol }) => {
const memeCoins = ["DOGEUSDT", "SHIBUSDT", "PEPEUSDT"];
if (memeCoins.includes(symbol)) {
throw new Error(`Meme coin ${symbol} not allowed`);
}
},
],
});
// Time-based trading windows
addRisk({
riskName: "trading-hours",
note: "Only trade during market hours (9 AM - 5 PM UTC)",
validations: [
({ timestamp }) => {
const date = new Date(timestamp);
const hour = date.getUTCHours();
if (hour < 9 || hour >= 17) {
throw new Error("Trading only allowed 9 AM - 5 PM UTC");
}
},
],
});
// Multi-strategy coordination with position inspection
addRisk({
riskName: "strategy-coordinator",
note: "Limit exposure per strategy and inspect active positions",
validations: [
({ activePositions, strategyName, symbol }) => {
// Count positions for this specific strategy
const strategyPositions = activePositions.filter(
(pos) => pos.strategyName === strategyName
);
if (strategyPositions.length >= 2) {
throw new Error(`Strategy ${strategyName} already has 2 positions`);
}
// Check if we already have a position on this symbol
const symbolPositions = activePositions.filter(
(pos) => pos.symbol === symbol
);
if (symbolPositions.length > 0) {
throw new Error(`Already have position on ${symbol}`);
}
},
],
});
// Use risk profile in strategy
addStrategy({
strategyName: "my-strategy",
interval: "5m",
riskName: "conservative", // Apply risk profile
getSignal: async (symbol) => {
// Signal generation logic
return { /* ... */ };
},
});9. Custom Persistence Adapters (Optional)
By default, backtest-kit uses file-based persistence with atomic writes. You can replace this with custom adapters (e.g., Redis, MongoDB, PostgreSQL) for distributed systems or high-performance scenarios.
Understanding the Persistence System
The library uses three persistence layers:
- PersistBase - Base class for all persistence operations (file-based by default)
- PersistSignalAdapter - Manages signal state persistence (used by Live mode)
- PersistRiskAdapter - Manages active positions for risk management
Default File-Based Persistence
By default, data is stored in JSON files:
./logs/data/
signal/
my-strategy/
BTCUSDT.json # Signal state for BTCUSDT
ETHUSDT.json # Signal state for ETHUSDT
risk/
conservative/
positions.json # Active positions for risk profileCreate Custom Adapter (Redis Example)
import { PersistBase, PersistSignalAdaper, PersistRiskAdapter } from "backtest-kit";
import Redis from "ioredis";
const redis = new Redis();
// Custom Redis-based persistence adapter
class RedisPersist extends PersistBase {
// Initialize Redis connection
async waitForInit(initial: boolean): Promise<void> {
// Redis connection is already established
console.log(`Redis persistence initialized for ${this.entityName}`);
}
// Read entity from Redis
async readValue<T>(entityId: string | number): Promise<T> {
const key = `${this.entityName}:${entityId}`;
const data = await redis.get(key);
if (!data) {
throw new Error(`Entity ${this.entityName}:${entityId} not found`);
}
return JSON.parse(data) as T;
}
// Check if entity exists in Redis
async hasValue(entityId: string | number): Promise<boolean> {
const key = `${this.entityName}:${entityId}`;
const exists = await redis.exists(key);
return exists === 1;
}
// Write entity to Redis
async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
const key = `${this.entityName}:${entityId}`;
const serializedData = JSON.stringify(entity);
await redis.set(key, serializedData);
// Optional: Set TTL (time to live)
// await redis.expire(key, 86400); // 24 hours
}
// Remove entity from Redis
async removeValue(entityId: string | number): Promise<void> {
const key = `${this.entityName}:${entityId}`;
const result = await redis.del(key);
if (result === 0) {
throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
}
}
// Remove all entities for this entity type
async removeAll(): Promise<void> {
const pattern = `${this.entityName}:*`;
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
// Iterate over all entity values
async *values<T>(): AsyncGenerator<T> {
const pattern = `${this.entityName}:*`;
const keys = await redis.keys(pattern);
// Sort keys alphanumerically
keys.sort((a, b) => a.localeCompare(b, undefined, {
numeric: true,
sensitivity: "base"
}));
for (const key of keys) {
const data = await redis.get(key);
if (data) {
yield JSON.parse(data) as T;
}
}
}
// Iterate over all entity IDs
async *keys(): AsyncGenerator<string> {
const pattern = `${this.entityName}:*`;
const keys = await redis.keys(pattern);
// Sort keys alphanumerically
keys.sort((a, b) => a.localeCompare(b, undefined, {
numeric: true,
sensitivity: "base"
}));
for (const key of keys) {
// Extract entity ID from key (remove prefix)
const entityId = key.slice(this.entityName.length + 1);
yield entityId;
}
}
}
// Register Redis adapter for signal persistence
PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
// Register Redis adapter for risk persistence
PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);Custom Adapter Registration (Before Running Strategies)
import { PersistSignalAdaper, PersistRiskAdapter, Live } from "backtest-kit";
// IMPORTANT: Register adapters BEFORE running any strategies
PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
// Now run live trading with Redis persistence
Live.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});MongoDB Adapter Example
import { PersistBase } from "backtest-kit";
import { MongoClient, Collection } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017");
const db = client.db("backtest-kit");
class MongoPersist extends PersistBase {
private collection: Collection;
constructor(entityName: string, baseDir: string) {
super(entityName, baseDir);
this.collection = db.collection(this.entityName);
}
async waitForInit(initial: boolean): Promise<void> {
await client.connect();
// Create index for faster lookups
await this.collection.createIndex({ entityId: 1 }, { unique: true });
console.log(`MongoDB persistence initialized for ${this.entityName}`);
}
async readValue<T>(entityId: string | number): Promise<T> {
const doc = await this.collection.findOne({ entityId });
if (!doc) {
throw new Error(`Entity ${this.entityName}:${entityId} not found`);
}
return doc.data as T;
}
async hasValue(entityId: string | number): Promise<boolean> {
const count = await this.collection.countDocuments({ entityId });
return count > 0;
}
async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
await this.collection.updateOne(
{ entityId },
{ $set: { entityId, data: entity, updatedAt: new Date() } },
{ upsert: true }
);
}
async removeValue(entityId: string | number): Promise<void> {
const result = await this.collection.deleteOne({ entityId });
if (result.deletedCount === 0) {
throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
}
}
async removeAll(): Promise<void> {
await this.collection.deleteMany({});
}
async *values<T>(): AsyncGenerator<T> {
const cursor = this.collection.find({}).sort({ entityId: 1 });
for await (const doc of cursor) {
yield doc.data as T;
}
}
async *keys(): AsyncGenerator<string> {
const cursor = this.collection.find({}, { projection: { entityId: 1 } }).sort({ entityId: 1 });
for await (const doc of cursor) {
yield String(doc.entityId);
}
}
}
// Register MongoDB adapter
PersistSignalAdaper.usePersistSignalAdapter(MongoPersist);
PersistRiskAdapter.usePersistRiskAdapter(MongoPersist);Direct Persistence API Usage (Advanced)
You can also use PersistBase directly for custom data storage:
import { PersistBase } from "backtest-kit";
// Create custom persistence for trading logs
const tradingLogs = new PersistBase("trading-logs", "./logs/custom");
// Initialize
await tradingLogs.waitForInit(true);
// Write log entry
await tradingLogs.writeValue("log-1", {
timestamp: Date.now(),
message: "Strategy started",
metadata: { symbol: "BTCUSDT", strategy: "sma-crossover" }
});
// Read log entry
const log = await tradingLogs.readValue("log-1");
console.log(log);
// Check if log exists
const exists = await tradingLogs.hasValue("log-1");
console.log(`Log exists: ${exists}`);
// Iterate over all logs
for await (const log of tradingLogs.values()) {
console.log("Log:", log);
}
// Get all log IDs
for await (const logId of tradingLogs.keys()) {
console.log("Log ID:", logId);
}
// Filter logs
for await (const log of tradingLogs.filter((l: any) => l.metadata.symbol === "BTCUSDT")) {
console.log("BTC Log:", log);
}
// Take first 5 logs
for await (const log of tradingLogs.take(5)) {
console.log("Recent Log:", log);
}
// Remove specific log
await tradingLogs.removeValue("log-1");
// Remove all logs
await tradingLogs.removeAll();When to Use Custom Adapters
Redis - Best for high-performance distributed systems with multiple instances
- Fast read/write operations
- Built-in TTL (automatic cleanup)
- Pub/sub for real-time updates
MongoDB - Best for complex queries and analytics
- Rich query language
- Aggregation pipelines
- Scalable for large datasets
PostgreSQL - Best for ACID transactions and relational data
- Strong consistency guarantees
- Complex joins and queries
- Mature ecosystem
File-based (default) - Best for single-instance deployments
- No dependencies
- Simple debugging (inspect JSON files)
- Sufficient for most use cases
Testing Custom Adapters
import { test } from "worker-testbed";
import { PersistBase } from "backtest-kit";
test("Custom Redis adapter works correctly", async ({ pass, fail }) => {
const persist = new RedisPersist("test-entity", "./logs/test");
await persist.waitForInit(true);
// Write
await persist.writeValue("key1", { data: "value1" });
// Read
const value = await persist.readValue("key1");
if (value.data === "value1") {
pass("Redis adapter read/write works");
} else {
fail("Redis adapter failed");
}
// Cleanup
await persist.removeValue("key1");
});📐 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
PersistSignalAdapter
✅ 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.
🧠 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";
// 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");
// Save to disk (default: ./logs/backtest/my-strategy.md)
await Backtest.dump("my-strategy");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, scheduled, opened, active, closed, cancelled)
// 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");Scheduled Signals Reports
import { Schedule } from "backtest-kit";
// Get raw scheduled signals data (Controller)
const stats = await Schedule.getData("my-strategy");
console.log(stats);
// Returns:
// {
// eventList: [...], // All scheduled/cancelled events
// totalEvents: 8,
// totalScheduled: 6, // Number of scheduled signals
// totalCancelled: 2, // Number of cancelled signals
// cancellationRate: 33.33, // Percentage (lower is better)
// avgWaitTime: 45.5, // Average wait time for cancelled signals in minutes
// }
// Generate markdown report (View)
const markdown = await Schedule.getReport("my-strategy");
// Save to disk (default: ./logs/schedule/my-strategy.md)
await Schedule.dump("my-strategy");
// Clear accumulated data
await Schedule.clear("my-strategy");Scheduled Signals Report Example:
# Scheduled Signals Report: my-strategy
| Timestamp | Action | Symbol | Signal ID | Position | Note | Current Price | Entry Price | Take Profit | Stop Loss | Wait Time (min) |
|-----------|--------|--------|-----------|----------|------|---------------|-------------|-------------|-----------|-----------------|
| 2024-01-15T10:30:00Z | SCHEDULED | BTCUSDT | sig-001 | LONG | BTC breakout | 42150.50 USD | 42000.00 USD | 43000.00 USD | 41000.00 USD | N/A |
| 2024-01-15T10:35:00Z | CANCELLED | BTCUSDT | sig-002 | LONG | BTC breakout | 42350.80 USD | 10000.00 USD | 11000.00 USD | 9000.00 USD | 60 |
**Total events:** 8
**Scheduled signals:** 6
**Cancelled signals:** 2
**Cancellation rate:** 33.33% (lower is better)
**Average wait time (cancelled):** 45.50 minutes🎧 Event Listeners
Listen to All Signals (Backtest + Live)
import { listenSignal } from "backtest-kit";
// Listen to both backtest and live signals
listenSignal((event) => {
console.log(`[${event.backtest ? "BT" : "LIVE"}] ${event.action}:`, event.signal.id);
if (event.action === "closed") {
console.log("PNL:", event.pnl.pnlPercentage);
console.log("Close reason:", event.closeReason);
}
});Listen Once with Filter
import { listenSignalOnce, listenSignalLiveOnce } from "backtest-kit";
// Listen once with filter
listenSignalOnce(
(event) => event.action === "closed" && event.pnl.pnlPercentage > 5,
(event) => {
console.log("Big win detected:", event.pnl.pnlPercentage);
}
);
// Listen once for specific symbol in live mode
listenSignalLiveOnce(
(event) => event.signal.symbol === "BTCUSDT" && event.action === "opened",
(event) => {
console.log("BTC signal opened:", event.signal.id);
}
);Listen to Background Completion
import { listenDoneBacktest, listenDoneLive, listenDoneWalker } from "backtest-kit";
// Backtest completion
listenDoneBacktest((event) => {
console.log("Backtest completed:", event.strategyName);
console.log("Symbol:", event.symbol);
console.log("Exchange:", event.exchangeName);
});
// Live trading completion
listenDoneLive((event) => {
console.log("Live trading stopped:", event.strategyName);
});
// Walker completion
listenDoneWalker((event) => {
console.log("Walker completed:", event.strategyName);
console.log("Best strategy:", event.bestStrategy);
});⚙️ Global Configuration
You can customize framework behavior using the setConfig() function. This allows you to adjust global parameters without modifying the source code.
Available Configuration Options
import { setConfig } from "backtest-kit";
// Configure global parameters
await setConfig({
// Time to wait for scheduled signal activation (in minutes)
// If a scheduled signal doesn't activate within this time, it will be cancelled
// Default: 120 minutes
CC_SCHEDULE_AWAIT_MINUTES: 90,
// Number of candles to use for average price calculation (VWAP)
// Used in both backtest and live modes for price calculations
// Default: 5 candles (last 5 minutes when using 1m interval)
CC_AVG_PRICE_CANDLES_COUNT: 10,
});Configuration Parameters
CC_SCHEDULE_AWAIT_MINUTES
Controls how long scheduled signals wait for activation before being cancelled.
- Default:
120minutes (2 hours) - Use case: Adjust based on market volatility and strategy timeframe
- Example: Lower for scalping strategies (30-60 min), higher for swing trading (180-360 min)
// For scalping strategies with tight entry windows
await setConfig({
CC_SCHEDULE_AWAIT_MINUTES: 30,
});
// For swing trading with wider entry windows
await setConfig({
CC_SCHEDULE_AWAIT_MINUTES: 240,
});CC_AVG_PRICE_CANDLES_COUNT
Controls the number of 1-minute candles used for VWAP (Volume Weighted Average Price) calculations.
- Default:
5candles (5 minutes of data) - Use case: Adjust for more stable (higher) or responsive (lower) price calculations
- Impact: Affects entry/exit prices in both backtest and live modes
// More responsive to recent price changes (3 minutes)
await setConfig({
CC_AVG_PRICE_CANDLES_COUNT: 3,
});
// More stable, less sensitive to spikes (10 minutes)
await setConfig({
CC_AVG_PRICE_CANDLES_COUNT: 10,
});When to Call setConfig()
Always call setConfig() before running any strategies to ensure configuration is applied:
import { setConfig, Backtest, Live } from "backtest-kit";
// 1. Configure framework first
await setConfig({
CC_SCHEDULE_AWAIT_MINUTES: 90,
CC_AVG_PRICE_CANDLES_COUNT: 7,
});
// 2. Then run strategies
Backtest.background("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest"
});
Live.background("ETHUSDT", {
strategyName: "my-strategy",
exchangeName: "binance"
});Partial Configuration
You can update individual parameters without specifying all of them:
// Only change candle count, keep other defaults
await setConfig({
CC_AVG_PRICE_CANDLES_COUNT: 8,
});
// Later, only change timeout
await setConfig({
CC_SCHEDULE_AWAIT_MINUTES: 60,
});✅ Tested & Reliable
backtest-kit comes with 123 unit and integration tests covering:
- Signal validation and throttling
- PNL calculation with fees and slippage
- Crash recovery and state persistence
- Callback execution order (onSchedule, onOpen, onActive, onClose, onCancel)
- Markdown report generation (backtest, live, scheduled signals)
- Walker strategy comparison
- Heatmap portfolio analysis
- Position sizing calculations
- Risk management validation
- Scheduled signals lifecycle and cancellation tracking
- Event system
🤝 Contribute
We'd love your input! Fork the repo, submit a PR, or open an issue on GitHub. 🙌
📜 License
MIT © tripolskypetr 🖋️