backtest-kit 2.1.2 → 2.1.3

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
@@ -1646,6 +1646,7 @@ class PersistCandleUtils {
1646
1646
  */
1647
1647
  const PersistCandleAdapter = new PersistCandleUtils();
1648
1648
 
1649
+ const MS_PER_MINUTE$1 = 60000;
1649
1650
  const INTERVAL_MINUTES$4 = {
1650
1651
  "1m": 1,
1651
1652
  "3m": 3,
@@ -1791,7 +1792,7 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
1791
1792
  const GET_CANDLES_FN = async (dto, since, self) => {
1792
1793
  const step = INTERVAL_MINUTES$4[dto.interval];
1793
1794
  const sinceTimestamp = since.getTime();
1794
- const untilTimestamp = sinceTimestamp + dto.limit * step * 60 * 1000;
1795
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$1;
1795
1796
  // Try to read from cache first
1796
1797
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
1797
1798
  if (cachedCandles !== null) {
@@ -1897,7 +1898,7 @@ class ClientExchange {
1897
1898
  if (!adjust) {
1898
1899
  throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
1899
1900
  }
1900
- const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
1901
+ const since = new Date(this.params.execution.context.when.getTime() - adjust * MS_PER_MINUTE$1);
1901
1902
  let allData = [];
1902
1903
  // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
1903
1904
  if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
@@ -1910,7 +1911,7 @@ class ClientExchange {
1910
1911
  remaining -= chunkLimit;
1911
1912
  if (remaining > 0) {
1912
1913
  // Move currentSince forward by the number of candles fetched
1913
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1914
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1914
1915
  }
1915
1916
  }
1916
1917
  }
@@ -1957,7 +1958,7 @@ class ClientExchange {
1957
1958
  const now = Date.now();
1958
1959
  // Вычисляем конечное время запроса
1959
1960
  const step = INTERVAL_MINUTES$4[interval];
1960
- const endTime = since.getTime() + limit * step * 60 * 1000;
1961
+ const endTime = since.getTime() + limit * step * MS_PER_MINUTE$1;
1961
1962
  // Проверяем что запрошенный период не заходит за Date.now()
1962
1963
  if (endTime > now) {
1963
1964
  return [];
@@ -1974,7 +1975,7 @@ class ClientExchange {
1974
1975
  remaining -= chunkLimit;
1975
1976
  if (remaining > 0) {
1976
1977
  // Move currentSince forward by the number of candles fetched
1977
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1978
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1978
1979
  }
1979
1980
  }
1980
1981
  }
@@ -2069,22 +2070,19 @@ class ClientExchange {
2069
2070
  /**
2070
2071
  * Fetches raw candles with flexible date/limit parameters.
2071
2072
  *
2072
- * Compatibility layer that:
2073
- * - RAW MODE (sDate + eDate + limit): fetches exactly as specified, NO look-ahead bias protection
2074
- * - Other modes: respects execution context and prevents look-ahead bias
2073
+ * All modes respect execution context and prevent look-ahead bias.
2075
2074
  *
2076
2075
  * Parameter combinations:
2077
- * 1. sDate + eDate + limit: RAW MODE - fetches exactly as specified, no validation against when
2078
- * 2. sDate + eDate: calculates limit from date range, validates endTimestamp <= when
2079
- * 3. eDate + limit: calculates sDate backward, validates endTimestamp <= when
2080
- * 4. sDate + limit: fetches forward, validates endTimestamp <= when
2076
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
2077
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
2078
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
2079
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
2081
2080
  * 5. Only limit: uses execution.context.when as reference (backward)
2082
2081
  *
2083
2082
  * Edge cases:
2084
2083
  * - If calculated limit is 0 or negative: throws error
2085
2084
  * - If sDate >= eDate: throws error
2086
- * - If startTimestamp >= endTimestamp: throws error
2087
- * - If endTimestamp > when (non-RAW modes only): throws error to prevent look-ahead bias
2085
+ * - If eDate > when: throws error to prevent look-ahead bias
2088
2086
  *
2089
2087
  * @param symbol - Trading pair symbol
2090
2088
  * @param interval - Candle interval
@@ -2103,73 +2101,75 @@ class ClientExchange {
2103
2101
  eDate,
2104
2102
  });
2105
2103
  const step = INTERVAL_MINUTES$4[interval];
2106
- const stepMs = step * 60 * 1000;
2107
- const when = this.params.execution.context.when.getTime();
2108
- let startTimestamp;
2109
- let endTimestamp;
2110
- let candleLimit;
2111
- let isRawMode = false;
2112
- // Case 1: sDate + eDate + limit - RAW MODE (no look-ahead bias protection)
2113
- if (sDate !== undefined &&
2114
- eDate !== undefined &&
2115
- limit !== undefined) {
2116
- isRawMode = true;
2104
+ if (!step) {
2105
+ throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2106
+ }
2107
+ const whenTimestamp = this.params.execution.context.when.getTime();
2108
+ let sinceTimestamp;
2109
+ let untilTimestamp;
2110
+ let calculatedLimit;
2111
+ // Case 1: all three parameters provided
2112
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
2117
2113
  if (sDate >= eDate) {
2118
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2114
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2115
+ }
2116
+ if (eDate > whenTimestamp) {
2117
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2119
2118
  }
2120
- startTimestamp = sDate;
2121
- endTimestamp = eDate;
2122
- candleLimit = limit;
2119
+ sinceTimestamp = sDate;
2120
+ untilTimestamp = eDate;
2121
+ calculatedLimit = limit;
2123
2122
  }
2124
- // Case 2: sDate + eDate - calculate limit, respect backtest context
2123
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
2125
2124
  else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
2126
2125
  if (sDate >= eDate) {
2127
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2128
- }
2129
- startTimestamp = sDate;
2130
- endTimestamp = eDate;
2131
- const rangeDuration = endTimestamp - startTimestamp;
2132
- candleLimit = Math.floor(rangeDuration / stepMs);
2133
- if (candleLimit <= 0) {
2134
- throw new Error(`ClientExchange getRawCandles: calculated limit is ${candleLimit} for range [${sDate}, ${eDate}]`);
2135
- }
2136
- }
2137
- // Case 3: eDate + limit - calculate sDate backward, respect backtest context
2138
- else if (eDate !== undefined && limit !== undefined && sDate === undefined) {
2139
- endTimestamp = eDate;
2140
- startTimestamp = eDate - limit * stepMs;
2141
- candleLimit = limit;
2142
- }
2143
- // Case 4: sDate + limit - fetch forward, respect backtest context
2144
- else if (sDate !== undefined && limit !== undefined && eDate === undefined) {
2145
- startTimestamp = sDate;
2146
- endTimestamp = sDate + limit * stepMs;
2147
- candleLimit = limit;
2148
- }
2149
- // Case 5: Only limit - use execution context (backward from when)
2150
- else if (limit !== undefined && sDate === undefined && eDate === undefined) {
2151
- endTimestamp = when;
2152
- startTimestamp = when - limit * stepMs;
2153
- candleLimit = limit;
2154
- }
2155
- // Invalid combination
2156
- else {
2157
- throw new Error(`ClientExchange getRawCandles: invalid parameter combination. Must provide either (limit), (eDate+limit), (sDate+limit), (sDate+eDate), or (sDate+eDate+limit)`);
2126
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2127
+ }
2128
+ if (eDate > whenTimestamp) {
2129
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2130
+ }
2131
+ sinceTimestamp = sDate;
2132
+ untilTimestamp = eDate;
2133
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE$1));
2134
+ if (calculatedLimit <= 0) {
2135
+ throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
2136
+ }
2137
+ }
2138
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
2139
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
2140
+ if (eDate > whenTimestamp) {
2141
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2142
+ }
2143
+ untilTimestamp = eDate;
2144
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE$1;
2145
+ calculatedLimit = limit;
2158
2146
  }
2159
- // Validate timestamps
2160
- if (startTimestamp >= endTimestamp) {
2161
- throw new Error(`ClientExchange getRawCandles: startTimestamp (${startTimestamp}) >= endTimestamp (${endTimestamp})`);
2147
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
2148
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
2149
+ sinceTimestamp = sDate;
2150
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE$1;
2151
+ if (untilTimestamp > whenTimestamp) {
2152
+ throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2153
+ }
2154
+ calculatedLimit = limit;
2155
+ }
2156
+ // Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
2157
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
2158
+ untilTimestamp = whenTimestamp;
2159
+ sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
2160
+ calculatedLimit = limit;
2162
2161
  }
2163
- // Check if trying to fetch future data (prevent look-ahead bias)
2164
- // ONLY for non-RAW modes - RAW MODE bypasses this check
2165
- if (!isRawMode && endTimestamp > when) {
2166
- throw new Error(`ClientExchange getRawCandles: endTimestamp (${endTimestamp}) is beyond execution context when (${when}) - look-ahead bias prevented`);
2162
+ // Invalid: no parameters or only sDate or only eDate
2163
+ else {
2164
+ throw new Error(`ClientExchange getRawCandles: invalid parameter combination. ` +
2165
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
2166
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
2167
2167
  }
2168
- const since = new Date(startTimestamp);
2168
+ // Fetch candles using existing logic
2169
+ const since = new Date(sinceTimestamp);
2169
2170
  let allData = [];
2170
- // Fetch data in chunks if limit exceeds max per request
2171
- if (candleLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2172
- let remaining = candleLimit;
2171
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2172
+ let remaining = calculatedLimit;
2173
2173
  let currentSince = new Date(since.getTime());
2174
2174
  while (remaining > 0) {
2175
2175
  const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
@@ -2177,16 +2177,16 @@ class ClientExchange {
2177
2177
  allData.push(...chunkData);
2178
2178
  remaining -= chunkLimit;
2179
2179
  if (remaining > 0) {
2180
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
2180
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
2181
2181
  }
2182
2182
  }
2183
2183
  }
2184
2184
  else {
2185
- allData = await GET_CANDLES_FN({ symbol, interval, limit: candleLimit }, since, this);
2185
+ allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
2186
2186
  }
2187
2187
  // Filter candles to strictly match the requested range
2188
- const filteredData = allData.filter((candle) => candle.timestamp >= startTimestamp &&
2189
- candle.timestamp < endTimestamp);
2188
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2189
+ candle.timestamp < untilTimestamp);
2190
2190
  // Apply distinct by timestamp to remove duplicates
2191
2191
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2192
2192
  if (filteredData.length !== uniqueData.length) {
@@ -2194,12 +2194,12 @@ class ClientExchange {
2194
2194
  this.params.logger.warn(msg);
2195
2195
  console.warn(msg);
2196
2196
  }
2197
- if (uniqueData.length < candleLimit) {
2198
- const msg = `ClientExchange getRawCandles: Expected ${candleLimit} candles, got ${uniqueData.length}`;
2197
+ if (uniqueData.length < calculatedLimit) {
2198
+ const msg = `ClientExchange getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`;
2199
2199
  this.params.logger.warn(msg);
2200
2200
  console.warn(msg);
2201
2201
  }
2202
- await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, candleLimit, uniqueData);
2202
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
2203
2203
  return uniqueData;
2204
2204
  }
2205
2205
  /**
@@ -2221,7 +2221,7 @@ class ClientExchange {
2221
2221
  });
2222
2222
  const to = new Date(this.params.execution.context.when.getTime());
2223
2223
  const from = new Date(to.getTime() -
2224
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
2224
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
2225
2225
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2226
2226
  }
2227
2227
  }
@@ -2412,6 +2412,28 @@ class ExchangeConnectionService {
2412
2412
  });
2413
2413
  return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
2414
2414
  };
2415
+ /**
2416
+ * Fetches raw candles with flexible date/limit parameters.
2417
+ *
2418
+ * Routes to exchange determined by methodContextService.context.exchangeName.
2419
+ *
2420
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2421
+ * @param interval - Candle interval (e.g., "1h", "1d")
2422
+ * @param limit - Optional number of candles to fetch
2423
+ * @param sDate - Optional start date in milliseconds
2424
+ * @param eDate - Optional end date in milliseconds
2425
+ * @returns Promise resolving to array of candle data
2426
+ */
2427
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
2428
+ this.loggerService.log("exchangeConnectionService getRawCandles", {
2429
+ symbol,
2430
+ interval,
2431
+ limit,
2432
+ sDate,
2433
+ eDate,
2434
+ });
2435
+ return await this.getExchange(this.methodContextService.context.exchangeName).getRawCandles(symbol, interval, limit, sDate, eDate);
2436
+ };
2415
2437
  }
2416
2438
  }
2417
2439
 
@@ -9955,6 +9977,40 @@ class ExchangeCoreService {
9955
9977
  backtest,
9956
9978
  });
9957
9979
  };
9980
+ /**
9981
+ * Fetches raw candles with flexible date/limit parameters and execution context.
9982
+ *
9983
+ * @param symbol - Trading pair symbol
9984
+ * @param interval - Candle interval (e.g., "1m", "1h")
9985
+ * @param when - Timestamp for context (used in backtest mode)
9986
+ * @param backtest - Whether running in backtest mode
9987
+ * @param limit - Optional number of candles to fetch
9988
+ * @param sDate - Optional start date in milliseconds
9989
+ * @param eDate - Optional end date in milliseconds
9990
+ * @returns Promise resolving to array of candles
9991
+ */
9992
+ this.getRawCandles = async (symbol, interval, when, backtest, limit, sDate, eDate) => {
9993
+ this.loggerService.log("exchangeCoreService getRawCandles", {
9994
+ symbol,
9995
+ interval,
9996
+ when,
9997
+ backtest,
9998
+ limit,
9999
+ sDate,
10000
+ eDate,
10001
+ });
10002
+ if (!MethodContextService.hasContext()) {
10003
+ throw new Error("exchangeCoreService getRawCandles requires a method context");
10004
+ }
10005
+ await this.validate(this.methodContextService.context.exchangeName);
10006
+ return await ExecutionContextService.runInContext(async () => {
10007
+ return await this.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
10008
+ }, {
10009
+ symbol,
10010
+ when,
10011
+ backtest,
10012
+ });
10013
+ };
9958
10014
  }
