UNPKG

backtest-kit

Version:

A TypeScript library for trading system backtest

1,473 lines (1,160 loc) β€’ 74.6 kB
<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.** [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit) [![npm](https://img.shields.io/npm/v/backtest-kit.svg?style=flat-square)](https://npmjs.org/package/backtest-kit) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]() 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