backtest-kit 3.0.17 → 3.1.0

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/build/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createActivator } from 'di-kit';
2
2
  import { scoped } from 'di-scoped';
3
- import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
3
+ import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, Source, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
4
4
  import * as fs from 'fs/promises';
5
5
  import fs__default, { stat, opendir, readFile } from 'fs/promises';
6
6
  import path, { join, dirname } from 'path';
@@ -422,6 +422,13 @@ const GLOBAL_CONFIG = {
422
422
  * Default: 50 signals
423
423
  */
424
424
  CC_MAX_SIGNALS: 50,
425
+ /**
426
+ * Enables mutex locking for candle fetching to prevent concurrent fetches of the same candles.
427
+ * This can help avoid redundant API calls and ensure data consistency when multiple processes/threads attempt to fetch candles simultaneously.
428
+ *
429
+ * Default: true (mutex locking enabled for candle fetching)
430
+ */
431
+ CC_ENABLE_CANDLE_FETCH_MUTEX: true,
425
432
  };
426
433
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
427
434
 
@@ -692,10 +699,10 @@ async function writeFileAtomic(file, data, options = {}) {
692
699
  }
693
700
  }
694
701
 
695
- var _a$2;
702
+ var _a$3;
696
703
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
697
704
  // Calculate step in milliseconds for candle close time validation
