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
π 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. β¨
π€ AI Strategy Optimizer: LLM-powered strategy generation from historical data. Train multiple strategy variants, compare performance, and auto-generate executable code. Supports Ollama integration with multi-timeframe analysis. π§
π§ͺ Comprehensive Test Coverage: Unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, scheduled signals, crash recovery, optimizer, and event system.
Backtest Kit supports multiple execution styles to match real trading behavior:
Market β instant execution using current VWAP
Limit β entry at a specified priceOpen
Take 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
Easy to add without modifying the core:
Stop / Stop-Limit β entry triggered by triggerPrice
Trailing 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
Get up and running in seconds:
npm install backtest-kit
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
});
listExchanges(), listStrategies(), listFrames(). π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. πPartial: Access partial profit/loss statistics and reports for risk management. Track signals reaching milestone levels (10%, 20%, 30%, etc.). πΉConstant: Kelly Criterion-based constants for optimal take profit (TP_LEVEL1-3) and stop loss (SL_LEVEL1-2) levels. π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 / PersistScheduleAdapter / PersistRiskAdapter / PersistPartialAdapter: Register custom adapters for signal, scheduled signal, risk, and partial state persistence.Optimizer: AI-powered strategy generation with LLM integration. Auto-generate strategies from historical data and export executable code. π§ Check out the sections below for detailed examples! π
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),
});
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);
},
},
});
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
}
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).
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|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:
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
Kelly Criterion - Optimal bet sizing based on win rate and win/loss ratio
ATR-based - Volatility-adjusted sizing, accounts for market conditions
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 { /* ... */ };
},
});
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.
The library uses three persistence layers:
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 profile
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);
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"
});
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);
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();
Redis - Best for high-performance distributed systems with multiple instances
MongoDB - Best for complex queries and analytics
PostgreSQL - Best for ACID transactions and relational data
File-based (default) - Best for single-instance deployments
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");
});
Partial Profit/Loss system tracks signal performance at fixed percentage levels (10%, 20%, 30%, etc.) for risk management and position scaling strategies.
The system automatically monitors profit/loss milestones and emits events when signals reach specific levels:
import {
listenPartialProfit,
listenPartialLoss,
listenPartialProfitOnce,
listenPartialLossOnce,
Constant
} from "backtest-kit";
// Listen to all profit levels (10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%)
listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
console.log(`${symbol} profit: ${level}% at ${price}`);
// Close portions at Kelly-optimized levels
if (level === Constant.TP_LEVEL3) {
console.log("Close 33% at 25% profit");
}
if (level === Constant.TP_LEVEL2) {
console.log("Close 33% at 50% profit");
}
if (level === Constant.TP_LEVEL1) {
console.log("Close 34% at 100% profit");
}
});
// Listen to all loss levels (10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%)
listenPartialLoss(({ symbol, signal, price, level, backtest }) => {
console.log(`${symbol} loss: -${level}% at ${price}`);
// Close portions at stop levels
if (level === Constant.SL_LEVEL2) {
console.log("Close 50% at -50% loss");
}
if (level === Constant.SL_LEVEL1) {
console.log("Close 50% at -100% loss");
}
});
// Listen once to first profit level reached
listenPartialProfitOnce(
() => true, // Accept any profit event
({ symbol, signal, price, level, backtest }) => {
console.log(`First profit milestone: ${level}%`);
}
);
// Listen once to first loss level reached
listenPartialLossOnce(
() => true, // Accept any loss event
({ symbol, signal, price, level, backtest }) => {
console.log(`First loss milestone: -${level}%`);
}
);
The Constant class provides predefined Kelly Criterion-based levels for optimal position sizing:
import { Constant } from "backtest-kit";
// Take Profit Levels
console.log(Constant.TP_LEVEL1); // 100% (aggressive target)
console.log(Constant.TP_LEVEL2); // 50% (moderate target)
console.log(Constant.TP_LEVEL3); // 25% (conservative target)
// Stop Loss Levels
console.log(Constant.SL_LEVEL1); // 100% (maximum risk)
console.log(Constant.SL_LEVEL2); // 50% (standard stop)
Use Case - Scale Out Strategy:
// Strategy: Close position in 3 tranches at optimal levels
listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
if (level === Constant.TP_LEVEL3) {
// Close 33% at 25% profit (secure early gains)
executePartialClose(symbol, signal.id, 0.33);
}
if (level === Constant.TP_LEVEL2) {
// Close 33% at 50% profit (lock in medium gains)
executePartialClose(symbol, signal.id, 0.33);
}
if (level === Constant.TP_LEVEL1) {
// Close 34% at 100% profit (maximize winners)
executePartialClose(symbol, signal.id, 0.34);
}
});
The Partial utility provides access to accumulated partial profit/loss data:
import { Partial } from "backtest-kit";
// Get statistical data
const stats = await Partial.getData("BTCUSDT");
console.log(stats);
// Returns:
// {
// totalEvents: 15, // Total profit/loss events
// totalProfit: 10, // Number of profit events
// totalLoss: 5, // Number of loss events
// eventList: [
// {
// timestamp: 1704370800000,
// action: "PROFIT", // PROFIT or LOSS
// symbol: "BTCUSDT",
// signalId: "abc123",
// position: "LONG", // or SHORT
// level: 10, // Percentage level reached
// price: 51500.00, // Current price at level
// mode: "Backtest" // or Live
// },
// // ... more events
// ]
// }
// Generate markdown report
const markdown = await Partial.getReport("BTCUSDT");
console.log(markdown);
// Save report to disk (default: ./dump/partial/BTCUSDT.md)
await Partial.dump("BTCUSDT");
// Custom output path
await Partial.dump("BTCUSDT", "./reports/partial");
Partial Report Example:
# Partial Profit/Loss Report: BTCUSDT
| Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
| --- | --- | --- | --- | --- | --- | --- | --- |
| PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
| PROFIT | BTCUSDT | abc123 | LONG | +20% | 53000.00000000 USD | 2024-01-15T11:15:00.000Z | Backtest |
| LOSS | BTCUSDT | def456 | SHORT | -10% | 51500.00000000 USD | 2024-01-15T14:00:00.000Z | Backtest |
**Total events:** 15
**Profit events:** 10
**Loss events:** 5
Partial profit/loss callbacks can also be configured at the strategy level:
import { addStrategy } from "backtest-kit";
addStrategy({
strategyName: "my-strategy",
interval: "5m",
getSignal: async (symbol) => { /* ... */ },
callbacks: {
onPartialProfit: (symbol, data, currentPrice, revenuePercent, backtest) => {
console.log(`Signal ${data.id} at ${revenuePercent.toFixed(2)}% profit`);
},
onPartialLoss: (symbol, data, currentPrice, lossPercent, backtest) => {
console.log(`Signal ${data.id} at ${lossPercent.toFixed(2)}% loss`);
},
},
});
Architecture:
ClientPartial - Tracks levels using Map<signalId, Set<level>> to prevent duplicatesClientStrategy - Calls partial.profit() / partial.loss() on every tickPartialMarkdownService - Accumulates events (max 250 per symbol) for reports./dump/data/partial/{symbol}/levels.jsonLevel Detection:
// For LONG position at entry price 50000
// Current price = 55000 β revenue = 10%
// Levels triggered: 10%
// Current price = 61000 β revenue = 22%
// Levels triggered: 10%, 20% (only 20% event emitted if 10% already triggered)
// For SHORT position at entry price 50000
// Current price = 45000 β revenue = 10%
// Levels triggered: 10%
Deduplication Guarantee:
Each level is emitted exactly once per signal:
Set<level> to track reached levelsCrash Recovery:
// Before crash:
// Signal opened at 50000, reached 10% and 20% profit
// State: { profitLevels: [10, 20], lossLevels: [] }
// Persisted to: ./dump/data/partial/BTCUSDT/levels.json
// After restart:
// State restored from disk
// Only new levels (30%, 40%, etc.) will emit events
// 10% and 20% won't fire again
Partial.getData() to track scaling effectivenesslistenPartialProfitOnce for first-level-only logicimport { Constant, listenPartialProfit } from "backtest-kit";
// Advanced: Dynamic scaling based on level
listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
const percentToClose =
level === Constant.TP_LEVEL3 ? 0.25 : // 25% at first level
level === Constant.TP_LEVEL2 ? 0.35 : // 35% at second level
level === Constant.TP_LEVEL1 ? 0.40 : // 40% at third level
0;
if (percentToClose > 0) {
executePartialClose(symbol, signal.id, percentToClose);
}
});
The framework includes a separate persistence system for scheduled signals (PersistScheduleAdapter) that works independently from pending/active signal persistence (PersistSignalAdapter). This separation ensures crash-safe recovery of both signal types.
The library uses two independent persistence layers for signals:
This dual-layer architecture ensures that both signal types can be recovered independently after crashes, with proper callbacks (onActive for pending signals, onSchedule for scheduled signals).
By default, scheduled signals are stored separately from pending signals:
./dump/data/
signal/
my-strategy/
BTCUSDT.json # Pending/active signal state
ETHUSDT.json
schedule/
my-strategy/
BTCUSDT.json # Scheduled signal state
ETHUSDT.json
During Normal Operation:
When a strategy generates a scheduled signal (limit order waiting for entry), the framework:
./dump/data/schedule/{strategyName}/{symbol}.jsonPersistSignalAdapterAfter System Crash:
When the system restarts:
_scheduledSignal)onSchedule() callback to notify about restored signalCrash Recovery Flow:
// Before crash:
// 1. Strategy generates signal with priceOpen = 50000 (current price = 49500)
// 2. Signal stored to ./dump/data/schedule/my-strategy/BTCUSDT.json
// 3. System waits for price to reach 50000
// 4. CRASH OCCURS at current price = 49800
// After restart:
// 1. System reads ./dump/data/schedule/my-strategy/BTCUSDT.json
// 2. Validates exchangeName and strategyName
// 3. Restores signal to _scheduledSignal
// 4. Calls onSchedule() callback with restored signal
// 5. Continues monitoring for price = 50000
// 6. When price reaches 50000, signal activates normally
interface IScheduledSignalRow {
id: string; // Unique signal ID
position: "long" | "short";
priceOpen: number; // Entry price (trigger price for scheduled signal)
priceTakeProfit: number;
priceStopLoss: number;
minuteEstimatedTime: number;
exchangeName: string; // Used for validation during restore
strategyName: string; // Used for validation during restore
timestamp: number;
pendingAt: number;
scheduledAt: number;
symbol: string;
_isScheduled: true; // Marker for scheduled signals
note?: string;
}
The ClientStrategy class uses setScheduledSignal() method to ensure all scheduled signal changes are persisted:
// WRONG - Direct assignment (not persisted)
this._scheduledSignal = newSignal;
// CORRECT - Using setScheduledSignal() method (persisted)
await this.setScheduledSignal(newSignal);
Automatic Persistence Locations:
All scheduled signal state changes are automatically persisted:
BACKTEST Mode Exception:
In backtest mode, persistence is skipped for performance reasons:
public async setScheduledSignal(scheduledSignal: IScheduledSignalRow | null) {
this._scheduledSignal = scheduledSignal;
if (this.params.execution.context.backtest) {
return; // Skip persistence in backtest mode
}
await PersistScheduleAdapter.writeScheduleData(
this._scheduledSignal,
this.params.strategyName,
this.params.execution.context.symbol
);
}
You can replace file-based scheduled signal persistence with custom adapters (Redis, MongoDB, etc.):
import { PersistScheduleAdapter, PersistBase } from "backtest-kit";
import Redis from "ioredis";
const redis = new Redis();
class RedisSchedulePersist extends PersistBase {
async waitForInit(initial: boolean): Promise<void> {
console.log(`Redis scheduled signal persistence initialized for ${this.entityName}`);
}
async readValue<T>(entityId: string | number): Promise<T> {
const key = `schedule:${this.entityName}:${entityId}`;
const data = await redis.get(key);
if (!data) {
throw new Error(`Scheduled signal ${this.entityName}:${entityId} not found`);
}
return JSON.parse(data) as T;
}
async hasValue(entityId: string | number): Promise<boolean> {
const key = `schedule:${this.entityName}:${entityId}`;
const exists = await redis.exists(key);
return exists === 1;
}
async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
const key = `schedule:${this.entityName}:${entityId}`;
const serializedData = JSON.stringify(entity);
await redis.set(key, serializedData);
// Optional: Set TTL for scheduled signals (e.g., 24 hours)
await redis.expire(key, 86400);
}
async removeValue(entityId: string | number): Promise<void> {
const key = `schedule:${this.entityName}:${entityId}`;
const result = await redis.del(key);
if (result === 0) {
throw new Error(`Scheduled signal ${this.entityName}:${entityId} not found for deletion`);
}
}
async removeAll(): Promise<void> {
const pattern = `schedule:${this.entityName}:*`;
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
async *values<T>(): AsyncGenerator<T> {
const pattern = `schedule:${this.entityName}:*`;
const keys = await redis.keys(pattern);
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;
}
}
}
async *keys(): AsyncGenerator<string> {
const pattern = `schedule:${this.entityName}:*`;
const keys = await redis.keys(pattern);
keys.sort((a, b) => a.localeCompare(b, undefined, {
numeric: true,
sensitivity: "base"
}));
for (const key of keys) {
const entityId = key.slice(`schedule:${this.entityName}:`.length);
yield entityId;
}
}
}
// Register Redis adapter for scheduled signal persistence
PersistScheduleAdapter.usePersistScheduleAdapter(RedisSchedulePersist);
Always use setScheduledSignal() - Never assign _scheduledSignal directly (except in waitForInit for restoration)
Validate signal metadata - Always store exchangeName and strategyName with signals for validation
Handle empty storage gracefully - Don't crash when readScheduleData() returns null
Test crash recovery - Write E2E tests that simulate system crashes and verify restoration
Choose persistence adapter wisely:
Monitor persistence operations - Use callbacks to track storage operations:
addStrategy({
strategyName: "my-strategy",
interval: "5m",
getSignal: async (symbol) => { /* ... */ },
callbacks: {
onSchedule: (symbol, signal, price, backtest) => {
console.log(`Scheduled signal created/restored: ${signal.id}`);
// Signal was either:
// 1. Newly generated and persisted
// 2. Restored from storage after crash
},
onCancel: (symbol, signal, price, backtest) => {
console.log(`Scheduled signal cancelled: ${signal.id}`);
// Signal was removed from storage
},
},
});
The Optimizer uses LLM (Large Language Models) to generate trading strategies from historical data. It automates the process of analyzing backtest results, generating strategy logic, and creating executable code.
import { addOptimizer, Optimizer } from "backtest-kit";
// Register optimizer configuration
addOptimizer({
optimizerName: "btc-optimizer",
// Training periods (multiple strategies generated)
rangeTrain: [
{
note: "Bull market Q1 2024",
startDate: new Date("2024-01-01T00:00:00Z"),
endDate: new Date("2024-03-31T23:59:59Z"),
},
{
note: "Consolidation Q2 2024",
startDate: new Date("2024-04-01T00:00:00Z"),
endDate: new Date("2024-06-30T23:59:59Z"),
},
],
// Testing period (Walker validates strategies)
rangeTest: {
note: "Validation Q3 2024",
startDate: new Date("2024-07-01T00:00:00Z"),
endDate: new Date("2024-09-30T23:59:59Z"),
},
// Data sources for strategy generation
source: [
{
name: "backtest-results",
fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
// Fetch closed signals from your backtest database
return await db.getBacktestResults({
symbol,
startDate,
endDate,
limit,
offset,
});
},
},
{
name: "market-indicators",
fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
// Fetch RSI, MACD, volume data, etc.
return await db.getIndicators({
symbol,
startDate,
endDate,
limit,
offset,
});
},
},
],
// LLM prompt generation from conversation history
getPrompt: async (symbol, messages) => {
// Analyze messages and create strategy prompt
return `
Based on the historical data, create a strategy that:
- Uses multi-timeframe analysis (1h, 15m, 5m, 1m)
- Identifies high-probability entry points
- Uses proper risk/reward ratios (min 1.5:1)
- Adapts to market conditions
`;
},
});
// Generate strategies and export code
await Optimizer.dump("BTCUSDT", {
optimizerName: "btc-optimizer"
}, "./generated");
// Output: ./generated/btc-optimizer_BTCUSDT.mjs
The Optimizer auto-generates a complete executable file with:
text() - LLM text generation for analysisjson() - Structured signal generation with schema validationdumpJson() - Debug logging to ./dump/strategyOverride default LLM message formatting:
addOptimizer({
optimizerName: "custom-optimizer",
rangeTrain: [...],
rangeTest: {...},
source: [...],
getPrompt: async (symbol, messages) => "...",
// Custom templates
template: {
getUserMessage: async (symbol, data, sourceName) => {
return `Analyze ${sourceName} data for ${symbol}:\n${JSON.stringify(data, null, 2)}`;
},
getAssistantMessage: async (symbol, data, sourceName) => {
return `Data from ${sourceName} analyzed successfully`;
},
},
});
Monitor optimizer operations:
addOptimizer({
optimizerName: "monitored-optimizer",
rangeTrain: [...],
rangeTest: {...},
source: [...],
getPrompt: async (symbol, messages) => "...",
callbacks: {
onSourceData: async (symbol, sourceName, data, startDate, endDate) => {
console.log(`Fetched ${data.length} rows from ${sourceName}`);
},
onData: async (symbol, strategyData) => {
console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
},
onCode: async (symbol, code) => {
console.log(`Generated ${code.length} bytes of code`);
},
onDump: async (symbol, filepath) => {
console.log(`Saved strategy to ${filepath}`);
},
},
});
Combine different data types for comprehensive analysis:
addOptimizer({
optimizerName: "multi-source-optimizer",
rangeTrain: [...],
rangeTest: {...},
source: [
// Source 1: Backtest results
{
name: "backtest-signals",
fetch: async (args) => await getBacktestSignals(args),
},
// Source 2: Market indicators
{
name: "technical-indicators",
fetch: async (args) => await getTechnicalIndicators(args),
},
// Source 3: Volume profile
{
name: "volume-analysis",
fetch: async (args) => await getVolumeProfile(args),
},
// Source 4: Order book depth
{
name: "order-book",
fetch: async (args) => await getOrderBookData(args),
},
],
getPrompt: async (symbol, messages) => {
// LLM has full context from all sources
return `Create strategy using all available data sources`;
},
});
// Get strategy metadata (no code generation)
const strategies = await Optimizer.getData("BTCUSDT", {
optimizerName: "my-optimizer"
});
// strategies[0].messages - LLM conversation history
// strategies[0].strategy - Generated strategy prompt
// Generate executable code
const code = await Optimizer.getCode("BTCUSDT", {
optimizerName: "my-optimizer"
});
// Save to file
await Optimizer.dump("BTCUSDT", {
optimizerName: "my-optimizer"
}, "./output"); // Default: "./"
The Optimizer uses Ollama for LLM inference:
# Set up Ollama API
export OLLAMA_API_KEY=your-api-key
# Run generated strategy
node generated/btc-optimizer_BTCUSDT.mjs
Generated strategies use:
gpt-oss:20b (configurable in templates)The framework follows clean architecture with:
PersistSignalAdapterAll 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.
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"
Generate detailed trading reports with statistics:
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");
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");
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
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);
}
});
import {
listenPartialProfit,
listenPartialLoss,
listenPartialProfitOnce,
listenPartialLossOnce,
Constant
} from "backtest-kit";
// Listen to all profit milestones
listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
console.log(`${symbol} reached ${level}% profit at ${price}`);
// Scale out at Kelly-optimized levels
if (level === Constant.TP_LEVEL3) {
console.log("Close 33% at 25% profit");
}
if (level === Constant.TP_LEVEL2) {
console.log("Close 33% at 50% profit");
}
if (level === Constant.TP_LEVEL1) {
console.log("Close 34% at 100% profit");
}
});
// Listen to all loss milestones
listenPartialLoss(({ symbol, signal, price, level, backtest }) => {
console.log(`${symbol} reached -${level}% loss at ${price}`);
// Scale out at stop levels
if (level === Constant.SL_LEVEL2) {
console.log("Close 50% at -50% loss");
}
if (level === Constant.SL_LEVEL1) {
console.log("Close 50% at -100% loss");
}
});
// Listen once to first profit level
listenPartialProfitOnce(
() => true, // Accept any profit event
({ symbol, signal, price, level, backtest }) => {
console.log(`First profit milestone: ${level}%`);
}
);
// Listen once to first loss level
listenPartialLossOnce(
() => true, // Accept any loss event
({ symbol, signal, price, level, backtest }) => {
console.log(`First loss milestone: -${level}%`);
}
);
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);
}
);
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);
});
You can customize framework behavior using the setConfig() function. This allows you to adjust global parameters without modifying the source code.
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,
});
CC_SCHEDULE_AWAIT_MINUTESControls how long scheduled signals wait for activation before being cancelled.
120 minutes (2 hours)// 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_COUNTControls the number of 1-minute candles used for VWAP (Volume Weighted Average Price) calculations.
5 candles (5 minutes of data)// 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,
});
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"
});
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,
});
backtest-kit comes with 217 unit and integration tests covering:
We'd love your input! Fork the repo, submit a PR, or open an issue on GitHub. π
MIT Β© tripolskypetr ποΈ