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.cjs CHANGED
@@ -1667,6 +1667,7 @@ class PersistCandleUtils {
1667
1667
  */
1668
1668
  const PersistCandleAdapter = new PersistCandleUtils();
1669
1669
 
1670
+ const MS_PER_MINUTE$1 = 60000;
1670
1671
  const INTERVAL_MINUTES$4 = {
1671
1672
  "1m": 1,
1672
1673
  "3m": 3,
@@ -1812,7 +1813,7 @@ const WRITE_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(functoolsKit.queued(async
1812
1813
  const GET_CANDLES_FN = async (dto, since, self) => {
1813
1814
  const step = INTERVAL_MINUTES$4[dto.interval];
1814
1815
  const sinceTimestamp = since.getTime();
1815
- const untilTimestamp = sinceTimestamp + dto.limit * step * 60 * 1000;
1816
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$1;
1816
1817
  // Try to read from cache first
1817
1818
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
1818
1819
  if (cachedCandles !== null) {
@@ -1918,7 +1919,7 @@ class ClientExchange {
1918
1919
  if (!adjust) {
1919
1920
  throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
1920
1921
  }
1921
- const since = new Date(this.params.execution.context.when.getTime() - adjust * 60 * 1000);
1922
+ const since = new Date(this.params.execution.context.when.getTime() - adjust * MS_PER_MINUTE$1);
1922
1923
  let allData = [];
1923
1924
  // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
1924
1925
  if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
@@ -1931,7 +1932,7 @@ class ClientExchange {
1931
1932
  remaining -= chunkLimit;
1932
1933
  if (remaining > 0) {
1933
1934
  // Move currentSince forward by the number of candles fetched
1934
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1935
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1935
1936
  }
1936
1937
  }
1937
1938
  }
@@ -1978,7 +1979,7 @@ class ClientExchange {
1978
1979
  const now = Date.now();
1979
1980
  // Вычисляем конечное время запроса
1980
1981
  const step = INTERVAL_MINUTES$4[interval];
1981
- const endTime = since.getTime() + limit * step * 60 * 1000;
1982
+ const endTime = since.getTime() + limit * step * MS_PER_MINUTE$1;
1982
1983
  // Проверяем что запрошенный период не заходит за Date.now()
1983
1984
  if (endTime > now) {
1984
1985
  return [];
@@ -1995,7 +1996,7 @@ class ClientExchange {
1995
1996
  remaining -= chunkLimit;
1996
1997
  if (remaining > 0) {
1997
1998
  // Move currentSince forward by the number of candles fetched
1998
- currentSince = new Date(currentSince.getTime() + chunkLimit * step * 60 * 1000);
1999
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
1999
2000
  }
2000
2001
  }
2001
2002
  }
@@ -2090,22 +2091,19 @@ class ClientExchange {
2090
2091
  /**
2091
2092
  * Fetches raw candles with flexible date/limit parameters.
2092
2093
  *
2093
- * Compatibility layer that:
2094
- * - RAW MODE (sDate + eDate + limit): fetches exactly as specified, NO look-ahead bias protection
2095
- * - Other modes: respects execution context and prevents look-ahead bias
2094
+ * All modes respect execution context and prevent look-ahead bias.
2096
2095
  *
2097
2096
  * Parameter combinations:
2098
- * 1. sDate + eDate + limit: RAW MODE - fetches exactly as specified, no validation against when
2099
- * 2. sDate + eDate: calculates limit from date range, validates endTimestamp <= when
2100
- * 3. eDate + limit: calculates sDate backward, validates endTimestamp <= when
2101
- * 4. sDate + limit: fetches forward, validates endTimestamp <= when
2097
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
2098
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
2099
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
2100
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
2102
2101
  * 5. Only limit: uses execution.context.when as reference (backward)
2103
2102
  *
2104
2103
  * Edge cases:
2105
2104
  * - If calculated limit is 0 or negative: throws error
2106
2105
  * - If sDate >= eDate: throws error
2107
- * - If startTimestamp >= endTimestamp: throws error
2108
- * - If endTimestamp > when (non-RAW modes only): throws error to prevent look-ahead bias
2106
+ * - If eDate > when: throws error to prevent look-ahead bias
2109
2107
  *
2110
2108
  * @param symbol - Trading pair symbol
2111
2109
  * @param interval - Candle interval
@@ -2124,73 +2122,75 @@ class ClientExchange {
2124
2122
  eDate,
2125
2123
  });
2126
2124
  const step = INTERVAL_MINUTES$4[interval];
2127
- const stepMs = step * 60 * 1000;
2128
- const when = this.params.execution.context.when.getTime();
2129
- let startTimestamp;
2130
- let endTimestamp;
2131
- let candleLimit;
2132
- let isRawMode = false;
2133
- // Case 1: sDate + eDate + limit - RAW MODE (no look-ahead bias protection)
2134
- if (sDate !== undefined &&
2135
- eDate !== undefined &&
2136
- limit !== undefined) {
2137
- isRawMode = true;
2125
+ if (!step) {
2126
+ throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2127
+ }
2128
+ const whenTimestamp = this.params.execution.context.when.getTime();
2129
+ let sinceTimestamp;
2130
+ let untilTimestamp;
2131
+ let calculatedLimit;
2132
+ // Case 1: all three parameters provided
2133
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
2138
2134
  if (sDate >= eDate) {
2139
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2135
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2136
+ }
2137
+ if (eDate > whenTimestamp) {
2138
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2140
2139
  }
2141
- startTimestamp = sDate;
2142
- endTimestamp = eDate;
2143
- candleLimit = limit;
2140
+ sinceTimestamp = sDate;
2141
+ untilTimestamp = eDate;
2142
+ calculatedLimit = limit;
2144
2143
  }
2145
- // Case 2: sDate + eDate - calculate limit, respect backtest context
2144
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
2146
2145
  else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
2147
2146
  if (sDate >= eDate) {
2148
- throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be less than eDate (${eDate})`);
2149
- }
2150
- startTimestamp = sDate;
2151
- endTimestamp = eDate;
2152
- const rangeDuration = endTimestamp - startTimestamp;
2153
- candleLimit = Math.floor(rangeDuration / stepMs);
2154
- if (candleLimit <= 0) {
2155
- throw new Error(`ClientExchange getRawCandles: calculated limit is ${candleLimit} for range [${sDate}, ${eDate}]`);
2156
- }
2157
- }
2158
- // Case 3: eDate + limit - calculate sDate backward, respect backtest context
2159
- else if (eDate !== undefined && limit !== undefined && sDate === undefined) {
2160
- endTimestamp = eDate;
2161
- startTimestamp = eDate - limit * stepMs;
2162
- candleLimit = limit;
2163
- }
2164
- // Case 4: sDate + limit - fetch forward, respect backtest context
2165
- else if (sDate !== undefined && limit !== undefined && eDate === undefined) {
2166
- startTimestamp = sDate;
2167
- endTimestamp = sDate + limit * stepMs;
2168
- candleLimit = limit;
2169
- }
2170
- // Case 5: Only limit - use execution context (backward from when)
2171
- else if (limit !== undefined && sDate === undefined && eDate === undefined) {
2172
- endTimestamp = when;
2173
- startTimestamp = when - limit * stepMs;
2174
- candleLimit = limit;
2175
- }
2176
- // Invalid combination
2177
- else {
2178
- throw new Error(`ClientExchange getRawCandles: invalid parameter combination. Must provide either (limit), (eDate+limit), (sDate+limit), (sDate+eDate), or (sDate+eDate+limit)`);
2147
+ throw new Error(`ClientExchange getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
2148
+ }
2149
+ if (eDate > whenTimestamp) {
2150
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2151
+ }
2152
+ sinceTimestamp = sDate;
2153
+ untilTimestamp = eDate;
2154
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE$1));
2155
+ if (calculatedLimit <= 0) {
2156
+ throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
2157
+ }
2158
+ }
2159
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
2160
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
2161
+ if (eDate > whenTimestamp) {
2162
+ throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2163
+ }
2164
+ untilTimestamp = eDate;
2165
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE$1;
2166
+ calculatedLimit = limit;
2179
2167
  }
2180
- // Validate timestamps
2181
- if (startTimestamp >= endTimestamp) {
2182
- throw new Error(`ClientExchange getRawCandles: startTimestamp (${startTimestamp}) >= endTimestamp (${endTimestamp})`);
2168
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
2169
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
2170
+ sinceTimestamp = sDate;
2171
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE$1;
2172
+ if (untilTimestamp > whenTimestamp) {
2173
+ throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2174
+ }
2175
+ calculatedLimit = limit;
2176
+ }
2177
+ // Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
2178
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
2179
+ untilTimestamp = whenTimestamp;
2180
+ sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
2181
+ calculatedLimit = limit;
2183
2182
  }
2184
- // Check if trying to fetch future data (prevent look-ahead bias)
2185
- // ONLY for non-RAW modes - RAW MODE bypasses this check
2186
- if (!isRawMode && endTimestamp > when) {
2187
- throw new Error(`ClientExchange getRawCandles: endTimestamp (${endTimestamp}) is beyond execution context when (${when}) - look-ahead bias prevented`);
2183
+ // Invalid: no parameters or only sDate or only eDate
2184
+ else {
2185
+ throw new Error(`ClientExchange getRawCandles: invalid parameter combination. ` +
2186
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
2187
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
2188
2188
  }
2189
- const since = new Date(startTimestamp);
2189
+ // Fetch candles using existing logic
2190
+ const since = new Date(sinceTimestamp);
2190
2191
  let allData = [];
2191
- // Fetch data in chunks if limit exceeds max per request
2192
- if (candleLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2193
- let remaining = candleLimit;
2192
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
2193
+ let remaining = calculatedLimit;
2194
2194
  let currentSince = new Date(since.getTime());
2195
2195
  while (remaining > 0) {
2196
2196
  const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
@@ -2198,16 +2198,16 @@ class ClientExchange {
2198
2198
  allData.push(...chunkData);
2199
2199
  remaining -= chunkLimit;
2200
2200
  if (remaining > 0) {
2201
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
2201
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE$1);
2202
2202
  }
2203
2203
  }
2204
2204
  }
2205
2205
  else {
2206
- allData = await GET_CANDLES_FN({ symbol, interval, limit: candleLimit }, since, this);
2206
+ allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
2207
2207
  }
2208
2208
  // Filter candles to strictly match the requested range
2209
- const filteredData = allData.filter((candle) => candle.timestamp >= startTimestamp &&
2210
- candle.timestamp < endTimestamp);
2209
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
2210
+ candle.timestamp < untilTimestamp);
2211
2211
  // Apply distinct by timestamp to remove duplicates
2212
2212
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
2213
2213
  if (filteredData.length !== uniqueData.length) {
@@ -2215,12 +2215,12 @@ class ClientExchange {
2215
2215
  this.params.logger.warn(msg);
2216
2216
  console.warn(msg);
2217
2217
  }
2218
- if (uniqueData.length < candleLimit) {
2219
- const msg = `ClientExchange getRawCandles: Expected ${candleLimit} candles, got ${uniqueData.length}`;
2218
+ if (uniqueData.length < calculatedLimit) {
2219
+ const msg = `ClientExchange getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`;
2220
2220
  this.params.logger.warn(msg);
2221
2221
  console.warn(msg);
2222
2222
  }
2223
- await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, candleLimit, uniqueData);
2223
+ await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
2224
2224
  return uniqueData;
2225
2225
  }
2226
2226
  /**
@@ -2242,7 +2242,7 @@ class ClientExchange {
2242
2242
  });
2243
2243
  const to = new Date(this.params.execution.context.when.getTime());
2244
2244
  const from = new Date(to.getTime() -
2245
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
2245
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
2246
2246
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2247
2247
  }
2248
2248
  }
@@ -2433,6 +2433,28 @@ class ExchangeConnectionService {
2433
2433
  });
2434
2434
  return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
2435
2435
  };
2436
+ /**
2437
+ * Fetches raw candles with flexible date/limit parameters.
2438
+ *
2439
+ * Routes to exchange determined by methodContextService.context.exchangeName.
2440
+ *
2441
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2442
+ * @param interval - Candle interval (e.g., "1h", "1d")
2443
+ * @param limit - Optional number of candles to fetch
2444
+ * @param sDate - Optional start date in milliseconds
2445
+ * @param eDate - Optional end date in milliseconds
2446
+ * @returns Promise resolving to array of candle data
2447
+ */
2448
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
2449
+ this.loggerService.log("exchangeConnectionService getRawCandles", {
2450
+ symbol,
2451
+ interval,
2452
+ limit,
2453
+ sDate,
2454
+ eDate,
2455
+ });
2456
+ return await this.getExchange(this.methodContextService.context.exchangeName).getRawCandles(symbol, interval, limit, sDate, eDate);
2457
+ };
2436
2458
  }
2437
2459
  }
2438
2460
 
@@ -9976,6 +9998,40 @@ class ExchangeCoreService {
9976
9998
  backtest,
9977
9999
  });
9978
10000
  };
10001
+ /**
10002
+ * Fetches raw candles with flexible date/limit parameters and execution context.
10003
+ *
10004
+ * @param symbol - Trading pair symbol
10005
+ * @param interval - Candle interval (e.g., "1m", "1h")
10006
+ * @param when - Timestamp for context (used in backtest mode)
10007
+ * @param backtest - Whether running in backtest mode
10008
+ * @param limit - Optional number of candles to fetch
10009
+ * @param sDate - Optional start date in milliseconds
10010
+ * @param eDate - Optional end date in milliseconds
10011
+ * @returns Promise resolving to array of candles
10012
+ */
10013
+ this.getRawCandles = async (symbol, interval, when, backtest, limit, sDate, eDate) => {
10014
+ this.loggerService.log("exchangeCoreService getRawCandles", {
10015
+ symbol,
10016
+ interval,
10017
+ when,
10018
+ backtest,
10019
+ limit,
10020
+ sDate,
10021
+ eDate,
10022
+ });
10023
+ if (!MethodContextService.hasContext()) {
10024
+ throw new Error("exchangeCoreService getRawCandles requires a method context");
10025
+ }
10026
+ await this.validate(this.methodContextService.context.exchangeName);
10027
+ return await ExecutionContextService.runInContext(async () => {
10028
+ return await this.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
10029
+ }, {
10030
+ symbol,
10031
+ when,
10032
+ backtest,
10033
+ });
10034
+ };
9979
10035
  }
9980
10036
  }
9981
10037
 
@@ -25279,6 +25335,7 @@ const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
25279
25335
  const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
25280
25336
  const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
25281
25337
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
25338
+ const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
25282
25339
  /**
25283
25340
  * Checks if trade context is active (execution and method contexts).
25284
25341
  *
@@ -25536,6 +25593,53 @@ async function getOrderBook(symbol, depth) {
25536
25593
  }
25537
25594
  return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
25538
25595
  }
25596
+ /**
25597
+ * Fetches raw candles with flexible date/limit parameters.
25598
+ *
25599
+ * All modes respect execution context and prevent look-ahead bias.
25600
+ *
25601
+ * Parameter combinations:
25602
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= when
25603
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= when
25604
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= when
25605
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= when
25606
+ * 5. Only limit: uses execution.context.when as reference (backward)
25607
+ *
25608
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
25609
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
25610
+ * @param limit - Optional number of candles to fetch
25611
+ * @param sDate - Optional start date in milliseconds
25612
+ * @param eDate - Optional end date in milliseconds
25613
+ * @returns Promise resolving to array of candle data
25614
+ *
25615
+ * @example
25616
+ * ```typescript
25617
+ * // Fetch 100 candles backward from current context time
25618
+ * const candles = await getRawCandles("BTCUSDT", "1m", 100);
25619
+ *
25620
+ * // Fetch candles for specific date range
25621
+ * const rangeCandles = await getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
25622
+ *
25623
+ * // Fetch with all parameters specified
25624
+ * const exactCandles = await getRawCandles("BTCUSDT", "1m", 100, startMs, endMs);
25625
+ * ```
25626
+ */
25627
+ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
25628
+ bt.loggerService.info(GET_RAW_CANDLES_METHOD_NAME, {
25629
+ symbol,
25630
+ interval,
25631
+ limit,
25632
+ sDate,
25633
+ eDate,
25634
+ });
25635
+ if (!ExecutionContextService.hasContext()) {
25636
+ throw new Error("getRawCandles requires an execution context");
25637
+ }
25638
+ if (!MethodContextService.hasContext()) {
25639
+ throw new Error("getRawCandles requires a method context");
25640
+ }
25641
+ return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
25642
+ }
25539
25643
 
