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