backtest-kit 1.0.4 → 1.1.1
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.
- package/README.md +707 -153
- package/build/index.cjs +2906 -192
- package/build/index.mjs +2901 -188
- package/package.json +2 -9
- package/types.d.ts +2205 -60
package/README.md
CHANGED
|
@@ -1,243 +1,616 @@
|
|
|
1
1
|
# Backtest Kit
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
> A production-ready TypeScript framework for backtesting and live trading strategies with crash-safe state persistence, signal validation, and memory-optimized architecture.
|
|
4
|
+
|
|
5
|
+
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
- 🚀 **
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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)
|
|
15
22
|
|
|
16
23
|
## Installation
|
|
17
24
|
|
|
18
25
|
```bash
|
|
19
|
-
npm install
|
|
26
|
+
npm install backtest-kit
|
|
20
27
|
```
|
|
21
28
|
|
|
22
29
|
## Quick Start
|
|
23
30
|
|
|
24
|
-
### 1.
|
|
31
|
+
### 1. Register Exchange Data Source
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { addExchange } from "backtest-kit";
|
|
35
|
+
import ccxt from "ccxt"; // Example using CCXT library
|
|
36
|
+
|
|
37
|
+
addExchange({
|
|
38
|
+
exchangeName: "binance",
|
|
39
|
+
|
|
40
|
+
// Fetch historical candles
|
|
41
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
42
|
+
const exchange = new ccxt.binance();
|
|
43
|
+
const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
|
|
44
|
+
|
|
45
|
+
return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
|
|
46
|
+
timestamp,
|
|
47
|
+
open,
|
|
48
|
+
high,
|
|
49
|
+
low,
|
|
50
|
+
close,
|
|
51
|
+
volume,
|
|
52
|
+
}));
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Format price according to exchange rules (e.g., 2 decimals for BTC)
|
|
56
|
+
formatPrice: async (symbol, price) => {
|
|
57
|
+
const exchange = new ccxt.binance();
|
|
58
|
+
const market = exchange.market(symbol);
|
|
59
|
+
return exchange.priceToPrecision(symbol, price);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Format quantity according to exchange rules (e.g., 8 decimals)
|
|
63
|
+
formatQuantity: async (symbol, quantity) => {
|
|
64
|
+
const exchange = new ccxt.binance();
|
|
65
|
+
return exchange.amountToPrecision(symbol, quantity);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Alternative: Database implementation**
|
|
25
71
|
|
|
26
72
|
```typescript
|
|
27
|
-
import { addExchange } from "
|
|
73
|
+
import { addExchange } from "backtest-kit";
|
|
74
|
+
import { db } from "./database"; // Your database client
|
|
28
75
|
|
|
29
76
|
addExchange({
|
|
77
|
+
exchangeName: "binance-db",
|
|
78
|
+
|
|
30
79
|
getCandles: async (symbol, interval, since, limit) => {
|
|
31
|
-
// Fetch
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
volume: 1000,
|
|
40
|
-
},
|
|
41
|
-
];
|
|
80
|
+
// Fetch from database for faster backtesting
|
|
81
|
+
return await db.query(`
|
|
82
|
+
SELECT timestamp, open, high, low, close, volume
|
|
83
|
+
FROM candles
|
|
84
|
+
WHERE symbol = $1 AND interval = $2 AND timestamp >= $3
|
|
85
|
+
ORDER BY timestamp ASC
|
|
86
|
+
LIMIT $4
|
|
87
|
+
`, [symbol, interval, since, limit]);
|
|
42
88
|
},
|
|
89
|
+
|
|
90
|
+
formatPrice: async (symbol, price) => price.toFixed(2),
|
|
91
|
+
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
|
|
43
92
|
});
|
|
44
93
|
```
|
|
45
94
|
|
|
46
|
-
### 2.
|
|
95
|
+
### 2. Register Trading Strategy
|
|
47
96
|
|
|
48
97
|
```typescript
|
|
49
|
-
import { addStrategy } from "
|
|
98
|
+
import { addStrategy } from "backtest-kit";
|
|
50
99
|
|
|
51
100
|
addStrategy({
|
|
101
|
+
strategyName: "my-strategy",
|
|
102
|
+
interval: "5m", // Throttling: signals generated max once per 5 minutes
|
|
52
103
|
getSignal: async (symbol) => {
|
|
53
104
|
// Your signal generation logic
|
|
105
|
+
// Validation happens automatically (prices, TP/SL logic, timestamps)
|
|
54
106
|
return {
|
|
55
|
-
id: "signal-1",
|
|
56
107
|
position: "long",
|
|
57
108
|
note: "BTC breakout",
|
|
58
109
|
priceOpen: 50000,
|
|
59
|
-
priceTakeProfit: 51000,
|
|
60
|
-
priceStopLoss: 49000,
|
|
61
|
-
minuteEstimatedTime: 60,
|
|
110
|
+
priceTakeProfit: 51000, // Must be > priceOpen for long
|
|
111
|
+
priceStopLoss: 49000, // Must be < priceOpen for long
|
|
112
|
+
minuteEstimatedTime: 60, // Signal duration in minutes
|
|
62
113
|
timestamp: Date.now(),
|
|
63
114
|
};
|
|
64
115
|
},
|
|
65
116
|
callbacks: {
|
|
66
|
-
onOpen: (backtest, symbol,
|
|
67
|
-
console.log("
|
|
117
|
+
onOpen: (backtest, symbol, signal) => {
|
|
118
|
+
console.log(`[${backtest ? "BT" : "LIVE"}] Signal opened:`, signal.id);
|
|
68
119
|
},
|
|
69
|
-
onClose: (backtest, symbol, priceClose,
|
|
70
|
-
console.log("Signal closed
|
|
120
|
+
onClose: (backtest, symbol, priceClose, signal) => {
|
|
121
|
+
console.log(`[${backtest ? "BT" : "LIVE"}] Signal closed:`, priceClose);
|
|
71
122
|
},
|
|
72
123
|
},
|
|
73
124
|
});
|
|
74
125
|
```
|
|
75
126
|
|
|
76
|
-
### 3.
|
|
127
|
+
### 3. Add Timeframe Generator
|
|
77
128
|
|
|
78
129
|
```typescript
|
|
79
|
-
import {
|
|
130
|
+
import { addFrame } from "backtest-kit";
|
|
80
131
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
addFrame({
|
|
133
|
+
frameName: "1d-backtest",
|
|
134
|
+
interval: "1m",
|
|
135
|
+
startDate: new Date("2024-01-01T00:00:00Z"),
|
|
136
|
+
endDate: new Date("2024-01-02T00:00:00Z"),
|
|
137
|
+
callbacks: {
|
|
138
|
+
onTimeframe: (timeframe, startDate, endDate, interval) => {
|
|
139
|
+
console.log(`Generated ${timeframe.length} timeframes from ${startDate} to ${endDate}`);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
86
142
|
});
|
|
143
|
+
```
|
|
87
144
|
|
|
88
|
-
|
|
89
|
-
const result = await runBacktest("BTCUSDT", timeframes);
|
|
90
|
-
console.log(result.results); // Array of closed trades with PNL
|
|
145
|
+
### 4. Run Backtest
|
|
91
146
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Prints beautiful ASCII table to console
|
|
95
|
-
```
|
|
147
|
+
```typescript
|
|
148
|
+
import { Backtest, listenSignalBacktest, listenError } from "backtest-kit";
|
|
96
149
|
|
|
97
|
-
|
|
150
|
+
// Run backtest in background
|
|
151
|
+
const stopBacktest = Backtest.background("BTCUSDT", {
|
|
152
|
+
strategyName: "my-strategy",
|
|
153
|
+
exchangeName: "binance",
|
|
154
|
+
frameName: "1d-backtest"
|
|
155
|
+
});
|
|
98
156
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
157
|
+
// Listen to closed signals
|
|
158
|
+
listenSignalBacktest((event) => {
|
|
159
|
+
if (event.action === "closed") {
|
|
160
|
+
console.log("PNL:", event.pnl.pnlPercentage);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Listen to errors
|
|
165
|
+
listenError((error) => {
|
|
166
|
+
console.error("Error:", error.message);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Generate and save report
|
|
170
|
+
const markdown = await Backtest.getReport("my-strategy");
|
|
171
|
+
await Backtest.dump("my-strategy"); // ./logs/backtest/my-strategy.md
|
|
110
172
|
```
|
|
111
173
|
|
|
112
|
-
###
|
|
174
|
+
### 5. Run Live Trading (Crash-Safe)
|
|
113
175
|
|
|
114
176
|
```typescript
|
|
115
|
-
import {
|
|
177
|
+
import { Live, listenSignalLive, listenError } from "backtest-kit";
|
|
116
178
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
179
|
+
// Run live trading in background (infinite loop, crash-safe)
|
|
180
|
+
const stop = Live.background("BTCUSDT", {
|
|
181
|
+
strategyName: "my-strategy",
|
|
182
|
+
exchangeName: "binance"
|
|
183
|
+
});
|
|
120
184
|
|
|
121
|
-
//
|
|
122
|
-
|
|
185
|
+
// Listen to all signal events
|
|
186
|
+
listenSignalLive((event) => {
|
|
187
|
+
if (event.action === "opened") {
|
|
188
|
+
console.log("Signal opened:", event.signal.id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (event.action === "closed") {
|
|
192
|
+
console.log("Signal closed:", {
|
|
193
|
+
reason: event.closeReason,
|
|
194
|
+
pnl: event.pnl.pnlPercentage,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Auto-save report
|
|
198
|
+
Live.dump(event.strategyName);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Listen to errors
|
|
203
|
+
listenError((error) => {
|
|
204
|
+
console.error("Error:", error.message);
|
|
205
|
+
});
|
|
123
206
|
|
|
124
|
-
// Stop
|
|
125
|
-
stopAll();
|
|
207
|
+
// Stop when needed: stop();
|
|
126
208
|
```
|
|
127
209
|
|
|
128
|
-
|
|
210
|
+
**Crash Recovery:** If process crashes, restart with same code - state automatically recovered from disk (no duplicate signals).
|
|
129
211
|
|
|
130
|
-
|
|
212
|
+
### 6. Alternative: Async Generators (Optional)
|
|
213
|
+
|
|
214
|
+
For manual control over execution flow:
|
|
131
215
|
|
|
132
216
|
```typescript
|
|
133
|
-
import {
|
|
217
|
+
import { Backtest, Live } from "backtest-kit";
|
|
218
|
+
|
|
219
|
+
// Manual backtest iteration
|
|
220
|
+
for await (const result of Backtest.run("BTCUSDT", {
|
|
221
|
+
strategyName: "my-strategy",
|
|
222
|
+
exchangeName: "binance",
|
|
223
|
+
frameName: "1d-backtest"
|
|
224
|
+
})) {
|
|
225
|
+
console.log("PNL:", result.pnl.pnlPercentage);
|
|
226
|
+
if (result.pnl.pnlPercentage < -5) break; // Early termination
|
|
227
|
+
}
|
|
134
228
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
229
|
+
// Manual live iteration (infinite loop)
|
|
230
|
+
for await (const result of Live.run("BTCUSDT", {
|
|
231
|
+
strategyName: "my-strategy",
|
|
232
|
+
exchangeName: "binance"
|
|
233
|
+
})) {
|
|
234
|
+
if (result.action === "closed") {
|
|
235
|
+
console.log("PNL:", result.pnl.pnlPercentage);
|
|
236
|
+
}
|
|
139
237
|
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Architecture Overview
|
|
241
|
+
|
|
242
|
+
The framework follows **clean architecture** with:
|
|
140
243
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
244
|
+
- **Client Layer** - Pure business logic without DI (ClientStrategy, ClientExchange, ClientFrame)
|
|
245
|
+
- **Service Layer** - DI-based services organized by responsibility
|
|
246
|
+
- **Schema Services** - Registry pattern for configuration
|
|
247
|
+
- **Connection Services** - Memoized client instance creators
|
|
248
|
+
- **Global Services** - Context wrappers for public API
|
|
249
|
+
- **Logic Services** - Async generator orchestration (backtest/live)
|
|
250
|
+
- **Persistence Layer** - Crash-safe atomic file writes with `PersistSignalAdaper`
|
|
147
251
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
252
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed documentation.
|
|
253
|
+
|
|
254
|
+
## Signal Validation
|
|
255
|
+
|
|
256
|
+
All signals are validated automatically before execution:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// ✅ Valid long signal
|
|
260
|
+
{
|
|
261
|
+
position: "long",
|
|
262
|
+
priceOpen: 50000,
|
|
263
|
+
priceTakeProfit: 51000, // ✅ 51000 > 50000
|
|
264
|
+
priceStopLoss: 49000, // ✅ 49000 < 50000
|
|
265
|
+
minuteEstimatedTime: 60, // ✅ positive
|
|
266
|
+
timestamp: Date.now(), // ✅ positive
|
|
267
|
+
}
|
|
151
268
|
|
|
152
|
-
|
|
269
|
+
// ❌ Invalid long signal - throws error
|
|
270
|
+
{
|
|
271
|
+
position: "long",
|
|
272
|
+
priceOpen: 50000,
|
|
273
|
+
priceTakeProfit: 49000, // ❌ 49000 < 50000 (must be higher for long)
|
|
274
|
+
priceStopLoss: 51000, // ❌ 51000 > 50000 (must be lower for long)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ✅ Valid short signal
|
|
278
|
+
{
|
|
279
|
+
position: "short",
|
|
280
|
+
priceOpen: 50000,
|
|
281
|
+
priceTakeProfit: 49000, // ✅ 49000 < 50000 (profit goes down for short)
|
|
282
|
+
priceStopLoss: 51000, // ✅ 51000 > 50000 (stop loss goes up for short)
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Validation errors include detailed messages for debugging.
|
|
287
|
+
|
|
288
|
+
## Interval Throttling
|
|
289
|
+
|
|
290
|
+
Prevent signal spam with automatic throttling:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
addStrategy({
|
|
294
|
+
strategyName: "my-strategy",
|
|
295
|
+
interval: "5m", // Signals generated max once per 5 minutes
|
|
296
|
+
getSignal: async (symbol) => {
|
|
297
|
+
// This function will be called max once per 5 minutes
|
|
298
|
+
// Even if tick() is called every second
|
|
299
|
+
return signal;
|
|
153
300
|
},
|
|
154
|
-
|
|
155
|
-
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Supported intervals: `"1m"`, `"3m"`, `"5m"`, `"15m"`, `"30m"`, `"1h"`
|
|
305
|
+
|
|
306
|
+
## Markdown Reports
|
|
307
|
+
|
|
308
|
+
Generate detailed trading reports with statistics:
|
|
309
|
+
|
|
310
|
+
### Backtest Reports
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { Backtest } from "backtest-kit";
|
|
314
|
+
|
|
315
|
+
// Run backtest
|
|
316
|
+
const stopBacktest = Backtest.background("BTCUSDT", {
|
|
317
|
+
strategyName: "my-strategy",
|
|
318
|
+
exchangeName: "binance",
|
|
319
|
+
frameName: "1d-backtest"
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Generate markdown report
|
|
323
|
+
const markdown = await Backtest.getReport("my-strategy");
|
|
324
|
+
console.log(markdown);
|
|
325
|
+
|
|
326
|
+
// Save to disk (default: ./logs/backtest/my-strategy.md)
|
|
327
|
+
await Backtest.dump("my-strategy");
|
|
156
328
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
// { count: 1440, timestamps: [...], apiCalls: 1440 }
|
|
329
|
+
// Save to custom path
|
|
330
|
+
await Backtest.dump("my-strategy", "./custom/path");
|
|
160
331
|
```
|
|
161
332
|
|
|
162
|
-
|
|
333
|
+
**Report includes:**
|
|
334
|
+
- Total closed signals
|
|
335
|
+
- All signal details (prices, TP/SL, PNL, duration, close reason)
|
|
336
|
+
- Timestamps for each signal
|
|
337
|
+
|
|
338
|
+
### Live Trading Reports
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { Live } from "backtest-kit";
|
|
163
342
|
|
|
343
|
+
// Generate live trading report
|
|
344
|
+
const markdown = await Live.getReport("my-strategy");
|
|
345
|
+
|
|
346
|
+
// Save to disk (default: ./logs/live/my-strategy.md)
|
|
347
|
+
await Live.dump("my-strategy");
|
|
164
348
|
```
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
349
|
+
|
|
350
|
+
**Report includes:**
|
|
351
|
+
- Total events (idle, opened, active, closed)
|
|
352
|
+
- Closed signals count
|
|
353
|
+
- Win rate (% wins, wins/losses)
|
|
354
|
+
- Average PNL percentage
|
|
355
|
+
- Signal-by-signal details with current state
|
|
356
|
+
|
|
357
|
+
**Report example:**
|
|
358
|
+
```markdown
|
|
359
|
+
# Live Trading Report: my-strategy
|
|
360
|
+
|
|
361
|
+
Total events: 15
|
|
362
|
+
Closed signals: 5
|
|
363
|
+
Win rate: 60.00% (3W / 2L)
|
|
364
|
+
Average PNL: +1.23%
|
|
365
|
+
|
|
366
|
+
| Timestamp | Action | Symbol | Signal ID | Position | ... | PNL (net) | Close Reason |
|
|
367
|
+
|-----------|--------|--------|-----------|----------|-----|-----------|--------------|
|
|
368
|
+
| ... | CLOSED | BTCUSD | abc-123 | LONG | ... | +2.45% | take_profit |
|
|
181
369
|
```
|
|
182
370
|
|
|
183
|
-
##
|
|
371
|
+
## Event Listeners
|
|
184
372
|
|
|
185
|
-
|
|
373
|
+
Subscribe to signal events with filtering support. Useful for running strategies in background while reacting to specific events.
|
|
186
374
|
|
|
187
|
-
|
|
375
|
+
### Background Execution with Event Listeners
|
|
188
376
|
|
|
189
377
|
```typescript
|
|
190
|
-
|
|
191
|
-
|
|
378
|
+
import { Backtest, listenSignalBacktest } from "backtest-kit";
|
|
379
|
+
|
|
380
|
+
// Run backtest in background (doesn't yield results)
|
|
381
|
+
Backtest.background("BTCUSDT", {
|
|
382
|
+
strategyName: "my-strategy",
|
|
383
|
+
exchangeName: "binance",
|
|
384
|
+
frameName: "1d-backtest"
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Listen to all backtest events
|
|
388
|
+
const unsubscribe = listenSignalBacktest((event) => {
|
|
389
|
+
if (event.action === "closed") {
|
|
390
|
+
console.log("Signal closed:", {
|
|
391
|
+
pnl: event.pnl.pnlPercentage,
|
|
392
|
+
reason: event.closeReason
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Stop listening when done
|
|
398
|
+
// unsubscribe();
|
|
192
399
|
```
|
|
193
400
|
|
|
194
|
-
###
|
|
401
|
+
### Listen Once with Filter
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { Backtest, listenSignalBacktestOnce } from "backtest-kit";
|
|
405
|
+
|
|
406
|
+
// Run backtest in background
|
|
407
|
+
Backtest.background("BTCUSDT", {
|
|
408
|
+
strategyName: "my-strategy",
|
|
409
|
+
exchangeName: "binance",
|
|
410
|
+
frameName: "1d-backtest"
|
|
411
|
+
});
|
|
195
412
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
413
|
+
// Wait for first take profit event
|
|
414
|
+
listenSignalBacktestOnce(
|
|
415
|
+
(event) => event.action === "closed" && event.closeReason === "take_profit",
|
|
416
|
+
(event) => {
|
|
417
|
+
console.log("First take profit hit!", event.pnl.pnlPercentage);
|
|
418
|
+
// Automatically unsubscribes after first match
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Live Trading with Event Listeners
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import { Live, listenSignalLive, listenSignalLiveOnce } from "backtest-kit";
|
|
427
|
+
|
|
428
|
+
// Run live trading in background (infinite loop)
|
|
429
|
+
const cancel = Live.background("BTCUSDT", {
|
|
430
|
+
strategyName: "my-strategy",
|
|
431
|
+
exchangeName: "binance"
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Listen to all live events
|
|
435
|
+
listenSignalLive((event) => {
|
|
436
|
+
if (event.action === "opened") {
|
|
437
|
+
console.log("Signal opened:", event.signal.id);
|
|
438
|
+
}
|
|
439
|
+
if (event.action === "closed") {
|
|
440
|
+
console.log("Signal closed:", event.pnl.pnlPercentage);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// React to first stop loss once
|
|
445
|
+
listenSignalLiveOnce(
|
|
446
|
+
(event) => event.action === "closed" && event.closeReason === "stop_loss",
|
|
447
|
+
(event) => {
|
|
448
|
+
console.error("Stop loss hit!", event.pnl.pnlPercentage);
|
|
449
|
+
// Send alert, dump report, etc.
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Stop live trading after some condition
|
|
454
|
+
// cancel();
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Listen to All Signals (Backtest + Live)
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
import { listenSignal, listenSignalOnce, Backtest, Live } from "backtest-kit";
|
|
461
|
+
|
|
462
|
+
// Listen to both backtest and live events
|
|
463
|
+
listenSignal((event) => {
|
|
464
|
+
console.log("Event:", event.action, event.strategyName);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Wait for first loss from any source
|
|
468
|
+
listenSignalOnce(
|
|
469
|
+
(event) => event.action === "closed" && event.pnl.pnlPercentage < 0,
|
|
470
|
+
(event) => {
|
|
471
|
+
console.log("First loss detected:", event.pnl.pnlPercentage);
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Run both modes
|
|
476
|
+
Backtest.background("BTCUSDT", {
|
|
477
|
+
strategyName: "my-strategy",
|
|
478
|
+
exchangeName: "binance",
|
|
479
|
+
frameName: "1d-backtest"
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
Live.background("BTCUSDT", {
|
|
483
|
+
strategyName: "my-strategy",
|
|
484
|
+
exchangeName: "binance"
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Available event listeners:**
|
|
489
|
+
|
|
490
|
+
- `listenSignal(callback)` - Subscribe to all signal events (backtest + live)
|
|
491
|
+
- `listenSignalOnce(filter, callback)` - Subscribe once with filter predicate
|
|
492
|
+
- `listenSignalBacktest(callback)` - Subscribe to backtest signals only
|
|
493
|
+
- `listenSignalBacktestOnce(filter, callback)` - Subscribe to backtest signals once
|
|
494
|
+
- `listenSignalLive(callback)` - Subscribe to live signals only
|
|
495
|
+
- `listenSignalLiveOnce(filter, callback)` - Subscribe to live signals once
|
|
496
|
+
|
|
497
|
+
All listeners return an `unsubscribe` function. All callbacks are processed sequentially using queued async execution.
|
|
199
498
|
|
|
200
499
|
## API Reference
|
|
201
500
|
|
|
202
|
-
### Functions
|
|
501
|
+
### High-Level Functions
|
|
203
502
|
|
|
204
|
-
####
|
|
205
|
-
Add exchange data source for candles.
|
|
503
|
+
#### Schema Registration
|
|
206
504
|
|
|
207
|
-
|
|
208
|
-
|
|
505
|
+
```typescript
|
|
506
|
+
// Register exchange
|
|
507
|
+
addExchange(exchangeSchema: IExchangeSchema): void
|
|
209
508
|
|
|
210
|
-
|
|
211
|
-
|
|
509
|
+
// Register strategy
|
|
510
|
+
addStrategy(strategySchema: IStrategySchema): void
|
|
212
511
|
|
|
213
|
-
|
|
214
|
-
|
|
512
|
+
// Register timeframe generator
|
|
513
|
+
addFrame(frameSchema: IFrameSchema): void
|
|
514
|
+
```
|
|
215
515
|
|
|
216
|
-
####
|
|
217
|
-
Run backtest and return closed trades only.
|
|
516
|
+
#### Exchange Data
|
|
218
517
|
|
|
219
|
-
|
|
220
|
-
|
|
518
|
+
```typescript
|
|
519
|
+
// Get historical candles
|
|
520
|
+
const candles = await getCandles("BTCUSDT", "1h", 5);
|
|
521
|
+
// Returns: [
|
|
522
|
+
// { timestamp: 1704067200000, open: 42150.5, high: 42380.2, low: 42100.0, close: 42250.8, volume: 125.43 },
|
|
523
|
+
// { timestamp: 1704070800000, open: 42250.8, high: 42500.0, low: 42200.0, close: 42450.3, volume: 98.76 },
|
|
524
|
+
// { timestamp: 1704074400000, open: 42450.3, high: 42600.0, low: 42400.0, close: 42580.5, volume: 110.22 },
|
|
525
|
+
// { timestamp: 1704078000000, open: 42580.5, high: 42700.0, low: 42550.0, close: 42650.0, volume: 95.18 },
|
|
526
|
+
// { timestamp: 1704081600000, open: 42650.0, high: 42750.0, low: 42600.0, close: 42720.0, volume: 102.35 }
|
|
527
|
+
// ]
|
|
528
|
+
|
|
529
|
+
// Get VWAP from last 5 1m candles
|
|
530
|
+
const vwap = await getAveragePrice("BTCUSDT");
|
|
531
|
+
// Returns: 42685.34
|
|
532
|
+
|
|
533
|
+
// Get current date in execution context
|
|
534
|
+
const date = await getDate();
|
|
535
|
+
// Returns: 2024-01-01T12:00:00.000Z (in backtest mode, returns frame's current timestamp)
|
|
536
|
+
// Returns: 2024-01-15T10:30:45.123Z (in live mode, returns current wall clock time)
|
|
537
|
+
|
|
538
|
+
// Get current mode
|
|
539
|
+
const mode = await getMode();
|
|
540
|
+
// Returns: "backtest" or "live"
|
|
541
|
+
|
|
542
|
+
// Format price/quantity for exchange
|
|
543
|
+
const price = await formatPrice("BTCUSDT", 42685.3456789);
|
|
544
|
+
// Returns: "42685.35" (formatted to exchange precision)
|
|
545
|
+
|
|
546
|
+
const quantity = await formatQuantity("BTCUSDT", 0.123456789);
|
|
547
|
+
// Returns: "0.12345" (formatted to exchange precision)
|
|
548
|
+
```
|
|
221
549
|
|
|
222
|
-
|
|
223
|
-
Iterate timeframes with accumulator pattern. Callback receives `(accumulator, index, when, symbol)`.
|
|
550
|
+
### Service APIs
|
|
224
551
|
|
|
225
|
-
####
|
|
226
|
-
Start real-time strategy execution.
|
|
552
|
+
#### Backtest API
|
|
227
553
|
|
|
228
|
-
|
|
229
|
-
|
|
554
|
+
```typescript
|
|
555
|
+
import { Backtest } from "backtest-kit";
|
|
556
|
+
|
|
557
|
+
// Stream backtest results
|
|
558
|
+
Backtest.run(
|
|
559
|
+
symbol: string,
|
|
560
|
+
context: {
|
|
561
|
+
strategyName: string;
|
|
562
|
+
exchangeName: string;
|
|
563
|
+
frameName: string;
|
|
564
|
+
}
|
|
565
|
+
): AsyncIterableIterator<IStrategyTickResultClosed>
|
|
566
|
+
|
|
567
|
+
// Run in background without yielding results
|
|
568
|
+
Backtest.background(
|
|
569
|
+
symbol: string,
|
|
570
|
+
context: { strategyName, exchangeName, frameName }
|
|
571
|
+
): Promise<() => void> // Returns cancellation function
|
|
572
|
+
|
|
573
|
+
// Generate markdown report
|
|
574
|
+
Backtest.getReport(strategyName: string): Promise<string>
|
|
575
|
+
|
|
576
|
+
// Save report to disk
|
|
577
|
+
Backtest.dump(strategyName: string, path?: string): Promise<void>
|
|
578
|
+
```
|
|
230
579
|
|
|
231
|
-
####
|
|
232
|
-
Stop all running strategies.
|
|
580
|
+
#### Live Trading API
|
|
233
581
|
|
|
234
|
-
|
|
582
|
+
```typescript
|
|
583
|
+
import { Live } from "backtest-kit";
|
|
584
|
+
|
|
585
|
+
// Stream live results (infinite)
|
|
586
|
+
Live.run(
|
|
587
|
+
symbol: string,
|
|
588
|
+
context: {
|
|
589
|
+
strategyName: string;
|
|
590
|
+
exchangeName: string;
|
|
591
|
+
}
|
|
592
|
+
): AsyncIterableIterator<IStrategyTickResult>
|
|
593
|
+
|
|
594
|
+
// Run in background without yielding results
|
|
595
|
+
Live.background(
|
|
596
|
+
symbol: string,
|
|
597
|
+
context: { strategyName, exchangeName }
|
|
598
|
+
): Promise<() => void> // Returns cancellation function
|
|
599
|
+
|
|
600
|
+
// Generate markdown report
|
|
601
|
+
Live.getReport(strategyName: string): Promise<string>
|
|
602
|
+
|
|
603
|
+
// Save report to disk
|
|
604
|
+
Live.dump(strategyName: string, path?: string): Promise<void>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## Type Definitions
|
|
235
608
|
|
|
236
609
|
### Signal Data
|
|
237
610
|
|
|
238
611
|
```typescript
|
|
239
|
-
interface
|
|
240
|
-
id: string;
|
|
612
|
+
interface ISignalRow {
|
|
613
|
+
id: string; // Auto-generated
|
|
241
614
|
position: "long" | "short";
|
|
242
615
|
note: string;
|
|
243
616
|
priceOpen: number;
|
|
@@ -248,29 +621,210 @@ interface ISignalData {
|
|
|
248
621
|
}
|
|
249
622
|
```
|
|
250
623
|
|
|
251
|
-
### Tick Results
|
|
624
|
+
### Tick Results (Discriminated Union)
|
|
252
625
|
|
|
253
626
|
```typescript
|
|
254
627
|
type IStrategyTickResult =
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
628
|
+
| { action: "idle"; signal: null }
|
|
629
|
+
| { action: "opened"; signal: ISignalRow }
|
|
630
|
+
| { action: "active"; signal: ISignalRow; currentPrice: number }
|
|
631
|
+
| {
|
|
632
|
+
action: "closed";
|
|
633
|
+
signal: ISignalRow;
|
|
634
|
+
currentPrice: number;
|
|
635
|
+
closeReason: "take_profit" | "stop_loss" | "time_expired";
|
|
636
|
+
closeTimestamp: number;
|
|
637
|
+
pnl: {
|
|
638
|
+
priceOpenWithCosts: number;
|
|
639
|
+
priceCloseWithCosts: number;
|
|
640
|
+
pnlPercentage: number;
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### PNL Calculation
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// Constants
|
|
649
|
+
PERCENT_SLIPPAGE = 0.1% // 0.001
|
|
650
|
+
PERCENT_FEE = 0.1% // 0.001
|
|
651
|
+
|
|
652
|
+
// LONG position
|
|
653
|
+
priceOpenWithCosts = priceOpen * (1 + slippage + fee)
|
|
654
|
+
priceCloseWithCosts = priceClose * (1 - slippage - fee)
|
|
655
|
+
pnl% = (priceCloseWithCosts - priceOpenWithCosts) / priceOpenWithCosts * 100
|
|
656
|
+
|
|
657
|
+
// SHORT position
|
|
658
|
+
priceOpenWithCosts = priceOpen * (1 - slippage + fee)
|
|
659
|
+
priceCloseWithCosts = priceClose * (1 + slippage + fee)
|
|
660
|
+
pnl% = (priceOpenWithCosts - priceCloseWithCosts) / priceOpenWithCosts * 100
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## Production Readiness
|
|
664
|
+
|
|
665
|
+
### ✅ Production-Ready Features
|
|
666
|
+
|
|
667
|
+
1. **Crash-Safe Persistence** - Atomic file writes with automatic recovery
|
|
668
|
+
2. **Signal Validation** - Comprehensive validation prevents invalid trades
|
|
669
|
+
3. **Type Safety** - Discriminated unions eliminate runtime type errors
|
|
670
|
+
4. **Memory Efficiency** - Prototype methods + async generators + memoization
|
|
671
|
+
5. **Interval Throttling** - Prevents signal spam
|
|
672
|
+
6. **Live Trading Ready** - Full implementation with real-time progression
|
|
673
|
+
7. **Error Recovery** - Stateless process with disk-based state
|
|
674
|
+
|
|
675
|
+
## File Structure
|
|
676
|
+
|
|
677
|
+
```
|
|
678
|
+
src/
|
|
679
|
+
├── client/ # Pure business logic (no DI)
|
|
680
|
+
│ ├── ClientStrategy.ts # Signal lifecycle + validation + persistence
|
|
681
|
+
│ ├── ClientExchange.ts # VWAP calculation
|
|
682
|
+
│ └── ClientFrame.ts # Timeframe generation
|
|
683
|
+
├── classes/
|
|
684
|
+
│ └── Persist.ts # Atomic file persistence
|
|
685
|
+
├── function/ # High-level API
|
|
686
|
+
│ ├── add.ts # addStrategy, addExchange, addFrame
|
|
687
|
+
│ ├── exchange.ts # getCandles, getAveragePrice, getDate, getMode
|
|
688
|
+
│ └── run.ts # DEPRECATED - use logic services instead
|
|
689
|
+
├── interfaces/ # TypeScript interfaces
|
|
690
|
+
│ ├── Strategy.interface.ts
|
|
691
|
+
│ ├── Exchange.interface.ts
|
|
692
|
+
│ └── Frame.interface.ts
|
|
693
|
+
├── lib/
|
|
694
|
+
│ ├── core/ # DI container
|
|
695
|
+
│ ├── services/
|
|
696
|
+
│ │ ├── base/ # LoggerService
|
|
697
|
+
│ │ ├── context/ # ExecutionContext, MethodContext
|
|
698
|
+
│ │ ├── connection/ # Client instance creators
|
|
699
|
+
│ │ ├── global/ # Context wrappers
|
|
700
|
+
│ │ ├── schema/ # Registry services
|
|
701
|
+
│ │ └── logic/
|
|
702
|
+
│ │ └── private/ # Async generator orchestration
|
|
703
|
+
│ │ ├── BacktestLogicPrivateService.ts
|
|
704
|
+
│ │ └── LiveLogicPrivateService.ts
|
|
705
|
+
│ └── index.ts # Public API
|
|
706
|
+
└── helpers/
|
|
707
|
+
└── toProfitLossDto.ts # PNL calculation
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## Advanced Examples
|
|
711
|
+
|
|
712
|
+
### Custom Persistence Adapter
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
import { PersistSignalAdaper, PersistBase } from "backtest-kit";
|
|
716
|
+
|
|
717
|
+
class RedisPersist extends PersistBase {
|
|
718
|
+
async readValue(entityId) {
|
|
719
|
+
return JSON.parse(await redis.get(entityId));
|
|
720
|
+
}
|
|
721
|
+
async writeValue(entityId, entity) {
|
|
722
|
+
await redis.set(entityId, JSON.stringify(entity));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
PersistSignalAdaper.usePersistSignalAdapter(RedisPersist);
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### Multi-Symbol Live Trading
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
import { Live } from "backtest-kit";
|
|
733
|
+
|
|
734
|
+
const symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT"];
|
|
735
|
+
|
|
736
|
+
// Run all symbols in parallel
|
|
737
|
+
await Promise.all(
|
|
738
|
+
symbols.map(async (symbol) => {
|
|
739
|
+
for await (const result of Live.run(symbol, {
|
|
740
|
+
strategyName: "my-strategy",
|
|
741
|
+
exchangeName: "binance"
|
|
742
|
+
})) {
|
|
743
|
+
console.log(`[${symbol}]`, result.action);
|
|
744
|
+
|
|
745
|
+
// Generate reports periodically
|
|
746
|
+
if (result.action === "closed") {
|
|
747
|
+
await Live.dump("my-strategy");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Early Termination
|
|
755
|
+
|
|
756
|
+
**Using async generator with break:**
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
import { Backtest } from "backtest-kit";
|
|
760
|
+
|
|
761
|
+
for await (const result of Backtest.run("BTCUSDT", {
|
|
762
|
+
strategyName: "my-strategy",
|
|
763
|
+
exchangeName: "binance",
|
|
764
|
+
frameName: "1d-backtest"
|
|
765
|
+
})) {
|
|
766
|
+
if (result.closeReason === "stop_loss") {
|
|
767
|
+
console.log("Stop loss hit - terminating backtest");
|
|
768
|
+
|
|
769
|
+
// Save final report before exit
|
|
770
|
+
await Backtest.dump("my-strategy");
|
|
771
|
+
break; // Generator stops immediately
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**Using background mode with stop() function:**
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
import { Backtest, Live, listenSignalLiveOnce } from "backtest-kit";
|
|
780
|
+
|
|
781
|
+
// Backtest.background returns a stop function
|
|
782
|
+
const stopBacktest = await Backtest.background("BTCUSDT", {
|
|
783
|
+
strategyName: "my-strategy",
|
|
784
|
+
exchangeName: "binance",
|
|
785
|
+
frameName: "1d-backtest"
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Stop backtest after some condition
|
|
789
|
+
setTimeout(() => {
|
|
790
|
+
console.log("Stopping backtest...");
|
|
791
|
+
stopBacktest(); // Stops the background execution
|
|
792
|
+
}, 5000);
|
|
793
|
+
|
|
794
|
+
// Live.background also returns a stop function
|
|
795
|
+
const stopLive = Live.background("BTCUSDT", {
|
|
796
|
+
strategyName: "my-strategy",
|
|
797
|
+
exchangeName: "binance"
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Stop live trading after detecting stop loss
|
|
801
|
+
listenSignalLiveOnce(
|
|
802
|
+
(event) => event.action === "closed" && event.closeReason === "stop_loss",
|
|
803
|
+
(event) => {
|
|
804
|
+
console.log("Stop loss detected - stopping live trading");
|
|
805
|
+
stopLive(); // Stops the infinite loop
|
|
806
|
+
}
|
|
807
|
+
);
|
|
259
808
|
```
|
|
260
809
|
|
|
261
810
|
## Use Cases
|
|
262
811
|
|
|
263
|
-
|
|
264
|
-
- **
|
|
265
|
-
- **
|
|
266
|
-
- **
|
|
267
|
-
- **
|
|
268
|
-
|
|
812
|
+
- **Algorithmic Trading** - Backtest and deploy strategies with crash recovery
|
|
813
|
+
- **Strategy Research** - Test hypotheses on historical data
|
|
814
|
+
- **Signal Generation** - Use with ML models or technical indicators
|
|
815
|
+
- **Portfolio Management** - Track multiple strategies across symbols
|
|
816
|
+
- **Educational Projects** - Learn trading system architecture
|
|
817
|
+
|
|
818
|
+
## Contributing
|
|
819
|
+
|
|
820
|
+
Pull requests are welcome. For major changes, please open an issue first.
|
|
269
821
|
|
|
270
822
|
## License
|
|
271
823
|
|
|
272
824
|
MIT
|
|
273
825
|
|
|
274
|
-
##
|
|
826
|
+
## Links
|
|
275
827
|
|
|
276
|
-
|
|
828
|
+
- [Architecture Documentation](./ARCHITECTURE.md)
|
|
829
|
+
- [TypeScript Documentation](https://www.typescriptlang.org/)
|
|
830
|
+
- [Dependency Injection](https://github.com/tripolskypetr/di-kit)
|