backtest-kit 2.2.26 → 2.3.2
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/README.md +98 -3
- package/build/index.cjs +462 -142
- package/build/index.mjs +463 -144
- package/package.json +3 -2
- package/types.d.ts +64 -6
package/build/index.cjs
CHANGED
|
@@ -700,6 +700,20 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
700
700
|
|
|
701
701
|
var _a$2;
|
|
702
702
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
703
|
+
// Calculate step in milliseconds for candle close time validation
|
|
704
|
+
const INTERVAL_MINUTES$5 = {
|
|
705
|
+
"1m": 1,
|
|
706
|
+
"3m": 3,
|
|
707
|
+
"5m": 5,
|
|
708
|
+
"15m": 15,
|
|
709
|
+
"30m": 30,
|
|
710
|
+
"1h": 60,
|
|
711
|
+
"2h": 120,
|
|
712
|
+
"4h": 240,
|
|
713
|
+
"6h": 360,
|
|
714
|
+
"8h": 480,
|
|
715
|
+
};
|
|
716
|
+
const MS_PER_MINUTE$2 = 60000;
|
|
703
717
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
704
718
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
705
719
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
@@ -1564,60 +1578,73 @@ class PersistCandleUtils {
|
|
|
1564
1578
|
]));
|
|
1565
1579
|
/**
|
|
1566
1580
|
* Reads cached candles for a specific exchange, symbol, and interval.
|
|
1567
|
-
* Returns candles only if cache contains
|
|
1581
|
+
* Returns candles only if cache contains ALL requested candles.
|
|
1582
|
+
*
|
|
1583
|
+
* Algorithm (matches ClientExchange.ts logic):
|
|
1584
|
+
* 1. Calculate expected timestamps: sinceTimestamp, sinceTimestamp + stepMs, ..., sinceTimestamp + (limit-1) * stepMs
|
|
1585
|
+
* 2. Try to read each expected candle by timestamp key
|
|
1586
|
+
* 3. If ANY candle is missing, return null (cache miss)
|
|
1587
|
+
* 4. If all candles found, return them in order
|
|
1568
1588
|
*
|
|
1569
1589
|
* @param symbol - Trading pair symbol
|
|
1570
1590
|
* @param interval - Candle interval
|
|
1571
1591
|
* @param exchangeName - Exchange identifier
|
|
1572
1592
|
* @param limit - Number of candles requested
|
|
1573
|
-
* @param sinceTimestamp -
|
|
1574
|
-
* @param
|
|
1593
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1594
|
+
* @param _untilTimestamp - Unused, kept for API compatibility
|
|
1575
1595
|
* @returns Promise resolving to array of candles or null if cache is incomplete
|
|
1576
1596
|
*/
|
|
1577
|
-
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp,
|
|
1597
|
+
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, _untilTimestamp) => {
|
|
1578
1598
|
bt.loggerService.info("PersistCandleUtils.readCandlesData", {
|
|
1579
1599
|
symbol,
|
|
1580
1600
|
interval,
|
|
1581
1601
|
exchangeName,
|
|
1582
1602
|
limit,
|
|
1583
1603
|
sinceTimestamp,
|
|
1584
|
-
untilTimestamp,
|
|
1585
1604
|
});
|
|
1586
1605
|
const key = `${symbol}:${interval}:${exchangeName}`;
|
|
1587
1606
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1588
1607
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1589
1608
|
await stateStorage.waitForInit(isInitial);
|
|
1590
|
-
|
|
1609
|
+
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1610
|
+
// Calculate expected timestamps and fetch each candle directly
|
|
1591
1611
|
const cachedCandles = [];
|
|
1592
|
-
for
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1612
|
+
for (let i = 0; i < limit; i++) {
|
|
1613
|
+
const expectedTimestamp = sinceTimestamp + i * stepMs;
|
|
1614
|
+
const timestampKey = String(expectedTimestamp);
|
|
1615
|
+
if (await functoolsKit.not(stateStorage.hasValue(timestampKey))) {
|
|
1616
|
+
// Cache miss - candle not found
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
try {
|
|
1620
|
+
const candle = await stateStorage.readValue(timestampKey);
|
|
1621
|
+
cachedCandles.push(candle);
|
|
1622
|
+
}
|
|
1623
|
+
catch (error) {
|
|
1624
|
+
// Invalid candle in cache - treat as cache miss
|
|
1625
|
+
const message = `PersistCandleUtils.readCandlesData found invalid candle symbol=${symbol} interval=${interval} timestamp=${expectedTimestamp}`;
|
|
1626
|
+
const payload = {
|
|
1627
|
+
error: functoolsKit.errorData(error),
|
|
1628
|
+
message: functoolsKit.getErrorMessage(error),
|
|
1629
|
+
};
|
|
1630
|
+
bt.loggerService.warn(message, payload);
|
|
1631
|
+
console.warn(message, payload);
|
|
1632
|
+
errorEmitter.next(error);
|
|
1633
|
+
return null;
|
|
1610
1634
|
}
|
|
1611
1635
|
}
|
|
1612
|
-
// Sort by timestamp ascending
|
|
1613
|
-
cachedCandles.sort((a, b) => a.timestamp - b.timestamp);
|
|
1614
1636
|
return cachedCandles;
|
|
1615
1637
|
};
|
|
1616
1638
|
/**
|
|
1617
1639
|
* Writes candles to cache with atomic file writes.
|
|
1618
1640
|
* Each candle is stored as a separate JSON file named by its timestamp.
|
|
1619
1641
|
*
|
|
1620
|
-
*
|
|
1642
|
+
* The candles passed to this function should be validated candles from the adapter:
|
|
1643
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1644
|
+
* - Exact number of candles as requested
|
|
1645
|
+
* - All candles are fully closed (timestamp + stepMs < untilTimestamp)
|
|
1646
|
+
*
|
|
1647
|
+
* @param candles - Array of candle data to cache (validated by the caller)
|
|
1621
1648
|
* @param symbol - Trading pair symbol
|
|
1622
1649
|
* @param interval - Candle interval
|
|
1623
1650
|
* @param exchangeName - Exchange identifier
|
|
@@ -1634,8 +1661,25 @@ class PersistCandleUtils {
|
|
|
1634
1661
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1635
1662
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1636
1663
|
await stateStorage.waitForInit(isInitial);
|
|
1637
|
-
//
|
|
1664
|
+
// Calculate step in milliseconds to determine candle close time
|
|
1665
|
+
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1666
|
+
const now = Date.now();
|
|
1667
|
+
// Write each candle as a separate file, skipping incomplete candles
|
|
1638
1668
|
for (const candle of candles) {
|
|
1669
|
+
// Skip incomplete candles: candle is complete when closeTime <= now
|
|
1670
|
+
// closeTime = timestamp + stepMs
|
|
1671
|
+
const candleCloseTime = candle.timestamp + stepMs;
|
|
1672
|
+
if (candleCloseTime > now) {
|
|
1673
|
+
bt.loggerService.debug("PersistCandleUtils.writeCandlesData: skipping incomplete candle", {
|
|
1674
|
+
symbol,
|
|
1675
|
+
interval,
|
|
1676
|
+
exchangeName,
|
|
1677
|
+
timestamp: candle.timestamp,
|
|
1678
|
+
closeTime: candleCloseTime,
|
|
1679
|
+
now,
|
|
1680
|
+
});
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1639
1683
|
if (await functoolsKit.not(stateStorage.hasValue(String(candle.timestamp)))) {
|
|
1640
1684
|
await stateStorage.writeValue(String(candle.timestamp), candle);
|
|
1641
1685
|
}
|
|
@@ -1793,6 +1837,27 @@ const INTERVAL_MINUTES$4 = {
|
|
|
1793
1837
|
"6h": 360,
|
|
1794
1838
|
"8h": 480,
|
|
1795
1839
|
};
|
|
1840
|
+
/**
|
|
1841
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
1842
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
1843
|
+
*
|
|
1844
|
+
* Candle timestamp convention:
|
|
1845
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
1846
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
1847
|
+
*
|
|
1848
|
+
* Adapter contract:
|
|
1849
|
+
* - Adapter must return candles with timestamp = openTime
|
|
1850
|
+
* - First returned candle.timestamp must equal aligned since
|
|
1851
|
+
* - Adapter must return exactly `limit` candles
|
|
1852
|
+
*
|
|
1853
|
+
* @param timestamp - Timestamp in milliseconds
|
|
1854
|
+
* @param intervalMinutes - Interval in minutes
|
|
1855
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
1856
|
+
*/
|
|
1857
|
+
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
1858
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
1859
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
1860
|
+
};
|
|
1796
1861
|
/**
|
|
1797
1862
|
* Validates that all candles have valid OHLCV data without anomalies.
|
|
1798
1863
|
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
@@ -1856,18 +1921,24 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
|
1856
1921
|
};
|
|
1857
1922
|
/**
|
|
1858
1923
|
* Attempts to read candles from cache.
|
|
1859
|
-
*
|
|
1924
|
+
*
|
|
1925
|
+
* Cache lookup calculates expected timestamps:
|
|
1926
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1927
|
+
* Returns all candles if found, null if any missing.
|
|
1860
1928
|
*
|
|
1861
1929
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1862
|
-
* @param sinceTimestamp -
|
|
1863
|
-
* @param untilTimestamp -
|
|
1930
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1931
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
1864
1932
|
* @param self - Instance of ClientExchange
|
|
1865
|
-
* @returns Cached candles array or null if cache miss
|
|
1933
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
1866
1934
|
*/
|
|
1867
1935
|
const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
|
|
1936
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
1937
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1938
|
+
// Returns all candles if found, null if any missing
|
|
1868
1939
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
1869
1940
|
// Return cached data only if we have exactly the requested limit
|
|
1870
|
-
if (cachedCandles
|
|
1941
|
+
if (cachedCandles?.length === dto.limit) {
|
|
1871
1942
|
self.params.logger.debug(`ClientExchange READ_CANDLES_CACHE_FN: cache hit for symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
1872
1943
|
return cachedCandles;
|
|
1873
1944
|
}
|
|
@@ -1889,7 +1960,12 @@ const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp
|
|
|
1889
1960
|
/**
|
|
1890
1961
|
* Writes candles to cache with error handling.
|
|
1891
1962
|
*
|
|
1892
|
-
*
|
|
1963
|
+
* The candles passed to this function should be validated:
|
|
1964
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1965
|
+
* - Exact number of candles as requested (limit)
|
|
1966
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
1967
|
+
*
|
|
1968
|
+
* @param candles - Array of validated candle data to cache
|
|
1893
1969
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1894
1970
|
* @param self - Instance of ClientExchange
|
|
1895
1971
|
*/
|
|
@@ -2016,6 +2092,13 @@ class ClientExchange {
|
|
|
2016
2092
|
/**
|
|
2017
2093
|
* Fetches historical candles backwards from execution context time.
|
|
2018
2094
|
*
|
|
2095
|
+
* Algorithm:
|
|
2096
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2097
|
+
* 2. Calculate since = alignedWhen - limit * step
|
|
2098
|
+
* 3. Fetch candles starting from since
|
|
2099
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2100
|
+
* 5. Slice to limit
|
|
2101
|
+
*
|
|
2019
2102
|
* @param symbol - Trading pair symbol
|
|
2020
2103
|
* @param interval - Candle interval
|
|
2021
2104
|
* @param limit - Number of candles to fetch
|
|
@@ -2028,11 +2111,16 @@ class ClientExchange {
|
|
|
2028
2111
|
limit,
|
|
2029
2112
|
});
|
|
2030
2113
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
2114
|
+
if (!step) {
|
|
2115
|
+
throw new Error(`ClientExchange unknown interval=${interval}`);
|
|
2034
2116
|
}
|
|
2035
|
-
const
|
|
2117
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2118
|
+
// Align when down to interval boundary
|
|
2119
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2120
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2121
|
+
// Calculate since: go back limit candles from aligned when
|
|
2122
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2123
|
+
const since = new Date(sinceTimestamp);
|
|
2036
2124
|
let allData = [];
|
|
2037
2125
|
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
2038
2126
|
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
@@ -2045,29 +2133,34 @@ class ClientExchange {
|
|
|
2045
2133
|
remaining -= chunkLimit;
|
|
2046
2134
|
if (remaining > 0) {
|
|
2047
2135
|
// Move currentSince forward by the number of candles fetched
|
|
2048
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2136
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2049
2137
|
}
|
|
2050
2138
|
}
|
|
2051
2139
|
}
|
|
2052
2140
|
else {
|
|
2053
2141
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2054
2142
|
}
|
|
2055
|
-
// Filter candles to strictly match the requested range
|
|
2056
|
-
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2057
|
-
const sinceTimestamp = since.getTime();
|
|
2058
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
|
|
2059
|
-
candle.timestamp < whenTimestamp);
|
|
2060
2143
|
// Apply distinct by timestamp to remove duplicates
|
|
2061
|
-
const uniqueData = Array.from(new Map(
|
|
2062
|
-
if (
|
|
2063
|
-
const msg = `ClientExchange Removed ${
|
|
2144
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2145
|
+
if (allData.length !== uniqueData.length) {
|
|
2146
|
+
const msg = `ClientExchange getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2064
2147
|
this.params.logger.warn(msg);
|
|
2065
2148
|
console.warn(msg);
|
|
2066
2149
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2150
|
+
// Validate adapter returned data
|
|
2151
|
+
if (uniqueData.length === 0) {
|
|
2152
|
+
throw new Error(`ClientExchange getCandles: adapter returned empty array. ` +
|
|
2153
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2154
|
+
}
|
|
2155
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2156
|
+
throw new Error(`ClientExchange getCandles: first candle timestamp mismatch. ` +
|
|
2157
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2158
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2159
|
+
}
|
|
2160
|
+
if (uniqueData.length !== limit) {
|
|
2161
|
+
throw new Error(`ClientExchange getCandles: candle count mismatch. ` +
|
|
2162
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2163
|
+
`Adapter must return exact number of candles requested.`);
|
|
2071
2164
|
}
|
|
2072
2165
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2073
2166
|
return uniqueData;
|
|
@@ -2076,6 +2169,13 @@ class ClientExchange {
|
|
|
2076
2169
|
* Fetches future candles forwards from execution context time.
|
|
2077
2170
|
* Used in backtest mode to get candles for signal duration.
|
|
2078
2171
|
*
|
|
2172
|
+
* Algorithm:
|
|
2173
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2174
|
+
* 2. since = alignedWhen (start from aligned when)
|
|
2175
|
+
* 3. Fetch candles starting from since
|
|
2176
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2177
|
+
* 5. Slice to limit
|
|
2178
|
+
*
|
|
2079
2179
|
* @param symbol - Trading pair symbol
|
|
2080
2180
|
* @param interval - Candle interval
|
|
2081
2181
|
* @param limit - Number of candles to fetch
|
|
@@ -2088,12 +2188,24 @@ class ClientExchange {
|
|
|
2088
2188
|
interval,
|
|
2089
2189
|
limit,
|
|
2090
2190
|
});
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2191
|
+
if (!this.params.execution.context.backtest) {
|
|
2192
|
+
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2193
|
+
}
|
|
2094
2194
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2095
|
-
|
|
2096
|
-
|
|
2195
|
+
if (!step) {
|
|
2196
|
+
throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
|
|
2197
|
+
}
|
|
2198
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2199
|
+
const now = Date.now();
|
|
2200
|
+
// Align when down to interval boundary
|
|
2201
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2202
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2203
|
+
// since = alignedWhen (start from aligned when, going forward)
|
|
2204
|
+
const sinceTimestamp = alignedWhen;
|
|
2205
|
+
const since = new Date(sinceTimestamp);
|
|
2206
|
+
// Calculate end time for Date.now() check
|
|
2207
|
+
const endTime = sinceTimestamp + limit * stepMs;
|
|
2208
|
+
// Check that requested period does not exceed Date.now()
|
|
2097
2209
|
if (endTime > now) {
|
|
2098
2210
|
return [];
|
|
2099
2211
|
}
|
|
@@ -2109,27 +2221,34 @@ class ClientExchange {
|
|
|
2109
2221
|
remaining -= chunkLimit;
|
|
2110
2222
|
if (remaining > 0) {
|
|
2111
2223
|
// Move currentSince forward by the number of candles fetched
|
|
2112
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2224
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2113
2225
|
}
|
|
2114
2226
|
}
|
|
2115
2227
|
}
|
|
2116
2228
|
else {
|
|
2117
2229
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2118
2230
|
}
|
|
2119
|
-
// Filter candles to strictly match the requested range
|
|
2120
|
-
const sinceTimestamp = since.getTime();
|
|
2121
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < endTime);
|
|
2122
2231
|
// Apply distinct by timestamp to remove duplicates
|
|
2123
|
-
const uniqueData = Array.from(new Map(
|
|
2124
|
-
if (
|
|
2125
|
-
const msg = `ClientExchange getNextCandles: Removed ${
|
|
2232
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2233
|
+
if (allData.length !== uniqueData.length) {
|
|
2234
|
+
const msg = `ClientExchange getNextCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2126
2235
|
this.params.logger.warn(msg);
|
|
2127
2236
|
console.warn(msg);
|
|
2128
2237
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2238
|
+
// Validate adapter returned data
|
|
2239
|
+
if (uniqueData.length === 0) {
|
|
2240
|
+
throw new Error(`ClientExchange getNextCandles: adapter returned empty array. ` +
|
|
2241
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2242
|
+
}
|
|
2243
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2244
|
+
throw new Error(`ClientExchange getNextCandles: first candle timestamp mismatch. ` +
|
|
2245
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2246
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2247
|
+
}
|
|
2248
|
+
if (uniqueData.length !== limit) {
|
|
2249
|
+
throw new Error(`ClientExchange getNextCandles: candle count mismatch. ` +
|
|
2250
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2251
|
+
`Adapter must return exact number of candles requested.`);
|
|
2133
2252
|
}
|
|
2134
2253
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2135
2254
|
return uniqueData;
|
|
@@ -2204,6 +2323,12 @@ class ClientExchange {
|
|
|
2204
2323
|
/**
|
|
2205
2324
|
* Fetches raw candles with flexible date/limit parameters.
|
|
2206
2325
|
*
|
|
2326
|
+
* Algorithm:
|
|
2327
|
+
* 1. Align all timestamps down to interval boundary
|
|
2328
|
+
* 2. Fetch candles starting from aligned since
|
|
2329
|
+
* 3. Validate first candle timestamp matches aligned since (adapter must return inclusive data)
|
|
2330
|
+
* 4. Slice to limit
|
|
2331
|
+
*
|
|
2207
2332
|
* All modes respect execution context and prevent look-ahead bias.
|
|
2208
2333
|
*
|
|
2209
2334
|
* Parameter combinations:
|
|
@@ -2238,9 +2363,10 @@ class ClientExchange {
|
|
|
2238
2363
|
if (!step) {
|
|
2239
2364
|
throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
|
|
2240
2365
|
}
|
|
2366
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2241
2367
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2368
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2242
2369
|
let sinceTimestamp;
|
|
2243
|
-
let untilTimestamp;
|
|
2244
2370
|
let calculatedLimit;
|
|
2245
2371
|
// Case 1: all three parameters provided
|
|
2246
2372
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -2250,8 +2376,8 @@ class ClientExchange {
|
|
|
2250
2376
|
if (eDate > whenTimestamp) {
|
|
2251
2377
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2252
2378
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2379
|
+
// Align sDate down to interval boundary
|
|
2380
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2255
2381
|
calculatedLimit = limit;
|
|
2256
2382
|
}
|
|
2257
2383
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -2262,9 +2388,10 @@ class ClientExchange {
|
|
|
2262
2388
|
if (eDate > whenTimestamp) {
|
|
2263
2389
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2264
2390
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2391
|
+
// Align sDate down to interval boundary
|
|
2392
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2393
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2394
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
2268
2395
|
if (calculatedLimit <= 0) {
|
|
2269
2396
|
throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
2270
2397
|
}
|
|
@@ -2274,23 +2401,24 @@ class ClientExchange {
|
|
|
2274
2401
|
if (eDate > whenTimestamp) {
|
|
2275
2402
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2276
2403
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2404
|
+
// Align eDate down and calculate sinceTimestamp
|
|
2405
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2406
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
2279
2407
|
calculatedLimit = limit;
|
|
2280
2408
|
}
|
|
2281
2409
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
2282
2410
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2411
|
+
// Align sDate down to interval boundary
|
|
2412
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2413
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
2414
|
+
if (endTimestamp > whenTimestamp) {
|
|
2415
|
+
throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2287
2416
|
}
|
|
2288
2417
|
calculatedLimit = limit;
|
|
2289
2418
|
}
|
|
2290
2419
|
// Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
|
|
2291
2420
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
2292
|
-
|
|
2293
|
-
sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
|
|
2421
|
+
sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2294
2422
|
calculatedLimit = limit;
|
|
2295
2423
|
}
|
|
2296
2424
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -2311,27 +2439,34 @@ class ClientExchange {
|
|
|
2311
2439
|
allData.push(...chunkData);
|
|
2312
2440
|
remaining -= chunkLimit;
|
|
2313
2441
|
if (remaining > 0) {
|
|
2314
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2442
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2315
2443
|
}
|
|
2316
2444
|
}
|
|
2317
2445
|
}
|
|
2318
2446
|
else {
|
|
2319
2447
|
allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
|
|
2320
2448
|
}
|
|
2321
|
-
// Filter candles to strictly match the requested range
|
|
2322
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
|
|
2323
|
-
candle.timestamp < untilTimestamp);
|
|
2324
2449
|
// Apply distinct by timestamp to remove duplicates
|
|
2325
|
-
const uniqueData = Array.from(new Map(
|
|
2326
|
-
if (
|
|
2327
|
-
const msg = `ClientExchange getRawCandles: Removed ${
|
|
2450
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2451
|
+
if (allData.length !== uniqueData.length) {
|
|
2452
|
+
const msg = `ClientExchange getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2328
2453
|
this.params.logger.warn(msg);
|
|
2329
2454
|
console.warn(msg);
|
|
2330
2455
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2456
|
+
// Validate adapter returned data
|
|
2457
|
+
if (uniqueData.length === 0) {
|
|
2458
|
+
throw new Error(`ClientExchange getRawCandles: adapter returned empty array. ` +
|
|
2459
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2460
|
+
}
|
|
2461
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2462
|
+
throw new Error(`ClientExchange getRawCandles: first candle timestamp mismatch. ` +
|
|
2463
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2464
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2465
|
+
}
|
|
2466
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
2467
|
+
throw new Error(`ClientExchange getRawCandles: candle count mismatch. ` +
|
|
2468
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
2469
|
+
`Adapter must return exact number of candles requested.`);
|
|
2335
2470
|
}
|
|
2336
2471
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
|
|
2337
2472
|
return uniqueData;
|
|
@@ -13839,6 +13974,18 @@ const live_columns = [
|
|
|
13839
13974
|
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
13840
13975
|
isVisible: () => true,
|
|
13841
13976
|
},
|
|
13977
|
+
{
|
|
13978
|
+
key: "pendingAt",
|
|
13979
|
+
label: "Pending At",
|
|
13980
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
13981
|
+
isVisible: () => true,
|
|
13982
|
+
},
|
|
13983
|
+
{
|
|
13984
|
+
key: "scheduledAt",
|
|
13985
|
+
label: "Scheduled At",
|
|
13986
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
13987
|
+
isVisible: () => true,
|
|
13988
|
+
},
|
|
13842
13989
|
];
|
|
13843
13990
|
|
|
13844
13991
|
/**
|
|
@@ -13963,6 +14110,18 @@ const partial_columns = [
|
|
|
13963
14110
|
format: (data) => data.note || "",
|
|
13964
14111
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
13965
14112
|
},
|
|
14113
|
+
{
|
|
14114
|
+
key: "pendingAt",
|
|
14115
|
+
label: "Pending At",
|
|
14116
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14117
|
+
isVisible: () => true,
|
|
14118
|
+
},
|
|
14119
|
+
{
|
|
14120
|
+
key: "scheduledAt",
|
|
14121
|
+
label: "Scheduled At",
|
|
14122
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14123
|
+
isVisible: () => true,
|
|
14124
|
+
},
|
|
13966
14125
|
{
|
|
13967
14126
|
key: "timestamp",
|
|
13968
14127
|
label: "Timestamp",
|
|
@@ -14085,6 +14244,18 @@ const breakeven_columns = [
|
|
|
14085
14244
|
format: (data) => data.note || "",
|
|
14086
14245
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
14087
14246
|
},
|
|
14247
|
+
{
|
|
14248
|
+
key: "pendingAt",
|
|
14249
|
+
label: "Pending At",
|
|
14250
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14251
|
+
isVisible: () => true,
|
|
14252
|
+
},
|
|
14253
|
+
{
|
|
14254
|
+
key: "scheduledAt",
|
|
14255
|
+
label: "Scheduled At",
|
|
14256
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14257
|
+
isVisible: () => true,
|
|
14258
|
+
},
|
|
14088
14259
|
{
|
|
14089
14260
|
key: "timestamp",
|
|
14090
14261
|
label: "Timestamp",
|
|
@@ -14363,6 +14534,22 @@ const risk_columns = [
|
|
|
14363
14534
|
format: (data) => data.rejectionNote,
|
|
14364
14535
|
isVisible: () => true,
|
|
14365
14536
|
},
|
|
14537
|
+
{
|
|
14538
|
+
key: "pendingAt",
|
|
14539
|
+
label: "Pending At",
|
|
14540
|
+
format: (data) => data.currentSignal.pendingAt !== undefined
|
|
14541
|
+
? new Date(data.currentSignal.pendingAt).toISOString()
|
|
14542
|
+
: "N/A",
|
|
14543
|
+
isVisible: () => true,
|
|
14544
|
+
},
|
|
14545
|
+
{
|
|
14546
|
+
key: "scheduledAt",
|
|
14547
|
+
label: "Scheduled At",
|
|
14548
|
+
format: (data) => data.currentSignal.scheduledAt !== undefined
|
|
14549
|
+
? new Date(data.currentSignal.scheduledAt).toISOString()
|
|
14550
|
+
: "N/A",
|
|
14551
|
+
isVisible: () => true,
|
|
14552
|
+
},
|
|
14366
14553
|
{
|
|
14367
14554
|
key: "timestamp",
|
|
14368
14555
|
label: "Timestamp",
|
|
@@ -14513,6 +14700,18 @@ const schedule_columns = [
|
|
|
14513
14700
|
format: (data) => data.cancelId ?? "N/A",
|
|
14514
14701
|
isVisible: () => true,
|
|
14515
14702
|
},
|
|
14703
|
+
{
|
|
14704
|
+
key: "pendingAt",
|
|
14705
|
+
label: "Pending At",
|
|
14706
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
14707
|
+
isVisible: () => true,
|
|
14708
|
+
},
|
|
14709
|
+
{
|
|
14710
|
+
key: "scheduledAt",
|
|
14711
|
+
label: "Scheduled At",
|
|
14712
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
14713
|
+
isVisible: () => true,
|
|
14714
|
+
},
|
|
14516
14715
|
];
|
|
14517
14716
|
|
|
14518
14717
|
/**
|
|
@@ -15904,6 +16103,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15904
16103
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
15905
16104
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
15906
16105
|
partialExecuted: data.signal.partialExecuted,
|
|
16106
|
+
pendingAt: data.signal.pendingAt,
|
|
16107
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15907
16108
|
});
|
|
15908
16109
|
// Trim queue if exceeded MAX_EVENTS
|
|
15909
16110
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -15934,6 +16135,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15934
16135
|
percentTp: data.percentTp,
|
|
15935
16136
|
percentSl: data.percentSl,
|
|
15936
16137
|
pnl: data.pnl.pnlPercentage,
|
|
16138
|
+
pendingAt: data.signal.pendingAt,
|
|
16139
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15937
16140
|
};
|
|
15938
16141
|
// Find the last active event with the same signalId
|
|
15939
16142
|
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
@@ -15974,6 +16177,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15974
16177
|
pnl: data.pnl.pnlPercentage,
|
|
15975
16178
|
closeReason: data.closeReason,
|
|
15976
16179
|
duration: durationMin,
|
|
16180
|
+
pendingAt: data.signal.pendingAt,
|
|
16181
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15977
16182
|
};
|
|
15978
16183
|
this._eventList.unshift(newEvent);
|
|
15979
16184
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -16001,6 +16206,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16001
16206
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
16002
16207
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16003
16208
|
partialExecuted: data.signal.partialExecuted,
|
|
16209
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16004
16210
|
});
|
|
16005
16211
|
// Trim queue if exceeded MAX_EVENTS
|
|
16006
16212
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16031,6 +16237,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16031
16237
|
percentTp: data.percentTp,
|
|
16032
16238
|
percentSl: data.percentSl,
|
|
16033
16239
|
pnl: data.pnl.pnlPercentage,
|
|
16240
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16034
16241
|
};
|
|
16035
16242
|
// Find the last waiting event with the same signalId
|
|
16036
16243
|
const lastWaitingIndex = this._eventList.findLastIndex((event) => event.action === "waiting" && event.signalId === data.signal.id);
|
|
@@ -16067,6 +16274,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16067
16274
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16068
16275
|
partialExecuted: data.signal.partialExecuted,
|
|
16069
16276
|
cancelReason: data.reason,
|
|
16277
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16070
16278
|
});
|
|
16071
16279
|
// Trim queue if exceeded MAX_EVENTS
|
|
16072
16280
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16558,6 +16766,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16558
16766
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
16559
16767
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16560
16768
|
partialExecuted: data.signal.partialExecuted,
|
|
16769
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16561
16770
|
});
|
|
16562
16771
|
// Trim queue if exceeded MAX_EVENTS
|
|
16563
16772
|
if (this._eventList.length > MAX_EVENTS$6) {
|
|
@@ -16587,6 +16796,8 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16587
16796
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16588
16797
|
partialExecuted: data.signal.partialExecuted,
|
|
16589
16798
|
duration: durationMin,
|
|
16799
|
+
pendingAt: data.signal.pendingAt,
|
|
16800
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16590
16801
|
};
|
|
16591
16802
|
this._eventList.unshift(newEvent);
|
|
16592
16803
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -16620,6 +16831,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16620
16831
|
duration: durationMin,
|
|
16621
16832
|
cancelReason: data.reason,
|
|
16622
16833
|
cancelId: data.cancelId,
|
|
16834
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16623
16835
|
};
|
|
16624
16836
|
this._eventList.unshift(newEvent);
|
|
16625
16837
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19761,6 +19973,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19761
19973
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19762
19974
|
partialExecuted: data.partialExecuted,
|
|
19763
19975
|
note: data.note,
|
|
19976
|
+
pendingAt: data.pendingAt,
|
|
19977
|
+
scheduledAt: data.scheduledAt,
|
|
19764
19978
|
backtest,
|
|
19765
19979
|
});
|
|
19766
19980
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19793,6 +20007,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19793
20007
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19794
20008
|
partialExecuted: data.partialExecuted,
|
|
19795
20009
|
note: data.note,
|
|
20010
|
+
pendingAt: data.pendingAt,
|
|
20011
|
+
scheduledAt: data.scheduledAt,
|
|
19796
20012
|
backtest,
|
|
19797
20013
|
});
|
|
19798
20014
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -20890,6 +21106,8 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
20890
21106
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
20891
21107
|
partialExecuted: data.partialExecuted,
|
|
20892
21108
|
note: data.note,
|
|
21109
|
+
pendingAt: data.pendingAt,
|
|
21110
|
+
scheduledAt: data.scheduledAt,
|
|
20893
21111
|
backtest,
|
|
20894
21112
|
});
|
|
20895
21113
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -23275,9 +23493,15 @@ class HeatReportService {
|
|
|
23275
23493
|
signalId: data.signal?.id,
|
|
23276
23494
|
position: data.signal?.position,
|
|
23277
23495
|
note: data.signal?.note,
|
|
23496
|
+
priceOpen: data.signal?.priceOpen,
|
|
23497
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
23498
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
23499
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
23500
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
23278
23501
|
pnl: data.pnl.pnlPercentage,
|
|
23279
23502
|
closeReason: data.closeReason,
|
|
23280
23503
|
openTime: data.signal?.pendingAt,
|
|
23504
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
23281
23505
|
closeTime: data.closeTimestamp,
|
|
23282
23506
|
}, {
|
|
23283
23507
|
symbol: data.symbol,
|
|
@@ -23686,6 +23910,8 @@ class RiskReportService {
|
|
|
23686
23910
|
originalPriceStopLoss: data.currentSignal?.originalPriceStopLoss,
|
|
23687
23911
|
partialExecuted: data.currentSignal?.partialExecuted,
|
|
23688
23912
|
note: data.currentSignal?.note,
|
|
23913
|
+
pendingAt: data.currentSignal?.pendingAt,
|
|
23914
|
+
scheduledAt: data.currentSignal?.scheduledAt,
|
|
23689
23915
|
minuteEstimatedTime: data.currentSignal?.minuteEstimatedTime,
|
|
23690
23916
|
}, {
|
|
23691
23917
|
symbol: data.symbol,
|
|
@@ -25628,6 +25854,7 @@ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
|
25628
25854
|
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
25629
25855
|
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
25630
25856
|
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
25857
|
+
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
25631
25858
|
/**
|
|
25632
25859
|
* Checks if trade context is active (execution and method contexts).
|
|
25633
25860
|
*
|
|
@@ -25932,6 +26159,30 @@ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
|
|
|
25932
26159
|
}
|
|
25933
26160
|
return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
|
|
25934
26161
|
}
|
|
26162
|
+
/**
|
|
26163
|
+
* Fetches the set of candles after current time based on execution context.
|
|
26164
|
+
*
|
|
26165
|
+
* Uses the exchange's getNextCandles implementation to retrieve candles
|
|
26166
|
+
* that occur after the current context time.
|
|
26167
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26168
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
26169
|
+
* @param limit - Number of candles to fetch
|
|
26170
|
+
* @returns Promise resolving to array of candle data
|
|
26171
|
+
*/
|
|
26172
|
+
async function getNextCandles(symbol, interval, limit) {
|
|
26173
|
+
bt.loggerService.info(GET_NEXT_CANDLES_METHOD_NAME, {
|
|
26174
|
+
symbol,
|
|
26175
|
+
interval,
|
|
26176
|
+
limit,
|
|
26177
|
+
});
|
|
26178
|
+
if (!ExecutionContextService.hasContext()) {
|
|
26179
|
+
throw new Error("getNextCandles requires an execution context");
|
|
26180
|
+
}
|
|
26181
|
+
if (!MethodContextService.hasContext()) {
|
|
26182
|
+
throw new Error("getNextCandles requires a method context");
|
|
26183
|
+
}
|
|
26184
|
+
return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
26185
|
+
}
|
|
25935
26186
|
|
|
25936
26187
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
25937
26188
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
@@ -32865,6 +33116,16 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
|
32865
33116
|
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
32866
33117
|
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
32867
33118
|
const MS_PER_MINUTE = 60000;
|
|
33119
|
+
/**
|
|
33120
|
+
* Gets current timestamp from execution context if available.
|
|
33121
|
+
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
33122
|
+
*/
|
|
33123
|
+
const GET_TIMESTAMP_FN = async () => {
|
|
33124
|
+
if (ExecutionContextService.hasContext()) {
|
|
33125
|
+
return new Date(bt.executionContextService.context.when);
|
|
33126
|
+
}
|
|
33127
|
+
return new Date();
|
|
33128
|
+
};
|
|
32868
33129
|
/**
|
|
32869
33130
|
* Gets backtest mode flag from execution context if available.
|
|
32870
33131
|
* Returns false if no execution context exists (live mode).
|
|
@@ -32921,6 +33182,27 @@ const INTERVAL_MINUTES$1 = {
|
|
|
32921
33182
|
"6h": 360,
|
|
32922
33183
|
"8h": 480,
|
|
32923
33184
|
};
|
|
33185
|
+
/**
|
|
33186
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
33187
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
33188
|
+
*
|
|
33189
|
+
* Candle timestamp convention:
|
|
33190
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
33191
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
33192
|
+
*
|
|
33193
|
+
* Adapter contract:
|
|
33194
|
+
* - Adapter must return candles with timestamp = openTime
|
|
33195
|
+
* - First returned candle.timestamp must equal aligned since
|
|
33196
|
+
* - Adapter must return exactly `limit` candles
|
|
33197
|
+
*
|
|
33198
|
+
* @param timestamp - Timestamp in milliseconds
|
|
33199
|
+
* @param intervalMinutes - Interval in minutes
|
|
33200
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
33201
|
+
*/
|
|
33202
|
+
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
33203
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE;
|
|
33204
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
33205
|
+
};
|
|
32924
33206
|
/**
|
|
32925
33207
|
* Creates exchange instance with methods resolved once during construction.
|
|
32926
33208
|
* Applies default implementations where schema methods are not provided.
|
|
@@ -32942,18 +33224,24 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
|
32942
33224
|
};
|
|
32943
33225
|
/**
|
|
32944
33226
|
* Attempts to read candles from cache.
|
|
32945
|
-
*
|
|
33227
|
+
*
|
|
33228
|
+
* Cache lookup calculates expected timestamps:
|
|
33229
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33230
|
+
* Returns all candles if found, null if any missing.
|
|
32946
33231
|
*
|
|
32947
33232
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32948
|
-
* @param sinceTimestamp -
|
|
32949
|
-
* @param untilTimestamp -
|
|
33233
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
33234
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
32950
33235
|
* @param exchangeName - Exchange name
|
|
32951
|
-
* @returns Cached candles array or null if cache miss
|
|
33236
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
32952
33237
|
*/
|
|
32953
33238
|
const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
33239
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
33240
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33241
|
+
// Returns all candles if found, null if any missing
|
|
32954
33242
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
32955
33243
|
// Return cached data only if we have exactly the requested limit
|
|
32956
|
-
if (cachedCandles
|
|
33244
|
+
if (cachedCandles?.length === dto.limit) {
|
|
32957
33245
|
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
32958
33246
|
return cachedCandles;
|
|
32959
33247
|
}
|
|
@@ -32975,7 +33263,12 @@ const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp,
|
|
|
32975
33263
|
/**
|
|
32976
33264
|
* Writes candles to cache with error handling.
|
|
32977
33265
|
*
|
|
32978
|
-
*
|
|
33266
|
+
* The candles passed to this function should be validated:
|
|
33267
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
33268
|
+
* - Exact number of candles as requested (limit)
|
|
33269
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
33270
|
+
*
|
|
33271
|
+
* @param candles - Array of validated candle data to cache
|
|
32979
33272
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32980
33273
|
* @param exchangeName - Exchange name
|
|
32981
33274
|
*/
|
|
@@ -33046,14 +33339,18 @@ class ExchangeInstance {
|
|
|
33046
33339
|
});
|
|
33047
33340
|
const getCandles = this._methods.getCandles;
|
|
33048
33341
|
const step = INTERVAL_MINUTES$1[interval];
|
|
33049
|
-
|
|
33050
|
-
|
|
33051
|
-
|
|
33052
|
-
|
|
33053
|
-
|
|
33054
|
-
const
|
|
33055
|
-
const
|
|
33056
|
-
const
|
|
33342
|
+
if (!step) {
|
|
33343
|
+
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
33344
|
+
}
|
|
33345
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33346
|
+
// Align when down to interval boundary
|
|
33347
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33348
|
+
const whenTimestamp = when.getTime();
|
|
33349
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
|
|
33350
|
+
// Calculate since: go back limit candles from aligned when
|
|
33351
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
33352
|
+
const since = new Date(sinceTimestamp);
|
|
33353
|
+
const untilTimestamp = alignedWhen;
|
|
33057
33354
|
// Try to read from cache first
|
|
33058
33355
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33059
33356
|
if (cachedCandles !== null) {
|
|
@@ -33072,7 +33369,7 @@ class ExchangeInstance {
|
|
|
33072
33369
|
remaining -= chunkLimit;
|
|
33073
33370
|
if (remaining > 0) {
|
|
33074
33371
|
// Move currentSince forward by the number of candles fetched
|
|
33075
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
33372
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33076
33373
|
}
|
|
33077
33374
|
}
|
|
33078
33375
|
}
|
|
@@ -33080,17 +33377,25 @@ class ExchangeInstance {
|
|
|
33080
33377
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33081
33378
|
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
33082
33379
|
}
|
|
33083
|
-
// Filter candles to strictly match the requested range
|
|
33084
|
-
const whenTimestamp = when.getTime();
|
|
33085
|
-
const stepMs = step * 60 * 1000;
|
|
33086
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < whenTimestamp + stepMs);
|
|
33087
33380
|
// Apply distinct by timestamp to remove duplicates
|
|
33088
|
-
const uniqueData = Array.from(new Map(
|
|
33089
|
-
if (
|
|
33090
|
-
bt.loggerService.warn(`ExchangeInstance Removed ${
|
|
33381
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
33382
|
+
if (allData.length !== uniqueData.length) {
|
|
33383
|
+
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
33091
33384
|
}
|
|
33092
|
-
|
|
33093
|
-
|
|
33385
|
+
// Validate adapter returned data
|
|
33386
|
+
if (uniqueData.length === 0) {
|
|
33387
|
+
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
33388
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33389
|
+
}
|
|
33390
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
33391
|
+
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
33392
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
33393
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
33394
|
+
}
|
|
33395
|
+
if (uniqueData.length !== limit) {
|
|
33396
|
+
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
33397
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
33398
|
+
`Adapter must return exact number of candles requested.`);
|
|
33094
33399
|
}
|
|
33095
33400
|
// Write to cache after successful fetch
|
|
33096
33401
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
@@ -33213,8 +33518,8 @@ class ExchangeInstance {
|
|
|
33213
33518
|
symbol,
|
|
33214
33519
|
depth,
|
|
33215
33520
|
});
|
|
33216
|
-
const to =
|
|
33217
|
-
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES *
|
|
33521
|
+
const to = await GET_TIMESTAMP_FN();
|
|
33522
|
+
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
|
|
33218
33523
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33219
33524
|
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
33220
33525
|
};
|
|
@@ -33261,9 +33566,11 @@ class ExchangeInstance {
|
|
|
33261
33566
|
if (!step) {
|
|
33262
33567
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
33263
33568
|
}
|
|
33264
|
-
const
|
|
33569
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33570
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33571
|
+
const nowTimestamp = when.getTime();
|
|
33572
|
+
const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
|
|
33265
33573
|
let sinceTimestamp;
|
|
33266
|
-
let untilTimestamp;
|
|
33267
33574
|
let calculatedLimit;
|
|
33268
33575
|
// Case 1: all three parameters provided
|
|
33269
33576
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -33273,8 +33580,8 @@ class ExchangeInstance {
|
|
|
33273
33580
|
if (eDate > nowTimestamp) {
|
|
33274
33581
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33275
33582
|
}
|
|
33276
|
-
|
|
33277
|
-
|
|
33583
|
+
// Align sDate down to interval boundary
|
|
33584
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33278
33585
|
calculatedLimit = limit;
|
|
33279
33586
|
}
|
|
33280
33587
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -33285,9 +33592,10 @@ class ExchangeInstance {
|
|
|
33285
33592
|
if (eDate > nowTimestamp) {
|
|
33286
33593
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33287
33594
|
}
|
|
33288
|
-
|
|
33289
|
-
|
|
33290
|
-
|
|
33595
|
+
// Align sDate down to interval boundary
|
|
33596
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33597
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
33598
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
33291
33599
|
if (calculatedLimit <= 0) {
|
|
33292
33600
|
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
33293
33601
|
}
|
|
@@ -33297,23 +33605,24 @@ class ExchangeInstance {
|
|
|
33297
33605
|
if (eDate > nowTimestamp) {
|
|
33298
33606
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33299
33607
|
}
|
|
33300
|
-
|
|
33301
|
-
|
|
33608
|
+
// Align eDate down and calculate sinceTimestamp
|
|
33609
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
33610
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
33302
33611
|
calculatedLimit = limit;
|
|
33303
33612
|
}
|
|
33304
33613
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
33305
33614
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
33306
|
-
|
|
33307
|
-
|
|
33308
|
-
|
|
33309
|
-
|
|
33615
|
+
// Align sDate down to interval boundary
|
|
33616
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33617
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
33618
|
+
if (endTimestamp > nowTimestamp) {
|
|
33619
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33310
33620
|
}
|
|
33311
33621
|
calculatedLimit = limit;
|
|
33312
33622
|
}
|
|
33313
33623
|
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
33314
33624
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
33315
|
-
|
|
33316
|
-
sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
|
|
33625
|
+
sinceTimestamp = alignedNow - limit * stepMs;
|
|
33317
33626
|
calculatedLimit = limit;
|
|
33318
33627
|
}
|
|
33319
33628
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -33323,6 +33632,7 @@ class ExchangeInstance {
|
|
|
33323
33632
|
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
33324
33633
|
}
|
|
33325
33634
|
// Try to read from cache first
|
|
33635
|
+
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
33326
33636
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33327
33637
|
if (cachedCandles !== null) {
|
|
33328
33638
|
return cachedCandles;
|
|
@@ -33341,23 +33651,32 @@ class ExchangeInstance {
|
|
|
33341
33651
|
allData.push(...chunkData);
|
|
33342
33652
|
remaining -= chunkLimit;
|
|
33343
33653
|
if (remaining > 0) {
|
|
33344
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
33654
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33345
33655
|
}
|
|
33346
33656
|
}
|
|
33347
33657
|
}
|
|
33348
33658
|
else {
|
|
33349
33659
|
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
33350
33660
|
}
|
|
33351
|
-
// Filter candles to strictly match the requested range
|
|
33352
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
|
|
33353
|
-
candle.timestamp < untilTimestamp);
|
|
33354
33661
|
// Apply distinct by timestamp to remove duplicates
|
|
33355
|
-
const uniqueData = Array.from(new Map(
|
|
33356
|
-
if (
|
|
33357
|
-
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${
|
|
33662
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
33663
|
+
if (allData.length !== uniqueData.length) {
|
|
33664
|
+
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
33665
|
+
}
|
|
33666
|
+
// Validate adapter returned data
|
|
33667
|
+
if (uniqueData.length === 0) {
|
|
33668
|
+
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
33669
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33670
|
+
}
|
|
33671
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
33672
|
+
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
33673
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
33674
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
33358
33675
|
}
|
|
33359
|
-
if (uniqueData.length
|
|
33360
|
-
|
|
33676
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
33677
|
+
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
33678
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
33679
|
+
`Adapter must return exact number of candles requested.`);
|
|
33361
33680
|
}
|
|
33362
33681
|
// Write to cache after successful fetch
|
|
33363
33682
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
@@ -35018,6 +35337,7 @@ exports.getDefaultConfig = getDefaultConfig;
|
|
|
35018
35337
|
exports.getExchangeSchema = getExchangeSchema;
|
|
35019
35338
|
exports.getFrameSchema = getFrameSchema;
|
|
35020
35339
|
exports.getMode = getMode;
|
|
35340
|
+
exports.getNextCandles = getNextCandles;
|
|
35021
35341
|
exports.getOrderBook = getOrderBook;
|
|
35022
35342
|
exports.getRawCandles = getRawCandles;
|
|
35023
35343
|
exports.getRiskSchema = getRiskSchema;
|