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 +1 -1
- package/build/index.cjs +87 -33
- package/build/index.mjs +87 -33
- package/package.json +1 -1
- package/types.d.ts +16 -5
package/README.md
CHANGED
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
|
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
|
|
36361
|
-
const
|
|
36362
|
-
if (
|
|
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
|
|
36413
|
+
* Clear cached values for current execution context.
|
|
36375
36414
|
*
|
|
36376
|
-
* Removes
|
|
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
|
|
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
|
|
36395
|
-
|
|
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
|
-
*
|
|
36440
|
-
* const
|
|
36441
|
-
*
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
|
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
|
|
36341
|
-
const
|
|
36342
|
-
if (
|
|
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
|
|
36393
|
+
* Clear cached values for current execution context.
|
|
36355
36394
|
*
|
|
36356
|
-
* Removes
|
|
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
|
|
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
|
|
36375
|
-
|
|
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
|
-
*
|
|
36420
|
-
* const
|
|
36421
|
-
*
|
|
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
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
|
-
*
|
|
13916
|
-
* const
|
|
13917
|
-
*
|
|
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.
|