backtest-kit 1.5.17 β†’ 1.5.19

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 +107 -2477
  2. package/build/index.cjs +225 -61
  3. package/build/index.mjs +225 -61
  4. package/package.json +2 -3
  5. package/types.d.ts +9884 -9874
package/README.md CHANGED
@@ -2,2550 +2,180 @@
2
2
 
3
3
  # 🧿 Backtest Kit
4
4
 
5
- > **A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.**
5
+ > A TypeScript framework for backtesting and live trading strategies on crypto markets, with crash-safe persistence, signal validation, and AI optimization.
6
6
 
7
7
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)
8
8
  [![npm](https://img.shields.io/npm/v/backtest-kit.svg?style=flat-square)](https://npmjs.org/package/backtest-kit)
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]()
10
10
 
11
- 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.
11
+ Build reliable trading systems: backtest on historical data, deploy live bots with recovery, and optimize strategies using LLMs like Ollama.
12
12
 
13
- πŸ“š **[API Reference](https://github.com/tripolskypetr/backtest-kit)** | 🌟 **[Quick Start](#quick-start)**
13
+ πŸ“š **[API Reference](https://backtest-kit.github.io/)** | 🌟 **[Quick Start](https://github.com/tripolskypetr/backtest-kit/tree/master/demo)**
14
14
 
15
15
  ## ✨ Why Choose Backtest Kit?
16
16
 
17
- - πŸš€ **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.
17
+ - πŸš€ **Production-Ready**: Seamless switch between backtest/live modes; identical code across environments.
18
+ - πŸ’Ύ **Crash-Safe**: Atomic persistence recovers states after crashes, preventing duplicates or losses.
19
+ - βœ… **Validation**: Checks signals for TP/SL logic, risk/reward ratios, and portfolio limits.
20
+ - πŸ”„ **Efficient Execution**: Streaming architecture for large datasets; VWAP pricing for realism.
21
+ - πŸ€– **AI Integration**: LLM-powered strategy generation (Optimizer) with multi-timeframe analysis.
22
+ - πŸ“Š **Reports & Metrics**: Auto Markdown reports with PNL, Sharpe Ratio, win rate, and more.
23
+ - πŸ›‘οΈ **Risk Management**: Custom rules for position limits, time windows, and multi-strategy coordination.
24
+ - πŸ”Œ **Pluggable**: Custom data sources (CCXT), persistence (file/Redis), and sizing calculators.
25
+ - πŸ§ͺ **Tested**: 244+ unit/integration tests for validation, recovery, and events.
18
26
 
19
- - πŸ’Ύ **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.
27
+ ### Supported Order Types
20
28
 
21
- - βœ… **Signal Validation**: Comprehensive validation prevents invalid trades before execution. Catches price logic errors (TP/SL), throttles signal spam, and ensures data integrity. πŸ›‘οΈ
29
+ - Market/Limit entries
30
+ - TP/SL/OCO exits
31
+ - Grid with auto-cancel on unmet conditions
22
32
 
23
- - πŸ”„ **Async Generator Architecture**: Memory-efficient streaming for backtest and live execution. Process years of historical data without loading everything into memory. ⚑
24
-
25
- - πŸ“Š **VWAP Pricing**: Volume-weighted average price from last 5 1-minute candles ensures realistic backtest results that match live execution. πŸ“ˆ
26
-
27
- - 🎯 **Type-Safe Signal Lifecycle**: State machine with compile-time guarantees (idle β†’ scheduled β†’ opened β†’ active β†’ closed/cancelled). No runtime state confusion. πŸ”’
28
-
29
- - πŸ“ˆ **Accurate PNL Calculation**: Realistic profit/loss with configurable fees (0.1%) and slippage (0.1%). Track gross and net returns separately. πŸ’°
30
-
31
- - ⏰ **Time-Travel Context**: Async context propagation allows same strategy code to run in backtest (with historical time) and live (with real-time) without modifications. 🌐
32
-
33
- - πŸ“ **Auto-Generated Reports**: Markdown reports with statistics (win rate, avg PNL, Sharpe Ratio, standard deviation, certainty ratio, expected yearly returns, risk-adjusted returns). πŸ“Š
34
-
35
- - πŸ“Š **Revenue Profiling**: Built-in performance tracking with aggregated statistics (avg, min, max, stdDev, P95, P99) for bottleneck analysis. ⚑
36
-
37
- - πŸƒ **Strategy Comparison (Walker)**: Compare multiple strategies in parallel with automatic ranking and statistical analysis. Find your best performer. πŸ†
38
-
39
- - πŸ”₯ **Portfolio Heatmap**: Multi-symbol performance analysis with extended metrics (Profit Factor, Expectancy, Win/Loss Streaks, Avg Win/Loss) sorted by Sharpe Ratio. πŸ“‰
40
-
41
- - πŸ’° **Position Sizing Calculator**: Built-in position sizing methods (Fixed Percentage, Kelly Criterion, ATR-based) with risk management constraints. πŸ’΅
42
-
43
- - πŸ›‘οΈ **Risk Management System**: Portfolio-level risk controls with custom validation logic, concurrent position limits, and cross-strategy coordination. πŸ”
44
-
45
- - πŸ’Ύ **Zero Data Download**: Unlike Freqtrade, no need to download gigabytes of historical dataβ€”plug any data source (CCXT, database, API). πŸš€
46
-
47
- - πŸ”Œ **Pluggable Persistence**: Replace default file-based persistence with custom adapters (Redis, MongoDB, PostgreSQL) for distributed systems and high-performance scenarios.
48
-
49
- - πŸ”’ **Safe Math & Robustness**: All metrics protected against NaN/Infinity with unsafe numeric checks. Returns N/A for invalid calculations. ✨
50
-
51
- - πŸ€– **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. 🧠
52
-
53
- - πŸ§ͺ **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.
54
-
55
- ---
56
-
57
- ### 🎳 Supported Order Types
58
-
59
- Backtest Kit supports multiple execution styles to match real trading behavior:
60
-
61
- - **Market** β€” instant execution using current VWAP
62
-
63
- - **Limit** β€” entry at a specified `priceOpen`
64
-
65
- - **Take Profit (TP)** β€” automatic exit at the target price
66
-
67
- - **Stop Loss (SL)** β€” protective exit at the stop level
68
-
69
- - **OCO (TP + SL)** β€” linked exits; one cancels the other
70
-
71
- - **Grid** β€” auto-cancel if price never reaches entry point or hits SL before activation
72
-
73
-
74
- ### πŸ†• Extendable Order Types
75
-
76
- Easy to add without modifying the core:
77
-
78
- - **Stop / Stop-Limit** β€” entry triggered by `triggerPrice`
79
-
80
- - **Trailing Stop** β€” dynamic SL based on market movement
81
-
82
- - **Conditional Entry** β€” enter only if price breaks a level (`above` / `below`)
83
-
84
- - **Post-Only / Reduce-Only** β€” exchange-level execution flags
85
-
86
- ---
87
-
88
- ## πŸš€ Getting Started
33
+ ## Quick Start
89
34
 
90
35
  ### Installation
91
-
92
- Get up and running in seconds:
93
-
94
36
  ```bash
95
- npm install backtest-kit
96
- ```
97
-
98
- ### Quick Example
99
-
100
- Here's a taste of what `backtest-kit` can doβ€”create a simple moving average crossover strategy with crash-safe persistence:
101
-
102
- ```typescript
103
- import {
104
- addExchange,
105
- addStrategy,
106
- addFrame,
107
- Backtest,
108
- listenSignalBacktest,
109
- listenError,
110
- listenDoneBacktest
111
- } from "backtest-kit";
112
- import ccxt from "ccxt";
113
-
114
- // 1. Register exchange data source
115
- addExchange({
116
- exchangeName: "binance",
117
- getCandles: async (symbol, interval, since, limit) => {
118
- const exchange = new ccxt.binance();
119
- const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
120
- return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
121
- timestamp, open, high, low, close, volume
122
- }));
123
- },
124
- formatPrice: async (symbol, price) => price.toFixed(2),
125
- formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
126
- });
127
-
128
- // 2. Register trading strategy
129
- addStrategy({
130
- strategyName: "sma-crossover",
131
- interval: "5m", // Throttling: signals generated max once per 5 minutes
132
- getSignal: async (symbol) => {
133
- const price = await getAveragePrice(symbol);
134
- return {
135
- position: "long",
136
- note: "BTC breakout",
137
- priceOpen: price,
138
- priceTakeProfit: price + 1_000, // Must be > priceOpen for long
139
- priceStopLoss: price - 1_000, // Must be < priceOpen for long
140
- minuteEstimatedTime: 60,
141
- };
142
- },
143
- callbacks: {
144
- onSchedule: (symbol, signal, currentPrice, backtest) => {
145
- console.log(`[${backtest ? "BT" : "LIVE"}] Scheduled signal created:`, signal.id);
146
- },
147
- onOpen: (symbol, signal, currentPrice, backtest) => {
148
- console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
149
- },
150
- onActive: (symbol, signal, currentPrice, backtest) => {
151
- console.log(`[${backtest ? "BT" : "LIVE"}] Signal active:`, signal.id);
152
- },
153
- onClose: (symbol, signal, priceClose, backtest) => {
154
- console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
155
- },
156
- onCancel: (symbol, signal, currentPrice, backtest) => {
157
- console.log(`[${backtest ? "BT" : "LIVE"}] Scheduled signal cancelled:`, signal.id);
158
- },
159
- },
160
- });
161
-
162
- // 3. Add timeframe generator
163
- addFrame({
164
- frameName: "1d-backtest",
165
- interval: "1m",
166
- startDate: new Date("2024-01-01T00:00:00Z"),
167
- endDate: new Date("2024-01-02T00:00:00Z"),
168
- });
169
-
170
- // 4. Run backtest in background
171
- Backtest.background("BTCUSDT", {
172
- strategyName: "sma-crossover",
173
- exchangeName: "binance",
174
- frameName: "1d-backtest"
175
- });
176
-
177
- // Listen to closed signals
178
- listenSignalBacktest((event) => {
179
- if (event.action === "closed") {
180
- console.log("PNL:", event.pnl.pnlPercentage);
181
- }
182
- });
183
-
184
- // Listen to backtest completion
185
- listenDoneBacktest((event) => {
186
- console.log("Backtest completed:", event.symbol);
187
- Backtest.dump(event.strategyName); // ./logs/backtest/sma-crossover.md
188
- });
37
+ npm install backtest-kit ccxt ollama uuid
189
38
  ```
190
39
 
191
- 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 🧩
192
-
40
+ ### Basic Configuration
193
41
  ```typescript
194
- export enum ExchangeName {
195
- Binance = "binance",
196
- Bybit = "bybit",
197
- }
42
+ import { setLogger, setConfig } from 'backtest-kit';
198
43
 
199
- export enum StrategyName {
200
- SMACrossover = "sma-crossover",
201
- RSIStrategy = "rsi-strategy",
202
- }
203
-
204
- export enum FrameName {
205
- OneDay = "1d-backtest",
206
- OneWeek = "1w-backtest",
207
- }
208
-
209
- // ...
210
-
211
- addStrategy({
212
- strategyName: StrategyName.SMACrossover,
213
- interval: "5m",
214
- // ...
44
+ // Enable logging
45
+ setLogger({
46
+ log: console.log,
47
+ debug: console.debug,
48
+ info: console.info,
49
+ warn: console.warn,
215
50
  });
216
51
 
217
- Backtest.background("BTCUSDT", {
218
- strategyName: StrategyName.SMACrossover,
219
- exchangeName: ExchangeName.Binance,
220
- frameName: FrameName.OneDay
52
+ // Global config (optional)
53
+ setConfig({
54
+ CC_PERCENT_SLIPPAGE: 0.1, // % slippage
55
+ CC_PERCENT_FEE: 0.1, // % fee
56
+ CC_SCHEDULE_AWAIT_MINUTES: 120, // Pending signal timeout
221
57
  });
222
58
  ```
223
59
 
224
- ---
225
-
226
- ## 🌟 Key Features
227
-
228
- - 🀝 **Mode Switching**: Seamlessly switch between backtest and live modes with identical strategy code. πŸ”„
229
- - πŸ“œ **Crash Recovery**: Atomic persistence ensures state recovery after crashesβ€”no duplicate signals. πŸ—‚οΈ
230
- - πŸ›‘ **Graceful Shutdown**: Stop backtests, live trading, and walkers programmatically with `stop()` methods. Current signals complete normally, no forced closures. ⏹️
231
- - πŸ“‹ **Task Monitoring**: Track all running instances with `list()` methods. Monitor task statuses: `ready`, `pending`, `fulfilled`, `rejected`. πŸ“Š
232
- - πŸ› οΈ **Custom Validators**: Define validation rules with strategy-level throttling and price logic checks. πŸ”§
233
- - πŸ›‘οΈ **Signal Lifecycle**: Type-safe state machine prevents invalid state transitions. πŸš‘
234
- - πŸ“¦ **Dependency Inversion**: Lazy-load components at runtime for modular, scalable designs. 🧩
235
- - πŸ” **Schema Reflection**: Runtime introspection with `listExchanges()`, `listStrategies()`, `listFrames()`. πŸ“Š
236
- - πŸ”¬ **Data Validation**: Automatic detection and rejection of incomplete candles from Binance API with anomaly checks. βœ…
237
-
238
- ---
239
-
240
- ## 🎯 Use Cases
241
-
242
- - πŸ“ˆ **Algorithmic Trading**: Backtest and deploy systematic trading strategies with confidence. πŸ’Ή
243
- - πŸ€– **Strategy Development**: Rapid prototyping with automatic validation and PNL tracking. πŸ› οΈ
244
- - πŸ“Š **Performance Analysis**: Compare strategies with Walker and analyze portfolios with Heatmap. πŸ“‰
245
- - πŸ’Ό **Portfolio Management**: Multi-symbol trading with risk controls and position sizing. 🏦
246
-
247
- ---
248
-
249
- ## πŸ“– API Highlights
250
-
251
- - πŸ› οΈ **`addExchange`**: Define exchange data sources (CCXT, database, API). πŸ“‘
252
- - πŸ€– **`addStrategy`**: Create trading strategies with custom signals and callbacks. πŸ’‘
253
- - 🌐 **`addFrame`**: Configure timeframes for backtesting. πŸ“…
254
- - πŸ”„ **`Backtest` / `Live`**: Run strategies in backtest or live mode (generator or background). ⚑
255
- - πŸ›‘ **`Backtest.stop()` / `Live.stop()` / `Walker.stop()`**: Gracefully stop running strategiesβ€”current signals complete, no forced exits. ⏹️
256
- - πŸ“‹ **`Backtest.list()` / `Live.list()` / `Walker.list()`**: Monitor all running instances with status tracking (`ready`, `pending`, `fulfilled`, `rejected`). πŸ“Š
257
- - πŸ“… **`Schedule`**: Track scheduled signals and cancellation rate for limit orders. πŸ“Š
258
- - πŸ“Š **`Partial`**: Access partial profit/loss statistics and reports for risk management. Track signals reaching milestone levels (10%, 20%, 30%, etc.). πŸ’Ή
259
- - 🎯 **`Constant`**: Kelly Criterion-based constants for optimal take profit (TP_LEVEL1-3) and stop loss (SL_LEVEL1-2) levels. πŸ“
260
- - πŸƒ **`Walker`**: Compare multiple strategies in parallel with ranking. πŸ†
261
- - πŸ”₯ **`Heat`**: Portfolio-wide performance analysis across multiple symbols. πŸ“Š
262
- - πŸ’° **`PositionSize`**: Calculate position sizes with Fixed %, Kelly Criterion, or ATR-based methods. πŸ’΅
263
- - πŸ›‘οΈ **`addRisk`**: Portfolio-level risk management with custom validation logic. πŸ”
264
- - πŸ’Ύ **`PersistBase`**: Base class for custom persistence adapters (Redis, MongoDB, PostgreSQL).
265
- - πŸ”Œ **`PersistSignalAdapter` / `PersistScheduleAdapter` / `PersistRiskAdapter` / `PersistPartialAdapter`**: Register custom adapters for signal, scheduled signal, risk, and partial state persistence.
266
- - πŸ€– **`Optimizer`**: AI-powered strategy generation with LLM integration. Auto-generate strategies from historical data and export executable code. 🧠
267
-
268
- Check out the sections below for detailed examples! πŸ“š
269
-
270
- ---
271
-
272
- ## πŸ›  Advanced Features
273
-
274
- ### 1. Register Exchange Data Source
275
-
276
- You can plug any data source: CCXT for live data or a database for faster backtesting:
277
-
60
+ ### Register Components
278
61
  ```typescript
279
- import { addExchange } from "backtest-kit";
280
- import ccxt from "ccxt";
62
+ import ccxt from 'ccxt';
63
+ import { addExchange, addStrategy, addFrame, addRisk } from 'backtest-kit';
281
64
 
282
- // Option 1: CCXT (live or historical)
65
+ // Exchange (data source)
283
66
  addExchange({
284
- exchangeName: "binance",
67
+ exchangeName: 'binance',
285
68
  getCandles: async (symbol, interval, since, limit) => {
286
69
  const exchange = new ccxt.binance();
287
70
  const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
288
- return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
289
- timestamp, open, high, low, close, volume
290
- }));
291
- },
292
- formatPrice: async (symbol, price) => price.toFixed(2),
293
- formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
294
- });
295
-
296
- // Option 2: Database (faster backtesting)
297
- import { db } from "./database";
298
-
299
- addExchange({
300
- exchangeName: "binance-db",
301
- getCandles: async (symbol, interval, since, limit) => {
302
- return await db.query(`
303
- SELECT timestamp, open, high, low, close, volume
304
- FROM candles
305
- WHERE symbol = $1 AND interval = $2 AND timestamp >= $3
306
- ORDER BY timestamp ASC
307
- LIMIT $4
308
- `, [symbol, interval, since, limit]);
309
- },
310
- formatPrice: async (symbol, price) => price.toFixed(2),
311
- formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
312
- });
313
- ```
314
-
315
- ### 2. Register Trading Strategy
316
-
317
- Define your signal generation logic with automatic validation:
318
-
319
- ```typescript
320
- import { addStrategy } from "backtest-kit";
321
-
322
- addStrategy({
323
- strategyName: "my-strategy",
324
- interval: "5m", // Throttling: signals generated max once per 5 minutes
325
- getSignal: async (symbol) => {
326
- const price = await getAveragePrice(symbol);
327
- return {
328
- position: "long",
329
- note: "BTC breakout",
330
- priceOpen: price,
331
- priceTakeProfit: price + 1_000, // Must be > priceOpen for long
332
- priceStopLoss: price - 1_000, // Must be < priceOpen for long
333
- minuteEstimatedTime: 60,
334
- };
335
- },
336
- callbacks: {
337
- onOpen: (symbol, signal, currentPrice, backtest) => {
338
- console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
339
- },
340
- onClose: (symbol, signal, priceClose, backtest) => {
341
- console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
342
- },
343
- },
344
- });
345
- ```
346
-
347
- ### 3. Run Backtest
348
-
349
- Run strategies in background mode (infinite loop) or manually iterate with async generators:
350
-
351
- ```typescript
352
- import { Backtest, listenSignalBacktest, listenDoneBacktest } from "backtest-kit";
353
-
354
- // Option 1: Background mode (recommended)
355
- const stopBacktest = Backtest.background("BTCUSDT", {
356
- strategyName: "my-strategy",
357
- exchangeName: "binance",
358
- frameName: "1d-backtest"
359
- });
360
-
361
- listenSignalBacktest((event) => {
362
- if (event.action === "closed") {
363
- console.log("PNL:", event.pnl.pnlPercentage);
364
- }
365
- });
366
-
367
- listenDoneBacktest((event) => {
368
- console.log("Backtest completed:", event.symbol);
369
- Backtest.dump(event.strategyName); // ./logs/backtest/my-strategy.md
370
- });
371
-
372
- // Graceful shutdown - stop backtest programmatically
373
- await Backtest.stop("BTCUSDT", "my-strategy");
374
- // - Current signal completes execution (onClose callback fires)
375
- // - No new signals are generated after stop
376
- // - listenDoneBacktest event fires when complete
377
-
378
- // Option 2: Manual iteration (for custom control)
379
- for await (const result of Backtest.run("BTCUSDT", {
380
- strategyName: "my-strategy",
381
- exchangeName: "binance",
382
- frameName: "1d-backtest"
383
- })) {
384
- console.log("PNL:", result.pnl.pnlPercentage);
385
- if (result.pnl.pnlPercentage < -5) break; // Early termination
386
- }
387
- ```
388
-
389
- ### 4. Run Live Trading (Crash-Safe)
390
-
391
- Live mode automatically persists state to disk with atomic writes:
392
-
393
- ```typescript
394
- import { Live, listenSignalLive } from "backtest-kit";
395
-
396
- // Run live trading in background (infinite loop, crash-safe)
397
- const cancelFn = Live.background("BTCUSDT", {
398
- strategyName: "my-strategy",
399
- exchangeName: "binance"
400
- });
401
-
402
- listenSignalLive((event) => {
403
- if (event.action === "opened") {
404
- console.log("Signal opened:", event.signal.id);
405
- }
406
-
407
- if (event.action === "closed") {
408
- console.log("Signal closed:", {
409
- reason: event.closeReason,
410
- pnl: event.pnl.pnlPercentage,
411
- });
412
- Live.dump(event.strategyName); // Auto-save report
413
- }
414
- });
415
-
416
- // Graceful shutdown - stop live trading programmatically
417
- await Live.stop("BTCUSDT", "my-strategy");
418
- // - Active signal completes normally (no forced close)
419
- // - No new signals are generated after stop
420
- // - State remains persisted for resume on restart
421
-
422
- // Or use cancelFn() for immediate cancellation
423
- cancelFn();
424
- ```
425
-
426
- **Crash Recovery:** If process crashes, restart with same codeβ€”state automatically recovered from disk (no duplicate signals).
427
-
428
- ### 5. Strategy Comparison with Walker
429
-
430
- Walker runs multiple strategies in parallel and ranks them by a selected metric:
431
-
432
- ```typescript
433
- import { addWalker, Walker, listenWalkerComplete } from "backtest-kit";
434
-
435
- // Register walker schema
436
- addWalker({
437
- walkerName: "btc-walker",
438
- exchangeName: "binance",
439
- frameName: "1d-backtest",
440
- strategies: ["strategy-a", "strategy-b", "strategy-c"],
441
- metric: "sharpeRatio", // Metric to compare strategies
442
- callbacks: {
443
- onStrategyStart: (strategyName, symbol) => {
444
- console.log(`Starting strategy: ${strategyName}`);
445
- },
446
- onStrategyComplete: async (strategyName, symbol, stats) => {
447
- console.log(`${strategyName} completed:`, stats.sharpeRatio);
448
-
449
- // Optional: Stop walker early after first strategy completes
450
- // await Walker.stop("BTCUSDT", "btc-walker");
451
- },
452
- onComplete: (results) => {
453
- console.log("Best strategy:", results.bestStrategy);
454
- console.log("Best metric:", results.bestMetric);
455
- },
456
- },
457
- });
458
-
459
- // Run walker in background
460
- const cancelFn = Walker.background("BTCUSDT", {
461
- walkerName: "btc-walker"
462
- });
463
-
464
- // Listen to walker completion
465
- listenWalkerComplete((results) => {
466
- console.log("Walker completed:", results.bestStrategy);
467
- Walker.dump("BTCUSDT", results.walkerName); // Save report
468
- });
469
-
470
- // Graceful shutdown - stop walker programmatically
471
- await Walker.stop("BTCUSDT", "btc-walker");
472
- // - Current strategy completes execution
473
- // - Remaining strategies in queue won't run
474
- // - listenWalkerComplete event fires with partial results
475
- // - Use case: Early termination when first strategy is good enough
476
-
477
- // Or use cancelFn() for immediate cancellation
478
- cancelFn();
479
-
480
- // Get raw comparison data
481
- const results = await Walker.getData("BTCUSDT", "btc-walker");
482
- console.log(results);
483
- // Returns:
484
- // {
485
- // bestStrategy: "strategy-b",
486
- // bestMetric: 1.85,
487
- // strategies: [
488
- // { strategyName: "strategy-a", stats: { sharpeRatio: 1.23, ... }, metric: 1.23 },
489
- // { strategyName: "strategy-b", stats: { sharpeRatio: 1.85, ... }, metric: 1.85 },
490
- // { strategyName: "strategy-c", stats: { sharpeRatio: 0.98, ... }, metric: 0.98 }
491
- // ]
492
- // }
493
-
494
- // Generate markdown report
495
- const markdown = await Walker.getReport("BTCUSDT", "btc-walker");
496
- console.log(markdown);
497
- ```
498
-
499
- **Available metrics for comparison:**
500
- - `sharpeRatio` - Risk-adjusted return (default)
501
- - `winRate` - Win percentage
502
- - `avgPnl` - Average PNL percentage
503
- - `totalPnl` - Total PNL percentage
504
- - `certaintyRatio` - avgWin / |avgLoss|
505
-
506
- ### 6. Portfolio Heatmap
507
-
508
- Heat provides portfolio-wide performance analysis across multiple symbols:
509
-
510
- ```typescript
511
- import { Heat, Backtest } from "backtest-kit";
512
-
513
- // Run backtests for multiple symbols
514
- for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"]) {
515
- for await (const _ of Backtest.run(symbol, {
516
- strategyName: "my-strategy",
517
- exchangeName: "binance",
518
- frameName: "2024-backtest"
519
- })) {}
520
- }
521
-
522
- // Get raw heatmap data
523
- const stats = await Heat.getData("my-strategy");
524
- console.log(stats);
525
- // Returns:
526
- // {
527
- // symbols: [
528
- // {
529
- // symbol: "BTCUSDT",
530
- // totalPnl: 15.5, // Total profit/loss %
531
- // sharpeRatio: 2.10, // Risk-adjusted return
532
- // profitFactor: 2.50, // Wins / Losses ratio
533
- // expectancy: 1.85, // Expected value per trade
534
- // winRate: 72.3, // Win percentage
535
- // avgWin: 2.45, // Average win %
536
- // avgLoss: -0.95, // Average loss %
537
- // maxDrawdown: -2.5, // Maximum drawdown %
538
- // maxWinStreak: 5, // Consecutive wins
539
- // maxLossStreak: 2, // Consecutive losses
540
- // totalTrades: 45,
541
- // winCount: 32,
542
- // lossCount: 13,
543
- // avgPnl: 0.34,
544
- // stdDev: 1.62
545
- // },
546
- // // ... more symbols sorted by Sharpe Ratio
547
- // ],
548
- // totalSymbols: 4,
549
- // portfolioTotalPnl: 45.3, // Portfolio-wide total PNL
550
- // portfolioSharpeRatio: 1.85, // Portfolio-wide Sharpe
551
- // portfolioTotalTrades: 120
552
- // }
553
-
554
- // Generate markdown report
555
- const markdown = await Heat.getReport("my-strategy");
556
- console.log(markdown);
557
-
558
- // Save to disk (default: ./logs/heatmap/my-strategy.md)
559
- await Heat.dump("my-strategy");
560
- ```
561
-
562
- **Heatmap Report Example:**
563
- ```markdown
564
- # Portfolio Heatmap: my-strategy
565
-
566
- **Total Symbols:** 4 | **Portfolio PNL:** +45.30% | **Portfolio Sharpe:** 1.85 | **Total Trades:** 120
567
-
568
- | Symbol | Total PNL | Sharpe | PF | Expect | WR | Avg Win | Avg Loss | Max DD | W Streak | L Streak | Trades |
569
- |--------|-----------|--------|-------|--------|-----|---------|----------|--------|----------|----------|--------|
570
- | BTCUSDT | +15.50% | 2.10 | 2.50 | +1.85% | 72.3% | +2.45% | -0.95% | -2.50% | 5 | 2 | 45 |
571
- | ETHUSDT | +12.30% | 1.85 | 2.15 | +1.45% | 68.5% | +2.10% | -1.05% | -3.10% | 4 | 2 | 38 |
572
- | SOLUSDT | +10.20% | 1.65 | 1.95 | +1.20% | 65.2% | +1.95% | -1.15% | -4.20% | 3 | 3 | 25 |
573
- | BNBUSDT | +7.30% | 1.40 | 1.75 | +0.95% | 62.5% | +1.75% | -1.20% | -3.80% | 3 | 2 | 12 |
574
- ```
575
-
576
- **Column Descriptions:**
577
- - **Total PNL** - Total profit/loss percentage across all trades
578
- - **Sharpe** - Risk-adjusted return (higher is better)
579
- - **PF** - Profit Factor: sum of wins / sum of losses (>1.0 is profitable)
580
- - **Expect** - Expectancy: expected value per trade
581
- - **WR** - Win Rate: percentage of winning trades
582
- - **Avg Win** - Average profit on winning trades
583
- - **Avg Loss** - Average loss on losing trades
584
- - **Max DD** - Maximum drawdown (largest peak-to-trough decline)
585
- - **W Streak** - Maximum consecutive winning trades
586
- - **L Streak** - Maximum consecutive losing trades
587
- - **Trades** - Total number of trades for this symbol
588
-
589
- ### 7. Position Sizing Calculator
590
-
591
- Position Sizing Calculator helps determine optimal position sizes based on risk management rules:
592
-
593
- ```typescript
594
- import { addSizing, PositionSize } from "backtest-kit";
595
-
596
- // Fixed Percentage Risk - risk fixed % of account per trade
597
- addSizing({
598
- sizingName: "conservative",
599
- note: "Conservative 2% risk per trade",
600
- method: "fixed-percentage",
601
- riskPercentage: 2, // Risk 2% of account per trade
602
- maxPositionPercentage: 10, // Max 10% of account in single position (optional)
603
- minPositionSize: 0.001, // Min 0.001 BTC position (optional)
604
- maxPositionSize: 1.0, // Max 1.0 BTC position (optional)
605
- });
606
-
607
- // Kelly Criterion - optimal bet sizing based on edge
608
- addSizing({
609
- sizingName: "kelly-quarter",
610
- note: "Kelly Criterion with 25% multiplier for safety",
611
- method: "kelly-criterion",
612
- kellyMultiplier: 0.25, // Use 25% of full Kelly (recommended for safety)
613
- maxPositionPercentage: 15, // Cap position at 15% of account (optional)
614
- minPositionSize: 0.001, // Min 0.001 BTC position (optional)
615
- maxPositionSize: 2.0, // Max 2.0 BTC position (optional)
616
- });
617
-
618
- // ATR-based - volatility-adjusted position sizing
619
- addSizing({
620
- sizingName: "atr-dynamic",
621
- note: "ATR-based sizing with 2x multiplier",
622
- method: "atr-based",
623
- riskPercentage: 2, // Risk 2% of account
624
- atrMultiplier: 2, // Use 2x ATR as stop distance
625
- maxPositionPercentage: 12, // Max 12% of account (optional)
626
- minPositionSize: 0.001, // Min 0.001 BTC position (optional)
627
- maxPositionSize: 1.5, // Max 1.5 BTC position (optional)
628
- });
629
-
630
- // Calculate position sizes
631
- const quantity1 = await PositionSize.fixedPercentage(
632
- "BTCUSDT",
633
- 10000, // Account balance: $10,000
634
- 50000, // Entry price: $50,000
635
- 49000, // Stop loss: $49,000
636
- { sizingName: "conservative" }
637
- );
638
- console.log(`Position size: ${quantity1} BTC`);
639
-
640
- const quantity2 = await PositionSize.kellyCriterion(
641
- "BTCUSDT",
642
- 10000, // Account balance: $10,000
643
- 50000, // Entry price: $50,000
644
- 0.55, // Win rate: 55%
645
- 1.5, // Win/loss ratio: 1.5
646
- { sizingName: "kelly-quarter" }
647
- );
648
- console.log(`Position size: ${quantity2} BTC`);
649
-
650
- const quantity3 = await PositionSize.atrBased(
651
- "BTCUSDT",
652
- 10000, // Account balance: $10,000
653
- 50000, // Entry price: $50,000
654
- 500, // ATR: $500
655
- { sizingName: "atr-dynamic" }
656
- );
657
- console.log(`Position size: ${quantity3} BTC`);
658
- ```
659
-
660
- **When to Use Each Method:**
661
-
662
- 1. **Fixed Percentage** - Simple risk management, consistent risk per trade
663
- - Best for: Beginners, conservative strategies
664
- - Risk: Fixed 1-2% per trade
665
-
666
- 2. **Kelly Criterion** - Optimal bet sizing based on win rate and win/loss ratio
667
- - Best for: Strategies with known edge, statistical advantage
668
- - Risk: Use fractional Kelly (0.25-0.5) to reduce volatility
669
-
670
- 3. **ATR-based** - Volatility-adjusted sizing, accounts for market conditions
671
- - Best for: Swing trading, volatile markets
672
- - Risk: Position size scales with volatility
673
-
674
- ### 8. Risk Management
675
-
676
- Risk Management provides portfolio-level risk controls across strategies:
677
-
678
- ```typescript
679
- import { addRisk } from "backtest-kit";
680
-
681
- // Simple concurrent position limit
682
- addRisk({
683
- riskName: "conservative",
684
- note: "Conservative risk profile with max 3 concurrent positions",
685
- validations: [
686
- ({ activePositionCount }) => {
687
- if (activePositionCount >= 3) {
688
- throw new Error("Maximum 3 concurrent positions allowed");
689
- }
690
- },
691
- ],
692
- callbacks: {
693
- onRejected: (symbol, params) => {
694
- console.warn(`Signal rejected for ${symbol}:`, params);
695
- },
696
- onAllowed: (symbol, params) => {
697
- console.log(`Signal allowed for ${symbol}`);
698
- },
71
+ return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({ timestamp, open, high, low, close, volume }));
699
72
  },
73
+ formatPrice: (symbol, price) => price.toFixed(2),
74
+ formatQuantity: (symbol, quantity) => quantity.toFixed(8),
700
75
  });
701
76
 
702
- // Symbol-based filtering
77
+ // Risk profile
703
78
  addRisk({
704
- riskName: "no-meme-coins",
705
- note: "Block meme coins from trading",
79
+ riskName: 'demo',
706
80
  validations: [
707
- ({ symbol }) => {
708
- const memeCoins = ["DOGEUSDT", "SHIBUSDT", "PEPEUSDT"];
709
- if (memeCoins.includes(symbol)) {
710
- throw new Error(`Meme coin ${symbol} not allowed`);
711
- }
81
+ // TP at least 1%
82
+ ({ pendingSignal, currentPrice }) => {
83
+ const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal;
84
+ const tpDistance = position === 'long' ? ((priceTakeProfit - priceOpen) / priceOpen) * 100 : ((priceOpen - priceTakeProfit) / priceOpen) * 100;
85
+ if (tpDistance < 1) throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);
86
+ },
87
+ // R/R at least 2:1
88
+ ({ pendingSignal, currentPrice }) => {
89
+ const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal;
90
+ const reward = position === 'long' ? priceTakeProfit - priceOpen : priceOpen - priceTakeProfit;
91
+ const risk = position === 'long' ? priceOpen - priceStopLoss : priceStopLoss - priceOpen;
92
+ if (reward / risk < 2) throw new Error('Poor R/R ratio');
712
93
  },
713
94
  ],
714
95
  });
715
96
 
716
- // Time-based trading windows
717
- addRisk({
718
- riskName: "trading-hours",
719
- note: "Only trade during market hours (9 AM - 5 PM UTC)",
720
- validations: [
721
- ({ timestamp }) => {
722
- const date = new Date(timestamp);
723
- const hour = date.getUTCHours();
724
-
725
- if (hour < 9 || hour >= 17) {
726
- throw new Error("Trading only allowed 9 AM - 5 PM UTC");
727
- }
728
- },
729
- ],
97
+ // Time frame
98
+ addFrame({
99
+ frameName: '1d-test',
100
+ interval: '1m',
101
+ startDate: new Date('2025-12-01'),
102
+ endDate: new Date('2025-12-02'),
730
103
  });
104
+ ```
731
105
 
732
- // Multi-strategy coordination with position inspection
733
- addRisk({
734
- riskName: "strategy-coordinator",
735
- note: "Limit exposure per strategy and inspect active positions",
736
- validations: [
737
- ({ activePositions, strategyName, symbol }) => {
738
- // Count positions for this specific strategy
739
- const strategyPositions = activePositions.filter(
740
- (pos) => pos.strategyName === strategyName
741
- );
742
-
743
- if (strategyPositions.length >= 2) {
744
- throw new Error(`Strategy ${strategyName} already has 2 positions`);
745
- }
746
-
747
- // Check if we already have a position on this symbol
748
- const symbolPositions = activePositions.filter(
749
- (pos) => pos.symbol === symbol
750
- );
751
-
752
- if (symbolPositions.length > 0) {
753
- throw new Error(`Already have position on ${symbol}`);
754
- }
755
- },
756
- ],
757
- });
106
+ ### Example Strategy (with LLM)
107
+ ```typescript
108
+ import { v4 as uuid } from 'uuid';
109
+ import { addStrategy, dumpSignal } from 'backtest-kit';
110
+ import { json } from './utils/json.mjs'; // LLM wrapper
111
+ import { getMessages } from './utils/messages.mjs'; // Market data prep
758
112
 
759
- // Use risk profile in strategy
760
113
  addStrategy({
761
- strategyName: "my-strategy",
762
- interval: "5m",
763
- riskName: "conservative", // Apply risk profile
114
+ strategyName: 'llm-strategy',
115
+ interval: '5m',
116
+ riskName: 'demo',
764
117
  getSignal: async (symbol) => {
765
- // Signal generation logic
766
- return { /* ... */ };
118
+ const messages = await getMessages(symbol); // Fetch indicators/news
119
+ const resultId = uuid();
120
+ const signal = await json(messages); // LLM generates signal
121
+ await dumpSignal(resultId, messages, signal); // Log
122
+ return { ...signal, id: resultId };
767
123
  },
768
124
  });
769
125
  ```
770
126
 
771
- ### 9. Custom Persistence Adapters (Optional)
772
-
773
- 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.
774
-
775
- #### Understanding the Persistence System
776
-
777
- The library uses three persistence layers:
778
-
779
- 1. **PersistBase** - Base class for all persistence operations (file-based by default)
780
- 2. **PersistSignalAdapter** - Manages signal state persistence (used by Live mode)
781
- 3. **PersistRiskAdapter** - Manages active positions for risk management
782
-
783
- #### Default File-Based Persistence
784
-
785
- By default, data is stored in JSON files:
786
-
787
- ```
788
- ./logs/data/
789
- signal/
790
- my-strategy/
791
- BTCUSDT.json # Signal state for BTCUSDT
792
- ETHUSDT.json # Signal state for ETHUSDT
793
- risk/
794
- conservative/
795
- positions.json # Active positions for risk profile
796
- ```
797
-
798
- #### Create Custom Adapter (Redis Example)
799
-
800
- ```typescript
801
- import { PersistBase, PersistSignalAdaper, PersistRiskAdapter } from "backtest-kit";
802
- import Redis from "ioredis";
803
-
804
- const redis = new Redis();
805
-
806
- // Custom Redis-based persistence adapter
807
- class RedisPersist extends PersistBase {
808
- // Initialize Redis connection
809
- async waitForInit(initial: boolean): Promise<void> {
810
- // Redis connection is already established
811
- console.log(`Redis persistence initialized for ${this.entityName}`);
812
- }
813
-
814
- // Read entity from Redis
815
- async readValue<T>(entityId: string | number): Promise<T> {
816
- const key = `${this.entityName}:${entityId}`;
817
- const data = await redis.get(key);
818
-
819
- if (!data) {
820
- throw new Error(`Entity ${this.entityName}:${entityId} not found`);
821
- }
822
-
823
- return JSON.parse(data) as T;
824
- }
825
-
826
- // Check if entity exists in Redis
827
- async hasValue(entityId: string | number): Promise<boolean> {
828
- const key = `${this.entityName}:${entityId}`;
829
- const exists = await redis.exists(key);
830
- return exists === 1;
831
- }
832
-
833
- // Write entity to Redis
834
- async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
835
- const key = `${this.entityName}:${entityId}`;
836
- const serializedData = JSON.stringify(entity);
837
- await redis.set(key, serializedData);
838
-
839
- // Optional: Set TTL (time to live)
840
- // await redis.expire(key, 86400); // 24 hours
841
- }
842
-
843
- // Remove entity from Redis
844
- async removeValue(entityId: string | number): Promise<void> {
845
- const key = `${this.entityName}:${entityId}`;
846
- const result = await redis.del(key);
847
-
848
- if (result === 0) {
849
- throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
850
- }
851
- }
852
-
853
- // Remove all entities for this entity type
854
- async removeAll(): Promise<void> {
855
- const pattern = `${this.entityName}:*`;
856
- const keys = await redis.keys(pattern);
857
-
858
- if (keys.length > 0) {
859
- await redis.del(...keys);
860
- }
861
- }
862
-
863
- // Iterate over all entity values
864
- async *values<T>(): AsyncGenerator<T> {
865
- const pattern = `${this.entityName}:*`;
866
- const keys = await redis.keys(pattern);
867
-
868
- // Sort keys alphanumerically
869
- keys.sort((a, b) => a.localeCompare(b, undefined, {
870
- numeric: true,
871
- sensitivity: "base"
872
- }));
873
-
874
- for (const key of keys) {
875
- const data = await redis.get(key);
876
- if (data) {
877
- yield JSON.parse(data) as T;
878
- }
879
- }
880
- }
881
-
882
- // Iterate over all entity IDs
883
- async *keys(): AsyncGenerator<string> {
884
- const pattern = `${this.entityName}:*`;
885
- const keys = await redis.keys(pattern);
886
-
887
- // Sort keys alphanumerically
888
- keys.sort((a, b) => a.localeCompare(b, undefined, {
889
- numeric: true,
890
- sensitivity: "base"
891
- }));
892
-
893
- for (const key of keys) {
894
- // Extract entity ID from key (remove prefix)
895
- const entityId = key.slice(this.entityName.length + 1);
896
- yield entityId;
897
- }
898
- }
899
- }
900
-
901
- // Register Redis adapter for signal persistence
902
- PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
903
-
904
- // Register Redis adapter for risk persistence
905
- PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
906
- ```
907
-
908
- #### Custom Adapter Registration (Before Running Strategies)
909
-
127
+ ### Run Backtest
910
128
  ```typescript
911
- import { PersistSignalAdaper, PersistRiskAdapter, Live } from "backtest-kit";
129
+ import { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';
912
130
 
913
- // IMPORTANT: Register adapters BEFORE running any strategies
914
- PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
915
- PersistRiskAdapter.usePersistRiskAdapter(RedisPersist);
131
+ Backtest.background('BTCUSDT', {
132
+ strategyName: 'llm-strategy',
133
+ exchangeName: 'binance',
134
+ frameName: '1d-test',
135
+ });
916
136
 
917
- // Now run live trading with Redis persistence
918
- Live.background("BTCUSDT", {
919
- strategyName: "my-strategy",
920
- exchangeName: "binance"
137
+ listenSignalBacktest((event) => console.log(event));
138
+ listenDoneBacktest(async (event) => {
139
+ await Backtest.dump(event.symbol, event.strategyName); // Generate report
921
140
  });
922
141
  ```
923
142
 
924
- #### MongoDB Adapter Example
925
-
143
+ ### Run Live Trading
926
144
  ```typescript
927
- import { PersistBase } from "backtest-kit";
928
- import { MongoClient, Collection } from "mongodb";
929
-
930
- const client = new MongoClient("mongodb://localhost:27017");
931
- const db = client.db("backtest-kit");
145
+ import { Live, listenSignalLive } from 'backtest-kit';
932
146
 
933
- class MongoPersist extends PersistBase {
934
- private collection: Collection;
935
-
936
- constructor(entityName: string, baseDir: string) {
937
- super(entityName, baseDir);
938
- this.collection = db.collection(this.entityName);
939
- }
940
-
941
- async waitForInit(initial: boolean): Promise<void> {
942
- await client.connect();
943
- // Create index for faster lookups
944
- await this.collection.createIndex({ entityId: 1 }, { unique: true });
945
- console.log(`MongoDB persistence initialized for ${this.entityName}`);
946
- }
947
-
948
- async readValue<T>(entityId: string | number): Promise<T> {
949
- const doc = await this.collection.findOne({ entityId });
147
+ Live.background('BTCUSDT', {
148
+ strategyName: 'llm-strategy',
149
+ exchangeName: 'binance', // Use API keys in .env
150
+ });
950
151
 
951
- if (!doc) {
952
- throw new Error(`Entity ${this.entityName}:${entityId} not found`);
953
- }
152
+ listenSignalLive((event) => console.log(event));
153
+ ```
954
154
 
955
- return doc.data as T;
956
- }
155
+ ### Monitoring & Events
957
156
 
958
- async hasValue(entityId: string | number): Promise<boolean> {
959
- const count = await this.collection.countDocuments({ entityId });
960
- return count > 0;
961
- }
157
+ - Use `listenRisk`, `listenError`, `listenPartialProfit/Loss` for alerts.
158
+ - Dump reports: `Backtest.dump()`, `Live.dump()`.
962
159
 
963
- async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
964
- await this.collection.updateOne(
965
- { entityId },
966
- { $set: { entityId, data: entity, updatedAt: new Date() } },
967
- { upsert: true }
968
- );
969
- }
160
+ ## Global Configuration
970
161
 
971
- async removeValue(entityId: string | number): Promise<void> {
972
- const result = await this.collection.deleteOne({ entityId });
162
+ Customize via `setConfig()`:
973
163
 
974
- if (result.deletedCount === 0) {
975
- throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
976
- }
977
- }
164
+ - `CC_SCHEDULE_AWAIT_MINUTES`: Pending timeout (default: 120).
165
+ - `CC_AVG_PRICE_CANDLES_COUNT`: VWAP candles (default: 5).
978
166
 
979
- async removeAll(): Promise<void> {
980
- await this.collection.deleteMany({});
981
- }
167
+ ## Tested & Reliable
982
168
 
983
- async *values<T>(): AsyncGenerator<T> {
984
- const cursor = this.collection.find({}).sort({ entityId: 1 });
169
+ 244+ tests cover validation, recovery, reports, and events.
985
170
 
986
- for await (const doc of cursor) {
987
- yield doc.data as T;
988
- }
989
- }
171
+ ## 🀝 Contribute
990
172
 
991
- async *keys(): AsyncGenerator<string> {
992
- const cursor = this.collection.find({}, { projection: { entityId: 1 } }).sort({ entityId: 1 });
173
+ Fork/PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
993
174
 
994
- for await (const doc of cursor) {
995
- yield String(doc.entityId);
996
- }
997
- }
998
- }
175
+ ## πŸ“œ License
999
176
 
