backtest-kit
Version:
A TypeScript library for trading system backtest
1,473 lines (1,160 loc) β’ 74.6 kB
Markdown
<img src="./assets/triangle.svg" height="105px" align="right">
# π§Ώ Backtest Kit
> **A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.**
[](https://deepwiki.com/tripolskypetr/backtest-kit)
[](https://npmjs.org/package/backtest-kit)
[]()
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](https://github.com/tripolskypetr/backtest-kit)** | π **[Quick Start](#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. β¨
- π€ **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.
---
### π³ 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 `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
### π Extendable Order Types
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
---
## π Getting Started
### Installation
Get up and running in seconds:
```bash
npm install backtest-kit
```
### Quick Example
Here's a taste of what `backtest-kit` can doβcreate a simple moving average crossover strategy with crash-safe persistence:
```typescript
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 π§©
```typescript
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. π
- π **`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! π
---
## π Advanced Features
### 1. Register Exchange Data Source
You can plug any data source: CCXT for live data or a database for faster backtesting:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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 percentage
- `avgPnl` - Average PNL percentage
- `totalPnl` - Total PNL percentage
- `certaintyRatio` - avgWin / |avgLoss|
### 6. Portfolio Heatmap
Heat provides portfolio-wide performance analysis across multiple symbols:
```typescript
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:**
```markdown
# 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:
```typescript
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:**
1. **Fixed Percentage** - Simple risk management, consistent risk per trade
- Best for: Beginners, conservative strategies
- Risk: Fixed 1-2% per trade
2. **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
3. **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:
```typescript
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:
1. **PersistBase** - Base class for all persistence operations (file-based by default)
2. **PersistSignalAdapter** - Manages signal state persistence (used by Live mode)
3. **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 profile
```
#### Create Custom Adapter (Redis Example)
```typescript
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)
```typescript
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
```typescript
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:
```typescript
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
1. **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
2. **MongoDB** - Best for complex queries and analytics
- Rich query language
- Aggregation pipelines
- Scalable for large datasets
3. **PostgreSQL** - Best for ACID transactions and relational data
- Strong consistency guarantees
- Complex joins and queries
- Mature ecosystem
4. **File-based (default)** - Best for single-instance deployments
- No dependencies
- Simple debugging (inspect JSON files)
- Sufficient for most use cases
#### Testing Custom Adapters
```typescript
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");
});
```
### 10. Partial Profit/Loss Tracking
Partial Profit/Loss system tracks signal performance at fixed percentage levels (10%, 20%, 30%, etc.) for risk management and position scaling strategies.
#### Understanding Partial Levels
The system automatically monitors profit/loss milestones and emits events when signals reach specific levels:
```typescript
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}%`);
}
);
```
#### Constant Utility - Kelly-Optimized Levels
The `Constant` class provides predefined Kelly Criterion-based levels for optimal position sizing:
```typescript
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:**
```typescript
// 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);
}
});
```
#### Partial Reports and Statistics
The `Partial` utility provides access to accumulated partial profit/loss data:
```typescript
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:**
```markdown
# 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
```
#### Strategy Callbacks
Partial profit/loss callbacks can also be configured at the strategy level:
```typescript
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`);
},
},
});
```
#### How Partial Levels Work
**Architecture:**
1. `ClientPartial` - Tracks levels using `Map<signalId, Set<level>>` to prevent duplicates
2. `ClientStrategy` - Calls `partial.profit()` / `partial.loss()` on every tick
3. `PartialMarkdownService` - Accumulates events (max 250 per symbol) for reports
4. State persisted to disk: `./dump/data/partial/{symbol}/levels.json`
**Level Detection:**
```typescript
// 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**:
- Uses `Set<level>` to track reached levels
- Persisted to disk for crash recovery
- Restored on system restart
**Crash Recovery:**
```typescript
// 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
```
#### Best Practices
1. **Use Constant for Kelly-Optimized Levels** - Don't hardcode profit/loss levels
2. **Scale Out Gradually** - Close positions in tranches (25%, 50%, 100%)
3. **Monitor Partial Statistics** - Use `Partial.getData()` to track scaling effectiveness
4. **Filter Events** - Use `listenPartialProfitOnce` for first-level-only logic
5. **Combine with Position Sizing** - Scale out inversely to volatility
```typescript
import { 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);
}
});
```
---
### 11. Scheduled Signal Persistence
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.
#### Understanding the Dual Persistence System
The library uses **two independent persistence layers** for signals:
1. **PersistSignalAdapter** - Manages pending/active signals (signals that are already opened or waiting to reach TP/SL)
2. **PersistScheduleAdapter** - Manages scheduled signals (signals waiting for entry price to activate)
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).
#### Default Storage Structure
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
```
#### How Scheduled Signal Persistence Works
**During Normal Operation:**
When a strategy generates a scheduled signal (limit order waiting for entry), the framework:
1. Stores the signal to disk using atomic writes: `./dump/data/schedule/{strategyName}/{symbol}.json`
2. Monitors price movements for activation
3. When price reaches entry point OR cancellation condition occurs:
- Deletes scheduled signal from storage
- Optionally creates pending signal in `PersistSignalAdapter`
**After System Crash:**
When the system restarts:
1. Framework checks for stored scheduled signals during initialization
2. Validates exchange name and strategy name match (security protection)
3. Restores scheduled signal to memory (`_scheduledSignal`)
4. Calls `onSchedule()` callback to notify about restored signal
5. Continues monitoring from where it left off
**Crash Recovery Flow:**
```typescript
// 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
```
#### Scheduled Signal Data Structure
```typescript
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;
}
```
#### Integration with ClientStrategy
The `ClientStrategy` class uses `setScheduledSignal()` method to ensure all scheduled signal changes are persisted:
```typescript
// 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:
- Signal generation (new scheduled signal created)
- Signal activation (scheduled β pending transition)
- Signal cancellation (timeout or stop loss hit before activation)
- Manual signal clearing
**BACKTEST Mode Exception:**
In backtest mode, persistence is **skipped** for performance reasons:
```typescript
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
);
}
```
#### Custom Scheduled Signal Adapters
You can replace file-based scheduled signal persistence with custom adapters (Redis, MongoDB, etc.):
```typescript
import { PersistScheduleAdapter, PersistBase } from "backtest-kit";
import Redis from "ioredis";
const redis = new Redis();
class RedisSchedulePersist extends PersistBase {
async waitForInit(initial: boolean): Promise<void