backtest-kit 5.5.1 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.cjs +68 -68
- package/build/index.mjs +68 -68
- package/package.json +1 -1
package/build/index.cjs
CHANGED
|
@@ -10538,6 +10538,45 @@ const get = (object, path) => {
|
|
|
10538
10538
|
return pathArrayFlat.reduce((obj, key) => obj && obj[key], object);
|
|
10539
10539
|
};
|
|
10540
10540
|
|
|
10541
|
+
const MS_PER_MINUTE$4 = 60000;
|
|
10542
|
+
const INTERVAL_MINUTES$4 = {
|
|
10543
|
+
"1m": 1,
|
|
10544
|
+
"3m": 3,
|
|
10545
|
+
"5m": 5,
|
|
10546
|
+
"15m": 15,
|
|
10547
|
+
"30m": 30,
|
|
10548
|
+
"1h": 60,
|
|
10549
|
+
"2h": 120,
|
|
10550
|
+
"4h": 240,
|
|
10551
|
+
"6h": 360,
|
|
10552
|
+
"8h": 480,
|
|
10553
|
+
};
|
|
10554
|
+
/**
|
|
10555
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
10556
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
10557
|
+
*
|
|
10558
|
+
* Candle timestamp convention:
|
|
10559
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
10560
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
10561
|
+
*
|
|
10562
|
+
* Adapter contract:
|
|
10563
|
+
* - Adapter must return candles with timestamp = openTime
|
|
10564
|
+
* - First returned candle.timestamp must equal aligned since
|
|
10565
|
+
* - Adapter must return exactly `limit` candles
|
|
10566
|
+
*
|
|
10567
|
+
* @param date - Date to align
|
|
10568
|
+
* @param interval - Candle interval (e.g., "1m", "15m", "1h")
|
|
10569
|
+
* @returns New Date aligned down to interval boundary
|
|
10570
|
+
*/
|
|
10571
|
+
const alignToInterval = (date, interval) => {
|
|
10572
|
+
const minutes = INTERVAL_MINUTES$4[interval];
|
|
10573
|
+
if (minutes === undefined) {
|
|
10574
|
+
throw new Error(`alignToInterval: unknown interval=${interval}`);
|
|
10575
|
+
}
|
|
10576
|
+
const intervalMs = minutes * MS_PER_MINUTE$4;
|
|
10577
|
+
return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
|
|
10578
|
+
};
|
|
10579
|
+
|
|
10541
10580
|
/**
|
|
10542
10581
|
* Retrieves the current timestamp based on the execution context.
|
|
10543
10582
|
* If an execution context is active (e.g., during a backtest), it returns the timestamp from the context to ensure consistency with the simulated time.
|
|
@@ -10549,7 +10588,7 @@ const getContextTimestamp = () => {
|
|
|
10549
10588
|
if (ExecutionContextService.hasContext()) {
|
|
10550
10589
|
return bt.executionContextService.context.when.getTime();
|
|
10551
10590
|
}
|
|
10552
|
-
return Date.
|
|
10591
|
+
return alignToInterval(new Date(), "1m").getTime();
|
|
10553
10592
|
};
|
|
10554
10593
|
|
|
10555
10594
|
/** Symbol indicating that positions need to be fetched from persistence */
|
|
@@ -15534,8 +15573,8 @@ class BacktestLogicPrivateService {
|
|
|
15534
15573
|
}
|
|
15535
15574
|
|
|
15536
15575
|
const EMITTER_CHECK_INTERVAL = 5000;
|
|
15537
|
-
const MS_PER_MINUTE$
|
|
15538
|
-
const INTERVAL_MINUTES$
|
|
15576
|
+
const MS_PER_MINUTE$3 = 60000;
|
|
15577
|
+
const INTERVAL_MINUTES$3 = {
|
|
15539
15578
|
"1m": 1,
|
|
15540
15579
|
"3m": 3,
|
|
15541
15580
|
"5m": 5,
|
|
@@ -15549,7 +15588,7 @@ const INTERVAL_MINUTES$4 = {
|
|
|
15549
15588
|
};
|
|
15550
15589
|
const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
|
|
15551
15590
|
const tickSubject = new functoolsKit.Subject();
|
|
15552
|
-
const intervalMs = INTERVAL_MINUTES$
|
|
15591
|
+
const intervalMs = INTERVAL_MINUTES$3[interval] * MS_PER_MINUTE$3;
|
|
15553
15592
|
{
|
|
15554
15593
|
let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
15555
15594
|
functoolsKit.Source.fromInterval(EMITTER_CHECK_INTERVAL)
|
|
@@ -15627,7 +15666,7 @@ class LiveLogicPrivateService {
|
|
|
15627
15666
|
let previousEventTimestamp = null;
|
|
15628
15667
|
while (true) {
|
|
15629
15668
|
const tickStartTime = performance.now();
|
|
15630
|
-
const when = new Date();
|
|
15669
|
+
const when = alignToInterval(new Date(), "1m");
|
|
15631
15670
|
let result;
|
|
15632
15671
|
try {
|
|
15633
15672
|
result = await this.strategyCoreService.tick(symbol, when, false, {
|
|
@@ -29498,7 +29537,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
|
29498
29537
|
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
29499
29538
|
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
29500
29539
|
const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
|
|
29501
|
-
const MS_PER_MINUTE$
|
|
29540
|
+
const MS_PER_MINUTE$2 = 60000;
|
|
29502
29541
|
/**
|
|
29503
29542
|
* Gets current timestamp from execution context if available.
|
|
29504
29543
|
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
@@ -29507,7 +29546,7 @@ const GET_TIMESTAMP_FN = async () => {
|
|
|
29507
29546
|
if (ExecutionContextService.hasContext()) {
|
|
29508
29547
|
return new Date(bt.executionContextService.context.when);
|
|
29509
29548
|
}
|
|
29510
|
-
return new Date();
|
|
29549
|
+
return alignToInterval(new Date(), "1m");
|
|
29511
29550
|
};
|
|
29512
29551
|
/**
|
|
29513
29552
|
* Gets backtest mode flag from execution context if available.
|
|
@@ -29565,7 +29604,7 @@ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest)
|
|
|
29565
29604
|
const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
|
|
29566
29605
|
throw new Error(`getAggregatedTrades is not implemented for this exchange`);
|
|
29567
29606
|
};
|
|
29568
|
-
const INTERVAL_MINUTES$
|
|
29607
|
+
const INTERVAL_MINUTES$2 = {
|
|
29569
29608
|
"1m": 1,
|
|
29570
29609
|
"3m": 3,
|
|
29571
29610
|
"5m": 5,
|
|
@@ -29595,7 +29634,7 @@ const INTERVAL_MINUTES$3 = {
|
|
|
29595
29634
|
* @returns Aligned timestamp rounded down to interval boundary
|
|
29596
29635
|
*/
|
|
29597
29636
|
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
29598
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$
|
|
29637
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
|
|
29599
29638
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
29600
29639
|
};
|
|
29601
29640
|
/**
|
|
@@ -29735,11 +29774,11 @@ class ExchangeInstance {
|
|
|
29735
29774
|
limit,
|
|
29736
29775
|
});
|
|
29737
29776
|
const getCandles = this._methods.getCandles;
|
|
29738
|
-
const step = INTERVAL_MINUTES$
|
|
29777
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
29739
29778
|
if (!step) {
|
|
29740
29779
|
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
29741
29780
|
}
|
|
29742
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
29781
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
29743
29782
|
// Align when down to interval boundary
|
|
29744
29783
|
const when = await GET_TIMESTAMP_FN();
|
|
29745
29784
|
const whenTimestamp = when.getTime();
|
|
@@ -29924,7 +29963,7 @@ class ExchangeInstance {
|
|
|
29924
29963
|
const when = await GET_TIMESTAMP_FN();
|
|
29925
29964
|
const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
29926
29965
|
const to = new Date(alignedTo);
|
|
29927
|
-
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$
|
|
29966
|
+
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
|
|
29928
29967
|
const isBacktest = await GET_BACKTEST_FN();
|
|
29929
29968
|
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
29930
29969
|
};
|
|
@@ -29956,7 +29995,7 @@ class ExchangeInstance {
|
|
|
29956
29995
|
const when = await GET_TIMESTAMP_FN();
|
|
29957
29996
|
// Align to 1-minute boundary to prevent look-ahead bias
|
|
29958
29997
|
const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
|
|
29959
|
-
const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$
|
|
29998
|
+
const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$2 - MS_PER_MINUTE$2;
|
|
29960
29999
|
const isBacktest = await GET_BACKTEST_FN();
|
|
29961
30000
|
// No limit: fetch a single window and return as-is
|
|
29962
30001
|
if (limit === undefined) {
|
|
@@ -30019,11 +30058,11 @@ class ExchangeInstance {
|
|
|
30019
30058
|
sDate,
|
|
30020
30059
|
eDate,
|
|
30021
30060
|
});
|
|
30022
|
-
const step = INTERVAL_MINUTES$
|
|
30061
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
30023
30062
|
if (!step) {
|
|
30024
30063
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
30025
30064
|
}
|
|
30026
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
30065
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
30027
30066
|
const when = await GET_TIMESTAMP_FN();
|
|
30028
30067
|
const nowTimestamp = when.getTime();
|
|
30029
30068
|
const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
|
|
@@ -30313,8 +30352,8 @@ const Exchange = new ExchangeUtils();
|
|
|
30313
30352
|
|
|
30314
30353
|
const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
|
|
30315
30354
|
const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
|
|
30316
|
-
const MS_PER_MINUTE$
|
|
30317
|
-
const INTERVAL_MINUTES$
|
|
30355
|
+
const MS_PER_MINUTE$1 = 60000;
|
|
30356
|
+
const INTERVAL_MINUTES$1 = {
|
|
30318
30357
|
"1m": 1,
|
|
30319
30358
|
"3m": 3,
|
|
30320
30359
|
"5m": 5,
|
|
@@ -30327,7 +30366,7 @@ const INTERVAL_MINUTES$2 = {
|
|
|
30327
30366
|
"8h": 480,
|
|
30328
30367
|
};
|
|
30329
30368
|
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
30330
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$
|
|
30369
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
30331
30370
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
30332
30371
|
};
|
|
30333
30372
|
const BAR_LENGTH = 30;
|
|
@@ -30352,11 +30391,11 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
|
|
|
30352
30391
|
async function checkCandles(params) {
|
|
30353
30392
|
const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
|
|
30354
30393
|
bt.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
|
|
30355
|
-
const step = INTERVAL_MINUTES$
|
|
30394
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
30356
30395
|
if (!step) {
|
|
30357
30396
|
throw new Error(`checkCandles: unsupported interval=${interval}`);
|
|
30358
30397
|
}
|
|
30359
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
30398
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
30360
30399
|
const dir = path.join(baseDir, exchangeName, symbol, interval);
|
|
30361
30400
|
const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
30362
30401
|
const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
@@ -30426,11 +30465,11 @@ async function warmCandles(params) {
|
|
|
30426
30465
|
from,
|
|
30427
30466
|
to,
|
|
30428
30467
|
});
|
|
30429
|
-
const step = INTERVAL_MINUTES$
|
|
30468
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
30430
30469
|
if (!step) {
|
|
30431
30470
|
throw new Error(`warmCandles: unsupported interval=${interval}`);
|
|
30432
30471
|
}
|
|
30433
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
30472
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
30434
30473
|
const instance = new ExchangeInstance(exchangeName);
|
|
30435
30474
|
const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
30436
30475
|
const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
@@ -39976,7 +40015,7 @@ const GET_DATE_FN = () => {
|
|
|
39976
40015
|
if (ExecutionContextService.hasContext()) {
|
|
39977
40016
|
return new Date(bt.executionContextService.context.when);
|
|
39978
40017
|
}
|
|
39979
|
-
return new Date();
|
|
40018
|
+
return alignToInterval(new Date(), "1m");
|
|
39980
40019
|
};
|
|
39981
40020
|
/**
|
|
39982
40021
|
* Method context retrieval function.
|
|
@@ -45318,8 +45357,8 @@ const CACHE_METHOD_NAME_GC = "CacheInstance.gc";
|
|
|
45318
45357
|
const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
|
|
45319
45358
|
const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
|
|
45320
45359
|
const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
|
|
45321
|
-
const MS_PER_MINUTE
|
|
45322
|
-
const INTERVAL_MINUTES
|
|
45360
|
+
const MS_PER_MINUTE = 60000;
|
|
45361
|
+
const INTERVAL_MINUTES = {
|
|
45323
45362
|
"1m": 1,
|
|
45324
45363
|
"3m": 3,
|
|
45325
45364
|
"5m": 5,
|
|
@@ -45352,11 +45391,11 @@ const INTERVAL_MINUTES$1 = {
|
|
|
45352
45391
|
* ```
|
|
45353
45392
|
*/
|
|
45354
45393
|
const align = (timestamp, interval) => {
|
|
45355
|
-
const intervalMinutes = INTERVAL_MINUTES
|
|
45394
|
+
const intervalMinutes = INTERVAL_MINUTES[interval];
|
|
45356
45395
|
if (!intervalMinutes) {
|
|
45357
45396
|
throw new Error(`align: unknown interval=${interval}`);
|
|
45358
45397
|
}
|
|
45359
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE
|
|
45398
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE;
|
|
45360
45399
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
45361
45400
|
};
|
|
45362
45401
|
/**
|
|
@@ -45445,7 +45484,7 @@ class CacheInstance {
|
|
|
45445
45484
|
*/
|
|
45446
45485
|
this.run = (...args) => {
|
|
45447
45486
|
bt.loggerService.debug(CACHE_METHOD_NAME_RUN, { args });
|
|
45448
|
-
const step = INTERVAL_MINUTES
|
|
45487
|
+
const step = INTERVAL_MINUTES[this.interval];
|
|
45449
45488
|
{
|
|
45450
45489
|
if (!MethodContextService.hasContext()) {
|
|
45451
45490
|
throw new Error("CacheInstance run requires method context");
|
|
@@ -45598,7 +45637,7 @@ class CacheFileInstance {
|
|
|
45598
45637
|
*/
|
|
45599
45638
|
this.run = async (...args) => {
|
|
45600
45639
|
bt.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
|
|
45601
|
-
const step = INTERVAL_MINUTES
|
|
45640
|
+
const step = INTERVAL_MINUTES[this.interval];
|
|
45602
45641
|
{
|
|
45603
45642
|
if (!MethodContextService.hasContext()) {
|
|
45604
45643
|
throw new Error("CacheFileInstance run requires method context");
|
|
@@ -46251,45 +46290,6 @@ class StrategyUtils {
|
|
|
46251
46290
|
*/
|
|
46252
46291
|
const Strategy = new StrategyUtils();
|
|
46253
46292
|
|
|
46254
|
-
const MS_PER_MINUTE = 60000;
|
|
46255
|
-
const INTERVAL_MINUTES = {
|
|
46256
|
-
"1m": 1,
|
|
46257
|
-
"3m": 3,
|
|
46258
|
-
"5m": 5,
|
|
46259
|
-
"15m": 15,
|
|
46260
|
-
"30m": 30,
|
|
46261
|
-
"1h": 60,
|
|
46262
|
-
"2h": 120,
|
|
46263
|
-
"4h": 240,
|
|
46264
|
-
"6h": 360,
|
|
46265
|
-
"8h": 480,
|
|
46266
|
-
};
|
|
46267
|
-
/**
|
|
46268
|
-
* Aligns timestamp down to the nearest interval boundary.
|
|
46269
|
-
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
46270
|
-
*
|
|
46271
|
-
* Candle timestamp convention:
|
|
46272
|
-
* - Candle timestamp = openTime (when candle opens)
|
|
46273
|
-
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
46274
|
-
*
|
|
46275
|
-
* Adapter contract:
|
|
46276
|
-
* - Adapter must return candles with timestamp = openTime
|
|
46277
|
-
* - First returned candle.timestamp must equal aligned since
|
|
46278
|
-
* - Adapter must return exactly `limit` candles
|
|
46279
|
-
*
|
|
46280
|
-
* @param date - Date to align
|
|
46281
|
-
* @param interval - Candle interval (e.g., "1m", "15m", "1h")
|
|
46282
|
-
* @returns New Date aligned down to interval boundary
|
|
46283
|
-
*/
|
|
46284
|
-
const alignToInterval = (date, interval) => {
|
|
46285
|
-
const minutes = INTERVAL_MINUTES[interval];
|
|
46286
|
-
if (minutes === undefined) {
|
|
46287
|
-
throw new Error(`alignToInterval: unknown interval=${interval}`);
|
|
46288
|
-
}
|
|
46289
|
-
const intervalMs = minutes * MS_PER_MINUTE;
|
|
46290
|
-
return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
|
|
46291
|
-
};
|
|
46292
|
-
|
|
46293
46293
|
/**
|
|
46294
46294
|
* Rounds a price to the appropriate precision based on the tick size.
|
|
46295
46295
|
*
|
package/build/index.mjs
CHANGED
|
@@ -10518,6 +10518,45 @@ const get = (object, path) => {
|
|
|
10518
10518
|
return pathArrayFlat.reduce((obj, key) => obj && obj[key], object);
|
|
10519
10519
|
};
|
|
10520
10520
|
|
|
10521
|
+
const MS_PER_MINUTE$4 = 60000;
|
|
10522
|
+
const INTERVAL_MINUTES$4 = {
|
|
10523
|
+
"1m": 1,
|
|
10524
|
+
"3m": 3,
|
|
10525
|
+
"5m": 5,
|
|
10526
|
+
"15m": 15,
|
|
10527
|
+
"30m": 30,
|
|
10528
|
+
"1h": 60,
|
|
10529
|
+
"2h": 120,
|
|
10530
|
+
"4h": 240,
|
|
10531
|
+
"6h": 360,
|
|
10532
|
+
"8h": 480,
|
|
10533
|
+
};
|
|
10534
|
+
/**
|
|
10535
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
10536
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
10537
|
+
*
|
|
10538
|
+
* Candle timestamp convention:
|
|
10539
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
10540
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
10541
|
+
*
|
|
10542
|
+
* Adapter contract:
|
|
10543
|
+
* - Adapter must return candles with timestamp = openTime
|
|
10544
|
+
* - First returned candle.timestamp must equal aligned since
|
|
10545
|
+
* - Adapter must return exactly `limit` candles
|
|
10546
|
+
*
|
|
10547
|
+
* @param date - Date to align
|
|
10548
|
+
* @param interval - Candle interval (e.g., "1m", "15m", "1h")
|
|
10549
|
+
* @returns New Date aligned down to interval boundary
|
|
10550
|
+
*/
|
|
10551
|
+
const alignToInterval = (date, interval) => {
|
|
10552
|
+
const minutes = INTERVAL_MINUTES$4[interval];
|
|
10553
|
+
if (minutes === undefined) {
|
|
10554
|
+
throw new Error(`alignToInterval: unknown interval=${interval}`);
|
|
10555
|
+
}
|
|
10556
|
+
const intervalMs = minutes * MS_PER_MINUTE$4;
|
|
10557
|
+
return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
|
|
10558
|
+
};
|
|
10559
|
+
|
|
10521
10560
|
/**
|
|
10522
10561
|
* Retrieves the current timestamp based on the execution context.
|
|
10523
10562
|
* If an execution context is active (e.g., during a backtest), it returns the timestamp from the context to ensure consistency with the simulated time.
|
|
@@ -10529,7 +10568,7 @@ const getContextTimestamp = () => {
|
|
|
10529
10568
|
if (ExecutionContextService.hasContext()) {
|
|
10530
10569
|
return bt.executionContextService.context.when.getTime();
|
|
10531
10570
|
}
|
|
10532
|
-
return Date.
|
|
10571
|
+
return alignToInterval(new Date(), "1m").getTime();
|
|
10533
10572
|
};
|
|
10534
10573
|
|
|
10535
10574
|
/** Symbol indicating that positions need to be fetched from persistence */
|
|
@@ -15514,8 +15553,8 @@ class BacktestLogicPrivateService {
|
|
|
15514
15553
|
}
|
|
15515
15554
|
|
|
15516
15555
|
const EMITTER_CHECK_INTERVAL = 5000;
|
|
15517
|
-
const MS_PER_MINUTE$
|
|
15518
|
-
const INTERVAL_MINUTES$
|
|
15556
|
+
const MS_PER_MINUTE$3 = 60000;
|
|
15557
|
+
const INTERVAL_MINUTES$3 = {
|
|
15519
15558
|
"1m": 1,
|
|
15520
15559
|
"3m": 3,
|
|
15521
15560
|
"5m": 5,
|
|
@@ -15529,7 +15568,7 @@ const INTERVAL_MINUTES$4 = {
|
|
|
15529
15568
|
};
|
|
15530
15569
|
const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
|
|
15531
15570
|
const tickSubject = new Subject();
|
|
15532
|
-
const intervalMs = INTERVAL_MINUTES$
|
|
15571
|
+
const intervalMs = INTERVAL_MINUTES$3[interval] * MS_PER_MINUTE$3;
|
|
15533
15572
|
{
|
|
15534
15573
|
let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
15535
15574
|
Source.fromInterval(EMITTER_CHECK_INTERVAL)
|
|
@@ -15607,7 +15646,7 @@ class LiveLogicPrivateService {
|
|
|
15607
15646
|
let previousEventTimestamp = null;
|
|
15608
15647
|
while (true) {
|
|
15609
15648
|
const tickStartTime = performance.now();
|
|
15610
|
-
const when = new Date();
|
|
15649
|
+
const when = alignToInterval(new Date(), "1m");
|
|
15611
15650
|
let result;
|
|
15612
15651
|
try {
|
|
15613
15652
|
result = await this.strategyCoreService.tick(symbol, when, false, {
|
|
@@ -29478,7 +29517,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
|
29478
29517
|
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
29479
29518
|
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
29480
29519
|
const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
|
|
29481
|
-
const MS_PER_MINUTE$
|
|
29520
|
+
const MS_PER_MINUTE$2 = 60000;
|
|
29482
29521
|
/**
|
|
29483
29522
|
* Gets current timestamp from execution context if available.
|
|
29484
29523
|
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
@@ -29487,7 +29526,7 @@ const GET_TIMESTAMP_FN = async () => {
|
|
|
29487
29526
|
if (ExecutionContextService.hasContext()) {
|
|
29488
29527
|
return new Date(bt.executionContextService.context.when);
|
|
29489
29528
|
}
|
|
29490
|
-
return new Date();
|
|
29529
|
+
return alignToInterval(new Date(), "1m");
|
|
29491
29530
|
};
|
|
29492
29531
|
/**
|
|
29493
29532
|
* Gets backtest mode flag from execution context if available.
|
|
@@ -29545,7 +29584,7 @@ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest)
|
|
|
29545
29584
|
const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
|
|
29546
29585
|
throw new Error(`getAggregatedTrades is not implemented for this exchange`);
|
|
29547
29586
|
};
|
|
29548
|
-
const INTERVAL_MINUTES$
|
|
29587
|
+
const INTERVAL_MINUTES$2 = {
|
|
29549
29588
|
"1m": 1,
|
|
29550
29589
|
"3m": 3,
|
|
29551
29590
|
"5m": 5,
|
|
@@ -29575,7 +29614,7 @@ const INTERVAL_MINUTES$3 = {
|
|
|
29575
29614
|
* @returns Aligned timestamp rounded down to interval boundary
|
|
29576
29615
|
*/
|
|
29577
29616
|
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
29578
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$
|
|
29617
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
|
|
29579
29618
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
29580
29619
|
};
|
|
29581
29620
|
/**
|
|
@@ -29715,11 +29754,11 @@ class ExchangeInstance {
|
|
|
29715
29754
|
limit,
|
|
29716
29755
|
});
|
|
29717
29756
|
const getCandles = this._methods.getCandles;
|
|
29718
|
-
const step = INTERVAL_MINUTES$
|
|
29757
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
29719
29758
|
if (!step) {
|
|
29720
29759
|
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
29721
29760
|
}
|
|
29722
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
29761
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
29723
29762
|
// Align when down to interval boundary
|
|
29724
29763
|
const when = await GET_TIMESTAMP_FN();
|
|
29725
29764
|
const whenTimestamp = when.getTime();
|
|
@@ -29904,7 +29943,7 @@ class ExchangeInstance {
|
|
|
29904
29943
|
const when = await GET_TIMESTAMP_FN();
|
|
29905
29944
|
const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
|
|
29906
29945
|
const to = new Date(alignedTo);
|
|
29907
|
-
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$
|
|
29946
|
+
const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
|
|
29908
29947
|
const isBacktest = await GET_BACKTEST_FN();
|
|
29909
29948
|
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
29910
29949
|
};
|
|
@@ -29936,7 +29975,7 @@ class ExchangeInstance {
|
|
|
29936
29975
|
const when = await GET_TIMESTAMP_FN();
|
|
29937
29976
|
// Align to 1-minute boundary to prevent look-ahead bias
|
|
29938
29977
|
const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
|
|
29939
|
-
const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$
|
|
29978
|
+
const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$2 - MS_PER_MINUTE$2;
|
|
29940
29979
|
const isBacktest = await GET_BACKTEST_FN();
|
|
29941
29980
|
// No limit: fetch a single window and return as-is
|
|
29942
29981
|
if (limit === undefined) {
|
|
@@ -29999,11 +30038,11 @@ class ExchangeInstance {
|
|
|
29999
30038
|
sDate,
|
|
30000
30039
|
eDate,
|
|
30001
30040
|
});
|
|
30002
|
-
const step = INTERVAL_MINUTES$
|
|
30041
|
+
const step = INTERVAL_MINUTES$2[interval];
|
|
30003
30042
|
if (!step) {
|
|
30004
30043
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
30005
30044
|
}
|
|
30006
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
30045
|
+
const stepMs = step * MS_PER_MINUTE$2;
|
|
30007
30046
|
const when = await GET_TIMESTAMP_FN();
|
|
30008
30047
|
const nowTimestamp = when.getTime();
|
|
30009
30048
|
const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
|
|
@@ -30293,8 +30332,8 @@ const Exchange = new ExchangeUtils();
|
|
|
30293
30332
|
|
|
30294
30333
|
const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
|
|
30295
30334
|
const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
|
|
30296
|
-
const MS_PER_MINUTE$
|
|
30297
|
-
const INTERVAL_MINUTES$
|
|
30335
|
+
const MS_PER_MINUTE$1 = 60000;
|
|
30336
|
+
const INTERVAL_MINUTES$1 = {
|
|
30298
30337
|
"1m": 1,
|
|
30299
30338
|
"3m": 3,
|
|
30300
30339
|
"5m": 5,
|
|
@@ -30307,7 +30346,7 @@ const INTERVAL_MINUTES$2 = {
|
|
|
30307
30346
|
"8h": 480,
|
|
30308
30347
|
};
|
|
30309
30348
|
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
30310
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE$
|
|
30349
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
30311
30350
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
30312
30351
|
};
|
|
30313
30352
|
const BAR_LENGTH = 30;
|
|
@@ -30332,11 +30371,11 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
|
|
|
30332
30371
|
async function checkCandles(params) {
|
|
30333
30372
|
const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
|
|
30334
30373
|
bt.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
|
|
30335
|
-
const step = INTERVAL_MINUTES$
|
|
30374
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
30336
30375
|
if (!step) {
|
|
30337
30376
|
throw new Error(`checkCandles: unsupported interval=${interval}`);
|
|
30338
30377
|
}
|
|
30339
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
30378
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
30340
30379
|
const dir = join(baseDir, exchangeName, symbol, interval);
|
|
30341
30380
|
const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
30342
30381
|
const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
@@ -30406,11 +30445,11 @@ async function warmCandles(params) {
|
|
|
30406
30445
|
from,
|
|
30407
30446
|
to,
|
|
30408
30447
|
});
|
|
30409
|
-
const step = INTERVAL_MINUTES$
|
|
30448
|
+
const step = INTERVAL_MINUTES$1[interval];
|
|
30410
30449
|
if (!step) {
|
|
30411
30450
|
throw new Error(`warmCandles: unsupported interval=${interval}`);
|
|
30412
30451
|
}
|
|
30413
|
-
const stepMs = step * MS_PER_MINUTE$
|
|
30452
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
30414
30453
|
const instance = new ExchangeInstance(exchangeName);
|
|
30415
30454
|
const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
30416
30455
|
const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
@@ -39956,7 +39995,7 @@ const GET_DATE_FN = () => {
|
|
|
39956
39995
|
if (ExecutionContextService.hasContext()) {
|
|
39957
39996
|
return new Date(bt.executionContextService.context.when);
|
|
39958
39997
|
}
|
|
39959
|
-
return new Date();
|
|
39998
|
+
return alignToInterval(new Date(), "1m");
|
|
39960
39999
|
};
|
|
39961
40000
|
/**
|
|
39962
40001
|
* Method context retrieval function.
|
|
@@ -45298,8 +45337,8 @@ const CACHE_METHOD_NAME_GC = "CacheInstance.gc";
|
|
|
45298
45337
|
const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
|
|
45299
45338
|
const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
|
|
45300
45339
|
const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
|
|
45301
|
-
const MS_PER_MINUTE
|
|
45302
|
-
const INTERVAL_MINUTES
|
|
45340
|
+
const MS_PER_MINUTE = 60000;
|
|
45341
|
+
const INTERVAL_MINUTES = {
|
|
45303
45342
|
"1m": 1,
|
|
45304
45343
|
"3m": 3,
|
|
45305
45344
|
"5m": 5,
|
|
@@ -45332,11 +45371,11 @@ const INTERVAL_MINUTES$1 = {
|
|
|
45332
45371
|
* ```
|
|
45333
45372
|
*/
|
|
45334
45373
|
const align = (timestamp, interval) => {
|
|
45335
|
-
const intervalMinutes = INTERVAL_MINUTES
|
|
45374
|
+
const intervalMinutes = INTERVAL_MINUTES[interval];
|
|
45336
45375
|
if (!intervalMinutes) {
|
|
45337
45376
|
throw new Error(`align: unknown interval=${interval}`);
|
|
45338
45377
|
}
|
|
45339
|
-
const intervalMs = intervalMinutes * MS_PER_MINUTE
|
|
45378
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE;
|
|
45340
45379
|
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
45341
45380
|
};
|
|
45342
45381
|
/**
|
|
@@ -45425,7 +45464,7 @@ class CacheInstance {
|
|
|
45425
45464
|
*/
|
|
45426
45465
|
this.run = (...args) => {
|
|
45427
45466
|
bt.loggerService.debug(CACHE_METHOD_NAME_RUN, { args });
|
|
45428
|
-
const step = INTERVAL_MINUTES
|
|
45467
|
+
const step = INTERVAL_MINUTES[this.interval];
|
|
45429
45468
|
{
|
|
45430
45469
|
if (!MethodContextService.hasContext()) {
|
|
45431
45470
|
throw new Error("CacheInstance run requires method context");
|
|
@@ -45578,7 +45617,7 @@ class CacheFileInstance {
|
|
|
45578
45617
|
*/
|
|
45579
45618
|
this.run = async (...args) => {
|
|
45580
45619
|
bt.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
|
|
45581
|
-
const step = INTERVAL_MINUTES
|
|
45620
|
+
const step = INTERVAL_MINUTES[this.interval];
|
|
45582
45621
|
{
|
|
45583
45622
|
if (!MethodContextService.hasContext()) {
|
|
45584
45623
|
throw new Error("CacheFileInstance run requires method context");
|
|
@@ -46231,45 +46270,6 @@ class StrategyUtils {
|
|
|
46231
46270
|
*/
|
|
46232
46271
|
const Strategy = new StrategyUtils();
|
|
46233
46272
|
|
|
46234
|
-
const MS_PER_MINUTE = 60000;
|
|
46235
|
-
const INTERVAL_MINUTES = {
|
|
46236
|
-
"1m": 1,
|
|
46237
|
-
"3m": 3,
|
|
46238
|
-
"5m": 5,
|
|
46239
|
-
"15m": 15,
|
|
46240
|
-
"30m": 30,
|
|
46241
|
-
"1h": 60,
|
|
46242
|
-
"2h": 120,
|
|
46243
|
-
"4h": 240,
|
|
46244
|
-
"6h": 360,
|
|
46245
|
-
"8h": 480,
|
|
46246
|
-
};
|
|
46247
|
-
/**
|
|
46248
|
-
* Aligns timestamp down to the nearest interval boundary.
|
|
46249
|
-
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
46250
|
-
*
|
|
46251
|
-
* Candle timestamp convention:
|
|
46252
|
-
* - Candle timestamp = openTime (when candle opens)
|
|
46253
|
-
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
46254
|
-
*
|
|
46255
|
-
* Adapter contract:
|
|
46256
|
-
* - Adapter must return candles with timestamp = openTime
|
|
46257
|
-
* - First returned candle.timestamp must equal aligned since
|
|
46258
|
-
* - Adapter must return exactly `limit` candles
|
|
46259
|
-
*
|
|
46260
|
-
* @param date - Date to align
|
|
46261
|
-
* @param interval - Candle interval (e.g., "1m", "15m", "1h")
|
|
46262
|
-
* @returns New Date aligned down to interval boundary
|
|
46263
|
-
*/
|
|
46264
|
-
const alignToInterval = (date, interval) => {
|
|
46265
|
-
const minutes = INTERVAL_MINUTES[interval];
|
|
46266
|
-
if (minutes === undefined) {
|
|
46267
|
-
throw new Error(`alignToInterval: unknown interval=${interval}`);
|
|
46268
|
-
}
|
|
46269
|
-
const intervalMs = minutes * MS_PER_MINUTE;
|
|
46270
|
-
return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
|
|
46271
|
-
};
|
|
46272
|
-
|
|
46273
46273
|
/**
|
|
46274
46274
|
* Rounds a price to the appropriate precision based on the tick size.
|
|
46275
46275
|
*
|