backtest-kit 2.2.26 → 2.3.1
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 +86 -3
- package/build/index.cjs +241 -28
- package/build/index.mjs +241 -29
- package/package.json +1 -1
- package/types.d.ts +40 -4
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";
|
|
@@ -1566,12 +1580,17 @@ class PersistCandleUtils {
|
|
|
1566
1580
|
* Reads cached candles for a specific exchange, symbol, and interval.
|
|
1567
1581
|
* Returns candles only if cache contains exactly the requested limit.
|
|
1568
1582
|
*
|
|
1583
|
+
* Boundary semantics (EXCLUSIVE):
|
|
1584
|
+
* - sinceTimestamp: candle.timestamp must be > sinceTimestamp
|
|
1585
|
+
* - untilTimestamp: candle.timestamp + stepMs must be < untilTimestamp
|
|
1586
|
+
* - Only fully closed candles within the exclusive range are returned
|
|
1587
|
+
*
|
|
1569
1588
|
* @param symbol - Trading pair symbol
|
|
1570
1589
|
* @param interval - Candle interval
|
|
1571
1590
|
* @param exchangeName - Exchange identifier
|
|
1572
1591
|
* @param limit - Number of candles requested
|
|
1573
|
-
* @param sinceTimestamp -
|
|
1574
|
-
* @param untilTimestamp -
|
|
1592
|
+
* @param sinceTimestamp - Exclusive start timestamp in milliseconds
|
|
1593
|
+
* @param untilTimestamp - Exclusive end timestamp in milliseconds
|
|
1575
1594
|
* @returns Promise resolving to array of candles or null if cache is incomplete
|
|
1576
1595
|
*/
|
|
1577
1596
|
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
|
|
@@ -1587,11 +1606,15 @@ class PersistCandleUtils {
|
|
|
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
|
+
// Collect all cached candles within the time range using EXCLUSIVE boundaries
|
|
1591
1611
|
const cachedCandles = [];
|
|
1592
1612
|
for await (const timestamp of stateStorage.keys()) {
|
|
1593
1613
|
const ts = Number(timestamp);
|
|
1594
|
-
|
|
1614
|
+
// EXCLUSIVE boundaries:
|
|
1615
|
+
// - candle.timestamp > sinceTimestamp
|
|
1616
|
+
// - candle.timestamp + stepMs < untilTimestamp (fully closed before untilTimestamp)
|
|
1617
|
+
if (ts > sinceTimestamp && ts + stepMs < untilTimestamp) {
|
|
1595
1618
|
try {
|
|
1596
1619
|
const candle = await stateStorage.readValue(timestamp);
|
|
1597
1620
|
cachedCandles.push(candle);
|
|
@@ -1617,7 +1640,11 @@ class PersistCandleUtils {
|
|
|
1617
1640
|
* Writes candles to cache with atomic file writes.
|
|
1618
1641
|
* Each candle is stored as a separate JSON file named by its timestamp.
|
|
1619
1642
|
*
|
|
1620
|
-
*
|
|
1643
|
+
* The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
|
|
1644
|
+
* - candle.timestamp > sinceTimestamp
|
|
1645
|
+
* - candle.timestamp + stepMs < untilTimestamp
|
|
1646
|
+
*
|
|
1647
|
+
* @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
|
|
1621
1648
|
* @param symbol - Trading pair symbol
|
|
1622
1649
|
* @param interval - Candle interval
|
|
1623
1650
|
* @param exchangeName - Exchange identifier
|
|
@@ -1858,13 +1885,20 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
|
1858
1885
|
* Attempts to read candles from cache.
|
|
1859
1886
|
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
1860
1887
|
*
|
|
1888
|
+
* Boundary semantics:
|
|
1889
|
+
* - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
|
|
1890
|
+
* - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
|
|
1891
|
+
* - Only fully closed candles within the exclusive range are returned
|
|
1892
|
+
*
|
|
1861
1893
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1862
|
-
* @param sinceTimestamp -
|
|
1863
|
-
* @param untilTimestamp -
|
|
1894
|
+
* @param sinceTimestamp - Exclusive start timestamp in milliseconds
|
|
1895
|
+
* @param untilTimestamp - Exclusive end timestamp in milliseconds
|
|
1864
1896
|
* @param self - Instance of ClientExchange
|
|
1865
1897
|
* @returns Cached candles array or null if cache miss or inconsistent
|
|
1866
1898
|
*/
|
|
1867
1899
|
const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
|
|
1900
|
+
// PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
|
|
1901
|
+
// Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
|
|
1868
1902
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
1869
1903
|
// Return cached data only if we have exactly the requested limit
|
|
1870
1904
|
if (cachedCandles.length === dto.limit) {
|
|
@@ -1889,7 +1923,11 @@ const READ_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(async (dto, sinceTimestamp
|
|
|
1889
1923
|
/**
|
|
1890
1924
|
* Writes candles to cache with error handling.
|
|
1891
1925
|
*
|
|
1892
|
-
*
|
|
1926
|
+
* The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
|
|
1927
|
+
* - candle.timestamp > sinceTimestamp
|
|
1928
|
+
* - candle.timestamp + stepMs < untilTimestamp
|
|
1929
|
+
*
|
|
1930
|
+
* @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
|
|
1893
1931
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1894
1932
|
* @param self - Instance of ClientExchange
|
|
1895
1933
|
*/
|
|
@@ -2055,8 +2093,18 @@ class ClientExchange {
|
|
|
2055
2093
|
// Filter candles to strictly match the requested range
|
|
2056
2094
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2057
2095
|
const sinceTimestamp = since.getTime();
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2096
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2097
|
+
const filteredData = allData.filter((candle) => {
|
|
2098
|
+
// EXCLUSIVE boundaries:
|
|
2099
|
+
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
2100
|
+
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
2101
|
+
if (candle.timestamp <= sinceTimestamp) {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
// Check against current time (when)
|
|
2105
|
+
// Only allow candles that have fully CLOSED before "when"
|
|
2106
|
+
return candle.timestamp + stepMs < whenTimestamp;
|
|
2107
|
+
});
|
|
2060
2108
|
// Apply distinct by timestamp to remove duplicates
|
|
2061
2109
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
2062
2110
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -2088,6 +2136,9 @@ class ClientExchange {
|
|
|
2088
2136
|
interval,
|
|
2089
2137
|
limit,
|
|
2090
2138
|
});
|
|
2139
|
+
if (!this.params.execution.context.backtest) {
|
|
2140
|
+
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2141
|
+
}
|
|
2091
2142
|
const since = new Date(this.params.execution.context.when.getTime());
|
|
2092
2143
|
const now = Date.now();
|
|
2093
2144
|
// Вычисляем конечное время запроса
|
|
@@ -2118,7 +2169,9 @@ class ClientExchange {
|
|
|
2118
2169
|
}
|
|
2119
2170
|
// Filter candles to strictly match the requested range
|
|
2120
2171
|
const sinceTimestamp = since.getTime();
|
|
2121
|
-
const
|
|
2172
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2173
|
+
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2174
|
+
candle.timestamp + stepMs < endTime);
|
|
2122
2175
|
// Apply distinct by timestamp to remove duplicates
|
|
2123
2176
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
2124
2177
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -2319,8 +2372,10 @@ class ClientExchange {
|
|
|
2319
2372
|
allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
|
|
2320
2373
|
}
|
|
2321
2374
|
// Filter candles to strictly match the requested range
|
|
2322
|
-
|
|
2323
|
-
|
|
2375
|
+
// Only include candles that have fully CLOSED before untilTimestamp
|
|
2376
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2377
|
+
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2378
|
+
candle.timestamp + stepMs < untilTimestamp);
|
|
2324
2379
|
// Apply distinct by timestamp to remove duplicates
|
|
2325
2380
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
2326
2381
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -12288,6 +12343,13 @@ class WalkerSchemaService {
|
|
|
12288
12343
|
}
|
|
12289
12344
|
}
|
|
12290
12345
|
|
|
12346
|
+
/**
|
|
12347
|
+
* Компенсация для exclusive boundaries при фильтрации свечей.
|
|
12348
|
+
* ClientExchange.getNextCandles использует фильтр:
|
|
12349
|
+
* timestamp > since && timestamp + stepMs < endTime
|
|
12350
|
+
* который исключает первую и последнюю свечи из запрошенного диапазона.
|
|
12351
|
+
*/
|
|
12352
|
+
const CANDLE_EXCLUSIVE_BOUNDARY_OFFSET = 2;
|
|
12291
12353
|
/**
|
|
12292
12354
|
* Private service for backtest orchestration using async generators.
|
|
12293
12355
|
*
|
|
@@ -12553,7 +12615,7 @@ class BacktestLogicPrivateService {
|
|
|
12553
12615
|
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
12554
12616
|
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
12555
12617
|
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
12556
|
-
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
12618
|
+
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT + CANDLE_EXCLUSIVE_BOUNDARY_OFFSET;
|
|
12557
12619
|
let candles;
|
|
12558
12620
|
try {
|
|
12559
12621
|
candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
@@ -13839,6 +13901,18 @@ const live_columns = [
|
|
|
13839
13901
|
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
13840
13902
|
isVisible: () => true,
|
|
13841
13903
|
},
|
|
13904
|
+
{
|
|
13905
|
+
key: "pendingAt",
|
|
13906
|
+
label: "Pending At",
|
|
13907
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
13908
|
+
isVisible: () => true,
|
|
13909
|
+
},
|
|
13910
|
+
{
|
|
13911
|
+
key: "scheduledAt",
|
|
13912
|
+
label: "Scheduled At",
|
|
13913
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
13914
|
+
isVisible: () => true,
|
|
13915
|
+
},
|
|
13842
13916
|
];
|
|
13843
13917
|
|
|
13844
13918
|
/**
|
|
@@ -13963,6 +14037,18 @@ const partial_columns = [
|
|
|
13963
14037
|
format: (data) => data.note || "",
|
|
13964
14038
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
13965
14039
|
},
|
|
14040
|
+
{
|
|
14041
|
+
key: "pendingAt",
|
|
14042
|
+
label: "Pending At",
|
|
14043
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14044
|
+
isVisible: () => true,
|
|
14045
|
+
},
|
|
14046
|
+
{
|
|
14047
|
+
key: "scheduledAt",
|
|
14048
|
+
label: "Scheduled At",
|
|
14049
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14050
|
+
isVisible: () => true,
|
|
14051
|
+
},
|
|
13966
14052
|
{
|
|
13967
14053
|
key: "timestamp",
|
|
13968
14054
|
label: "Timestamp",
|
|
@@ -14085,6 +14171,18 @@ const breakeven_columns = [
|
|
|
14085
14171
|
format: (data) => data.note || "",
|
|
14086
14172
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
14087
14173
|
},
|
|
14174
|
+
{
|
|
14175
|
+
key: "pendingAt",
|
|
14176
|
+
label: "Pending At",
|
|
14177
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14178
|
+
isVisible: () => true,
|
|
14179
|
+
},
|
|
14180
|
+
{
|
|
14181
|
+
key: "scheduledAt",
|
|
14182
|
+
label: "Scheduled At",
|
|
14183
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14184
|
+
isVisible: () => true,
|
|
14185
|
+
},
|
|
14088
14186
|
{
|
|
14089
14187
|
key: "timestamp",
|
|
14090
14188
|
label: "Timestamp",
|
|
@@ -14363,6 +14461,22 @@ const risk_columns = [
|
|
|
14363
14461
|
format: (data) => data.rejectionNote,
|
|
14364
14462
|
isVisible: () => true,
|
|
14365
14463
|
},
|
|
14464
|
+
{
|
|
14465
|
+
key: "pendingAt",
|
|
14466
|
+
label: "Pending At",
|
|
14467
|
+
format: (data) => data.currentSignal.pendingAt !== undefined
|
|
14468
|
+
? new Date(data.currentSignal.pendingAt).toISOString()
|
|
14469
|
+
: "N/A",
|
|
14470
|
+
isVisible: () => true,
|
|
14471
|
+
},
|
|
14472
|
+
{
|
|
14473
|
+
key: "scheduledAt",
|
|
14474
|
+
label: "Scheduled At",
|
|
14475
|
+
format: (data) => data.currentSignal.scheduledAt !== undefined
|
|
14476
|
+
? new Date(data.currentSignal.scheduledAt).toISOString()
|
|
14477
|
+
: "N/A",
|
|
14478
|
+
isVisible: () => true,
|
|
14479
|
+
},
|
|
14366
14480
|
{
|
|
14367
14481
|
key: "timestamp",
|
|
14368
14482
|
label: "Timestamp",
|
|
@@ -14513,6 +14627,18 @@ const schedule_columns = [
|
|
|
14513
14627
|
format: (data) => data.cancelId ?? "N/A",
|
|
14514
14628
|
isVisible: () => true,
|
|
14515
14629
|
},
|
|
14630
|
+
{
|
|
14631
|
+
key: "pendingAt",
|
|
14632
|
+
label: "Pending At",
|
|
14633
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
14634
|
+
isVisible: () => true,
|
|
14635
|
+
},
|
|
14636
|
+
{
|
|
14637
|
+
key: "scheduledAt",
|
|
14638
|
+
label: "Scheduled At",
|
|
14639
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
14640
|
+
isVisible: () => true,
|
|
14641
|
+
},
|
|
14516
14642
|
];
|
|
14517
14643
|
|
|
14518
14644
|
/**
|
|
@@ -15904,6 +16030,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15904
16030
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
15905
16031
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
15906
16032
|
partialExecuted: data.signal.partialExecuted,
|
|
16033
|
+
pendingAt: data.signal.pendingAt,
|
|
16034
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15907
16035
|
});
|
|
15908
16036
|
// Trim queue if exceeded MAX_EVENTS
|
|
15909
16037
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -15934,6 +16062,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15934
16062
|
percentTp: data.percentTp,
|
|
15935
16063
|
percentSl: data.percentSl,
|
|
15936
16064
|
pnl: data.pnl.pnlPercentage,
|
|
16065
|
+
pendingAt: data.signal.pendingAt,
|
|
16066
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15937
16067
|
};
|
|
15938
16068
|
// Find the last active event with the same signalId
|
|
15939
16069
|
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
@@ -15974,6 +16104,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15974
16104
|
pnl: data.pnl.pnlPercentage,
|
|
15975
16105
|
closeReason: data.closeReason,
|
|
15976
16106
|
duration: durationMin,
|
|
16107
|
+
pendingAt: data.signal.pendingAt,
|
|
16108
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15977
16109
|
};
|
|
15978
16110
|
this._eventList.unshift(newEvent);
|
|
15979
16111
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -16001,6 +16133,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16001
16133
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
16002
16134
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16003
16135
|
partialExecuted: data.signal.partialExecuted,
|
|
16136
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16004
16137
|
});
|
|
16005
16138
|
// Trim queue if exceeded MAX_EVENTS
|
|
16006
16139
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16031,6 +16164,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16031
16164
|
percentTp: data.percentTp,
|
|
16032
16165
|
percentSl: data.percentSl,
|
|
16033
16166
|
pnl: data.pnl.pnlPercentage,
|
|
16167
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16034
16168
|
};
|
|
16035
16169
|
// Find the last waiting event with the same signalId
|
|
16036
16170
|
const lastWaitingIndex = this._eventList.findLastIndex((event) => event.action === "waiting" && event.signalId === data.signal.id);
|
|
@@ -16067,6 +16201,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16067
16201
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16068
16202
|
partialExecuted: data.signal.partialExecuted,
|
|
16069
16203
|
cancelReason: data.reason,
|
|
16204
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16070
16205
|
});
|
|
16071
16206
|
// Trim queue if exceeded MAX_EVENTS
|
|
16072
16207
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16558,6 +16693,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16558
16693
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
16559
16694
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16560
16695
|
partialExecuted: data.signal.partialExecuted,
|
|
16696
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16561
16697
|
});
|
|
16562
16698
|
// Trim queue if exceeded MAX_EVENTS
|
|
16563
16699
|
if (this._eventList.length > MAX_EVENTS$6) {
|
|
@@ -16587,6 +16723,8 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16587
16723
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16588
16724
|
partialExecuted: data.signal.partialExecuted,
|
|
16589
16725
|
duration: durationMin,
|
|
16726
|
+
pendingAt: data.signal.pendingAt,
|
|
16727
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16590
16728
|
};
|
|
16591
16729
|
this._eventList.unshift(newEvent);
|
|
16592
16730
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -16620,6 +16758,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16620
16758
|
duration: durationMin,
|
|
16621
16759
|
cancelReason: data.reason,
|
|
16622
16760
|
cancelId: data.cancelId,
|
|
16761
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16623
16762
|
};
|
|
16624
16763
|
this._eventList.unshift(newEvent);
|
|
16625
16764
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19761,6 +19900,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19761
19900
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19762
19901
|
partialExecuted: data.partialExecuted,
|
|
19763
19902
|
note: data.note,
|
|
19903
|
+
pendingAt: data.pendingAt,
|
|
19904
|
+
scheduledAt: data.scheduledAt,
|
|
19764
19905
|
backtest,
|
|
19765
19906
|
});
|
|
19766
19907
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19793,6 +19934,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19793
19934
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19794
19935
|
partialExecuted: data.partialExecuted,
|
|
19795
19936
|
note: data.note,
|
|
19937
|
+
pendingAt: data.pendingAt,
|
|
19938
|
+
scheduledAt: data.scheduledAt,
|
|
19796
19939
|
backtest,
|
|
19797
19940
|
});
|
|
19798
19941
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -20890,6 +21033,8 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
20890
21033
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
20891
21034
|
partialExecuted: data.partialExecuted,
|
|
20892
21035
|
note: data.note,
|
|
21036
|
+
pendingAt: data.pendingAt,
|
|
21037
|
+
scheduledAt: data.scheduledAt,
|
|
20893
21038
|
backtest,
|
|
20894
21039
|
});
|
|
20895
21040
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -23275,9 +23420,15 @@ class HeatReportService {
|
|
|
23275
23420
|
signalId: data.signal?.id,
|
|
23276
23421
|
position: data.signal?.position,
|
|
23277
23422
|
note: data.signal?.note,
|
|
23423
|
+
priceOpen: data.signal?.priceOpen,
|
|
23424
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
23425
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
23426
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
23427
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
23278
23428
|
pnl: data.pnl.pnlPercentage,
|
|
23279
23429
|
closeReason: data.closeReason,
|
|
23280
23430
|
openTime: data.signal?.pendingAt,
|
|
23431
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
23281
23432
|
closeTime: data.closeTimestamp,
|
|
23282
23433
|
}, {
|
|
23283
23434
|
symbol: data.symbol,
|
|
@@ -23686,6 +23837,8 @@ class RiskReportService {
|
|
|
23686
23837
|
originalPriceStopLoss: data.currentSignal?.originalPriceStopLoss,
|
|
23687
23838
|
partialExecuted: data.currentSignal?.partialExecuted,
|
|
23688
23839
|
note: data.currentSignal?.note,
|
|
23840
|
+
pendingAt: data.currentSignal?.pendingAt,
|
|
23841
|
+
scheduledAt: data.currentSignal?.scheduledAt,
|
|
23689
23842
|
minuteEstimatedTime: data.currentSignal?.minuteEstimatedTime,
|
|
23690
23843
|
}, {
|
|
23691
23844
|
symbol: data.symbol,
|
|
@@ -25628,6 +25781,7 @@ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
|
25628
25781
|
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
25629
25782
|
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
25630
25783
|
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
25784
|
+
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
25631
25785
|
/**
|
|
25632
25786
|
* Checks if trade context is active (execution and method contexts).
|
|
25633
25787
|
*
|
|
@@ -25932,6 +26086,30 @@ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
|
|
|
25932
26086
|
}
|
|
25933
26087
|
return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
|
|
25934
26088
|
}
|
|
26089
|
+
/**
|
|
26090
|
+
* Fetches the set of candles after current time based on execution context.
|
|
26091
|
+
*
|
|
26092
|
+
* Uses the exchange's getNextCandles implementation to retrieve candles
|
|
26093
|
+
* that occur after the current context time.
|
|
26094
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26095
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
26096
|
+
* @param limit - Number of candles to fetch
|
|
26097
|
+
* @returns Promise resolving to array of candle data
|
|
26098
|
+
*/
|
|
26099
|
+
async function getNextCandles(symbol, interval, limit) {
|
|
26100
|
+
bt.loggerService.info(GET_NEXT_CANDLES_METHOD_NAME, {
|
|
26101
|
+
symbol,
|
|
26102
|
+
interval,
|
|
26103
|
+
limit,
|
|
26104
|
+
});
|
|
26105
|
+
if (!ExecutionContextService.hasContext()) {
|
|
26106
|
+
throw new Error("getNextCandles requires an execution context");
|
|
26107
|
+
}
|
|
26108
|
+
if (!MethodContextService.hasContext()) {
|
|
26109
|
+
throw new Error("getNextCandles requires a method context");
|
|
26110
|
+
}
|
|
26111
|
+
return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
26112
|
+
}
|
|
25935
26113
|
|
|
25936
26114
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
25937
26115
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
@@ -32865,6 +33043,16 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
|
32865
33043
|
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
32866
33044
|
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
32867
33045
|
const MS_PER_MINUTE = 60000;
|
|
33046
|
+
/**
|
|
33047
|
+
* Gets current timestamp from execution context if available.
|
|
33048
|
+
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
33049
|
+
*/
|
|
33050
|
+
const GET_TIMESTAMP_FN = async () => {
|
|
33051
|
+
if (ExecutionContextService.hasContext()) {
|
|
33052
|
+
return new Date(bt.executionContextService.context.when);
|
|
33053
|
+
}
|
|
33054
|
+
return new Date();
|
|
33055
|
+
};
|
|
32868
33056
|
/**
|
|
32869
33057
|
* Gets backtest mode flag from execution context if available.
|
|
32870
33058
|
* Returns false if no execution context exists (live mode).
|
|
@@ -32944,13 +33132,20 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
|
32944
33132
|
* Attempts to read candles from cache.
|
|
32945
33133
|
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
32946
33134
|
*
|
|
33135
|
+
* Boundary semantics:
|
|
33136
|
+
* - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
|
|
33137
|
+
* - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
|
|
33138
|
+
* - Only fully closed candles within the exclusive range are returned
|
|
33139
|
+
*
|
|
32947
33140
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32948
|
-
* @param sinceTimestamp -
|
|
32949
|
-
* @param untilTimestamp -
|
|
33141
|
+
* @param sinceTimestamp - Exclusive start timestamp in milliseconds
|
|
33142
|
+
* @param untilTimestamp - Exclusive end timestamp in milliseconds
|
|
32950
33143
|
* @param exchangeName - Exchange name
|
|
32951
33144
|
* @returns Cached candles array or null if cache miss or inconsistent
|
|
32952
33145
|
*/
|
|
32953
33146
|
const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
33147
|
+
// PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
|
|
33148
|
+
// Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
|
|
32954
33149
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
32955
33150
|
// Return cached data only if we have exactly the requested limit
|
|
32956
33151
|
if (cachedCandles.length === dto.limit) {
|
|
@@ -32975,7 +33170,11 @@ const READ_CANDLES_CACHE_FN = functoolsKit.trycatch(async (dto, sinceTimestamp,
|
|
|
32975
33170
|
/**
|
|
32976
33171
|
* Writes candles to cache with error handling.
|
|
32977
33172
|
*
|
|
32978
|
-
*
|
|
33173
|
+
* The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
|
|
33174
|
+
* - candle.timestamp > sinceTimestamp
|
|
33175
|
+
* - candle.timestamp + stepMs < untilTimestamp
|
|
33176
|
+
*
|
|
33177
|
+
* @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
|
|
32979
33178
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32980
33179
|
* @param exchangeName - Exchange name
|
|
32981
33180
|
*/
|
|
@@ -33050,10 +33249,10 @@ class ExchangeInstance {
|
|
|
33050
33249
|
if (!adjust) {
|
|
33051
33250
|
throw new Error(`ExchangeInstance unknown time adjust for interval=${interval}`);
|
|
33052
33251
|
}
|
|
33053
|
-
const when =
|
|
33054
|
-
const since = new Date(when.getTime() - adjust *
|
|
33252
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33253
|
+
const since = new Date(when.getTime() - adjust * MS_PER_MINUTE);
|
|
33055
33254
|
const sinceTimestamp = since.getTime();
|
|
33056
|
-
const untilTimestamp = sinceTimestamp + limit * step *
|
|
33255
|
+
const untilTimestamp = sinceTimestamp + limit * step * MS_PER_MINUTE;
|
|
33057
33256
|
// Try to read from cache first
|
|
33058
33257
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33059
33258
|
if (cachedCandles !== null) {
|
|
@@ -33072,7 +33271,7 @@ class ExchangeInstance {
|
|
|
33072
33271
|
remaining -= chunkLimit;
|
|
33073
33272
|
if (remaining > 0) {
|
|
33074
33273
|
// Move currentSince forward by the number of candles fetched
|
|
33075
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit * step *
|
|
33274
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
|
|
33076
33275
|
}
|
|
33077
33276
|
}
|
|
33078
33277
|
}
|
|
@@ -33082,8 +33281,18 @@ class ExchangeInstance {
|
|
|
33082
33281
|
}
|
|
33083
33282
|
// Filter candles to strictly match the requested range
|
|
33084
33283
|
const whenTimestamp = when.getTime();
|
|
33085
|
-
const stepMs = step *
|
|
33086
|
-
const filteredData = allData.filter((candle) =>
|
|
33284
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33285
|
+
const filteredData = allData.filter((candle) => {
|
|
33286
|
+
// EXCLUSIVE boundaries:
|
|
33287
|
+
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
33288
|
+
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
33289
|
+
if (candle.timestamp <= sinceTimestamp) {
|
|
33290
|
+
return false;
|
|
33291
|
+
}
|
|
33292
|
+
// Check against current time (when)
|
|
33293
|
+
// Only allow candles that have fully CLOSED before "when"
|
|
33294
|
+
return candle.timestamp + stepMs < whenTimestamp;
|
|
33295
|
+
});
|
|
33087
33296
|
// Apply distinct by timestamp to remove duplicates
|
|
33088
33297
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
33089
33298
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -33213,8 +33422,8 @@ class ExchangeInstance {
|
|
|
33213
33422
|
symbol,
|
|
33214
33423
|
depth,
|
|
33215
33424
|
});
|
|
33216
|
-
const to =
|
|
33217
|
-
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES *
|
|
33425
|
+
const to = await GET_TIMESTAMP_FN();
|
|
33426
|
+
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
|
|
33218
33427
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33219
33428
|
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
33220
33429
|
};
|
|
@@ -33261,7 +33470,8 @@ class ExchangeInstance {
|
|
|
33261
33470
|
if (!step) {
|
|
33262
33471
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
33263
33472
|
}
|
|
33264
|
-
const
|
|
33473
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33474
|
+
const nowTimestamp = when.getTime();
|
|
33265
33475
|
let sinceTimestamp;
|
|
33266
33476
|
let untilTimestamp;
|
|
33267
33477
|
let calculatedLimit;
|
|
@@ -33349,8 +33559,10 @@ class ExchangeInstance {
|
|
|
33349
33559
|
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
33350
33560
|
}
|
|
33351
33561
|
// Filter candles to strictly match the requested range
|
|
33352
|
-
|
|
33353
|
-
|
|
33562
|
+
// Only include candles that have fully CLOSED before untilTimestamp
|
|
33563
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33564
|
+
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
33565
|
+
candle.timestamp + stepMs < untilTimestamp);
|
|
33354
33566
|
// Apply distinct by timestamp to remove duplicates
|
|
33355
33567
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
33356
33568
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -35018,6 +35230,7 @@ exports.getDefaultConfig = getDefaultConfig;
|
|
|
35018
35230
|
exports.getExchangeSchema = getExchangeSchema;
|
|
35019
35231
|
exports.getFrameSchema = getFrameSchema;
|
|
35020
35232
|
exports.getMode = getMode;
|
|
35233
|
+
exports.getNextCandles = getNextCandles;
|
|
35021
35234
|
exports.getOrderBook = getOrderBook;
|
|
35022
35235
|
exports.getRawCandles = getRawCandles;
|
|
35023
35236
|
exports.getRiskSchema = getRiskSchema;
|