backtest-kit 2.2.25 → 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.mjs CHANGED
@@ -680,6 +680,20 @@ async function writeFileAtomic(file, data, options = {}) {
680
680
 
681
681
  var _a$2;
682
682
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
683
+ // Calculate step in milliseconds for candle close time validation
684
+ const INTERVAL_MINUTES$5 = {
685
+ "1m": 1,
686
+ "3m": 3,
687
+ "5m": 5,
688
+ "15m": 15,
689
+ "30m": 30,
690
+ "1h": 60,
691
+ "2h": 120,
692
+ "4h": 240,
693
+ "6h": 360,
694
+ "8h": 480,
695
+ };
696
+ const MS_PER_MINUTE$2 = 60000;
683
697
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
684
698
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
685
699
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -1546,12 +1560,17 @@ class PersistCandleUtils {
1546
1560
  * Reads cached candles for a specific exchange, symbol, and interval.
1547
1561
  * Returns candles only if cache contains exactly the requested limit.
1548
1562
  *
1563
+ * Boundary semantics (EXCLUSIVE):
1564
+ * - sinceTimestamp: candle.timestamp must be > sinceTimestamp
1565
+ * - untilTimestamp: candle.timestamp + stepMs must be < untilTimestamp
1566
+ * - Only fully closed candles within the exclusive range are returned
1567
+ *
1549
1568
  * @param symbol - Trading pair symbol
1550
1569
  * @param interval - Candle interval
1551
1570
  * @param exchangeName - Exchange identifier
1552
1571
  * @param limit - Number of candles requested
1553
- * @param sinceTimestamp - Start timestamp (inclusive)
1554
- * @param untilTimestamp - End timestamp (exclusive)
1572
+ * @param sinceTimestamp - Exclusive start timestamp in milliseconds
1573
+ * @param untilTimestamp - Exclusive end timestamp in milliseconds
1555
1574
  * @returns Promise resolving to array of candles or null if cache is incomplete
1556
1575
  */
1557
1576
  this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
@@ -1567,11 +1586,15 @@ class PersistCandleUtils {
1567
1586
  const isInitial = !this.getCandlesStorage.has(key);
1568
1587
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1569
1588
  await stateStorage.waitForInit(isInitial);
1570
- // Collect all cached candles within the time range
1589
+ const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
1590
+ // Collect all cached candles within the time range using EXCLUSIVE boundaries
1571
1591
  const cachedCandles = [];
1572
1592
  for await (const timestamp of stateStorage.keys()) {
1573
1593
  const ts = Number(timestamp);
1574
- if (ts >= sinceTimestamp && ts < untilTimestamp) {
1594
+ // EXCLUSIVE boundaries:
1595
+ // - candle.timestamp > sinceTimestamp
1596
+ // - candle.timestamp + stepMs < untilTimestamp (fully closed before untilTimestamp)
1597
+ if (ts > sinceTimestamp && ts + stepMs < untilTimestamp) {
1575
1598
  try {
1576
1599
  const candle = await stateStorage.readValue(timestamp);
1577
1600
  cachedCandles.push(candle);
@@ -1597,7 +1620,11 @@ class PersistCandleUtils {
1597
1620
  * Writes candles to cache with atomic file writes.
1598
1621
  * Each candle is stored as a separate JSON file named by its timestamp.
1599
1622
  *
1600
- * @param candles - Array of candle data to cache
1623
+ * The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
1624
+ * - candle.timestamp > sinceTimestamp
1625
+ * - candle.timestamp + stepMs < untilTimestamp
1626
+ *
1627
+ * @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
1601
1628
  * @param symbol - Trading pair symbol
1602
1629
  * @param interval - Candle interval
1603
1630
  * @param exchangeName - Exchange identifier
@@ -1838,13 +1865,20 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
1838
1865
  * Attempts to read candles from cache.
1839
1866
  * Validates cache consistency (no gaps in timestamps) before returning.
1840
1867
  *
1868
+ * Boundary semantics:
1869
+ * - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
1870
+ * - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
1871
+ * - Only fully closed candles within the exclusive range are returned
1872
+ *
1841
1873
  * @param dto - Data transfer object containing symbol, interval, and limit
1842
- * @param sinceTimestamp - Start timestamp in milliseconds
1843
- * @param untilTimestamp - End timestamp in milliseconds
1874
+ * @param sinceTimestamp - Exclusive start timestamp in milliseconds
1875
+ * @param untilTimestamp - Exclusive end timestamp in milliseconds
1844
1876
  * @param self - Instance of ClientExchange
1845
1877
  * @returns Cached candles array or null if cache miss or inconsistent
1846
1878
  */
1847
1879
  const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
1880
+ // PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
1881
+ // Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
1848
1882
  const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
1849
1883
  // Return cached data only if we have exactly the requested limit
1850
1884
  if (cachedCandles.length === dto.limit) {
@@ -1869,7 +1903,11 @@ const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimest
1869
1903
  /**
1870
1904
  * Writes candles to cache with error handling.
1871
1905
  *
1872
- * @param candles - Array of candle data to cache
1906
+ * The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
1907
+ * - candle.timestamp > sinceTimestamp
1908
+ * - candle.timestamp + stepMs < untilTimestamp
1909
+ *
1910
+ * @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
1873
1911
  * @param dto - Data transfer object containing symbol, interval, and limit
1874
1912
  * @param self - Instance of ClientExchange
1875
1913
  */
@@ -2035,8 +2073,18 @@ class ClientExchange {
2035
2073
  // Filter candles to strictly match the requested range
2036
2074
  const whenTimestamp = this.params.execution.context.when.getTime();
2037
2075
  const sinceTimestamp = since.getTime();
2038
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2039
- candle.timestamp < whenTimestamp);
2076
+ const stepMs = step * MS_PER_MINUTE$1;
2077
+ const filteredData = allData.filter((candle) => {
2078
+ // EXCLUSIVE boundaries:
2079
+ // - candle.timestamp > sinceTimestamp (exclude exact boundary)
2080
+ // - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
2081
+ if (candle.timestamp <= sinceTimestamp) {
2082
+ return false;
2083
+ }
2084
+ // Check against current time (when)
2085
+ // Only allow candles that have fully CLOSED before "when"
2086
+ return candle.timestamp + stepMs < whenTimestamp;
2087
+ });
2040
2088
  // Apply distinct by timestamp to remove duplicates
2041
2089
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2042
2090
  if (filteredData.length !== uniqueData.length) {
@@ -2068,6 +2116,9 @@ class ClientExchange {
2068
2116
  interval,
2069
2117
  limit,
2070
2118
  });
2119
+ if (!this.params.execution.context.backtest) {
2120
+ throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
2121
+ }
2071
2122
  const since = new Date(this.params.execution.context.when.getTime());
2072
2123
  const now = Date.now();
2073
2124
  // Вычисляем конечное время запроса
@@ -2098,7 +2149,9 @@ class ClientExchange {
2098
2149
  }
2099
2150
  // Filter candles to strictly match the requested range
2100
2151
  const sinceTimestamp = since.getTime();
2101
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < endTime);
2152
+ const stepMs = step * MS_PER_MINUTE$1;
2153
+ const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
2154
+ candle.timestamp + stepMs < endTime);
2102
2155
  // Apply distinct by timestamp to remove duplicates
2103
2156
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2104
2157
  if (filteredData.length !== uniqueData.length) {
@@ -2299,8 +2352,10 @@ class ClientExchange {
2299
2352
  allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
2300
2353
  }
2301
2354
  // Filter candles to strictly match the requested range
2302
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2303
- candle.timestamp < untilTimestamp);
2355
+ // Only include candles that have fully CLOSED before untilTimestamp
2356
+ const stepMs = step * MS_PER_MINUTE$1;
2357
+ const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
2358
+ candle.timestamp + stepMs < untilTimestamp);
2304
2359
  // Apply distinct by timestamp to remove duplicates
2305
2360
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2306
2361
  if (filteredData.length !== uniqueData.length) {
@@ -12268,6 +12323,13 @@ class WalkerSchemaService {
12268
12323
  }
12269
12324
  }
12270
12325
 
12326
+ /**
12327
+ * Компенсация для exclusive boundaries при фильтрации свечей.
12328
+ * ClientExchange.getNextCandles использует фильтр:
12329
+ * timestamp > since && timestamp + stepMs < endTime
12330
+ * который исключает первую и последнюю свечи из запрошенного диапазона.
12331
+ */
12332
+ const CANDLE_EXCLUSIVE_BOUNDARY_OFFSET = 2;
12271
12333
  /**
12272
12334
  * Private service for backtest orchestration using async generators.
12273
12335
  *
@@ -12533,7 +12595,7 @@ class BacktestLogicPrivateService {
12533
12595
  // Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
12534
12596
  const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
12535
12597
  const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
12536
- const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
12598
+ const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT + CANDLE_EXCLUSIVE_BOUNDARY_OFFSET;
12537
12599
  let candles;
12538
12600
  try {
12539
12601
  candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
@@ -13819,6 +13881,18 @@ const live_columns = [
13819
13881
  format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
13820
13882
  isVisible: () => true,
13821
13883
  },
13884
+ {
13885
+ key: "pendingAt",
13886
+ label: "Pending At",
13887
+ format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
13888
+ isVisible: () => true,
13889
+ },
13890
+ {
13891
+ key: "scheduledAt",
13892
+ label: "Scheduled At",
13893
+ format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
13894
+ isVisible: () => true,
13895
+ },
13822
13896
  ];
13823
13897
 
13824
13898
  /**
@@ -13943,6 +14017,18 @@ const partial_columns = [
13943
14017
  format: (data) => data.note || "",
13944
14018
  isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
13945
14019
  },
14020
+ {
14021
+ key: "pendingAt",
14022
+ label: "Pending At",
14023
+ format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
14024
+ isVisible: () => true,
14025
+ },
14026
+ {
14027
+ key: "scheduledAt",
14028
+ label: "Scheduled At",
14029
+ format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
14030
+ isVisible: () => true,
14031
+ },
13946
14032
  {
13947
14033
  key: "timestamp",
13948
14034
  label: "Timestamp",
@@ -14065,6 +14151,18 @@ const breakeven_columns = [
14065
14151
  format: (data) => data.note || "",
14066
14152
  isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
14067
14153
  },
14154
+ {
14155
+ key: "pendingAt",
14156
+ label: "Pending At",
14157
+ format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
14158
+ isVisible: () => true,
14159
+ },
14160
+ {
14161
+ key: "scheduledAt",
14162
+ label: "Scheduled At",
14163
+ format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
14164
+ isVisible: () => true,
14165
+ },
14068
14166
  {
14069
14167
  key: "timestamp",
14070
14168
  label: "Timestamp",
@@ -14343,6 +14441,22 @@ const risk_columns = [
14343
14441
  format: (data) => data.rejectionNote,
14344
14442
  isVisible: () => true,
14345
14443
  },
14444
+ {
14445
+ key: "pendingAt",
14446
+ label: "Pending At",
14447
+ format: (data) => data.currentSignal.pendingAt !== undefined
14448
+ ? new Date(data.currentSignal.pendingAt).toISOString()
14449
+ : "N/A",
14450
+ isVisible: () => true,
14451
+ },
14452
+ {
14453
+ key: "scheduledAt",
14454
+ label: "Scheduled At",
14455
+ format: (data) => data.currentSignal.scheduledAt !== undefined
14456
+ ? new Date(data.currentSignal.scheduledAt).toISOString()
14457
+ : "N/A",
14458
+ isVisible: () => true,
14459
+ },
14346
14460
  {
14347
14461
  key: "timestamp",
14348
14462
  label: "Timestamp",
@@ -14493,6 +14607,18 @@ const schedule_columns = [
14493
14607
  format: (data) => data.cancelId ?? "N/A",
14494
14608
  isVisible: () => true,
14495
14609
  },
14610
+ {
14611
+ key: "pendingAt",
14612
+ label: "Pending At",
14613
+ format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
14614
+ isVisible: () => true,
14615
+ },
14616
+ {
14617
+ key: "scheduledAt",
14618
+ label: "Scheduled At",
14619
+ format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
14620
+ isVisible: () => true,
14621
+ },
14496
14622
  ];
14497
14623
 
14498
14624
  /**
@@ -15884,6 +16010,8 @@ let ReportStorage$6 = class ReportStorage {
15884
16010
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
15885
16011
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
15886
16012
  partialExecuted: data.signal.partialExecuted,
16013
+ pendingAt: data.signal.pendingAt,
16014
+ scheduledAt: data.signal.scheduledAt,
15887
16015
  });
15888
16016
  // Trim queue if exceeded MAX_EVENTS
15889
16017
  if (this._eventList.length > MAX_EVENTS$7) {
@@ -15914,6 +16042,8 @@ let ReportStorage$6 = class ReportStorage {
15914
16042
  percentTp: data.percentTp,
15915
16043
  percentSl: data.percentSl,
15916
16044
  pnl: data.pnl.pnlPercentage,
16045
+ pendingAt: data.signal.pendingAt,
16046
+ scheduledAt: data.signal.scheduledAt,
15917
16047
  };
15918
16048
  // Find the last active event with the same signalId
15919
16049
  const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
@@ -15954,6 +16084,8 @@ let ReportStorage$6 = class ReportStorage {
15954
16084
  pnl: data.pnl.pnlPercentage,
15955
16085
  closeReason: data.closeReason,
15956
16086
  duration: durationMin,
16087
+ pendingAt: data.signal.pendingAt,
16088
+ scheduledAt: data.signal.scheduledAt,
15957
16089
  };
15958
16090
  this._eventList.unshift(newEvent);
15959
16091
  // Trim queue if exceeded MAX_EVENTS
@@ -15981,6 +16113,7 @@ let ReportStorage$6 = class ReportStorage {
15981
16113
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
15982
16114
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
15983
16115
  partialExecuted: data.signal.partialExecuted,
16116
+ scheduledAt: data.signal.scheduledAt,
15984
16117
  });
15985
16118
  // Trim queue if exceeded MAX_EVENTS
15986
16119
  if (this._eventList.length > MAX_EVENTS$7) {
@@ -16011,6 +16144,7 @@ let ReportStorage$6 = class ReportStorage {
16011
16144
  percentTp: data.percentTp,
16012
16145
  percentSl: data.percentSl,
16013
16146
  pnl: data.pnl.pnlPercentage,
16147
+ scheduledAt: data.signal.scheduledAt,
16014
16148
  };
16015
16149
  // Find the last waiting event with the same signalId
16016
16150
  const lastWaitingIndex = this._eventList.findLastIndex((event) => event.action === "waiting" && event.signalId === data.signal.id);
@@ -16047,6 +16181,7 @@ let ReportStorage$6 = class ReportStorage {
16047
16181
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16048
16182
  partialExecuted: data.signal.partialExecuted,
16049
16183
  cancelReason: data.reason,
16184
+ scheduledAt: data.signal.scheduledAt,
16050
16185
  });
16051
16186
  // Trim queue if exceeded MAX_EVENTS
16052
16187
  if (this._eventList.length > MAX_EVENTS$7) {
@@ -16538,6 +16673,7 @@ let ReportStorage$5 = class ReportStorage {
16538
16673
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
16539
16674
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16540
16675
  partialExecuted: data.signal.partialExecuted,
16676
+ scheduledAt: data.signal.scheduledAt,
16541
16677
  });
16542
16678
  // Trim queue if exceeded MAX_EVENTS
16543
16679
  if (this._eventList.length > MAX_EVENTS$6) {
@@ -16567,6 +16703,8 @@ let ReportStorage$5 = class ReportStorage {
16567
16703
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
16568
16704
  partialExecuted: data.signal.partialExecuted,
16569
16705
  duration: durationMin,
16706
+ pendingAt: data.signal.pendingAt,
16707
+ scheduledAt: data.signal.scheduledAt,
16570
16708
  };
16571
16709
  this._eventList.unshift(newEvent);
16572
16710
  // Trim queue if exceeded MAX_EVENTS
@@ -16600,6 +16738,7 @@ let ReportStorage$5 = class ReportStorage {
16600
16738
  duration: durationMin,
16601
16739
  cancelReason: data.reason,
16602
16740
  cancelId: data.cancelId,
16741
+ scheduledAt: data.signal.scheduledAt,
16603
16742
  };
16604
16743
  this._eventList.unshift(newEvent);
16605
16744
  // Trim queue if exceeded MAX_EVENTS
@@ -19741,6 +19880,8 @@ let ReportStorage$3 = class ReportStorage {
19741
19880
  originalPriceStopLoss: data.originalPriceStopLoss,
19742
19881
  partialExecuted: data.partialExecuted,
19743
19882
  note: data.note,
19883
+ pendingAt: data.pendingAt,
19884
+ scheduledAt: data.scheduledAt,
19744
19885
  backtest,
19745
19886
  });
19746
19887
  // Trim queue if exceeded MAX_EVENTS
@@ -19773,6 +19914,8 @@ let ReportStorage$3 = class ReportStorage {
19773
19914
  originalPriceStopLoss: data.originalPriceStopLoss,
19774
19915
  partialExecuted: data.partialExecuted,
19775
19916
  note: data.note,
19917
+ pendingAt: data.pendingAt,
19918
+ scheduledAt: data.scheduledAt,
19776
19919
  backtest,
19777
19920
  });
19778
19921
  // Trim queue if exceeded MAX_EVENTS
@@ -20870,6 +21013,8 @@ let ReportStorage$2 = class ReportStorage {
20870
21013
  originalPriceStopLoss: data.originalPriceStopLoss,
20871
21014
  partialExecuted: data.partialExecuted,
20872
21015
  note: data.note,
21016
+ pendingAt: data.pendingAt,
21017
+ scheduledAt: data.scheduledAt,
20873
21018
  backtest,
20874
21019
  });
20875
21020
  // Trim queue if exceeded MAX_EVENTS
@@ -23255,9 +23400,15 @@ class HeatReportService {
23255
23400
  signalId: data.signal?.id,
23256
23401
  position: data.signal?.position,
23257
23402
  note: data.signal?.note,
23403
+ priceOpen: data.signal?.priceOpen,
23404
+ priceTakeProfit: data.signal?.priceTakeProfit,
23405
+ priceStopLoss: data.signal?.priceStopLoss,
23406
+ originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
23407
+ originalPriceStopLoss: data.signal?.originalPriceStopLoss,
23258
23408
  pnl: data.pnl.pnlPercentage,
23259
23409
  closeReason: data.closeReason,
23260
23410
  openTime: data.signal?.pendingAt,
23411
+ scheduledAt: data.signal?.scheduledAt,
23261
23412
  closeTime: data.closeTimestamp,
23262
23413
  }, {
23263
23414
  symbol: data.symbol,
@@ -23666,6 +23817,8 @@ class RiskReportService {
23666
23817
  originalPriceStopLoss: data.currentSignal?.originalPriceStopLoss,
23667
23818
  partialExecuted: data.currentSignal?.partialExecuted,
23668
23819
  note: data.currentSignal?.note,
23820
+ pendingAt: data.currentSignal?.pendingAt,
23821
+ scheduledAt: data.currentSignal?.scheduledAt,
23669
23822
  minuteEstimatedTime: data.currentSignal?.minuteEstimatedTime,
23670
23823
  }, {
23671
23824
  symbol: data.symbol,
@@ -25608,6 +25761,7 @@ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
25608
25761
  const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
25609
25762
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
25610
25763
  const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
25764
+ const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
25611
25765
  /**
25612
25766
  * Checks if trade context is active (execution and method contexts).
25613
25767
  *
@@ -25912,6 +26066,30 @@ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
25912
26066
  }
25913
26067
  return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
25914
26068
  }
26069
+ /**
26070
+ * Fetches the set of candles after current time based on execution context.
26071
+ *
26072
+ * Uses the exchange's getNextCandles implementation to retrieve candles
26073
+ * that occur after the current context time.
26074
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26075
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
26076
+ * @param limit - Number of candles to fetch
26077
+ * @returns Promise resolving to array of candle data
26078
+ */
26079
+ async function getNextCandles(symbol, interval, limit) {
26080
+ bt.loggerService.info(GET_NEXT_CANDLES_METHOD_NAME, {
26081
+ symbol,
26082
+ interval,
26083
+ limit,
26084
+ });
26085
+ if (!ExecutionContextService.hasContext()) {
26086
+ throw new Error("getNextCandles requires an execution context");
26087
+ }
26088
+ if (!MethodContextService.hasContext()) {
26089
+ throw new Error("getNextCandles requires a method context");
26090
+ }
26091
+ return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
26092
+ }
25915
26093
 
25916
26094
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
25917
26095
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
@@ -32845,6 +33023,16 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
32845
33023
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
32846
33024
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
32847
33025
  const MS_PER_MINUTE = 60000;
33026
+ /**
33027
+ * Gets current timestamp from execution context if available.
33028
+ * Returns current Date() if no execution context exists (non-trading GUI).
33029
+ */
33030
+ const GET_TIMESTAMP_FN = async () => {
33031
+ if (ExecutionContextService.hasContext()) {
33032
+ return new Date(bt.executionContextService.context.when);
33033
+ }
33034
+ return new Date();
33035
+ };
32848
33036
  /**
32849
33037
  * Gets backtest mode flag from execution context if available.
32850
33038
  * Returns false if no execution context exists (live mode).
@@ -32924,13 +33112,20 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
32924
33112
  * Attempts to read candles from cache.
32925
33113
  * Validates cache consistency (no gaps in timestamps) before returning.
32926
33114
  *
33115
+ * Boundary semantics:
33116
+ * - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
33117
+ * - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
33118
+ * - Only fully closed candles within the exclusive range are returned
33119
+ *
32927
33120
  * @param dto - Data transfer object containing symbol, interval, and limit
32928
- * @param sinceTimestamp - Start timestamp in milliseconds
32929
- * @param untilTimestamp - End timestamp in milliseconds
33121
+ * @param sinceTimestamp - Exclusive start timestamp in milliseconds
33122
+ * @param untilTimestamp - Exclusive end timestamp in milliseconds
32930
33123
  * @param exchangeName - Exchange name
32931
33124
  * @returns Cached candles array or null if cache miss or inconsistent
32932
33125
  */
32933
33126
  const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
33127
+ // PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
33128
+ // Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
32934
33129
  const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
32935
33130
  // Return cached data only if we have exactly the requested limit
32936
33131
  if (cachedCandles.length === dto.limit) {
@@ -32955,7 +33150,11 @@ const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestam
32955
33150
  /**
32956
33151
  * Writes candles to cache with error handling.
32957
33152
  *
32958
- * @param candles - Array of candle data to cache
33153
+ * The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
33154
+ * - candle.timestamp > sinceTimestamp
33155
+ * - candle.timestamp + stepMs < untilTimestamp
33156
+ *
33157
+ * @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
32959
33158
  * @param dto - Data transfer object containing symbol, interval, and limit
32960
33159
  * @param exchangeName - Exchange name
32961
33160
  */
@@ -33030,10 +33229,10 @@ class ExchangeInstance {
33030
33229
  if (!adjust) {
33031
33230
  throw new Error(`ExchangeInstance unknown time adjust for interval=${interval}`);
33032
33231
  }
33033
- const when = new Date(Date.now());
33034
- const since = new Date(when.getTime() - adjust * 60 * 1000);
33232
+ const when = await GET_TIMESTAMP_FN();
33233
+ const since = new Date(when.getTime() - adjust * MS_PER_MINUTE);
33035
33234
  const sinceTimestamp = since.getTime();
33036
- const untilTimestamp = sinceTimestamp + limit * step * 60 * 1000;
33235
+ const untilTimestamp = sinceTimestamp + limit * step * MS_PER_MINUTE;
33037
33236
  // Try to read from cache first
33038
33237
  const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
33039
33238
  if (cachedCandles !== null) {
@@ -33052,7 +33251,7 @@ class ExchangeInstance {
33052
33251
  remaining -= chunkLimit;
33053
33252
  if (remaining > 0) {
33054
33253
  // Move currentSince forward by the number of candles fetched
33055
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
33254
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
33056
33255
  }
33057
33256
  }
33058
33257
  }
@@ -33062,8 +33261,18 @@ class ExchangeInstance {
33062
33261
  }
33063
33262
  // Filter candles to strictly match the requested range
33064
33263
  const whenTimestamp = when.getTime();
33065
- const stepMs = step * 60 * 1000;
33066
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < whenTimestamp + stepMs);
33264
+ const stepMs = step * MS_PER_MINUTE;
33265
+ const filteredData = allData.filter((candle) => {
33266
+ // EXCLUSIVE boundaries:
33267
+ // - candle.timestamp > sinceTimestamp (exclude exact boundary)
33268
+ // - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
33269
+ if (candle.timestamp <= sinceTimestamp) {
33270
+ return false;
33271
+ }
33272
+ // Check against current time (when)
33273
+ // Only allow candles that have fully CLOSED before "when"
33274
+ return candle.timestamp + stepMs < whenTimestamp;
33275
+ });
33067
33276
  // Apply distinct by timestamp to remove duplicates
33068
33277
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
33069
33278
  if (filteredData.length !== uniqueData.length) {
@@ -33193,8 +33402,8 @@ class ExchangeInstance {
33193
33402
  symbol,
33194
33403
  depth,
33195
33404
  });
33196
- const to = new Date(Date.now());
33197
- const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
33405
+ const to = await GET_TIMESTAMP_FN();
33406
+ const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
33198
33407
  const isBacktest = await GET_BACKTEST_FN();
33199
33408
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
33200
33409
  };
@@ -33241,7 +33450,8 @@ class ExchangeInstance {
33241
33450
  if (!step) {
33242
33451
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
33243
33452
  }
33244
- const nowTimestamp = Date.now();
33453
+ const when = await GET_TIMESTAMP_FN();
33454
+ const nowTimestamp = when.getTime();
33245
33455
  let sinceTimestamp;
33246
33456
  let untilTimestamp;
33247
33457
  let calculatedLimit;
@@ -33329,8 +33539,10 @@ class ExchangeInstance {
33329
33539
  allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
33330
33540
  }
33331
33541
  // Filter candles to strictly match the requested range
33332
- const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
33333
- candle.timestamp < untilTimestamp);
33542
+ // Only include candles that have fully CLOSED before untilTimestamp
33543
+ const stepMs = step * MS_PER_MINUTE;
33544
+ const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
33545
+ candle.timestamp + stepMs < untilTimestamp);
33334
33546
  // Apply distinct by timestamp to remove duplicates
33335
33547
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
33336
33548
  if (filteredData.length !== uniqueData.length) {
@@ -33920,6 +34132,7 @@ class NotificationInstance {
33920
34132
  exchangeName: data.exchangeName,
33921
34133
  signalId: data.signal.id,
33922
34134
  position: data.signal.position,
34135
+ priceOpen: data.signal.priceOpen,
33923
34136
  priceTakeProfit: data.signal.priceTakeProfit,
33924
34137
  priceStopLoss: data.signal.priceStopLoss,
33925
34138
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
@@ -34932,4 +35145,4 @@ const set = (object, path, value) => {
34932
35145
  }
34933
35146
  };
34934
35147
 
34935
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, 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 };
35148
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "2.2.25",
3
+ "version": "2.3.1",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",