25540
25644
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
25541
25645
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
@@ -31807,6 +31911,8 @@ const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
31807
31911
  const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
31808
31912
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
31809
31913
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
31914
+ const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
31915
+ const MS_PER_MINUTE = 60000;
31810
31916
  /**
31811
31917
  * Gets backtest mode flag from execution context if available.
31812
31918
  * Returns false if no execution context exists (live mode).
@@ -32160,6 +32266,151 @@ class ExchangeInstance {
32160
32266
  const isBacktest = await GET_BACKTEST_FN();
32161
32267
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
32162
32268
  };
32269
+ /**
32270
+ * Fetches raw candles with flexible date/limit parameters.
32271
+ *
32272
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
32273
+ *
32274
+ * Parameter combinations:
32275
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
32276
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= now
32277
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= now
32278
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
32279
+ * 5. Only limit: uses Date.now() as reference (backward)
32280
+ *
32281
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
32282
+ * @param interval - Candle interval (e.g., "1m", "1h")
32283
+ * @param limit - Optional number of candles to fetch
32284
+ * @param sDate - Optional start date in milliseconds
32285
+ * @param eDate - Optional end date in milliseconds
32286
+ * @returns Promise resolving to array of candle data
32287
+ *
32288
+ * @example
32289
+ * ```typescript
32290
+ * const instance = new ExchangeInstance("binance");
32291
+ *
32292
+ * // Fetch 100 candles backward from now
32293
+ * const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
32294
+ *
32295
+ * // Fetch candles for specific date range
32296
+ * const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
32297
+ * ```
32298
+ */
32299
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
32300
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
32301
+ exchangeName: this.exchangeName,
32302
+ symbol,
32303
+ interval,
32304
+ limit,
32305
+ sDate,
32306
+ eDate,
32307
+ });
32308
+ const step = INTERVAL_MINUTES$1[interval];
32309
+ if (!step) {
32310
+ throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
32311
+ }
32312
+ const nowTimestamp = Date.now();
32313
+ let sinceTimestamp;
32314
+ let untilTimestamp;
32315
+ let calculatedLimit;
32316
+ // Case 1: all three parameters provided
32317
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
32318
+ if (sDate >= eDate) {
32319
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
32320
+ }
32321
+ if (eDate > nowTimestamp) {
32322
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32323
+ }
32324
+ sinceTimestamp = sDate;
32325
+ untilTimestamp = eDate;
32326
+ calculatedLimit = limit;
32327
+ }
32328
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
32329
+ else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
32330
+ if (sDate >= eDate) {
32331
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
32332
+ }
32333
+ if (eDate > nowTimestamp) {
32334
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32335
+ }
32336
+ sinceTimestamp = sDate;
32337
+ untilTimestamp = eDate;
32338
+ calculatedLimit = Math.ceil((eDate - sDate) / (step * MS_PER_MINUTE));
32339
+ if (calculatedLimit <= 0) {
32340
+ throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
32341
+ }
32342
+ }
32343
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
32344
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
32345
+ if (eDate > nowTimestamp) {
32346
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32347
+ }
32348
+ untilTimestamp = eDate;
32349
+ sinceTimestamp = eDate - limit * step * MS_PER_MINUTE;
32350
+ calculatedLimit = limit;
32351
+ }
32352
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
32353
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
32354
+ sinceTimestamp = sDate;
32355
+ untilTimestamp = sDate + limit * step * MS_PER_MINUTE;
32356
+ if (untilTimestamp > nowTimestamp) {
32357
+ throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${untilTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
32358
+ }
32359
+ calculatedLimit = limit;
32360
+ }
32361
+ // Case 5: Only limit - use Date.now() as reference (backward)
32362
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
32363
+ untilTimestamp = nowTimestamp;
32364
+ sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
32365
+ calculatedLimit = limit;
32366
+ }
32367
+ // Invalid: no parameters or only sDate or only eDate
32368
+ else {
32369
+ throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
32370
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
32371
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
32372
+ }
32373
+ // Try to read from cache first
32374
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
32375
+ if (cachedCandles !== null) {
32376
+ return cachedCandles;
32377
+ }
32378
+ // Fetch candles
32379
+ const since = new Date(sinceTimestamp);
32380
+ let allData = [];
32381
+ const isBacktest = await GET_BACKTEST_FN();
32382
+ const getCandles = this._methods.getCandles;
32383
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
32384
+ let remaining = calculatedLimit;
32385
+ let currentSince = new Date(since.getTime());
32386
+ while (remaining > 0) {
32387
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
32388
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
32389
+ allData.push(...chunkData);
32390
+ remaining -= chunkLimit;
32391
+ if (remaining > 0) {
32392
+ currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
32393
+ }
32394
+ }
32395
+ }
32396
+ else {
32397
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
32398
+ }
32399
+ // Filter candles to strictly match the requested range
32400
+ const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
32401
+ candle.timestamp < untilTimestamp);
32402
+ // Apply distinct by timestamp to remove duplicates
32403
+ const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
32404
+ if (filteredData.length !== uniqueData.length) {
32405
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`);
32406
+ }
32407
+ if (uniqueData.length < calculatedLimit) {
32408
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Expected ${calculatedLimit} candles, got ${uniqueData.length}`);
32409
+ }
32410
+ // Write to cache after successful fetch
32411
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
32412
+ return uniqueData;
32413
+ };
32163
32414
  const schema = bt.exchangeSchemaService.get(this.exchangeName);
