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.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActivator } from 'di-kit';
|
|
2
2
|
import { scoped } from 'di-scoped';
|
|
3
|
-
import { Subject, makeExtendable, singleshot, getErrorMessage, memoize,
|
|
3
|
+
import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
|
|
4
4
|
import * as fs from 'fs/promises';
|
|
5
5
|
import fs__default from 'fs/promises';
|
|
6
6
|
import path, { join, dirname } from 'path';
|
|
@@ -680,6 +680,20 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
680
680
|
|
|
681
681
|
var _a$2;
|
|
682
682
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
683
|
+
// Calculate step in milliseconds for candle close time validation
|
|
684
|
+
const INTERVAL_MINUTES$5 = {
|
|
685
|
+
"1m": 1,
|
|
686
|
+
"3m": 3,
|
|
687
|
+
"5m": 5,
|
|
688
|
+
"15m": 15,
|
|
689
|
+
"30m": 30,
|
|
690
|
+
"1h": 60,
|
|
691
|
+
"2h": 120,
|
|
692
|
+
"4h": 240,
|
|
693
|
+
"6h": 360,
|
|
694
|
+
"8h": 480,
|
|
695
|
+
};
|
|
696
|
+
const MS_PER_MINUTE$2 = 60000;
|
|
683
697
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
684
698
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
685
699
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
|
|
@@ -1544,60 +1558,73 @@ class PersistCandleUtils {
|
|
|
1544
1558
|
]));
|
|
1545
1559
|
/**
|
|
1546
1560
|
* Reads cached candles for a specific exchange, symbol, and interval.
|
|
1547
|
-
* Returns candles only if cache contains
|
|
1561
|
+
* Returns candles only if cache contains ALL requested candles.
|
|
1562
|
+
*
|
|
1563
|
+
* Algorithm (matches ClientExchange.ts logic):
|
|
1564
|
+
* 1. Calculate expected timestamps: sinceTimestamp, sinceTimestamp + stepMs, ..., sinceTimestamp + (limit-1) * stepMs
|
|
1565
|
+
* 2. Try to read each expected candle by timestamp key
|
|
1566
|
+
* 3. If ANY candle is missing, return null (cache miss)
|
|
1567
|
+
* 4. If all candles found, return them in order
|
|
1548
1568
|
*
|
|
1549
1569
|
* @param symbol - Trading pair symbol
|
|
1550
1570
|
* @param interval - Candle interval
|
|
1551
1571
|
* @param exchangeName - Exchange identifier
|
|
1552
1572
|
* @param limit - Number of candles requested
|
|
1553
|
-
* @param sinceTimestamp -
|
|
1554
|
-
* @param
|
|
1573
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1574
|
+
* @param _untilTimestamp - Unused, kept for API compatibility
|
|
1555
1575
|
* @returns Promise resolving to array of candles or null if cache is incomplete
|
|
1556
1576
|
*/
|
|
1557
|
-
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp,
|
|
1577
|
+
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, _untilTimestamp) => {
|
|
1558
1578
|
bt.loggerService.info("PersistCandleUtils.readCandlesData", {
|
|
1559
1579
|
symbol,
|
|
1560
1580
|
interval,
|
|
1561
1581
|
exchangeName,
|
|
1562
1582
|
limit,
|
|
1563
1583
|
sinceTimestamp,
|
|
1564
|
-
untilTimestamp,
|
|
1565
1584
|
});
|
|
1566
1585
|
const key = `${symbol}:${interval}:${exchangeName}`;
|
|
1567
1586
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1568
1587
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1569
1588
|
await stateStorage.waitForInit(isInitial);
|
|
1570
|
-
|
|
1589
|
+
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1590
|
+
// Calculate expected timestamps and fetch each candle directly
|
|
1571
1591
|
const cachedCandles = [];
|
|
1572
|
-
for
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
}
|
|
1592
|
+
for (let i = 0; i < limit; i++) {
|
|
1593
|
+
const expectedTimestamp = sinceTimestamp + i * stepMs;
|
|
1594
|
+
const timestampKey = String(expectedTimestamp);
|
|
1595
|
+
if (await not(stateStorage.hasValue(timestampKey))) {
|
|
1596
|
+
// Cache miss - candle not found
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
const candle = await stateStorage.readValue(timestampKey);
|
|
1601
|
+
cachedCandles.push(candle);
|
|
1602
|
+
}
|
|
1603
|
+
catch (error) {
|
|
1604
|
+
// Invalid candle in cache - treat as cache miss
|
|
1605
|
+
const message = `PersistCandleUtils.readCandlesData found invalid candle symbol=${symbol} interval=${interval} timestamp=${expectedTimestamp}`;
|
|
1606
|
+
const payload = {
|
|
1607
|
+
error: errorData(error),
|
|
1608
|
+
message: getErrorMessage(error),
|
|
1609
|
+
};
|
|
1610
|
+
bt.loggerService.warn(message, payload);
|
|
1611
|
+
console.warn(message, payload);
|
|
1612
|
+
errorEmitter.next(error);
|
|
1613
|
+
return null;
|
|
1590
1614
|
}
|
|
1591
1615
|
}
|
|
1592
|
-
// Sort by timestamp ascending
|
|
1593
|
-
cachedCandles.sort((a, b) => a.timestamp - b.timestamp);
|
|
1594
1616
|
return cachedCandles;
|
|
1595
1617
|
};
|
|
1596
1618
|
/**
|
|
1597
1619
|
* Writes candles to cache with atomic file writes.
|
|
1598
1620
|
* Each candle is stored as a separate JSON file named by its timestamp.
|
|
1599
1621
|
*
|
|
1600
|
-
*
|
|
1622
|
+
* The candles passed to this function should be validated candles from the adapter:
|
|
1623
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1624
|
+
* - Exact number of candles as requested
|
|
1625
|
+
* - All candles are fully closed (timestamp + stepMs < untilTimestamp)
|
|
1626
|
+
*
|
|
1627
|
+
* @param candles - Array of candle data to cache (validated by the caller)
|
|
1601
1628
|
* @param symbol - Trading pair symbol
|
|
1602
1629
|
* @param interval - Candle interval
|
|
1603
1630
|
* @param exchangeName - Exchange identifier
|
|
@@ -1614,8 +1641,25 @@ class PersistCandleUtils {
|
|
|
1614
1641
|
const isInitial = !this.getCandlesStorage.has(key);
|
|
1615
1642
|
const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
|
|
1616
1643
|
await stateStorage.waitForInit(isInitial);
|
|
1617
|
-
//
|
|
1644
|
+
// Calculate step in milliseconds to determine candle close time
|
|
1645
|
+
const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$2;
|
|
1646
|
+
const now = Date.now();
|
|
1647
|
+
// Write each candle as a separate file, skipping incomplete candles
|
|
1618
1648
|
for (const candle of candles) {
|
|
1649
|
+
// Skip incomplete candles: candle is complete when closeTime <= now
|
|
1650
|
+
// closeTime = timestamp + stepMs
|
|
1651
|
+
const candleCloseTime = candle.timestamp + stepMs;
|
|
1652
|
+
if (candleCloseTime > now) {
|
|
1653
|
+
bt.loggerService.debug("PersistCandleUtils.writeCandlesData: skipping incomplete candle", {
|
|
1654
|
+
symbol,
|
|
1655
|
+
interval,
|
|
1656
|
+
exchangeName,
|
|
1657
|
+
timestamp: candle.timestamp,
|
|
1658
|
+
closeTime: candleCloseTime,
|
|
1659
|
+
now,
|
|
1660
|
+
});
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1619
1663
|
if (await not(stateStorage.hasValue(String(candle.timestamp)))) {
|
|
1620
1664
|
await stateStorage.writeValue(String(candle.timestamp), candle);
|
|
1621
1665
|
}
|
|
@@ -1773,6 +1817,27 @@ const INTERVAL_MINUTES$4 = {
|
|
|
1773
1817
|
"6h": 360,
|
|
1774
1818
|
"8h": 480,
|
|
1775
1819
|
};
|
|
1820
|
+
/**
|
|
1821
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
1822
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
1823
|
+
*
|
|
1824
|
+
* Candle timestamp convention:
|
|
1825
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
1826
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
1827
|
+
*
|
|
1828
|
+
* Adapter contract:
|
|
1829
|
+
* - Adapter must return candles with timestamp = openTime
|
|
1830
|
+
* - First returned candle.timestamp must equal aligned since
|
|
1831
|
+
* - Adapter must return exactly `limit` candles
|
|
1832
|
+
*
|
|
1833
|
+
* @param timestamp - Timestamp in milliseconds
|
|
1834
|
+
* @param intervalMinutes - Interval in minutes
|
|
1835
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
1836
|
+
*/
|
|
1837
|
+
const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
|
|
1838
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
|
|
1839
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
1840
|
+
};
|
|
1776
1841
|
/**
|
|
1777
1842
|
* Validates that all candles have valid OHLCV data without anomalies.
|
|
1778
1843
|
* Detects incomplete candles from Binance API by checking for abnormally low prices or volumes.
|
|
@@ -1836,18 +1901,24 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
|
1836
1901
|
};
|
|
1837
1902
|
/**
|
|
1838
1903
|
* Attempts to read candles from cache.
|
|
1839
|
-
*
|
|
1904
|
+
*
|
|
1905
|
+
* Cache lookup calculates expected timestamps:
|
|
1906
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1907
|
+
* Returns all candles if found, null if any missing.
|
|
1840
1908
|
*
|
|
1841
1909
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1842
|
-
* @param sinceTimestamp -
|
|
1843
|
-
* @param untilTimestamp -
|
|
1910
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
1911
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
1844
1912
|
* @param self - Instance of ClientExchange
|
|
1845
|
-
* @returns Cached candles array or null if cache miss
|
|
1913
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
1846
1914
|
*/
|
|
1847
1915
|
const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
|
|
1916
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
1917
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
1918
|
+
// Returns all candles if found, null if any missing
|
|
1848
1919
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
1849
1920
|
// Return cached data only if we have exactly the requested limit
|
|
1850
|
-
if (cachedCandles
|
|
1921
|
+
if (cachedCandles?.length === dto.limit) {
|
|
1851
1922
|
self.params.logger.debug(`ClientExchange READ_CANDLES_CACHE_FN: cache hit for symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
1852
1923
|
return cachedCandles;
|
|
1853
1924
|
}
|
|
@@ -1869,7 +1940,12 @@ const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimest
|
|
|
1869
1940
|
/**
|
|
1870
1941
|
* Writes candles to cache with error handling.
|
|
1871
1942
|
*
|
|
1872
|
-
*
|
|
1943
|
+
* The candles passed to this function should be validated:
|
|
1944
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
1945
|
+
* - Exact number of candles as requested (limit)
|
|
1946
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
1947
|
+
*
|
|
1948
|
+
* @param candles - Array of validated candle data to cache
|
|
1873
1949
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1874
1950
|
* @param self - Instance of ClientExchange
|
|
1875
1951
|
*/
|
|
@@ -1996,6 +2072,13 @@ class ClientExchange {
|
|
|
1996
2072
|
/**
|
|
1997
2073
|
* Fetches historical candles backwards from execution context time.
|
|
1998
2074
|
*
|
|
2075
|
+
* Algorithm:
|
|
2076
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2077
|
+
* 2. Calculate since = alignedWhen - limit * step
|
|
2078
|
+
* 3. Fetch candles starting from since
|
|
2079
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2080
|
+
* 5. Slice to limit
|
|
2081
|
+
*
|
|
1999
2082
|
* @param symbol - Trading pair symbol
|
|
2000
2083
|
* @param interval - Candle interval
|
|
2001
2084
|
* @param limit - Number of candles to fetch
|
|
@@ -2008,11 +2091,16 @@ class ClientExchange {
|
|
|
2008
2091
|
limit,
|
|
2009
2092
|
});
|
|
2010
2093
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
throw new Error(`ClientExchange unknown time adjust for interval=${interval}`);
|
|
2094
|
+
if (!step) {
|
|
2095
|
+
throw new Error(`ClientExchange unknown interval=${interval}`);
|
|
2014
2096
|
}
|
|
2015
|
-
const
|
|
2097
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2098
|
+
// Align when down to interval boundary
|
|
2099
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2100
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2101
|
+
// Calculate since: go back limit candles from aligned when
|
|
2102
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2103
|
+
const since = new Date(sinceTimestamp);
|
|
2016
2104
|
let allData = [];
|
|
2017
2105
|
// If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
|
|
2018
2106
|
if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
|
|
@@ -2025,29 +2113,34 @@ class ClientExchange {
|
|
|
2025
2113
|
remaining -= chunkLimit;
|
|
2026
2114
|
if (remaining > 0) {
|
|
2027
2115
|
// Move currentSince forward by the number of candles fetched
|
|
2028
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2116
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2029
2117
|
}
|
|
2030
2118
|
}
|
|
2031
2119
|
}
|
|
2032
2120
|
else {
|
|
2033
2121
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2034
2122
|
}
|
|
2035
|
-
// Filter candles to strictly match the requested range
|
|
2036
|
-
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2037
|
-
const sinceTimestamp = since.getTime();
|
|
2038
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
|
|
2039
|
-
candle.timestamp < whenTimestamp);
|
|
2040
2123
|
// Apply distinct by timestamp to remove duplicates
|
|
2041
|
-
const uniqueData = Array.from(new Map(
|
|
2042
|
-
if (
|
|
2043
|
-
const msg = `ClientExchange Removed ${
|
|
2124
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2125
|
+
if (allData.length !== uniqueData.length) {
|
|
2126
|
+
const msg = `ClientExchange getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2044
2127
|
this.params.logger.warn(msg);
|
|
2045
2128
|
console.warn(msg);
|
|
2046
2129
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2130
|
+
// Validate adapter returned data
|
|
2131
|
+
if (uniqueData.length === 0) {
|
|
2132
|
+
throw new Error(`ClientExchange getCandles: adapter returned empty array. ` +
|
|
2133
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2134
|
+
}
|
|
2135
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2136
|
+
throw new Error(`ClientExchange getCandles: first candle timestamp mismatch. ` +
|
|
2137
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2138
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2139
|
+
}
|
|
2140
|
+
if (uniqueData.length !== limit) {
|
|
2141
|
+
throw new Error(`ClientExchange getCandles: candle count mismatch. ` +
|
|
2142
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2143
|
+
`Adapter must return exact number of candles requested.`);
|
|
2051
2144
|
}
|
|
2052
2145
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2053
2146
|
return uniqueData;
|
|
@@ -2056,6 +2149,13 @@ class ClientExchange {
|
|
|
2056
2149
|
* Fetches future candles forwards from execution context time.
|
|
2057
2150
|
* Used in backtest mode to get candles for signal duration.
|
|
2058
2151
|
*
|
|
2152
|
+
* Algorithm:
|
|
2153
|
+
* 1. Align when down to interval boundary (e.g., 00:17 -> 00:15 for 15m)
|
|
2154
|
+
* 2. since = alignedWhen (start from aligned when)
|
|
2155
|
+
* 3. Fetch candles starting from since
|
|
2156
|
+
* 4. Validate first candle timestamp matches since (adapter must return inclusive data)
|
|
2157
|
+
* 5. Slice to limit
|
|
2158
|
+
*
|
|
2059
2159
|
* @param symbol - Trading pair symbol
|
|
2060
2160
|
* @param interval - Candle interval
|
|
2061
2161
|
* @param limit - Number of candles to fetch
|
|
@@ -2068,12 +2168,24 @@ class ClientExchange {
|
|
|
2068
2168
|
interval,
|
|
2069
2169
|
limit,
|
|
2070
2170
|
});
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2171
|
+
if (!this.params.execution.context.backtest) {
|
|
2172
|
+
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2173
|
+
}
|
|
2074
2174
|
const step = INTERVAL_MINUTES$4[interval];
|
|
2075
|
-
|
|
2076
|
-
|
|
2175
|
+
if (!step) {
|
|
2176
|
+
throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
|
|
2177
|
+
}
|
|
2178
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2179
|
+
const now = Date.now();
|
|
2180
|
+
// Align when down to interval boundary
|
|
2181
|
+
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2182
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2183
|
+
// since = alignedWhen (start from aligned when, going forward)
|
|
2184
|
+
const sinceTimestamp = alignedWhen;
|
|
2185
|
+
const since = new Date(sinceTimestamp);
|
|
2186
|
+
// Calculate end time for Date.now() check
|
|
2187
|
+
const endTime = sinceTimestamp + limit * stepMs;
|
|
2188
|
+
// Check that requested period does not exceed Date.now()
|
|
2077
2189
|
if (endTime > now) {
|
|
2078
2190
|
return [];
|
|
2079
2191
|
}
|
|
@@ -2089,27 +2201,34 @@ class ClientExchange {
|
|
|
2089
2201
|
remaining -= chunkLimit;
|
|
2090
2202
|
if (remaining > 0) {
|
|
2091
2203
|
// Move currentSince forward by the number of candles fetched
|
|
2092
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2204
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2093
2205
|
}
|
|
2094
2206
|
}
|
|
2095
2207
|
}
|
|
2096
2208
|
else {
|
|
2097
2209
|
allData = await GET_CANDLES_FN({ symbol, interval, limit }, since, this);
|
|
2098
2210
|
}
|
|
2099
|
-
// Filter candles to strictly match the requested range
|
|
2100
|
-
const sinceTimestamp = since.getTime();
|
|
2101
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < endTime);
|
|
2102
2211
|
// Apply distinct by timestamp to remove duplicates
|
|
2103
|
-
const uniqueData = Array.from(new Map(
|
|
2104
|
-
if (
|
|
2105
|
-
const msg = `ClientExchange getNextCandles: Removed ${
|
|
2212
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2213
|
+
if (allData.length !== uniqueData.length) {
|
|
2214
|
+
const msg = `ClientExchange getNextCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2106
2215
|
this.params.logger.warn(msg);
|
|
2107
2216
|
console.warn(msg);
|
|
2108
2217
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2218
|
+
// Validate adapter returned data
|
|
2219
|
+
if (uniqueData.length === 0) {
|
|
2220
|
+
throw new Error(`ClientExchange getNextCandles: adapter returned empty array. ` +
|
|
2221
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2222
|
+
}
|
|
2223
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2224
|
+
throw new Error(`ClientExchange getNextCandles: first candle timestamp mismatch. ` +
|
|
2225
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2226
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2227
|
+
}
|
|
2228
|
+
if (uniqueData.length !== limit) {
|
|
2229
|
+
throw new Error(`ClientExchange getNextCandles: candle count mismatch. ` +
|
|
2230
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
2231
|
+
`Adapter must return exact number of candles requested.`);
|
|
2113
2232
|
}
|
|
2114
2233
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
|
|
2115
2234
|
return uniqueData;
|
|
@@ -2184,6 +2303,12 @@ class ClientExchange {
|
|
|
2184
2303
|
/**
|
|
2185
2304
|
* Fetches raw candles with flexible date/limit parameters.
|
|
2186
2305
|
*
|
|
2306
|
+
* Algorithm:
|
|
2307
|
+
* 1. Align all timestamps down to interval boundary
|
|
2308
|
+
* 2. Fetch candles starting from aligned since
|
|
2309
|
+
* 3. Validate first candle timestamp matches aligned since (adapter must return inclusive data)
|
|
2310
|
+
* 4. Slice to limit
|
|
2311
|
+
*
|
|
2187
2312
|
* All modes respect execution context and prevent look-ahead bias.
|
|
2188
2313
|
*
|
|
2189
2314
|
* Parameter combinations:
|
|
@@ -2218,9 +2343,10 @@ class ClientExchange {
|
|
|
2218
2343
|
if (!step) {
|
|
2219
2344
|
throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
|
|
2220
2345
|
}
|
|
2346
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2221
2347
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2348
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
|
|
2222
2349
|
let sinceTimestamp;
|
|
2223
|
-
let untilTimestamp;
|
|
2224
2350
|
let calculatedLimit;
|
|
2225
2351
|
// Case 1: all three parameters provided
|
|
2226
2352
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -2230,8 +2356,8 @@ class ClientExchange {
|
|
|
2230
2356
|
if (eDate > whenTimestamp) {
|
|
2231
2357
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2232
2358
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2359
|
+
// Align sDate down to interval boundary
|
|
2360
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2235
2361
|
calculatedLimit = limit;
|
|
2236
2362
|
}
|
|
2237
2363
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -2242,9 +2368,10 @@ class ClientExchange {
|
|
|
2242
2368
|
if (eDate > whenTimestamp) {
|
|
2243
2369
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2244
2370
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2371
|
+
// Align sDate down to interval boundary
|
|
2372
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2373
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2374
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
2248
2375
|
if (calculatedLimit <= 0) {
|
|
2249
2376
|
throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
2250
2377
|
}
|
|
@@ -2254,23 +2381,24 @@ class ClientExchange {
|
|
|
2254
2381
|
if (eDate > whenTimestamp) {
|
|
2255
2382
|
throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2256
2383
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2384
|
+
// Align eDate down and calculate sinceTimestamp
|
|
2385
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
|
|
2386
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
2259
2387
|
calculatedLimit = limit;
|
|
2260
2388
|
}
|
|
2261
2389
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
2262
2390
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2391
|
+
// Align sDate down to interval boundary
|
|
2392
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
|
|
2393
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
2394
|
+
if (endTimestamp > whenTimestamp) {
|
|
2395
|
+
throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
|
|
2267
2396
|
}
|
|
2268
2397
|
calculatedLimit = limit;
|
|
2269
2398
|
}
|
|
2270
2399
|
// Case 5: Only limit - use execution.context.when as reference (backward like getCandles)
|
|
2271
2400
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
2272
|
-
|
|
2273
|
-
sinceTimestamp = whenTimestamp - limit * step * MS_PER_MINUTE$1;
|
|
2401
|
+
sinceTimestamp = alignedWhen - limit * stepMs;
|
|
2274
2402
|
calculatedLimit = limit;
|
|
2275
2403
|
}
|
|
2276
2404
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -2291,27 +2419,34 @@ class ClientExchange {
|
|
|
2291
2419
|
allData.push(...chunkData);
|
|
2292
2420
|
remaining -= chunkLimit;
|
|
2293
2421
|
if (remaining > 0) {
|
|
2294
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
2422
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
2295
2423
|
}
|
|
2296
2424
|
}
|
|
2297
2425
|
}
|
|
2298
2426
|
else {
|
|
2299
2427
|
allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
|
|
2300
2428
|
}
|
|
2301
|
-
// Filter candles to strictly match the requested range
|
|
2302
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
|
|
2303
|
-
candle.timestamp < untilTimestamp);
|
|
2304
2429
|
// Apply distinct by timestamp to remove duplicates
|
|
2305
|
-
const uniqueData = Array.from(new Map(
|
|
2306
|
-
if (
|
|
2307
|
-
const msg = `ClientExchange getRawCandles: Removed ${
|
|
2430
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
2431
|
+
if (allData.length !== uniqueData.length) {
|
|
2432
|
+
const msg = `ClientExchange getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`;
|
|
2308
2433
|
this.params.logger.warn(msg);
|
|
2309
2434
|
console.warn(msg);
|
|
2310
2435
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2436
|
+
// Validate adapter returned data
|
|
2437
|
+
if (uniqueData.length === 0) {
|
|
2438
|
+
throw new Error(`ClientExchange getRawCandles: adapter returned empty array. ` +
|
|
2439
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
2440
|
+
}
|
|
2441
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
2442
|
+
throw new Error(`ClientExchange getRawCandles: first candle timestamp mismatch. ` +
|
|
2443
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
2444
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
2445
|
+
}
|
|
2446
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
2447
|
+
throw new Error(`ClientExchange getRawCandles: candle count mismatch. ` +
|
|
2448
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
2449
|
+
`Adapter must return exact number of candles requested.`);
|
|
2315
2450
|
}
|
|
2316
2451
|
await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, calculatedLimit, uniqueData);
|
|
2317
2452
|
return uniqueData;
|
|
@@ -13819,6 +13954,18 @@ const live_columns = [
|
|
|
13819
13954
|
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
13820
13955
|
isVisible: () => true,
|
|
13821
13956
|
},
|
|
13957
|
+
{
|
|
13958
|
+
key: "pendingAt",
|
|
13959
|
+
label: "Pending At",
|
|
13960
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
13961
|
+
isVisible: () => true,
|
|
13962
|
+
},
|
|
13963
|
+
{
|
|
13964
|
+
key: "scheduledAt",
|
|
13965
|
+
label: "Scheduled At",
|
|
13966
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
13967
|
+
isVisible: () => true,
|
|
13968
|
+
},
|
|
13822
13969
|
];
|
|
13823
13970
|
|
|
13824
13971
|
/**
|
|
@@ -13943,6 +14090,18 @@ const partial_columns = [
|
|
|
13943
14090
|
format: (data) => data.note || "",
|
|
13944
14091
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
13945
14092
|
},
|
|
14093
|
+
{
|
|
14094
|
+
key: "pendingAt",
|
|
14095
|
+
label: "Pending At",
|
|
14096
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14097
|
+
isVisible: () => true,
|
|
14098
|
+
},
|
|
14099
|
+
{
|
|
14100
|
+
key: "scheduledAt",
|
|
14101
|
+
label: "Scheduled At",
|
|
14102
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14103
|
+
isVisible: () => true,
|
|
14104
|
+
},
|
|
13946
14105
|
{
|
|
13947
14106
|
key: "timestamp",
|
|
13948
14107
|
label: "Timestamp",
|
|
@@ -14065,6 +14224,18 @@ const breakeven_columns = [
|
|
|
14065
14224
|
format: (data) => data.note || "",
|
|
14066
14225
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
14067
14226
|
},
|
|
14227
|
+
{
|
|
14228
|
+
key: "pendingAt",
|
|
14229
|
+
label: "Pending At",
|
|
14230
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14231
|
+
isVisible: () => true,
|
|
14232
|
+
},
|
|
14233
|
+
{
|
|
14234
|
+
key: "scheduledAt",
|
|
14235
|
+
label: "Scheduled At",
|
|
14236
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14237
|
+
isVisible: () => true,
|
|
14238
|
+
},
|
|
14068
14239
|
{
|
|
14069
14240
|
key: "timestamp",
|
|
14070
14241
|
label: "Timestamp",
|
|
@@ -14343,6 +14514,22 @@ const risk_columns = [
|
|
|
14343
14514
|
format: (data) => data.rejectionNote,
|
|
14344
14515
|
isVisible: () => true,
|
|
14345
14516
|
},
|
|
14517
|
+
{
|
|
14518
|
+
key: "pendingAt",
|
|
14519
|
+
label: "Pending At",
|
|
14520
|
+
format: (data) => data.currentSignal.pendingAt !== undefined
|
|
14521
|
+
? new Date(data.currentSignal.pendingAt).toISOString()
|
|
14522
|
+
: "N/A",
|
|
14523
|
+
isVisible: () => true,
|
|
14524
|
+
},
|
|
14525
|
+
{
|
|
14526
|
+
key: "scheduledAt",
|
|
14527
|
+
label: "Scheduled At",
|
|
14528
|
+
format: (data) => data.currentSignal.scheduledAt !== undefined
|
|
14529
|
+
? new Date(data.currentSignal.scheduledAt).toISOString()
|
|
14530
|
+
: "N/A",
|
|
14531
|
+
isVisible: () => true,
|
|
14532
|
+
},
|
|
14346
14533
|
{
|
|
14347
14534
|
key: "timestamp",
|
|
14348
14535
|
label: "Timestamp",
|
|
@@ -14493,6 +14680,18 @@ const schedule_columns = [
|
|
|
14493
14680
|
format: (data) => data.cancelId ?? "N/A",
|
|
14494
14681
|
isVisible: () => true,
|
|
14495
14682
|
},
|
|
14683
|
+
{
|
|
14684
|
+
key: "pendingAt",
|
|
14685
|
+
label: "Pending At",
|
|
14686
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
14687
|
+
isVisible: () => true,
|
|
14688
|
+
},
|
|
14689
|
+
{
|
|
14690
|
+
key: "scheduledAt",
|
|
14691
|
+
label: "Scheduled At",
|
|
14692
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
14693
|
+
isVisible: () => true,
|
|
14694
|
+
},
|
|
14496
14695
|
];
|
|
14497
14696
|
|
|
14498
14697
|
/**
|
|
@@ -15884,6 +16083,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15884
16083
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
15885
16084
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
15886
16085
|
partialExecuted: data.signal.partialExecuted,
|
|
16086
|
+
pendingAt: data.signal.pendingAt,
|
|
16087
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15887
16088
|
});
|
|
15888
16089
|
// Trim queue if exceeded MAX_EVENTS
|
|
15889
16090
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -15914,6 +16115,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15914
16115
|
percentTp: data.percentTp,
|
|
15915
16116
|
percentSl: data.percentSl,
|
|
15916
16117
|
pnl: data.pnl.pnlPercentage,
|
|
16118
|
+
pendingAt: data.signal.pendingAt,
|
|
16119
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15917
16120
|
};
|
|
15918
16121
|
// Find the last active event with the same signalId
|
|
15919
16122
|
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
@@ -15954,6 +16157,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15954
16157
|
pnl: data.pnl.pnlPercentage,
|
|
15955
16158
|
closeReason: data.closeReason,
|
|
15956
16159
|
duration: durationMin,
|
|
16160
|
+
pendingAt: data.signal.pendingAt,
|
|
16161
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15957
16162
|
};
|
|
15958
16163
|
this._eventList.unshift(newEvent);
|
|
15959
16164
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -15981,6 +16186,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15981
16186
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
15982
16187
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
15983
16188
|
partialExecuted: data.signal.partialExecuted,
|
|
16189
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15984
16190
|
});
|
|
15985
16191
|
// Trim queue if exceeded MAX_EVENTS
|
|
15986
16192
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16011,6 +16217,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16011
16217
|
percentTp: data.percentTp,
|
|
16012
16218
|
percentSl: data.percentSl,
|
|
16013
16219
|
pnl: data.pnl.pnlPercentage,
|
|
16220
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16014
16221
|
};
|
|
16015
16222
|
// Find the last waiting event with the same signalId
|
|
16016
16223
|
const lastWaitingIndex = this._eventList.findLastIndex((event) => event.action === "waiting" && event.signalId === data.signal.id);
|
|
@@ -16047,6 +16254,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16047
16254
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16048
16255
|
partialExecuted: data.signal.partialExecuted,
|
|
16049
16256
|
cancelReason: data.reason,
|
|
16257
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16050
16258
|
});
|
|
16051
16259
|
// Trim queue if exceeded MAX_EVENTS
|
|
16052
16260
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16538,6 +16746,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16538
16746
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
16539
16747
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16540
16748
|
partialExecuted: data.signal.partialExecuted,
|
|
16749
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16541
16750
|
});
|
|
16542
16751
|
// Trim queue if exceeded MAX_EVENTS
|
|
16543
16752
|
if (this._eventList.length > MAX_EVENTS$6) {
|
|
@@ -16567,6 +16776,8 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16567
16776
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16568
16777
|
partialExecuted: data.signal.partialExecuted,
|
|
16569
16778
|
duration: durationMin,
|
|
16779
|
+
pendingAt: data.signal.pendingAt,
|
|
16780
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16570
16781
|
};
|
|
16571
16782
|
this._eventList.unshift(newEvent);
|
|
16572
16783
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -16600,6 +16811,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16600
16811
|
duration: durationMin,
|
|
16601
16812
|
cancelReason: data.reason,
|
|
16602
16813
|
cancelId: data.cancelId,
|
|
16814
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16603
16815
|
};
|
|
16604
16816
|
this._eventList.unshift(newEvent);
|
|
16605
16817
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19741,6 +19953,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19741
19953
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19742
19954
|
partialExecuted: data.partialExecuted,
|
|
19743
19955
|
note: data.note,
|
|
19956
|
+
pendingAt: data.pendingAt,
|
|
19957
|
+
scheduledAt: data.scheduledAt,
|
|
19744
19958
|
backtest,
|
|
19745
19959
|
});
|
|
19746
19960
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19773,6 +19987,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19773
19987
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19774
19988
|
partialExecuted: data.partialExecuted,
|
|
19775
19989
|
note: data.note,
|
|
19990
|
+
pendingAt: data.pendingAt,
|
|
19991
|
+
scheduledAt: data.scheduledAt,
|
|
19776
19992
|
backtest,
|
|
19777
19993
|
});
|
|
19778
19994
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -20870,6 +21086,8 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
20870
21086
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
20871
21087
|
partialExecuted: data.partialExecuted,
|
|
20872
21088
|
note: data.note,
|
|
21089
|
+
pendingAt: data.pendingAt,
|
|
21090
|
+
scheduledAt: data.scheduledAt,
|
|
20873
21091
|
backtest,
|
|
20874
21092
|
});
|
|
20875
21093
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -23255,9 +23473,15 @@ class HeatReportService {
|
|
|
23255
23473
|
signalId: data.signal?.id,
|
|
23256
23474
|
position: data.signal?.position,
|
|
23257
23475
|
note: data.signal?.note,
|
|
23476
|
+
priceOpen: data.signal?.priceOpen,
|
|
23477
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
23478
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
23479
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
23480
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
23258
23481
|
pnl: data.pnl.pnlPercentage,
|
|
23259
23482
|
closeReason: data.closeReason,
|
|
23260
23483
|
openTime: data.signal?.pendingAt,
|
|
23484
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
23261
23485
|
closeTime: data.closeTimestamp,
|
|
23262
23486
|
}, {
|
|
23263
23487
|
symbol: data.symbol,
|
|
@@ -23666,6 +23890,8 @@ class RiskReportService {
|
|
|
23666
23890
|
originalPriceStopLoss: data.currentSignal?.originalPriceStopLoss,
|
|
23667
23891
|
partialExecuted: data.currentSignal?.partialExecuted,
|
|
23668
23892
|
note: data.currentSignal?.note,
|
|
23893
|
+
pendingAt: data.currentSignal?.pendingAt,
|
|
23894
|
+
scheduledAt: data.currentSignal?.scheduledAt,
|
|
23669
23895
|
minuteEstimatedTime: data.currentSignal?.minuteEstimatedTime,
|
|
23670
23896
|
}, {
|
|
23671
23897
|
symbol: data.symbol,
|
|
@@ -25608,6 +25834,7 @@ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
|
25608
25834
|
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
25609
25835
|
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
25610
25836
|
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
25837
|
+
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
25611
25838
|
/**
|
|
25612
25839
|
* Checks if trade context is active (execution and method contexts).
|
|
25613
25840
|
*
|
|
@@ -25912,6 +26139,30 @@ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
|
|
|
25912
26139
|
}
|
|
25913
26140
|
return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
|
|
25914
26141
|
}
|
|
26142
|
+
/**
|
|
26143
|
+
* Fetches the set of candles after current time based on execution context.
|
|
26144
|
+
*
|
|
26145
|
+
* Uses the exchange's getNextCandles implementation to retrieve candles
|
|
26146
|
+
* that occur after the current context time.
|
|
26147
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26148
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
26149
|
+
* @param limit - Number of candles to fetch
|
|
26150
|
+
* @returns Promise resolving to array of candle data
|
|
26151
|
+
*/
|
|
26152
|
+
async function getNextCandles(symbol, interval, limit) {
|
|
26153
|
+
bt.loggerService.info(GET_NEXT_CANDLES_METHOD_NAME, {
|
|
26154
|
+
symbol,
|
|
26155
|
+
interval,
|
|
26156
|
+
limit,
|
|
26157
|
+
});
|
|
26158
|
+
if (!ExecutionContextService.hasContext()) {
|
|
26159
|
+
throw new Error("getNextCandles requires an execution context");
|
|
26160
|
+
}
|
|
26161
|
+
if (!MethodContextService.hasContext()) {
|
|
26162
|
+
throw new Error("getNextCandles requires a method context");
|
|
26163
|
+
}
|
|
26164
|
+
return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
26165
|
+
}
|
|
25915
26166
|
|
|
25916
26167
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
25917
26168
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
@@ -32845,6 +33096,16 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
|
32845
33096
|
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
32846
33097
|
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
32847
33098
|
const MS_PER_MINUTE = 60000;
|
|
33099
|
+
/**
|
|
33100
|
+
* Gets current timestamp from execution context if available.
|
|
33101
|
+
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
33102
|
+
*/
|
|
33103
|
+
const GET_TIMESTAMP_FN = async () => {
|
|
33104
|
+
if (ExecutionContextService.hasContext()) {
|
|
33105
|
+
return new Date(bt.executionContextService.context.when);
|
|
33106
|
+
}
|
|
33107
|
+
return new Date();
|
|
33108
|
+
};
|
|
32848
33109
|
/**
|
|
32849
33110
|
* Gets backtest mode flag from execution context if available.
|
|
32850
33111
|
* Returns false if no execution context exists (live mode).
|
|
@@ -32901,6 +33162,27 @@ const INTERVAL_MINUTES$1 = {
|
|
|
32901
33162
|
"6h": 360,
|
|
32902
33163
|
"8h": 480,
|
|
32903
33164
|
};
|
|
33165
|
+
/**
|
|
33166
|
+
* Aligns timestamp down to the nearest interval boundary.
|
|
33167
|
+
* For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
|
|
33168
|
+
*
|
|
33169
|
+
* Candle timestamp convention:
|
|
33170
|
+
* - Candle timestamp = openTime (when candle opens)
|
|
33171
|
+
* - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
|
|
33172
|
+
*
|
|
33173
|
+
* Adapter contract:
|
|
33174
|
+
* - Adapter must return candles with timestamp = openTime
|
|
33175
|
+
* - First returned candle.timestamp must equal aligned since
|
|
33176
|
+
* - Adapter must return exactly `limit` candles
|
|
33177
|
+
*
|
|
33178
|
+
* @param timestamp - Timestamp in milliseconds
|
|
33179
|
+
* @param intervalMinutes - Interval in minutes
|
|
33180
|
+
* @returns Aligned timestamp rounded down to interval boundary
|
|
33181
|
+
*/
|
|
33182
|
+
const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
|
|
33183
|
+
const intervalMs = intervalMinutes * MS_PER_MINUTE;
|
|
33184
|
+
return Math.floor(timestamp / intervalMs) * intervalMs;
|
|
33185
|
+
};
|
|
32904
33186
|
/**
|
|
32905
33187
|
* Creates exchange instance with methods resolved once during construction.
|
|
32906
33188
|
* Applies default implementations where schema methods are not provided.
|
|
@@ -32922,18 +33204,24 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
|
32922
33204
|
};
|
|
32923
33205
|
/**
|
|
32924
33206
|
* Attempts to read candles from cache.
|
|
32925
|
-
*
|
|
33207
|
+
*
|
|
33208
|
+
* Cache lookup calculates expected timestamps:
|
|
33209
|
+
* sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33210
|
+
* Returns all candles if found, null if any missing.
|
|
32926
33211
|
*
|
|
32927
33212
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32928
|
-
* @param sinceTimestamp -
|
|
32929
|
-
* @param untilTimestamp -
|
|
33213
|
+
* @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
|
|
33214
|
+
* @param untilTimestamp - Unused, kept for API compatibility
|
|
32930
33215
|
* @param exchangeName - Exchange name
|
|
32931
|
-
* @returns Cached candles array or null if cache miss
|
|
33216
|
+
* @returns Cached candles array (exactly limit) or null if cache miss
|
|
32932
33217
|
*/
|
|
32933
33218
|
const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
33219
|
+
// PersistCandleAdapter.readCandlesData calculates expected timestamps:
|
|
33220
|
+
// sinceTimestamp + i * stepMs for i = 0..limit-1
|
|
33221
|
+
// Returns all candles if found, null if any missing
|
|
32934
33222
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
32935
33223
|
// Return cached data only if we have exactly the requested limit
|
|
32936
|
-
if (cachedCandles
|
|
33224
|
+
if (cachedCandles?.length === dto.limit) {
|
|
32937
33225
|
bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
|
|
32938
33226
|
return cachedCandles;
|
|
32939
33227
|
}
|
|
@@ -32955,7 +33243,12 @@ const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestam
|
|
|
32955
33243
|
/**
|
|
32956
33244
|
* Writes candles to cache with error handling.
|
|
32957
33245
|
*
|
|
32958
|
-
*
|
|
33246
|
+
* The candles passed to this function should be validated:
|
|
33247
|
+
* - First candle.timestamp equals aligned sinceTimestamp (openTime)
|
|
33248
|
+
* - Exact number of candles as requested (limit)
|
|
33249
|
+
* - Sequential timestamps: sinceTimestamp + i * stepMs
|
|
33250
|
+
*
|
|
33251
|
+
* @param candles - Array of validated candle data to cache
|
|
32959
33252
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32960
33253
|
* @param exchangeName - Exchange name
|
|
32961
33254
|
*/
|
|
@@ -33026,14 +33319,18 @@ class ExchangeInstance {
|
|
|
33026
33319
|
});
|
|
33027
33320
|
const getCandles = this._methods.getCandles;
|
|
33028
33321
|
const step = INTERVAL_MINUTES$1[interval];
|
|
33029
|
-
|
|
33030
|
-
|
|
33031
|
-
|
|
33032
|
-
|
|
33033
|
-
|
|
33034
|
-
const
|
|
33035
|
-
const
|
|
33036
|
-
const
|
|
33322
|
+
if (!step) {
|
|
33323
|
+
throw new Error(`ExchangeInstance unknown interval=${interval}`);
|
|
33324
|
+
}
|
|
33325
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33326
|
+
// Align when down to interval boundary
|
|
33327
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33328
|
+
const whenTimestamp = when.getTime();
|
|
33329
|
+
const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
|
|
33330
|
+
// Calculate since: go back limit candles from aligned when
|
|
33331
|
+
const sinceTimestamp = alignedWhen - limit * stepMs;
|
|
33332
|
+
const since = new Date(sinceTimestamp);
|
|
33333
|
+
const untilTimestamp = alignedWhen;
|
|
33037
33334
|
// Try to read from cache first
|
|
33038
33335
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33039
33336
|
if (cachedCandles !== null) {
|
|
@@ -33052,7 +33349,7 @@ class ExchangeInstance {
|
|
|
33052
33349
|
remaining -= chunkLimit;
|
|
33053
33350
|
if (remaining > 0) {
|
|
33054
33351
|
// Move currentSince forward by the number of candles fetched
|
|
33055
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
33352
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33056
33353
|
}
|
|
33057
33354
|
}
|
|
33058
33355
|
}
|
|
@@ -33060,17 +33357,25 @@ class ExchangeInstance {
|
|
|
33060
33357
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33061
33358
|
allData = await getCandles(symbol, interval, since, limit, isBacktest);
|
|
33062
33359
|
}
|
|
33063
|
-
// Filter candles to strictly match the requested range
|
|
33064
|
-
const whenTimestamp = when.getTime();
|
|
33065
|
-
const stepMs = step * 60 * 1000;
|
|
33066
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp && candle.timestamp < whenTimestamp + stepMs);
|
|
33067
33360
|
// Apply distinct by timestamp to remove duplicates
|
|
33068
|
-
const uniqueData = Array.from(new Map(
|
|
33069
|
-
if (
|
|
33070
|
-
bt.loggerService.warn(`ExchangeInstance Removed ${
|
|
33361
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
33362
|
+
if (allData.length !== uniqueData.length) {
|
|
33363
|
+
bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
33071
33364
|
}
|
|
33072
|
-
|
|
33073
|
-
|
|
33365
|
+
// Validate adapter returned data
|
|
33366
|
+
if (uniqueData.length === 0) {
|
|
33367
|
+
throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
|
|
33368
|
+
`Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33369
|
+
}
|
|
33370
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
33371
|
+
throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
|
|
33372
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
33373
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
33374
|
+
}
|
|
33375
|
+
if (uniqueData.length !== limit) {
|
|
33376
|
+
throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
|
|
33377
|
+
`Expected ${limit} candles, got ${uniqueData.length}. ` +
|
|
33378
|
+
`Adapter must return exact number of candles requested.`);
|
|
33074
33379
|
}
|
|
33075
33380
|
// Write to cache after successful fetch
|
|
33076
33381
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
|
|
@@ -33193,8 +33498,8 @@ class ExchangeInstance {
|
|
|
33193
33498
|
symbol,
|
|
33194
33499
|
depth,
|
|
33195
33500
|
});
|
|
33196
|
-
const to =
|
|
33197
|
-
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES *
|
|
33501
|
+
const to = await GET_TIMESTAMP_FN();
|
|
33502
|
+
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
|
|
33198
33503
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33199
33504
|
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
33200
33505
|
};
|
|
@@ -33241,9 +33546,11 @@ class ExchangeInstance {
|
|
|
33241
33546
|
if (!step) {
|
|
33242
33547
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
33243
33548
|
}
|
|
33244
|
-
const
|
|
33549
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33550
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33551
|
+
const nowTimestamp = when.getTime();
|
|
33552
|
+
const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
|
|
33245
33553
|
let sinceTimestamp;
|
|
33246
|
-
let untilTimestamp;
|
|
33247
33554
|
let calculatedLimit;
|
|
33248
33555
|
// Case 1: all three parameters provided
|
|
33249
33556
|
if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
|
|
@@ -33253,8 +33560,8 @@ class ExchangeInstance {
|
|
|
33253
33560
|
if (eDate > nowTimestamp) {
|
|
33254
33561
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33255
33562
|
}
|
|
33256
|
-
|
|
33257
|
-
|
|
33563
|
+
// Align sDate down to interval boundary
|
|
33564
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33258
33565
|
calculatedLimit = limit;
|
|
33259
33566
|
}
|
|
33260
33567
|
// Case 2: sDate + eDate (no limit) - calculate limit from date range
|
|
@@ -33265,9 +33572,10 @@ class ExchangeInstance {
|
|
|
33265
33572
|
if (eDate > nowTimestamp) {
|
|
33266
33573
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33267
33574
|
}
|
|
33268
|
-
|
|
33269
|
-
|
|
33270
|
-
|
|
33575
|
+
// Align sDate down to interval boundary
|
|
33576
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33577
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
33578
|
+
calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
|
|
33271
33579
|
if (calculatedLimit <= 0) {
|
|
33272
33580
|
throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
|
|
33273
33581
|
}
|
|
@@ -33277,23 +33585,24 @@ class ExchangeInstance {
|
|
|
33277
33585
|
if (eDate > nowTimestamp) {
|
|
33278
33586
|
throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33279
33587
|
}
|
|
33280
|
-
|
|
33281
|
-
|
|
33588
|
+
// Align eDate down and calculate sinceTimestamp
|
|
33589
|
+
const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
|
|
33590
|
+
sinceTimestamp = alignedEDate - limit * stepMs;
|
|
33282
33591
|
calculatedLimit = limit;
|
|
33283
33592
|
}
|
|
33284
33593
|
// Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
|
|
33285
33594
|
else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
|
|
33286
|
-
|
|
33287
|
-
|
|
33288
|
-
|
|
33289
|
-
|
|
33595
|
+
// Align sDate down to interval boundary
|
|
33596
|
+
sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
|
|
33597
|
+
const endTimestamp = sinceTimestamp + limit * stepMs;
|
|
33598
|
+
if (endTimestamp > nowTimestamp) {
|
|
33599
|
+
throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
|
|
33290
33600
|
}
|
|
33291
33601
|
calculatedLimit = limit;
|
|
33292
33602
|
}
|
|
33293
33603
|
// Case 5: Only limit - use Date.now() as reference (backward)
|
|
33294
33604
|
else if (sDate === undefined && eDate === undefined && limit !== undefined) {
|
|
33295
|
-
|
|
33296
|
-
sinceTimestamp = nowTimestamp - limit * step * MS_PER_MINUTE;
|
|
33605
|
+
sinceTimestamp = alignedNow - limit * stepMs;
|
|
33297
33606
|
calculatedLimit = limit;
|
|
33298
33607
|
}
|
|
33299
33608
|
// Invalid: no parameters or only sDate or only eDate
|
|
@@ -33303,6 +33612,7 @@ class ExchangeInstance {
|
|
|
33303
33612
|
`Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
|
|
33304
33613
|
}
|
|
33305
33614
|
// Try to read from cache first
|
|
33615
|
+
const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
|
|
33306
33616
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33307
33617
|
if (cachedCandles !== null) {
|
|
33308
33618
|
return cachedCandles;
|
|
@@ -33321,23 +33631,32 @@ class ExchangeInstance {
|
|
|
33321
33631
|
allData.push(...chunkData);
|
|
33322
33632
|
remaining -= chunkLimit;
|
|
33323
33633
|
if (remaining > 0) {
|
|
33324
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit *
|
|
33634
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
|
|
33325
33635
|
}
|
|
33326
33636
|
}
|
|
33327
33637
|
}
|
|
33328
33638
|
else {
|
|
33329
33639
|
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
33330
33640
|
}
|
|
33331
|
-
// Filter candles to strictly match the requested range
|
|
33332
|
-
const filteredData = allData.filter((candle) => candle.timestamp >= sinceTimestamp &&
|
|
33333
|
-
candle.timestamp < untilTimestamp);
|
|
33334
33641
|
// Apply distinct by timestamp to remove duplicates
|
|
33335
|
-
const uniqueData = Array.from(new Map(
|
|
33336
|
-
if (
|
|
33337
|
-
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${
|
|
33642
|
+
const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
|
|
33643
|
+
if (allData.length !== uniqueData.length) {
|
|
33644
|
+
bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
|
|
33645
|
+
}
|
|
33646
|
+
// Validate adapter returned data
|
|
33647
|
+
if (uniqueData.length === 0) {
|
|
33648
|
+
throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
|
|
33649
|
+
`Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
|
|
33650
|
+
}
|
|
33651
|
+
if (uniqueData[0].timestamp !== sinceTimestamp) {
|
|
33652
|
+
throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
|
|
33653
|
+
`Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
|
|
33654
|
+
`Adapter must return candles with timestamp=openTime, starting from aligned since.`);
|
|
33338
33655
|
}
|
|
33339
|
-
if (uniqueData.length
|
|
33340
|
-
|
|
33656
|
+
if (uniqueData.length !== calculatedLimit) {
|
|
33657
|
+
throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
|
|
33658
|
+
`Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
|
|
33659
|
+
`Adapter must return exact number of candles requested.`);
|
|
33341
33660
|
}
|
|
33342
33661
|
// Write to cache after successful fetch
|
|
33343
33662
|
await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
|
|
@@ -34933,4 +35252,4 @@ const set = (object, path, value) => {
|
|
|
34933
35252
|
}
|
|
34934
35253
|
};
|
|
34935
35254
|
|
|
34936
|
-
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
|
|
35255
|
+
export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate };
|