backtest-kit 3.0.18 → 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/README.md CHANGED
@@ -555,6 +555,28 @@ cd my-trading-bot
555
555
  npm start
556
556
  ```
557
557
 
558
+
559
+ ### @backtest-kit/graph
560
+
561
+ > **[Explore on NPM](https://www.npmjs.com/package/@backtest-kit/graph)** 🔗
562
+
563
+ The **@backtest-kit/graph** package lets you compose backtest-kit computations as a typed directed acyclic graph (DAG). Define source nodes that fetch market data and output nodes that compute derived values — then resolve the whole graph in topological order with automatic parallelism.
564
+
565
+ #### Key Features
566
+ - 🔌 **DAG Execution**: Nodes are resolved bottom-up in topological order with `Promise.all` parallelism
567
+ - 🔒 **Type-Safe Values**: TypeScript infers the return type of every node through the graph via generics
568
+ - 🧱 **Two APIs**: Low-level `INode` for runtime/storage, high-level `sourceNode` + `outputNode` builders for authoring
569
+ - 💾 **DB-Ready Serialization**: `serialize` / `deserialize` convert the graph to a flat `IFlatNode[]` list with `id` / `nodeIds`
570
+ - 🌐 **Context-Aware Fetch**: `sourceNode` receives `(symbol, when, exchangeName)` from the execution context automatically
571
+
572
+ #### Use Case
573
+ Perfect for multi-timeframe strategies where multiple Pine Script or indicator computations must be combined. Instead of manually chaining async calls, define each computation as a node and let the graph resolve dependencies in parallel. Adding a new filter or timeframe requires no changes to the existing wiring.
574
+
575
+ #### Get Started
576
+ ```bash
577
+ npm install @backtest-kit/graph backtest-kit
578
+ ```
579
+
558
580
  ## 🤖 Are you a robot?
559
581
 
560
582
  **For language models**: Read extended description in [./LLMs.md](./LLMs.md)
package/build/index.cjs CHANGED
@@ -442,6 +442,13 @@ const GLOBAL_CONFIG = {
442
442
  * Default: 50 signals
443
443
  */
444
444
  CC_MAX_SIGNALS: 50,
445
+ /**
446
+ * Enables mutex locking for candle fetching to prevent concurrent fetches of the same candles.
447
+ * This can help avoid redundant API calls and ensure data consistency when multiple processes/threads attempt to fetch candles simultaneously.
448
+ *
449
+ * Default: true (mutex locking enabled for candle fetching)
450
+ */
451
+ CC_ENABLE_CANDLE_FETCH_MUTEX: true,
445
452
  };
446
453
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
447
454
 
@@ -712,7 +719,7 @@ async function writeFileAtomic(file, data, options = {}) {
712
719
  }
713
720
  }
714
721
 
715
- var _a$2;
722
+ var _a$3;
716
723
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
717
724
  // Calculate step in milliseconds for candle close time validation
718
725
  const INTERVAL_MINUTES$8 = {
@@ -830,7 +837,7 @@ class PersistBase {
830
837
  constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
831
838
  this.entityName = entityName;
832
839
  this.baseDir = baseDir;
833
- this[_a$2] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
840
+ this[_a$3] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
834
841
  bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
835
842
  entityName: this.entityName,
836
843
  baseDir,
@@ -933,7 +940,7 @@ class PersistBase {
933
940
  }
934
941
  }
935
942
  }
936
- _a$2 = BASE_WAIT_FOR_INIT_SYMBOL;
943
+ _a$3 = BASE_WAIT_FOR_INIT_SYMBOL;
937
944
  // @ts-ignore
938
945
  PersistBase = functoolsKit.makeExtendable(PersistBase);
939
946
  /**
@@ -1940,6 +1947,69 @@ class PersistNotificationUtils {
1940
1947
  */
1941
1948
  const PersistNotificationAdapter = new PersistNotificationUtils();
1942
1949
 
1950
+ var _a$2, _b$2;
1951
+ const BUSY_DELAY = 100;
1952
+ const SET_BUSY_SYMBOL = Symbol("setBusy");
1953
+ const GET_BUSY_SYMBOL = Symbol("getBusy");
1954
+ const ACQUIRE_LOCK_SYMBOL = Symbol("acquireLock");
1955
+ const RELEASE_LOCK_SYMBOL = Symbol("releaseLock");
1956
+ const ACQUIRE_LOCK_FN = async (self) => {
1957
+ while (self[GET_BUSY_SYMBOL]()) {
1958
+ await functoolsKit.sleep(BUSY_DELAY);
1959
+ }
1960
+ self[SET_BUSY_SYMBOL](true);
1961
+ };
1962
+ class Lock {
1963
+ constructor() {
1964
+ this._isBusy = 0;
1965
+ this[_a$2] = functoolsKit.queued(ACQUIRE_LOCK_FN);
1966
+ this[_b$2] = () => this[SET_BUSY_SYMBOL](false);
1967
+ this.acquireLock = async () => {
1968
+ await this[ACQUIRE_LOCK_SYMBOL](this);
1969
+ };
1970
+ this.releaseLock = async () => {
1971
+ await this[RELEASE_LOCK_SYMBOL]();
1972
+ };
1973
+ }
1974
+ [SET_BUSY_SYMBOL](isBusy) {
1975
+ this._isBusy += isBusy ? 1 : -1;
1976
+ if (this._isBusy < 0) {
1977
+ throw new Error("Extra release in finally block");
1978
+ }
1979
+ }
1980
+ [GET_BUSY_SYMBOL]() {
1981
+ return !!this._isBusy;
1982
+ }
1983
+ }
1984
+ _a$2 = ACQUIRE_LOCK_SYMBOL, _b$2 = RELEASE_LOCK_SYMBOL;
1985
+
1986
+ const METHOD_NAME_ACQUIRE_LOCK = "CandleUtils.acquireLock";
1987
+ const METHOD_NAME_RELEASE_LOCK = "CandleUtils.releaseLock";
1988
+ class CandleUtils {
1989
+ constructor() {
1990
+ this._lock = new Lock();
1991
+ this.acquireLock = async (source) => {
1992
+ bt.loggerService.info(METHOD_NAME_ACQUIRE_LOCK, {
1993
+ source,
1994
+ });
1995
+ if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
1996
+ return;
1997
+ }
1998
+ return await this._lock.acquireLock();
1999
+ };
2000
+ this.releaseLock = async (source) => {
2001
+ bt.loggerService.info(METHOD_NAME_RELEASE_LOCK, {
2002
+ source,
2003
+ });
2004
+ if (!GLOBAL_CONFIG.CC_ENABLE_CANDLE_FETCH_MUTEX) {
2005
+ return;
2006
+ }
2007
+ return await this._lock.releaseLock();
2008
+ };
2009
+ }
2010
+ }
2011
+ const Candle = new CandleUtils();
2012
+
1943
2013
  const MS_PER_MINUTE$5 = 60000;
1944
2014
  const INTERVAL_MINUTES$7 = {
1945
2015
  "1m": 1,
@@ -2119,34 +2189,40 @@ const GET_CANDLES_FN = async (dto, since, self) => {
2119
2189
  const step = INTERVAL_MINUTES$7[dto.interval];
2120
2190
  const sinceTimestamp = since.getTime();
2121
2191
  const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$5;
2122
- // Try to read from cache first
2123
- const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2124
- if (cachedCandles !== null) {
2125
- return cachedCandles;
2126
- }
2127
- // Cache miss or error - fetch from API
2128
- let lastError;
2129
- for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
2130
- try {
2131
- const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit, self.params.execution.context.backtest);
2132
- VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
2133
- // Write to cache after successful fetch
2134
- await WRITE_CANDLES_CACHE_FN$1(result, dto, self);
2135
- return result;
2192
+ await Candle.acquireLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
2193
+ try {
2194
+ // Try to read from cache first
2195
+ const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2196
+ if (cachedCandles !== null) {
2197
+ return cachedCandles;
2136
2198
  }
2137
- catch (err) {
2138
- const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
2139
- const payload = {
2140
- error: functoolsKit.errorData(err),
2141
- message: functoolsKit.getErrorMessage(err),
2142
- };
2143
- self.params.logger.warn(message, payload);
2144
- console.warn(message, payload);
2145
- lastError = err;
2146
- await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
2199
+ // Cache miss or error - fetch from API
2200
+ let lastError;
2201
+ for (let i = 0; i !== GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_COUNT; i++) {
2202
+ try {
2203
+ const result = await self.params.getCandles(dto.symbol, dto.interval, since, dto.limit, self.params.execution.context.backtest);
2204
+ VALIDATE_NO_INCOMPLETE_CANDLES_FN(result);
2205
+ // Write to cache after successful fetch
2206
+ await WRITE_CANDLES_CACHE_FN$1(result, dto, self);
2207
+ return result;
2208
+ }
2209
+ catch (err) {
2210
+ const message = `ClientExchange GET_CANDLES_FN: attempt ${i + 1} failed for symbol=${dto.symbol}, interval=${dto.interval}, since=${since.toISOString()}, limit=${dto.limit}}`;
2211
+ const payload = {
2212
+ error: functoolsKit.errorData(err),
2213
+ message: functoolsKit.getErrorMessage(err),
2214
+ };
2215
+ self.params.logger.warn(message, payload);
2216
+ console.warn(message, payload);
2217
+ lastError = err;
2218
+ await functoolsKit.sleep(GLOBAL_CONFIG.CC_GET_CANDLES_RETRY_DELAY_MS);
2219
+ }
2147
2220
  }
2221
+ throw lastError;
2222
+ }
2223
+ finally {
2224
+ Candle.releaseLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
2148
2225
  }
2149
- throw lastError;
2150
2226
  };
2151
2227
  /**
2152
2228
  * Wrapper to call onCandleData callback with error handling.
@@ -26327,55 +26403,61 @@ class ExchangeInstance {
26327
26403
  const sinceTimestamp = alignedWhen - limit * stepMs;
26328
26404
  const since = new Date(sinceTimestamp);
26329
26405
  const untilTimestamp = alignedWhen;
26330
- // Try to read from cache first
26331
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26332
- if (cachedCandles !== null) {
26333
- return cachedCandles;
26334
- }
26335
- let allData = [];
26336
- // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26337
- if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26338
- let remaining = limit;
26339
- let currentSince = new Date(since.getTime());
26340
- const isBacktest = await GET_BACKTEST_FN();
26341
- while (remaining > 0) {
26342
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26343
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26344
- allData.push(...chunkData);
26345
- remaining -= chunkLimit;
26346
- if (remaining > 0) {
26347
- // Move currentSince forward by the number of candles fetched
26348
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26406
+ await Candle.acquireLock(`ExchangeInstance.getCandles symbol=${symbol} interval=${interval} limit=${limit}`);
26407
+ try {
26408
+ // Try to read from cache first
26409
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26410
+ if (cachedCandles !== null) {
26411
+ return cachedCandles;
26412
+ }
26413
+ let allData = [];
26414
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26415
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26416
+ let remaining = limit;
26417
+ let currentSince = new Date(since.getTime());
26418
+ const isBacktest = await GET_BACKTEST_FN();
26419
+ while (remaining > 0) {
26420
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26421
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26422
+ allData.push(...chunkData);
26423
+ remaining -= chunkLimit;
26424
+ if (remaining > 0) {
26425
+ // Move currentSince forward by the number of candles fetched
26426
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26427
+ }
26349
26428
  }
26350
26429
  }
26430
+ else {
26431
+ const isBacktest = await GET_BACKTEST_FN();
26432
+ allData = await getCandles(symbol, interval, since, limit, isBacktest);
26433
+ }
26434
+ // Apply distinct by timestamp to remove duplicates
26435
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26436
+ if (allData.length !== uniqueData.length) {
26437
+ bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26438
+ }
26439
+ // Validate adapter returned data
26440
+ if (uniqueData.length === 0) {
26441
+ throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
26442
+ `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
26443
+ }
26444
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
26445
+ throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
26446
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26447
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26448
+ }
26449
+ if (uniqueData.length !== limit) {
26450
+ throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
26451
+ `Expected ${limit} candles, got ${uniqueData.length}. ` +
26452
+ `Adapter must return exact number of candles requested.`);
26453
+ }
26454
+ // Write to cache after successful fetch
26455
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
26456
+ return uniqueData;
26351
26457
  }