9959
10015
  }
9960
10016
 
@@ -25258,6 +25314,7 @@ const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
25258
25314
  const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
25259
25315
  const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
25260
25316
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
25317
+ const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
25261
25318
  /**
25262
25319
  * Checks if trade context is active (execution and method contexts).
25263
25320
  *
@@ -25515,6 +25572,53 @@ async function getOrderBook(symbol, depth) {
25515
25572
  }
25516
25573
  return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
25517
25574
  }
25575
+ /**
25576
+ * Fetches raw candles with flexible date/limit parameters.
25577
+ *
25578
+ * All modes respect execution context and prevent look-ahead bias.
25579
+ *
25580
+ * Parameter combinations:
25581
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
25582
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
25583
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
25584
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
25585
+ * 5. Only limit: uses execution.context.when as reference (backward)
25586
+ *
25587
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
25588
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
25589
+ * @param limit - Optional number of candles to fetch
25590
+ * @param sDate - Optional start date in milliseconds
25591
+ * @param eDate - Optional end date in milliseconds
25592
+ * @returns Promise resolving to array of candle data
25593
+ *
25594
+ * @example
25595
+ * ```typescript
25596
+ * // Fetch 100 candles backward from current context time
25597
+ * const candles = await getRawCandles("BTCUSDT", "1m", 100);
25598
+ *
25599
+ * // Fetch candles for specific date range
25600
+ * const rangeCandles = await getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
25601
+ *
25602
+ * // Fetch with all parameters specified
25603
+ * const exactCandles = await getRawCandles("BTCUSDT", "1m", 100, startMs, endMs);
25604
+ * ```
25605
+ */
25606
+ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
25607
+ bt.loggerService.info(GET_RAW_CANDLES_METHOD_NAME, {
25608
+ symbol,
25609
+ interval,
25610
+ limit,
25611
+ sDate,
25612
+ eDate,
25613
+ });
25614
+ if (!ExecutionContextService.hasContext()) {
25615
+ throw new Error("getRawCandles requires an execution context");
25616
+ }
25617
+ if (!MethodContextService.hasContext()) {
25618
+ throw new Error("getRawCandles requires a method context");
25619
+ }
25620
+ return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
25621
+ }
25518
25622
 
