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/README.md +22 -0
- package/build/index.cjs +281 -149
- package/build/index.mjs +282 -151
- package/package.json +1 -1
- package/types.d.ts +17 -1
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
/**
|
|
@@ -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$
|
|
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$
|
|
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
|
-
|
|
1924
|
-
const
|
|
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$
|
|
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$
|
|
2169
|
+
const step = INTERVAL_MINUTES$7[dto.interval];
|
|
2100
2170
|
const sinceTimestamp = since.getTime();
|
|
2101
|
-
const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$
|
|
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;
|
|
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
|
-
|
|
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.
|
|
@@ -2206,11 +2282,11 @@ class ClientExchange {
|
|
|
2206
2282
|
interval,
|
|
2207
2283
|
limit,
|
|
2208
2284
|
});
|
|
2209
|
-
const step = INTERVAL_MINUTES$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3645
|
+
const intervalMinutes = INTERVAL_MINUTES$6[self.params.interval];
|
|
3570
3646
|
const intervalMs = intervalMinutes * 60 * 1000;
|
|
3571
|
-
|
|
3647
|
+
const alignedTime = Math.floor(currentTime / intervalMs) * intervalMs;
|
|
3648
|
+
// Проверяем что наступил новый интервал (по aligned timestamp)
|
|
3572
3649
|
if (self._lastSignalTimestamp !== null &&
|
|
3573
|
-
|
|
3650
|
+
alignedTime === self._lastSignalTimestamp) {
|
|
3574
3651
|
return null;
|
|
3575
3652
|
}
|
|
3576
|
-
self._lastSignalTimestamp =
|
|
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$
|
|
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$
|
|
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
|
|
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
|
|
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
|
|
13422
|
+
await waitForCandle("1m");
|
|
13304
13423
|
continue;
|
|
13305
13424
|
}
|
|
13306
13425
|
if (result.action === "active") {
|
|
13307
|
-
await
|
|
13426
|
+
await waitForCandle("1m");
|
|
13308
13427
|
continue;
|
|
13309
13428
|
}
|
|
13310
13429
|
if (result.action === "scheduled") {
|
|
13311
|
-
await
|
|
13430
|
+
await waitForCandle("1m");
|
|
13312
13431
|
continue;
|
|
13313
13432
|
}
|
|
13314
13433
|
if (result.action === "waiting") {
|
|
13315
|
-
await
|
|
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
|
|
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
|
-
|
|
26268
|
-
|
|
26269
|
-
|
|
26270
|
-
|
|
26271
|
-
|
|
26272
|
-
|
|
26273
|
-
|
|
26274
|
-
|
|
26275
|
-
|
|
26276
|
-
|
|
26277
|
-
|
|
26278
|
-
|
|
26279
|
-
const
|
|
26280
|
-
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
|
|
26284
|
-
|
|
26285
|
-
|
|
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
|
-
|
|
26290
|
-
|
|
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
|
-
|
|
26550
|
-
|
|
26551
|
-
|
|
26552
|
-
|
|
26553
|
-
|
|
26554
|
-
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26560
|
-
|
|
26561
|
-
|
|
26562
|
-
|
|
26563
|
-
|
|
26564
|
-
|
|
26565
|
-
|
|
26566
|
-
|
|
26567
|
-
|
|
26568
|
-
|
|
26569
|
-
|
|
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
|
-
|
|
26574
|
-
|
|
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 };
|