backtest-kit 2.2.26 → 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 CHANGED
@@ -205,14 +205,109 @@ Backtest Kit is **not a data-processing library** - it is a **time execution eng
205
205
 
206
206
  ### 🔍 How getCandles Works
207
207
 
208
- backtest-kit uses Node.js `AsyncLocalStorage` to automatically provide
209
- temporal time context to your strategies. Exclusive (aka `[startTime, endTime)`) candle limit being used
208
+ backtest-kit uses Node.js `AsyncLocalStorage` to automatically provide
209
+ temporal time context to your strategies.
210
+
211
+ <details>
212
+ <summary>
213
+ The Math
214
+ </summary>
215
+
216
+ For a candle with:
217
+ - `timestamp` = candle open time (openTime)
218
+ - `stepMs` = interval duration (e.g., 60000ms for "1m")
219
+ - Candle close time = `timestamp + stepMs`
220
+
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
228
+
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
234
+
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
239
+ - Throws error in live mode to prevent look-ahead bias
240
+ - Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles after aligned when
241
+
242
+ - `getRawCandles(symbol, interval, limit?, sDate?, eDate?)` - Flexible parameter combinations:
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)
249
+
250
+ **Persistent 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
254
+
255
+ </details>
256
+
257
+ #### Candle Timestamp Convention:
258
+
259
+ According to this `timestamp` of a candle in backtest-kit is exactly the `openTime`, not ~~`closeTime`~~
260
+
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`
266
+
267
+ ### 🔬 Technical Details: Timestamp Alignment
268
+
269
+ **Why align timestamps to interval boundaries?**
270
+
271
+ Because candle APIs return data starting from exact interval boundaries:
272
+
273
+ ```typescript
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)
294
+ ```
295
+
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
302
+
303
+ **Result:** Deterministic candle retrieval with exact timestamp matching.
210
304
 
211
305
  ### 💭 What this means:
212
306
  - `getCandles()` always returns data UP TO the current backtest timestamp using `async_hooks`
213
307
  - Multi-timeframe data is automatically synchronized
214
- - **Impossible to introduce look-ahead bias**
308
+ - **Impossible to introduce look-ahead bias** - all time boundaries are enforced
215
309
  - Same code works in both backtest and live modes
310
+ - Boundary semantics prevent edge cases in signal generation
216
311
 
217
312
 
218
313
  ## 🧠 Two Ways to Run the Engine