25519
25623
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
25520
25624
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
@@ -31786,6 +31890,8 @@ const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
31786
31890
  const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
31787
31891
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
31788
31892
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
31893
+ const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
31894
+ const MS_PER_MINUTE = 60000;
31789
31895
  /**
31790
31896
  * Gets backtest mode flag from execution context if available.
31791
31897
  * Returns false if no execution context exists (live mode).
@@ -32139,6 +32245,151 @@ class ExchangeInstance {
32139
32245
  const isBacktest = await GET_BACKTEST_FN();
32140
32246
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
32141
32247
  };
32248
+ /**
32249
+ * Fetches raw candles with flexible date/limit parameters.
32250
+ *
32251
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
32252
+ *
32253
+ * Parameter combinations:
32254
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
32255
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= now
32256
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= now
32257
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
32258
+ * 5. Only limit: uses Date.now() as reference (backward)
32259
+ *
32260
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
32261
+ * @param interval - Candle interval (e.g., "1m", "1h")
32262
+ * @param limit - Optional number of candles to fetch
32263
+ * @param sDate - Optional start date in milliseconds
32264
+ * @param eDate - Optional end date in milliseconds
32265
+ * @returns Promise resolving to array of candle data
32266
+ *
32267
+ * @example
32268
+ * ```typescript
32269
+ * const instance = new ExchangeInstance("binance");
32270
+ *
32271
+ * // Fetch 100 candles backward from now
32272
+ * const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
32273
+ *
32274
+ * // Fetch candles for specific date range
32275
+ * const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
32276
+ * ```
32277
+ */
32278
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
32279
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
32280
+ exchangeName: this.exchangeName,
32281
+ symbol,
32282
+ interval,
32283
+ limit,
32284
+ sDate,
32285
+ eDate,
32286
+ });
32287
+ const step = INTERVAL_MINUTES$1[interval];
32288
+ if (!step) {
32289
+ throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
32290
+ }
32291
+ const nowTimestamp = Date.now();
32292
+ let sinceTimestamp;
32293
+ let untilTimestamp;
32294
+ let calculatedLimit;
32295
+ // Case 1: all three parameters provided
32296
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
32297
+ if (sDate >= eDate) {
32298
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
32299
+ }
32300
+ if (eDate > nowTimestamp) {
32301
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32302
+ }
32303
+ sinceTimestamp = sDate;
32304
+ untilTimestamp = eDate;
32305
+ calculatedLimit = limit;
32306
+ }
32307
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
32308
+ else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
32309
+ if (sDate >= eDate) {
32310
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
32311
+ }
32312
+ if (eDate > nowTimestamp) {
32313
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32314
+ }
32315
+ sinceTimestamp = sDate;
32316
+ untilTimestamp = eDate;
32317
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE));
32318
+ if (calculatedLimit <= 0) {
32319
+ throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
32320
+ }
32321
+ }
32322
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
32323
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
32324
+ if (eDate > nowTimestamp) {
32325
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32326
+ }
32327
+ untilTimestamp = eDate;
32328
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE;
32329
+ calculatedLimit = limit;
32330
+ }
32331
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
32332
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
32333
+ sinceTimestamp = sDate;
32334
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE;
32335
+ if (untilTimestamp > nowTimestamp) {
32336
+ throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32337
+ }
32338
+ calculatedLimit = limit;
32339
+ }
32340
+ // Case 5: Only limit - use Date.now() as reference (backward)
32341
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
32342
+ untilTimestamp = nowTimestamp;
32343
+ sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
32344
+ calculatedLimit = limit;
32345
+ }
32346
+ // Invalid: no parameters or only sDate or only eDate
32347
+ else {
32348
+ throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
32349
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
32350
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
32351
+ }
32352
+ // Try to read from cache first
32353
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
32354
+ if (cachedCandles !== null) {
32355
+ return cachedCandles;
32356
+ }
32357
+ // Fetch candles
32358
+ const since = new Date(sinceTimestamp);
32359
+ let allData = [];
32360
+ const isBacktest = await GET_BACKTEST_FN();
32361
+ const getCandles = this._methods.getCandles;
32362
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
32363
+ let remaining = calculatedLimit;
32364
+ let currentSince = new Date(since.getTime());
32365
+ while (remaining > 0) {
32366
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
32367
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
32368
+ allData.push(...chunkData);
32369
+ remaining -= chunkLimit;
32370
+ if (remaining > 0) {
32371
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
32372
+ }
32373
+ }
32374
+ }
32375
+ else {
32376
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
32377
+ }
32378
+ // Filter candles to strictly match the requested range
32379
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
32380
+ candle.timestamp < untilTimestamp);
32381
+ // Apply distinct by timestamp to remove duplicates
32382
+ const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
32383
+ if (filteredData.length !== uniqueData.length) {
32384
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`);
32385
+ }
32386
+ if (uniqueData.length < calculatedLimit) {
32387
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`);
32388
+ }
32389
+ // Write to cache after successful fetch
32390
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
32391
+ return uniqueData;
32392
+ };
32142
32393
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
32143
32394
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
32144
32395
  }
