backtest-kit 2.3.1 → 2.3.3
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 +85 -54
- package/build/index.cjs +928 -266
- package/build/index.mjs +929 -268
- package/package.json +2 -2
- package/types.d.ts +383 -17
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
10
10
|
[](https://npmjs.org/package/backtest-kit)
|
|
11
11
|
[]()
|
|
12
|
+
[](https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml)
|
|
12
13
|
|
|
13
14
|
Build reliable trading systems: backtest on historical data, deploy live bots with recovery, and optimize strategies using LLMs like Ollama.
|
|
14
15
|
|
|
@@ -212,83 +213,113 @@ temporal time context to your strategies.
|
|
|
212
213
|
<summary>
|
|
213
214
|
The Math
|
|
214
215
|
</summary>
|
|
215
|
-
|
|
216
|
+
|
|
216
217
|
For a candle with:
|
|
217
|
-
- `timestamp` = candle open time
|
|
218
|
+
- `timestamp` = candle open time (openTime)
|
|
218
219
|
- `stepMs` = interval duration (e.g., 60000ms for "1m")
|
|
219
220
|
- Candle close time = `timestamp + stepMs`
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
- `
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
222
|
+
**Alignment:** All timestamps are aligned down to interval boundary.
|
|
223
|
+
For example, for 15m interval: 00:17 → 00:15, 00:44 → 00:30
|
|
224
|
+
|
|
225
|
+
**Adapter contract:**
|
|
226
|
+
- First candle.timestamp must equal aligned `since`
|
|
227
|
+
- Adapter must return exactly `limit` candles
|
|
228
|
+
- Sequential timestamps: `since + i * stepMs` for i = 0..limit-1
|
|
229
|
+
|
|
230
|
+
**How `since` is calculated from `when`:**
|
|
231
|
+
- `when` = current execution context time (from AsyncLocalStorage)
|
|
232
|
+
- `alignedWhen` = `Math.floor(when / stepMs) * stepMs` (aligned down to interval boundary)
|
|
233
|
+
- `since` = `alignedWhen - limit * stepMs` (go back `limit` candles from aligned when)
|
|
234
|
+
|
|
235
|
+
**Boundary semantics (inclusive/exclusive):**
|
|
236
|
+
- `since` is always **inclusive** — first candle has `timestamp === since`
|
|
237
|
+
- Exactly `limit` candles are returned
|
|
238
|
+
- Last candle has `timestamp === since + (limit - 1) * stepMs` — **inclusive**
|
|
239
|
+
- For `getCandles`: `alignedWhen` is **exclusive** — candle at that timestamp is NOT included (it's a pending/incomplete candle)
|
|
240
|
+
- For `getRawCandles`: `eDate` is **exclusive** — candle at that timestamp is NOT included (it's a pending/incomplete candle)
|
|
241
|
+
- For `getNextCandles`: `alignedWhen` is **inclusive** — first candle starts at `alignedWhen` (it's the current candle for backtest, already closed in historical data)
|
|
242
|
+
|
|
243
|
+
- `getCandles(symbol, interval, limit)` - Returns exactly `limit` candles
|
|
244
|
+
- Aligns `when` down to interval boundary
|
|
245
|
+
- Calculates `since = alignedWhen - limit * stepMs`
|
|
246
|
+
- **since — inclusive**, first candle.timestamp === since
|
|
247
|
+
- **alignedWhen — exclusive**, candle at alignedWhen is NOT returned
|
|
248
|
+
- Range: `[since, alignedWhen)` — half-open interval
|
|
249
|
+
- Example: `getCandles("BTCUSDT", "1m", 100)` returns 100 candles ending before aligned when
|
|
250
|
+
|
|
251
|
+
- `getNextCandles(symbol, interval, limit)` - Returns exactly `limit` candles (backtest only)
|
|
252
|
+
- Aligns `when` down to interval boundary
|
|
253
|
+
- `since = alignedWhen` (starts from aligned when, going forward)
|
|
254
|
+
- **since — inclusive**, first candle.timestamp === since
|
|
255
|
+
- Range: `[alignedWhen, alignedWhen + limit * stepMs)` — half-open interval
|
|
235
256
|
- Throws error in live mode to prevent look-ahead bias
|
|
236
|
-
- Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles
|
|
257
|
+
- Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles starting from aligned when
|
|
237
258
|
|
|
238
259
|
- `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
|
|
260
|
+
- `(limit)` - since = alignedWhen - limit * stepMs, range `[since, alignedWhen)`
|
|
261
|
+
- `(limit, sDate)` - since = align(sDate), returns `limit` candles forward, range `[since, since + limit * stepMs)`
|
|
262
|
+
- `(limit, undefined, eDate)` - since = align(eDate) - limit * stepMs, **eDate — exclusive**, range `[since, eDate)`
|
|
263
|
+
- `(undefined, sDate, eDate)` - since = align(sDate), limit calculated from range, **sDate — inclusive, eDate — exclusive**, range `[sDate, eDate)`
|
|
264
|
+
- `(limit, sDate, eDate)` - since = align(sDate), returns `limit` candles, **sDate — inclusive**
|
|
265
|
+
- All combinations respect look-ahead bias protection (eDate/endTime <= when)
|
|
246
266
|
|
|
247
267
|
**Persistent Cache:**
|
|
248
|
-
-
|
|
249
|
-
-
|
|
250
|
-
- Cache
|
|
268
|
+
- Cache lookup calculates expected timestamps: `since + i * stepMs` for i = 0..limit-1
|
|
269
|
+
- Returns all candles if found, null if any missing (cache miss)
|
|
270
|
+
- Cache and runtime use identical timestamp calculation logic
|
|
251
271
|
|
|
252
272
|
</details>
|
|
253
273
|
|
|
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.
|
|
274
|
+
#### Candle Timestamp Convention:
|
|
257
275
|
|
|
258
276
|
According to this `timestamp` of a candle in backtest-kit is exactly the `openTime`, not ~~`closeTime`~~
|
|
259
277
|
|
|
260
|
-
**Key
|
|
278
|
+
**Key principles:**
|
|
279
|
+
- All timestamps are aligned down to interval boundary
|
|
280
|
+
- First candle.timestamp must equal aligned `since`
|
|
281
|
+
- Adapter must return exactly `limit` candles
|
|
282
|
+
- Sequential timestamps: `since + i * stepMs`
|
|
261
283
|
|
|
262
|
-
### 🔬 Technical Details:
|
|
284
|
+
### 🔬 Technical Details: Timestamp Alignment
|
|
263
285
|
|
|
264
|
-
**Why
|
|
286
|
+
**Why align timestamps to interval boundaries?**
|
|
265
287
|
|
|
266
|
-
Because
|
|
288
|
+
Because candle APIs return data starting from exact interval boundaries:
|
|
267
289
|
|
|
268
290
|
```typescript
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
// 15-minute interval example:
|
|
292
|
+
when = 1704067920000 // 00:12:00
|
|
293
|
+
step = 15 // 15 minutes
|
|
294
|
+
stepMs = 15 * 60000 // 900000ms
|
|
295
|
+
|
|
296
|
+
// Alignment: round down to nearest interval boundary
|
|
297
|
+
alignedWhen = Math.floor(when / stepMs) * stepMs
|
|
298
|
+
// = Math.floor(1704067920000 / 900000) * 900000
|
|
299
|
+
// = 1704067200000 (00:00:00)
|
|
300
|
+
|
|
301
|
+
// Calculate since for 4 candles backwards:
|
|
302
|
+
since = alignedWhen - 4 * stepMs
|
|
303
|
+
// = 1704067200000 - 4 * 900000
|
|
304
|
+
// = 1704063600000 (23:00:00 previous day)
|
|
305
|
+
|
|
306
|
+
// Expected candles:
|
|
307
|
+
// [0] timestamp = 1704063600000 (23:00)
|
|
308
|
+
// [1] timestamp = 1704064500000 (23:15)
|
|
309
|
+
// [2] timestamp = 1704065400000 (23:30)
|
|
310
|
+
// [3] timestamp = 1704066300000 (23:45)
|
|
282
311
|
```
|
|
283
312
|
|
|
284
|
-
**
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- ✅ `
|
|
288
|
-
- ✅
|
|
289
|
-
- ✅
|
|
313
|
+
**Pending candle exclusion:** The candle at `00:00:00` (alignedWhen) is NOT included in the result. At `when=00:12:00`, this candle covers the period `[00:00, 00:15)` and is still open (pending). Pending candles have incomplete OHLCV data that would distort technical indicators. Only fully closed candles are returned.
|
|
314
|
+
|
|
315
|
+
**Validation is applied consistently across:**
|
|
316
|
+
- ✅ `getCandles()` - validates first timestamp and count
|
|
317
|
+
- ✅ `getNextCandles()` - validates first timestamp and count
|
|
318
|
+
- ✅ `getRawCandles()` - validates first timestamp and count
|
|
319
|
+
- ✅ Cache read - calculates exact expected timestamps
|
|
320
|
+
- ✅ Cache write - stores validated candles
|
|
290
321
|
|
|
291
|
-
**Result:**
|
|
322
|
+
**Result:** Deterministic candle retrieval with exact timestamp matching.
|
|
292
323
|
|
|
293
324
|
### 💭 What this means:
|
|
294
325
|
- `getCandles()` always returns data UP TO the current backtest timestamp using `async_hooks`
|