@tradejs/cli 2.0.6 → 2.0.8

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.
@@ -46,6 +46,7 @@ var import_progress = __toESM(require("progress"));
46
46
  var import_chalk = __toESM(require("chalk"));
47
47
  var import_connectors = require("@tradejs/connectors");
48
48
  var import_connectors2 = require("@tradejs/node/connectors");
49
+ var import_strategies = require("@tradejs/node/strategies");
49
50
  var import_timescale = require("@tradejs/infra/timescale");
50
51
 
51
52
  // src/lib/binanceMarketData.ts
@@ -55,14 +56,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
55
56
  "15m": 9e5,
56
57
  "1h": 36e5
57
58
  };
58
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
59
- "USDCUSDT",
60
- "FDUSDUSDT",
61
- "TUSDUSDT",
62
- "BUSDUSDT",
63
- "USDPUSDT",
64
- "DAIUSDT"
65
- ]);
66
59
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
67
60
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
68
61
  var standardDeviation = (values) => {
@@ -106,9 +99,6 @@ var classifyBtcAltRegime = ({
106
99
  if (btcVsAltReturn24h < -5e-3) return "alt_lead";
107
100
  return "neutral";
108
101
  };
109
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
110
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
111
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
112
102
  var aggregateAggTradesToRows = ({
113
103
  symbol,
114
104
  interval,
@@ -590,10 +580,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
590
580
  process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
591
581
  2
592
582
  );
593
- const breadthLimit = asInt(
594
- process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
595
- 30
596
- );
597
583
  const breadthChunkDays = asFloat(
598
584
  process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
599
585
  30
@@ -716,88 +702,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
716
702
  }
717
703
  }
718
704
  if (includeBreadth) {
719
- const tickers = await connector.getTickers();
720
- const breadthSymbols = selectBreadthUniverseFromTickers(
721
- tickers,
722
- breadthLimit
723
- );
724
- const universe = `binance_top${breadthSymbols.length}_usdt`;
725
- const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
726
- universe,
727
- interval,
728
- startMs: breadthStartMs,
729
- endMs
730
- });
731
- const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
732
- if (!hasCoverage({
733
- coverage,
705
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
706
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
707
+ const connectorInterval = marketIntervalToConnectorInterval(interval);
708
+ const chunks = buildBreadthBackfillChunks({
734
709
  startMs: breadthStartMs,
735
710
  endMs,
736
- intervalMs
737
- }) || !hasBtcAltMetrics) {
738
- const connectorInterval = marketIntervalToConnectorInterval(interval);
739
- const chunks = buildBreadthBackfillChunks({
740
- startMs: breadthStartMs,
741
- endMs,
742
- intervalMs,
743
- chunkDays: breadthChunkDays
744
- });
745
- const missingChunks = filterMissingBreadthBackfillChunks({
746
- chunks,
747
- coverage,
748
- intervalMs
749
- });
750
- const bar = new import_progress.default(
751
- "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
752
- {
753
- total: Math.max(1, missingChunks.length * breadthSymbols.length),
754
- width: 24
755
- }
756
- );
757
- let candlesRead = 0;
758
- let skippedBreadthChunks = chunks.length - missingChunks.length;
759
- if (!missingChunks.length) {
760
- bar.tick(1, {
761
- candles: 0,
762
- skip: skippedBreadthChunks,
763
- chunk: "cached",
764
- symbol: universe
711
+ intervalMs,
712
+ chunkDays: breadthChunkDays
713
+ });
714
+ const plans = await Promise.all(
715
+ breadthUniverses.map(async (definition) => {
716
+ const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
717
+ universe: definition.universe,
718
+ interval,
719
+ startMs: breadthStartMs,
720
+ endMs
765
721
  });
766
- }
767
- for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
768
- const chunk = missingChunks[chunkIndex];
769
- const originalChunkIndex = chunks.findIndex(
770
- (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
771
- );
772
- const candlesBySymbol = {};
773
- for (const symbol of breadthSymbols) {
774
- const candles = await connector.kline({
775
- symbol,
776
- interval: connectorInterval,
777
- start: chunk.fetchStartMs,
778
- end: chunk.endMs,
779
- silent: true
780
- });
781
- candlesBySymbol[symbol] = candles;
782
- candlesRead += candles.length;
783
- bar.tick(1, {
784
- candles: candlesRead,
785
- skip: skippedBreadthChunks,
786
- chunk: `${originalChunkIndex + 1}/${chunks.length}`,
787
- symbol
788
- });
722
+ const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
723
+ const missingChunks2 = !hasCoverage({
724
+ coverage,
725
+ startMs: breadthStartMs,
726
+ endMs,
727
+ intervalMs
728
+ }) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
729
+ chunks,
730
+ coverage,
731
+ intervalMs
732
+ }) : [];
733
+ if (!missingChunks2.length) {
734
+ console.log(
735
+ import_chalk.default.gray(`breadth cached: universe=${definition.universe}`)
736
+ );
789
737
  }
790
- const btcCandles = await connector.kline({
791
- symbol: "BTCUSDT",
738
+ return {
739
+ definition,
740
+ missingChunkKeys: new Set(
741
+ missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
742
+ )
743
+ };
744
+ })
745
+ );
746
+ const missingChunks = chunks.filter((chunk) => {
747
+ const key = `${chunk.startMs}:${chunk.endMs}`;
748
+ return plans.some((plan) => plan.missingChunkKeys.has(key));
749
+ });
750
+ const bar = new import_progress.default(
751
+ "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
752
+ {
753
+ total: Math.max(1, missingChunks.length * breadthSymbols.length),
754
+ width: 24
755
+ }
756
+ );
757
+ let candlesRead = 0;
758
+ const skippedBreadthChunks = chunks.length - missingChunks.length;
759
+ if (!missingChunks.length) {
760
+ bar.tick(1, {
761
+ candles: 0,
762
+ skip: skippedBreadthChunks,
763
+ chunk: "cached",
764
+ symbol: "top5/top10/top30/top50/top100"
765
+ });
766
+ }
767
+ for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
768
+ const chunk = missingChunks[chunkIndex];
769
+ const originalChunkIndex = chunks.findIndex(
770
+ (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
771
+ );
772
+ const candlesBySymbol = {};
773
+ for (const symbol of breadthSymbols) {
774
+ const candles = await connector.kline({
775
+ symbol,
792
776
  interval: connectorInterval,
793
777
  start: chunk.fetchStartMs,
794
778
  end: chunk.endMs,
795
779
  silent: true
796
780
  });
781
+ candlesBySymbol[symbol] = candles;
782
+ candlesRead += candles.length;
783
+ bar.tick(1, {
784
+ candles: candlesRead,
785
+ skip: skippedBreadthChunks,
786
+ chunk: `${originalChunkIndex + 1}/${chunks.length}`,
787
+ symbol
788
+ });
789
+ }
790
+ const btcCandles = await connector.kline({
791
+ symbol: "BTCUSDT",
792
+ interval: connectorInterval,
793
+ start: chunk.fetchStartMs,
794
+ end: chunk.endMs,
795
+ silent: true
796
+ });
797
+ const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
798
+ for (const plan of plans) {
799
+ if (!plan.missingChunkKeys.has(chunkKey)) {
800
+ continue;
801
+ }
802
+ const symbolSet = new Set(plan.definition.symbols);
803
+ const universeCandles = Object.fromEntries(
804
+ Object.entries(candlesBySymbol).filter(
805
+ ([symbol]) => symbolSet.has(symbol)
806
+ )
807
+ );
797
808
  const rows = buildMarketBreadthRows({
798
- universe,
809
+ universe: plan.definition.universe,
799
810
  interval,
800
- candlesBySymbol,
811
+ candlesBySymbol: universeCandles,
801
812
  btcCandles
802
813
  }).filter((row) => {
803
814
  const ts = row.ts.getTime();
@@ -806,8 +817,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
806
817
  await (0, import_timescale.upsertMarketBreadthRows)(rows);
807
818
  breadthRows += rows.length;
808
819
  }
809
- } else {
810
- console.log(import_chalk.default.gray(`breadth cached: universe=${universe}`));
811
820
  }
812
821
  }
813
822
  console.log(
@@ -106,14 +106,17 @@ var estimateBinanceMarketDataVolume = ({
106
106
  interval,
107
107
  includeAggTrades,
108
108
  includeBreadth,
109
- breadthLimit
109
+ breadthSizes
110
110
  }) => {
111
111
  const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
112
112
  const bucketRowsPerSymbol = Math.ceil(days * 864e5 / intervalMs);
113
113
  const aggTradeBucketRows = includeAggTrades ? symbols.length * bucketRowsPerSymbol : 0;
114
- const breadthSymbols = includeBreadth ? Math.max(0, breadthLimit) : 0;
114
+ const normalizedBreadthSizes = breadthSizes.filter(
115
+ (size) => Number.isFinite(size) && size > 0
116
+ );
117
+ const breadthSymbols = includeBreadth ? Math.max(0, ...normalizedBreadthSizes) : 0;
115
118
  const breadthCandleRows = breadthSymbols * bucketRowsPerSymbol;
116
- const breadthRows = includeBreadth ? bucketRowsPerSymbol : 0;
119
+ const breadthRows = includeBreadth ? bucketRowsPerSymbol * normalizedBreadthSizes.length : 0;
117
120
  return {
118
121
  interval,
119
122
  days,
@@ -1649,6 +1649,7 @@ var import_progress = __toESM(require("progress"));
1649
1649
  var import_chalk = __toESM(require("chalk"));
1650
1650
  var import_connectors = require("@tradejs/connectors");
1651
1651
  var import_connectors2 = require("@tradejs/node/connectors");
1652
+ var import_strategies = require("@tradejs/node/strategies");
1652
1653
  var import_timescale = require("@tradejs/infra/timescale");
1653
1654
 
1654
1655
  // src/lib/binanceMarketData.ts
@@ -1658,14 +1659,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
1658
1659
  "15m": 9e5,
1659
1660
  "1h": 36e5
1660
1661
  };
1661
- var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
1662
- "USDCUSDT",
1663
- "FDUSDUSDT",
1664
- "TUSDUSDT",
1665
- "BUSDUSDT",
1666
- "USDPUSDT",
1667
- "DAIUSDT"
1668
- ]);
1669
1662
  var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
1670
1663
  var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
1671
1664
  var standardDeviation = (values) => {
@@ -1709,9 +1702,6 @@ var classifyBtcAltRegime = ({
1709
1702
  if (btcVsAltReturn24h < -5e-3) return "alt_lead";
1710
1703
  return "neutral";
1711
1704
  };
1712
- var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
1713
- (ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
1714
- ).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
1715
1705
  var aggregateAggTradesToRows = ({
1716
1706
  symbol,
1717
1707
  interval,
@@ -2193,10 +2183,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2193
2183
  process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
2194
2184
  2
2195
2185
  );
2196
- const breadthLimit = asInt(
2197
- process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
2198
- 30
2199
- );
2200
2186
  const breadthChunkDays = asFloat(
2201
2187
  process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
2202
2188
  30
@@ -2319,88 +2305,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2319
2305
  }
2320
2306
  }
2321
2307
  if (includeBreadth) {
2322
- const tickers = await connector.getTickers();
2323
- const breadthSymbols = selectBreadthUniverseFromTickers(
2324
- tickers,
2325
- breadthLimit
2326
- );
2327
- const universe = `binance_top${breadthSymbols.length}_usdt`;
2328
- const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
2329
- universe,
2330
- interval,
2331
- startMs: breadthStartMs,
2332
- endMs
2333
- });
2334
- const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
2335
- if (!hasCoverage({
2336
- coverage,
2308
+ const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
2309
+ const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
2310
+ const connectorInterval = marketIntervalToConnectorInterval(interval);
2311
+ const chunks = buildBreadthBackfillChunks({
2337
2312
  startMs: breadthStartMs,
2338
2313
  endMs,
2339
- intervalMs
2340
- }) || !hasBtcAltMetrics) {
2341
- const connectorInterval = marketIntervalToConnectorInterval(interval);
2342
- const chunks = buildBreadthBackfillChunks({
2343
- startMs: breadthStartMs,
2344
- endMs,
2345
- intervalMs,
2346
- chunkDays: breadthChunkDays
2347
- });
2348
- const missingChunks = filterMissingBreadthBackfillChunks({
2349
- chunks,
2350
- coverage,
2351
- intervalMs
2352
- });
2353
- const bar = new import_progress.default(
2354
- "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
2355
- {
2356
- total: Math.max(1, missingChunks.length * breadthSymbols.length),
2357
- width: 24
2358
- }
2359
- );
2360
- let candlesRead = 0;
2361
- let skippedBreadthChunks = chunks.length - missingChunks.length;
2362
- if (!missingChunks.length) {
2363
- bar.tick(1, {
2364
- candles: 0,
2365
- skip: skippedBreadthChunks,
2366
- chunk: "cached",
2367
- symbol: universe
2314
+ intervalMs,
2315
+ chunkDays: breadthChunkDays
2316
+ });
2317
+ const plans = await Promise.all(
2318
+ breadthUniverses.map(async (definition) => {
2319
+ const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
2320
+ universe: definition.universe,
2321
+ interval,
2322
+ startMs: breadthStartMs,
2323
+ endMs
2368
2324
  });
2369
- }
2370
- for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
2371
- const chunk = missingChunks[chunkIndex];
2372
- const originalChunkIndex = chunks.findIndex(
2373
- (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
2374
- );
2375
- const candlesBySymbol = {};
2376
- for (const symbol of breadthSymbols) {
2377
- const candles = await connector.kline({
2378
- symbol,
2379
- interval: connectorInterval,
2380
- start: chunk.fetchStartMs,
2381
- end: chunk.endMs,
2382
- silent: true
2383
- });
2384
- candlesBySymbol[symbol] = candles;
2385
- candlesRead += candles.length;
2386
- bar.tick(1, {
2387
- candles: candlesRead,
2388
- skip: skippedBreadthChunks,
2389
- chunk: `${originalChunkIndex + 1}/${chunks.length}`,
2390
- symbol
2391
- });
2325
+ const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
2326
+ const missingChunks2 = !hasCoverage({
2327
+ coverage,
2328
+ startMs: breadthStartMs,
2329
+ endMs,
2330
+ intervalMs
2331
+ }) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
2332
+ chunks,
2333
+ coverage,
2334
+ intervalMs
2335
+ }) : [];
2336
+ if (!missingChunks2.length) {
2337
+ console.log(
2338
+ import_chalk.default.gray(`breadth cached: universe=${definition.universe}`)
2339
+ );
2392
2340
  }
2393
- const btcCandles = await connector.kline({
2394
- symbol: "BTCUSDT",
2341
+ return {
2342
+ definition,
2343
+ missingChunkKeys: new Set(
2344
+ missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
2345
+ )
2346
+ };
2347
+ })
2348
+ );
2349
+ const missingChunks = chunks.filter((chunk) => {
2350
+ const key = `${chunk.startMs}:${chunk.endMs}`;
2351
+ return plans.some((plan) => plan.missingChunkKeys.has(key));
2352
+ });
2353
+ const bar = new import_progress.default(
2354
+ "breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
2355
+ {
2356
+ total: Math.max(1, missingChunks.length * breadthSymbols.length),
2357
+ width: 24
2358
+ }
2359
+ );
2360
+ let candlesRead = 0;
2361
+ const skippedBreadthChunks = chunks.length - missingChunks.length;
2362
+ if (!missingChunks.length) {
2363
+ bar.tick(1, {
2364
+ candles: 0,
2365
+ skip: skippedBreadthChunks,
2366
+ chunk: "cached",
2367
+ symbol: "top5/top10/top30/top50/top100"
2368
+ });
2369
+ }
2370
+ for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
2371
+ const chunk = missingChunks[chunkIndex];
2372
+ const originalChunkIndex = chunks.findIndex(
2373
+ (item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
2374
+ );
2375
+ const candlesBySymbol = {};
2376
+ for (const symbol of breadthSymbols) {
2377
+ const candles = await connector.kline({
2378
+ symbol,
2395
2379
  interval: connectorInterval,
2396
2380
  start: chunk.fetchStartMs,
2397
2381
  end: chunk.endMs,
2398
2382
  silent: true
2399
2383
  });
2384
+ candlesBySymbol[symbol] = candles;
2385
+ candlesRead += candles.length;
2386
+ bar.tick(1, {
2387
+ candles: candlesRead,
2388
+ skip: skippedBreadthChunks,
2389
+ chunk: `${originalChunkIndex + 1}/${chunks.length}`,
2390
+ symbol
2391
+ });
2392
+ }
2393
+ const btcCandles = await connector.kline({
2394
+ symbol: "BTCUSDT",
2395
+ interval: connectorInterval,
2396
+ start: chunk.fetchStartMs,
2397
+ end: chunk.endMs,
2398
+ silent: true
2399
+ });
2400
+ const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
2401
+ for (const plan of plans) {
2402
+ if (!plan.missingChunkKeys.has(chunkKey)) {
2403
+ continue;
2404
+ }
2405
+ const symbolSet = new Set(plan.definition.symbols);
2406
+ const universeCandles = Object.fromEntries(
2407
+ Object.entries(candlesBySymbol).filter(
2408
+ ([symbol]) => symbolSet.has(symbol)
2409
+ )
2410
+ );
2400
2411
  const rows = buildMarketBreadthRows({
2401
- universe,
2412
+ universe: plan.definition.universe,
2402
2413
  interval,
2403
- candlesBySymbol,
2414
+ candlesBySymbol: universeCandles,
2404
2415
  btcCandles
2405
2416
  }).filter((row) => {
2406
2417
  const ts = row.ts.getTime();
@@ -2409,8 +2420,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
2409
2420
  await (0, import_timescale.upsertMarketBreadthRows)(rows);
2410
2421
  breadthRows += rows.length;
2411
2422
  }
2412
- } else {
2413
- console.log(import_chalk.default.gray(`breadth cached: universe=${universe}`));
2414
2423
  }
2415
2424
  }
2416
2425
  console.log(
@@ -1675,11 +1675,30 @@ var resolveTimeWindow = ({
1675
1675
  endTime,
1676
1676
  nowMs = Date.now(),
1677
1677
  defaultStartMs,
1678
- defaultEndMs
1678
+ defaultEndMs,
1679
+ closedIntervalMs
1679
1680
  }) => {
1680
1681
  const explicitStart = toEpochMs(startTime);
1681
1682
  const explicitEnd = toEpochMs(endTime);
1682
1683
  const resolvedDays = toPositiveNumber(days);
1684
+ const alignToLastClosedInterval = (window2, preserveDuration) => {
1685
+ if (typeof closedIntervalMs !== "number" || !Number.isFinite(closedIntervalMs) || closedIntervalMs <= 0) {
1686
+ return window2;
1687
+ }
1688
+ const alignedEnd = Math.floor(window2.end / closedIntervalMs) * closedIntervalMs - 1;
1689
+ const delta = window2.end - alignedEnd;
1690
+ const alignedStart = preserveDuration ? window2.start - delta : window2.start;
1691
+ if (alignedStart >= alignedEnd) {
1692
+ throw new Error(
1693
+ `Invalid closed-candle window: start (${alignedStart}) must be less than end (${alignedEnd})`
1694
+ );
1695
+ }
1696
+ return {
1697
+ ...window2,
1698
+ start: Math.trunc(alignedStart),
1699
+ end: Math.trunc(alignedEnd)
1700
+ };
1701
+ };
1683
1702
  if (explicitStart != null || explicitEnd != null) {
1684
1703
  const resolvedStart = explicitStart ?? (resolvedDays != null && explicitEnd != null ? explicitEnd - resolvedDays * ONE_DAY_MS : defaultStartMs);
1685
1704
  const resolvedEnd2 = explicitEnd ?? (resolvedDays != null && explicitStart != null ? explicitStart + resolvedDays * ONE_DAY_MS : defaultEndMs ?? nowMs);
@@ -1688,11 +1707,14 @@ var resolveTimeWindow = ({
1688
1707
  `Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
1689
1708
  );
1690
1709
  }
1691
- return {
1692
- start: resolvedStart,
1693
- end: resolvedEnd2,
1694
- source: "explicit"
1695
- };
1710
+ return alignToLastClosedInterval(
1711
+ {
1712
+ start: resolvedStart,
1713
+ end: resolvedEnd2,
1714
+ source: "explicit"
1715
+ },
1716
+ explicitStart == null && resolvedDays != null
1717
+ );
1696
1718
  }
1697
1719
  if (resolvedDays != null) {
1698
1720
  const resolvedEnd2 = defaultEndMs ?? nowMs;
@@ -1702,11 +1724,14 @@ var resolveTimeWindow = ({
1702
1724
  `Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
1703
1725
  );
1704
1726
  }
1705
- return {
1706
- start: Math.trunc(resolvedStart),
1707
- end: Math.trunc(resolvedEnd2),
1708
- source: "days"
1709
- };
1727
+ return alignToLastClosedInterval(
1728
+ {
1729
+ start: Math.trunc(resolvedStart),
1730
+ end: Math.trunc(resolvedEnd2),
1731
+ source: "days"
1732
+ },
1733
+ true
1734
+ );
1710
1735
  }
1711
1736
  const resolvedEnd = defaultEndMs ?? nowMs;
1712
1737
  if (defaultStartMs >= resolvedEnd) {
@@ -1714,11 +1739,14 @@ var resolveTimeWindow = ({
1714
1739
  `Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
1715
1740
  );
1716
1741
  }
1717
- return {
1718
- start: Math.trunc(defaultStartMs),
1719
- end: Math.trunc(resolvedEnd),
1720
- source: "default"
1721
- };
1742
+ return alignToLastClosedInterval(
1743
+ {
1744
+ start: Math.trunc(defaultStartMs),
1745
+ end: Math.trunc(resolvedEnd),
1746
+ source: "default"
1747
+ },
1748
+ true
1749
+ );
1722
1750
  };
1723
1751
 
1724
1752
  // src/lib/runtimeStrategyBacktest.ts
@@ -2135,7 +2163,8 @@ var prepareRunEnvironment = async ({
2135
2163
  accountId,
2136
2164
  deploymentId,
2137
2165
  assetClasses,
2138
- deployment
2166
+ deployment,
2167
+ closedIntervalMs
2139
2168
  }) => {
2140
2169
  const connectorName = await resolveRunConnectorName({
2141
2170
  value: connector,
@@ -2198,7 +2227,8 @@ var prepareRunEnvironment = async ({
2198
2227
  startTime,
2199
2228
  endTime,
2200
2229
  defaultStartMs: (0, import_time2.getTimestamp)(import_constants.BACKTEST_DEFAULT_DAYS),
2201
- defaultEndMs: (0, import_time2.getTimestamp)()
2230
+ defaultEndMs: (0, import_time2.getTimestamp)(),
2231
+ closedIntervalMs
2202
2232
  });
2203
2233
  const preloadStart = (0, import_time2.getBacktestPreloadStart)(
2204
2234
  window2.start,