1000
- // Register MongoDB adapter
1001
- PersistSignalAdaper.usePersistSignalAdapter(MongoPersist);
1002
- PersistRiskAdapter.usePersistRiskAdapter(MongoPersist);
1003
- ```
177
+ MIT Β© [tripolskypetr](https://github.com/tripolskypetr)
1004
178
 
1005
- #### Direct Persistence API Usage (Advanced)
1006
-
1007
- You can also use PersistBase directly for custom data storage:
1008
-
1009
- ```typescript
1010
- import { PersistBase } from "backtest-kit";
1011
-
1012
- // Create custom persistence for trading logs
1013
- const tradingLogs = new PersistBase("trading-logs", "./logs/custom");
1014
-
1015
- // Initialize
1016
- await tradingLogs.waitForInit(true);
1017
-
1018
- // Write log entry
1019
- await tradingLogs.writeValue("log-1", {
1020
- timestamp: Date.now(),
1021
- message: "Strategy started",
1022
- metadata: { symbol: "BTCUSDT", strategy: "sma-crossover" }
1023
- });
1024
-
1025
- // Read log entry
1026
- const log = await tradingLogs.readValue("log-1");
1027
- console.log(log);
1028
-
1029
- // Check if log exists
1030
- const exists = await tradingLogs.hasValue("log-1");
1031
- console.log(`Log exists: ${exists}`);
1032
-
1033
- // Iterate over all logs
1034
- for await (const log of tradingLogs.values()) {
1035
- console.log("Log:", log);
1036
- }
1037
-
1038
- // Get all log IDs
1039
- for await (const logId of tradingLogs.keys()) {
1040
- console.log("Log ID:", logId);
1041
- }
1042
-
1043
- // Filter logs
1044
- for await (const log of tradingLogs.filter((l: any) => l.metadata.symbol === "BTCUSDT")) {
1045
- console.log("BTC Log:", log);
1046
- }
1047
-
1048
- // Take first 5 logs
1049
- for await (const log of tradingLogs.take(5)) {
1050
- console.log("Recent Log:", log);
1051
- }
1052
-
1053
- // Remove specific log
1054
- await tradingLogs.removeValue("log-1");
1055
-
1056
- // Remove all logs
1057
- await tradingLogs.removeAll();
1058
- ```
1059
-
1060
- #### When to Use Custom Adapters
1061
-
1062
- 1. **Redis** - Best for high-performance distributed systems with multiple instances
1063
- - Fast read/write operations
1064
- - Built-in TTL (automatic cleanup)
1065
- - Pub/sub for real-time updates
1066
-
1067
- 2. **MongoDB** - Best for complex queries and analytics
1068
- - Rich query language
1069
- - Aggregation pipelines
1070
- - Scalable for large datasets
1071
-
1072
- 3. **PostgreSQL** - Best for ACID transactions and relational data
1073
- - Strong consistency guarantees
1074
- - Complex joins and queries
1075
- - Mature ecosystem
1076
-
1077
- 4. **File-based (default)** - Best for single-instance deployments
1078
- - No dependencies
1079
- - Simple debugging (inspect JSON files)
1080
- - Sufficient for most use cases
1081
-
1082
- #### Testing Custom Adapters
1083
-
1084
- ```typescript
1085
- import { test } from "worker-testbed";
1086
- import { PersistBase } from "backtest-kit";
1087
-
1088
- test("Custom Redis adapter works correctly", async ({ pass, fail }) => {
1089
- const persist = new RedisPersist("test-entity", "./logs/test");
1090
-
1091
- await persist.waitForInit(true);
1092
-
1093
- // Write
1094
- await persist.writeValue("key1", { data: "value1" });
1095
-
1096
- // Read
1097
- const value = await persist.readValue("key1");
1098
- if (value.data === "value1") {
1099
- pass("Redis adapter read/write works");
1100
- } else {
1101
- fail("Redis adapter failed");
1102
- }
1103
-
1104
- // Cleanup
1105
- await persist.removeValue("key1");
1106
- });
1107
- ```
1108
-
1109
- ### 10. Partial Profit/Loss Tracking
1110
-
1111
- Partial Profit/Loss system tracks signal performance at fixed percentage levels (10%, 20%, 30%, etc.) for risk management and position scaling strategies.
1112
-
1113
- #### Understanding Partial Levels
1114
-
1115
- The system automatically monitors profit/loss milestones and emits events when signals reach specific levels:
1116
-
1117
- ```typescript
1118
- import {
1119
- listenPartialProfit,
1120
- listenPartialLoss,
1121
- listenPartialProfitOnce,
1122
- listenPartialLossOnce,
1123
- Constant
1124
- } from "backtest-kit";
1125
-
1126
- // Listen to all profit levels (10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%)
1127
- listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
1128
- console.log(`${symbol} profit: ${level}% at ${price}`);
1129
-
1130
- // Close portions at Kelly-optimized levels
1131
- if (level === Constant.TP_LEVEL3) {
1132
- console.log("Close 33% at 25% profit");
1133
- }
1134
- if (level === Constant.TP_LEVEL2) {
1135
- console.log("Close 33% at 50% profit");
1136
- }
1137
- if (level === Constant.TP_LEVEL1) {
1138
- console.log("Close 34% at 100% profit");
1139
- }
1140
- });
1141
-
1142
- // Listen to all loss levels (10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90%, 100%)
1143
- listenPartialLoss(({ symbol, signal, price, level, backtest }) => {
1144
- console.log(`${symbol} loss: -${level}% at ${price}`);
1145
-
1146
- // Close portions at stop levels
1147
- if (level === Constant.SL_LEVEL2) {
1148
- console.log("Close 50% at -50% loss");
1149
- }
1150
- if (level === Constant.SL_LEVEL1) {
1151
- console.log("Close 50% at -100% loss");
1152
- }
1153
- });
1154
-
1155
- // Listen once to first profit level reached
1156
- listenPartialProfitOnce(
1157
- () => true, // Accept any profit event
1158
- ({ symbol, signal, price, level, backtest }) => {
1159
- console.log(`First profit milestone: ${level}%`);
1160
- }
1161
- );
1162
-
1163
- // Listen once to first loss level reached
1164
- listenPartialLossOnce(
1165
- () => true, // Accept any loss event
1166
- ({ symbol, signal, price, level, backtest }) => {
1167
- console.log(`First loss milestone: -${level}%`);
1168
- }
1169
- );
1170
- ```
1171
-
1172
- #### Constant Utility - Kelly-Optimized Levels
1173
-
1174
- The `Constant` class provides predefined Kelly Criterion-based levels for optimal position sizing:
1175
-
1176
- ```typescript
1177
- import { Constant } from "backtest-kit";
1178
-
1179
- // Take Profit Levels
1180
- console.log(Constant.TP_LEVEL1); // 100% (aggressive target)
1181
- console.log(Constant.TP_LEVEL2); // 50% (moderate target)
1182
- console.log(Constant.TP_LEVEL3); // 25% (conservative target)
1183
-
1184
- // Stop Loss Levels
1185
- console.log(Constant.SL_LEVEL1); // 100% (maximum risk)
1186
- console.log(Constant.SL_LEVEL2); // 50% (standard stop)
1187
- ```
1188
-
1189
- **Use Case - Scale Out Strategy:**
1190
-
1191
- ```typescript
1192
- // Strategy: Close position in 3 tranches at optimal levels
1193
- listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
1194
- if (level === Constant.TP_LEVEL3) {
1195
- // Close 33% at 25% profit (secure early gains)
1196
- executePartialClose(symbol, signal.id, 0.33);
1197
- }
1198
- if (level === Constant.TP_LEVEL2) {
1199
- // Close 33% at 50% profit (lock in medium gains)
1200
- executePartialClose(symbol, signal.id, 0.33);
1201
- }
1202
- if (level === Constant.TP_LEVEL1) {
1203
- // Close 34% at 100% profit (maximize winners)
1204
- executePartialClose(symbol, signal.id, 0.34);
1205
- }
1206
- });
1207
- ```
1208
-
1209
- #### Partial Reports and Statistics
1210
-
1211
- The `Partial` utility provides access to accumulated partial profit/loss data:
1212
-
1213
- ```typescript
1214
- import { Partial } from "backtest-kit";
1215
-
1216
- // Get statistical data
1217
- const stats = await Partial.getData("BTCUSDT");
1218
- console.log(stats);
1219
- // Returns:
1220
- // {
1221
- // totalEvents: 15, // Total profit/loss events
1222
- // totalProfit: 10, // Number of profit events
1223
- // totalLoss: 5, // Number of loss events
1224
- // eventList: [
1225
- // {
1226
- // timestamp: 1704370800000,
1227
- // action: "PROFIT", // PROFIT or LOSS
1228
- // symbol: "BTCUSDT",
1229
- // signalId: "abc123",
1230
- // position: "LONG", // or SHORT
1231
- // level: 10, // Percentage level reached
1232
- // price: 51500.00, // Current price at level
1233
- // mode: "Backtest" // or Live
1234
- // },
1235
- // // ... more events
1236
- // ]
1237
- // }
1238
-
1239
- // Generate markdown report
1240
- const markdown = await Partial.getReport("BTCUSDT");
1241
- console.log(markdown);
1242
-
1243
- // Save report to disk (default: ./dump/partial/BTCUSDT.md)
1244
- await Partial.dump("BTCUSDT");
1245
-
1246
- // Custom output path
1247
- await Partial.dump("BTCUSDT", "./reports/partial");
1248
- ```
1249
-
1250
- **Partial Report Example:**
1251
-
1252
- ```markdown
1253
- # Partial Profit/Loss Report: BTCUSDT
1254
-
1255
- | Action | Symbol | Signal ID | Position | Level % | Current Price | Timestamp | Mode |
1256
- | --- | --- | --- | --- | --- | --- | --- | --- |
1257
- | PROFIT | BTCUSDT | abc123 | LONG | +10% | 51500.00000000 USD | 2024-01-15T10:30:00.000Z | Backtest |
1258
- | PROFIT | BTCUSDT | abc123 | LONG | +20% | 53000.00000000 USD | 2024-01-15T11:15:00.000Z | Backtest |
1259
- | LOSS | BTCUSDT | def456 | SHORT | -10% | 51500.00000000 USD | 2024-01-15T14:00:00.000Z | Backtest |
1260
-
1261
- **Total events:** 15
1262
- **Profit events:** 10
1263
- **Loss events:** 5
1264
- ```
1265
-
1266
- #### Strategy Callbacks
1267
-
1268
- Partial profit/loss callbacks can also be configured at the strategy level:
1269
-
1270
- ```typescript
1271
- import { addStrategy } from "backtest-kit";
1272
-
1273
- addStrategy({
1274
- strategyName: "my-strategy",
1275
- interval: "5m",
1276
- getSignal: async (symbol) => { /* ... */ },
1277
- callbacks: {
1278
- onPartialProfit: (symbol, data, currentPrice, revenuePercent, backtest) => {
1279
- console.log(`Signal ${data.id} at ${revenuePercent.toFixed(2)}% profit`);
1280
- },
1281
- onPartialLoss: (symbol, data, currentPrice, lossPercent, backtest) => {
1282
- console.log(`Signal ${data.id} at ${lossPercent.toFixed(2)}% loss`);
1283
- },
1284
- },
1285
- });
1286
- ```
1287
-
1288
- #### How Partial Levels Work
1289
-
1290
- **Architecture:**
1291
-
1292
- 1. `ClientPartial` - Tracks levels using `Map<signalId, Set<level>>` to prevent duplicates
1293
- 2. `ClientStrategy` - Calls `partial.profit()` / `partial.loss()` on every tick
1294
- 3. `PartialMarkdownService` - Accumulates events (max 250 per symbol) for reports
1295
- 4. State persisted to disk: `./dump/data/partial/{symbol}/levels.json`
1296
-
1297
- **Level Detection:**
1298
-
1299
- ```typescript
1300
- // For LONG position at entry price 50000
1301
- // Current price = 55000 β†’ revenue = 10%
1302
- // Levels triggered: 10%
1303
-
1304
- // Current price = 61000 β†’ revenue = 22%
1305
- // Levels triggered: 10%, 20% (only 20% event emitted if 10% already triggered)
1306
-
1307
- // For SHORT position at entry price 50000
1308
- // Current price = 45000 β†’ revenue = 10%
1309
- // Levels triggered: 10%
1310
- ```
1311
-
1312
- **Deduplication Guarantee:**
1313
-
1314
- Each level is emitted **exactly once per signal**:
1315
-
1316
- - Uses `Set<level>` to track reached levels
1317
- - Persisted to disk for crash recovery
1318
- - Restored on system restart
1319
-
1320
- **Crash Recovery:**
1321
-
1322
- ```typescript
1323
- // Before crash:
1324
- // Signal opened at 50000, reached 10% and 20% profit
1325
- // State: { profitLevels: [10, 20], lossLevels: [] }
1326
- // Persisted to: ./dump/data/partial/BTCUSDT/levels.json
1327
-
1328
- // After restart:
1329
- // State restored from disk
1330
- // Only new levels (30%, 40%, etc.) will emit events
1331
- // 10% and 20% won't fire again
1332
- ```
1333
-
1334
- #### Best Practices
1335
-
1336
- 1. **Use Constant for Kelly-Optimized Levels** - Don't hardcode profit/loss levels
1337
- 2. **Scale Out Gradually** - Close positions in tranches (25%, 50%, 100%)
1338
- 3. **Monitor Partial Statistics** - Use `Partial.getData()` to track scaling effectiveness
1339
- 4. **Filter Events** - Use `listenPartialProfitOnce` for first-level-only logic
1340
- 5. **Combine with Position Sizing** - Scale out inversely to volatility
1341
-
1342
- ```typescript
1343
- import { Constant, listenPartialProfit } from "backtest-kit";
1344
-
1345
- // Advanced: Dynamic scaling based on level
1346
- listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
1347
- const percentToClose =
1348
- level === Constant.TP_LEVEL3 ? 0.25 : // 25% at first level
1349
- level === Constant.TP_LEVEL2 ? 0.35 : // 35% at second level
1350
- level === Constant.TP_LEVEL1 ? 0.40 : // 40% at third level
1351
- 0;
1352
-
1353
- if (percentToClose > 0) {
1354
- executePartialClose(symbol, signal.id, percentToClose);
1355
- }
1356
- });
1357
- ```
1358
-
1359
- ---
1360
-
1361
- ### 11. Graceful Shutdown and Task Monitoring
1362
-
1363
- The framework provides graceful shutdown mechanisms and task monitoring for all execution modes: backtests, live trading, and walkers. This ensures clean termination without forced signal closures and visibility into running tasks.
1364
-
1365
- #### Task Status Monitoring
1366
-
1367
- Use `list()` method to monitor all running instances and their statuses:
1368
-
1369
- ```typescript
1370
- // Get list of all backtest instances
1371
- const backtests = await Backtest.list();
1372
- console.log(backtests);
1373
- // [
1374
- // { symbol: "BTCUSDT", strategyName: "strategy-1", status: "pending" },
1375
- // { symbol: "ETHUSDT", strategyName: "strategy-2", status: "fulfilled" }
1376
- // ]
1377
-
1378
- // Get list of all live trading instances
1379
- const liveInstances = await Live.list();
1380
- console.log(liveInstances);
1381
- // [
1382
- // { symbol: "BTCUSDT", strategyName: "my-strategy", status: "pending" }
1383
- // ]
1384
-
1385
- // Get list of all walker instances
1386
- const walkers = await Walker.list();
1387
- console.log(walkers);
1388
- // [
1389
- // { symbol: "BTCUSDT", walkerName: "btc-walker", status: "fulfilled" }
1390
- // ]
1391
- ```
1392
-
1393
- **Task Statuses:**
1394
- - `ready` - Task created but not yet started
1395
- - `pending` - Task is currently running
1396
- - `fulfilled` - Task completed successfully
1397
- - `rejected` - Task failed with an error
1398
-
1399
- #### How Graceful Shutdown Works
1400
-
1401
- When you call `Backtest.stop()`, `Live.stop()`, or `Walker.stop()`:
1402
-
1403
- 1. **Current Signal Completes** - Active signals finish normally (reach TP/SL or expire)
1404
- 2. **No New Signals** - Strategy stops generating new signals after stop is called
1405
- 3. **Callbacks Fire** - All lifecycle callbacks (`onClose`, etc.) execute as expected
1406
- 4. **Events Emitted** - Completion events (`listenDoneBacktest`, `listenDoneLive`, `listenWalkerComplete`) fire
1407
- 5. **State Persisted** - In live mode, final state is saved to disk
1408
- 6. **Status Updated** - Task status transitions from `pending` to `fulfilled`
1409
-
1410
- #### Backtest Shutdown
1411
-
1412
- ```typescript
1413
- import { Backtest, listenDoneBacktest } from "backtest-kit";
1414
-
1415
- Backtest.background("BTCUSDT", {
1416
- strategyName: "my-strategy",
1417
- exchangeName: "binance",
1418
- frameName: "1d-backtest"
1419
- });
1420
-
1421
- // Stop backtest gracefully
1422
- await Backtest.stop("BTCUSDT", "my-strategy");
1423
- // - Current active signal completes (TP/SL reached)
1424
- // - No new signals generated after stop
1425
- // - listenDoneBacktest event fires
1426
-
1427
- listenDoneBacktest((event) => {
1428
- console.log("Backtest stopped:", event.strategyName);
1429
- });
1430
- ```
1431
-
1432
- #### Live Trading Shutdown
1433
-
1434
- ```typescript
1435
- import { Live, listenDoneLive } from "backtest-kit";
1436
-
1437
- Live.background("BTCUSDT", {
1438
- strategyName: "my-strategy",
1439
- exchangeName: "binance"
1440
- });
1441
-
1442
- // Stop live trading gracefully
1443
- await Live.stop("BTCUSDT", "my-strategy");
1444
- // - Active signal completes normally (no forced close)
1445
- // - No new signals generated after stop
1446
- // - State persisted to disk for resume on restart
1447
-
1448
- listenDoneLive((event) => {
1449
- console.log("Live trading stopped:", event.strategyName);
1450
- });
1451
- ```
1452
-
1453
- #### Walker Shutdown (Early Termination)
1454
-
1455
- Walker shutdown is particularly useful for early termination when comparing strategies:
1456
-
1457
- ```typescript
1458
- import { addWalker, Walker, listenWalkerComplete } from "backtest-kit";
1459
-
1460
- addWalker({
1461
- walkerName: "btc-walker",
1462
- exchangeName: "binance",
1463
- frameName: "1d-backtest",
1464
- strategies: ["strategy-a", "strategy-b", "strategy-c"],
1465
- callbacks: {
1466
- onStrategyComplete: async (strategyName, symbol, stats) => {
1467
- console.log(`${strategyName} completed with Sharpe: ${stats.sharpeRatio}`);
1468
-
1469
- // Early termination: Stop walker if first strategy is good enough
1470
- if (stats.sharpeRatio > 2.0) {
1471
- console.log("Found excellent strategy, stopping walker early");
1472
- await Walker.stop("BTCUSDT", "btc-walker");
1473
- }
1474
- },
1475
- },
1476
- });
1477
-
1478
- Walker.background("BTCUSDT", {
1479
- walkerName: "btc-walker"
1480
- });
1481
-
1482
- // Monitor walker status
1483
- const walkers = await Walker.list();
1484
- console.log(walkers.find(w => w.walkerName === "btc-walker"));
1485
- // { symbol: "BTCUSDT", walkerName: "btc-walker", status: "pending" }
1486
-
1487
- // Or stop walker manually after some condition
1488
- await Walker.stop("BTCUSDT", "btc-walker");
1489
- // - Current strategy completes execution
1490
- // - Remaining strategies (not yet started) won't run
1491
- // - listenWalkerComplete fires with partial results
1492
- // - Status changes to "fulfilled"
1493
-
1494
- listenWalkerComplete((results) => {
1495
- console.log("Walker stopped early:", results.bestStrategy);
1496
- console.log(`Tested ${results.strategies.length}/3 strategies`);
1497
- });
1498
- ```
1499
-
1500
- #### Multiple Walkers on Same Symbol
1501
-
1502
- Walker shutdown only affects the specified walker, not others running on the same symbol:
1503
-
1504
- ```typescript
1505
- // Start two independent walkers on same symbol
1506
- Walker.background("BTCUSDT", { walkerName: "walker-A" });
1507
- Walker.background("BTCUSDT", { walkerName: "walker-B" });
1508
-
1509
- // Stop walker-A only
1510
- await Walker.stop("BTCUSDT", "walker-A");
1511
- // - walker-A stops gracefully
1512
- // - walker-B continues unaffected
1513
- ```
1514
-
1515
- #### Monitoring Multiple Instances
1516
-
1517
- Track multiple running instances across different symbols:
1518
-
1519
- ```typescript
1520
- // Start multiple backtests
1521
- Backtest.background("BTCUSDT", { strategyName: "strategy-1", ... });
1522
- Backtest.background("ETHUSDT", { strategyName: "strategy-2", ... });
1523
- Backtest.background("SOLUSDT", { strategyName: "strategy-3", ... });
1524
-
1525
- // Monitor all running backtests
1526
- const allBacktests = await Backtest.list();
1527
- console.log(`Running: ${allBacktests.filter(b => b.status === "pending").length}`);
1528
- console.log(`Completed: ${allBacktests.filter(b => b.status === "fulfilled").length}`);
1529
- console.log(`Failed: ${allBacktests.filter(b => b.status === "rejected").length}`);
1530
-
1531
- // Stop specific instance
1532
- await Backtest.stop("ETHUSDT", "strategy-2");
1533
-
1534
- // Verify status change
1535
- const updated = await Backtest.list();
1536
- const eth = updated.find(b => b.symbol === "ETHUSDT");
1537
- console.log(eth.status); // "fulfilled"
1538
- ```
1539
-
1540
- #### Use Cases
1541
-
1542
- 1. **Backtest Early Exit** - Stop backtest when strategy performs poorly (e.g., drawdown > 10%)
1543
- 2. **Live Trading Maintenance** - Gracefully stop live trading for system updates
1544
- 3. **Walker Optimization** - Skip remaining strategies when first one is excellent
1545
- 4. **Resource Management** - Stop long-running backtests to free up resources
1546
- 5. **Conditional Termination** - Stop based on external events (API limits, market conditions)
1547
- 6. **Task Monitoring Dashboard** - Build real-time monitoring UI with `list()` method
1548
- 7. **Health Checks** - Monitor task statuses for alerting and automation
1549
-
1550
- #### Best Practices
1551
-
1552
- 1. **Always await stop()** - Ensure graceful shutdown completes before exiting process
1553
- 2. **Use listenDone events** - Track completion with `listenDoneBacktest`, `listenDoneLive`, `listenWalkerComplete`
1554
- 3. **Monitor task status** - Use `list()` method to track running instances and their states
1555
- 4. **Don't force-kill** - Let signals complete naturally instead of process.exit()
1556
- 5. **Save reports** - Call `dump()` methods before stopping to preserve results
1557
- 6. **Test shutdown paths** - Write tests that verify graceful shutdown behavior
1558
- 7. **Handle rejected status** - Check for `rejected` status and handle errors appropriately
1559
-
1560
- ```typescript
1561
- // GOOD - Graceful shutdown with cleanup and monitoring
1562
- const instances = await Backtest.list();
1563
- console.log(`Running instances: ${instances.filter(i => i.status === "pending").length}`);
1564
-
1565
- await Backtest.stop("BTCUSDT", "my-strategy");
1566
- await Backtest.dump("my-strategy"); // Save report
1567
-
1568
- // Verify status change
1569
- const updated = await Backtest.list();
1570
- const stopped = updated.find(i => i.symbol === "BTCUSDT");
1571
- console.log(`Status: ${stopped.status}`); // "fulfilled"
1572
- console.log("Shutdown complete");
1573
-
1574
- // BAD - Forced exit without cleanup
1575
- process.exit(0); // Signals may not complete, callbacks may not fire
1576
- ```
1577
-
1578
- #### Live Trading Considerations
1579
-
1580
- **Important:** Live mode operates on real-time data with actual minute intervals. When testing graceful shutdown in live mode:
1581
-
1582
- - Expect longer wait times (minimum 1 minute per candle)
1583
- - Use backtest mode for quick iteration and testing
1584
- - Live mode is designed for production trading, not rapid testing
1585
- - Status transitions may take longer due to real-time constraints
1586
-
1587
- ---
1588
-
1589
- ### 12. Scheduled Signal Persistence
1590
-
1591
- 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.
1592
-
1593
- #### Understanding the Dual Persistence System
1594
-
1595
- The library uses **two independent persistence layers** for signals:
1596
-
1597
- 1. **PersistSignalAdapter** - Manages pending/active signals (signals that are already opened or waiting to reach TP/SL)
1598
- 2. **PersistScheduleAdapter** - Manages scheduled signals (signals waiting for entry price to activate)
1599
-
1600
- 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).
1601
-
1602
- #### Default Storage Structure
1603
-
1604
- By default, scheduled signals are stored separately from pending signals:
1605
-
1606
- ```
1607
- ./dump/data/
1608
- signal/
1609
- my-strategy/
1610
- BTCUSDT.json # Pending/active signal state
1611
- ETHUSDT.json
1612
- schedule/
1613
- my-strategy/
1614
- BTCUSDT.json # Scheduled signal state
1615
- ETHUSDT.json
1616
- ```
1617
-
1618
- #### How Scheduled Signal Persistence Works
1619
-
1620
- **During Normal Operation:**
1621
-
1622
- When a strategy generates a scheduled signal (limit order waiting for entry), the framework:
1623
-
1624
- 1. Stores the signal to disk using atomic writes: `./dump/data/schedule/{strategyName}/{symbol}.json`
1625
- 2. Monitors price movements for activation
1626
- 3. When price reaches entry point OR cancellation condition occurs:
1627
- - Deletes scheduled signal from storage
1628
- - Optionally creates pending signal in `PersistSignalAdapter`
1629
-
1630
- **After System Crash:**
1631
-
1632
- When the system restarts:
1633
-
1634
- 1. Framework checks for stored scheduled signals during initialization
1635
- 2. Validates exchange name and strategy name match (security protection)
1636
- 3. Restores scheduled signal to memory (`_scheduledSignal`)
1637
- 4. Calls `onSchedule()` callback to notify about restored signal
1638
- 5. Continues monitoring from where it left off
1639
-
1640
- **Crash Recovery Flow:**
1641
-
1642
- ```typescript
1643
- // Before crash:
1644
- // 1. Strategy generates signal with priceOpen = 50000 (current price = 49500)
1645
- // 2. Signal stored to ./dump/data/schedule/my-strategy/BTCUSDT.json
1646
- // 3. System waits for price to reach 50000
1647
- // 4. CRASH OCCURS at current price = 49800
1648
-
1649
- // After restart:
1650
- // 1. System reads ./dump/data/schedule/my-strategy/BTCUSDT.json
1651
- // 2. Validates exchangeName and strategyName
1652
- // 3. Restores signal to _scheduledSignal
1653
- // 4. Calls onSchedule() callback with restored signal
1654
- // 5. Continues monitoring for price = 50000
1655
- // 6. When price reaches 50000, signal activates normally
1656
- ```
1657
-
1658
- #### Scheduled Signal Data Structure
1659
-
1660
- ```typescript
1661
- interface IScheduledSignalRow {
1662
- id: string; // Unique signal ID
1663
- position: "long" | "short";
1664
- priceOpen: number; // Entry price (trigger price for scheduled signal)
1665
- priceTakeProfit: number;
1666
- priceStopLoss: number;
1667
- minuteEstimatedTime: number;
1668
- exchangeName: string; // Used for validation during restore
1669
- strategyName: string; // Used for validation during restore
1670
- timestamp: number;
1671
- pendingAt: number;
1672
- scheduledAt: number;
1673
- symbol: string;
1674
- _isScheduled: true; // Marker for scheduled signals
1675
- note?: string;
1676
- }
1677
- ```
1678
-
1679
- #### Integration with ClientStrategy
1680
-
1681
- The `ClientStrategy` class uses `setScheduledSignal()` method to ensure all scheduled signal changes are persisted:
1682
-
1683
- ```typescript
1684
- // WRONG - Direct assignment (not persisted)
1685
- this._scheduledSignal = newSignal;
1686
-
1687
- // CORRECT - Using setScheduledSignal() method (persisted)
1688
- await this.setScheduledSignal(newSignal);
1689
- ```
1690
-
1691
- **Automatic Persistence Locations:**
1692
-
1693
- All scheduled signal state changes are automatically persisted:
1694
-
1695
- - Signal generation (new scheduled signal created)
1696
- - Signal activation (scheduled β†’ pending transition)
1697
- - Signal cancellation (timeout or stop loss hit before activation)
1698
- - Manual signal clearing
1699
-
1700
- **BACKTEST Mode Exception:**
1701
-
1702
- In backtest mode, persistence is **skipped** for performance reasons:
1703
-
1704
- ```typescript
1705
- public async setScheduledSignal(scheduledSignal: IScheduledSignalRow | null) {
1706
- this._scheduledSignal = scheduledSignal;
1707
-
1708
- if (this.params.execution.context.backtest) {
1709
- return; // Skip persistence in backtest mode
1710
- }
1711
-
1712
- await PersistScheduleAdapter.writeScheduleData(
1713
- this._scheduledSignal,
1714
- this.params.strategyName,
1715
- this.params.execution.context.symbol
1716
- );
1717
- }
1718
- ```
1719
-
1720
- #### Custom Scheduled Signal Adapters
1721
-
1722
- You can replace file-based scheduled signal persistence with custom adapters (Redis, MongoDB, etc.):
1723
-
1724
- ```typescript
1725
- import { PersistScheduleAdapter, PersistBase } from "backtest-kit";
1726
- import Redis from "ioredis";
1727
-
1728
- const redis = new Redis();
1729
-
1730
- class RedisSchedulePersist extends PersistBase {
1731
- async waitForInit(initial: boolean): Promise<void> {
1732
- console.log(`Redis scheduled signal persistence initialized for ${this.entityName}`);
1733
- }
1734
-
1735
- async readValue<T>(entityId: string | number): Promise<T> {
1736
- const key = `schedule:${this.entityName}:${entityId}`;
1737
- const data = await redis.get(key);
1738
-
1739
- if (!data) {
1740
- throw new Error(`Scheduled signal ${this.entityName}:${entityId} not found`);
1741
- }
1742
-
1743
- return JSON.parse(data) as T;
1744
- }
1745
-
1746
- async hasValue(entityId: string | number): Promise<boolean> {
1747
- const key = `schedule:${this.entityName}:${entityId}`;
1748
- const exists = await redis.exists(key);
1749
- return exists === 1;
1750
- }
1751
-
1752
- async writeValue<T>(entityId: string | number, entity: T): Promise<void> {
1753
- const key = `schedule:${this.entityName}:${entityId}`;
1754
- const serializedData = JSON.stringify(entity);
1755
- await redis.set(key, serializedData);
1756
-
1757
- // Optional: Set TTL for scheduled signals (e.g., 24 hours)
1758
- await redis.expire(key, 86400);
1759
- }
1760
-
1761
- async removeValue(entityId: string | number): Promise<void> {
1762
- const key = `schedule:${this.entityName}:${entityId}`;
1763
- const result = await redis.del(key);
1764
-
1765
- if (result === 0) {
1766
- throw new Error(`Scheduled signal ${this.entityName}:${entityId} not found for deletion`);
1767
- }
1768
- }
1769
-
1770
- async removeAll(): Promise<void> {
1771
- const pattern = `schedule:${this.entityName}:*`;
1772
- const keys = await redis.keys(pattern);
1773
-
1774
- if (keys.length > 0) {
1775
- await redis.del(...keys);
1776
- }
1777
- }
1778
-
1779
- async *values<T>(): AsyncGenerator<T> {
1780
- const pattern = `schedule:${this.entityName}:*`;
1781
- const keys = await redis.keys(pattern);
1782
-
1783
- keys.sort((a, b) => a.localeCompare(b, undefined, {
1784
- numeric: true,
1785
- sensitivity: "base"
1786
- }));
1787
-
1788
- for (const key of keys) {
1789
- const data = await redis.get(key);
1790
- if (data) {
1791
- yield JSON.parse(data) as T;
1792
- }
1793
- }
1794
- }
1795
-
1796
- async *keys(): AsyncGenerator<string> {
1797
- const pattern = `schedule:${this.entityName}:*`;
1798
- const keys = await redis.keys(pattern);
1799
-
1800
- keys.sort((a, b) => a.localeCompare(b, undefined, {
1801
- numeric: true,
1802
- sensitivity: "base"
1803
- }));
1804
-
1805
- for (const key of keys) {
1806
- const entityId = key.slice(`schedule:${this.entityName}:`.length);
1807
- yield entityId;
1808
- }
1809
- }
1810
- }
1811
-
1812
- // Register Redis adapter for scheduled signal persistence
1813
- PersistScheduleAdapter.usePersistScheduleAdapter(RedisSchedulePersist);
1814
- ```
1815
-
1816
- #### Best Practices
1817
-
1818
- 1. **Always use `setScheduledSignal()`** - Never assign `_scheduledSignal` directly (except in `waitForInit` for restoration)
1819
-
1820
- 2. **Validate signal metadata** - Always store `exchangeName` and `strategyName` with signals for validation
1821
-
1822
- 3. **Handle empty storage gracefully** - Don't crash when `readScheduleData()` returns `null`
1823
-
1824
- 4. **Test crash recovery** - Write E2E tests that simulate system crashes and verify restoration
1825
-
1826
- 5. **Choose persistence adapter wisely**:
1827
- - Use file-based for single-instance deployments
1828
- - Use Redis for distributed systems with multiple instances
1829
- - Use MongoDB for analytics and complex queries
1830
-
1831
- 6. **Monitor persistence operations** - Use callbacks to track storage operations:
1832
-
1833
- ```typescript
1834
- addStrategy({
1835
- strategyName: "my-strategy",
1836
- interval: "5m",
1837
- getSignal: async (symbol) => { /* ... */ },
1838
- callbacks: {
1839
- onSchedule: (symbol, signal, price, backtest) => {
1840
- console.log(`Scheduled signal created/restored: ${signal.id}`);
1841
- // Signal was either:
1842
- // 1. Newly generated and persisted
1843
- // 2. Restored from storage after crash
1844
- },
1845
- onCancel: (symbol, signal, price, backtest) => {
1846
- console.log(`Scheduled signal cancelled: ${signal.id}`);
1847
- // Signal was removed from storage
1848
- },
1849
- },
1850
- });
1851
- ```
1852
-
1853
- ---
1854
-
1855
- ## πŸ€– AI Strategy Optimizer
1856
-
1857
- The Optimizer uses LLM (Large Language Models) to generate trading strategies from historical data. It automates the process of analyzing backtest results, generating strategy logic, and creating executable code.
1858
-
1859
- ### How It Works
1860
-
1861
- 1. **Data Collection**: Fetch historical data from multiple sources (backtest results, market data, indicators)
1862
- 2. **LLM Training**: Build conversation context with data for each training period
1863
- 3. **Strategy Generation**: LLM analyzes patterns and generates strategy logic
1864
- 4. **Code Export**: Auto-generate complete executable code with Walker for testing
1865
-
1866
- ### Basic Example
1867
-
1868
- ```typescript
1869
- import { addOptimizer, Optimizer } from "backtest-kit";
1870
-
1871
- // Register optimizer configuration
1872
- addOptimizer({
1873
- optimizerName: "btc-optimizer",
1874
-
1875
- // Training periods (multiple strategies generated)
1876
- rangeTrain: [
1877
- {
1878
- note: "Bull market Q1 2024",
1879
- startDate: new Date("2024-01-01T00:00:00Z"),
1880
- endDate: new Date("2024-03-31T23:59:59Z"),
1881
- },
1882
- {
1883
- note: "Consolidation Q2 2024",
1884
- startDate: new Date("2024-04-01T00:00:00Z"),
1885
- endDate: new Date("2024-06-30T23:59:59Z"),
1886
- },
1887
- ],
1888
-
1889
- // Testing period (Walker validates strategies)
1890
- rangeTest: {
1891
- note: "Validation Q3 2024",
1892
- startDate: new Date("2024-07-01T00:00:00Z"),
1893
- endDate: new Date("2024-09-30T23:59:59Z"),
1894
- },
1895
-
1896
- // Data sources for strategy generation
1897
- source: [
1898
- {
1899
- name: "backtest-results",
1900
- fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
1901
- // Fetch closed signals from your backtest database
1902
- return await db.getBacktestResults({
1903
- symbol,
1904
- startDate,
1905
- endDate,
1906
- limit,
1907
- offset,
1908
- });
1909
- },
1910
- },
1911
- {
1912
- name: "market-indicators",
1913
- fetch: async ({ symbol, startDate, endDate, limit, offset }) => {
1914
- // Fetch RSI, MACD, volume data, etc.
1915
- return await db.getIndicators({
1916
- symbol,
1917
- startDate,
1918
- endDate,
1919
- limit,
1920
- offset,
1921
- });
1922
- },
1923
- },
1924
- ],
1925
-
1926
- // LLM prompt generation from conversation history
1927
- getPrompt: async (symbol, messages) => {
1928
- // Analyze messages and create strategy prompt
1929
- return `
1930
- Based on the historical data, create a strategy that:
1931
- - Uses multi-timeframe analysis (1h, 15m, 5m, 1m)
1932
- - Identifies high-probability entry points
1933
- - Uses proper risk/reward ratios (min 1.5:1)
1934
- - Adapts to market conditions
1935
- `;
1936
- },
1937
- });
1938
-
1939
- // Generate strategies and export code
1940
- await Optimizer.dump("BTCUSDT", {
1941
- optimizerName: "btc-optimizer"
1942
- }, "./generated");
1943
-
1944
- // Output: ./generated/btc-optimizer_BTCUSDT.mjs
1945
- ```
1946
-
1947
- ### Generated Code Structure
1948
-
1949
- The Optimizer auto-generates a complete executable file with:
1950
-
1951
- 1. **Imports** - All necessary dependencies (backtest-kit, ollama, ccxt)
1952
- 2. **Helper Functions**:
1953
- - `text()` - LLM text generation for analysis
1954
- - `json()` - Structured signal generation with schema validation
1955
- - `dumpJson()` - Debug logging to ./dump/strategy
1956
- 3. **Exchange Configuration** - CCXT Binance integration
1957
- 4. **Frame Definitions** - Training and testing timeframes
1958
- 5. **Strategy Implementations** - One per training range with multi-timeframe analysis
1959
- 6. **Walker Setup** - Automatic strategy comparison on test period
1960
- 7. **Event Listeners** - Progress tracking and result collection
1961
-
1962
- ### Advanced Configuration
1963
-
1964
- #### Custom Message Templates
1965
-
1966
- Override default LLM message formatting:
1967
-
1968
- ```typescript
1969
- addOptimizer({
1970
- optimizerName: "custom-optimizer",
1971
- rangeTrain: [...],
1972
- rangeTest: {...},
1973
- source: [...],
1974
- getPrompt: async (symbol, messages) => "...",
1975
-
1976
- // Custom templates
1977
- template: {
1978
- getUserMessage: async (symbol, data, sourceName) => {
1979
- return `Analyze ${sourceName} data for ${symbol}:\n${JSON.stringify(data, null, 2)}`;
1980
- },
1981
- getAssistantMessage: async (symbol, data, sourceName) => {
1982
- return `Data from ${sourceName} analyzed successfully`;
1983
- },
1984
- },
1985
- });
1986
- ```
1987
-
1988
- #### Lifecycle Callbacks
1989
-
1990
- Monitor optimizer operations:
1991
-
1992
- ```typescript
1993
- addOptimizer({
1994
- optimizerName: "monitored-optimizer",
1995
- rangeTrain: [...],
1996
- rangeTest: {...},
1997
- source: [...],
1998
- getPrompt: async (symbol, messages) => "...",
1999
-
2000
- callbacks: {
2001
- onSourceData: async (symbol, sourceName, data, startDate, endDate) => {
2002
- console.log(`Fetched ${data.length} rows from ${sourceName}`);
2003
- },
2004
- onData: async (symbol, strategyData) => {
2005
- console.log(`Generated ${strategyData.length} strategies for ${symbol}`);
2006
- },
2007
- onCode: async (symbol, code) => {
2008
- console.log(`Generated ${code.length} bytes of code`);
2009
- },
2010
- onDump: async (symbol, filepath) => {
2011
- console.log(`Saved strategy to ${filepath}`);
2012
- },
2013
- },
2014
- });
2015
- ```
2016
-
2017
- #### Multiple Data Sources
2018
-
2019
- Combine different data types for comprehensive analysis:
2020
-
2021
- ```typescript
2022
- addOptimizer({
2023
- optimizerName: "multi-source-optimizer",
2024
- rangeTrain: [...],
2025
- rangeTest: {...},
2026
-
2027
- source: [
2028
- // Source 1: Backtest results
2029
- {
2030
- name: "backtest-signals",
2031
- fetch: async (args) => await getBacktestSignals(args),
2032
- },
2033
-
2034
- // Source 2: Market indicators
2035
- {
2036
- name: "technical-indicators",
2037
- fetch: async (args) => await getTechnicalIndicators(args),
2038
- },
2039
-
2040
- // Source 3: Volume profile
2041
- {
2042
- name: "volume-analysis",
2043
- fetch: async (args) => await getVolumeProfile(args),
2044
- },
2045
-
2046
- // Source 4: Order book depth
2047
- {
2048
- name: "order-book",
2049
- fetch: async (args) => await getOrderBookData(args),
2050
- },
2051
- ],
2052
-
2053
- getPrompt: async (symbol, messages) => {
2054
- // LLM has full context from all sources
2055
- return `Create strategy using all available data sources`;
2056
- },
2057
- });
2058
- ```
2059
-
2060
- ### API Methods
2061
-
2062
- ```typescript
2063
- // Get strategy metadata (no code generation)
2064
- const strategies = await Optimizer.getData("BTCUSDT", {
2065
- optimizerName: "my-optimizer"
2066
- });
2067
-
2068
- // strategies[0].messages - LLM conversation history
2069
- // strategies[0].strategy - Generated strategy prompt
2070
-
2071
- // Generate executable code
2072
- const code = await Optimizer.getCode("BTCUSDT", {
2073
- optimizerName: "my-optimizer"
2074
- });
2075
-
2076
- // Save to file
2077
- await Optimizer.dump("BTCUSDT", {
2078
- optimizerName: "my-optimizer"
2079
- }, "./output"); // Default: "./"
2080
- ```
2081
-
2082
- ### LLM Integration
2083
-
2084
- The Optimizer uses Ollama for LLM inference:
2085
-
2086
- ```bash
2087
- # Set up Ollama API
2088
- export OLLAMA_API_KEY=your-api-key
2089
-
2090
- # Run generated strategy
2091
- node generated/btc-optimizer_BTCUSDT.mjs
2092
- ```
2093
-
2094
- Generated strategies use:
2095
- - **Model**: `gpt-oss:20b` (configurable in templates)
2096
- - **Multi-timeframe analysis**: 1h, 15m, 5m, 1m candles
2097
- - **Structured output**: JSON schema with signal validation
2098
- - **Debug logging**: Saves conversations to ./dump/strategy
2099
-
2100
- ### Best Practices
2101
-
2102
- 1. **Training Periods**: Use 2-4 diverse market conditions (bull, bear, sideways)
2103
- 2. **Data Quality**: Ensure data sources have unique IDs for deduplication
2104
- 3. **Pagination**: Sources automatically paginated (25 records per request)
2105
- 4. **Testing**: Always validate generated strategies on unseen data (rangeTest)
2106
- 5. **Monitoring**: Use callbacks to track data fetching and code generation
2107
-
2108
- ---
2109
-
2110
- ## πŸ“ Architecture Overview
2111
-
2112
- The framework follows **clean architecture** with:
2113
-
2114
- - **Client Layer** - Pure business logic without DI (ClientStrategy, ClientExchange, ClientFrame)
2115
- - **Service Layer** - DI-based services organized by responsibility
2116
- - **Schema Services** - Registry pattern for configuration
2117
- - **Connection Services** - Memoized client instance creators
2118
- - **Global Services** - Context wrappers for public API
2119
- - **Logic Services** - Async generator orchestration (backtest/live)
2120
- - **Persistence Layer** - Crash-safe atomic file writes with `PersistSignalAdapter`
2121
-
2122
- ---
2123
-
2124
- ## βœ… Signal Validation
2125
-
2126
- All signals are validated automatically before execution:
2127
-
2128
- ```typescript
2129
- // βœ… Valid long signal
2130
- {
2131
- position: "long",
2132
- priceOpen: 50000,
2133
- priceTakeProfit: 51000, // βœ… 51000 > 50000
2134
- priceStopLoss: 49000, // βœ… 49000 < 50000
2135
- minuteEstimatedTime: 60, // βœ… positive
2136
- }
2137
-
2138
- // ❌ Invalid long signal - throws error
2139
- {
2140
- position: "long",
2141
- priceOpen: 50000,
2142
- priceTakeProfit: 49000, // ❌ 49000 < 50000 (must be higher for long)
2143
- priceStopLoss: 51000, // ❌ 51000 > 50000 (must be lower for long)
2144
- }
2145
-
2146
- // βœ… Valid short signal
2147
- {
2148
- position: "short",
2149
- priceOpen: 50000,
2150
- priceTakeProfit: 49000, // βœ… 49000 < 50000 (profit goes down for short)
2151
- priceStopLoss: 51000, // βœ… 51000 > 50000 (stop loss goes up for short)
2152
- }
2153
- ```
2154
-
2155
- Validation errors include detailed messages for debugging.
2156
-
2157
- ---
2158
-
2159
- ## 🧠 Interval Throttling
2160
-
2161
- Prevent signal spam with automatic throttling:
2162
-
2163
- ```typescript
2164
- addStrategy({
2165
- strategyName: "my-strategy",
2166
- interval: "5m", // Signals generated max once per 5 minutes
2167
- getSignal: async (symbol) => {
2168
- // This function will be called max once per 5 minutes
2169
- // Even if tick() is called every second
2170
- return signal;
2171
- },
2172
- });
2173
- ```
2174
-
2175
- Supported intervals: `"1m"`, `"3m"`, `"5m"`, `"15m"`, `"30m"`, `"1h"`
2176
-
2177
- ---
2178
-
2179
- ## πŸ“ Markdown Reports
2180
-
2181
- Generate detailed trading reports with statistics:
2182
-
2183
- ### Backtest Reports
2184
-
2185
- ```typescript
2186
- import { Backtest } from "backtest-kit";
2187
-
2188
- // Get raw statistical data (Controller)
2189
- const stats = await Backtest.getData("my-strategy");
2190
- console.log(stats);
2191
- // Returns:
2192
- // {
2193
- // signalList: [...], // All closed signals
2194
- // totalSignals: 10,
2195
- // winCount: 7,
2196
- // lossCount: 3,
2197
- // winRate: 70.0, // Percentage (higher is better)
2198
- // avgPnl: 1.23, // Average PNL % (higher is better)
2199
- // totalPnl: 12.30, // Total PNL % (higher is better)
2200
- // stdDev: 2.45, // Standard deviation (lower is better)
2201
- // sharpeRatio: 0.50, // Risk-adjusted return (higher is better)
2202
- // annualizedSharpeRatio: 9.55, // Sharpe Γ— √365 (higher is better)
2203
- // certaintyRatio: 1.75, // avgWin / |avgLoss| (higher is better)
2204
- // expectedYearlyReturns: 156 // Estimated yearly trades (higher is better)
2205
- // }
2206
-
2207
- // Generate markdown report (View)
2208
- const markdown = await Backtest.getReport("my-strategy");
2209
-
2210
- // Save to disk (default: ./logs/backtest/my-strategy.md)
2211
- await Backtest.dump("my-strategy");
2212
- ```
2213
-
2214
- ### Live Trading Reports
2215
-
2216
- ```typescript
2217
- import { Live } from "backtest-kit";
2218
-
2219
- // Get raw statistical data (Controller)
2220
- const stats = await Live.getData("my-strategy");
2221
- console.log(stats);
2222
- // Returns:
2223
- // {
2224
- // eventList: [...], // All events (idle, scheduled, opened, active, closed, cancelled)
2225
- // totalEvents: 15,
2226
- // totalClosed: 5,
2227
- // winCount: 3,
2228
- // lossCount: 2,
2229
- // winRate: 60.0, // Percentage (higher is better)
2230
- // avgPnl: 1.23, // Average PNL % (higher is better)
2231
- // totalPnl: 6.15, // Total PNL % (higher is better)
2232
- // stdDev: 1.85, // Standard deviation (lower is better)
2233
- // sharpeRatio: 0.66, // Risk-adjusted return (higher is better)
2234
- // annualizedSharpeRatio: 12.61,// Sharpe Γ— √365 (higher is better)
2235
- // certaintyRatio: 2.10, // avgWin / |avgLoss| (higher is better)
2236
- // expectedYearlyReturns: 365 // Estimated yearly trades (higher is better)
2237
- // }
2238
-
2239
- // Generate markdown report (View)
2240
- const markdown = await Live.getReport("my-strategy");
2241
-
2242
- // Save to disk (default: ./logs/live/my-strategy.md)
2243
- await Live.dump("my-strategy");
2244
- ```
2245
-
2246
- ### Scheduled Signals Reports
2247
-
2248
- ```typescript
2249
- import { Schedule } from "backtest-kit";
2250
-
2251
- // Get raw scheduled signals data (Controller)
2252
- const stats = await Schedule.getData("my-strategy");
2253
- console.log(stats);
2254
- // Returns:
2255
- // {
2256
- // eventList: [...], // All scheduled/cancelled events
2257
- // totalEvents: 8,
2258
- // totalScheduled: 6, // Number of scheduled signals
2259
- // totalCancelled: 2, // Number of cancelled signals
2260
- // cancellationRate: 33.33, // Percentage (lower is better)
2261
- // avgWaitTime: 45.5, // Average wait time for cancelled signals in minutes
2262
- // }
2263
-
2264
- // Generate markdown report (View)
2265
- const markdown = await Schedule.getReport("my-strategy");
2266
-
2267
- // Save to disk (default: ./logs/schedule/my-strategy.md)
2268
- await Schedule.dump("my-strategy");
2269
-
2270
- // Clear accumulated data
2271
- await Schedule.clear("my-strategy");
2272
- ```
2273
-
2274
- **Scheduled Signals Report Example:**
2275
- ```markdown
2276
- # Scheduled Signals Report: my-strategy
2277
-
2278
- | Timestamp | Action | Symbol | Signal ID | Position | Note | Current Price | Entry Price | Take Profit | Stop Loss | Wait Time (min) |
2279
- |-----------|--------|--------|-----------|----------|------|---------------|-------------|-------------|-----------|-----------------|
2280
- | 2024-01-15T10:30:00Z | SCHEDULED | BTCUSDT | sig-001 | LONG | BTC breakout | 42150.50 USD | 42000.00 USD | 43000.00 USD | 41000.00 USD | N/A |
2281
- | 2024-01-15T10:35:00Z | CANCELLED | BTCUSDT | sig-002 | LONG | BTC breakout | 42350.80 USD | 10000.00 USD | 11000.00 USD | 9000.00 USD | 60 |
2282
-
2283
- **Total events:** 8
2284
- **Scheduled signals:** 6
2285
- **Cancelled signals:** 2
2286
- **Cancellation rate:** 33.33% (lower is better)
2287
- **Average wait time (cancelled):** 45.50 minutes
2288
- ```
2289
-
2290
- ---
2291
-
2292
- ## 🎧 Event Listeners
2293
-
2294
- ### Listen to All Signals (Backtest + Live)
2295
-
2296
- ```typescript
2297
- import { listenSignal } from "backtest-kit";
2298
-
2299
- // Listen to both backtest and live signals
2300
- listenSignal((event) => {
2301
- console.log(`[${event.backtest ? "BT" : "LIVE"}] ${event.action}:`, event.signal.id);
2302
-
2303
- if (event.action === "closed") {
2304
- console.log("PNL:", event.pnl.pnlPercentage);
2305
- console.log("Close reason:", event.closeReason);
2306
- }
2307
- });
2308
- ```
2309
-
2310
- ### Listen to Partial Profit/Loss Events
2311
-
2312
- ```typescript
2313
- import {
2314
- listenPartialProfit,
2315
- listenPartialLoss,
2316
- listenPartialProfitOnce,
2317
- listenPartialLossOnce,
2318
- Constant
2319
- } from "backtest-kit";
2320
-
2321
- // Listen to all profit milestones
2322
- listenPartialProfit(({ symbol, signal, price, level, backtest }) => {
2323
- console.log(`${symbol} reached ${level}% profit at ${price}`);
2324
-
2325
- // Scale out at Kelly-optimized levels
2326
- if (level === Constant.TP_LEVEL3) {
2327
- console.log("Close 33% at 25% profit");
2328
- }
2329
- if (level === Constant.TP_LEVEL2) {
2330
- console.log("Close 33% at 50% profit");
2331
- }
2332
- if (level === Constant.TP_LEVEL1) {
2333
- console.log("Close 34% at 100% profit");
2334
- }
2335
- });
2336
-
2337
- // Listen to all loss milestones
2338
- listenPartialLoss(({ symbol, signal, price, level, backtest }) => {
2339
- console.log(`${symbol} reached -${level}% loss at ${price}`);
2340
-
2341
- // Scale out at stop levels
2342
- if (level === Constant.SL_LEVEL2) {
2343
- console.log("Close 50% at -50% loss");
2344
- }
2345
- if (level === Constant.SL_LEVEL1) {
2346
- console.log("Close 50% at -100% loss");
2347
- }
2348
- });
2349
-
2350
- // Listen once to first profit level
2351
- listenPartialProfitOnce(
2352
- () => true, // Accept any profit event
2353
- ({ symbol, signal, price, level, backtest }) => {
2354
- console.log(`First profit milestone: ${level}%`);
2355
- }
2356
- );
2357
-
2358
- // Listen once to first loss level
2359
- listenPartialLossOnce(
2360
- () => true, // Accept any loss event
2361
- ({ symbol, signal, price, level, backtest }) => {
2362
- console.log(`First loss milestone: -${level}%`);
2363
- }
2364
- );
2365
- ```
2366
-
2367
- ### Listen Once with Filter
2368
-
2369
- ```typescript
2370
- import { listenSignalOnce, listenSignalLiveOnce } from "backtest-kit";
2371
-
2372
- // Listen once with filter
2373
- listenSignalOnce(
2374
- (event) => event.action === "closed" && event.pnl.pnlPercentage > 5,
2375
- (event) => {
2376
- console.log("Big win detected:", event.pnl.pnlPercentage);
2377
- }
2378
- );
2379
-
2380
- // Listen once for specific symbol in live mode
2381
- listenSignalLiveOnce(
2382
- (event) => event.signal.symbol === "BTCUSDT" && event.action === "opened",
2383
- (event) => {
2384
- console.log("BTC signal opened:", event.signal.id);
2385
- }
2386
- );
2387
- ```
2388
-
2389
- ### Listen to Background Completion
2390
-
2391
- ```typescript
2392
- import { listenDoneBacktest, listenDoneLive, listenDoneWalker } from "backtest-kit";
2393
-
2394
- // Backtest completion
2395
- listenDoneBacktest((event) => {
2396
- console.log("Backtest completed:", event.strategyName);
2397
- console.log("Symbol:", event.symbol);
2398
- console.log("Exchange:", event.exchangeName);
2399
- });
2400
-
2401
- // Live trading completion
2402
- listenDoneLive((event) => {
2403
- console.log("Live trading stopped:", event.strategyName);
2404
- });
2405
-
2406
- // Walker completion
2407
- listenDoneWalker((event) => {
2408
- console.log("Walker completed:", event.strategyName);
2409
- console.log("Best strategy:", event.bestStrategy);
2410
- });
2411
- ```
2412
-
2413
- ---
2414
-
2415
- ## βš™οΈ Global Configuration
2416
-
2417
- You can customize framework behavior using the `setConfig()` function. This allows you to adjust global parameters without modifying the source code.
2418
-
2419
- ### Available Configuration Options
2420
-
2421
- ```typescript
2422
- import { setConfig } from "backtest-kit";
2423
-
2424
- // Configure global parameters
2425
- await setConfig({
2426
- // Time to wait for scheduled signal activation (in minutes)
2427
- // If a scheduled signal doesn't activate within this time, it will be cancelled
2428
- // Default: 120 minutes
2429
- CC_SCHEDULE_AWAIT_MINUTES: 90,
2430
-
2431
- // Number of candles to use for average price calculation (VWAP)
2432
- // Used in both backtest and live modes for price calculations
2433
- // Default: 5 candles (last 5 minutes when using 1m interval)
2434
- CC_AVG_PRICE_CANDLES_COUNT: 10,
2435
- });
2436
- ```
2437
-
2438
- ### Configuration Parameters
2439
-
2440
- #### `CC_SCHEDULE_AWAIT_MINUTES`
2441
-
2442
- Controls how long scheduled signals wait for activation before being cancelled.
2443
-
2444
- - **Default:** `120` minutes (2 hours)
2445
- - **Use case:** Adjust based on market volatility and strategy timeframe
2446
- - **Example:** Lower for scalping strategies (30-60 min), higher for swing trading (180-360 min)
2447
-
2448
- ```typescript
2449
- // For scalping strategies with tight entry windows
2450
- await setConfig({
2451
- CC_SCHEDULE_AWAIT_MINUTES: 30,
2452
- });
2453
-
2454
- // For swing trading with wider entry windows
2455
- await setConfig({
2456
- CC_SCHEDULE_AWAIT_MINUTES: 240,
2457
- });
2458
- ```
2459
-
2460
- #### `CC_AVG_PRICE_CANDLES_COUNT`
2461
-
2462
- Controls the number of 1-minute candles used for VWAP (Volume Weighted Average Price) calculations.
2463
-
2464
- - **Default:** `5` candles (5 minutes of data)
2465
- - **Use case:** Adjust for more stable (higher) or responsive (lower) price calculations
2466
- - **Impact:** Affects entry/exit prices in both backtest and live modes
2467
-
2468
- ```typescript
2469
- // More responsive to recent price changes (3 minutes)
2470
- await setConfig({
2471
- CC_AVG_PRICE_CANDLES_COUNT: 3,
2472
- });
2473
-
2474
- // More stable, less sensitive to spikes (10 minutes)
2475
- await setConfig({
2476
- CC_AVG_PRICE_CANDLES_COUNT: 10,
2477
- });
2478
- ```
2479
-
2480
- ### When to Call `setConfig()`
2481
-
2482
- Always call `setConfig()` **before** running any strategies to ensure configuration is applied:
2483
-
2484
- ```typescript
2485
- import { setConfig, Backtest, Live } from "backtest-kit";
2486
-
2487
- // 1. Configure framework first
2488
- await setConfig({
2489
- CC_SCHEDULE_AWAIT_MINUTES: 90,
2490
- CC_AVG_PRICE_CANDLES_COUNT: 7,
2491
- });
2492
-
2493
- // 2. Then run strategies
2494
- Backtest.background("BTCUSDT", {
2495
- strategyName: "my-strategy",
2496
- exchangeName: "binance",
2497
- frameName: "1d-backtest"
2498
- });
2499
-
2500
- Live.background("ETHUSDT", {
2501
- strategyName: "my-strategy",
2502
- exchangeName: "binance"
2503
- });
2504
- ```
2505
-
2506
- ### Partial Configuration
2507
-
2508
- You can update individual parameters without specifying all of them:
2509
-
2510
- ```typescript
2511
- // Only change candle count, keep other defaults
2512
- await setConfig({
2513
- CC_AVG_PRICE_CANDLES_COUNT: 8,
2514
- });
2515
-
2516
- // Later, only change timeout
2517
- await setConfig({
2518
- CC_SCHEDULE_AWAIT_MINUTES: 60,
2519
- });
2520
- ```
2521
-
2522
- ---
2523
-
2524
- ## βœ… Tested & Reliable
2525
-
2526
- `backtest-kit` comes with **244 unit and integration tests** covering:
2527
-
2528
- - Signal validation and throttling
2529
- - PNL calculation with fees and slippage
2530
- - Crash recovery and state persistence
2531
- - Dual-layer persistence (pending signals and scheduled signals)
2532
- - Crash recovery validation (exchange/strategy name mismatch protection)
2533
- - Graceful shutdown (Backtest.stop, Live.stop, Walker.stop) with signal completion
2534
- - Callback execution order (onSchedule, onOpen, onActive, onClose, onCancel)
2535
- - Markdown report generation (backtest, live, scheduled signals)
2536
- - Walker strategy comparison and early termination
2537
- - Heatmap portfolio analysis
2538
- - Position sizing calculations
2539
- - Risk management validation
2540
- - Scheduled signals lifecycle and cancellation tracking
2541
- - Event system
2542
-
2543
- ---
2544
-
2545
- ## 🀝 Contribute
2546
-
2547
- We'd love your input! Fork the repo, submit a PR, or open an issue on **[GitHub](https://github.com/tripolskypetr/backtest-kit)**. πŸ™Œ
2548
-
2549
- ## πŸ“œ License
179
+ ## πŸ€– Are you a robot?
2550
180
 
2551
- MIT Β© [tripolskypetr](https://github.com/tripolskypetr) πŸ–‹οΈ
181
+ **For language models**: Read extended description in [./LLMs.md](./LLMs.md)