backtest-kit 3.0.5 → 3.0.7

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
@@ -1,4 +1,4 @@
1
- <img src="./assets/triangle.svg" height="45px" align="right">
1
+ <img src="https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/consciousness.svg" height="45px" align="right">
2
2
 
3
3
  # 🧿 Backtest Kit
4
4
 
package/build/index.cjs CHANGED
@@ -727,7 +727,7 @@ const INTERVAL_MINUTES$5 = {
727
727
  "6h": 360,
728
728
  "8h": 480,
729
729
  };
730
- const MS_PER_MINUTE$2 = 60000;
730
+ const MS_PER_MINUTE$3 = 60000;
731
731
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
732
732
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
733
733
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -1625,7 +1625,7 @@ class PersistCandleUtils {
1625
1625
  const isInitial = !this.getCandlesStorage.has(key);
1626
1626
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1627
1627
  await stateStorage.waitForInit(isInitial);
1628
- const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
1628
+ const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$3;
1629
1629
  // Calculate expected timestamps and fetch each candle directly
1630
1630
  const cachedCandles = [];
1631
1631
  for (let i = 0; i < limit; i++) {
@@ -1681,7 +1681,7 @@ class PersistCandleUtils {
1681
1681
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1682
1682
  await stateStorage.waitForInit(isInitial);
1683
1683
  // Calculate step in milliseconds to determine candle close time
1684
- const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
1684
+ const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$3;
1685
1685
  const now = Date.now();
1686
1686
  // Write each candle as a separate file, skipping incomplete candles
1687
1687
  for (const candle of candles) {
@@ -1938,7 +1938,7 @@ class PersistNotificationUtils {
1938
1938
  */
1939
1939
  const PersistNotificationAdapter = new PersistNotificationUtils();
1940
1940
 
1941
- const MS_PER_MINUTE$1 = 60000;
1941
+ const MS_PER_MINUTE$2 = 60000;
1942
1942
  const INTERVAL_MINUTES$4 = {
1943
1943
  "1m": 1,
1944
1944
  "3m": 3,
@@ -1969,7 +1969,7 @@ const INTERVAL_MINUTES$4 = {
1969
1969
  * @returns Aligned timestamp rounded down to interval boundary
1970
1970
  */
1971
1971
  const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
1972
- const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
1972
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
1973
1973
  return Math.floor(timestamp / intervalMs) * intervalMs;
1974
1974
  };
1975
1975
  /**
@@ -2116,7 +2116,7 @@ const WRITE_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(functoolsKit.queued(async
2116
2116
  const GET_CANDLES_FN = async (dto, since, self) => {
2117
2117
  const step = INTERVAL_MINUTES$4[dto.interval];
2118
2118
  const sinceTimestamp = since.getTime();
2119
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$1;
2119
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$2;
2120
2120
  // Try to read from cache first
2121
2121
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2122
2122
  if (cachedCandles !== null) {
@@ -2228,7 +2228,7 @@ class ClientExchange {
2228
2228
  if (!step) {
2229
2229
  throw new Error(`ClientExchange unknown interval=${interval}`);
2230
2230
  }
2231
- const stepMs = step * MS_PER_MINUTE$1;
2231
+ const stepMs = step * MS_PER_MINUTE$2;
2232
2232
  // Align when down to interval boundary
2233
2233
  const whenTimestamp = this.params.execution.context.when.getTime();
2234
2234
  const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
@@ -2309,7 +2309,7 @@ class ClientExchange {
2309
2309
  if (!step) {
2310
2310
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
2311
2311
  }
2312
- const stepMs = step * MS_PER_MINUTE$1;
2312
+ const stepMs = step * MS_PER_MINUTE$2;
2313
2313
  const now = Date.now();
2314
2314
  // Align when down to interval boundary
2315
2315
  const whenTimestamp = this.params.execution.context.when.getTime();
@@ -2477,7 +2477,7 @@ class ClientExchange {
2477
2477
  if (!step) {
2478
2478
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2479
2479
  }
2480
- const stepMs = step * MS_PER_MINUTE$1;
2480
+ const stepMs = step * MS_PER_MINUTE$2;
2481
2481
  const whenTimestamp = this.params.execution.context.when.getTime();
2482
2482
  const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
2483
2483
  let sinceTimestamp;
@@ -2604,7 +2604,7 @@ class ClientExchange {
2604
2604
  });
2605
2605
  const to = new Date(this.params.execution.context.when.getTime());
2606
2606
  const from = new Date(to.getTime() -
2607
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
2607
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
2608
2608
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2609
2609
  }
2610
2610
  }
@@ -35527,7 +35527,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
35527
35527
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
35528
35528
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
35529
35529
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
35530
- const MS_PER_MINUTE = 60000;
35530
+ const MS_PER_MINUTE$1 = 60000;
35531
35531
  /**
35532
35532
  * Gets current timestamp from execution context if available.
35533
35533
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -35612,7 +35612,7 @@ const INTERVAL_MINUTES$1 = {
35612
35612
  * @returns Aligned timestamp rounded down to interval boundary
35613
35613
  */
35614
35614
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
35615
- const intervalMs = intervalMinutes * MS_PER_MINUTE;
35615
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
35616
35616
  return Math.floor(timestamp / intervalMs) * intervalMs;
35617
35617
  };
35618
35618
  /**
@@ -35754,7 +35754,7 @@ class ExchangeInstance {
35754
35754
  if (!step) {
35755
35755
  throw new Error(`ExchangeInstance unknown interval=${interval}`);
35756
35756
  }
35757
- const stepMs = step * MS_PER_MINUTE;
35757
+ const stepMs = step * MS_PER_MINUTE$1;
35758
35758
  // Align when down to interval boundary
35759
35759
  const when = await GET_TIMESTAMP_FN();
35760
35760
  const whenTimestamp = when.getTime();
@@ -35931,7 +35931,7 @@ class ExchangeInstance {
35931
35931
  depth,
35932
35932
  });
35933
35933
  const to = await GET_TIMESTAMP_FN();
35934
- const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
35934
+ const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
35935
35935
  const isBacktest = await GET_BACKTEST_FN();
35936
35936
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
35937
35937
  };
@@ -35978,7 +35978,7 @@ class ExchangeInstance {
35978
35978
  if (!step) {
35979
35979
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
35980
35980
  }
35981
- const stepMs = step * MS_PER_MINUTE;
35981
+ const stepMs = step * MS_PER_MINUTE$1;
35982
35982
  const when = await GET_TIMESTAMP_FN();
35983
35983
  const nowTimestamp = when.getTime();
35984
35984
  const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
@@ -36251,6 +36251,7 @@ const CACHE_METHOD_NAME_FLUSH = "CacheUtils.flush";
36251
36251
  const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
36252
36252
  const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
36253
36253
  const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
36254
+ const MS_PER_MINUTE = 60000;
36254
36255
  const INTERVAL_MINUTES = {
36255
36256
  "1m": 1,
36256
36257
  "3m": 3,
@@ -36263,6 +36264,34 @@ const INTERVAL_MINUTES = {
36263
36264
  "6h": 360,
36264
36265
  "8h": 480,
36265
36266
  };
36267
+ /**
36268
+ * Aligns timestamp down to the nearest interval boundary.
36269
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
36270
+ *
36271
+ * @param timestamp - Timestamp in milliseconds
36272
+ * @param interval - Candle interval
36273
+ * @returns Aligned timestamp rounded down to interval boundary
36274
+ * @throws Error if interval is unknown
36275
+ *
36276
+ * @example
36277
+ * ```typescript
36278
+ * // Align to 15-minute boundary
36279
+ * const aligned = align(new Date("2025-10-01T00:35:00Z").getTime(), "15m");
36280
+ * // Returns timestamp for 2025-10-01T00:30:00Z
36281
+ *
36282
+ * // Align to 1-hour boundary
36283
+ * const aligned = align(new Date("2025-10-01T01:47:00Z").getTime(), "1h");
36284
+ * // Returns timestamp for 2025-10-01T01:00:00Z
36285
+ * ```
36286
+ */
36287
+ const align = (timestamp, interval) => {
36288
+ const intervalMinutes = INTERVAL_MINUTES[interval];
36289
+ if (!intervalMinutes) {
36290
+ throw new Error(`align: unknown interval=${interval}`);
36291
+ }
36292
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
36293
+ return Math.floor(timestamp / intervalMs) * intervalMs;
36294
+ };
36266
36295
  /**
36267
36296
  * Create a cache key string from strategy name, exchange name, and backtest mode.
36268
36297
  *
@@ -36278,6 +36307,11 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
36278
36307
  parts.push(backtest ? "backtest" : "live");
36279
36308
  return parts.join(":");
36280
36309
  };
36310
+ /**
36311
+ * A unique symbol representing a value that should never occur.
36312
+ * Used as default key when no key function is provided.
36313
+ */
36314
+ const NEVER_VALUE = Symbol("never");
36281
36315
  /**
36282
36316
  * Instance class for caching function results with timeframe-based invalidation.
36283
36317
  *
@@ -36286,6 +36320,7 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
36286
36320
  * Cache is invalidated when the current time moves to a different interval.
36287
36321
  *
36288
36322
  * @template T - Function type to cache
36323
+ * @template K - Key type for argument-based caching
36289
36324
  *
36290
36325
  * @example
36291
36326
  * ```typescript
@@ -36302,11 +36337,13 @@ class CacheInstance {
36302
36337
  *
36303
36338
  * @param fn - Function to cache
36304
36339
  * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
36340
+ * @param key - Optional key generator function for argument-based caching
36305
36341
  */
36306
- constructor(fn, interval) {
36342
+ constructor(fn, interval, key = () => NEVER_VALUE) {
36307
36343
  this.fn = fn;
36308
36344
  this.interval = interval;
36309
- /** Cache map storing results per strategy/exchange/mode combination */
36345
+ this.key = key;
36346
+ /** Cache map storing results per strategy/exchange/mode/argKey combination */
36310
36347
  this._cacheMap = new Map();
36311
36348
  /**
36312
36349
  * Execute function with caching based on timeframe intervals.
@@ -36353,13 +36390,15 @@ class CacheInstance {
36353
36390
  throw new Error(`CacheInstance unknown cache ttl interval=${this.interval}`);
36354
36391
  }
36355
36392
  }
36356
- const key = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36393
+ const contextKey = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36394
+ const argKey = String(this.key(args));
36395
+ const key = `${contextKey}:${argKey}`;
36357
36396
  const currentWhen = bt.executionContextService.context.when;
36358
36397
  const cached = this._cacheMap.get(key);
36359
36398
  if (cached) {
36360
- const stepMs = step * 60 * 1000;
36361
- const elapsed = currentWhen.getTime() - cached.when.getTime();
36362
- if (elapsed >= 0 && elapsed < stepMs) {
36399
+ const currentAligned = align(currentWhen.getTime(), this.interval);
36400
+ const cachedAligned = align(cached.when.getTime(), this.interval);
36401
+ if (currentAligned === cachedAligned) {
36363
36402
  return cached;
36364
36403
  }
36365
36404
  }
@@ -36371,13 +36410,13 @@ class CacheInstance {
36371
36410
  return newCache;
36372
36411
  };
36373
36412
  /**
36374
- * Clear cached value for current execution context.
36413
+ * Clear cached values for current execution context.
36375
36414
  *
36376
- * Removes the cached entry for the current strategy/exchange/mode combination
36415
+ * Removes all cached entries for the current strategy/exchange/mode combination
36377
36416
  * from this instance's cache map. The next `run()` call will recompute the value.
36378
36417
  *
36379
36418
  * Requires active execution context (strategy, exchange, backtest mode) and method context
36380
- * to determine which cache entry to clear.
36419
+ * to determine which cache entries to clear.
36381
36420
  *
36382
36421
  * @example
36383
36422
  * ```typescript
@@ -36385,14 +36424,19 @@ class CacheInstance {
36385
36424
  * const result1 = instance.run("BTCUSDT", 14); // Computed
36386
36425
  * const result2 = instance.run("BTCUSDT", 14); // Cached
36387
36426
  *
36388
- * instance.clear(); // Clear cache for current context
36427
+ * instance.clear(); // Clear all cache entries for current context
36389
36428
  *
36390
36429
  * const result3 = instance.run("BTCUSDT", 14); // Recomputed
36391
36430
  * ```
36392
36431
  */
36393
36432
  this.clear = () => {
36394
- const key = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36395
- this._cacheMap.delete(key);
36433
+ const contextKey = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36434
+ const prefix = `${contextKey}:`;
36435
+ for (const key of this._cacheMap.keys()) {
36436
+ if (key.startsWith(prefix)) {
36437
+ this._cacheMap.delete(key);
36438
+ }
36439
+ }
36396
36440
  };
36397
36441
  }
36398
36442
  }
@@ -36417,7 +36461,7 @@ class CacheUtils {
36417
36461
  * Memoized function to get or create CacheInstance for a function.
36418
36462
  * Each function gets its own isolated cache instance.
36419
36463
  */
36420
- this._getInstance = functoolsKit.memoize(([run]) => run, (run, interval) => new CacheInstance(run, interval));
36464
+ this._getInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
36421
36465
  /**
36422
36466
  * Wrap a function with caching based on timeframe intervals.
36423
36467
  *
@@ -36425,8 +36469,10 @@ class CacheUtils {
36425
36469
  * and invalidates based on the specified candle interval.
36426
36470
  *
36427
36471
  * @template T - Function type to cache
36472
+ * @template K - Key type for argument-based caching
36428
36473
  * @param run - Function to wrap with caching
36429
- * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
36474
+ * @param context.interval - Candle interval for cache invalidation (e.g., "1m", "1h")
36475
+ * @param context.key - Optional key generator function for argument-based caching
36430
36476
  * @returns Wrapped function with automatic caching
36431
36477
  *
36432
36478
  * @example
@@ -36436,9 +36482,17 @@ class CacheUtils {
36436
36482
  * return result;
36437
36483
  * };
36438
36484
  *
36439
- * const cachedCalculate = Cache.fn(calculateIndicator, "15m");
36440
- * const result = cachedCalculate("BTCUSDT", 14); // Computed
36441
- * const result2 = cachedCalculate("BTCUSDT", 14); // Cached (same 15m interval)
36485
+ * // Without key - single cache entry per context
36486
+ * const cachedCalculate = Cache.fn(calculateIndicator, { interval: "15m" });
36487
+ *
36488
+ * // With key - separate cache entries per symbol
36489
+ * const cachedCalculate = Cache.fn(calculateIndicator, {
36490
+ * interval: "15m",
36491
+ * key: ([symbol]) => symbol,
36492
+ * });
36493
+ * const result1 = cachedCalculate("BTCUSDT", 14); // Computed
36494
+ * const result2 = cachedCalculate("ETHUSDT", 14); // Computed (different key)
36495
+ * const result3 = cachedCalculate("BTCUSDT", 14); // Cached (same key, same interval)
36442
36496
  * ```
36443
36497
  */
36444
36498
  this.fn = (run, context) => {
@@ -36446,7 +36500,7 @@ class CacheUtils {
36446
36500
  context,
36447
36501
  });
36448
36502
  const wrappedFn = (...args) => {
36449
- const instance = this._getInstance(run, context.interval);
36503
+ const instance = this._getInstance(run, context.interval, context.key);
36450
36504
  return instance.run(...args).value;
36451
36505
  };
36452
36506
  return wrappedFn;
package/build/index.mjs CHANGED
@@ -707,7 +707,7 @@ const INTERVAL_MINUTES$5 = {
707
707
  "6h": 360,
708
708
  "8h": 480,
709
709
  };
710
- const MS_PER_MINUTE$2 = 60000;
710
+ const MS_PER_MINUTE$3 = 60000;
711
711
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
712
712
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
713
713
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -1605,7 +1605,7 @@ class PersistCandleUtils {
1605
1605
  const isInitial = !this.getCandlesStorage.has(key);
1606
1606
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1607
1607
  await stateStorage.waitForInit(isInitial);
1608
- const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
1608
+ const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$3;
1609
1609
  // Calculate expected timestamps and fetch each candle directly
1610
1610
  const cachedCandles = [];
1611
1611
  for (let i = 0; i < limit; i++) {
@@ -1661,7 +1661,7 @@ class PersistCandleUtils {
1661
1661
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1662
1662
  await stateStorage.waitForInit(isInitial);
1663
1663
  // Calculate step in milliseconds to determine candle close time
1664
- const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
1664
+ const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$3;
1665
1665
  const now = Date.now();
1666
1666
  // Write each candle as a separate file, skipping incomplete candles
1667
1667
  for (const candle of candles) {
@@ -1918,7 +1918,7 @@ class PersistNotificationUtils {
1918
1918
  */
1919
1919
  const PersistNotificationAdapter = new PersistNotificationUtils();
1920
1920
 
1921
- const MS_PER_MINUTE$1 = 60000;
1921
+ const MS_PER_MINUTE$2 = 60000;
1922
1922
  const INTERVAL_MINUTES$4 = {
1923
1923
  "1m": 1,
1924
1924
  "3m": 3,
@@ -1949,7 +1949,7 @@ const INTERVAL_MINUTES$4 = {
1949
1949
  * @returns Aligned timestamp rounded down to interval boundary
1950
1950
  */
1951
1951
  const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
1952
- const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
1952
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
1953
1953
  return Math.floor(timestamp / intervalMs) * intervalMs;
1954
1954
  };
1955
1955
  /**
@@ -2096,7 +2096,7 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
2096
2096
  const GET_CANDLES_FN = async (dto, since, self) => {
2097
2097
  const step = INTERVAL_MINUTES$4[dto.interval];
2098
2098
  const sinceTimestamp = since.getTime();
2099
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$1;
2099
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$2;
2100
2100
  // Try to read from cache first
2101
2101
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2102
2102
  if (cachedCandles !== null) {
@@ -2208,7 +2208,7 @@ class ClientExchange {
2208
2208
  if (!step) {
2209
2209
  throw new Error(`ClientExchange unknown interval=${interval}`);
2210
2210
  }
2211
- const stepMs = step * MS_PER_MINUTE$1;
2211
+ const stepMs = step * MS_PER_MINUTE$2;
2212
2212
  // Align when down to interval boundary
2213
2213
  const whenTimestamp = this.params.execution.context.when.getTime();
2214
2214
  const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
@@ -2289,7 +2289,7 @@ class ClientExchange {
2289
2289
  if (!step) {
2290
2290
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
2291
2291
  }
2292
- const stepMs = step * MS_PER_MINUTE$1;
2292
+ const stepMs = step * MS_PER_MINUTE$2;
2293
2293
  const now = Date.now();
2294
2294
  // Align when down to interval boundary
2295
2295
  const whenTimestamp = this.params.execution.context.when.getTime();
@@ -2457,7 +2457,7 @@ class ClientExchange {
2457
2457
  if (!step) {
2458
2458
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2459
2459
  }
2460
- const stepMs = step * MS_PER_MINUTE$1;
2460
+ const stepMs = step * MS_PER_MINUTE$2;
2461
2461
  const whenTimestamp = this.params.execution.context.when.getTime();
2462
2462
  const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
2463
2463
  let sinceTimestamp;
@@ -2584,7 +2584,7 @@ class ClientExchange {
2584
2584
  });
2585
2585
  const to = new Date(this.params.execution.context.when.getTime());
2586
2586
  const from = new Date(to.getTime() -
2587
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
2587
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
2588
2588
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2589
2589
  }
2590
2590
  }
@@ -35507,7 +35507,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
35507
35507
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
35508
35508
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
35509
35509
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
35510
- const MS_PER_MINUTE = 60000;
35510
+ const MS_PER_MINUTE$1 = 60000;
35511
35511
  /**
35512
35512
  * Gets current timestamp from execution context if available.
35513
35513
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -35592,7 +35592,7 @@ const INTERVAL_MINUTES$1 = {
35592
35592
  * @returns Aligned timestamp rounded down to interval boundary
35593
35593
  */
35594
35594
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
35595
- const intervalMs = intervalMinutes * MS_PER_MINUTE;
35595
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
35596
35596
  return Math.floor(timestamp / intervalMs) * intervalMs;
35597
35597
  };
35598
35598
  /**
@@ -35734,7 +35734,7 @@ class ExchangeInstance {
35734
35734
  if (!step) {
35735
35735
  throw new Error(`ExchangeInstance unknown interval=${interval}`);
35736
35736
  }
35737
- const stepMs = step * MS_PER_MINUTE;
35737
+ const stepMs = step * MS_PER_MINUTE$1;
35738
35738
  // Align when down to interval boundary
35739
35739
  const when = await GET_TIMESTAMP_FN();
35740
35740
  const whenTimestamp = when.getTime();
@@ -35911,7 +35911,7 @@ class ExchangeInstance {
35911
35911
  depth,
35912
35912
  });
35913
35913
  const to = await GET_TIMESTAMP_FN();
35914
- const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
35914
+ const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
35915
35915
  const isBacktest = await GET_BACKTEST_FN();
35916
35916
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
35917
35917
  };
@@ -35958,7 +35958,7 @@ class ExchangeInstance {
35958
35958
  if (!step) {
35959
35959
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
35960
35960
  }
35961
- const stepMs = step * MS_PER_MINUTE;
35961
+ const stepMs = step * MS_PER_MINUTE$1;
35962
35962
  const when = await GET_TIMESTAMP_FN();
35963
35963
  const nowTimestamp = when.getTime();
35964
35964
  const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
@@ -36231,6 +36231,7 @@ const CACHE_METHOD_NAME_FLUSH = "CacheUtils.flush";
36231
36231
  const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
36232
36232
  const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
36233
36233
  const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
36234
+ const MS_PER_MINUTE = 60000;
36234
36235
  const INTERVAL_MINUTES = {
36235
36236
  "1m": 1,
36236
36237
  "3m": 3,
@@ -36243,6 +36244,34 @@ const INTERVAL_MINUTES = {
36243
36244
  "6h": 360,
36244
36245
  "8h": 480,
36245
36246
  };
36247
+ /**
36248
+ * Aligns timestamp down to the nearest interval boundary.
36249
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
36250
+ *
36251
+ * @param timestamp - Timestamp in milliseconds
36252
+ * @param interval - Candle interval
36253
+ * @returns Aligned timestamp rounded down to interval boundary
36254
+ * @throws Error if interval is unknown
36255
+ *
36256
+ * @example
36257
+ * ```typescript
36258
+ * // Align to 15-minute boundary
36259
+ * const aligned = align(new Date("2025-10-01T00:35:00Z").getTime(), "15m");
36260
+ * // Returns timestamp for 2025-10-01T00:30:00Z
36261
+ *
36262
+ * // Align to 1-hour boundary
36263
+ * const aligned = align(new Date("2025-10-01T01:47:00Z").getTime(), "1h");
36264
+ * // Returns timestamp for 2025-10-01T01:00:00Z
36265
+ * ```
36266
+ */
36267
+ const align = (timestamp, interval) => {
36268
+ const intervalMinutes = INTERVAL_MINUTES[interval];
36269
+ if (!intervalMinutes) {
36270
+ throw new Error(`align: unknown interval=${interval}`);
36271
+ }
36272
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
36273
+ return Math.floor(timestamp / intervalMs) * intervalMs;
36274
+ };
36246
36275
  /**
36247
36276
  * Create a cache key string from strategy name, exchange name, and backtest mode.
36248
36277
  *
@@ -36258,6 +36287,11 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
36258
36287
  parts.push(backtest ? "backtest" : "live");
36259
36288
  return parts.join(":");
36260
36289
  };
36290
+ /**
36291
+ * A unique symbol representing a value that should never occur.
36292
+ * Used as default key when no key function is provided.
36293
+ */
36294
+ const NEVER_VALUE = Symbol("never");
36261
36295
  /**
36262
36296
  * Instance class for caching function results with timeframe-based invalidation.
36263
36297
  *
@@ -36266,6 +36300,7 @@ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
36266
36300
  * Cache is invalidated when the current time moves to a different interval.
36267
36301
  *
36268
36302
  * @template T - Function type to cache
36303
+ * @template K - Key type for argument-based caching
36269
36304
  *
36270
36305
  * @example
36271
36306
  * ```typescript
@@ -36282,11 +36317,13 @@ class CacheInstance {
36282
36317
  *
36283
36318
  * @param fn - Function to cache
36284
36319
  * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
36320
+ * @param key - Optional key generator function for argument-based caching
36285
36321
  */
36286
- constructor(fn, interval) {
36322
+ constructor(fn, interval, key = () => NEVER_VALUE) {
36287
36323
  this.fn = fn;
36288
36324
  this.interval = interval;
36289
- /** Cache map storing results per strategy/exchange/mode combination */
36325
+ this.key = key;
36326
+ /** Cache map storing results per strategy/exchange/mode/argKey combination */
36290
36327
  this._cacheMap = new Map();
36291
36328
  /**
36292
36329
  * Execute function with caching based on timeframe intervals.
@@ -36333,13 +36370,15 @@ class CacheInstance {
36333
36370
  throw new Error(`CacheInstance unknown cache ttl interval=${this.interval}`);
36334
36371
  }
36335
36372
  }
36336
- const key = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36373
+ const contextKey = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36374
+ const argKey = String(this.key(args));
36375
+ const key = `${contextKey}:${argKey}`;
36337
36376
  const currentWhen = bt.executionContextService.context.when;
36338
36377
  const cached = this._cacheMap.get(key);
36339
36378
  if (cached) {
36340
- const stepMs = step * 60 * 1000;
36341
- const elapsed = currentWhen.getTime() - cached.when.getTime();
36342
- if (elapsed >= 0 && elapsed < stepMs) {
36379
+ const currentAligned = align(currentWhen.getTime(), this.interval);
36380
+ const cachedAligned = align(cached.when.getTime(), this.interval);
36381
+ if (currentAligned === cachedAligned) {
36343
36382
  return cached;
36344
36383
  }
36345
36384
  }
@@ -36351,13 +36390,13 @@ class CacheInstance {
36351
36390
  return newCache;
36352
36391
  };
36353
36392
  /**
36354
- * Clear cached value for current execution context.
36393
+ * Clear cached values for current execution context.
36355
36394
  *
36356
- * Removes the cached entry for the current strategy/exchange/mode combination
36395
+ * Removes all cached entries for the current strategy/exchange/mode combination
36357
36396
  * from this instance's cache map. The next `run()` call will recompute the value.
36358
36397
  *
36359
36398
  * Requires active execution context (strategy, exchange, backtest mode) and method context
36360
- * to determine which cache entry to clear.
36399
+ * to determine which cache entries to clear.
36361
36400
  *
36362
36401
  * @example
36363
36402
  * ```typescript
@@ -36365,14 +36404,19 @@ class CacheInstance {
36365
36404
  * const result1 = instance.run("BTCUSDT", 14); // Computed
36366
36405
  * const result2 = instance.run("BTCUSDT", 14); // Cached
36367
36406
  *
36368
- * instance.clear(); // Clear cache for current context
36407
+ * instance.clear(); // Clear all cache entries for current context
36369
36408
  *
36370
36409
  * const result3 = instance.run("BTCUSDT", 14); // Recomputed
36371
36410
  * ```
36372
36411
  */
36373
36412
  this.clear = () => {
36374
- const key = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36375
- this._cacheMap.delete(key);
36413
+ const contextKey = CREATE_KEY_FN(bt.methodContextService.context.strategyName, bt.methodContextService.context.exchangeName, bt.methodContextService.context.frameName, bt.executionContextService.context.backtest);
36414
+ const prefix = `${contextKey}:`;
36415
+ for (const key of this._cacheMap.keys()) {
36416
+ if (key.startsWith(prefix)) {
36417
+ this._cacheMap.delete(key);
36418
+ }
36419
+ }
36376
36420
  };
36377
36421
  }
36378
36422
  }
@@ -36397,7 +36441,7 @@ class CacheUtils {
36397
36441
  * Memoized function to get or create CacheInstance for a function.
36398
36442
  * Each function gets its own isolated cache instance.
36399
36443
  */
36400
- this._getInstance = memoize(([run]) => run, (run, interval) => new CacheInstance(run, interval));
36444
+ this._getInstance = memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
36401
36445
  /**
36402
36446
  * Wrap a function with caching based on timeframe intervals.
36403
36447
  *
@@ -36405,8 +36449,10 @@ class CacheUtils {
36405
36449
  * and invalidates based on the specified candle interval.
36406
36450
  *
36407
36451
  * @template T - Function type to cache
36452
+ * @template K - Key type for argument-based caching
36408
36453
  * @param run - Function to wrap with caching
36409
- * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
36454
+ * @param context.interval - Candle interval for cache invalidation (e.g., "1m", "1h")
36455
+ * @param context.key - Optional key generator function for argument-based caching
36410
36456
  * @returns Wrapped function with automatic caching
36411
36457
  *
36412
36458
  * @example
@@ -36416,9 +36462,17 @@ class CacheUtils {
36416
36462
  * return result;
36417
36463
  * };
36418
36464
  *
36419
- * const cachedCalculate = Cache.fn(calculateIndicator, "15m");
36420
- * const result = cachedCalculate("BTCUSDT", 14); // Computed
36421
- * const result2 = cachedCalculate("BTCUSDT", 14); // Cached (same 15m interval)
36465
+ * // Without key - single cache entry per context
36466
+ * const cachedCalculate = Cache.fn(calculateIndicator, { interval: "15m" });
36467
+ *
36468
+ * // With key - separate cache entries per symbol
36469
+ * const cachedCalculate = Cache.fn(calculateIndicator, {
36470
+ * interval: "15m",
36471
+ * key: ([symbol]) => symbol,
36472
+ * });
36473
+ * const result1 = cachedCalculate("BTCUSDT", 14); // Computed
36474
+ * const result2 = cachedCalculate("ETHUSDT", 14); // Computed (different key)
36475
+ * const result3 = cachedCalculate("BTCUSDT", 14); // Cached (same key, same interval)
36422
36476
  * ```
36423
36477
  */
36424
36478
  this.fn = (run, context) => {
@@ -36426,7 +36480,7 @@ class CacheUtils {
36426
36480
  context,
36427
36481
  });
36428
36482
  const wrappedFn = (...args) => {
36429
- const instance = this._getInstance(run, context.interval);
36483
+ const instance = this._getInstance(run, context.interval, context.key);
36430
36484
  return instance.run(...args).value;
36431
36485
  };
36432
36486
  return wrappedFn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -13901,8 +13901,10 @@ declare class CacheUtils {
13901
13901
  * and invalidates based on the specified candle interval.
13902
13902
  *
13903
13903
  * @template T - Function type to cache
13904
+ * @template K - Key type for argument-based caching
13904
13905
  * @param run - Function to wrap with caching
13905
- * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
13906
+ * @param context.interval - Candle interval for cache invalidation (e.g., "1m", "1h")
13907
+ * @param context.key - Optional key generator function for argument-based caching
13906
13908
  * @returns Wrapped function with automatic caching
13907
13909
  *
13908
13910
  * @example
@@ -13912,13 +13914,22 @@ declare class CacheUtils {
13912
13914
  * return result;
13913
13915
  * };
13914
13916
  *
13915
- * const cachedCalculate = Cache.fn(calculateIndicator, "15m");
13916
- * const result = cachedCalculate("BTCUSDT", 14); // Computed
13917
- * const result2 = cachedCalculate("BTCUSDT", 14); // Cached (same 15m interval)
13917
+ * // Without key - single cache entry per context
13918
+ * const cachedCalculate = Cache.fn(calculateIndicator, { interval: "15m" });
13919
+ *
13920
+ * // With key - separate cache entries per symbol
13921
+ * const cachedCalculate = Cache.fn(calculateIndicator, {
13922
+ * interval: "15m",
13923
+ * key: ([symbol]) => symbol,
13924
+ * });
13925
+ * const result1 = cachedCalculate("BTCUSDT", 14); // Computed
13926
+ * const result2 = cachedCalculate("ETHUSDT", 14); // Computed (different key)
13927
+ * const result3 = cachedCalculate("BTCUSDT", 14); // Cached (same key, same interval)
13918
13928
  * ```
13919
13929
  */
13920
- fn: <T extends Function>(run: T, context: {
13930
+ fn: <T extends Function, K = symbol>(run: T, context: {
13921
13931
  interval: CandleInterval;
13932
+ key?: (args: Parameters<T>) => K;
13922
13933
  }) => T;
13923
13934
  /**
13924
13935
  * Flush (remove) cached CacheInstance for a specific function or all functions.