26352
- else {
26353
- const isBacktest = await GET_BACKTEST_FN();
26354
- allData = await getCandles(symbol, interval, since, limit, isBacktest);
26355
- }
26356
- // Apply distinct by timestamp to remove duplicates
26357
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26358
- if (allData.length !== uniqueData.length) {
26359
- bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26360
- }
26361
- // Validate adapter returned data
26362
- if (uniqueData.length === 0) {
26363
- throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
26364
- `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
26365
- }
26366
- if (uniqueData[0].timestamp !== sinceTimestamp) {
26367
- throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
26368
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26369
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26370
- }
26371
- if (uniqueData.length !== limit) {
26372
- throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
26373
- `Expected ${limit} candles, got ${uniqueData.length}. ` +
26374
- `Adapter must return exact number of candles requested.`);
26458
+ finally {
26459
+ Candle.releaseLock(`ExchangeInstance.getCandles symbol=${symbol} interval=${interval} limit=${limit}`);
26375
26460
  }
26376
- // Write to cache after successful fetch
26377
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
26378
- return uniqueData;
26379
26461
  };
26380
26462
  /**
26381
26463
  * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
@@ -26609,56 +26691,62 @@ class ExchangeInstance {
26609
26691
  `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
26610
26692
  `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
26611
26693
  }
26612
- // Try to read from cache first
26613
- const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
26614
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26615
- if (cachedCandles !== null) {
26616
- return cachedCandles;
26617
- }
26618
- // Fetch candles
26619
- const since = new Date(sinceTimestamp);
26620
- let allData = [];
26621
- const isBacktest = await GET_BACKTEST_FN();
26622
- const getCandles = this._methods.getCandles;
26623
- if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26624
- let remaining = calculatedLimit;
26625
- let currentSince = new Date(since.getTime());
26626
- while (remaining > 0) {
26627
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26628
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26629
- allData.push(...chunkData);
26630
- remaining -= chunkLimit;
26631
- if (remaining > 0) {
26632
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26694
+ await Candle.acquireLock(`ExchangeInstance.getRawCandles symbol=${symbol} interval=${interval} limit=${calculatedLimit}`);
26695
+ try {
26696
+ // Try to read from cache first
26697
+ const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
26698
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26699
+ if (cachedCandles !== null) {
26700
+ return cachedCandles;
26701
+ }
26702
+ // Fetch candles
26703
+ const since = new Date(sinceTimestamp);
26704
+ let allData = [];
26705
+ const isBacktest = await GET_BACKTEST_FN();
26706
+ const getCandles = this._methods.getCandles;
26707
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26708
+ let remaining = calculatedLimit;
26709
+ let currentSince = new Date(since.getTime());
26710
+ while (remaining > 0) {
26711
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26712
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26713
+ allData.push(...chunkData);
26714
+ remaining -= chunkLimit;
26715
+ if (remaining > 0) {
26716
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26717
+ }
26633
26718
  }
26634
26719
  }
26720
+ else {
26721
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
26722
+ }
26723
+ // Apply distinct by timestamp to remove duplicates
26724
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26725
+ if (allData.length !== uniqueData.length) {
26726
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26727
+ }
26728
+ // Validate adapter returned data
26729
+ if (uniqueData.length === 0) {
26730
+ throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
26731
+ `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
26732
+ }
26733
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
26734
+ throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
26735
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26736
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26737
+ }
26738
+ if (uniqueData.length !== calculatedLimit) {
26739
+ throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
26740
+ `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
26741
+ `Adapter must return exact number of candles requested.`);
26742
+ }
26743
+ // Write to cache after successful fetch
26744
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
26745
+ return uniqueData;
26746
+ }
26747
+ finally {
26748
+ Candle.releaseLock(`ExchangeInstance.getRawCandles symbol=${symbol} interval=${interval} limit=${calculatedLimit}`);
26635
26749
  }
26636
- else {
26637
- allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
26638
- }
26639
- // Apply distinct by timestamp to remove duplicates
26640
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26641
- if (allData.length !== uniqueData.length) {
26642
- bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26643
- }
26644
- // Validate adapter returned data
26645
- if (uniqueData.length === 0) {
26646
- throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
26647
- `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
26648
- }
26649
- if (uniqueData[0].timestamp !== sinceTimestamp) {
26650
- throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
26651
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26652
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26653
- }
26654
- if (uniqueData.length !== calculatedLimit) {
26655
- throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
26656
- `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
26657
- `Adapter must return exact number of candles requested.`);
26658
- }
26659
- // Write to cache after successful fetch
26660
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
26661
- return uniqueData;
26662
26750
  };
26663
26751
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
26664
26752
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
package/build/index.mjs CHANGED
@@ -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,7 +699,7 @@ 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
705
  const INTERVAL_MINUTES$8 = {
@@ -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
  /**
@@ -1920,6 +1927,69 @@ class PersistNotificationUtils {
1920
1927
  */
1921
1928
  const PersistNotificationAdapter = new PersistNotificationUtils();
1922
1929
 
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
+
1923
1993
  const MS_PER_MINUTE$5 = 60000;
1924
1994
  const INTERVAL_MINUTES$7 = {
1925
1995
  "1m": 1,
@@ -2099,34 +2169,40 @@ const GET_CANDLES_FN = async (dto, since, self) => {
2099
2169
  const step = INTERVAL_MINUTES$7[dto.interval];
2100
2170
  const sinceTimestamp = since.getTime();
2101
2171
  const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$5;
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;
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.
@@ -26307,55 +26383,61 @@ class ExchangeInstance {
26307
26383
  const sinceTimestamp = alignedWhen - limit * stepMs;
26308
26384
  const since = new Date(sinceTimestamp);
26309
26385
  const untilTimestamp = alignedWhen;
26310
- // Try to read from cache first
26311
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26312
- if (cachedCandles !== null) {
26313
- return cachedCandles;
26314
- }
26315
- let allData = [];
26316
- // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26317
- if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26318
- let remaining = limit;
26319
- let currentSince = new Date(since.getTime());
26320
- const isBacktest = await GET_BACKTEST_FN();
26321
- while (remaining > 0) {
26322
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26323
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26324
- allData.push(...chunkData);
26325
- remaining -= chunkLimit;
26326
- if (remaining > 0) {
26327
- // Move currentSince forward by the number of candles fetched
26328
- 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
+ }
26329
26408
  }
26330
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;
26331
26437
  }
26332
- else {
26333
- const isBacktest = await GET_BACKTEST_FN();
26334
- allData = await getCandles(symbol, interval, since, limit, isBacktest);
26335
- }
26336
- // Apply distinct by timestamp to remove duplicates
26337
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26338
- if (allData.length !== uniqueData.length) {
26339
- bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26340
- }
26341
- // Validate adapter returned data
26342
- if (uniqueData.length === 0) {
26343
- throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
26344
- `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
26345
- }
26346
- if (uniqueData[0].timestamp !== sinceTimestamp) {
26347
- throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
26348
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26349
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26350
- }
26351
- if (uniqueData.length !== limit) {
26352
- throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
26353
- `Expected ${limit} candles, got ${uniqueData.length}. ` +
26354
- `Adapter must return exact number of candles requested.`);
26438
+ finally {
26439
+ Candle.releaseLock(`ExchangeInstance.getCandles symbol=${symbol} interval=${interval} limit=${limit}`);
26355
26440
  }
