backtest-kit 2.2.26 → 2.3.1

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,97 @@ 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
218
+ - `stepMs` = interval duration (e.g., 60000ms for "1m")
219
+ - Candle close time = `timestamp + stepMs`
220
+
221
+ The candle is included if: `timestamp + stepMs < upperBoundary`
222
+
223
+ - `getCandles(symbol, interval, limit)` - Returns data in range `(when - limit*interval, when)`
224
+ - Fetches historical candles backwards from execution context time
225
+ - Only fully closed candles are included (candle must close before `when`)
226
+ - Lower bound: `candle.timestamp > sinceTimestamp` (exclusive)
227
+ - Upper bound: `candle.timestamp + stepMs < when` (exclusive)
228
+ - Example: `getCandles("BTCUSDT", "1m", 100)` returns 100 candles ending before current time
229
+
230
+ - `getNextCandles(symbol, interval, limit)` - Returns data in range `(when, when + limit*interval)`
231
+ - Fetches future candles forwards from execution context time (backtest only)
232
+ - Only fully closed candles are included
233
+ - Lower bound: `candle.timestamp > when` (exclusive)
234
+ - Upper bound: `candle.timestamp + stepMs < endTime` (exclusive)
235
+ - Throws error in live mode to prevent look-ahead bias
236
+ - Example: `getNextCandles("BTCUSDT", "1m", 10)` returns next 10 candles after current time
237
+
238
+ - `getRawCandles(symbol, interval, limit?, sDate?, eDate?)` - Flexible parameter combinations:
239
+ - `(limit)` - Returns data in range `(now - limit*interval, now)`
240
+ - `(limit, sDate)` - Returns data in range `(sDate, sDate + limit*interval)`
241
+ - `(limit, undefined, eDate)` - Returns data in range `(eDate - limit*interval, eDate)`
242
+ - `(undefined, sDate, eDate)` - Returns data in range `(sDate, eDate)`, limit calculated from range
243
+ - `(limit, sDate, eDate)` - Returns data in range `(sDate, eDate)`, limit used only for fetch size
244
+ - All combinations use: `candle.timestamp > sDate && candle.timestamp + stepMs < eDate`
245
+ - All combinations respect exclusive boundaries and look-ahead bias protection
246
+
247
+ **Persistent Cache:**
248
+ - Candle cache uses identical boundary semantics: `timestamp > sinceTimestamp && timestamp + stepMs < untilTimestamp`
249
+ - Cache and runtime filters are synchronized to prevent inconsistencies
250
+ - Cache returns only candles that match the requested time range exactly
251
+
252
+ </details>
253
+
254
+ #### Boundary Semantics:
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
+
258
+ According to this `timestamp` of a candle in backtest-kit is exactly the `openTime`, not ~~`closeTime`~~
259
+
260
+ **Key principle:** A candle is included only if it **fully closed** before the upper boundary.
261
+
262
+ ### 🔬 Technical Details: The `+ stepMs` Check
263
+
264
+ **Why check `candle.timestamp + stepMs < upperBoundary` instead of just `candle.timestamp < upperBoundary`?**
265
+
266
+ Because a candle's **timestamp is when it opens**, not when it closes:
267
+
268
+ ```typescript
269
+ // 1-minute candle example:
270
+ timestamp = 1000 // Candle opens at 1000ms
271
+ stepMs = 60000 // Duration: 60 seconds
272
+ // Candle closes at: 1000 + 60000 = 61000ms
273
+
274
+ // Without + stepMs (WRONG):
275
+ candle.timestamp < 61000
276
+ 1000 < 61000 // TRUE - includes candle that hasn't finished yet!
277
+
278
+ // With + stepMs (CORRECT):
279
+ candle.timestamp + stepMs < 61000
280
+ 1000 + 60000 < 61000
281
+ 61000 < 61000 // FALSE - correctly excludes unclosed candle
282
+ ```
283
+
284
+ **This check is applied consistently across:**
285
+ - ✅ `getCandles()` filtering
286
+ - ✅ `getNextCandles()` filtering
287
+ - ✅ `getRawCandles()` filtering (all parameter combinations)
288
+ - ✅ Cache read operations
289
+ - ✅ Cache write operations
290
+
291
+ **Result:** Zero chance of including incomplete or "forming" candles in your strategy logic.
210
292
 
211
293
  ### 💭 What this means:
212
294
  - `getCandles()` always returns data UP TO the current backtest timestamp using `async_hooks`
213
295
  - Multi-timeframe data is automatically synchronized
214
- - **Impossible to introduce look-ahead bias**
296
+ - **Impossible to introduce look-ahead bias** - all time boundaries are enforced
215
297
  - Same code works in both backtest and live modes
298
+ - Boundary semantics prevent edge cases in signal generation
216
299
 
217
300
 
218
301
  ## 🧠 Two Ways to Run the Engine