32164
32415
  this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
32165
32416
  }
@@ -32264,6 +32515,24 @@ class ExchangeUtils {
32264
32515
  const instance = this._getInstance(context.exchangeName);
32265
32516
  return await instance.getOrderBook(symbol, depth);
32266
32517
  };
32518
+ /**
32519
+ * Fetches raw candles with flexible date/limit parameters.
32520
+ *
32521
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
32522
+ *
32523
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
32524
+ * @param interval - Candle interval (e.g., "1m", "1h")
32525
+ * @param context - Execution context with exchange name
32526
+ * @param limit - Optional number of candles to fetch
32527
+ * @param sDate - Optional start date in milliseconds
32528
+ * @param eDate - Optional end date in milliseconds
32529
+ * @returns Promise resolving to array of candle data
32530
+ */
32531
+ this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
32532
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
32533
+ const instance = this._getInstance(context.exchangeName);
32534
+ return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
32535
+ };
32267
32536
  }
32268
32537
  }
32269
32538
  /**
@@ -33461,6 +33730,7 @@ exports.getFrameSchema = getFrameSchema;
33461
33730
  exports.getMode = getMode;
33462
33731
  exports.getOptimizerSchema = getOptimizerSchema;
33463
33732
  exports.getOrderBook = getOrderBook;
33733
+ exports.getRawCandles = getRawCandles;
33464
33734
  exports.getRiskSchema = getRiskSchema;
33465
33735
  exports.getSizingSchema = getSizingSchema;
33466
33736
  exports.getStrategySchema = getStrategySchema;