26356
- // Write to cache after successful fetch
26357
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
26358
- return uniqueData;
26359
26441
  };
26360
26442
  /**
26361
26443
  * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
@@ -26589,56 +26671,62 @@ class ExchangeInstance {
26589
26671
  `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
26590
26672
  `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
26591
26673
  }
26592
- // Try to read from cache first
26593
- const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
26594
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26595
- if (cachedCandles !== null) {
26596
- return cachedCandles;
26597
- }
26598
- // Fetch candles
26599
- const since = new Date(sinceTimestamp);
26600
- let allData = [];
26601
- const isBacktest = await GET_BACKTEST_FN();
26602
- const getCandles = this._methods.getCandles;
26603
- if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26604
- let remaining = calculatedLimit;
26605
- let currentSince = new Date(since.getTime());
26606
- while (remaining > 0) {
26607
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26608
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26609
- allData.push(...chunkData);
26610
- remaining -= chunkLimit;
26611
- if (remaining > 0) {
26612
- 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
+ }
26613
26698
  }
26614
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;
26726
+ }
26727
+ finally {
26728
+ Candle.releaseLock(`ExchangeInstance.getRawCandles symbol=${symbol} interval=${interval} limit=${calculatedLimit}`);
26615
26729
  }
26616
- else {
26617
- allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
26618
- }
26619
- // Apply distinct by timestamp to remove duplicates
26620
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26621
- if (allData.length !== uniqueData.length) {
26622
- bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26623
- }
26624
- // Validate adapter returned data
26625
- if (uniqueData.length === 0) {
26626
- throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
26627
- `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
26628
- }
26629
- if (uniqueData[0].timestamp !== sinceTimestamp) {
26630
- throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
26631
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26632
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26633
- }
26634
- if (uniqueData.length !== calculatedLimit) {
26635
- throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
26636
- `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
26637
- `Adapter must return exact number of candles requested.`);
26638
- }
26639
- // Write to cache after successful fetch
26640
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
26641
- return uniqueData;
26642
26730
  };
26643
26731
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
26644
26732
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "3.0.18",
3
+ "version": "3.1.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -4246,6 +4246,13 @@ declare const GLOBAL_CONFIG: {
4246
4246
  * Default: 50 signals
4247
4247
  */
