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.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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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
|
-
*
|
|
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:
|
|
2078
|
-
* 2. sDate + eDate: calculates limit from date range, validates
|
|
2079
|
-
* 3. eDate + limit: calculates sDate backward, validates
|
|
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
|
|
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
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
let
|
|
2111
|
-
let
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
|
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
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2119
|
+
sinceTimestamp = sDate;
|
|
2120
|
+
untilTimestamp = eDate;
|
|
2121
|
+
calculatedLimit = limit;
|
|
2123
2122
|
}
|
|
2124
|
-
// Case 2: sDate + eDate - calculate limit
|
|
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
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
-
//
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
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
|
-
//
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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
|
-
|
|
2168
|
+
// Fetch candles using existing logic
|
|
2169
|
+
const since = new Date(sinceTimestamp);
|
|
2169
2170
|
let allData = [];
|
|
2170
|
-
|
|
2171
|
-
|
|
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 *
|
|
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:
|
|
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 >=
|
|
2189
|
-
candle.timestamp <
|
|
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 <
|
|
2198
|
-
const msg = `ClientExchange getRawCandles: Expected ${
|
|
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,
|
|
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 *
|
|
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 };
|