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