backtest-kit 1.1.8 β†’ 1.2.0

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