backtest-kit 2.3.1 → 2.3.2
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 +64 -52
- package/build/index.cjs +307 -200
- package/build/index.mjs +308 -201
- package/package.json +3 -2
- package/types.d.ts +34 -12
package/README.md
CHANGED
|
@@ -212,83 +212,95 @@ temporal time context to your strategies.
|
|
|
212
212
|
<summary>
|
|
213
213
|
The Math
|
|
214
214
|
</summary>
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
For a candle with:
|
|
217
|
-
- `timestamp` = candle open time
|
|
217
|
+
- `timestamp` = candle open time (openTime)
|
|
218
218
|
- `stepMs` = interval duration (e.g., 60000ms for "1m")
|
|
219
219
|
- Candle close time = `timestamp + stepMs`
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
**Alignment:** All timestamps are aligned down to interval boundary.
|
|
222
|
+
For example, for 15m interval: 00:17 → 00:15, 00:44 → 00:30
|
|
223
|
+
|
|
224
|
+
**Adapter contract:**
|
|
225
|
+
- First candle.timestamp must equal aligned `since`
|
|
226
|
+
- Adapter must return exactly `limit` candles
|
|
227
|
+
- Sequential timestamps: `since + i * stepMs` for i = 0..limit-1
|
|
222
228
|
|
|
223
|
-
- `getCandles(symbol, interval, limit)` - Returns
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
- Example: `getCandles("BTCUSDT", "1m", 100)` returns 100 candles ending before current time
|
|
229
|
+
- `getCandles(symbol, interval, limit)` - Returns exactly `limit` candles
|
|
230
|
+
- Aligns `when` down to interval boundary
|
|
231
|
+
- Calculates `since = alignedWhen - limit * stepMs`
|
|
232
|
+
- First candle.timestamp === since
|
|
233
|
+
- Example: `getCandles("BTCUSDT", "1m", 100)` returns 100 candles ending at aligned when
|
|
229
234
|
|
|
230
|
-
- `getNextCandles(symbol, interval, limit)` - Returns
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
- Upper bound: `candle.timestamp + stepMs < endTime` (exclusive)
|
|
235
|
+
- `getNextCandles(symbol, interval, limit)` - Returns exactly `limit` candles (backtest only)
|
|
236
|
+
- Aligns `when` down to interval boundary
|
|
237
|
+
- `since = alignedWhen` (starts from aligned when, going forward)
|
|
238
|
+
- First candle.timestamp === since
|
|
235
239
|
- Throws error in live mode to prevent look-ahead bias
|
|
236
|
-
- Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles after
|
|
240
|
+
- Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles after aligned when
|
|
237
241
|
|
|
238
242
|
- `getRawCandles(symbol, interval, limit?, sDate?, eDate?)` - Flexible parameter combinations:
|
|
239
|
-
- `(limit)` -
|
|
240
|
-
- `(limit, sDate)` -
|
|
241
|
-
- `(limit, undefined, eDate)` -
|
|
242
|
-
- `(undefined, sDate, eDate)` -
|
|
243
|
-
- `(limit, sDate, eDate)` -
|
|
244
|
-
- All combinations
|
|
245
|
-
- All combinations respect exclusive boundaries and look-ahead bias protection
|
|
243
|
+
- `(limit)` - since = alignedWhen - limit * stepMs
|
|
244
|
+
- `(limit, sDate)` - since = align(sDate), returns `limit` candles forward
|
|
245
|
+
- `(limit, undefined, eDate)` - since = align(eDate) - limit * stepMs
|
|
246
|
+
- `(undefined, sDate, eDate)` - since = align(sDate), limit calculated from range
|
|
247
|
+
- `(limit, sDate, eDate)` - since = align(sDate), returns `limit` candles
|
|
248
|
+
- All combinations respect look-ahead bias protection (eDate/endTime <= when)
|
|
246
249
|
|
|
247
250
|
**Persistent Cache:**
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
- Cache
|
|
251
|
+
- Cache lookup calculates expected timestamps: `since + i * stepMs` for i = 0..limit-1
|
|
252
|
+
- Returns all candles if found, null if any missing (cache miss)
|
|
253
|
+
- Cache and runtime use identical timestamp calculation logic
|
|
251
254
|
|
|
252
255
|
</details>
|
|
253
256
|
|
|
254
|
-
####
|
|
255
|
-
|
|
256
|
-
All methods use **strict exclusive boundaries** - candles at exact boundary times are excluded. This prevents accidental inclusion of boundary conditions in backtest logic and ensures consistent behavior across cache and runtime.
|
|
257
|
+
#### Candle Timestamp Convention:
|
|
257
258
|
|
|
258
259
|
According to this `timestamp` of a candle in backtest-kit is exactly the `openTime`, not ~~`closeTime`~~
|
|
259
260
|
|
|
260
|
-
**Key
|
|
261
|
+
**Key principles:**
|
|
262
|
+
- All timestamps are aligned down to interval boundary
|
|
263
|
+
- First candle.timestamp must equal aligned `since`
|
|
264
|
+
- Adapter must return exactly `limit` candles
|
|
265
|
+
- Sequential timestamps: `since + i * stepMs`
|
|
261
266
|
|
|
262
|
-
### 🔬 Technical Details:
|
|
267
|
+
### 🔬 Technical Details: Timestamp Alignment
|
|
263
268
|
|
|
264
|
-
**Why
|
|
269
|
+
**Why align timestamps to interval boundaries?**
|
|
265
270
|
|
|
266
|
-
Because
|
|
271
|
+
Because candle APIs return data starting from exact interval boundaries:
|
|
267
272
|
|
|
268
273
|
```typescript
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
274
|
+
// 15-minute interval example:
|
|
275
|
+
when = 1704067920000 // 00:12:00
|
|
276
|
+
step = 15 // 15 minutes
|
|
277
|
+
stepMs = 15 * 60000 // 900000ms
|
|
278
|
+
|
|
279
|
+
// Alignment: round down to nearest interval boundary
|
|
280
|
+
alignedWhen = Math.floor(when / stepMs) * stepMs
|
|
281
|
+
// = Math.floor(1704067920000 / 900000) * 900000
|
|
282
|
+
// = 1704067200000 (00:00:00)
|
|
283
|
+
|
|
284
|
+
// Calculate since for 4 candles backwards:
|
|
285
|
+
since = alignedWhen - 4 * stepMs
|
|
286
|
+
// = 1704067200000 - 4 * 900000
|
|
287
|
+
// = 1704063600000 (23:00:00 previous day)
|
|
288
|
+
|
|
289
|
+
// Expected candles:
|
|
290
|
+
// [0] timestamp = 1704063600000 (23:00)
|
|
291
|
+
// [1] timestamp = 1704064500000 (23:15)
|
|
292
|
+
// [2] timestamp = 1704065400000 (23:30)
|
|
293
|
+
// [3] timestamp = 1704066300000 (23:45)
|
|
282
294
|
```
|
|
283
295
|
|
|
284
|
-
**
|
|
285
|
-
- ✅ `getCandles()`
|
|
286
|
-
- ✅ `getNextCandles()`
|
|
287
|
-
- ✅ `getRawCandles()`
|
|
288
|
-
- ✅ Cache read
|
|
289
|
-
- ✅ Cache write
|
|
296
|
+
**Validation is applied consistently across:**
|
|
297
|
+
- ✅ `getCandles()` - validates first timestamp and count
|
|
298
|
+
- ✅ `getNextCandles()` - validates first timestamp and count
|
|
299
|
+
- ✅ `getRawCandles()` - validates first timestamp and count
|
|
300
|
+
- ✅ Cache read - calculates exact expected timestamps
|
|
301
|
+
- ✅ Cache write - stores validated candles
|
|
290
302
|
|
|
291
|
-
**Result:**
|
|
303
|
+
**Result:** Deterministic candle retrieval with exact timestamp matching.
|
|
292
304
|
|
|
293
305
|
### 💭 What this means:
|
|
294
306
|
- `getCandles()` always returns data UP TO the current backtest timestamp using `async_hooks`
|