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 +22 -0
- package/build/index.cjs +208 -120
- package/build/index.mjs +208 -120
- package/package.json +1 -1
- package/types.d.ts +9 -0
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
-
|
|
26331
|
-
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
26341
|
-
|
|
26342
|
-
const
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
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
|
-
|
|
26353
|
-
|
|
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
|
-
|
|
26613
|
-
|
|
26614
|
-
|
|
26615
|
-
|
|
26616
|
-
|
|
26617
|
-
|
|
26618
|
-
|
|
26619
|
-
|
|
26620
|
-
|
|
26621
|
-
|
|
26622
|
-
|
|
26623
|
-
|
|
26624
|
-
|
|
26625
|
-
|
|
26626
|
-
|
|
26627
|
-
|
|
26628
|
-
|
|
26629
|
-
|
|
26630
|
-
|
|
26631
|
-
|
|
26632
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
-
|
|
26311
|
-
|
|
26312
|
-
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
|
|
26316
|
-
|
|
26317
|
-
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
const
|
|
26323
|
-
|
|
26324
|
-
|
|
26325
|
-
|
|
26326
|
-
|
|
26327
|
-
|
|
26328
|
-
|
|
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
|
-
|
|
26333
|
-
|
|
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
|
-
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
26598
|
-
|
|
26599
|
-
|
|
26600
|
-
|
|
26601
|
-
|
|
26602
|
-
|
|
26603
|
-
|
|
26604
|
-
|
|
26605
|
-
|
|
26606
|
-
|
|
26607
|
-
|
|
26608
|
-
|
|
26609
|
-
|
|
26610
|
-
|
|
26611
|
-
|
|
26612
|
-
|
|
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
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.
|