@@ -32243,6 +32494,24 @@ class ExchangeUtils {
32243
32494
  const instance = this._getInstance(context.exchangeName);
32244
32495
  return await instance.getOrderBook(symbol, depth);
32245
32496
  };
32497
+ /**
32498
+ * Fetches raw candles with flexible date/limit parameters.
32499
+ *
32500
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
32501
+ *
32502
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
32503
+ * @param interval - Candle interval (e.g., "1m", "1h")
32504
+ * @param context - Execution context with exchange name
32505
+ * @param limit - Optional number of candles to fetch
32506
+ * @param sDate - Optional start date in milliseconds
32507
+ * @param eDate - Optional end date in milliseconds
32508
+ * @returns Promise resolving to array of candle data
32509
+ */
32510
+ this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
32511
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
32512
+ const instance = this._getInstance(context.exchangeName);
32513
+ return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
32514
+ };
32246
32515
  }
32247
32516
  }
32248
32517
  /**
@@ -33374,4 +33643,4 @@ const set = (object, path, value) => {
33374
33643
  }
33375
33644
  };
33376
33645
 
33377
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Optimizer, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addOptimizerSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitSignalPromptHistory, commitTrailingStop, commitTrailingTake, dumpSignalData, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getOptimizerSchema, getOrderBook, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listOptimizerSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideOptimizerSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
33646
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Optimizer, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addOptimizerSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitSignalPromptHistory, commitTrailingStop, commitTrailingTake, dumpSignalData, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getOptimizerSchema, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listOptimizerSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenOptimizerProgress, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideOptimizerSchema, 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.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",