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