backtest-kit 2.2.25 → 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 +242 -28
- package/build/index.mjs +242 -29
- package/package.json +1 -1
- package/types.d.ts +42 -4
package/build/index.mjs
CHANGED
|
@@ -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";
|
|
@@ -1546,12 +1560,17 @@ class PersistCandleUtils {
|
|
|
1546
1560
|
* Reads cached candles for a specific exchange, symbol, and interval.
|
|
1547
1561
|
* Returns candles only if cache contains exactly the requested limit.
|
|
1548
1562
|
*
|
|
1563
|
+
* Boundary semantics (EXCLUSIVE):
|
|
1564
|
+
* - sinceTimestamp: candle.timestamp must be > sinceTimestamp
|
|
1565
|
+
* - untilTimestamp: candle.timestamp + stepMs must be < untilTimestamp
|
|
1566
|
+
* - Only fully closed candles within the exclusive range are returned
|
|
1567
|
+
*
|
|
1549
1568
|
* @param symbol - Trading pair symbol
|
|
1550
1569
|
* @param interval - Candle interval
|
|
1551
1570
|
* @param exchangeName - Exchange identifier
|
|
1552
1571
|
* @param limit - Number of candles requested
|
|
1553
|
-
* @param sinceTimestamp -
|
|
1554
|
-
* @param untilTimestamp -
|
|
1572
|
+
* @param sinceTimestamp - Exclusive start timestamp in milliseconds
|
|
1573
|
+
* @param untilTimestamp - Exclusive end timestamp in milliseconds
|
|
1555
1574
|
* @returns Promise resolving to array of candles or null if cache is incomplete
|
|
1556
1575
|
*/
|
|
1557
1576
|
this.readCandlesData = async (symbol, interval, exchangeName, limit, sinceTimestamp, untilTimestamp) => {
|
|
@@ -1567,11 +1586,15 @@ class PersistCandleUtils {
|
|
|
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
|
+
// Collect all cached candles within the time range using EXCLUSIVE boundaries
|
|
1571
1591
|
const cachedCandles = [];
|
|
1572
1592
|
for await (const timestamp of stateStorage.keys()) {
|
|
1573
1593
|
const ts = Number(timestamp);
|
|
1574
|
-
|
|
1594
|
+
// EXCLUSIVE boundaries:
|
|
1595
|
+
// - candle.timestamp > sinceTimestamp
|
|
1596
|
+
// - candle.timestamp + stepMs < untilTimestamp (fully closed before untilTimestamp)
|
|
1597
|
+
if (ts > sinceTimestamp && ts + stepMs < untilTimestamp) {
|
|
1575
1598
|
try {
|
|
1576
1599
|
const candle = await stateStorage.readValue(timestamp);
|
|
1577
1600
|
cachedCandles.push(candle);
|
|
@@ -1597,7 +1620,11 @@ class PersistCandleUtils {
|
|
|
1597
1620
|
* Writes candles to cache with atomic file writes.
|
|
1598
1621
|
* Each candle is stored as a separate JSON file named by its timestamp.
|
|
1599
1622
|
*
|
|
1600
|
-
*
|
|
1623
|
+
* The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
|
|
1624
|
+
* - candle.timestamp > sinceTimestamp
|
|
1625
|
+
* - candle.timestamp + stepMs < untilTimestamp
|
|
1626
|
+
*
|
|
1627
|
+
* @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
|
|
1601
1628
|
* @param symbol - Trading pair symbol
|
|
1602
1629
|
* @param interval - Candle interval
|
|
1603
1630
|
* @param exchangeName - Exchange identifier
|
|
@@ -1838,13 +1865,20 @@ const VALIDATE_NO_INCOMPLETE_CANDLES_FN = (candles) => {
|
|
|
1838
1865
|
* Attempts to read candles from cache.
|
|
1839
1866
|
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
1840
1867
|
*
|
|
1868
|
+
* Boundary semantics:
|
|
1869
|
+
* - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
|
|
1870
|
+
* - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
|
|
1871
|
+
* - Only fully closed candles within the exclusive range are returned
|
|
1872
|
+
*
|
|
1841
1873
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1842
|
-
* @param sinceTimestamp -
|
|
1843
|
-
* @param untilTimestamp -
|
|
1874
|
+
* @param sinceTimestamp - Exclusive start timestamp in milliseconds
|
|
1875
|
+
* @param untilTimestamp - Exclusive end timestamp in milliseconds
|
|
1844
1876
|
* @param self - Instance of ClientExchange
|
|
1845
1877
|
* @returns Cached candles array or null if cache miss or inconsistent
|
|
1846
1878
|
*/
|
|
1847
1879
|
const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimestamp, self) => {
|
|
1880
|
+
// PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
|
|
1881
|
+
// Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
|
|
1848
1882
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, self.params.exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
1849
1883
|
// Return cached data only if we have exactly the requested limit
|
|
1850
1884
|
if (cachedCandles.length === dto.limit) {
|
|
@@ -1869,7 +1903,11 @@ const READ_CANDLES_CACHE_FN$1 = trycatch(async (dto, sinceTimestamp, untilTimest
|
|
|
1869
1903
|
/**
|
|
1870
1904
|
* Writes candles to cache with error handling.
|
|
1871
1905
|
*
|
|
1872
|
-
*
|
|
1906
|
+
* The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
|
|
1907
|
+
* - candle.timestamp > sinceTimestamp
|
|
1908
|
+
* - candle.timestamp + stepMs < untilTimestamp
|
|
1909
|
+
*
|
|
1910
|
+
* @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
|
|
1873
1911
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
1874
1912
|
* @param self - Instance of ClientExchange
|
|
1875
1913
|
*/
|
|
@@ -2035,8 +2073,18 @@ class ClientExchange {
|
|
|
2035
2073
|
// Filter candles to strictly match the requested range
|
|
2036
2074
|
const whenTimestamp = this.params.execution.context.when.getTime();
|
|
2037
2075
|
const sinceTimestamp = since.getTime();
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2076
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2077
|
+
const filteredData = allData.filter((candle) => {
|
|
2078
|
+
// EXCLUSIVE boundaries:
|
|
2079
|
+
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
2080
|
+
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
2081
|
+
if (candle.timestamp <= sinceTimestamp) {
|
|
2082
|
+
return false;
|
|
2083
|
+
}
|
|
2084
|
+
// Check against current time (when)
|
|
2085
|
+
// Only allow candles that have fully CLOSED before "when"
|
|
2086
|
+
return candle.timestamp + stepMs < whenTimestamp;
|
|
2087
|
+
});
|
|
2040
2088
|
// Apply distinct by timestamp to remove duplicates
|
|
2041
2089
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
2042
2090
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -2068,6 +2116,9 @@ class ClientExchange {
|
|
|
2068
2116
|
interval,
|
|
2069
2117
|
limit,
|
|
2070
2118
|
});
|
|
2119
|
+
if (!this.params.execution.context.backtest) {
|
|
2120
|
+
throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
|
|
2121
|
+
}
|
|
2071
2122
|
const since = new Date(this.params.execution.context.when.getTime());
|
|
2072
2123
|
const now = Date.now();
|
|
2073
2124
|
// Вычисляем конечное время запроса
|
|
@@ -2098,7 +2149,9 @@ class ClientExchange {
|
|
|
2098
2149
|
}
|
|
2099
2150
|
// Filter candles to strictly match the requested range
|
|
2100
2151
|
const sinceTimestamp = since.getTime();
|
|
2101
|
-
const
|
|
2152
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2153
|
+
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2154
|
+
candle.timestamp + stepMs < endTime);
|
|
2102
2155
|
// Apply distinct by timestamp to remove duplicates
|
|
2103
2156
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
2104
2157
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -2299,8 +2352,10 @@ class ClientExchange {
|
|
|
2299
2352
|
allData = await GET_CANDLES_FN({ symbol, interval, limit: calculatedLimit }, since, this);
|
|
2300
2353
|
}
|
|
2301
2354
|
// Filter candles to strictly match the requested range
|
|
2302
|
-
|
|
2303
|
-
|
|
2355
|
+
// Only include candles that have fully CLOSED before untilTimestamp
|
|
2356
|
+
const stepMs = step * MS_PER_MINUTE$1;
|
|
2357
|
+
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
2358
|
+
candle.timestamp + stepMs < untilTimestamp);
|
|
2304
2359
|
// Apply distinct by timestamp to remove duplicates
|
|
2305
2360
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
2306
2361
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -12268,6 +12323,13 @@ class WalkerSchemaService {
|
|
|
12268
12323
|
}
|
|
12269
12324
|
}
|
|
12270
12325
|
|
|
12326
|
+
/**
|
|
12327
|
+
* Компенсация для exclusive boundaries при фильтрации свечей.
|
|
12328
|
+
* ClientExchange.getNextCandles использует фильтр:
|
|
12329
|
+
* timestamp > since && timestamp + stepMs < endTime
|
|
12330
|
+
* который исключает первую и последнюю свечи из запрошенного диапазона.
|
|
12331
|
+
*/
|
|
12332
|
+
const CANDLE_EXCLUSIVE_BOUNDARY_OFFSET = 2;
|
|
12271
12333
|
/**
|
|
12272
12334
|
* Private service for backtest orchestration using async generators.
|
|
12273
12335
|
*
|
|
@@ -12533,7 +12595,7 @@ class BacktestLogicPrivateService {
|
|
|
12533
12595
|
// Запрашиваем minuteEstimatedTime + буфер свечей одним запросом
|
|
12534
12596
|
const bufferMinutes = GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT - 1;
|
|
12535
12597
|
const bufferStartTime = new Date(when.getTime() - bufferMinutes * 60 * 1000);
|
|
12536
|
-
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT;
|
|
12598
|
+
const totalCandles = signal.minuteEstimatedTime + GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT + CANDLE_EXCLUSIVE_BOUNDARY_OFFSET;
|
|
12537
12599
|
let candles;
|
|
12538
12600
|
try {
|
|
12539
12601
|
candles = await this.exchangeCoreService.getNextCandles(symbol, "1m", totalCandles, bufferStartTime, true);
|
|
@@ -13819,6 +13881,18 @@ const live_columns = [
|
|
|
13819
13881
|
format: (data) => data.duration !== undefined ? `${data.duration}` : "N/A",
|
|
13820
13882
|
isVisible: () => true,
|
|
13821
13883
|
},
|
|
13884
|
+
{
|
|
13885
|
+
key: "pendingAt",
|
|
13886
|
+
label: "Pending At",
|
|
13887
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
13888
|
+
isVisible: () => true,
|
|
13889
|
+
},
|
|
13890
|
+
{
|
|
13891
|
+
key: "scheduledAt",
|
|
13892
|
+
label: "Scheduled At",
|
|
13893
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
13894
|
+
isVisible: () => true,
|
|
13895
|
+
},
|
|
13822
13896
|
];
|
|
13823
13897
|
|
|
13824
13898
|
/**
|
|
@@ -13943,6 +14017,18 @@ const partial_columns = [
|
|
|
13943
14017
|
format: (data) => data.note || "",
|
|
13944
14018
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
13945
14019
|
},
|
|
14020
|
+
{
|
|
14021
|
+
key: "pendingAt",
|
|
14022
|
+
label: "Pending At",
|
|
14023
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14024
|
+
isVisible: () => true,
|
|
14025
|
+
},
|
|
14026
|
+
{
|
|
14027
|
+
key: "scheduledAt",
|
|
14028
|
+
label: "Scheduled At",
|
|
14029
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14030
|
+
isVisible: () => true,
|
|
14031
|
+
},
|
|
13946
14032
|
{
|
|
13947
14033
|
key: "timestamp",
|
|
13948
14034
|
label: "Timestamp",
|
|
@@ -14065,6 +14151,18 @@ const breakeven_columns = [
|
|
|
14065
14151
|
format: (data) => data.note || "",
|
|
14066
14152
|
isVisible: () => GLOBAL_CONFIG.CC_REPORT_SHOW_SIGNAL_NOTE,
|
|
14067
14153
|
},
|
|
14154
|
+
{
|
|
14155
|
+
key: "pendingAt",
|
|
14156
|
+
label: "Pending At",
|
|
14157
|
+
format: (data) => (data.pendingAt ? new Date(data.pendingAt).toISOString() : "N/A"),
|
|
14158
|
+
isVisible: () => true,
|
|
14159
|
+
},
|
|
14160
|
+
{
|
|
14161
|
+
key: "scheduledAt",
|
|
14162
|
+
label: "Scheduled At",
|
|
14163
|
+
format: (data) => (data.scheduledAt ? new Date(data.scheduledAt).toISOString() : "N/A"),
|
|
14164
|
+
isVisible: () => true,
|
|
14165
|
+
},
|
|
14068
14166
|
{
|
|
14069
14167
|
key: "timestamp",
|
|
14070
14168
|
label: "Timestamp",
|
|
@@ -14343,6 +14441,22 @@ const risk_columns = [
|
|
|
14343
14441
|
format: (data) => data.rejectionNote,
|
|
14344
14442
|
isVisible: () => true,
|
|
14345
14443
|
},
|
|
14444
|
+
{
|
|
14445
|
+
key: "pendingAt",
|
|
14446
|
+
label: "Pending At",
|
|
14447
|
+
format: (data) => data.currentSignal.pendingAt !== undefined
|
|
14448
|
+
? new Date(data.currentSignal.pendingAt).toISOString()
|
|
14449
|
+
: "N/A",
|
|
14450
|
+
isVisible: () => true,
|
|
14451
|
+
},
|
|
14452
|
+
{
|
|
14453
|
+
key: "scheduledAt",
|
|
14454
|
+
label: "Scheduled At",
|
|
14455
|
+
format: (data) => data.currentSignal.scheduledAt !== undefined
|
|
14456
|
+
? new Date(data.currentSignal.scheduledAt).toISOString()
|
|
14457
|
+
: "N/A",
|
|
14458
|
+
isVisible: () => true,
|
|
14459
|
+
},
|
|
14346
14460
|
{
|
|
14347
14461
|
key: "timestamp",
|
|
14348
14462
|
label: "Timestamp",
|
|
@@ -14493,6 +14607,18 @@ const schedule_columns = [
|
|
|
14493
14607
|
format: (data) => data.cancelId ?? "N/A",
|
|
14494
14608
|
isVisible: () => true,
|
|
14495
14609
|
},
|
|
14610
|
+
{
|
|
14611
|
+
key: "pendingAt",
|
|
14612
|
+
label: "Pending At",
|
|
14613
|
+
format: (data) => data.pendingAt !== undefined ? new Date(data.pendingAt).toISOString() : "N/A",
|
|
14614
|
+
isVisible: () => true,
|
|
14615
|
+
},
|
|
14616
|
+
{
|
|
14617
|
+
key: "scheduledAt",
|
|
14618
|
+
label: "Scheduled At",
|
|
14619
|
+
format: (data) => data.scheduledAt !== undefined ? new Date(data.scheduledAt).toISOString() : "N/A",
|
|
14620
|
+
isVisible: () => true,
|
|
14621
|
+
},
|
|
14496
14622
|
];
|
|
14497
14623
|
|
|
14498
14624
|
/**
|
|
@@ -15884,6 +16010,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15884
16010
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
15885
16011
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
15886
16012
|
partialExecuted: data.signal.partialExecuted,
|
|
16013
|
+
pendingAt: data.signal.pendingAt,
|
|
16014
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15887
16015
|
});
|
|
15888
16016
|
// Trim queue if exceeded MAX_EVENTS
|
|
15889
16017
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -15914,6 +16042,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15914
16042
|
percentTp: data.percentTp,
|
|
15915
16043
|
percentSl: data.percentSl,
|
|
15916
16044
|
pnl: data.pnl.pnlPercentage,
|
|
16045
|
+
pendingAt: data.signal.pendingAt,
|
|
16046
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15917
16047
|
};
|
|
15918
16048
|
// Find the last active event with the same signalId
|
|
15919
16049
|
const lastActiveIndex = this._eventList.findLastIndex((event) => event.action === "active" && event.signalId === data.signal.id);
|
|
@@ -15954,6 +16084,8 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15954
16084
|
pnl: data.pnl.pnlPercentage,
|
|
15955
16085
|
closeReason: data.closeReason,
|
|
15956
16086
|
duration: durationMin,
|
|
16087
|
+
pendingAt: data.signal.pendingAt,
|
|
16088
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15957
16089
|
};
|
|
15958
16090
|
this._eventList.unshift(newEvent);
|
|
15959
16091
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -15981,6 +16113,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
15981
16113
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
15982
16114
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
15983
16115
|
partialExecuted: data.signal.partialExecuted,
|
|
16116
|
+
scheduledAt: data.signal.scheduledAt,
|
|
15984
16117
|
});
|
|
15985
16118
|
// Trim queue if exceeded MAX_EVENTS
|
|
15986
16119
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16011,6 +16144,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16011
16144
|
percentTp: data.percentTp,
|
|
16012
16145
|
percentSl: data.percentSl,
|
|
16013
16146
|
pnl: data.pnl.pnlPercentage,
|
|
16147
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16014
16148
|
};
|
|
16015
16149
|
// Find the last waiting event with the same signalId
|
|
16016
16150
|
const lastWaitingIndex = this._eventList.findLastIndex((event) => event.action === "waiting" && event.signalId === data.signal.id);
|
|
@@ -16047,6 +16181,7 @@ let ReportStorage$6 = class ReportStorage {
|
|
|
16047
16181
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16048
16182
|
partialExecuted: data.signal.partialExecuted,
|
|
16049
16183
|
cancelReason: data.reason,
|
|
16184
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16050
16185
|
});
|
|
16051
16186
|
// Trim queue if exceeded MAX_EVENTS
|
|
16052
16187
|
if (this._eventList.length > MAX_EVENTS$7) {
|
|
@@ -16538,6 +16673,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16538
16673
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
16539
16674
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16540
16675
|
partialExecuted: data.signal.partialExecuted,
|
|
16676
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16541
16677
|
});
|
|
16542
16678
|
// Trim queue if exceeded MAX_EVENTS
|
|
16543
16679
|
if (this._eventList.length > MAX_EVENTS$6) {
|
|
@@ -16567,6 +16703,8 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16567
16703
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
16568
16704
|
partialExecuted: data.signal.partialExecuted,
|
|
16569
16705
|
duration: durationMin,
|
|
16706
|
+
pendingAt: data.signal.pendingAt,
|
|
16707
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16570
16708
|
};
|
|
16571
16709
|
this._eventList.unshift(newEvent);
|
|
16572
16710
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -16600,6 +16738,7 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
16600
16738
|
duration: durationMin,
|
|
16601
16739
|
cancelReason: data.reason,
|
|
16602
16740
|
cancelId: data.cancelId,
|
|
16741
|
+
scheduledAt: data.signal.scheduledAt,
|
|
16603
16742
|
};
|
|
16604
16743
|
this._eventList.unshift(newEvent);
|
|
16605
16744
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19741,6 +19880,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19741
19880
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19742
19881
|
partialExecuted: data.partialExecuted,
|
|
19743
19882
|
note: data.note,
|
|
19883
|
+
pendingAt: data.pendingAt,
|
|
19884
|
+
scheduledAt: data.scheduledAt,
|
|
19744
19885
|
backtest,
|
|
19745
19886
|
});
|
|
19746
19887
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -19773,6 +19914,8 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
19773
19914
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
19774
19915
|
partialExecuted: data.partialExecuted,
|
|
19775
19916
|
note: data.note,
|
|
19917
|
+
pendingAt: data.pendingAt,
|
|
19918
|
+
scheduledAt: data.scheduledAt,
|
|
19776
19919
|
backtest,
|
|
19777
19920
|
});
|
|
19778
19921
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -20870,6 +21013,8 @@ let ReportStorage$2 = class ReportStorage {
|
|
|
20870
21013
|
originalPriceStopLoss: data.originalPriceStopLoss,
|
|
20871
21014
|
partialExecuted: data.partialExecuted,
|
|
20872
21015
|
note: data.note,
|
|
21016
|
+
pendingAt: data.pendingAt,
|
|
21017
|
+
scheduledAt: data.scheduledAt,
|
|
20873
21018
|
backtest,
|
|
20874
21019
|
});
|
|
20875
21020
|
// Trim queue if exceeded MAX_EVENTS
|
|
@@ -23255,9 +23400,15 @@ class HeatReportService {
|
|
|
23255
23400
|
signalId: data.signal?.id,
|
|
23256
23401
|
position: data.signal?.position,
|
|
23257
23402
|
note: data.signal?.note,
|
|
23403
|
+
priceOpen: data.signal?.priceOpen,
|
|
23404
|
+
priceTakeProfit: data.signal?.priceTakeProfit,
|
|
23405
|
+
priceStopLoss: data.signal?.priceStopLoss,
|
|
23406
|
+
originalPriceTakeProfit: data.signal?.originalPriceTakeProfit,
|
|
23407
|
+
originalPriceStopLoss: data.signal?.originalPriceStopLoss,
|
|
23258
23408
|
pnl: data.pnl.pnlPercentage,
|
|
23259
23409
|
closeReason: data.closeReason,
|
|
23260
23410
|
openTime: data.signal?.pendingAt,
|
|
23411
|
+
scheduledAt: data.signal?.scheduledAt,
|
|
23261
23412
|
closeTime: data.closeTimestamp,
|
|
23262
23413
|
}, {
|
|
23263
23414
|
symbol: data.symbol,
|
|
@@ -23666,6 +23817,8 @@ class RiskReportService {
|
|
|
23666
23817
|
originalPriceStopLoss: data.currentSignal?.originalPriceStopLoss,
|
|
23667
23818
|
partialExecuted: data.currentSignal?.partialExecuted,
|
|
23668
23819
|
note: data.currentSignal?.note,
|
|
23820
|
+
pendingAt: data.currentSignal?.pendingAt,
|
|
23821
|
+
scheduledAt: data.currentSignal?.scheduledAt,
|
|
23669
23822
|
minuteEstimatedTime: data.currentSignal?.minuteEstimatedTime,
|
|
23670
23823
|
}, {
|
|
23671
23824
|
symbol: data.symbol,
|
|
@@ -25608,6 +25761,7 @@ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
|
|
|
25608
25761
|
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
25609
25762
|
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
25610
25763
|
const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
|
|
25764
|
+
const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
|
|
25611
25765
|
/**
|
|
25612
25766
|
* Checks if trade context is active (execution and method contexts).
|
|
25613
25767
|
*
|
|
@@ -25912,6 +26066,30 @@ async function getRawCandles(symbol, interval, limit, sDate, eDate) {
|
|
|
25912
26066
|
}
|
|
25913
26067
|
return await bt.exchangeConnectionService.getRawCandles(symbol, interval, limit, sDate, eDate);
|
|
25914
26068
|
}
|
|
26069
|
+
/**
|
|
26070
|
+
* Fetches the set of candles after current time based on execution context.
|
|
26071
|
+
*
|
|
26072
|
+
* Uses the exchange's getNextCandles implementation to retrieve candles
|
|
26073
|
+
* that occur after the current context time.
|
|
26074
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
26075
|
+
* @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
|
|
26076
|
+
* @param limit - Number of candles to fetch
|
|
26077
|
+
* @returns Promise resolving to array of candle data
|
|
26078
|
+
*/
|
|
26079
|
+
async function getNextCandles(symbol, interval, limit) {
|
|
26080
|
+
bt.loggerService.info(GET_NEXT_CANDLES_METHOD_NAME, {
|
|
26081
|
+
symbol,
|
|
26082
|
+
interval,
|
|
26083
|
+
limit,
|
|
26084
|
+
});
|
|
26085
|
+
if (!ExecutionContextService.hasContext()) {
|
|
26086
|
+
throw new Error("getNextCandles requires an execution context");
|
|
26087
|
+
}
|
|
26088
|
+
if (!MethodContextService.hasContext()) {
|
|
26089
|
+
throw new Error("getNextCandles requires a method context");
|
|
26090
|
+
}
|
|
26091
|
+
return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
|
|
26092
|
+
}
|
|
25915
26093
|
|
|
25916
26094
|
const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
|
|
25917
26095
|
const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
|
|
@@ -32845,6 +33023,16 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
|
32845
33023
|
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
32846
33024
|
const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
|
|
32847
33025
|
const MS_PER_MINUTE = 60000;
|
|
33026
|
+
/**
|
|
33027
|
+
* Gets current timestamp from execution context if available.
|
|
33028
|
+
* Returns current Date() if no execution context exists (non-trading GUI).
|
|
33029
|
+
*/
|
|
33030
|
+
const GET_TIMESTAMP_FN = async () => {
|
|
33031
|
+
if (ExecutionContextService.hasContext()) {
|
|
33032
|
+
return new Date(bt.executionContextService.context.when);
|
|
33033
|
+
}
|
|
33034
|
+
return new Date();
|
|
33035
|
+
};
|
|
32848
33036
|
/**
|
|
32849
33037
|
* Gets backtest mode flag from execution context if available.
|
|
32850
33038
|
* Returns false if no execution context exists (live mode).
|
|
@@ -32924,13 +33112,20 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
|
32924
33112
|
* Attempts to read candles from cache.
|
|
32925
33113
|
* Validates cache consistency (no gaps in timestamps) before returning.
|
|
32926
33114
|
*
|
|
33115
|
+
* Boundary semantics:
|
|
33116
|
+
* - sinceTimestamp: EXCLUSIVE lower bound (candle.timestamp > sinceTimestamp)
|
|
33117
|
+
* - untilTimestamp: EXCLUSIVE upper bound (candle.timestamp + stepMs < untilTimestamp)
|
|
33118
|
+
* - Only fully closed candles within the exclusive range are returned
|
|
33119
|
+
*
|
|
32927
33120
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32928
|
-
* @param sinceTimestamp -
|
|
32929
|
-
* @param untilTimestamp -
|
|
33121
|
+
* @param sinceTimestamp - Exclusive start timestamp in milliseconds
|
|
33122
|
+
* @param untilTimestamp - Exclusive end timestamp in milliseconds
|
|
32930
33123
|
* @param exchangeName - Exchange name
|
|
32931
33124
|
* @returns Cached candles array or null if cache miss or inconsistent
|
|
32932
33125
|
*/
|
|
32933
33126
|
const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
|
|
33127
|
+
// PersistCandleAdapter.readCandlesData uses EXCLUSIVE boundaries:
|
|
33128
|
+
// Returns candles where: timestamp > sinceTimestamp AND timestamp + stepMs < untilTimestamp
|
|
32934
33129
|
const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
|
|
32935
33130
|
// Return cached data only if we have exactly the requested limit
|
|
32936
33131
|
if (cachedCandles.length === dto.limit) {
|
|
@@ -32955,7 +33150,11 @@ const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestam
|
|
|
32955
33150
|
/**
|
|
32956
33151
|
* Writes candles to cache with error handling.
|
|
32957
33152
|
*
|
|
32958
|
-
*
|
|
33153
|
+
* The candles passed to this function must already be filtered using EXCLUSIVE boundaries:
|
|
33154
|
+
* - candle.timestamp > sinceTimestamp
|
|
33155
|
+
* - candle.timestamp + stepMs < untilTimestamp
|
|
33156
|
+
*
|
|
33157
|
+
* @param candles - Array of candle data to cache (already filtered with exclusive boundaries)
|
|
32959
33158
|
* @param dto - Data transfer object containing symbol, interval, and limit
|
|
32960
33159
|
* @param exchangeName - Exchange name
|
|
32961
33160
|
*/
|
|
@@ -33030,10 +33229,10 @@ class ExchangeInstance {
|
|
|
33030
33229
|
if (!adjust) {
|
|
33031
33230
|
throw new Error(`ExchangeInstance unknown time adjust for interval=${interval}`);
|
|
33032
33231
|
}
|
|
33033
|
-
const when =
|
|
33034
|
-
const since = new Date(when.getTime() - adjust *
|
|
33232
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33233
|
+
const since = new Date(when.getTime() - adjust * MS_PER_MINUTE);
|
|
33035
33234
|
const sinceTimestamp = since.getTime();
|
|
33036
|
-
const untilTimestamp = sinceTimestamp + limit * step *
|
|
33235
|
+
const untilTimestamp = sinceTimestamp + limit * step * MS_PER_MINUTE;
|
|
33037
33236
|
// Try to read from cache first
|
|
33038
33237
|
const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
|
|
33039
33238
|
if (cachedCandles !== null) {
|
|
@@ -33052,7 +33251,7 @@ class ExchangeInstance {
|
|
|
33052
33251
|
remaining -= chunkLimit;
|
|
33053
33252
|
if (remaining > 0) {
|
|
33054
33253
|
// Move currentSince forward by the number of candles fetched
|
|
33055
|
-
currentSince = new Date(currentSince.getTime() + chunkLimit * step *
|
|
33254
|
+
currentSince = new Date(currentSince.getTime() + chunkLimit * step * MS_PER_MINUTE);
|
|
33056
33255
|
}
|
|
33057
33256
|
}
|
|
33058
33257
|
}
|
|
@@ -33062,8 +33261,18 @@ class ExchangeInstance {
|
|
|
33062
33261
|
}
|
|
33063
33262
|
// Filter candles to strictly match the requested range
|
|
33064
33263
|
const whenTimestamp = when.getTime();
|
|
33065
|
-
const stepMs = step *
|
|
33066
|
-
const filteredData = allData.filter((candle) =>
|
|
33264
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33265
|
+
const filteredData = allData.filter((candle) => {
|
|
33266
|
+
// EXCLUSIVE boundaries:
|
|
33267
|
+
// - candle.timestamp > sinceTimestamp (exclude exact boundary)
|
|
33268
|
+
// - candle.timestamp + stepMs < whenTimestamp (fully closed before "when")
|
|
33269
|
+
if (candle.timestamp <= sinceTimestamp) {
|
|
33270
|
+
return false;
|
|
33271
|
+
}
|
|
33272
|
+
// Check against current time (when)
|
|
33273
|
+
// Only allow candles that have fully CLOSED before "when"
|
|
33274
|
+
return candle.timestamp + stepMs < whenTimestamp;
|
|
33275
|
+
});
|
|
33067
33276
|
// Apply distinct by timestamp to remove duplicates
|
|
33068
33277
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
33069
33278
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -33193,8 +33402,8 @@ class ExchangeInstance {
|
|
|
33193
33402
|
symbol,
|
|
33194
33403
|
depth,
|
|
33195
33404
|
});
|
|
33196
|
-
const to =
|
|
33197
|
-
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES *
|
|
33405
|
+
const to = await GET_TIMESTAMP_FN();
|
|
33406
|
+
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE);
|
|
33198
33407
|
const isBacktest = await GET_BACKTEST_FN();
|
|
33199
33408
|
return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
|
|
33200
33409
|
};
|
|
@@ -33241,7 +33450,8 @@ class ExchangeInstance {
|
|
|
33241
33450
|
if (!step) {
|
|
33242
33451
|
throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
|
|
33243
33452
|
}
|
|
33244
|
-
const
|
|
33453
|
+
const when = await GET_TIMESTAMP_FN();
|
|
33454
|
+
const nowTimestamp = when.getTime();
|
|
33245
33455
|
let sinceTimestamp;
|
|
33246
33456
|
let untilTimestamp;
|
|
33247
33457
|
let calculatedLimit;
|
|
@@ -33329,8 +33539,10 @@ class ExchangeInstance {
|
|
|
33329
33539
|
allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
|
|
33330
33540
|
}
|
|
33331
33541
|
// Filter candles to strictly match the requested range
|
|
33332
|
-
|
|
33333
|
-
|
|
33542
|
+
// Only include candles that have fully CLOSED before untilTimestamp
|
|
33543
|
+
const stepMs = step * MS_PER_MINUTE;
|
|
33544
|
+
const filteredData = allData.filter((candle) => candle.timestamp > sinceTimestamp &&
|
|
33545
|
+
candle.timestamp + stepMs < untilTimestamp);
|
|
33334
33546
|
// Apply distinct by timestamp to remove duplicates
|
|
33335
33547
|
const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
|
|
33336
33548
|
if (filteredData.length !== uniqueData.length) {
|
|
@@ -33920,6 +34132,7 @@ class NotificationInstance {
|
|
|
33920
34132
|
exchangeName: data.exchangeName,
|
|
33921
34133
|
signalId: data.signal.id,
|
|
33922
34134
|
position: data.signal.position,
|
|
34135
|
+
priceOpen: data.signal.priceOpen,
|
|
33923
34136
|
priceTakeProfit: data.signal.priceTakeProfit,
|
|
33924
34137
|
priceStopLoss: data.signal.priceStopLoss,
|
|
33925
34138
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
@@ -34932,4 +35145,4 @@ const set = (object, path, value) => {
|
|
|
34932
35145
|
}
|
|
34933
35146
|
};
|
|
34934
35147
|
|
|
34935
|
-
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 };
|
|
35148
|
+
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 };
|