backtest-kit 1.1.8 β†’ 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +806 -970
  2. package/build/index.cjs +3588 -275
  3. package/build/index.mjs +3569 -275
  4. package/package.json +1 -1
  5. package/types.d.ts +2955 -520
package/README.md CHANGED
@@ -1,90 +1,284 @@
1
1
  # 🧿 Backtest Kit
2
2
 
3
- > A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.
3
+ > **A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.**
4
4
 
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)
6
+ [![npm](https://img.shields.io/npm/v/backtest-kit.svg?style=flat-square)](https://npmjs.org/package/backtest-kit)
6
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]()
7
- [![Architecture](https://img.shields.io/badge/architecture-clean-orange)]()
8
-
9
- ## Features
10
-
11
- - πŸš€ **Production-Ready Architecture** - Backtest/live mode, robust error recovery
12
- - πŸ’Ύ **Crash-Safe Persistence** - Atomic file writes with automatic state recovery
13
- - βœ… **Signal Validation** - Comprehensive validation prevents invalid trades
14
- - πŸ”„ **Async Generators** - Memory-efficient streaming for backtest and live execution
15
- - πŸ“Š **VWAP Pricing** - Volume-weighted average price from last 5 1m candles
16
- - 🎯 **Signal Lifecycle** - Type-safe state machine (idle β†’ opened β†’ active β†’ closed)
17
- - πŸ“ˆ **Accurate PNL** - Calculation with fees (0.1%) and slippage (0.1%)
18
- - 🧠 **Interval Throttling** - Prevents signal spam at strategy level
19
- - ⚑ **Memory Optimized** - Prototype methods + memoization + streaming
20
- - πŸ”Œ **Flexible Architecture** - Plug your own exchanges and strategies
21
- - πŸ“ **Markdown Reports** - Auto-generated trading reports with statistics (win rate, avg PNL, Sharpe Ratio, Standard Deviation, Certainty Ratio, Expected Yearly Returns, Risk-Adjusted Returns)
22
- - πŸ“Š **Performance Profiling** - Built-in performance tracking with aggregated statistics (avg, min, max, stdDev, P95, P99) for bottleneck analysis
23
- - πŸ›‘ **Graceful Shutdown** - Live.background() waits for open positions to close before stopping
24
- - πŸ’‰ **Strategy Dependency Injection** - addStrategy() enables DI pattern for trading strategies
25
- - πŸ” **Schema Reflection API** - listExchanges(), listStrategies(), listFrames() for runtime introspection
26
- - πŸ§ͺ **Comprehensive Test Coverage** - 61 unit tests covering validation, PNL, callbacks, reports, performance tracking, and event system
27
- - πŸ’Ύ **Zero Data Download** - Unlike Freqtrade, no need to download gigabytes of historical data - plug any data source (CCXT, database, API)
28
- - πŸ”’ **Safe Math & Robustness** - All metrics protected against NaN/Infinity with unsafe numeric checks, returns N/A for invalid calculations
29
-
30
- ## Installation
8
+
9
+ 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.
10
+
11
+ πŸ“š **[API Reference](https://github.com/tripolskypetr/backtest-kit)** | 🌟 **[Quick Start](#quick-start)**
12
+
13
+ ## 🎯 Supported Order Types
14
+
15
+ Backtest Kit supports multiple execution styles to match real trading behavior:
16
+
17
+ ## ✨ Why Choose Backtest Kit?
18
+
19
+ - πŸš€ **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. βœ…
20
+
21
+ - πŸ’Ύ **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. πŸ”„
22
+
23
+ - βœ… **Signal Validation**: Comprehensive validation prevents invalid trades before execution. Catches price logic errors (TP/SL), throttles signal spam, and ensures data integrity. πŸ›‘οΈ
24
+
25
+ - πŸ”„ **Async Generator Architecture**: Memory-efficient streaming for backtest and live execution. Process years of historical data without loading everything into memory. ⚑
26
+
27
+ - πŸ“Š **VWAP Pricing**: Volume-weighted average price from last 5 1-minute candles ensures realistic backtest results that match live execution. πŸ“ˆ
28
+
29
+ - 🎯 **Type-Safe Signal Lifecycle**: State machine with compile-time guarantees (idle β†’ opened β†’ active β†’ closed). No runtime state confusion. πŸ”’
30
+
31
+ - πŸ“ˆ **Accurate PNL Calculation**: Realistic profit/loss with configurable fees (0.1%) and slippage (0.1%). Track gross and net returns separately. πŸ’°
32
+
33
+ - ⏰ **Time-Travel Context**: Async context propagation allows same strategy code to run in backtest (with historical time) and live (with real-time) without modifications. 🌐
34
+
35
+ - πŸ“ **Auto-Generated Reports**: Markdown reports with statistics (win rate, avg PNL, Sharpe Ratio, standard deviation, certainty ratio, expected yearly returns, risk-adjusted returns). πŸ“Š
36
+
37
+ - πŸ“Š **Revenue Profiling**: Built-in performance tracking with aggregated statistics (avg, min, max, stdDev, P95, P99) for bottleneck analysis. ⚑
38
+
39
+ - πŸƒ **Strategy Comparison (Walker)**: Compare multiple strategies in parallel with automatic ranking and statistical analysis. Find your best performer. πŸ†
40
+
41
+ - πŸ”₯ **Portfolio Heatmap**: Multi-symbol performance analysis with extended metrics (Profit Factor, Expectancy, Win/Loss Streaks, Avg Win/Loss) sorted by Sharpe Ratio. πŸ“‰
42
+
43
+ - πŸ’° **Position Sizing Calculator**: Built-in position sizing methods (Fixed Percentage, Kelly Criterion, ATR-based) with risk management constraints. πŸ’΅
44
+
45
+ - πŸ›‘οΈ **Risk Management System**: Portfolio-level risk controls with custom validation logic, concurrent position limits, and cross-strategy coordination. πŸ”
46
+
47
+ - πŸ’Ύ **Zero Data Download**: Unlike Freqtrade, no need to download gigabytes of historical dataβ€”plug any data source (CCXT, database, API). πŸš€
48
+
49
+ - πŸ”Œ **Pluggable Persistence**: Replace default file-based persistence with custom adapters (Redis, MongoDB, PostgreSQL) for distributed systems and high-performance scenarios. πŸ’Ύ
50
+
51
+ - πŸ”’ **Safe Math & Robustness**: All metrics protected against NaN/Infinity with unsafe numeric checks. Returns N/A for invalid calculations. ✨
52
+
53
+ - πŸ§ͺ **Comprehensive Test Coverage**: 109 unit and integration tests covering validation, PNL, callbacks, reports, performance tracking, walker, heatmap, position sizing, risk management, and event system. βœ…
54
+
55
+ ---
56
+
57
+ ### βœ… Built-in Order Types
58
+
59
+ - **Market** β€” instant execution using current VWAP
60
+
61
+ - **Limit** β€” entry at a specified `priceOpen`
62
+
63
+ - **Take Profit (TP)** β€” automatic exit at the target price
64
+
65
+ - **Stop Loss (SL)** β€” protective exit at the stop level
66
+
67
+ - **OCO (TP + SL)** β€” linked exits; one cancels the other
68
+
69
+ - **Time-Expired** β€” automatic closure after `minuteEstimatedTime` ⏱️
70
+
71
+
72
+ ### βž• Extendable Order Types
73
+
74
+ Easy to add without modifying the core:
75
+
76
+ - **Stop / Stop-Limit** β€” entry triggered by `triggerPrice`
77
+
78
+ - **Trailing Stop** β€” dynamic SL based on market movement
79
+
80
+ - **Conditional Entry** β€” enter only if price breaks a level (`above` / `below`)
81
+
82
+ - **Post-Only / Reduce-Only** β€” exchange-level execution flags
83
+
84
+ ---
85
+
86
+ ## πŸš€ Getting Started
87
+
88
+ ### Installation
89
+
90
+ Get up and running in seconds:
31
91
 
32
92
  ```bash
33
93
  npm install backtest-kit
34
94
  ```
35
95
 
36
- ## Quick Start
96
+ ### Quick Example
37
97
 
38
- ### 1. Register Exchange Data Source
98
+ Here's a taste of what `backtest-kit` can doβ€”create a simple moving average crossover strategy with crash-safe persistence:
39
99
 
40
100
  ```typescript
41
- import { addExchange } from "backtest-kit";
42
- import ccxt from "ccxt"; // Example using CCXT library
101
+ import {
102
+ addExchange,
103
+ addStrategy,
104
+ addFrame,
105
+ Backtest,
106
+ listenSignalBacktest,
107
+ listenError,
108
+ listenDoneBacktest
109
+ } from "backtest-kit";
110
+ import ccxt from "ccxt";
43
111
 
112
+ // 1. Register exchange data source
44
113
  addExchange({
45
114
  exchangeName: "binance",
46
-
47
- // Fetch historical candles
48
115
  getCandles: async (symbol, interval, since, limit) => {
49
116
  const exchange = new ccxt.binance();
50
117
  const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
51
-
52
118
  return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
53
- timestamp,
54
- open,
55
- high,
56
- low,
57
- close,
58
- volume,
119
+ timestamp, open, high, low, close, volume
59
120
  }));
60
121
  },
122
+ formatPrice: async (symbol, price) => price.toFixed(2),
123
+ formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
124
+ });
61
125
 
62
- // Format price according to exchange rules (e.g., 2 decimals for BTC)
63
- formatPrice: async (symbol, price) => {
64
- const exchange = new ccxt.binance();
65
- const market = exchange.market(symbol);
66
- return exchange.priceToPrecision(symbol, price);
126
+ // 2. Register trading strategy
127
+ addStrategy({
128
+ strategyName: "sma-crossover",
129
+ interval: "5m", // Throttling: signals generated max once per 5 minutes
130
+ getSignal: async (symbol) => {
131
+ // Your signal generation logic
132
+ return {
133
+ position: "long",
134
+ note: "BTC breakout",
135
+ priceOpen: 50000,
136
+ priceTakeProfit: 51000, // Must be > priceOpen for long
137
+ priceStopLoss: 49000, // Must be < priceOpen for long
138
+ minuteEstimatedTime: 60, // Signal duration in minutes
139
+ };
67
140
  },
68
-
69
- // Format quantity according to exchange rules (e.g., 8 decimals)
70
- formatQuantity: async (symbol, quantity) => {
71
- const exchange = new ccxt.binance();
72
- return exchange.amountToPrecision(symbol, quantity);
141
+ callbacks: {
142
+ onOpen: (symbol, signal, currentPrice, backtest) => {
143
+ console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
144
+ },
145
+ onClose: (symbol, signal, priceClose, backtest) => {
146
+ console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
147
+ },
73
148
  },
74
149
  });
150
+
151
+ // 3. Add timeframe generator
152
+ addFrame({
153
+ frameName: "1d-backtest",
154
+ interval: "1m",
155
+ startDate: new Date("2024-01-01T00:00:00Z"),
156
+ endDate: new Date("2024-01-02T00:00:00Z"),
157
+ });
158
+
159
+ // 4. Run backtest in background
160
+ Backtest.background("BTCUSDT", {
161
+ strategyName: "sma-crossover",
162
+ exchangeName: "binance",
163
+ frameName: "1d-backtest"
164
+ });
165
+
166
+ // Listen to closed signals
167
+ listenSignalBacktest((event) => {
168
+ if (event.action === "closed") {
169
+ console.log("PNL:", event.pnl.pnlPercentage);
170
+ }
171
+ });
172
+
173
+ // Listen to backtest completion
174
+ listenDoneBacktest((event) => {
175
+ console.log("Backtest completed:", event.symbol);
176
+ Backtest.dump(event.strategyName); // ./logs/backtest/sma-crossover.md
177
+ });
75
178
  ```
76
179
 
77
- **Alternative: Database implementation**
180
+ 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 🧩
181
+
182
+ ```typescript
183
+ export enum ExchangeName {
184
+ Binance = "binance",
185
+ Bybit = "bybit",
186
+ }
187
+
188
+ export enum StrategyName {
189
+ SMACrossover = "sma-crossover",
190
+ RSIStrategy = "rsi-strategy",
191
+ }
192
+
193
+ export enum FrameName {
194
+ OneDay = "1d-backtest",
195
+ OneWeek = "1w-backtest",
196
+ }
197
+
198
+ // ...
199
+
200
+ addStrategy({
201
+ strategyName: StrategyName.SMACrossover,
202
+ interval: "5m",
203
+ // ...
204
+ });
205
+
206
+ Backtest.background("BTCUSDT", {
207
+ strategyName: StrategyName.SMACrossover,
208
+ exchangeName: ExchangeName.Binance,
209
+ frameName: FrameName.OneDay
210
+ });
211
+ ```
212
+
213
+ ---
214
+
215
+ ## 🌟 Key Features
216
+
217
+ - 🀝 **Mode Switching**: Seamlessly switch between backtest and live modes with identical strategy code. πŸ”„
218
+ - πŸ“œ **Crash Recovery**: Atomic persistence ensures state recovery after crashesβ€”no duplicate signals. πŸ—‚οΈ
219
+ - πŸ› οΈ **Custom Validators**: Define validation rules with strategy-level throttling and price logic checks. πŸ”§
220
+ - πŸ›‘οΈ **Signal Lifecycle**: Type-safe state machine prevents invalid state transitions. πŸš‘
221
+ - πŸ“¦ **Dependency Inversion**: Lazy-load components at runtime for modular, scalable designs. 🧩
222
+ - πŸ” **Schema Reflection**: Runtime introspection with `listExchanges()`, `listStrategies()`, `listFrames()`. πŸ“Š
223
+
224
+ ---
225
+
226
+ ## 🎯 Use Cases
227
+
228
+ - πŸ“ˆ **Algorithmic Trading**: Backtest and deploy systematic trading strategies with confidence. πŸ’Ή
229
+ - πŸ€– **Strategy Development**: Rapid prototyping with automatic validation and PNL tracking. πŸ› οΈ
230
+ - πŸ“Š **Performance Analysis**: Compare strategies with Walker and analyze portfolios with Heatmap. πŸ“‰
231
+ - πŸ’Ό **Portfolio Management**: Multi-symbol trading with risk controls and position sizing. 🏦
232
+
233
+ ---
234
+
235
+ ## πŸ“– API Highlights
236
+
237
+ - πŸ› οΈ **`addExchange`**: Define exchange data sources (CCXT, database, API). πŸ“‘
238
+ - πŸ€– **`addStrategy`**: Create trading strategies with custom signals and callbacks. πŸ’‘
239
+ - 🌐 **`addFrame`**: Configure timeframes for backtesting. πŸ“…
240
+ - πŸ”„ **`Backtest` / `Live`**: Run strategies in backtest or live mode (generator or background). ⚑
241
+ - πŸƒ **`Walker`**: Compare multiple strategies in parallel with ranking. πŸ†
242
+ - πŸ”₯ **`Heat`**: Portfolio-wide performance analysis across multiple symbols. πŸ“Š
243
+ - πŸ’° **`PositionSize`**: Calculate position sizes with Fixed %, Kelly Criterion, or ATR-based methods. πŸ’΅
244
+ - πŸ›‘οΈ **`addRisk`**: Portfolio-level risk management with custom validation logic. πŸ”
245
+ - πŸ’Ύ **`PersistBase`**: Base class for custom persistence adapters (Redis, MongoDB, PostgreSQL). πŸ—„οΈ
246
+ - πŸ”Œ **`PersistSignalAdapter` / `PersistRiskAdapter`**: Register custom adapters for signal and risk persistence. πŸ”„
247
+
248
+ Check out the sections below for detailed examples! πŸ“š
249
+
250
+ ---
251
+
252
+ ## πŸ›  Advanced Features
253
+
254
+ ### 1. Register Exchange Data Source
255
+
256
+ You can plug any data sourceβ€”CCXT for live data or a database for faster backtesting:
78
257
 
79
258
  ```typescript
80
259
  import { addExchange } from "backtest-kit";
81
- import { db } from "./database"; // Your database client
260
+ import ccxt from "ccxt";
82
261
 
262
+ // Option 1: CCXT (live or historical)
83
263
  addExchange({
84
- exchangeName: "binance-db",
264
+ exchangeName: "binance",
265
+ getCandles: async (symbol, interval, since, limit) => {
266
+ const exchange = new ccxt.binance();
267
+ const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
268
+ return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
269
+ timestamp, open, high, low, close, volume
270
+ }));
271
+ },
272
+ formatPrice: async (symbol, price) => price.toFixed(2),
273
+ formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
274
+ });
85
275
 
276
+ // Option 2: Database (faster backtesting)
277
+ import { db } from "./database";
278
+
279
+ addExchange({
280
+ exchangeName: "binance-db",
86
281
  getCandles: async (symbol, interval, since, limit) => {
87
- // Fetch from database for faster backtesting
88
282
  return await db.query(`
89
283
  SELECT timestamp, open, high, low, close, volume
90
284
  FROM candles
@@ -93,7 +287,6 @@ addExchange({
93
287
  LIMIT $4
94
288
  `, [symbol, interval, since, limit]);
95
289
  },
96
-
97
290
  formatPrice: async (symbol, price) => price.toFixed(2),
98
291
  formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
99
292
  });
@@ -101,6 +294,8 @@ addExchange({
101
294
 
102
295
  ### 2. Register Trading Strategy
103
296
 
297
+ Define your signal generation logic with automatic validation:
298
+
104
299
  ```typescript
105
300
  import { addStrategy } from "backtest-kit";
106
301
 
@@ -109,7 +304,6 @@ addStrategy({
109
304
  interval: "5m", // Throttling: signals generated max once per 5 minutes
110
305
  getSignal: async (symbol) => {
111
306
  // Your signal generation logic
112
- // Validation happens automatically (prices, TP/SL logic)
113
307
  return {
114
308
  position: "long",
115
309
  note: "BTC breakout",
@@ -130,62 +324,48 @@ addStrategy({
130
324
  });
131
325
  ```
132
326
 
133
- ### 3. Add Timeframe Generator
327
+ ### 3. Run Backtest
134
328
 
135
- ```typescript
136
- import { addFrame } from "backtest-kit";
137
-
138
- addFrame({
139
- frameName: "1d-backtest",
140
- interval: "1m",
141
- startDate: new Date("2024-01-01T00:00:00Z"),
142
- endDate: new Date("2024-01-02T00:00:00Z"),
143
- callbacks: {
144
- onTimeframe: (timeframe, startDate, endDate, interval) => {
145
- console.log(`Generated ${timeframe.length} timeframes from ${startDate} to ${endDate}`);
146
- },
147
- },
148
- });
149
- ```
150
-
151
- ### 4. Run Backtest
329
+ Run strategies in background mode (infinite loop) or manually iterate with async generators:
152
330
 
153
331
  ```typescript
154
- import { Backtest, listenSignalBacktest, listenError, listenDone } from "backtest-kit";
332
+ import { Backtest, listenSignalBacktest, listenDoneBacktest } from "backtest-kit";
155
333
 
156
- // Run backtest in background
334
+ // Option 1: Background mode (recommended)
157
335
  const stopBacktest = Backtest.background("BTCUSDT", {
158
336
  strategyName: "my-strategy",
159
337
  exchangeName: "binance",
160
338
  frameName: "1d-backtest"
161
339
  });
162
340
 
163
- // Listen to closed signals
164
341
  listenSignalBacktest((event) => {
165
342
  if (event.action === "closed") {
166
343
  console.log("PNL:", event.pnl.pnlPercentage);
167
344
  }
168
345
  });
169
346
 
170
- // Listen to errors
171
- listenError((error) => {
172
- console.error("Error:", error.message);
347
+ listenDoneBacktest((event) => {
348
+ console.log("Backtest completed:", event.symbol);
349
+ Backtest.dump(event.strategyName); // ./logs/backtest/my-strategy.md
173
350
  });
174
351
 
175
- // Listen to completion
176
- listenDone((event) => {
177
- if (event.backtest) {
178
- console.log("Backtest completed:", event.symbol);
179
- // Generate and save report
180
- Backtest.dump(event.strategyName); // ./logs/backtest/my-strategy.md
181
- }
182
- });
352
+ // Option 2: Manual iteration (for custom control)
353
+ for await (const result of Backtest.run("BTCUSDT", {
354
+ strategyName: "my-strategy",
355
+ exchangeName: "binance",
356
+ frameName: "1d-backtest"
357
+ })) {
358
+ console.log("PNL:", result.pnl.pnlPercentage);
359
+ if (result.pnl.pnlPercentage < -5) break; // Early termination
360
+ }
183
361
  ```
184
362
 
185
- ### 5. Run Live Trading (Crash-Safe)
363
+ ### 4. Run Live Trading (Crash-Safe)
364
+
365
+ Live mode automatically persists state to disk with atomic writes:
186
366
 
187
367
  ```typescript
188
- import { Live, listenSignalLive, listenError, listenDone } from "backtest-kit";
368
+ import { Live, listenSignalLive } from "backtest-kit";
189
369
 
190
370
  // Run live trading in background (infinite loop, crash-safe)
191
371
  const stop = Live.background("BTCUSDT", {
@@ -193,7 +373,6 @@ const stop = Live.background("BTCUSDT", {
193
373
  exchangeName: "binance"
194
374
  });
195
375
 
196
- // Listen to all signal events
197
376
  listenSignalLive((event) => {
198
377
  if (event.action === "opened") {
199
378
  console.log("Signal opened:", event.signal.id);
@@ -204,1054 +383,711 @@ listenSignalLive((event) => {
204
383
  reason: event.closeReason,
205
384
  pnl: event.pnl.pnlPercentage,
206
385
  });
207
-
208
- // Auto-save report
209
- Live.dump(event.strategyName);
210
- }
211
- });
212
-
213
- // Listen to errors
214
- listenError((error) => {
215
- console.error("Error:", error.message);
216
- });
217
-
218
- // Listen to completion
219
- listenDone((event) => {
220
- if (!event.backtest) {
221
- console.log("Live trading stopped:", event.symbol);
386
+ Live.dump(event.strategyName); // Auto-save report
222
387
  }
223
388
  });
224
389
 
225
390
  // Stop when needed: stop();
226
391
  ```
227
392
 
228
- **Crash Recovery:** If process crashes, restart with same code - state automatically recovered from disk (no duplicate signals).
229
-
230
- ### 6. Alternative: Async Generators (Optional)
231
-
232
- For manual control over execution flow:
233
-
234
- ```typescript
235
- import { Backtest, Live } from "backtest-kit";
236
-
237
- // Manual backtest iteration
238
- for await (const result of Backtest.run("BTCUSDT", {
239
- strategyName: "my-strategy",
240
- exchangeName: "binance",
241
- frameName: "1d-backtest"
242
- })) {
243
- console.log("PNL:", result.pnl.pnlPercentage);
244
- if (result.pnl.pnlPercentage < -5) break; // Early termination
245
- }
246
-
247
- // Manual live iteration (infinite loop)
248
- for await (const result of Live.run("BTCUSDT", {
249
- strategyName: "my-strategy",
250
- exchangeName: "binance"
251
- })) {
252
- if (result.action === "closed") {
253
- console.log("PNL:", result.pnl.pnlPercentage);
254
- }
255
- }
256
- ```
393
+ **Crash Recovery:** If process crashes, restart with same codeβ€”state automatically recovered from disk (no duplicate signals).
257
394
 
258
- ### 7. Schema Reflection API (Optional)
395
+ ### 5. Strategy Comparison with Walker
259
396
 
260
- Retrieve registered schemas at runtime for debugging, documentation, or building dynamic UIs:
397
+ Walker runs multiple strategies in parallel and ranks them by a selected metric:
261
398
 
262
399
  ```typescript
263
- import {
264
- addExchange,
265
- addStrategy,
266
- addFrame,
267
- listExchanges,
268
- listStrategies,
269
- listFrames
270
- } from "backtest-kit";
400
+ import { addWalker, Walker, listenWalkerComplete } from "backtest-kit";
271
401
 
272
- // Register schemas with notes
273
- addExchange({
402
+ // Register walker schema
403
+ addWalker({
404
+ walkerName: "btc-walker",
274
405
  exchangeName: "binance",
275
- note: "Binance cryptocurrency exchange with database backend",
276
- getCandles: async (symbol, interval, since, limit) => [...],
277
- formatPrice: async (symbol, price) => price.toFixed(2),
278
- formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
406
+ frameName: "1d-backtest",
407
+ strategies: ["strategy-a", "strategy-b", "strategy-c"],
408
+ metric: "sharpeRatio", // Metric to compare strategies
409
+ callbacks: {
410
+ onStrategyStart: (strategyName, symbol) => {
411
+ console.log(`Starting strategy: ${strategyName}`);
412
+ },
413
+ onStrategyComplete: (strategyName, symbol, stats) => {
414
+ console.log(`${strategyName} completed:`, stats.sharpeRatio);
415
+ },
416
+ onComplete: (results) => {
417
+ console.log("Best strategy:", results.bestStrategy);
418
+ console.log("Best metric:", results.bestMetric);
419
+ },
420
+ },
279
421
  });
280
422
 
281
- addStrategy({
282
- strategyName: "sma-crossover",
283
- note: "Simple moving average crossover strategy (50/200)",
284
- interval: "5m",
285
- getSignal: async (symbol) => ({...}),
423
+ // Run walker in background
424
+ Walker.background("BTCUSDT", {
425
+ walkerName: "btc-walker"
286
426
  });
287
427
 
288
- addFrame({
289
- frameName: "january-2024",
290
- note: "Full month backtest for January 2024",
291
- interval: "1m",
292
- startDate: new Date("2024-01-01"),
293
- endDate: new Date("2024-02-01"),
428
+ // Listen to walker completion
429
+ listenWalkerComplete((results) => {
430
+ console.log("Walker completed:", results.bestStrategy);
431
+ Walker.dump("BTCUSDT", results.walkerName); // Save report
294
432
  });
295
433
 
296
- // List all registered schemas
297
- const exchanges = await listExchanges();
298
- console.log("Available exchanges:", exchanges.map(e => ({
299
- name: e.exchangeName,
300
- note: e.note
301
- })));
302
- // Output: [{ name: "binance", note: "Binance cryptocurrency exchange..." }]
303
-
304
- const strategies = await listStrategies();
305
- console.log("Available strategies:", strategies.map(s => ({
306
- name: s.strategyName,
307
- note: s.note,
308
- interval: s.interval
309
- })));
310
- // Output: [{ name: "sma-crossover", note: "Simple moving average...", interval: "5m" }]
311
-
312
- const frames = await listFrames();
313
- console.log("Available frames:", frames.map(f => ({
314
- name: f.frameName,
315
- note: f.note,
316
- period: `${f.startDate.toISOString()} - ${f.endDate.toISOString()}`
317
- })));
318
- // Output: [{ name: "january-2024", note: "Full month backtest...", period: "2024-01-01..." }]
434
+ // Get raw comparison data
435
+ const results = await Walker.getData("BTCUSDT", "btc-walker");
436
+ console.log(results);
437
+ // Returns:
438
+ // {
439
+ // bestStrategy: "strategy-b",
440
+ // bestMetric: 1.85,
441
+ // strategies: [
442
+ // { strategyName: "strategy-a", stats: { sharpeRatio: 1.23, ... }, metric: 1.23 },
443
+ // { strategyName: "strategy-b", stats: { sharpeRatio: 1.85, ... }, metric: 1.85 },
444
+ // { strategyName: "strategy-c", stats: { sharpeRatio: 0.98, ... }, metric: 0.98 }
445
+ // ]
446
+ // }
447
+
448
+ // Generate markdown report
449
+ const markdown = await Walker.getReport("BTCUSDT", "btc-walker");
450
+ console.log(markdown);
319
451
  ```
320
452
 
321
- **Use cases:**
322
- - Generate documentation automatically from registered schemas
323
- - Build admin dashboards showing available strategies and exchanges
324
- - Create CLI tools with auto-completion based on registered schemas
325
- - Validate configuration files against registered schemas
453
+ **Available metrics for comparison:**
454
+ - `sharpeRatio` - Risk-adjusted return (default)
455
+ - `winRate` - Win percentage
456
+ - `avgPnl` - Average PNL percentage
457
+ - `totalPnl` - Total PNL percentage
458
+ - `certaintyRatio` - avgWin / |avgLoss|
326
459
 
327
- ## Architecture Overview
460
+ ### 6. Portfolio Heatmap
328
461
 
329
- The framework follows **clean architecture** with:
462
+ Heat provides portfolio-wide performance analysis across multiple symbols:
330
463
 
331
- - **Client Layer** - Pure business logic without DI (ClientStrategy, ClientExchange, ClientFrame)
332
- - **Service Layer** - DI-based services organized by responsibility
333
- - **Schema Services** - Registry pattern for configuration
334
- - **Connection Services** - Memoized client instance creators
335
- - **Global Services** - Context wrappers for public API
336
- - **Logic Services** - Async generator orchestration (backtest/live)
337
- - **Persistence Layer** - Crash-safe atomic file writes with `PersistSignalAdaper`
464
+ ```typescript
465
+ import { Heat, Backtest } from "backtest-kit";
466
+
467
+ // Run backtests for multiple symbols
468
+ for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"]) {
469
+ for await (const _ of Backtest.run(symbol, {
470
+ strategyName: "my-strategy",
471
+ exchangeName: "binance",
472
+ frameName: "2024-backtest"
473
+ })) {}
474
+ }
338
475
 
339
- See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed documentation.
476
+ // Get raw heatmap data
477
+ const stats = await Heat.getData("my-strategy");
478
+ console.log(stats);
479
+ // Returns:
480
+ // {
481
+ // symbols: [
482
+ // {
483
+ // symbol: "BTCUSDT",
484
+ // totalPnl: 15.5, // Total profit/loss %
485
+ // sharpeRatio: 2.10, // Risk-adjusted return
486
+ // profitFactor: 2.50, // Wins / Losses ratio
487
+ // expectancy: 1.85, // Expected value per trade
488
+ // winRate: 72.3, // Win percentage
489
+ // avgWin: 2.45, // Average win %
490
+ // avgLoss: -0.95, // Average loss %
491
+ // maxDrawdown: -2.5, // Maximum drawdown %
492
+ // maxWinStreak: 5, // Consecutive wins
493
+ // maxLossStreak: 2, // Consecutive losses
494
+ // totalTrades: 45,
495
+ // winCount: 32,
496
+ // lossCount: 13,
497
+ // avgPnl: 0.34,
498
+ // stdDev: 1.62
499
+ // },
500
+ // // ... more symbols sorted by Sharpe Ratio
501
+ // ],
502
+ // totalSymbols: 4,
503
+ // portfolioTotalPnl: 45.3, // Portfolio-wide total PNL
504
+ // portfolioSharpeRatio: 1.85, // Portfolio-wide Sharpe
505
+ // portfolioTotalTrades: 120
506
+ // }
340
507
 
341
- ## Signal Validation
508
+ // Generate markdown report
509
+ const markdown = await Heat.getReport("my-strategy");
510
+ console.log(markdown);
342
511
 
343
- All signals are validated automatically before execution:
512
+ // Save to disk (default: ./logs/heatmap/my-strategy.md)
513
+ await Heat.dump("my-strategy");
514
+ ```
344
515
 
345
- ```typescript
346
- // βœ… Valid long signal
347
- {
348
- position: "long",
349
- priceOpen: 50000,
350
- priceTakeProfit: 51000, // βœ… 51000 > 50000
351
- priceStopLoss: 49000, // βœ… 49000 < 50000
352
- minuteEstimatedTime: 60, // βœ… positive
353
- }
516
+ **Heatmap Report Example:**
517
+ ```markdown
518
+ # Portfolio Heatmap: my-strategy
354
519
 
355
- // ❌ Invalid long signal - throws error
356
- {
357
- position: "long",
358
- priceOpen: 50000,
359
- priceTakeProfit: 49000, // ❌ 49000 < 50000 (must be higher for long)
360
- priceStopLoss: 51000, // ❌ 51000 > 50000 (must be lower for long)
361
- }
520
+ **Total Symbols:** 4 | **Portfolio PNL:** +45.30% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
362
521
 
363
- // βœ… Valid short signal
364
- {
365
- position: "short",
366
- priceOpen: 50000,
367
- priceTakeProfit: 49000, // βœ… 49000 < 50000 (profit goes down for short)
368
- priceStopLoss: 51000, // βœ… 51000 > 50000 (stop loss goes up for short)
369
- }
522
+ | Symbol | Total PNL | Sharpe | PF | Expect | WR | Avg Win | Avg Loss | Max DD | W Streak | L Streak | Trades |
523
+ |--------|-----------|--------|-------|--------|-----|---------|----------|--------|----------|----------|--------|
524
+ | BTCUSDT | +15.50% | 2.10 | 2.50 | +1.85% | 72.3% | +2.45% | -0.95% | -2.50% | 5 | 2 | 45 |
525
+ | ETHUSDT | +12.30% | 1.85 | 2.15 | +1.45% | 68.5% | +2.10% | -1.05% | -3.10% | 4 | 2 | 38 |
526
+ | SOLUSDT | +10.20% | 1.65 | 1.95 | +1.20% | 65.2% | +1.95% | -1.15% | -4.20% | 3 | 3 | 25 |
527
+ | BNBUSDT | +7.30% | 1.40 | 1.75 | +0.95% | 62.5% | +1.75% | -1.20% | -3.80% | 3 | 2 | 12 |
370
528
  ```
371
529
 
372
- Validation errors include detailed messages for debugging.
530
+ **Column Descriptions:**
531
+ - **Total PNL** - Total profit/loss percentage across all trades
532
+ - **Sharpe** - Risk-adjusted return (higher is better)
533
+ - **PF** - Profit Factor: sum of wins / sum of losses (>1.0 is profitable)
534
+ - **Expect** - Expectancy: expected value per trade
535
+ - **WR** - Win Rate: percentage of winning trades
536
+ - **Avg Win** - Average profit on winning trades
537
+ - **Avg Loss** - Average loss on losing trades
538
+ - **Max DD** - Maximum drawdown (largest peak-to-trough decline)
539
+ - **W Streak** - Maximum consecutive winning trades
540
+ - **L Streak** - Maximum consecutive losing trades
541
+ - **Trades** - Total number of trades for this symbol
373
542
 
374
- ## Custom Persistence Adapter
543
+ ### 7. Position Sizing Calculator
375
544
 
376
- By default, signals are persisted to disk using atomic file writes (`./logs/data/signal/`). You can override the persistence layer with a custom adapter (e.g., Redis, MongoDB):
545
+ Position Sizing Calculator helps determine optimal position sizes based on risk management rules:
377
546
 
378
547
  ```typescript
379
- import { PersistBase, PersistSignalAdaper, ISignalData, EntityId } from "backtest-kit";
380
- import Redis from "ioredis";
548
+ import { addSizing, PositionSize } from "backtest-kit";
549
+
550
+ // Fixed Percentage Risk - risk fixed % of account per trade
551
+ addSizing({
552
+ sizingName: "conservative",
553
+ note: "Conservative 2% risk per trade",
554
+ method: "fixed-percentage",
555
+ riskPercentage: 2, // Risk 2% of account per trade
556
+ maxPositionPercentage: 10, // Max 10% of account in single position (optional)
557
+ minPositionSize: 0.001, // Min 0.001 BTC position (optional)
558
+ maxPositionSize: 1.0, // Max 1.0 BTC position (optional)
559
+ });
381
560
 
382
- // Create custom Redis adapter
383
- class RedisPersist extends PersistBase {
384
- private redis = new Redis({
385
- host: "localhost",
386
- port: 6379,
387
- });
561
+ // Kelly Criterion - optimal bet sizing based on edge
562
+ addSizing({
563
+ sizingName: "kelly-quarter",
564
+ note: "Kelly Criterion with 25% multiplier for safety",
565
+ method: "kelly-criterion",
566
+ kellyMultiplier: 0.25, // Use 25% of full Kelly (recommended for safety)
567
+ maxPositionPercentage: 15, // Cap position at 15% of account (optional)
568
+ minPositionSize: 0.001, // Min 0.001 BTC position (optional)
569
+ maxPositionSize: 2.0, // Max 2.0 BTC position (optional)
570
+ });
388
571
 
389
- async waitForInit(initial: boolean): Promise<void> {
390
- // Initialize Redis connection if needed
391
- await this.redis.ping();
392
- }
572
+ // ATR-based - volatility-adjusted position sizing
573
+ addSizing({
574
+ sizingName: "atr-dynamic",
575
+ note: "ATR-based sizing with 2x multiplier",
576
+ method: "atr-based",
577
+ riskPercentage: 2, // Risk 2% of account
578
+ atrMultiplier: 2, // Use 2x ATR as stop distance
579
+ maxPositionPercentage: 12, // Max 12% of account (optional)
580
+ minPositionSize: 0.001, // Min 0.001 BTC position (optional)
581
+ maxPositionSize: 1.5, // Max 1.5 BTC position (optional)
582
+ });
393
583
 
394
- async readValue(entityId: EntityId): Promise<ISignalData> {
395
- const key = `${this.entityName}:${entityId}`;
396
- const data = await this.redis.get(key);
584
+ // Calculate position sizes
585
+ const quantity1 = await PositionSize.fixedPercentage(
586
+ "BTCUSDT",
587
+ 10000, // Account balance: $10,000
588
+ 50000, // Entry price: $50,000
589
+ 49000, // Stop loss: $49,000
590
+ { sizingName: "conservative" }
591
+ );
592
+ console.log(`Position size: ${quantity1} BTC`);
593
+
594
+ const quantity2 = await PositionSize.kellyCriterion(
595
+ "BTCUSDT",
596
+ 10000, // Account balance: $10,000
597
+ 50000, // Entry price: $50,000
598
+ 0.55, // Win rate: 55%
599
+ 1.5, // Win/loss ratio: 1.5
600
+ { sizingName: "kelly-quarter" }
601
+ );
602
+ console.log(`Position size: ${quantity2} BTC`);
603
+
604
+ const quantity3 = await PositionSize.atrBased(
605
+ "BTCUSDT",
606
+ 10000, // Account balance: $10,000
607
+ 50000, // Entry price: $50,000
608
+ 500, // ATR: $500
609
+ { sizingName: "atr-dynamic" }
610
+ );
611
+ console.log(`Position size: ${quantity3} BTC`);
612
+ ```
397
613
 
398
- if (!data) {
399
- throw new Error(`Entity ${this.entityName}:${entityId} not found`);
400
- }
614
+ **When to Use Each Method:**
401
615
 
402
- return JSON.parse(data);
403
- }
616
+ 1. **Fixed Percentage** - Simple risk management, consistent risk per trade
617
+ - Best for: Beginners, conservative strategies
618
+ - Risk: Fixed 1-2% per trade
404
619
 
405
- async hasValue(entityId: EntityId): Promise<boolean> {
406
- const key = `${this.entityName}:${entityId}`;
407
- const exists = await this.redis.exists(key);
408
- return exists === 1;
409
- }
620
+ 2. **Kelly Criterion** - Optimal bet sizing based on win rate and win/loss ratio
621
+ - Best for: Strategies with known edge, statistical advantage
622
+ - Risk: Use fractional Kelly (0.25-0.5) to reduce volatility
410
623
 
411
- async writeValue(entityId: EntityId, entity: ISignalData): Promise<void> {
412
- const key = `${this.entityName}:${entityId}`;
413
- await this.redis.set(key, JSON.stringify(entity));
414
- }
415
- }
624
+ 3. **ATR-based** - Volatility-adjusted sizing, accounts for market conditions
625
+ - Best for: Swing trading, volatile markets
626
+ - Risk: Position size scales with volatility
416
627
 
417
- // Register custom adapter
418
- PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
628
+ ### 8. Risk Management
419
629
 
420
- // Now all signal persistence uses Redis
421
- Live.background("BTCUSDT", {
422
- strategyName: "my-strategy",
423
- exchangeName: "binance"
630
+ Risk Management provides portfolio-level risk controls across strategies:
631
+
632
+ ```typescript
633
+ import { addRisk } from "backtest-kit";
634
+
635
+ // Simple concurrent position limit
636
+ addRisk({
637
+ riskName: "conservative",
638
+ note: "Conservative risk profile with max 3 concurrent positions",
639
+ validations: [
640
+ ({ activePositionCount }) => {
641
+ if (activePositionCount >= 3) {
642
+ throw new Error("Maximum 3 concurrent positions allowed");
643
+ }
644
+ },
645
+ ],
646
+ callbacks: {
647
+ onRejected: (symbol, params) => {
648
+ console.warn(`Signal rejected for ${symbol}:`, params);
649
+ },
650
+ onAllowed: (symbol, params) => {
651
+ console.log(`Signal allowed for ${symbol}`);
652
+ },
653
+ },
424
654
  });
425
- ```
426
655
 
427
- **Key methods to implement:**
428
- - `waitForInit(initial)` - Initialize storage connection
429
- - `readValue(entityId)` - Read entity from storage
430
- - `hasValue(entityId)` - Check if entity exists
431
- - `writeValue(entityId, entity)` - Write entity to storage
656
+ // Symbol-based filtering
657
+ addRisk({
658
+ riskName: "no-meme-coins",
659
+ note: "Block meme coins from trading",
660
+ validations: [
661
+ ({ symbol }) => {
662
+ const memeCoins = ["DOGEUSDT", "SHIBUSDT", "PEPEUSDT"];
663
+ if (memeCoins.includes(symbol)) {
664
+ throw new Error(`Meme coin ${symbol} not allowed`);
665
+ }
666
+ },
667
+ ],
668
+ });
432
669
 
433
- The adapter is registered globally and applies to all strategies.
670
+ // Time-based trading windows
671
+ addRisk({
672
+ riskName: "trading-hours",
673
+ note: "Only trade during market hours (9 AM - 5 PM UTC)",
674
+ validations: [
675
+ ({ timestamp }) => {
676
+ const date = new Date(timestamp);
677
+ const hour = date.getUTCHours();
678
+
679
+ if (hour < 9 || hour >= 17) {
680
+ throw new Error("Trading only allowed 9 AM - 5 PM UTC");
681
+ }
682
+ },
683
+ ],
684
+ });
434
685
 
435
- ## Interval Throttling
686
+ // Multi-strategy coordination with position inspection
687
+ addRisk({
688
+ riskName: "strategy-coordinator",
689
+ note: "Limit exposure per strategy and inspect active positions",
690
+ validations: [
691
+ ({ activePositions, strategyName, symbol }) => {
692
+ // Count positions for this specific strategy
693
+ const strategyPositions = activePositions.filter(
694
+ (pos) => pos.strategyName === strategyName
695
+ );
696
+
697
+ if (strategyPositions.length >= 2) {
698
+ throw new Error(`Strategy ${strategyName} already has 2 positions`);
699
+ }
436
700
 
437
- Prevent signal spam with automatic throttling:
701
+ // Check if we already have a position on this symbol
702
+ const symbolPositions = activePositions.filter(
703
+ (pos) => pos.symbol === symbol
704
+ );
438
705
 
439
- ```typescript
706
+ if (symbolPositions.length > 0) {
707
+ throw new Error(`Already have position on ${symbol}`);
708
+ }
709
+ },
710
+ ],
711
+ });
712
+
713
+ // Use risk profile in strategy
440
714
  addStrategy({
441
715
  strategyName: "my-strategy",
442
- interval: "5m", // Signals generated max once per 5 minutes
716
+ interval: "5m",
717
+ riskName: "conservative", // Apply risk profile
443
718
  getSignal: async (symbol) => {
444
- // This function will be called max once per 5 minutes
445
- // Even if tick() is called every second
446
- return signal;
719
+ // Signal generation logic
720
+ return { /* ... */ };
447
721
  },
448
722
  });
449
723
  ```
450
724
 
451
- Supported intervals: `"1m"`, `"3m"`, `"5m"`, `"15m"`, `"30m"`, `"1h"`
725
+ ### 9. Custom Persistence Adapters (Optional)
452
726
 
453
- ## Markdown Reports
727
+ 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.
454
728
 
455
- Generate detailed trading reports with statistics:
729
+ #### Understanding the Persistence System
456
730
 
457
- ### Backtest Reports
731
+ The library uses three persistence layers:
458
732
 
459
- ```typescript
460
- import { Backtest } from "backtest-kit";
733
+ 1. **PersistBase** - Base class for all persistence operations (file-based by default)
734
+ 2. **PersistSignalAdapter** - Manages signal state persistence (used by Live mode)
735
+ 3. **PersistRiskAdapter** - Manages active positions for risk management
461
736
 
462
- // Run backtest
463
- const stopBacktest = Backtest.background("BTCUSDT", {
464
- strategyName: "my-strategy",
465
- exchangeName: "binance",
466
- frameName: "1d-backtest"
467
- });
737
+ #### Default File-Based Persistence
468
738
 
469
- // Get raw statistical data (Controller)
470
- const stats = await Backtest.getData("my-strategy");
471
- console.log(stats);
472
- // Returns:
473
- // {
474
- // signalList: [...], // All closed signals
475
- // totalSignals: 10,
476
- // winCount: 7,
477
- // lossCount: 3,
478
- // winRate: 70.0, // Percentage (higher is better)
479
- // avgPnl: 1.23, // Average PNL % (higher is better)
480
- // totalPnl: 12.30, // Total PNL % (higher is better)
481
- // stdDev: 2.45, // Standard deviation (lower is better)
482
- // sharpeRatio: 0.50, // Risk-adjusted return (higher is better)
483
- // annualizedSharpeRatio: 9.55, // Sharpe Γ— √365 (higher is better)
484
- // certaintyRatio: 1.75, // avgWin / |avgLoss| (higher is better)
485
- // expectedYearlyReturns: 156 // Estimated yearly trades (higher is better)
486
- // }
739
+ By default, data is stored in JSON files:
487
740
 
488
- // Generate markdown report (View)
489
- const markdown = await Backtest.getReport("my-strategy");
490
- console.log(markdown);
491
-
492
- // Save to disk (default: ./logs/backtest/my-strategy.md)
493
- await Backtest.dump("my-strategy");
494
-
495
- // Save to custom path
496
- await Backtest.dump("my-strategy", "./custom/path");
741
+ ```
742
+ ./logs/data/
743
+ signal/
744
+ my-strategy/
745
+ BTCUSDT.json # Signal state for BTCUSDT
746
+ ETHUSDT.json # Signal state for ETHUSDT
747
+ risk/
748
+ conservative/
749
+ positions.json # Active positions for risk profile
497
750
  ```
498
751
 
499
- **getData() returns BacktestStatistics:**
500
- - `signalList` - Array of all closed signals
501
- - `totalSignals` - Total number of closed signals
502
- - `winCount` / `lossCount` - Number of winning/losing trades
503
- - `winRate` - Win percentage (higher is better)
504
- - `avgPnl` - Average PNL percentage (higher is better)
505
- - `totalPnl` - Total PNL percentage (higher is better)
506
- - `stdDev` - Standard deviation / volatility (lower is better)
507
- - `sharpeRatio` - Risk-adjusted return (higher is better)
508
- - `annualizedSharpeRatio` - Sharpe Ratio Γ— √365 (higher is better)
509
- - `certaintyRatio` - avgWin / |avgLoss| (higher is better)
510
- - `expectedYearlyReturns` - Estimated number of trades per year (higher is better)
511
-
512
- **getReport() includes:**
513
- - All metrics from getData() formatted as markdown
514
- - All signal details (prices, TP/SL, PNL, duration, close reason)
515
- - Timestamps for each signal
516
- - "Higher is better" / "Lower is better" annotations
517
-
518
- ### Live Trading Reports
752
+ #### Create Custom Adapter (Redis Example)
519
753
 
520
754
  ```typescript
521
- import { Live } from "backtest-kit";
755
+ import { PersistBase, PersistSignalAdaper, PersistRiskAdapter } from "backtest-kit";
756
+ import Redis from "ioredis";
522
757
 
523
- // Get raw statistical data (Controller)
524
- const stats = await Live.getData("my-strategy");
525
- console.log(stats);
526
- // Returns:
527
- // {
528
- // eventList: [...], // All events (idle, opened, active, closed)
529
- // totalEvents: 15,
530
- // totalClosed: 5,
531
- // winCount: 3,
532
- // lossCount: 2,
533
- // winRate: 60.0, // Percentage (higher is better)
534
- // avgPnl: 1.23, // Average PNL % (higher is better)
535
- // totalPnl: 6.15, // Total PNL % (higher is better)
536
- // stdDev: 1.85, // Standard deviation (lower is better)
537
- // sharpeRatio: 0.66, // Risk-adjusted return (higher is better)
538
- // annualizedSharpeRatio: 12.61,// Sharpe Γ— √365 (higher is better)
539
- // certaintyRatio: 2.10, // avgWin / |avgLoss| (higher is better)
540
- // expectedYearlyReturns: 365 // Estimated yearly trades (higher is better)
541
- // }
758
+ const redis = new Redis();
542
759
 
543
- // Generate markdown report (View)
544
- const markdown = await Live.getReport("my-strategy");
760
+ // Custom Redis-based persistence adapter
761
+ class RedisPersist extends PersistBase {
762
+ // Initialize Redis connection
763
+ async waitForInit(initial: boolean): Promise<void> {
764
+ // Redis connection is already established
765
+ console.log(`Redis persistence initialized for ${this.entityName}`);
766
+ }
545
767
 
546
- // Save to disk (default: ./logs/live/my-strategy.md)
547
- await Live.dump("my-strategy");
548
- ```
768
+ // Read entity from Redis
769
+ async readValue<T>(entityId: string | number): Promise<T> {
770
+ const key = `${this.entityName}:${entityId}`;
771
+ const data = await redis.get(key);
549
772
 
550
- **getData() returns LiveStatistics:**
551
- - `eventList` - Array of all events (idle, opened, active, closed)
552
- - `totalEvents` - Total number of events
553
- - `totalClosed` - Total number of closed signals
554
- - `winCount` / `lossCount` - Number of winning/losing trades
555
- - `winRate` - Win percentage (higher is better)
556
- - `avgPnl` - Average PNL percentage (higher is better)
557
- - `totalPnl` - Total PNL percentage (higher is better)
558
- - `stdDev` - Standard deviation / volatility (lower is better)
559
- - `sharpeRatio` - Risk-adjusted return (higher is better)
560
- - `annualizedSharpeRatio` - Sharpe Ratio Γ— √365 (higher is better)
561
- - `certaintyRatio` - avgWin / |avgLoss| (higher is better)
562
- - `expectedYearlyReturns` - Estimated number of trades per year (higher is better)
563
-
564
- **getReport() includes:**
565
- - All metrics from getData() formatted as markdown
566
- - Signal-by-signal details with current state
567
- - "Higher is better" / "Lower is better" annotations
568
-
569
- **Report example:**
570
- ```markdown
571
- # Live Trading Report: my-strategy
572
-
573
- Total events: 15
574
- Closed signals: 5
575
- Win rate: 60.00% (3W / 2L) (higher is better)
576
- Average PNL: +1.23% (higher is better)
577
- Total PNL: +6.15% (higher is better)
578
- Standard Deviation: 1.85% (lower is better)
579
- Sharpe Ratio: 0.66 (higher is better)
580
- Annualized Sharpe Ratio: 12.61 (higher is better)
581
- Certainty Ratio: 2.10 (higher is better)
582
- Expected Yearly Returns: 365 trades (higher is better)
583
-
584
- | Timestamp | Action | Symbol | Signal ID | Position | ... | PNL (net) | Close Reason |
585
- |-----------|--------|--------|-----------|----------|-----|-----------|--------------|
586
- | ... | CLOSED | BTCUSD | abc-123 | LONG | ... | +2.45% | take_profit |
587
- ```
773
+ if (!data) {
774
+ throw new Error(`Entity ${this.entityName}:${entityId} not found`);
775
+ }
588
776
 
589
- ## Event Listeners
777
+ return JSON.parse(data) as T;
778
+ }
590
779
 
591
- Subscribe to signal events with filtering support. Useful for running strategies in background while reacting to specific events.
780
+ // Check if entity exists in Redis
781
+ async hasValue(entityId: string | number): Promise<boolean> {
782
+ const key = `${this.entityName}:${entityId}`;
783
+ const exists = await redis.exists(key);
784
+ return exists === 1;
785
+ }
592
786
 
593
- ### Background Execution with Event Listeners
787
+ // Write entity to Redis
788
+ async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
789
+ const key = `${this.entityName}:${entityId}`;
790
+ const serializedData = JSON.stringify(entity);
791
+ await redis.set(key, serializedData);
594
792
 
595
- ```typescript
596
- import { Backtest, listenSignalBacktest } from "backtest-kit";
793
+ // Optional: Set TTL (time to live)
794
+ // await redis.expire(key, 86400); // 24 hours
795
+ }
597
796
 
598
- // Run backtest in background (doesn't yield results)
599
- Backtest.background("BTCUSDT", {
600
- strategyName: "my-strategy",
601
- exchangeName: "binance",
602
- frameName: "1d-backtest"
603
- });
797
+ // Remove entity from Redis
798
+ async removeValue(entityId: string | number): Promise<void> {
799
+ const key = `${this.entityName}:${entityId}`;
800
+ const result = await redis.del(key);
604
801
 
605
- // Listen to all backtest events
606
- const unsubscribe = listenSignalBacktest((event) => {
607
- if (event.action === "closed") {
608
- console.log("Signal closed:", {
609
- pnl: event.pnl.pnlPercentage,
610
- reason: event.closeReason
611
- });
802
+ if (result === 0) {
803
+ throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
804
+ }
612
805
  }
613
- });
614
806
 
615
- // Stop listening when done
616
- // unsubscribe();
617
- ```
807
+ // Remove all entities for this entity type
808
+ async removeAll(): Promise<void> {
809
+ const pattern = `${this.entityName}:*`;
810
+ const keys = await redis.keys(pattern);
618
811
 
619
- ### Listen Once with Filter
812
+ if (keys.length > 0) {
813
+ await redis.del(...keys);
814
+ }
815
+ }
620
816
 
621
- ```typescript
622
- import { Backtest, listenSignalBacktestOnce } from "backtest-kit";
817
+ // Iterate over all entity values
818
+ async *values<T>(): AsyncGenerator<T> {
819
+ const pattern = `${this.entityName}:*`;
820
+ const keys = await redis.keys(pattern);
623
821
 
624
- // Run backtest in background
625
- Backtest.background("BTCUSDT", {
626
- strategyName: "my-strategy",
627
- exchangeName: "binance",
628
- frameName: "1d-backtest"
629
- });
822
+ // Sort keys alphanumerically
823
+ keys.sort((a, b) => a.localeCompare(b, undefined, {
824
+ numeric: true,
825
+ sensitivity: "base"
826
+ }));
630
827
 
631
- // Wait for first take profit event
632
- listenSignalBacktestOnce(
633
- (event) => event.action === "closed" && event.closeReason === "take_profit",
634
- (event) => {
635
- console.log("First take profit hit!", event.pnl.pnlPercentage);
636
- // Automatically unsubscribes after first match
828
+ for (const key of keys) {
829
+ const data = await redis.get(key);
830
+ if (data) {
831
+ yield JSON.parse(data) as T;
832
+ }
833
+ }
637
834
  }
638
- );
639
- ```
640
835
 
641
- ### Live Trading with Event Listeners
836
+ // Iterate over all entity IDs
837
+ async *keys(): AsyncGenerator<string> {
838
+ const pattern = `${this.entityName}:*`;
839
+ const keys = await redis.keys(pattern);
642
840
 
643
- ```typescript
644
- import { Live, listenSignalLive, listenSignalLiveOnce } from "backtest-kit";
645
-
646
- // Run live trading in background (infinite loop)
647
- const cancel = Live.background("BTCUSDT", {
648
- strategyName: "my-strategy",
649
- exchangeName: "binance"
650
- });
841
+ // Sort keys alphanumerically
842
+ keys.sort((a, b) => a.localeCompare(b, undefined, {
843
+ numeric: true,
844
+ sensitivity: "base"
845
+ }));
651
846
 
652
- // Listen to all live events
653
- listenSignalLive((event) => {
654
- if (event.action === "opened") {
655
- console.log("Signal opened:", event.signal.id);
656
- }
657
- if (event.action === "closed") {
658
- console.log("Signal closed:", event.pnl.pnlPercentage);
847
+ for (const key of keys) {
848
+ // Extract entity ID from key (remove prefix)
849
+ const entityId = key.slice(this.entityName.length + 1);
850
+ yield entityId;
851
+ }
659
852
  }
660
- });
853
+ }
661
854
 
662
- // React to first stop loss once
663
- listenSignalLiveOnce(
664
- (event) => event.action === "closed" && event.closeReason === "stop_loss",
665
- (event) => {
666
- console.error("Stop loss hit!", event.pnl.pnlPercentage);
667
- // Send alert, dump report, etc.
668
- }
669
- );
855
+ // Register Redis adapter for signal persistence
856
+ PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
670
857
 
671
- // Stop live trading after some condition
672
- // cancel();
858
+ // Register Redis adapter for risk persistence
859
+ PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
673
860
  ```
674
861
 
675
- ### Listen to All Signals (Backtest + Live)
862
+ #### Custom Adapter Registration (Before Running Strategies)
676
863
 
677
864
  ```typescript
678
- import { listenSignal, listenSignalOnce, Backtest, Live } from "backtest-kit";
679
-
680
- // Listen to both backtest and live events
681
- listenSignal((event) => {
682
- console.log("Event:", event.action, event.strategyName);
683
- });
684
-
685
- // Wait for first loss from any source
686
- listenSignalOnce(
687
- (event) => event.action === "closed" && event.pnl.pnlPercentage < 0,
688
- (event) => {
689
- console.log("First loss detected:", event.pnl.pnlPercentage);
690
- }
691
- );
865
+ import { PersistSignalAdaper, PersistRiskAdapter, Live } from "backtest-kit";
692
866
 
693
- // Run both modes
694
- Backtest.background("BTCUSDT", {
695
- strategyName: "my-strategy",
696
- exchangeName: "binance",
697
- frameName: "1d-backtest"
698
- });
867
+ // IMPORTANT: Register adapters BEFORE running any strategies
868
+ PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
869
+ PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
699
870
 
871
+ // Now run live trading with Redis persistence
700
872
  Live.background("BTCUSDT", {
701
873
  strategyName: "my-strategy",
702
874
  exchangeName: "binance"
703
875
  });
704
876
  ```
705
877
 
706
- **Available event listeners:**
878
+ #### MongoDB Adapter Example
707
879
 
708
- - `listenSignal(callback)` - Subscribe to all signal events (backtest + live)
709
- - `listenSignalOnce(filter, callback)` - Subscribe once with filter predicate
710
- - `listenSignalBacktest(callback)` - Subscribe to backtest signals only
711
- - `listenSignalBacktestOnce(filter, callback)` - Subscribe to backtest signals once
712
- - `listenSignalLive(callback)` - Subscribe to live signals only
713
- - `listenSignalLiveOnce(filter, callback)` - Subscribe to live signals once
714
- - `listenPerformance(callback)` - Subscribe to performance metrics (backtest + live)
715
- - `listenProgress(callback)` - Subscribe to backtest progress events
716
- - `listenError(callback)` - Subscribe to background execution errors
717
- - `listenDone(callback)` - Subscribe to background completion events
718
- - `listenDoneOnce(filter, callback)` - Subscribe to background completion once
880
+ ```typescript
881
+ import { PersistBase } from "backtest-kit";
882
+ import { MongoClient, Collection } from "mongodb";
719
883
 
720
- All listeners return an `unsubscribe` function. All callbacks are processed sequentially using queued async execution.
884
+ const client = new MongoClient("mongodb://localhost:27017");
885
+ const db = client.db("backtest-kit");
721
886
 
722
- ### Listen to Background Completion
887
+ class MongoPersist extends PersistBase {
888
+ private collection: Collection;
723
889
 
724
- ```typescript
725
- import { listenDone, listenDoneOnce, Backtest, Live } from "backtest-kit";
726
-
727
- // Listen to all completion events
728
- listenDone((event) => {
729
- console.log("Execution completed:", {
730
- mode: event.backtest ? "backtest" : "live",
731
- symbol: event.symbol,
732
- strategy: event.strategyName,
733
- exchange: event.exchangeName,
734
- });
735
-
736
- // Auto-generate report on completion
737
- if (event.backtest) {
738
- Backtest.dump(event.strategyName);
739
- } else {
740
- Live.dump(event.strategyName);
890
+ constructor(entityName: string, baseDir: string) {
891
+ super(entityName, baseDir);
892
+ this.collection = db.collection(this.entityName);
741
893
  }
742
- });
743
894
 
744
- // Wait for specific backtest to complete
745
- listenDoneOnce(
746
- (event) => event.backtest && event.symbol === "BTCUSDT",
747
- (event) => {
748
- console.log("BTCUSDT backtest finished");
749
- // Start next backtest or live trading
750
- Live.background(event.symbol, {
751
- strategyName: event.strategyName,
752
- exchangeName: event.exchangeName,
753
- });
895
+ async waitForInit(initial: boolean): Promise<void> {
896
+ await client.connect();
897
+ // Create index for faster lookups
898
+ await this.collection.createIndex({ entityId: 1 }, { unique: true });
899
+ console.log(`MongoDB persistence initialized for ${this.entityName}`);
754
900
  }
755
- );
756
-
757
- // Run backtests
758
- Backtest.background("BTCUSDT", {
759
- strategyName: "my-strategy",
760
- exchangeName: "binance",
761
- frameName: "1d-backtest"
762
- });
763
- ```
764
-
765
- ## API Reference
766
-
767
- ### High-Level Functions
768
-
769
- #### Schema Registration
770
-
771
- ```typescript
772
- // Register exchange
773
- addExchange(exchangeSchema: IExchangeSchema): void
774
901
 
775
- // Register strategy
776
- addStrategy(strategySchema: IStrategySchema): void
902
+ async readValue<T>(entityId: string | number): Promise<T> {
903
+ const doc = await this.collection.findOne({ entityId });
777
904
 
778
- // Register timeframe generator
779
- addFrame(frameSchema: IFrameSchema): void
780
- ```
781
-
782
- #### Exchange Data
783
-
784
- ```typescript
785
- // Get historical candles
786
- const candles = await getCandles("BTCUSDT", "1h", 5);
787
- // Returns: [
788
- // { timestamp: 1704067200000, open: 42150.5, high: 42380.2, low: 42100.0, close: 42250.8, volume: 125.43 },
789
- // { timestamp: 1704070800000, open: 42250.8, high: 42500.0, low: 42200.0, close: 42450.3, volume: 98.76 },
790
- // { timestamp: 1704074400000, open: 42450.3, high: 42600.0, low: 42400.0, close: 42580.5, volume: 110.22 },
791
- // { timestamp: 1704078000000, open: 42580.5, high: 42700.0, low: 42550.0, close: 42650.0, volume: 95.18 },
792
- // { timestamp: 1704081600000, open: 42650.0, high: 42750.0, low: 42600.0, close: 42720.0, volume: 102.35 }
793
- // ]
794
-
795
- // Get VWAP from last 5 1m candles
796
- const vwap = await getAveragePrice("BTCUSDT");
797
- // Returns: 42685.34
798
-
799
- // Get current date in execution context
800
- const date = await getDate();
801
- // Returns: 2024-01-01T12:00:00.000Z (in backtest mode, returns frame's current timestamp)
802
- // Returns: 2024-01-15T10:30:45.123Z (in live mode, returns current wall clock time)
803
-
804
- // Get current mode
805
- const mode = await getMode();
806
- // Returns: "backtest" or "live"
807
-
808
- // Format price/quantity for exchange
809
- const price = await formatPrice("BTCUSDT", 42685.3456789);
810
- // Returns: "42685.35" (formatted to exchange precision)
811
-
812
- const quantity = await formatQuantity("BTCUSDT", 0.123456789);
813
- // Returns: "0.12345" (formatted to exchange precision)
814
- ```
815
-
816
- ### Service APIs
905
+ if (!doc) {
906
+ throw new Error(`Entity ${this.entityName}:${entityId} not found`);
907
+ }
817
908
 
818
- #### Backtest API
909
+ return doc.data as T;
910
+ }
819
911
 
820
- ```typescript
821
- import { Backtest, BacktestStatistics } from "backtest-kit";
822
-
823
- // Stream backtest results
824
- Backtest.run(
825
- symbol: string,
826
- context: {
827
- strategyName: string;
828
- exchangeName: string;
829
- frameName: string;
912
+ async hasValue(entityId: string | number): Promise<boolean> {
913
+ const count = await this.collection.countDocuments({ entityId });
914
+ return count > 0;
830
915
  }
831
- ): AsyncIterableIterator<IStrategyTickResultClosed>
832
916
 
833
- // Run in background without yielding results
834
- Backtest.background(
835
- symbol: string,
836
- context: { strategyName, exchangeName, frameName }
837
- ): Promise<() => void> // Returns cancellation function
917
+ async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
918
+ await this.collection.updateOne(
919
+ { entityId },
920
+ { $set: { entityId, data: entity, updatedAt: new Date() } },
921
+ { upsert: true }
922
+ );
923
+ }
838
924
 
839
- // Get raw statistical data (Controller)
840
- Backtest.getData(strategyName: string): Promise<BacktestStatistics>
925
+ async removeValue(entityId: string | number): Promise<void> {
926
+ const result = await this.collection.deleteOne({ entityId });
841
927
 
842
- // Generate markdown report (View)
843
- Backtest.getReport(strategyName: string): Promise<string>
928
+ if (result.deletedCount === 0) {
929
+ throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
930
+ }
931
+ }
844
932
 
845
- // Save report to disk
846
- Backtest.dump(strategyName: string, path?: string): Promise<void>
847
- ```
933
+ async removeAll(): Promise<void> {
934
+ await this.collection.deleteMany({});
935
+ }
848
936
 
849
- #### Live Trading API
937
+ async *values<T>(): AsyncGenerator<T> {
938
+ const cursor = this.collection.find({}).sort({ entityId: 1 });
850
939
 
851
- ```typescript
852
- import { Live, LiveStatistics } from "backtest-kit";
853
-
854
- // Stream live results (infinite)
855
- Live.run(
856
- symbol: string,
857
- context: {
858
- strategyName: string;
859
- exchangeName: string;
940
+ for await (const doc of cursor) {
941
+ yield doc.data as T;
942
+ }
860
943
  }
861
- ): AsyncIterableIterator<IStrategyTickResult>
862
944
 
863
- // Run in background without yielding results
864
- Live.background(
865
- symbol: string,
866
- context: { strategyName, exchangeName }
867
- ): Promise<() => void> // Returns cancellation function
945
+ async *keys(): AsyncGenerator<string> {
946
+ const cursor = this.collection.find({}, { projection: { entityId: 1 } }).sort({ entityId: 1 });
868
947
 
869
- // Get raw statistical data (Controller)
870
- Live.getData(strategyName: string): Promise<LiveStatistics>
871
-
872
- // Generate markdown report (View)
873
- Live.getReport(strategyName: string): Promise<string>
948
+ for await (const doc of cursor) {
949
+ yield String(doc.entityId);
950
+ }
951
+ }
952
+ }
874
953
 
875
- // Save report to disk
876
- Live.dump(strategyName: string, path?: string): Promise<void>
954
+ // Register MongoDB adapter
955
+ PersistSignalAdaper.usePersistSignalAdapter(MongoPersist);
956
+ PersistRiskAdapter.usePersistRiskAdapter(MongoPersist);
877
957
  ```
878
958
 
879
- #### Performance Profiling API
880
-
881
- ```typescript
882
- import { Performance, PerformanceStatistics, listenPerformance } from "backtest-kit";
959
+ #### Direct Persistence API Usage (Advanced)
883
960
 
884
- // Get raw performance statistics (Controller)
885
- Performance.getData(strategyName: string): Promise<PerformanceStatistics>
961
+ You can also use PersistBase directly for custom data storage:
886
962
 
887
- // Generate markdown report with bottleneck analysis (View)
888
- Performance.getReport(strategyName: string): Promise<string>
963
+ ```typescript
964
+ import { PersistBase } from "backtest-kit";
889
965
 
890
- // Save performance report to disk (default: ./logs/performance)
891
- Performance.dump(strategyName: string, path?: string): Promise<void>
966
+ // Create custom persistence for trading logs
967
+ const tradingLogs = new PersistBase("trading-logs", "./logs/custom");
892
968
 
893
- // Clear accumulated performance data
894
- Performance.clear(strategyName?: string): Promise<void>
969
+ // Initialize
970
+ await tradingLogs.waitForInit(true);
895
971
 
896
- // Listen to real-time performance events
897
- listenPerformance((event) => {
898
- console.log(`${event.metricType}: ${event.duration.toFixed(2)}ms`);
899
- console.log(`Strategy: ${event.strategyName} @ ${event.exchangeName}`);
900
- console.log(`Symbol: ${event.symbol}, Backtest: ${event.backtest}`);
972
+ // Write log entry
973
+ await tradingLogs.writeValue("log-1", {
974
+ timestamp: Date.now(),
975
+ message: "Strategy started",
976
+ metadata: { symbol: "BTCUSDT", strategy: "sma-crossover" }
901
977
  });
902
- ```
903
978
 
904
- ## Type Definitions
979
+ // Read log entry
980
+ const log = await tradingLogs.readValue("log-1");
981
+ console.log(log);
905
982
 
906
- ### Statistics Types
983
+ // Check if log exists
984
+ const exists = await tradingLogs.hasValue("log-1");
985
+ console.log(`Log exists: ${exists}`);
907
986
 
908
- ```typescript
909
- // Backtest statistics (exported from "backtest-kit")
910
- interface BacktestStatistics {
911
- signalList: IStrategyTickResultClosed[]; // All closed signals
912
- totalSignals: number;
913
- winCount: number;
914
- lossCount: number;
915
- winRate: number | null; // Win percentage (higher is better)
916
- avgPnl: number | null; // Average PNL % (higher is better)
917
- totalPnl: number | null; // Total PNL % (higher is better)
918
- stdDev: number | null; // Standard deviation (lower is better)
919
- sharpeRatio: number | null; // Risk-adjusted return (higher is better)
920
- annualizedSharpeRatio: number | null; // Sharpe Γ— √365 (higher is better)
921
- certaintyRatio: number | null; // avgWin / |avgLoss| (higher is better)
922
- expectedYearlyReturns: number | null; // Estimated yearly trades (higher is better)
987
+ // Iterate over all logs
988
+ for await (const log of tradingLogs.values()) {
989
+ console.log("Log:", log);
923
990
  }
924
991
 
925
- // Live statistics (exported from "backtest-kit")
926
- interface LiveStatistics {
927
- eventList: TickEvent[]; // All events (idle, opened, active, closed)
928
- totalEvents: number;
929
- totalClosed: number;
930
- winCount: number;
931
- lossCount: number;
932
- winRate: number | null; // Win percentage (higher is better)
933
- avgPnl: number | null; // Average PNL % (higher is better)
934
- totalPnl: number | null; // Total PNL % (higher is better)
935
- stdDev: number | null; // Standard deviation (lower is better)
936
- sharpeRatio: number | null; // Risk-adjusted return (higher is better)
937
- annualizedSharpeRatio: number | null; // Sharpe Γ— √365 (higher is better)
938
- certaintyRatio: number | null; // avgWin / |avgLoss| (higher is better)
939
- expectedYearlyReturns: number | null; // Estimated yearly trades (higher is better)
992
+ // Get all log IDs
993
+ for await (const logId of tradingLogs.keys()) {
994
+ console.log("Log ID:", logId);
940
995
  }
941
996
 
942
- // Performance statistics (exported from "backtest-kit")
943
- interface PerformanceStatistics {
944
- strategyName: string; // Strategy name
945
- totalEvents: number; // Total number of performance events
946
- totalDuration: number; // Total execution time (ms)
947
- metricStats: Record<string, { // Statistics by metric type
948
- metricType: PerformanceMetricType; // backtest_total | backtest_timeframe | backtest_signal | live_tick
949
- count: number; // Number of samples
950
- totalDuration: number; // Total duration (ms)
951
- avgDuration: number; // Average duration (ms)
952
- minDuration: number; // Minimum duration (ms)
953
- maxDuration: number; // Maximum duration (ms)
954
- stdDev: number; // Standard deviation (ms)
955
- median: number; // Median duration (ms)
956
- p95: number; // 95th percentile (ms)
957
- p99: number; // 99th percentile (ms)
958
- }>;
959
- events: PerformanceContract[]; // All raw performance events
997
+ // Filter logs
998
+ for await (const log of tradingLogs.filter((l: any) => l.metadata.symbol === "BTCUSDT")) {
999
+ console.log("BTC Log:", log);
960
1000
  }
961
1001
 
962
- // Performance event (exported from "backtest-kit")
963
- interface PerformanceContract {
964
- timestamp: number; // When metric was recorded (epoch ms)
965
- metricType: PerformanceMetricType; // Type of operation measured
966
- duration: number; // Operation duration (ms)
967
- strategyName: string; // Strategy name
968
- exchangeName: string; // Exchange name
969
- symbol: string; // Trading symbol
970
- backtest: boolean; // true = backtest, false = live
971
- }
972
-
973
- // Performance metric types (exported from "backtest-kit")
974
- type PerformanceMetricType =
975
- | "backtest_total" // Total backtest duration
976
- | "backtest_timeframe" // Single timeframe processing
977
- | "backtest_signal" // Signal processing (tick + getNextCandles + backtest)
978
- | "live_tick"; // Single live tick duration
979
- ```
980
-
981
- ### Signal Data
982
-
983
- ```typescript
984
- interface ISignalRow {
985
- id: string; // UUID v4 auto-generated
986
- position: "long" | "short";
987
- note?: string;
988
- priceOpen: number;
989
- priceTakeProfit: number;
990
- priceStopLoss: number;
991
- minuteEstimatedTime: number;
992
- exchangeName: string;
993
- strategyName: string;
994
- timestamp: number; // Signal creation timestamp
995
- symbol: string; // Trading pair (e.g., "BTCUSDT")
1002
+ // Take first 5 logs
1003
+ for await (const log of tradingLogs.take(5)) {
1004
+ console.log("Recent Log:", log);
996
1005
  }
997
- ```
998
1006
 
999
- ### Tick Results (Discriminated Union)
1007
+ // Remove specific log
1008
+ await tradingLogs.removeValue("log-1");
1000
1009
 
1001
- ```typescript
1002
- type IStrategyTickResult =
1003
- | {
1004
- action: "idle";
1005
- signal: null;
1006
- strategyName: string;
1007
- exchangeName: string;
1008
- currentPrice: number;
1009
- }
1010
- | {
1011
- action: "opened";
1012
- signal: ISignalRow;
1013
- strategyName: string;
1014
- exchangeName: string;
1015
- currentPrice: number;
1016
- }
1017
- | {
1018
- action: "active";
1019
- signal: ISignalRow;
1020
- currentPrice: number;
1021
- strategyName: string;
1022
- exchangeName: string;
1023
- }
1024
- | {
1025
- action: "closed";
1026
- signal: ISignalRow;
1027
- currentPrice: number;
1028
- closeReason: "take_profit" | "stop_loss" | "time_expired";
1029
- closeTimestamp: number;
1030
- pnl: {
1031
- pnlPercentage: number;
1032
- priceOpen: number; // Entry price adjusted with slippage and fees
1033
- priceClose: number; // Exit price adjusted with slippage and fees
1034
- };
1035
- strategyName: string;
1036
- exchangeName: string;
1037
- };
1010
+ // Remove all logs
1011
+ await tradingLogs.removeAll();
1038
1012
  ```
1039
1013
 
1040
- ### PNL Calculation
1014
+ #### When to Use Custom Adapters
1041
1015
 
1042
- ```typescript
1043
- // Constants
1044
- PERCENT_SLIPPAGE = 0.1% // 0.001
1045
- PERCENT_FEE = 0.1% // 0.001
1046
-
1047
- // LONG position
1048
- priceOpenWithCosts = priceOpen * (1 + slippage + fee)
1049
- priceCloseWithCosts = priceClose * (1 - slippage - fee)
1050
- pnl% = (priceCloseWithCosts - priceOpenWithCosts) / priceOpenWithCosts * 100
1051
-
1052
- // SHORT position
1053
- priceOpenWithCosts = priceOpen * (1 - slippage + fee)
1054
- priceCloseWithCosts = priceClose * (1 + slippage + fee)
1055
- pnl% = (priceOpenWithCosts - priceCloseWithCosts) / priceOpenWithCosts * 100
1056
- ```
1016
+ 1. **Redis** - Best for high-performance distributed systems with multiple instances
1017
+ - Fast read/write operations
1018
+ - Built-in TTL (automatic cleanup)
1019
+ - Pub/sub for real-time updates
1057
1020
 
1058
- ## Production Readiness
1021
+ 2. **MongoDB** - Best for complex queries and analytics
1022
+ - Rich query language
1023
+ - Aggregation pipelines
1024
+ - Scalable for large datasets
1059
1025
 
1060
- ### βœ… Production-Ready Features
1026
+ 3. **PostgreSQL** - Best for ACID transactions and relational data
1027
+ - Strong consistency guarantees
1028
+ - Complex joins and queries
1029
+ - Mature ecosystem
1061
1030
 
1062
- 1. **Crash-Safe Persistence** - Atomic file writes with automatic recovery
1063
- 2. **Signal Validation** - Comprehensive validation prevents invalid trades
1064
- 3. **Type Safety** - Discriminated unions eliminate runtime type errors
1065
- 4. **Memory Efficiency** - Prototype methods + async generators + memoization
1066
- 5. **Interval Throttling** - Prevents signal spam
1067
- 6. **Live Trading Ready** - Full implementation with real-time progression
1068
- 7. **Error Recovery** - Stateless process with disk-based state
1031
+ 4. **File-based (default)** - Best for single-instance deployments
1032
+ - No dependencies
1033
+ - Simple debugging (inspect JSON files)
1034
+ - Sufficient for most use cases
1069
1035
 
1070
- ## Advanced Examples
1071
-
1072
- ### Multi-Symbol Live Trading
1036
+ #### Testing Custom Adapters
1073
1037
 
1074
1038
  ```typescript
1075
- import { Live } from "backtest-kit";
1076
-
1077
- const symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT"];
1078
-
1079
- // Run all symbols in parallel
1080
- await Promise.all(
1081
- symbols.map(async (symbol) => {
1082
- for await (const result of Live.run(symbol, {
1083
- strategyName: "my-strategy",
1084
- exchangeName: "binance"
1085
- })) {
1086
- console.log(`[${symbol}]`, result.action);
1087
-
1088
- // Generate reports periodically
1089
- if (result.action === "closed") {
1090
- await Live.dump("my-strategy");
1091
- }
1092
- }
1093
- })
1094
- );
1095
- ```
1039
+ import { test } from "worker-testbed";
1040
+ import { PersistBase } from "backtest-kit";
1096
1041
 
1097
- ### Backtest Progress Listener
1042
+ test("Custom Redis adapter works correctly", async ({ pass, fail }) => {
1043
+ const persist = new RedisPersist("test-entity", "./logs/test");
1098
1044
 
1099
- ```typescript
1100
- import { listenProgress, Backtest } from "backtest-kit";
1101
-
1102
- listenProgress((event) => {
1103
- console.log(`Progress: ${(event.progress * 100).toFixed(2)}%`);
1104
- console.log(`${event.processedFrames} / ${event.totalFrames} frames`);
1105
- console.log(`Strategy: ${event.strategyName}, Symbol: ${event.symbol}`);
1106
- });
1107
-
1108
- Backtest.background("BTCUSDT", {
1109
- strategyName: "my-strategy",
1110
- exchangeName: "binance",
1111
- frameName: "1d-backtest"
1112
- });
1113
- ```
1114
-
1115
- ### Performance Profiling
1116
-
1117
- ```typescript
1118
- import { Performance, listenPerformance, Backtest } from "backtest-kit";
1119
-
1120
- // Listen to real-time performance metrics
1121
- listenPerformance((event) => {
1122
- console.log(`[${event.metricType}] ${event.duration.toFixed(2)}ms`);
1123
- console.log(` Strategy: ${event.strategyName}`);
1124
- console.log(` Symbol: ${event.symbol}, Backtest: ${event.backtest}`);
1125
- });
1126
-
1127
- // Run backtest
1128
- await Backtest.background("BTCUSDT", {
1129
- strategyName: "my-strategy",
1130
- exchangeName: "binance",
1131
- frameName: "1d-backtest"
1132
- });
1133
-
1134
- // Get aggregated performance statistics
1135
- const perfStats = await Performance.getData("my-strategy");
1136
- console.log("Performance Statistics:");
1137
- console.log(` Total events: ${perfStats.totalEvents}`);
1138
- console.log(` Total duration: ${perfStats.totalDuration.toFixed(2)}ms`);
1139
- console.log(` Metrics tracked: ${Object.keys(perfStats.metricStats).join(", ")}`);
1140
-
1141
- // Analyze bottlenecks
1142
- for (const [type, stats] of Object.entries(perfStats.metricStats)) {
1143
- console.log(`\n${type}:`);
1144
- console.log(` Count: ${stats.count}`);
1145
- console.log(` Average: ${stats.avgDuration.toFixed(2)}ms`);
1146
- console.log(` Min/Max: ${stats.minDuration.toFixed(2)}ms / ${stats.maxDuration.toFixed(2)}ms`);
1147
- console.log(` P95/P99: ${stats.p95.toFixed(2)}ms / ${stats.p99.toFixed(2)}ms`);
1148
- console.log(` Std Dev: ${stats.stdDev.toFixed(2)}ms`);
1149
- }
1150
-
1151
- // Generate and save performance report
1152
- const markdown = await Performance.getReport("my-strategy");
1153
- await Performance.dump("my-strategy"); // Saves to ./logs/performance/my-strategy.md
1154
- ```
1155
-
1156
- **Performance Report Example:**
1157
- ```markdown
1158
- # Performance Report: my-strategy
1159
-
1160
- **Total events:** 1440
1161
- **Total execution time:** 12345.67ms
1162
- **Number of metric types:** 3
1163
-
1164
- ## Time Distribution
1165
-
1166
- - **backtest_timeframe**: 65.4% (8074.32ms total)
1167
- - **backtest_signal**: 28.3% (3493.85ms total)
1168
- - **backtest_total**: 6.3% (777.50ms total)
1169
-
1170
- ## Detailed Metrics
1045
+ await persist.waitForInit(true);
1171
1046
 
1172
- | Metric Type | Count | Total (ms) | Avg (ms) | Min (ms) | Max (ms) | Std Dev (ms) | Median (ms) | P95 (ms) | P99 (ms) |
1173
- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
1174
- | backtest_timeframe | 1440 | 8074.32 | 5.61 | 2.10 | 12.45 | 1.85 | 5.20 | 8.90 | 10.50 |
1175
- | backtest_signal | 45 | 3493.85 | 77.64 | 45.20 | 125.80 | 18.32 | 75.10 | 110.20 | 120.15 |
1176
- | backtest_total | 1 | 777.50 | 777.50 | 777.50 | 777.50 | 0.00 | 777.50 | 777.50 | 777.50 |
1047
+ // Write
1048
+ await persist.writeValue("key1", { data: "value1" });
1177
1049
 
1178
- **Note:** All durations are in milliseconds. P95/P99 represent 95th and 99th percentile response times.
1179
- ```
1180
-
1181
- ### Early Termination
1182
-
1183
- **Using async generator with break:**
1184
-
1185
- ```typescript
1186
- import { Backtest } from "backtest-kit";
1187
-
1188
- for await (const result of Backtest.run("BTCUSDT", {
1189
- strategyName: "my-strategy",
1190
- exchangeName: "binance",
1191
- frameName: "1d-backtest"
1192
- })) {
1193
- if (result.closeReason === "stop_loss") {
1194
- console.log("Stop loss hit - terminating backtest");
1195
-
1196
- // Save final report before exit
1197
- await Backtest.dump("my-strategy");
1198
- break; // Generator stops immediately
1050
+ // Read
1051
+ const value = await persist.readValue("key1");
1052
+ if (value.data === "value1") {
1053
+ pass("Redis adapter read/write works");
1054
+ } else {
1055
+ fail("Redis adapter failed");
1199
1056
  }
1200
- }
1201
- ```
1202
1057
 
1203
- **Using background mode with stop() function:**
1204
-
1205
- ```typescript
1206
- import { Backtest, Live, listenSignalLiveOnce } from "backtest-kit";
1207
-
1208
- // Backtest.background returns a stop function
1209
- const stopBacktest = await Backtest.background("BTCUSDT", {
1210
- strategyName: "my-strategy",
1211
- exchangeName: "binance",
1212
- frameName: "1d-backtest"
1058
+ // Cleanup
1059
+ await persist.removeValue("key1");
1213
1060
  });
1214
-
1215
- // Stop backtest after some condition
1216
- setTimeout(() => {
1217
- console.log("Stopping backtest...");
1218
- stopBacktest(); // Stops the background execution
1219
- }, 5000);
1220
-
1221
- // Live.background also returns a stop function
1222
- const stopLive = Live.background("BTCUSDT", {
1223
- strategyName: "my-strategy",
1224
- exchangeName: "binance"
1225
- });
1226
-
1227
- // Stop live trading after detecting stop loss
1228
- listenSignalLiveOnce(
1229
- (event) => event.action === "closed" && event.closeReason === "stop_loss",
1230
- (event) => {
1231
- console.log("Stop loss detected - stopping live trading");
1232
- stopLive(); // Stops the infinite loop
1233
- }
1234
- );
1235
1061
  ```
1236
1062
 
1237
- ## Use Cases
1063
+ ---
1064
+
1065
+ ## βœ… Tested & Reliable
1238
1066
 
1239
- - **Algorithmic Trading** - Backtest and deploy strategies with crash recovery
1240
- - **Strategy Research** - Test hypotheses on historical data
1241
- - **Signal Generation** - Use with ML models or technical indicators
1242
- - **Portfolio Management** - Track multiple strategies across symbols
1243
- - **Educational Projects** - Learn trading system architecture
1067
+ `backtest-kit` comes with a robust test suite covering:
1068
+ - πŸ›‘οΈ **Validation**: Ensures all components (exchanges, strategies, frames, risk profiles) are properly configured. βœ…
1069
+ - πŸš‘ **Recovery**: Handles edge cases like invalid signals or empty outputs. πŸ› οΈ
1070
+ - πŸ”„ **Navigation**: Smoothly switches between backtest and live modes without errors. 🌐
1071
+ - ⚑ **Performance**: Efficient memory usage and history management. πŸ“ˆ
1244
1072
 
1245
- ## Contributing
1073
+ **109 unit and integration tests** covering:
1074
+ - Signal validation and throttling
1075
+ - PNL calculation with fees and slippage
1076
+ - Crash recovery and state persistence
1077
+ - Callback execution order
1078
+ - Markdown report generation
1079
+ - Walker strategy comparison
1080
+ - Heatmap portfolio analysis
1081
+ - Position sizing calculations
1082
+ - Risk management validation
1083
+ - Event system
1246
1084
 
1247
- Pull requests are welcome. For major changes, please open an issue first.
1085
+ ---
1248
1086
 
1249
- ## License
1087
+ ## 🀝 Contribute
1250
1088
 
1251
- MIT
1089
+ We'd love your input! Fork the repo, submit a PR, or open an issue on **[GitHub](https://github.com/tripolskypetr/backtest-kit)**. πŸ™Œ
1252
1090
 
1253
- ## Links
1091
+ ## πŸ“œ License
1254
1092
 
1255
- - [Architecture Documentation](./ARCHITECTURE.md)
1256
- - [TypeScript Documentation](https://www.typescriptlang.org/)
1257
- - [Dependency Injection](https://github.com/tripolskypetr/di-kit)
1093
+ MIT Β© [tripolskypetr](https://github.com/tripolskypetr) πŸ–‹οΈ