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 +350 -80
- package/build/index.mjs +350 -81
- package/package.json +1 -1
- package/types.d.ts +101 -10
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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
|
-
*
|
|
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:
|
|
2099
|
-
* 2. sDate + eDate: calculates limit from date range, validates
|
|
2100
|
-
* 3. eDate + limit: calculates sDate backward, validates
|
|
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
|
|
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
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
let
|
|
2132
|
-
let
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
|
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
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2140
|
+
sinceTimestamp = sDate;
|
|
2141
|
+
untilTimestamp = eDate;
|
|
2142
|
+
calculatedLimit = limit;
|
|
2144
2143
|
}
|
|
2145
|
-
// Case 2: sDate + eDate - calculate limit
|
|
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
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
-
//
|
|
2181
|
-
if (
|
|
2182
|
-
|
|
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
|
-
//
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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
|
-
|
|
2189
|
+
// Fetch candles using existing logic
|
|
2190
|
+
const since = new Date(sinceTimestamp);
|
|
2190
2191
|
let allData = [];
|
|
2191
|
-
|
|
2192
|
-
|
|
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 *
|
|
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:
|
|
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 >=
|
|
2210
|
-
candle.timestamp <
|
|
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 <
|
|
2219
|
-
const msg = `ClientExchange getRawCandles: Expected ${
|
|
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,
|
|
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 *
|
|
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;
|