4248
4248
  CC_MAX_SIGNALS: number;
4249
+ /**
4250
+ * Enables mutex locking for candle fetching to prevent concurrent fetches of the same candles.
4251
+ * This can help avoid redundant API calls and ensure data consistency when multiple processes/threads attempt to fetch candles simultaneously.
4252
+ *
4253
+ * Default: true (mutex locking enabled for candle fetching)
4254
+ */
4255
+ CC_ENABLE_CANDLE_FETCH_MUTEX: boolean;
4249
4256
  };
4250
4257
  /**
4251
4258
  * Type for global configuration object.
@@ -4354,6 +4361,7 @@ declare function getConfig(): {
4354
4361
  CC_ORDER_BOOK_MAX_DEPTH_LEVELS: number;
4355
4362
  CC_MAX_NOTIFICATIONS: number;
4356
4363
  CC_MAX_SIGNALS: number;
4364
+ CC_ENABLE_CANDLE_FETCH_MUTEX: boolean;
4357
4365
  };
4358
4366
  /**
4359
4367
  * Retrieves the default configuration object for the framework.
@@ -4390,6 +4398,7 @@ declare function getDefaultConfig(): Readonly<{
4390
4398
  CC_ORDER_BOOK_MAX_DEPTH_LEVELS: number;
4391
4399
  CC_MAX_NOTIFICATIONS: number;
4392
4400
  CC_MAX_SIGNALS: number;
4401
+ CC_ENABLE_CANDLE_FETCH_MUTEX: boolean;
4393
4402
  }>;
4394
4403
  /**
4395
4404
  * Sets custom column configurations for markdown report generation.