698
- const INTERVAL_MINUTES$7 = {
705
+ const INTERVAL_MINUTES$8 = {
699
706
  "1m": 1,
700
707
  "3m": 3,
701
708
  "5m": 5,
@@ -707,7 +714,7 @@ const INTERVAL_MINUTES$7 = {
707
714
  "6h": 360,
708
715
  "8h": 480,
709
716
  };
710
- const MS_PER_MINUTE$5 = 60000;
717
+ const MS_PER_MINUTE$6 = 60000;
711
718
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
712
719
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
713
720
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -810,7 +817,7 @@ class PersistBase {
810
817
  constructor(entityName, baseDir = join(process.cwd(), "logs/data")) {
811
818
  this.entityName = entityName;
812
819
  this.baseDir = baseDir;
813
- this[_a$2] = singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
820
+ this[_a$3] = singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
814
821
  bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
815
822
  entityName: this.entityName,
816
823
  baseDir,
@@ -913,7 +920,7 @@ class PersistBase {
913
920
  }
914
921
  }
915
922
  }
916
- _a$2 = BASE_WAIT_FOR_INIT_SYMBOL;
923
+ _a$3 = BASE_WAIT_FOR_INIT_SYMBOL;
917
924
  // @ts-ignore
918
925
  PersistBase = makeExtendable(PersistBase);
919
926
  /**
@@ -1607,7 +1614,7 @@ class PersistCandleUtils {
1607
1614
  const isInitial = !this.getCandlesStorage.has(key);
1608
1615
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1609
1616
  await stateStorage.waitForInit(isInitial);
1610
- const stepMs = INTERVAL_MINUTES$7[interval] * MS_PER_MINUTE$5;
1617
+ const stepMs = INTERVAL_MINUTES$8[interval] * MS_PER_MINUTE$6;
1611
1618
  // Calculate expected timestamps and fetch each candle directly
1612
1619
  const cachedCandles = [];
1613
1620
  for (let i = 0; i < limit; i++) {
@@ -1663,7 +1670,7 @@ class PersistCandleUtils {
1663
1670
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1664
1671
  await stateStorage.waitForInit(isInitial);
1665
1672
  // Calculate step in milliseconds to determine candle close time
1666
- const stepMs = INTERVAL_MINUTES$7[interval] * MS_PER_MINUTE$5;
1673
+ const stepMs = INTERVAL_MINUTES$8[interval] * MS_PER_MINUTE$6;
1667
1674
  const now = Date.now();
1668
1675
  // Write each candle as a separate file, skipping incomplete candles
1669
1676
  for (const candle of candles) {
@@ -1920,8 +1927,71 @@ class PersistNotificationUtils {
1920
1927
  */
1921
1928
  const PersistNotificationAdapter = new PersistNotificationUtils();
1922
1929
 
1923
- const MS_PER_MINUTE$4 = 60000;
1924
- const INTERVAL_MINUTES$6 = {
1930
+ var _a$2, _b$2;
1931
+ const BUSY_DELAY = 100;
1932
+ const SET_BUSY_SYMBOL = Symbol("setBusy");
1933
+ const GET_BUSY_SYMBOL = Symbol("getBusy");
1934
+ const ACQUIRE_LOCK_SYMBOL = Symbol("acquireLock");
1935
+ const RELEASE_LOCK_SYMBOL = Symbol("releaseLock");
1936
+ const ACQUIRE_LOCK_FN = async (self) => {
1937
+ while (self[GET_BUSY_SYMBOL]()) {
1938
+ await sleep(BUSY_DELAY);
1939
+ }
1940
+ self[SET_BUSY_SYMBOL](true);
1941
+ };
1942
+ class Lock {
1943
+ constructor() {
1944
+ this._isBusy = 0;
1945
+ this[_a$2] = queued(ACQUIRE_LOCK_FN);
1946
+ this[_b$2] = () => this[SET_BUSY_SYMBOL](false);
1947
+ this.acquireLock = async () => {
1948
+ await this[ACQUIRE_LOCK_SYMBOL](this);
1949
+ };
1950
+ this.releaseLock = async () => {
1951
+ await this[RELEASE_LOCK_SYMBOL]();
1952
+ };
1953
+ }
1954
+ [SET_BUSY_SYMBOL](isBusy) {
1955
+ this._isBusy += isBusy ? 1 : -1;
1956
+ if (this._isBusy < 0) {
1957
+ throw new Error("Extra release in finally block");
1958
+ }
1959
+ }
1960
+ [GET_BUSY_SYMBOL]() {
1961
+ return !!this._isBusy;
1962
+ }
1963
+ }
1964
+ _a$2 = ACQUIRE_LOCK_SYMBOL, _b$2 = RELEASE_LOCK_SYMBOL;
1965
+
1966
+ const METHOD_NAME_ACQUIRE_LOCK = "CandleUtils.acquireLock";
1967
+ const METHOD_NAME_RELEASE_LOCK = "CandleUtils.releaseLock";
1968
+ class CandleUtils {
1969
+ constructor() {
1970
+ this._lock = new Lock();
1971
+ this.acquireLock = async (source) => {
1972
+ bt.loggerService.info(METHOD_NAME_ACQUIRE_LOCK, {
1973
+ source,
1974
+ });
1975
+ if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
1976
+ return;
1977
+ }
1978
+ return await this._lock.acquireLock();
1979
+ };
1980
+ this.releaseLock = async (source) => {
1981
+ bt.loggerService.info(METHOD_NAME_RELEASE_LOCK, {
1982
+ source,
1983
+ });
1984
+ if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
1985
+ return;
1986
+ }
1987
+ return await this._lock.releaseLock();
1988
+ };
1989
+ }
1990
+ }
1991
+ const Candle = new CandleUtils();
1992
+
1993
+ const MS_PER_MINUTE$5 = 60000;
1994
+ const INTERVAL_MINUTES$7 = {
1925
1995
  "1m": 1,
1926
1996
  "3m": 3,
1927
1997
  "5m": 5,
@@ -1951,7 +2021,7 @@ const INTERVAL_MINUTES$6 = {
1951
2021
  * @returns Aligned timestamp rounded down to interval boundary
1952
2022
  */
1953
2023
  const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
1954
- const intervalMs = intervalMinutes * MS_PER_MINUTE$4;
2024
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$5;
1955
2025
  return Math.floor(timestamp / intervalMs) * intervalMs;
1956
2026
  };
1957
2027
  /**
@@ -2096,37 +2166,43 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
2096
2166
  * @returns Promise resolving to array of candle data
2097
2167
  */
2098
2168
  const GET_CANDLES_FN = async (dto, since, self) => {
2099
- const step = INTERVAL_MINUTES$6[dto.interval];
2169
+ const step = INTERVAL_MINUTES$7[dto.interval];
2100
2170
  const sinceTimestamp = since.getTime();
2101
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$4;
2102
- // Try to read from cache first
2103
- const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2104
- if (cachedCandles !== null) {
2105
- return cachedCandles;
2106
- }
2107
- // Cache miss or error - fetch from API
2108
- let lastError;
2109
- for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
2110
- try {
2111
- const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit, self.params.execution.context.backtest);
2112
- VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
2113
- // Write to cache after successful fetch
2114
- await WRITE_CANDLES_CACHE_FN$1(result, dto, self);
2115
- return result;
2171
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$5;
2172
+ await Candle.acquireLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
2173
+ try {
2174
+ // Try to read from cache first
2175
+ const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2176
+ if (cachedCandles !== null) {
2177
+ return cachedCandles;
2116
2178
  }
2117
- catch (err) {
2118
- const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
2119
- const payload = {
2120
- error: errorData(err),
2121
- message: getErrorMessage(err),
2122
- };
2123
- self.params.logger.warn(message, payload);
2124
- console.warn(message, payload);
2125
- lastError = err;
2126
- await sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
2179
+ // Cache miss or error - fetch from API
2180
+ let lastError;
2181
+ for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
2182
+ try {
2183
+ const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit, self.params.execution.context.backtest);
2184
+ VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
2185
+ // Write to cache after successful fetch
2186
+ await WRITE_CANDLES_CACHE_FN$1(result, dto, self);
2187
+ return result;
2188
+ }
2189
+ catch (err) {
2190
+ const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
2191
+ const payload = {
2192
+ error: errorData(err),
2193
+ message: getErrorMessage(err),
2194
+ };
2195
+ self.params.logger.warn(message, payload);
2196
+ console.warn(message, payload);
2197
+ lastError = err;
2198
+ await sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
2199
+ }
2127
2200
  }
2201
+ throw lastError;
2202
+ }
2203
+ finally {
2204
+ Candle.releaseLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
2128
2205
  }
2129
- throw lastError;
2130
2206
  };
2131
2207
  /**
2132
2208
  * Wrapper to call onCandleData callback with error handling.
@@ -2206,11 +2282,11 @@ class ClientExchange {
2206
2282
  interval,
2207
2283
  limit,
2208
2284
  });
2209
- const step = INTERVAL_MINUTES$6[interval];
2285
+ const step = INTERVAL_MINUTES$7[interval];
2210
2286
  if (!step) {
2211
2287
  throw new Error(`ClientExchange unknown interval=${interval}`);
2212
2288
  }
2213
- const stepMs = step * MS_PER_MINUTE$4;
2289
+ const stepMs = step * MS_PER_MINUTE$5;
2214
2290
  // Align when down to interval boundary
2215
2291
  const whenTimestamp = this.params.execution.context.when.getTime();
2216
2292
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
@@ -2287,11 +2363,11 @@ class ClientExchange {
2287
2363
  if (!this.params.execution.context.backtest) {
2288
2364
  throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
2289
2365
  }
2290
- const step = INTERVAL_MINUTES$6[interval];
2366
+ const step = INTERVAL_MINUTES$7[interval];
2291
2367
  if (!step) {
2292
2368
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
2293
2369
  }
2294
- const stepMs = step * MS_PER_MINUTE$4;
2370
+ const stepMs = step * MS_PER_MINUTE$5;
2295
2371
  const now = Date.now();
2296
2372
  // Align when down to interval boundary
2297
2373
  const whenTimestamp = this.params.execution.context.when.getTime();
@@ -2455,11 +2531,11 @@ class ClientExchange {
2455
2531
  sDate,
2456
2532
  eDate,
2457
2533
  });
2458
- const step = INTERVAL_MINUTES$6[interval];
2534
+ const step = INTERVAL_MINUTES$7[interval];
2459
2535
  if (!step) {
2460
2536
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2461
2537
  }
2462
- const stepMs = step * MS_PER_MINUTE$4;
2538
+ const stepMs = step * MS_PER_MINUTE$5;
2463
2539
  const whenTimestamp = this.params.execution.context.when.getTime();
2464
2540
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
2465
2541
  let sinceTimestamp;
@@ -2588,7 +2664,7 @@ class ClientExchange {
2588
2664
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
2589
2665
  const to = new Date(alignedTo);
2590
2666
  const from = new Date(alignedTo -
2591
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$4);
2667
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$5);
2592
2668
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2593
2669
  }
2594
2670
  }
@@ -3053,7 +3129,7 @@ const beginTime = (run) => (...args) => {
3053
3129
  return fn();
3054
3130
  };
3055
3131
 
3056
- const INTERVAL_MINUTES$5 = {
3132
+ const INTERVAL_MINUTES$6 = {
3057
3133
  "1m": 1,
3058
3134
  "3m": 3,
3059
3135
  "5m": 5,
@@ -3566,14 +3642,15 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3566
3642
  }
3567
3643
  const currentTime = self.params.execution.context.when.getTime();
3568
3644
  {
3569
- const intervalMinutes = INTERVAL_MINUTES$5[self.params.interval];
3645
+ const intervalMinutes = INTERVAL_MINUTES$6[self.params.interval];
3570
3646
  const intervalMs = intervalMinutes * 60 * 1000;
3571
- // Проверяем что прошел нужный интервал с последнего getSignal
3647
+ const alignedTime = Math.floor(currentTime / intervalMs) * intervalMs;
3648
+ // Проверяем что наступил новый интервал (по aligned timestamp)
3572
3649
  if (self._lastSignalTimestamp !== null &&
3573
- currentTime - self._lastSignalTimestamp < intervalMs) {
3650
+ alignedTime === self._lastSignalTimestamp) {
3574
3651
  return null;
3575
3652
  }
3576
- self._lastSignalTimestamp = currentTime;
3653
+ self._lastSignalTimestamp = alignedTime;
3577
3654
  }
3578
3655
  const currentPrice = await self.params.exchange.getAveragePrice(self.params.execution.context.symbol);
3579
3656
  const timeoutMs = GLOBAL_CONFIG.CC_MAX_SIGNAL_GENERATION_SECONDS * 1000;
@@ -8177,7 +8254,7 @@ class StrategyConnectionService {
8177
8254
  * Maps FrameInterval to minutes for timestamp calculation.
8178
8255
  * Used to generate timeframe arrays with proper spacing.
8179
8256
  */
8180
- const INTERVAL_MINUTES$4 = {
8257
+ const INTERVAL_MINUTES$5 = {
8181
8258
  "1m": 1,
8182
8259
  "3m": 3,
8183
8260
  "5m": 5,
@@ -8232,7 +8309,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
8232
8309
  symbol,
8233
8310
  });
8234
8311
  const { interval, startDate, endDate } = self.params;
8235
- const intervalMinutes = INTERVAL_MINUTES$4[interval];
8312
+ const intervalMinutes = INTERVAL_MINUTES$5[interval];
8236
8313
  if (!intervalMinutes) {
8237
8314
  throw new Error(`ClientFrame unknown interval: ${interval}`);
8238
8315
  }
@@ -13195,7 +13272,49 @@ class BacktestLogicPrivateService {
13195
13272
  }
13196
13273
  }
