backtest-kit 2.2.26 → 2.3.1

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 CHANGED
@@ -700,6 +700,20 @@ async function writeFileAtomic(file, data, options = {}) {
700
700
 
701
701
  var _a$2;
702
702
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
703
+ // Calculate step in milliseconds for candle close time validation
704
+ const INTERVAL_MINUTES$5 = {
705
+ "1m": 1,
706
+ "3m": 3,
707
+ "5m": 5,
708
+ "15m": 15,
709
+ "30m": 30,
710
+ "1h": 60,
711
+ "2h": 120,
712
+ "4h": 240,
713
+ "6h": 360,
714
+ "8h": 480,
715
+ };
716
+ const MS_PER_MINUTE$2 = 60000;
703
717
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
704
718
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
705
719
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -1566,12 +1580,17 @@ class PersistCandleUtils {
1566
1580
  * Reads cached candles for a specific exchange, symbol, and interval.
1567
1581
  * Returns candles only if cache contains exactly the requested limit.
1568
1582
  *
1583
+ * Boundary semantics (EXCLUSIVE):
1584
+ * - sinceTimestamp: candle.timestamp must be > sinceTimestamp
1585
+ * - untilTimestamp: candle.timestamp + stepMs must be < untilTimestamp
1586
+ * - Only fully closed candles within the exclusive range are returned
1587
+ *
1569
1588
  * @param symbol - Trading pair symbol
1570
1589
  * @param interval - Candle interval
1571
1590
  * @param exchangeName - Exchange identifier
1572
1591
  * @param limit - Number of candles requested
1573
- * @param sinceTimestamp - Start timestamp (inclusive)
1574
- * @param untilTimestamp - End timestamp (exclusive)
1592
+ * @param sinceTimestamp - Exclusive start timestamp in milliseconds
1593
+ * @param untilTimestamp - Exclusive end timestamp in milliseconds
1575
1594
  * @returns Promise resolving to array of candles or null if cache is incomplete
1576
1595
  */
1577
1596
  this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
@@ -1587,11 +1606,15 @@ class PersistCandleUtils {
1587
1606
  const isInitial = !this.getCandlesStorage.has(key);
1588
1607
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1589
1608
  await stateStorage.waitForInit(isInitial);
1590
- // Collect all cached candles within the time range
1609
+ const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
1610
+ // Collect all cached candles within the time range using EXCLUSIVE boundaries
1591
1611
  const cachedCandles = [];
1592
1612
  for await (const timestamp of stateStorage.keys()) {
1593
1613
  const ts = Number(timestamp);
1594
- if (ts >= sinceTimestamp && ts < untilTimestamp) {
1614
+ // EXCLUSIVE boundaries:
1615
+ // - candle.timestamp > sinceTimestamp
1616
+ // - candle.timestamp + stepMs < untilTimestamp (fully closed before untilTimestamp)
1617
+ if (ts > sinceTimestamp && ts + stepMs < untilTimestamp) {
1595
1618
  try {
1596
1619
  const candle = await stateStorage.readValue(timestamp);
1597
1620
  cachedCandles.push(candle);
@@ -1617,7 +1640,11 @@ class PersistCandleUtils {
1617
1640
  * Writes candles to cache with atomic file writes.
1618
1641
  * Each candle is stored as a separate JSON file named by its timestamp.
1619
1642
  *
1620
- * @param candles - Array of candle data to cache
1643
+ * The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
1644
+ * - candle.timestamp > sinceTimestamp
1645
+ * - candle.timestamp + stepMs < untilTimestamp
1646
+ *
1647
+ * @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
1621
1648
  * @param symbol - Trading pair symbol
1622
1649
  * @param interval - Candle interval
1623
1650
  * @param exchangeName - Exchange identifier
@@ -1858,13 +1885,20 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
1858
1885
  * Attempts to read candles from cache.
1859
1886
  * Validates cache consistency (no gaps in timestamps) before returning.
1860
1887
  *
1888
+ * Boundary semantics:
1889
+ * - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
1890
+ * - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
1891
+ * - Only fully closed candles within the exclusive range are returned
1892
+ *
1861
1893
  * @param dto - Data transfer object containing symbol, interval, and limit
1862
- * @param sinceTimestamp - Start timestamp in milliseconds
1863
- * @param untilTimestamp - End timestamp in milliseconds
1894
+ * @param sinceTimestamp - Exclusive start timestamp in milliseconds
1895
+ * @param untilTimestamp - Exclusive end timestamp in milliseconds
1864
1896
  * @param self - Instance of ClientExchange
1865
1897
  * @returns Cached candles array or null if cache miss or inconsistent
1866
1898
  */
1867
1899
  const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
1900
+ // PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
1901
+ // Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
1868
1902
  const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
1869
1903
  // Return cached data only if we have exactly the requested limit
1870
1904
  if (cachedCandles.length === dto.limit) {
@@ -1889,7 +1923,11 @@ const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp
1889
1923
  /**
1890
1924
  * Writes candles to cache with error handling.
1891
1925
  *
1892
- * @param candles - Array of candle data to cache
1926
+ * The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
1927
+ * - candle.timestamp > sinceTimestamp
1928
+ * - candle.timestamp + stepMs < untilTimestamp
1929
+ *
1930
+ * @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
1893
1931
  * @param dto - Data transfer object containing symbol, interval, and limit
1894
1932
  * @param self - Instance of ClientExchange
1895
1933
  */
@@ -2055,8 +2093,18 @@ class ClientExchange {
2055
2093
  // Filter candles to strictly match the requested range
2056
2094
  const whenTimestamp = this.params.execution.context.when.getTime();
2057
2095
  const sinceTimestamp = since.getTime();
2058
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2059
- candle.timestamp < whenTimestamp);
2096
+ const stepMs = step * MS_PER_MINUTE$1;
2097
+ const filteredData = allData.filter((candle) => {
2098
+ // EXCLUSIVE boundaries:
2099
+ // - candle.timestamp > sinceTimestamp (exclude exact boundary)
2100
+ // - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
2101
+ if (candle.timestamp <= sinceTimestamp) {
2102
+ return false;
2103
+ }
2104
+ // Check against current time (when)
2105
+ // Only allow candles that have fully CLOSED before "when"
2106
+ return candle.timestamp + stepMs < whenTimestamp;
2107
+ });
2060
2108
  // Apply distinct by timestamp to remove duplicates
2061
2109
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2062
2110
  if (filteredData.length !== uniqueData.length) {
@@ -2088,6 +2136,9 @@ class ClientExchange {
2088
2136
  interval,
2089
2137
  limit,
2090
2138
  });
2139
+ if (!this.params.execution.context.backtest) {
2140
+ throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
2141
+ }
2091
2142
  const since = new Date(this.params.execution.context.when.getTime());
2092
2143
  const now = Date.now();
2093
2144
  // Вычисляем конечное время запроса
@@ -2118,7 +2169,9 @@ class ClientExchange {
2118
2169
  }
2119
2170
  // Filter candles to strictly match the requested range
2120
2171
  const sinceTimestamp = since.getTime();
2121
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < endTime);
2172
+ const stepMs = step * MS_PER_MINUTE$1;
2173
+ const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
2174
+ candle.timestamp + stepMs < endTime);
2122
2175
  // Apply distinct by timestamp to remove duplicates
2123
2176
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2124
2177
  if (filteredData.length !== uniqueData.length) {
@@ -2319,8 +2372,10 @@ class ClientExchange {
2319
2372
  allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
2320
2373
  }
2321
2374
  // Filter candles to strictly match the requested range
2322
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2323
- candle.timestamp < untilTimestamp);
2375
+ // Only include candles that have fully CLOSED before untilTimestamp
2376
+ const stepMs = step * MS_PER_MINUTE$1;
2377
+ const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
2378
+ candle.timestamp + stepMs < untilTimestamp);
2324
2379
  // Apply distinct by timestamp to remove duplicates
2325
2380
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2326
2381
  if (filteredData.length !== uniqueData.length) {
@@ -12288,6 +12343,13 @@ class WalkerSchemaService {
12288
12343
  }
12289
12344
  }
12290
12345
 
12346
+ /**
12347
+ * Компенсация для exclusive boundaries при фильтрации свечей.
12348
+ * ClientExchange.getNextCandles использует фильтр:
12349
+ * timestamp > since && timestamp + stepMs < endTime
12350
+ * который исключает первую и последнюю свечи из запрошенного диапазона.
12351
+ */
12352
+ const CANDLE_EXCLUSIVE_BOUNDARY_OFFSET = 2;
12291
12353
  /**
12292
12354
  * Private service for backtest orchestration using async generators.
12293
12355
  *
@@ -12553,7 +12615,7 @@ class BacktestLogicPrivateService {
12553
12615
  // Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
12554
12616
  const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
12555
12617
  const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
12556
- const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
12618
+ const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT + CANDLE_EXCLUSIVE_BOUNDARY_OFFSET;
12557
12619
  let candles;
12558
12620
  try {
12559
12621
  candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
@@ -13839,6 +13901,18 @@ const live_columns = [
13839
13901
  format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
13840
13902
  isVisible: () => true,
13841
13903
  },
13904
+ {
13905
+ key: "pendingAt",
13906
+ label: "Pending At",
13907
+ format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
13908
+ isVisible: () => true,
13909
+ },
13910
+ {
13911
+ key: "scheduledAt",
13912
+ label: "Scheduled At",
13913
+ format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
13914
+ isVisible: () => true,
13915
+ },
13842
13916
  ];
13843
13917
 
13844
13918
  /**
@@ -13963,6 +14037,18 @@ const partial_columns = [
13963
14037
  format: (data) => data.note || "",
13964
14038
  isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
13965
14039
  },
14040
+ {
14041
+ key: "pendingAt",
14042
+ label: "Pending At",
14043
+ format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
14044
+ isVisible: () => true,
14045
+ },
14046
+ {
14047
+ key: "scheduledAt",
14048
+ label: "Scheduled At",
14049
+ format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
14050
+ isVisible: () => true,
14051
+ },
13966
14052
  {
13967
14053
  key: "timestamp",
13968
14054
  label: "Timestamp",
@@ -14085,6 +14171,18 @@ const breakeven_columns = [
14085
14171
  format: (data) => data.note || "",
14086
14172
  isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
14087
14173
  },
14174
+ {
14175
+ key: "pendingAt",
14176
+ label: "Pending At",
14177
+ format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
14178
+ isVisible: () => true,
14179
+ },
14180
+ {
14181
+ key: "scheduledAt",
14182
+ label: "Scheduled At",
14183
+ format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
14184
+ isVisible: () => true,
14185
+ },
14088
14186
  {
14089
14187
  key: "timestamp",
14090
14188
  label: "Timestamp",
@@ -14363,6 +14461,22 @@ const risk_columns = [
14363
14461
  format: (data) => data.rejectionNote,
14364
14462
  isVisible: () => true,
14365
14463
  },
14464
+ {
14465
+ key: "pendingAt",
14466
+ label: "Pending At",
14467
+ format: (data) => data.currentSignal.pendingAt !== undefined
14468
+ ? new Date(data.currentSignal.pendingAt).toISOString()
14469
+ : "N/A",
14470
+ isVisible: () => true,
14471
+ },
14472
+ {
14473
+ key: "scheduledAt",
14474
+ label: "Scheduled At",
14475
+ format: (data) => data.currentSignal.scheduledAt !== undefined
14476
+ ? new Date(data.currentSignal.scheduledAt).toISOString()
14477
+ : "N/A",
14478
+ isVisible: () => true,
14479
+ },
14366
14480
  {
14367
14481
  key: "timestamp",
14368
14482
  label: "Timestamp",
@@ -14513,6 +14627,18 @@ const schedule_columns = [
14513
14627
  format: (data) => data.cancelId ?? "N/A",
14514
14628
  isVisible: () => true,
14515
14629
  },
14630
+ {
14631
+ key: "pendingAt",
14632
+ label: "Pending At",
14633
+ format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
14634
+ isVisible: () => true,
14635
+ },
14636
+ {
14637
+ key: "scheduledAt",
14638
+ label: "Scheduled At",
14639
+ format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
14640
+ isVisible: () => true,
14641
+ },
14516
14642
  ];
14517
14643
 
14518
14644
  /**
@@ -15904,6 +16030,8 @@ let ReportStorage$6 = class ReportStorage {
15904
16030
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
15905
16031
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
15906
16032
  partialExecuted: data.signal.partialExecuted,
16033
+ pendingAt: data.signal.pendingAt,
16034
+ scheduledAt: data.signal.scheduledAt,
15907
16035
  });
15908
16036
  // Trim queue if exceeded MAX_EVENTS
15909
16037
  if (this._eventList.length > MAX_EVENTS$7) {
@@ -15934,6 +16062,8 @@ let ReportStorage$6 = class ReportStorage {
15934
16062
  percentTp: data.percentTp,
15935
16063
  percentSl: data.percentSl,
15936
16064
  pnl: data.pnl.pnlPercentage,
16065
+ pendingAt: data.signal.pendingAt,
16066
+ scheduledAt: data.signal.scheduledAt,
15937
16067
  };
15938
16068
  // Find the last active event with the same signalId
15939
16069
  const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
@@ -15974,6 +16104,8 @@ let ReportStorage$6 = class ReportStorage {
15974
16104
  pnl: data.pnl.pnlPercentage,
15975
16105
  closeReason: data.closeReason,
15976
16106
  duration: durationMin,
16107
+ pendingAt: data.signal.pendingAt,
16108
+ scheduledAt: data.signal.scheduledAt,
15977
16109
  };
15978
16110
  this._eventList.unshift(newEvent);
15979
16111
  // Trim queue if exceeded MAX_EVENTS
@@ -16001,6 +16133,7 @@ let ReportStorage$6 = class ReportStorage {
16001
16133
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
16002
16134
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16003
16135
  partialExecuted: data.signal.partialExecuted,
16136
+ scheduledAt: data.signal.scheduledAt,
16004
16137
  });
16005
16138
  // Trim queue if exceeded MAX_EVENTS
16006
16139
  if (this._eventList.length > MAX_EVENTS$7) {
@@ -16031,6 +16164,7 @@ let ReportStorage$6 = class ReportStorage {
16031
16164
  percentTp: data.percentTp,
16032
16165
  percentSl: data.percentSl,
16033
16166
  pnl: data.pnl.pnlPercentage,
16167
+ scheduledAt: data.signal.scheduledAt,
16034
16168
  };
16035
16169
  // Find the last waiting event with the same signalId
16036
16170
  const lastWaitingIndex = this._eventList.findLastIndex((event) => event.action === "waiting" && event.signalId === data.signal.id);
@@ -16067,6 +16201,7 @@ let ReportStorage$6 = class ReportStorage {
16067
16201
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16068
16202
  partialExecuted: data.signal.partialExecuted,
16069
16203
  cancelReason: data.reason,
16204
+ scheduledAt: data.signal.scheduledAt,
16070
16205
  });
16071
16206
  // Trim queue if exceeded MAX_EVENTS
16072
16207
  if (this._eventList.length > MAX_EVENTS$7) {
@@ -16558,6 +16693,7 @@ let ReportStorage$5 = class ReportStorage {
16558
16693
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
16559
16694
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16560
16695
  partialExecuted: data.signal.partialExecuted,
16696
+ scheduledAt: data.signal.scheduledAt,
16561
16697
  });
16562
16698
  // Trim queue if exceeded MAX_EVENTS
16563
16699
  if (this._eventList.length > MAX_EVENTS$6) {
@@ -16587,6 +16723,8 @@ let ReportStorage$5 = class ReportStorage {
16587
16723
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16588
16724
  partialExecuted: data.signal.partialExecuted,
16589
16725
  duration: durationMin,
16726
+ pendingAt: data.signal.pendingAt,
16727
+ scheduledAt: data.signal.scheduledAt,
16590
16728
  };
16591
16729
  this._eventList.unshift(newEvent);
16592
16730
  // Trim queue if exceeded MAX_EVENTS
@@ -16620,6 +16758,7 @@ let ReportStorage$5 = class ReportStorage {
16620
16758
  duration: durationMin,
16621
16759
  cancelReason: data.reason,
16622
16760
  cancelId: data.cancelId,
16761
+ scheduledAt: data.signal.scheduledAt,
16623
16762
  };
16624
16763
  this._eventList.unshift(newEvent);
16625
16764
  // Trim queue if exceeded MAX_EVENTS
@@ -19761,6 +19900,8 @@ let ReportStorage$3 = class ReportStorage {
19761
19900
  originalPriceStopLoss: data.originalPriceStopLoss,
19762
19901
  partialExecuted: data.partialExecuted,
19763
19902
  note: data.note,
19903
+ pendingAt: data.pendingAt,
19904
+ scheduledAt: data.scheduledAt,
19764
19905
  backtest,
19765
19906
  });
19766
19907
  // Trim queue if exceeded MAX_EVENTS
@@ -19793,6 +19934,8 @@ let ReportStorage$3 = class ReportStorage {
19793
19934
  originalPriceStopLoss: data.originalPriceStopLoss,
19794
19935
  partialExecuted: data.partialExecuted,
19795
19936
  note: data.note,
19937
+ pendingAt: data.pendingAt,
19938
+ scheduledAt: data.scheduledAt,
19796
19939
  backtest,
19797
19940
  });
19798
19941
  // Trim queue if exceeded MAX_EVENTS
@@ -20890,6 +21033,8 @@ let ReportStorage$2 = class ReportStorage {
20890
21033
  originalPriceStopLoss: data.originalPriceStopLoss,
20891
21034
  partialExecuted: data.partialExecuted,
20892
21035
  note: data.note,
21036
+ pendingAt: data.pendingAt,
21037
+ scheduledAt: data.scheduledAt,
20893
21038
  backtest,
20894
21039
  });
20895
21040
  // Trim queue if exceeded MAX_EVENTS
@@ -23275,9 +23420,15 @@ class HeatReportService {
23275
23420
  signalId: data.signal?.id,
23276
23421
  position: data.signal?.position,
23277
23422
  note: data.signal?.note,
23423
+ priceOpen: data.signal?.priceOpen,
23424
+ priceTakeProfit: data.signal?.priceTakeProfit,
23425
+ priceStopLoss: data.signal?.priceStopLoss,
23426
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
23427
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
23278
23428
  pnl: data.pnl.pnlPercentage,
23279
23429
  closeReason: data.closeReason,
23280
23430
  openTime: data.signal?.pendingAt,
23431
+ scheduledAt: data.signal?.scheduledAt,
23281
23432
  closeTime: data.closeTimestamp,
23282
23433
  }, {
23283
23434
  symbol: data.symbol,
@@ -23686,6 +23837,8 @@ class RiskReportService {
23686
23837
  originalPriceStopLoss: data.currentSignal?.originalPriceStopLoss,
23687
23838
  partialExecuted: data.currentSignal?.partialExecuted,
23688
23839
  note: data.currentSignal?.note,
23840
+ pendingAt: data.currentSignal?.pendingAt,
23841
+ scheduledAt: data.currentSignal?.scheduledAt,
23689
23842
  minuteEstimatedTime: data.currentSignal?.minuteEstimatedTime,
23690
23843
  }, {
23691
23844
  symbol: data.symbol,
@@ -25628,6 +25781,7 @@ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
25628
25781
  const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
25629
25782
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
25630
25783
  const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
25784
+ const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
25631
25785
  /**
25632
25786
  * Checks if trade context is active (execution and method contexts).
25633
25787
  *
@@ -25932,6 +26086,30 @@ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
25932
26086
  }
25933
26087
  return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
25934
26088
  }
26089
+ /**
26090
+ * Fetches the set of candles after current time based on execution context.
26091
+ *
26092
+ * Uses the exchange's getNextCandles implementation to retrieve candles
26093
+ * that occur after the current context time.
26094
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26095
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
26096
+ * @param limit - Number of candles to fetch
26097
+ * @returns Promise resolving to array of candle data
26098
+ */
26099
+ async function getNextCandles(symbol, interval, limit) {
26100
+ bt.loggerService.info(GET_NEXT_CANDLES_METHOD_NAME, {
26101
+ symbol,
26102
+ interval,
26103
+ limit,
26104
+ });
26105
+ if (!ExecutionContextService.hasContext()) {
26106
+ throw new Error("getNextCandles requires an execution context");
26107
+ }
26108
+ if (!MethodContextService.hasContext()) {
26109
+ throw new Error("getNextCandles requires a method context");
26110
+ }
26111
+ return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
26112
+ }
25935
26113
 
25936
26114
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
25937
26115
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
@@ -32865,6 +33043,16 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
32865
33043
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
32866
33044
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
32867
33045
  const MS_PER_MINUTE = 60000;
33046
+ /**
33047
+ * Gets current timestamp from execution context if available.
33048
+ * Returns current Date() if no execution context exists (non-trading GUI).
33049
+ */
33050
+ const GET_TIMESTAMP_FN = async () => {
33051
+ if (ExecutionContextService.hasContext()) {
33052
+ return new Date(bt.executionContextService.context.when);
33053
+ }
33054
+ return new Date();
33055
+ };
32868
33056
  /**
32869
33057
  * Gets backtest mode flag from execution context if available.
32870
33058
  * Returns false if no execution context exists (live mode).
@@ -32944,13 +33132,20 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
32944
33132
  * Attempts to read candles from cache.
32945
33133
  * Validates cache consistency (no gaps in timestamps) before returning.
32946
33134
  *
33135
+ * Boundary semantics:
33136
+ * - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
33137
+ * - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
33138
+ * - Only fully closed candles within the exclusive range are returned
33139
+ *
32947
33140
  * @param dto - Data transfer object containing symbol, interval, and limit
32948
- * @param sinceTimestamp - Start timestamp in milliseconds
32949
- * @param untilTimestamp - End timestamp in milliseconds
33141
+ * @param sinceTimestamp - Exclusive start timestamp in milliseconds
33142
+ * @param untilTimestamp - Exclusive end timestamp in milliseconds
32950
33143
  * @param exchangeName - Exchange name
32951
33144
  * @returns Cached candles array or null if cache miss or inconsistent
32952
33145
  */
32953
33146
  const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
33147
+ // PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
33148
+ // Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
32954
33149
  const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
32955
33150
  // Return cached data only if we have exactly the requested limit
32956
33151
  if (cachedCandles.length === dto.limit) {
@@ -32975,7 +33170,11 @@ const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp,
32975
33170
  /**
32976
33171
  * Writes candles to cache with error handling.
32977
33172
  *
32978
- * @param candles - Array of candle data to cache
33173
+ * The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
33174
+ * - candle.timestamp > sinceTimestamp
33175
+ * - candle.timestamp + stepMs < untilTimestamp
33176
+ *
33177
+ * @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
32979
33178
  * @param dto - Data transfer object containing symbol, interval, and limit
32980
33179
  * @param exchangeName - Exchange name
32981
33180
  */
@@ -33050,10 +33249,10 @@ class ExchangeInstance {
33050
33249
  if (!adjust) {
33051
33250
  throw new Error(`ExchangeInstance unknown time adjust for interval=${interval}`);
33052
33251
  }
33053
- const when = new Date(Date.now());
33054
- const since = new Date(when.getTime() - adjust * 60 * 1000);
33252
+ const when = await GET_TIMESTAMP_FN();
33253
+ const since = new Date(when.getTime() - adjust * MS_PER_MINUTE);
33055
33254
  const sinceTimestamp = since.getTime();
33056
- const untilTimestamp = sinceTimestamp + limit * step * 60 * 1000;
33255
+ const untilTimestamp = sinceTimestamp + limit * step * MS_PER_MINUTE;
33057
33256
  // Try to read from cache first
33058
33257
  const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
33059
33258
  if (cachedCandles !== null) {
@@ -33072,7 +33271,7 @@ class ExchangeInstance {
33072
33271
  remaining -= chunkLimit;
33073
33272
  if (remaining > 0) {
33074
33273
  // Move currentSince forward by the number of candles fetched
33075
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
33274
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
33076
33275
  }
33077
33276
  }
33078
33277
  }
@@ -33082,8 +33281,18 @@ class ExchangeInstance {
33082
33281
  }
33083
33282
  // Filter candles to strictly match the requested range
33084
33283
  const whenTimestamp = when.getTime();
33085
- const stepMs = step * 60 * 1000;
33086
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < whenTimestamp + stepMs);
33284
+ const stepMs = step * MS_PER_MINUTE;
33285
+ const filteredData = allData.filter((candle) => {
33286
+ // EXCLUSIVE boundaries:
33287
+ // - candle.timestamp > sinceTimestamp (exclude exact boundary)
33288
+ // - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
33289
+ if (candle.timestamp <= sinceTimestamp) {
33290
+ return false;
33291
+ }
33292
+ // Check against current time (when)
33293
+ // Only allow candles that have fully CLOSED before "when"
33294
+ return candle.timestamp + stepMs < whenTimestamp;
33295
+ });
33087
33296
  // Apply distinct by timestamp to remove duplicates
33088
33297
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
33089
33298
  if (filteredData.length !== uniqueData.length) {
@@ -33213,8 +33422,8 @@ class ExchangeInstance {
33213
33422
  symbol,
33214
33423
  depth,
33215
33424
  });
33216
- const to = new Date(Date.now());
33217
- const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
33425
+ const to = await GET_TIMESTAMP_FN();
33426
+ const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
33218
33427
  const isBacktest = await GET_BACKTEST_FN();
33219
33428
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
33220
33429
  };
@@ -33261,7 +33470,8 @@ class ExchangeInstance {
33261
33470
  if (!step) {
33262
33471
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
33263
33472
  }
33264
- const nowTimestamp = Date.now();
33473
+ const when = await GET_TIMESTAMP_FN();
33474
+ const nowTimestamp = when.getTime();
33265
33475
  let sinceTimestamp;
33266
33476
  let untilTimestamp;
33267
33477
  let calculatedLimit;
@@ -33349,8 +33559,10 @@ class ExchangeInstance {
33349
33559
  allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
33350
33560
  }
33351
33561
  // Filter candles to strictly match the requested range
33352
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
33353
- candle.timestamp < untilTimestamp);
33562
+ // Only include candles that have fully CLOSED before untilTimestamp
33563
+ const stepMs = step * MS_PER_MINUTE;
33564
+ const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
33565
+ candle.timestamp + stepMs < untilTimestamp);
33354
33566
  // Apply distinct by timestamp to remove duplicates
33355
33567
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
33356
33568
  if (filteredData.length !== uniqueData.length) {
@@ -35018,6 +35230,7 @@ exports.getDefaultConfig = getDefaultConfig;
35018
35230
  exports.getExchangeSchema = getExchangeSchema;
35019
35231
  exports.getFrameSchema = getFrameSchema;
35020
35232
  exports.getMode = getMode;
35233
+ exports.getNextCandles = getNextCandles;
35021
35234
  exports.getOrderBook = getOrderBook;
35022
35235
  exports.getRawCandles = getRawCandles;
35023
35236
  exports.getRiskSchema = getRiskSchema;