13197
13274
 
13198
- const TICK_TTL = 1 * 60 * 1000 + 1;
13275
+ const EMITTER_CHECK_INTERVAL = 5000;
13276
+ const MS_PER_MINUTE$4 = 60000;
13277
+ const INTERVAL_MINUTES$4 = {
13278
+ "1m": 1,
13279
+ "3m": 3,
13280
+ "5m": 5,
13281
+ "15m": 15,
13282
+ "30m": 30,
13283
+ "1h": 60,
13284
+ "2h": 120,
13285
+ "4h": 240,
13286
+ "6h": 360,
13287
+ "8h": 480,
13288
+ };
13289
+ const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
13290
+ const tickSubject = new Subject();
13291
+ const intervalMs = INTERVAL_MINUTES$4[interval] * MS_PER_MINUTE$4;
13292
+ {
13293
+ let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
13294
+ Source.fromInterval(EMITTER_CHECK_INTERVAL)
13295
+ .map(() => Math.floor(Date.now() / intervalMs) * intervalMs)
13296
+ .filter((aligned) => {
13297
+ if (aligned !== lastAligned) {
13298
+ lastAligned = aligned;
13299
+ return true;
13300
+ }
13301
+ return false;
13302
+ })
13303
+ .connect(tickSubject.next);
13304
+ }
13305
+ return tickSubject;
13306
+ });
13307
+ /**
13308
+ * Waits for the next candle interval to start and returns the timestamp of the new candle.
13309
+ * @param {CandleInterval} interval - The candle interval (e.g., "1m", "1h") to wait for.
13310
+ * @returns {Promise<number>} A promise that resolves with the timestamp (in milliseconds) of the next candle start.
13311
+ */
13312
+ const waitForCandle = async (interval) => {
13313
+ const emitter = createEmitter(interval);
13314
+ // Subject is multicast
13315
+ return emitter.toPromise();
13316
+ };
13317
+
13199
13318
  /**
13200
13319
  * Private service for live trading orchestration using async generators.
13201
13320
  *
@@ -13265,7 +13384,7 @@ class LiveLogicPrivateService {
13265
13384
  message: getErrorMessage(error),
13266
13385
  });
13267
13386
  await errorEmitter.next(error);
13268
- await sleep(TICK_TTL);
13387
+ await waitForCandle("1m");
13269
13388
  continue;
13270
13389
  }
13271
13390
  this.loggerService.info("liveLogicPrivateService tick result", {
@@ -13300,19 +13419,19 @@ class LiveLogicPrivateService {
13300
13419
  });
13301
13420
  break;
13302
13421
  }
13303
- await sleep(TICK_TTL);
13422
+ await waitForCandle("1m");
13304
13423
  continue;
13305
13424
  }
13306
13425
  if (result.action === "active") {
13307
- await sleep(TICK_TTL);
13426
+ await waitForCandle("1m");
13308
13427
  continue;
13309
13428
  }
13310
13429
  if (result.action === "scheduled") {
13311
- await sleep(TICK_TTL);
13430
+ await waitForCandle("1m");
13312
13431
  continue;
13313
13432
  }
13314
13433
  if (result.action === "waiting") {
13315
- await sleep(TICK_TTL);
13434
+ await waitForCandle("1m");
13316
13435
  continue;
13317
13436
  }
13318
13437
  // Yield opened, closed, cancelled results
@@ -13331,7 +13450,7 @@ class LiveLogicPrivateService {
13331
13450
  break;
13332
13451
  }
13333
13452
  }
13334
- await sleep(TICK_TTL);
13453
+ await waitForCandle("1m");
13335
13454
  }
13336
13455
  }
13337
13456
  }
@@ -26264,55 +26383,61 @@ class ExchangeInstance {
26264
26383
  const sinceTimestamp = alignedWhen - limit * stepMs;
26265
26384
  const since = new Date(sinceTimestamp);
26266
26385
  const untilTimestamp = alignedWhen;
26267
- // Try to read from cache first
26268
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26269
- if (cachedCandles !== null) {
26270
- return cachedCandles;
26271
- }
26272
- let allData = [];
26273
- // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26274
- if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26275
- let remaining = limit;
26276
- let currentSince = new Date(since.getTime());
26277
- const isBacktest = await GET_BACKTEST_FN();
26278
- while (remaining > 0) {
26279
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26280
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26281
- allData.push(...chunkData);
26282
- remaining -= chunkLimit;
26283
- if (remaining > 0) {
26284
- // Move currentSince forward by the number of candles fetched
26285
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26386
+ await Candle.acquireLock(`ExchangeInstance.getCandles symbol=${symbol} interval=${interval} limit=${limit}`);
26387
+ try {
26388
+ // Try to read from cache first
26389
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26390
+ if (cachedCandles !== null) {
26391
+ return cachedCandles;
26392
+ }
26393
+ let allData = [];
26394
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26395
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26396
+ let remaining = limit;
26397
+ let currentSince = new Date(since.getTime());
26398
+ const isBacktest = await GET_BACKTEST_FN();
26399
+ while (remaining > 0) {
26400
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26401
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26402
+ allData.push(...chunkData);
26403
+ remaining -= chunkLimit;
26404
+ if (remaining > 0) {
26405
+ // Move currentSince forward by the number of candles fetched
26406
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26407
+ }
26286
26408
  }
26287
26409
  }
26410
+ else {
26411
+ const isBacktest = await GET_BACKTEST_FN();
26412
+ allData = await getCandles(symbol, interval, since, limit, isBacktest);
26413
+ }
26414
+ // Apply distinct by timestamp to remove duplicates
26415
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26416
+ if (allData.length !== uniqueData.length) {
26417
+ bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26418
+ }
26419
+ // Validate adapter returned data
26420
+ if (uniqueData.length === 0) {
26421
+ throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
26422
+ `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
26423
+ }
26424
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
26425
+ throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
26426
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26427
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26428
+ }
26429
+ if (uniqueData.length !== limit) {
26430
+ throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
26431
+ `Expected ${limit} candles, got ${uniqueData.length}. ` +
26432
+ `Adapter must return exact number of candles requested.`);
26433
+ }
26434
+ // Write to cache after successful fetch
26435
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
26436
+ return uniqueData;
26288
26437
  }
26289
- else {
26290
- const isBacktest = await GET_BACKTEST_FN();
26291
- allData = await getCandles(symbol, interval, since, limit, isBacktest);
26292
- }
26293
- // Apply distinct by timestamp to remove duplicates
26294
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26295
- if (allData.length !== uniqueData.length) {
26296
- bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26297
- }
26298
- // Validate adapter returned data
26299
- if (uniqueData.length === 0) {
26300
- throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
26301
- `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
26302
- }
26303
- if (uniqueData[0].timestamp !== sinceTimestamp) {
26304
- throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
26305
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26306
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26307
- }
26308
- if (uniqueData.length !== limit) {
26309
- throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
26310
- `Expected ${limit} candles, got ${uniqueData.length}. ` +
26311
- `Adapter must return exact number of candles requested.`);
26438
+ finally {
26439
+ Candle.releaseLock(`ExchangeInstance.getCandles symbol=${symbol} interval=${interval} limit=${limit}`);
26312
26440
  }
26313
- // Write to cache after successful fetch
26314
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
26315
- return uniqueData;
26316
26441
  };
26317
26442
  /**
26318
26443
  * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
@@ -26546,56 +26671,62 @@ class ExchangeInstance {
26546
26671
  `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
26547
26672
  `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
26548
26673
  }
26549
- // Try to read from cache first
26550
- const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
26551
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26552
- if (cachedCandles !== null) {
26553
- return cachedCandles;
26554
- }
26555
- // Fetch candles
26556
- const since = new Date(sinceTimestamp);
26557
- let allData = [];
26558
- const isBacktest = await GET_BACKTEST_FN();
26559
- const getCandles = this._methods.getCandles;
26560
- if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26561
- let remaining = calculatedLimit;
26562
- let currentSince = new Date(since.getTime());
26563
- while (remaining > 0) {
26564
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26565
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26566
- allData.push(...chunkData);
26567
- remaining -= chunkLimit;
26568
- if (remaining > 0) {
26569
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26674
+ await Candle.acquireLock(`ExchangeInstance.getRawCandles symbol=${symbol} interval=${interval} limit=${calculatedLimit}`);
26675
+ try {
26676
+ // Try to read from cache first
26677
+ const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
26678
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26679
+ if (cachedCandles !== null) {
26680
+ return cachedCandles;
26681
+ }
26682
+ // Fetch candles
26683
+ const since = new Date(sinceTimestamp);
26684
+ let allData = [];
26685
+ const isBacktest = await GET_BACKTEST_FN();
26686
+ const getCandles = this._methods.getCandles;
26687
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26688
+ let remaining = calculatedLimit;
26689
+ let currentSince = new Date(since.getTime());
26690
+ while (remaining > 0) {
26691
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26692
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26693
+ allData.push(...chunkData);
26694
+ remaining -= chunkLimit;
26695
+ if (remaining > 0) {
26696
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26697
+ }
26570
26698
  }
26571
26699
  }
26700
+ else {
26701
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
26702
+ }
26703
+ // Apply distinct by timestamp to remove duplicates
26704
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26705
+ if (allData.length !== uniqueData.length) {
26706
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26707
+ }
26708
+ // Validate adapter returned data
26709
+ if (uniqueData.length === 0) {
26710
+ throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
26711
+ `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
26712
+ }
26713
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
26714
+ throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
26715
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26716
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26717
+ }
26718
+ if (uniqueData.length !== calculatedLimit) {
26719
+ throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
26720
+ `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
26721
+ `Adapter must return exact number of candles requested.`);
26722
+ }
26723
+ // Write to cache after successful fetch
26724
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
26725
+ return uniqueData;
26572
26726
  }
26573
- else {
26574
- allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
26575
- }
26576
- // Apply distinct by timestamp to remove duplicates
26577
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26578
- if (allData.length !== uniqueData.length) {
26579
- bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26580
- }
26581
- // Validate adapter returned data
26582
- if (uniqueData.length === 0) {
26583
- throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
26584
- `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
26585
- }
26586
- if (uniqueData[0].timestamp !== sinceTimestamp) {
26587
- throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
26588
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26589
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26590
- }
26591
- if (uniqueData.length !== calculatedLimit) {
26592
- throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
26593
- `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
26594
- `Adapter must return exact number of candles requested.`);
26727
+ finally {
26728
+ Candle.releaseLock(`ExchangeInstance.getRawCandles symbol=${symbol} interval=${interval} limit=${calculatedLimit}`);
26595
26729
  }
26596
- // Write to cache after successful fetch
26597
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
26598
- return uniqueData;
26599
26730
  };
26600
26731
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
26601
26732
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
@@ -37341,4 +37472,4 @@ const set = (object, path, value) => {
37341
37472
  }
37342
37473
  };
37343
37474
 
37344
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, warmCandles };
37475
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, waitForCandle, warmCandles };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "3.0.17",
3
+ "version": "3.1.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",